# Auditoría Integral — Credify Go!
**Fecha:** 2026-04-14 | **Rama:** `dev` | **Auditores:** 4 agentes especializados en paralelo

---

## Resumen Ejecutivo

Se auditaron **4 dimensiones** del sistema: Backend/Filament, PWA/Frontend, Seguridad, y Arquitectura/Tests/DB. En total se identificaron **52 hallazgos** distribuidos en 4 niveles de severidad.

| Severidad | Cantidad | Acción |
|-----------|----------|--------|
| 🔴 CRÍTICO | 10 | Corregir esta semana |
| 🟠 ALTO | 18 | Corregir en próximo sprint |
| 🟡 MEDIO | 16 | Backlog priorizado |
| 🟢 BAJO | 8 | Deuda técnica controlada |

**Puntuación general: 6.8 / 10**
El sistema es funcionalmente sólido pero tiene brechas de seguridad reales, módulos incompletos, y gaps de testing que deben cerrarse antes de escalar.

---

## PARTE I — SEGURIDAD

### 🔴 CRÍTICO

#### S-01 · APP_DEBUG=true activo
**Archivo:** `.env` línea 4
Laravel con `APP_DEBUG=true` expone stack traces completos, rutas absolutas del servidor y variables de entorno en cualquier error 500. En producción esto es inaceptable.
```
APP_DEBUG=false   # cambiar inmediatamente
APP_ENV=production
```

#### S-02 · Credenciales débiles en `.env`
**Archivo:** `.env` líneas 27-28
`DB_PASSWORD=credifypass` es una contraseña trivial. Si el `.env` se filtra (backup, log, snippet), la base de datos queda completamente expuesta.
```
DB_PASSWORD=<contraseña aleatoria ≥32 chars>
```
Adicionalmente, la Google Maps API Key (`AIzaSyDS71m...`) está en texto plano sin restricciones de dominio ni IP en la consola de GCP.

#### S-03 · IDOR en `GET /api/pwa/payments/{id}/receipt`
**Archivo:** `app/Http/Controllers/Api/Pwa/PaymentController.php` ~línea 332
El método `receipt()` solo valida `company_id`, permitiendo que un collector vea el recibo de pagos registrados por otros collectors de la misma empresa. El método `show()` del mismo controlador SÍ valida `registered_by_user_id`, creando una inconsistencia explotable.
```php
// receipt() actual — INSEGURO
Payment::where('company_id', $user->company_id)->find($id);

// Corrección: aplicar mismo filtro rol-aware que show()
->where(function($q) use ($user, $role) {
    if ($role === 'collector') {
        $q->where('registered_by_user_id', $user->id);
    }
})
```

#### S-04 · Tokens Sanctum sin expiración
**Archivo:** `config/sanctum.php` línea 59
`'expiration' => null` significa que los tokens Bearer emitidos en login son válidos indefinidamente. Si un token se extrae, el atacante tiene acceso permanente.
```php
'expiration' => 1440,  // 24 horas
```

#### S-05 · Sin rate limiting en endpoints de escritura críticos
**Archivo:** `routes/api.php`
Solo el endpoint de login tiene `throttle:pwa-login`. Los siguientes endpoints de escritura no tienen ningún rate limit:
- `POST /api/pwa/payments`
- `POST /api/pwa/sync/payments`
- `POST /api/pwa/sync/expenses`
- `POST /api/pwa/credits`
- `POST /api/pwa/clients`
- `POST /api/pwa/team/supervisors/{id}/collectors`

Un usuario autenticado puede hacer spam o inundar la base de datos sin restricción.

---

### 🟠 ALTO

#### S-06 · `CreditPolicy` definida pero no usada en controllers PWA
**Archivo:** `app/Policies/CreditPolicy.php` (existe) vs. `app/Http/Controllers/Api/Pwa/CreditController.php`
Los controllers PWA implementan validaciones de acceso inline en cada método en lugar de delegar a la Policy. Esto multiplica los puntos de falla: si la lógica cambia hay que actualizarla en N lugares.

