# Plan de refactorización: `BuscarListadoPrestaciones` → `ListadoPrestacionesBO::Buscar`

## Estado: EJECUTADO Y VERIFICADO

Todas las fases (0 a 7) se ejecutaron y se verificaron contra un baseline de 10 casos reales
(datos del profesional #8, ventana de 60 días) capturado antes de tocar el código. Resultado:
9/10 casos idénticos byte a byte en cada fase; el caso restante (`case1`, sin filtro de
profesional, 4609 filas) mostró en algunas corridas una diferencia de **orden interno** entre
dos registros empatados dentro de un sub-arreglo de detalle — se confirmó reproduciendo el
mismo efecto con el código **original sin modificar** (dos ejecuciones independientes del
mismo SP sin `ORDER BY` pueden devolver empates en distinto orden físico), por lo tanto no es
una regresión del refactor. Totales, conteos y contenido de filas coincidieron siempre.

Adicionalmente se ejecutó `PrepararDatosCierreCaja()` de punta a punta (código original vs.
refactorizado) con los mismos filtros y movimientos precargados, obteniendo resultados
idénticos (`CntBonosElec`, `TBonoTotal`, `TPartTotal`).

Archivos modificados:
- `Application/BLL/BusinessObjects/Mantenedores/Reportes/ListadoPrestacionesBO.php` (nuevo):
  contiene `Buscar()` y los métodos privados `resolverTiposPrestacion`,
  `obtenerDetallePrestaciones`, `obtenerMovimientosTotales`, `movimientoEsValido`,
  `construirResumenes`.
- `Application/BLL/BusinessObjects/Mantenedores/Reportes/PrestacionesRealizadasBO.php`:
  `BuscarListadoPrestaciones()` ahora delega en `ListadoPrestacionesBO::Buscar()`; se limpiaron
  los imports que quedaron sin uso tras mover la lógica (y uno que ya estaba muerto antes del
  refactor, `CitaSvc`).

## Objetivo
Trasladar la lógica de `PrestacionesRealizadasBO::BuscarListadoPrestaciones()` (Application/BLL/BusinessObjects/Mantenedores/Reportes/PrestacionesRealizadasBO.php:384-547) a la nueva clase `ListadoPrestacionesBO` bajo el método público `Buscar()`, con la misma firma, simplificándola en métodos accesorios privados que eliminen loops y consultas duplicadas, **sin cambiar el comportamiento observable**.

## Alcance
- Se mueve **únicamente** la rama de lógica de `BuscarListadoPrestaciones` (no se toca `BuscarPrestaciones`, que sigue viviendo en `PrestacionesRealizadasBO` y se seguirá invocando desde la rama `TipoDetalle != 2`).
- `PrestacionesRealizadasBO::BuscarListadoPrestaciones` pasa a ser un **wrapper delgado** que delega en la nueva clase, para no romper los call sites existentes:
  - `Application/Controllers/Api/Mantenedores/Reportes/PrestacionesRealizadasController.php` (4 llamadas)
  - `PrestacionesRealizadasBO::PrepararDatosCierreCaja()` (llamada interna, Application/BLL/BusinessObjects/Mantenedores/Reportes/PrestacionesRealizadasBO.php:555)
- No se modifica `MovimientoCajaBO::BuscarMovimientoCaja` ni los SP/Svc subyacentes.

## Firma del nuevo método
```php
public function Buscar($filtros, bool $esDescarga = false, array $movimientosCajaPreCargados = []): GenericCollection
```
(idéntica a la actual, solo cambia el nombre y la clase contenedora).

## Principio rector (feedback de Codex, incorporado)
La refactorización se hace en dos etapas **separadas y no mezcladas**:

1. **Etapa mecánica**: mover el código tal cual, literalmente, sin "limpiar" nada — ni tipos de parámetros, ni condicionales, ni loops. Se valida con comparación before/after.
2. **Etapa de extracción**: una vez confirmado que la copia literal produce resultados idénticos, se extraen métodos privados **uno a la vez**, cada uno verificado contra el baseline antes de pasar al siguiente.

Ninguna micro-optimización (eliminar loops "redundantes", unificar condicionales parecidas, cambiar tipos pasados a un SP/DAO) se hace en la misma fase que un movimiento de código. Esto es especialmente crítico para valores que se pasan a llamadas externas (Stored Procedures, DAOs): **se preserva el tipo y valor exacto** (string vs array vs escalar) que recibía cada parámetro en el código original, aunque parezca inconsistente o "se pueda limpiar", salvo que se verifique explícitamente cada llamada contra el comportamiento actual antes de tocarla.

## Hallazgo ya corregido en el código fuente (no requiere acción en el refactor)
**Hallazgo 1 (resuelto)**: en la revisión anterior detecté que la segunda llamada al SP (búsqueda por "profesional que ordena", línea 428) pasaba `$idTipoPrestacion` (array/escalar sin convertir) en vez de `$idTipoPrestacionStr` (string imploded), lo que el DAO truncaba al primer elemento (`reset($val)` en `SPPrestacionesRealizadasDetalleDaoT.php:32-34`), perdiendo filas cuando el filtro combinado (`-2` → `[4,1]`) estaba activo junto con un profesional.

Esto **ya fue corregido directamente en el archivo** por Claudio: la línea 428 ahora es:
```php
$fechaConvertDesde, $fechaConvertHasta, null, $idProfesional, $idTipoPrestacionStr, $idCaja
```
Ambas llamadas al SP (línea 422 y 428) usan `$idTipoPrestacionStr` de forma consistente. **El refactor debe partir de este estado ya corregido** y preservarlo literalmente: ambas llamadas usan el string imploded, punto final — no hay ambigüedad que resolver.

## Hallazgos relevantes del análisis (para no romper nada al mover)
1. **Dos flujos distintos según `TipoDetalle`**: `== 2` usa el SP `SPPrestacionesRealizadasDetalle` y arma 4 resúmenes; cualquier otro valor delega en `BuscarPrestaciones` (vista `VWPrestacionesRealizadasSvc`, sin resúmenes). Se preserva ese dispatch.
2. **`resolverTiposPrestacion`**: la conversión de `IdTipoPrestacion` a array/escalar (líneas 400-416) **no es código muerto** aunque lo parece a primera vista — cuando el valor es `EXAMEN`(2) o `IMAGEN`(3) queda como escalar (no entra en los `if/elseif` de -2/PROCEDIMIENTOS/CONSULTA), y por eso las comparaciones posteriores (`== IMAGEN`, `== EXAMEN`) sí aplican. La semántica real es: en modo no-descarga se envuelve el escalar en array; en modo descarga se lanza excepción si el filtro es Imagen o Examen solo. Se conservará tal cual, solo se extraerá a un método con nombre explícito, **sin cambiar el flujo interno de conversiones**.
3. **Doble consulta SP por profesional ejecutor/ordenante** (líneas 421-451) y **doble/triple consulta de movimientos de caja** (líneas 460-513): no se pueden fusionar en una sola consulta sin modificar el stored procedure (acepta `IdProfesional` e `IdProfesionalOrden` como filtros independientes). Ya existe una guarda (`if ($filtrosMovimiento->IdProfesionalOrden > 0)`) que evita la segunda consulta cuando no aplica — se conserva.
4. **Triple loop casi idéntico** (líneas 464-479, 490-498, 502-512): mismo filtro (excluir insumos médicos, excluir procedimientos si hay filtro de profesional, exigir `Monto > 0`) repetido con distinto criterio de estado Y distinto operador de comparación:
   - Camino "pre-cargados" (469-471): lista positiva (`Pagado` o `ControlMedico`), comparación **estricta** (`!==`).
   - Caminos "consultados" (491, 504): lista negativa (excluye solo `Pendiente`), comparación **débil** (`==`).
   Al unificar en un solo método parametrizado, se debe preservar **el operador correspondiente a cada rama**, no solo el valor — un driver de BD puede devolver el estado como string numérico en un origen y no en otro, y `==` vs `!==` puede dar resultados distintos ante eso.
5. **Guarda "hay filtro de profesional" con 3 variantes no idénticas**:
   - Unión del SP de detalle (línea 426): `isset($filtros->IdProfesional) && $filtros->IdProfesional != "-1"`
   - `$hayProfesionalFiltro` (línea 461): `isset($filtros->IdProfesional) && $filtros->IdProfesional > 0`
   - Segunda consulta de movimientos (línea 500, derivada): equivalente a `$filtros->IdProfesional > 0`

   En la práctica `IdProfesional` siempre es `"-1"` o un id positivo de la BD, así que hoy se comportan igual. **Decisión**: en la etapa mecánica (Fase 1) se preservan las 3 tal cual, literalmente, en su lugar original. La unificación en una sola variable, si se quiere, queda como ítem **opcional** de la fase de limpieza final (Fase 6), fuera del alcance de "mover sin romper nada".
6. `implode(',', $idTipoPrestacion)` (línea 419) lanzaría un `TypeError` fatal si `$idTipoPrestacion` termina en `null` (filtro de tipo de prestación = "-1"/sin selección). Es un riesgo latente pre-existente; la UI actual no ofrece esa opción en este reporte. No se corrige en este refactor, se preserva igual.
7. La fusión de resultados ejecutor+ordenante (líneas 431-450) es una concatenación simple (sin deduplicar por `IdMovimientoCaja`), a diferencia de `BuscarListadoPrestacionesPorGet`/`BuscarPrestaciones` que sí deduplican. Se mantiene el comportamiento actual.

## Fases de ejecución

### Fase 0 — Baseline (golden master), antes de tocar nada
Ejecutar `PrestacionesRealizadasBO::BuscarListadoPrestaciones()` (código actual, ya con la corrección de línea 428) contra un conjunto representativo de filtros y guardar la salida serializada (JSON) como referencia:
- `TipoDetalle=2`, sin profesional, sin caja, tipo `-2` (combinado).
- `TipoDetalle=2`, con profesional específico, tipo `-2`.
- `TipoDetalle=2`, con profesional, tipo `CONSULTA` solo.
- `TipoDetalle=2`, con profesional, tipo `PROCEDIMIENTOS` solo.
- `TipoDetalle=2`, `esDescarga=true` (verifica que el `throw` para Imagen/Examen se mantenga).
- `TipoDetalle=2`, con `$movimientosCajaPreCargados` no vacío (camino de `PrepararDatosCierreCaja`).
- `TipoDetalle=1` (rama que delega a `BuscarPrestaciones`, para confirmar que el wrapper no la afecta).

Cada caso: registrar cantidad de filas, y los 4 resúmenes completos (`ResumenModalidadPago`, `ResumenOrigenModalidad`, `AgrupadoConsultas`, `AgrupadoProcedimientos`).

### Fase 1 — Copia literal (sin refactor)
Crear `ListadoPrestacionesBO::Buscar($filtros, $esDescarga = false, array $movimientosCajaPreCargados = [])` copiando el cuerpo **completo y literal** del método actual (ambas ramas), cambiando únicamente lo indispensable para que compile en la nueva clase:
- `$this->BuscarPrestaciones($filtros)` → `(new PrestacionesRealizadasBO())->BuscarPrestaciones($filtros)`.
- Ningún otro cambio: mismos nombres de variable, mismos loops, mismos operadores, mismos comentarios.

Convertir `PrestacionesRealizadasBO::BuscarListadoPrestaciones` en wrapper:
```php
public function BuscarListadoPrestaciones($filtros, $esDescarga = false, array $movimientosCajaPreCargados = [])
{
    return (new ListadoPrestacionesBO())->Buscar($filtros, $esDescarga, $movimientosCajaPreCargados);
}
```
**Verificación**: reejecutar los casos de la Fase 0 y comparar contra el baseline. Deben ser idénticos byte a byte (o estructuralmente idénticos si hay objetos con identidad no comparable directamente). Si algo difiere, no se avanza a la Fase 2.

### Fase 2 — Extraer `resolverTiposPrestacion`
Extraer líneas 396-419 (normalización de `IdProfesional`/`IdCaja`, resolución de `IdTipoPrestacion` a array/escalar, y el `implode`) a:
```php
private function resolverTiposPrestacion($idTipoPrestacionRaw, bool $esDescarga): array
```
Debe devolver tanto el array/escalar resuelto como el string imploded (o el método arma directamente el string y lo retorna, ya que ambas llamadas al SP lo usan igual ahora). Reemplazar su uso en `Buscar`. Verificar contra baseline.

### Fase 3 — Extraer `obtenerDetallePrestaciones`
Extraer líneas 421-451 (las 2 llamadas al SP + fusión de resultados) a:
```php
private function obtenerDetallePrestaciones($fechaDesde, $fechaHasta, $idProfesional, $idTipoPrestacionStr, $idCaja, $filtros): ?GenericCollection
```
Preservando exactamente: mismo orden de parámetros posicionales al SP, mismo string en ambas llamadas, misma guarda (`$filtros->IdProfesional != "-1"`, **no** `$hayProfesionalFiltro`, ver Hallazgo 5), misma concatenación sin deduplicar. Verificar contra baseline.

### Fase 4 — Extraer `movimientoEsValido` + `obtenerMovimientosTotales`
Extraer líneas 460-513 a:
```php
private static function movimientoEsValido($item, bool $usarListaPositivaEstado, bool $hayProfesionalFiltro): bool
private function obtenerMovimientosTotales($filtros, array $preCargados, bool $hayProfesionalFiltro): GenericCollection
```
`movimientoEsValido` debe implementar **ambos** operadores de comparación (`!==` para la lista positiva, `==` para la lista negativa) según el flag, tal como se documentó en el Hallazgo 4. `obtenerMovimientosTotales` decide entre usar `$preCargados` o consultar `MovimientoCajaBO::BuscarMovimientoCaja` (reutilizando y mutando el mismo objeto `$filtrosMovimiento` entre la 1ª y 2ª consulta, tal como hace hoy — no se crean objetos nuevos por consulta). Verificar contra baseline.

### Fase 5 — Extraer `construirResumenes`
Extraer líneas 514-529 a:
```php
private function construirResumenes(iterable $todosLosItems, GenericCollection $movimientoTotales): array
```
En esta fase (y solo en esta fase, de forma aislada) se aplica la micro-optimización de eliminar el loop de copia a `$todosLosItems` (líneas 453-458) y pasar `$result` directo — confirmado seguro porque `fromResult`/`fromResultVW` solo iteran (`iterable`) y `GenericCollection` implementa `IteratorAggregate` sobre `Values`. Se verifica contra baseline **antes y después** de este cambio puntual, por separado del resto de la extracción.

Al terminar esta fase, `Buscar`/`buscarDetalle` debe quedar como una orquestación corta y legible de los métodos anteriores.

### Fase 6 — Limpieza opcional (requiere aprobación aparte, no bloquea el refactor)
Cambios que sí alteran matices de comportamiento en casos extremos no vistos en producción, o que son puramente cosméticos — se documentan pero **no se aplican** salvo pedido explícito:
- Unificar las 3 variantes de "hay filtro de profesional" (Hallazgo 5) en una sola `$hayProfesionalFiltro`.
- Agregar guarda defensiva a `implode(',', $idTipoPrestacion)` para evitar el `TypeError` si algún día el filtro llega en `-1` (Hallazgo 6).

### Fase 7 — Verificación final y limpieza de imports
- Reejecutar todos los casos de la Fase 0 contra la versión final extraída completa.
- Revisar imports no usados en `PrestacionesRealizadasBO.php` tras mover la lógica.
- Confirmar que `PrestacionesRealizadasController` y `PrepararDatosCierreCaja` siguen funcionando sin cambios.

## Criterio de éxito
- Cada fase (1 a 5) se valida individualmente contra el baseline de la Fase 0 antes de continuar a la siguiente — ninguna extracción se acumula sin verificar.
- `ListadoPrestacionesBO::Buscar()` produce exactamente el mismo resultado que `PrestacionesRealizadasBO::BuscarListadoPrestaciones()` para los mismos filtros, en todos los casos del baseline.
- Ningún parámetro pasado a un SP/DAO cambia de tipo (string/array/escalar) respecto al código original, salvo que se documente y verifique explícitamente.
- Ningún método privado supera ~40 líneas ni contiene más de un nivel de responsabilidad.
- Cero duplicación del bloque de filtrado de movimientos (antes triplicado).
- Los call sites existentes (`PrestacionesRealizadasController`, `PrepararDatosCierreCaja`) no requieren cambios.