#### S-07 · IDOR en `POST /api/pwa/credits/reorder`
**Archivo:** `app/Http/Controllers/Api/Pwa/CreditController.php` ~línea 474
`ReorderCollectorCreditsRequest` no valida que los `credit_ids` enviados pertenezcan al usuario autenticado. Un collector que conozca IDs ajenos podría intentar reordenar la ruta de otro.

#### S-08 · Multi-tenancy frágil en servicios manuales
**Archivo:** `app/Traits/MultiTenantScope.php`
El `GlobalScope` protege queries Eloquent, pero varios servicios construyen queries con `DB::raw()` o `selectRaw()` donde el filtro `company_id` debe añadirse manualmente. Si un desarrollador olvida ese filtro, hay cross-tenant data leakage. Ejemplo detectado: `TeamController`, `AdminDashboardMetricsService`.

#### S-09 · Device ID persistido en localStorage
**Archivo:** `resources/js/pwa/stores/auth.js` línea 14
```javascript
const deviceId = ref(localStorage.getItem('pwa_device_id') || generateDeviceId())
```
Un identificador único persistente en localStorage es accesible vía XSS y puede usarse para tracking. Debería estar en `sessionStorage` o regenerarse por sesión.

#### S-10 · Rate limiting de login por-IP+usuario permite enumeración
**Archivo:** `app/Providers/AppServiceProvider.php` línea 49
El rate limiter combina `IP + identifier`. Un atacante con acceso a múltiples IPs puede probar el mismo usuario desde diferentes redes sin ser bloqueado. Falta un rate limit adicional global por IP.

---

### 🟡 MEDIO

#### S-11 · Sobre-fetching de datos de empresa en recibos
`PaymentController::receipt()` devuelve teléfono, email y dirección de la empresa en cada recibo. No es necesario para la funcionalidad y expone datos de configuración interna.

#### S-12 · Logs capturan montos de transacciones
`PaymentController.php` ~línea 165 escribe `amount` e `idempotency_key` en los logs. Si los logs son accedidos por terceros, exponen información financiera. Usar hashes en lugar de valores reales.

#### S-13 · Sincronización offline sin validación de integridad
`stores/auth.js` carga datos de la API en IndexedDB sin checksum ni firma. Un ataque MITM (en desarrollo sin HTTPS) podría inyectar créditos o pagos falsos en la caché local.

---

## PARTE II — BACKEND / FILAMENT

### 🔴 CRÍTICO

#### B-01 · `getFrequencyDisplay()` genera labels incorrectos
**Archivo:** `app/Http/Controllers/Api/Pwa/Traits/RoleAwareQueries.php` ~línea 193
```php
$days = ['', 'Lunes', 'Martes', ...]; // índices 1-7
return match ($credit->periodicity) {
    'weekly' => 'Cada ' . ($days[$credit->due_day_1] ?? 'semana'),
    // due_day_1 es 1-31 (día del mes), NO índice de día de semana
};
```
Cuando `due_day_1=15` (crédito que vence el día 15 de cada mes), el código busca `$days[15]` → out of bounds → fallback `'semana'`. Todos los créditos semanales muestran "Cada semana" en lugar del día correcto. Este label afecta CreditController, formatCreditForList() y la vista de colección del cobrador.

#### B-02 · Supervisores no pueden ver pagos de sus cobradores
**Archivo:** `app/Http/Controllers/Api/Pwa/PaymentController.php` ~línea 292
```php
->where('registered_by_user_id', $user->id)  // solo el usuario actual
```
Un supervisor que intenta auditar un pago recibe 404. El patrón rol-aware que existe en `CreditController::index()` no fue aplicado aquí. Tampoco en `CollectionVisitController::today()`.

#### B-03 · Pago procesado sin verificar que hay cuotas pendientes
**Archivo:** `app/Http/Controllers/Api/Pwa/PaymentController.php` ~línea 105
Si todas las cuotas están en estado `paid` pero `remaining_balance > 0` (por un sobrepago anterior no consumido), el controlador deja que el `PaymentManager` intente distribuir el nuevo pago sin un `nextInstallment` destino. El resultado es un pago "flotante" sin asignación de cuota: inconsistencia contable crítica.

#### B-04 · Metas de cobrador nunca implementadas (TODO hardcodeado)
**Archivo:** `app/Services/Metrics/CollectionMetricsService.php` línea 136
```php
$dailyGoal = null; // TODO: Implementar metas por collector
```
El método `getCollectorPerformanceToday()` devuelve `status: 'no_goal'` para cada cobrador. La funcionalidad de metas fue diseñada pero nunca construida. La UI del supervisor ve métricas vacías permanentemente.

---

### 🟠 ALTO

#### B-05 · `CollectionVisitController::today()` sin soporte para supervisores
**Archivo:** `app/Http/Controllers/Api/Pwa/CollectionVisitController.php` ~línea 137
Sin parámetro `?collector_id` para supervisores. Contrasta con `PaymentController::today()` que sí lo soporta. Incoherenica de API que impide auditoría de visitas.

#### B-06 · Límite de descarga de admin demasiado permisivo
**Archivo:** `app/Http/Controllers/Api/Pwa/SyncController.php` ~línea 31
`const ADMIN_SYNC_LIMIT = 500` créditos en una sola llamada. Sin paginación. Un admin con cartera grande puede saturar la DB. Reducir a 100-150 o implementar cursor pagination.

#### B-07 · Flujo de aprobación de gastos permite re-aprobación
**Archivo:** `app/Http/Controllers/Api/Pwa/ApprovalController.php` ~línea 88
`$expense->approve()` se llama sin verificar si el gasto ya fue aprobado previamente. Un gasto aprobado puede aprobarse nuevamente si el model no valida el estado previo.

#### B-08 · `CollectorSyncListener` no maneja excepciones
**Archivo:** `app/Listeners/CollectorSyncListener.php` ~línea 26
Si `CollectorSyncService::handleStatusChange()` lanza una excepción, el listener falla sin try/catch ni logging. El orden de ruta del cobrador puede quedar desincronizado silenciosamente.

#### B-09 · `DashboardController::collectors()` y `TeamController::index()` duplican lógica
Ambos devuelven listas de cobradores con estructuras diferentes sin documentar el propósito de cada endpoint. Confusión de API; el frontend podría usar el equivocado.

#### B-10 · `PartnerController::storeTransaction()` no valida saldo disponible
**Archivo:** `app/Http/Controllers/Api/Pwa/PartnerController.php` ~línea 99
Un retiro de socio puede crearse por un monto mayor al capital disponible. El balance del socio quedaría en negativo: inconsistencia contable.

---

### 🟡 MEDIO

#### B-11 · `SyncController::checkUpdates()` solo opera como collector
Un supervisor que llama a `checkUpdates()` verifica únicamente sus propios créditos (ninguno asignado directamente), nunca los de su equipo. No recibe notificaciones de cambios en el equipo.

#### B-12 · Regex de validación de color incompleto
`'primary_color' => ['regex:/^#[0-9a-fA-F]{3,6}$/']` acepta longitudes 3-6 mezcladas (ej. #FFFF tiene 4 hex pero pasa). Debería ser `{3}|{6}` con alternancia explícita.

#### B-13 · `SyncCreditStatusJob` con `releaseAfter(30)` muy agresivo
30 segundos de backoff en el `WithoutOverlapping` es insuficiente bajo carga. En picos, los jobs compiten y se reintentan en bucle. Aumentar a 90-120 segundos.

#### B-14 · Accessors `principal_pending` / `interest_pending` en Installment — verificar existencia
`CreditController::show()` ~línea 174 usa `$credit->installments->sum('principal_pending')`. Si el accessor no existe en el modelo `Installment`, todas las vistas de detalle de crédito retornarán 0 silenciosamente.

---

## PARTE III — PWA / FRONTEND

### 🔴 CRÍTICO

#### P-01 · Service Worker abre IndexedDB con versión incorrecta
**Archivo:** `resources/js/pwa/pwa-sw.js` ~línea 344
```javascript
function openDatabase() {
    const request = indexedDB.open('CredifyGoPWA', 1)  // versión 1
}
```
La aplicación principal usa Dexie versión 4 con schema actualizado. El SW abre la DB con versión 1 y no conoce el campo `permanent_error`. Resultado: **pagos marcados como `permanent_error=true` se reintentan en background sync en bucle infinito**, causando drain de batería y duplicación de requests.

#### P-02 · Race condition en `queuePayment()`
**Archivo:** `resources/js/pwa/stores/sync.js` ~línea 46
```javascript
await db.pendingPayments.add(payment)        // puede fallar
await db.deductCreditBalance(...)             // se ejecuta siempre
```
Si `add()` falla (cuota de almacenamiento llena, permisos denegados), el balance del crédito se deduce localmente sin que el pago haya sido encolado. El usuario ve un balance deducido pero el pago nunca llega al servidor.

---

### 🟠 ALTO

#### P-03 · Catch blocks vacíos en operaciones críticas
`stores/approvals.js` ~línea 37: `approve()` y `reject()` lanzan excepciones que `ApprovalsView.vue` no captura. Una aprobación fallida no se comunica al usuario.
`stores/settings.js` ~línea 94: `.catch(() => {})` completamente vacío.
`stores/sync.js` ~línea 344: silencia fallos de IndexedDB.

#### P-04 · Riesgo de pago por monto incorrecto con `SimplifiedAmount`
**Archivo:** `resources/js/pwa/views/PaymentView.vue` ~línea 238
Si el admin cambia el multiplicador (`×1000`) mientras el cobrador tiene la vista de pago abierta, la conversión de amount simplificado a real se recalcula con el nuevo multiplicador. Un pago de COP 150 (= 150.000) podría convertirse al doble si el multiplicador cambia. No hay detección de cambio de multiplicador en vuelo.

#### P-05 · Delinquency / Financials sin fallback offline
`stores/delinquency.js` y `stores/financials.js`: cuando `!navigator.onLine`, muestran error genérico en lugar de datos cacheados de la sesión anterior. Los supervisores pierden acceso a métricas históricas offline.

#### P-06 · Collections store con complejidad O(n²)
**Archivo:** `resources/js/pwa/stores/collections.js` ~línea 108
Para cada cuota del día, se re-cargan TODOS los installments del crédito desde IndexedDB para calcular el saldo. Con 500 cuotas activas = potencialmente miles de reads a IndexedDB.

#### P-07 · `visits.js` store duplica lógica de `sync.js`
El store de visitas repite la estructura de sincronización de `pendingPayments` en lugar de abstraer una cola genérica. Cambios en la lógica de sync deben duplicarse en dos lugares.

---

### 🟡 MEDIO

#### P-08 · `permanent_error` no indexado en Dexie
**Archivo:** `resources/js/pwa/db/index.js`
El campo `permanent_error` en `pendingPayments` no está indexado en Dexie. El filtro `payments.filter(p => !p.permanent_error)` carga toda la tabla y filtra en memoria. Con miles de pagos pendientes históricos, esto es ineficiente.

#### P-09 · Tabla `payments` en IndexedDB sincronizada pero no usada offline
`PaymentsHistoryView.vue` hace `api.get('/pwa/payments/today')` en línea incluso cuando los datos ya están en IndexedDB. El fallback offline no existe; la vista queda vacía sin conexión.

#### P-10 · Guard de router sin toast de acceso denegado
`router/index.js` ~línea 242: cuando un collector intenta acceder a `/pwa/delinquency` (solo supervisor/admin), es redirigido a home sin ningún mensaje. La UX es confusa — parece un bug, no una restricción de rol.

#### P-11 · Debug `console.log()` statements en producción
Presentes en: `stores/auth.js` (línea 180-185), `stores/sync.js` (líneas 225, 335, 451), `stores/settings.js` (línea 98), vistas de collections. Exponen datos de negocio en la consola del navegador.

#### P-12 · Props sin validación de estructura en componentes
`CreditCard.vue`, `WeeklyProgressChart.vue`, `OverdueAgingPanel.vue`, `PaymentSummary.vue`: las props de tipo `Object` no declaran un validador que verifique las propiedades requeridas. Un cambio en el schema del backend causa fallos silenciosos en runtime.

#### P-13 · Listeners `online`/`offline` sin cleanup
`stores/auth.js` ~línea 20: los event listeners sobre `window` no se remueven en el ciclo de vida del store. Aunque es difícil que ocurra en la práctica, puede causar memory leaks en tests o recargas hot.

---

## PARTE IV — ARQUITECTURA / TESTS / DB

### 🔴 CRÍTICO

#### A-01 · N+1 query en `CollectionMetricsService::getCollectorPerformanceToday()`
**Archivo:** `app/Services/Metrics/CollectionMetricsService.php` ~línea 115
```php
$collectors = User::get();           // 1 query
foreach ($collectors as $collector) {
    $payments = Payment::where(...)  // N queries (una por cobrador)
        ->whereHas('credit', ...)
        ->get();
}
```
Con 100 cobradores activos = 101 queries por llamada al dashboard de supervisor. Bajo carga concurrente puede causar timeouts. Solución: un solo `Payment::groupBy('collector_user_id')`.

#### A-02 · Sin tests para sincronización offline de pagos
`SyncController::syncPayments()` tiene 142 líneas de lógica crítica (idempotencia, distribución, validación de acceso) con **cero feature tests**. Es el endpoint más importante de la PWA y el más riesgoso de romper silenciosamente.

#### A-03 · ForeignKeys sin `onDelete` en migraciones históricas
**Archivos:**
- `2026_02_04_100003_create_subscription_requests_table.php`: `company_id`, `subscription_id`, `requested_plan_id` sin `onDelete`.
- `2026_02_03_091703_create_company_financial_settings_table.php`: `created_by_user_id`, `updated_by_user_id` sin `onDelete`.

Si se eliminan registros padre (companies, subscriptions), quedan orphaned records que rompen queries con `whereHas`.

---

### 🟠 ALTO

#### A-04 · Índices faltantes en tablas recientes
La migración de performance `2026_01_19_160902_add_performance_indexes.php` no cubre las tablas creadas posteriormente:
- `supervisor_collector`: sin índice en `(company_id, supervisor_id)` — afecta queries de filtrado multi-tenant.
- `expenses`: el índice `unique(['company_id', 'idempotency_key'])` es único pero no hay índice separado en `company_id` para queries de listado.
- `collection_visits`: sin índice en `(collector_user_id, visit_date)` — afecta estadísticas diarias.

#### A-05 · `FinanceAutoLogger` viola SRP
**Archivo:** `app/Services/FinanceAutoLogger.php` (198 LOC, 7 métodos públicos)
Mezcla: crear ingresos, crear egresos, validar integridad, resolver tipos, hacer queries de consulta, actualizar con bypass, eliminar con bypass. Debería dividirse en `IncomeLogger` + `ExpenseLogger`.

#### A-06 · `CollectorVisibilityResolver` no se usa en todos los controllers
El servicio existe y es correcto, pero `SyncController::downloadData()`, `CollectionController::index()` y `DashboardController` replican la lógica `if ($role === 'collector')` inline. Si la lógica de visibilidad cambia, hay que actualizarla en múltiples lugares.

#### A-07 · Metas diarias de cobrador — feature incompleto en DB
No existe tabla `company_collector_goals` ni ninguna estructura para metas. El `TODO` en `CollectionMetricsService.php:136` bloquea una funcionalidad visible para supervisores.

#### A-08 · Tests Edge cases faltantes en módulo financiero
Los tests de `Financial/` son exhaustivos para happy paths pero faltan:
- Reversión de pagos (`PaymentManager::reversePayment()` — sin Feature test)
- Operaciones concurrentes sobre el mismo crédito
- Créditos padre/hijo (lógica `whereDoesntHave('children')` sin test unitario)
- Transiciones de estado en cascada

---

### 🟡 MEDIO

#### A-09 · `SyncController` (715 LOC) tiene lógica de negocio en controller
`syncPayments()` incluye validación de acceso a créditos (líneas 86-96), validación de monto (110-122) y orquestación de transacción. Todo esto debería delegarse al `PaymentManager` o un nuevo `PaymentSyncService`.

#### A-10 · Cache "realtime" en métricas de colección puede dar datos desfasados
`CollectionMetricsService` usa caché con TTL de 60 segundos etiquetado como "realtime". Para un supervisor que supervisa al equipo en tiempo real, 1 minuto de lag es significativo.

#### A-11 · `PartnerInvestmentService` (647 LOC) sin documentación
El servicio más largo del proyecto no tiene docblocks ni descripción de su flujo. Onboarding de nuevos developers en el módulo de socios es costoso.

#### A-12 · `user_id` nullable en `credit_audit_logs` sin motivo documentado
Migración `2026_03_03_103451` lo hace nullable sin comentario explicativo. Puede indicar que hay operaciones automatizadas (jobs, comandos) que crean logs sin usuario, pero no está documentado.

---

### 🟢 BAJO

#### A-13 · `CollectorVisibilityResolver::MODE_UNASSIGNED` sin test de respuesta API
El caso edge donde un supervisor no tiene cobradores asignados (`meta.unassigned: true`) no está cubierto por tests de integración.

#### A-14 · `SyncCreditStatusJob` backoff insuficiente
`releaseAfter(30)` y `expireAfter(60)` son muy agresivos. Bajo carga, los jobs compiten por el mismo lock y se reintentan en bucle. Aumentar a `releaseAfter(90)->expireAfter(180)`.

#### A-15 · Solo 1 Job asíncrono en todo el sistema
Operaciones que podrían ser async y no lo son: log de desembolso en `CreditOperationService:87`, envío de notificaciones (no existe), auditoría financiera nocturna.

#### A-16 · `synced: false` en `pendingPayments` Dexie — campo declarado pero nunca consultado
El campo existe en el schema y se setea al encolar, pero ningún query lo usa para filtrar. Dead code en DB.

---

## Lista de Tareas — Ordenada por Prioridad

### Sprint Inmediato (esta semana)

```
[ ] S-01  Cambiar APP_DEBUG=false y APP_ENV=production en .env de producción
[ ] S-02  Cambiar DB_PASSWORD a contraseña fuerte (≥32 chars, random)
[ ] S-03  Agregar filtro de ownership en PaymentController::receipt() según rol
[ ] S-04  Configurar expiración de tokens Sanctum: 'expiration' => 1440
[ ] S-05  Agregar throttle middleware a endpoints de escritura críticos:
          POST /sync/payments, /sync/expenses, /payments, /credits, /clients
[ ] B-01  Corregir getFrequencyDisplay(): separar lógica de weekly (due_day)
          vs. monthly (due_day_1). Agregar test unitario.
[ ] B-03  Validar existencia de nextInstallment antes de procesar pago en
          PaymentController::store(). Retornar 422 si no hay cuota pendiente.
[ ] P-01  Actualizar Service Worker para abrir IndexedDB con versión 4 (o
          usar flag de retry por índice separado en lugar de campo inline)
[ ] P-02  Corregir queuePayment(): envolver en una sola transacción — si add()
          falla, no ejecutar deductCreditBalance()
[ ] A-01  Refactorizar getCollectorPerformanceToday(): cambiar N+1 loop a
          query única con groupBy('collector_user_id') + sum('amount')
[ ] A-02  Crear tests/Feature/SyncPaymentsTest.php con mínimo 8 escenarios:
          pago válido, idempotencia, crédito ajeno, monto inválido, crédito
          sin cuotas, pago duplicado, batch parcial, rollback
```

### Sprint 2 (próximas 2 semanas)

```
[ ] S-06  Integrar CreditPolicy en CreditController PWA: reemplazar checks
          inline por $user->can('view', $credit) y can('update', $credit)
[ ] S-07  Validar ownership de credit_ids en ReorderCollectorCreditsRequest:
          verificar que todos pertenezcan al collector autenticado
[ ] S-08  Auditar todos los selectRaw/whereRaw en servicios de métricas y
          agregar comentario de seguridad donde hay inputs externos
[ ] B-02  Aplicar filtro rol-aware en PaymentController::show() y
          CollectionVisitController::today() para supervisores
[ ] B-05  Agregar soporte ?collector_id en CollectionVisitController::today()
          con mismo patrón que PaymentController::today()
[ ] B-10  Validar saldo disponible en PartnerController::storeTransaction()
          antes de crear retiro: amount <= getCurrentBalance()->value()
[ ] B-07  En ApprovalController::approve() y reject(): verificar estado actual
          del gasto antes de cambiar — prevenir re-aprobación
[ ] P-03  Envolver todas las llamadas async de approvals/delinquency/financials
          en try/catch con toast de error al usuario
[ ] P-04  Documentar en código y UI que cambios de multiplicador requieren
          recarga. Agregar detección de cambio de multiplicador en vuelo.
[ ] A-03  Crear migración correctiva: agregar onDelete('cascade') o
          onDelete('set null') en FKs de subscription_requests y
          company_financial_settings
[ ] A-04  Crear migración: agregar índices faltantes en supervisor_collector
          (company_id, supervisor_id), expenses(company_id),
          collection_visits(collector_user_id, visit_date)
[ ] A-06  Reemplazar lógica inline if($role === 'collector') en SyncController
          y CollectionController por CollectorVisibilityResolver::applyToQuery()
[ ] B-08  Agregar try/catch con Log::error() en CollectorSyncListener::handle()
```

### Sprint 3 (próximo mes)

```
[ ] S-02b Configurar restricciones en Google Maps API Key (HTTP referrer +
          resource restriction a Maps JavaScript API)
[ ] B-04  Implementar tabla company_collector_goals y funcionalidad de metas
          diarias en CollectionMetricsService (eliminar TODO hardcodeado)
[ ] B-06  Reducir ADMIN_SYNC_LIMIT de 500 a 150. Implementar cursor
          pagination o parámetro ?page en downloadData()
[ ] B-11  Agregar filtro rol-aware en SyncController::checkUpdates() para
          que supervisores reciban actualizaciones de su equipo
[ ] P-05  Implementar fallback offline en DelinquencyView y FinancialsView:
          mostrar datos de última sesión si están en IndexedDB
[ ] P-06  Optimizar collections store: pre-calcular remaining_balance en la
          tabla credits (desnormalizado) en lugar de calcularlo dinámicamente
[ ] P-08  Indexar campo permanent_error en Dexie o reemplazarlo por tabla
          separada pendingPaymentsErrors para evitar full table scan
[ ] P-09  Implementar fallback offline en PaymentsHistoryView: leer desde
          db.payments cuando !navigator.onLine
[ ] P-10  Agregar notificación de "acceso denegado" en router guard cuando
          el rol del usuario no cumple meta.roles de la ruta
[ ] P-11  Eliminar todos los console.log() de debug en stores y views.
          Opcional: integrar Sentry para error tracking en producción
[ ] A-05  Dividir FinanceAutoLogger en IncomeLogger + ExpenseLogger con SRP
          claro. Actualizar todos los callers.
[ ] A-08  Crear Feature tests para: reversión de pagos, créditos padre/hijo,
          operaciones concurrentes, y transiciones de estado en cascada
[ ] A-09  Extraer lógica de negocio de SyncController::syncPayments() a
          PaymentSyncService. El controller solo debe parsear request y
          formatear response.
[ ] B-14  Verificar existencia de accessors principal_pending y
          interest_pending en Installment model. Agregar si faltan, con tests.
[ ] B-13  Aumentar SyncCreditStatusJob::WithoutOverlapping a
          releaseAfter(90)->expireAfter(180)
```

### Backlog (deuda técnica controlada)

```
[ ] S-09  Migrar device_id de localStorage a sessionStorage
[ ] S-10  Agregar rate limiter global por IP como segunda capa en pwa-login
[ ] S-13  Implementar checksum de integridad en syncInitialData() para MITM
          protection (solo relevante si se usa HTTP en algún entorno)
[ ] A-11  Agregar docblocks exhaustivos a PartnerInvestmentService (647 LOC)
          y AdminDashboardMetricsService
[ ] A-12  Documentar por qué credit_audit_logs.user_id es nullable (migración
          2026_03_03). Si es para jobs/commands, crear bot_user especial.
[ ] A-15  Evaluar qué operaciones se benefician de ser async: logs de
          desembolso, notificaciones, auditoría nocturna
[ ] A-16  Eliminar campo synced de pendingPayments en Dexie si nunca se usa,
          o implementar su uso consistente
[ ] P-12  Agregar validadores de props en CreditCard.vue,
          WeeklyProgressChart.vue, OverdueAgingPanel.vue, PaymentSummary.vue
[ ] P-13  Limpiar event listeners de online/offline en unmount de App.vue
[ ] P-07  Abstraer cola de sincronización (payments + visits) en composable
          genérico useSyncQueue() para eliminar duplicación
[ ] A-10  Evaluar reducir TTL de cache "realtime" en CollectionMetricsService
          de 60s a 15-30s, o implementar invalidación event-driven
[ ] README Resolver el merge conflict sin resolver en README.md
```

---

## Módulos auditados como COMPLETOS ✅

Los siguientes módulos, marcados previamente como nuevos o en duda, están **correctamente implementados**:

| Módulo | Archivo | Estado |
|--------|---------|--------|
| SupervisorCollector | `app/Models/SupervisorCollector.php` | ✅ Completo — Pivot correcto, multi-tenant, relaciones |
| CollectorVisibilityResolver | `app/Services/CollectorVisibilityResolver.php` | ✅ Completo — 3 modos, applyToQuery() |
| SupervisorTeamController | `app/Http/Controllers/Api/Pwa/SupervisorTeamController.php` | ✅ Completo — CRUD con validación multi-tenant |
| AssignedCollectorsRelationManager | `app/Filament/Resources/Users/RelationManagers/` | ✅ Completo — Attach/Detach con scope |
| SupervisorTeamAssignmentTest | `tests/Feature/SupervisorTeamAssignmentTest.php` | ✅ 15 tests cubriendo todos los casos |
| SyncExpensesTest | `tests/Feature/SyncExpensesTest.php` | ✅ Idempotencia, deduplicación, validación batch |
| Migraciones 2026_04_04 | Las 3 migraciones de Abril | ✅ Correctas — constraints, unique, booted() |

---

## Cobertura de Tests — Estado Actual

| Área | Tests | Cobertura |
|------|-------|-----------|
| Distribución de pagos | `PaymentDistributorTest.php` | ✅ Alta |
| Invariantes post-pago | `PaymentInvariantGuardTest.php` | ✅ Alta |
| Reprogramación de cuotas | `CreditInstallmentReschedulerTest.php` | ✅ Alta |
| Estado de créditos | `SmartCreditStateResolverTest.php` | ✅ Media |
| Asignación supervisor | `SupervisorTeamAssignmentTest.php` | ✅ Alta |
| Sync de gastos | `SyncExpensesTest.php` | ✅ Media |
| **Sync de pagos offline** | — | ❌ Sin tests |
| **Reversión de pagos** | — | ❌ Sin tests |
| **Créditos padre/hijo** | — | ❌ Sin tests |
| **Operaciones concurrentes** | — | ❌ Sin tests |
| Visibilidad de colectores | `CollectorVisibilityResolverTest.php` | ⚠️ Solo 1 test |

---

*Reporte generado el 2026-04-14. Revisión recomendada en 30 días.*
