# Changelog / handoff — Hardening 2026-07-10

Documento de referencia para agentes y desarrolladores.  
Resume el **análisis**, los **bugs**, el **plan de mejoras** y lo **implementado** en la sesión del 2026-07-10.

---

## 1. Commit

| Campo | Valor |
|-------|--------|
| Hash | `689ed101` |
| Mensaje | Hardening de cobranza y operaciones: saldos, seguridad y reportes. |
| Alcance | 44 archivos, +3113 / −1501 |
| Rama | `main` (al momento del commit, 1 adelante de origin) |

Cuerpo del commit:

> Recalcula sales.pending desde pagos reales, valida montos en servidor y  
> activa CSRF/permisos de borrado. Extrae reportes a ReportService, añade  
> nueva venta, recibos, filtros de cartera y corrige payment_day nulo en  
> ventas históricas.

---

## 2. Contexto de la sesión (cronología)

1. **Bug DataTables** en `/reportes/cobranza` (análisis sin tocar código, luego fix).
2. **Análisis general** del proyecto → `docs/PROJECT_CONTEXT.md`.
3. **Implementación de todas las mejoras** P0–P3 del contexto.
4. **Migraciones en prod**: sí hay que correr las de julio 2026.
5. **Bug** `/ventas`: `Field "payment_day" is not nullable, but null was passed`.
6. **Commit** del hardening.
7. Este paquete de docs de referencia.

---

## 3. Análisis del sistema (resumen)

### Qué es

App interna de **venta de lotes** (manzana/lote): clientes, ventas a crédito/contado, cobros, gastos, reportes de cobranza/finanzas/vendedores.

### Stack

- CodeIgniter 4 + Shield (sesión)
- MySQL, soft deletes
- Tema Paces (Bootstrap 5, jQuery, DataTables 2.3.3)
- FrankenPHP/Caddy Docker (red `damp`)

### Arquitectura

- **SSR:** `Home` + `ReportController` → `Views/components/*`
- **API REST:** `Client`, `Sale`, `Payment`, `Expense`, `User` + `ResponseTrait`
- **Layout:** `Views/app.php` + partials
- **Dos mundos de vistas:** negocio en `components/`; demos del tema en raíz de Views (no son producto)

### Modelo de datos

```
clients 1──* sales 1──* payments
users (Shield) ── created_by
expenses (independientes)
```

Campos clave de `sales`: `amount`, `front_payment`, `pending` (denormalizado), `block`, `lot`, `payment_day`, `order_status`.

### Hallazgo crítico original (ya corregido)

Al registrar un pago en `/nuevo-pago`, **no se actualizaba `sales.pending`**.  
El saldo se fijaba solo al crear la venta. Eso contaminaba cartera, proyección y reportes.

**Solución:** `SaleBalanceService` + callbacks en `PaymentModel` + transacciones en controllers.

---

## 4. Bugs encontrados y resueltos

### 4.1 DataTables en `/reportes/cobranza`

| | |
|--|--|
| **Síntoma** | Warning DataTables tn/4: *Requested unknown parameter '1' for row 0, column 1* |
| **Causa** | Filas PHP con `<td colspan="N">` en tbody vacío; DT exige N celdas por fila |
| **Fix** | Quitar filas vacías; `language.emptyTable`; `data-order` en montos/fechas; CSS responsive |
| **Archivo** | `app/Views/components/reportes/cobranza.php` |

### 4.2 `payment_day` null en `/ventas`

| | |
|--|--|
| **Síntoma** | `Field "payment_day" is not nullable, but null was passed` |
| **Causa** | Cast Model `'payment_day' => 'integer'` (no nullable) + filas históricas NULL |
| **Fix** | Cast `?integer`; `afterFind` con default 1/15; migración backfill |
| **Archivos** | `SaleModel.php`, `2026-07-10-174500_BackfillNullPaymentDay.php` |

### 4.3 Controllers con `$e` indefinida

Ramas `fail($e->getMessage())` cuando `save()` devolvía false sin excepción.  
Reemplazado por `failValidationErrors($model->errors())` / `failServerError(...)`.

---

## 5. Inventario de cambios implementados

### 5.1 Librerías nuevas (`app/Libraries/`)

| Clase | Responsabilidad |
|-------|-----------------|
| `SaleBalanceService` | `recalculate($id)`, `currentPending($id)`, `recalculateAll()` |
| `ReportService` | Queries cobranza, cartera, proyección, financiero, clientes, vendedores |
| `PaymentMethods` | Códigos + labels ES + validación |

### 5.2 Controllers

| Archivo | Cambio |
|---------|--------|
| `Payment.php` | Transacción, tope ≤ pending, created_by sesión, delete con permiso, recalc |
| `Sale.php` | Transacción, enganche + payment_day, created_by sesión, byClient solo pending>0 |
| `Client.php` / `Expense.php` | Validación limpia, created_by, permisos delete |
| `Home.php` | Solo páginas SSR; sin SQL de reportes; `newSale`, `paymentReceipt` |
| **`ReportController.php`** | cobranza, financiero, clientes, vendedores |

### 5.3 Models

- Validaciones server-side
- `useTimestamps = true`
- `PaymentModel`: callbacks `afterInsert` / `afterDelete` → `SaleBalanceService`
- `SaleModel`: cast `?integer` payment_day + normalize afterFind
- `ClientModel`: title-case nombres en insert/update

### 5.4 Seguridad y config

| Archivo | Cambio |
|---------|--------|
| `Filters.php` | CSRF global; forcehttps solo `production` |
| `Security.php` | `regenerate = false` (AJAX) |
| `AuthGroups.php` | Permisos de dominio (clients/sales/payments/expenses/reports) |
| `Routes.php` | Reportes → ReportController; `/nueva-venta`; `/pago/(:num)/recibo` |
| `Autoload.php` | helper `app` |
| `app.php` layout | `csrf_meta()`, `app-common.js`, errores AJAX unificados |
| `sidenav.php` | Usuario real, logout `/logout`, menú negocio (sin demos) |

### 5.5 Frontend / UX

| Pieza | Detalle |
|-------|---------|
| `public/js/app-common.js` | CSRF AJAX, `LoteApp.dtDefaults`, labels métodos, `handleAjaxError` |
| `/nueva-venta` | Form dedicado (antes embebido en dashboard) |
| `/nuevo-pago` | Muestra saldo pendiente, max amount, solo ventas con saldo |
| `/pago/{id}/recibo` | Recibo imprimible |
| Cobranza | Filtros mes + bucket antigüedad + manzana; export Excel/PDF/print |
| Dashboard `/` | Stats + acciones rápidas (sin form de venta) |
| Listados DT | Convención `new DataTable` + `LoteApp.dtDefaults` |

### 5.6 Datos / tests / docs

| Pieza | Detalle |
|-------|---------|
| Migración índices + recalc pending | `2026-07-10-180000_*` |
| Migración backfill payment_day | `2026-07-10-174500_*` |
| `DemoSeeder` | Clientes/ventas/pagos/gasto demo |
| `tests/unit/SaleBalanceServiceTest.php` | Labels + fórmula documentada |
| `README.md` | Documentación de producto (reemplaza starter genérico) |
| `docs/*` | Este handoff + context + operations |

### 5.7 Infra versionada

`Dockerfile`, `docker-compose.yml`, `Caddyfile`, `Dampfile` añadidos al repo.

---

## 6. Fórmula de pending (fuente de verdad)

```text
pending = max(0, round(amount - SUM(payments.amount), 2))
```

- Pagos soft-deleted **no** suman.
- Enganche = payment con amount = front_payment en el alta.
- Estado: pending ≤ 0.009 → `Delivered`, else `Processing`.

Validación al cobrar: `amount > currentPending + 0.009` → error de validación.

---

## 7. Plan P0–P3 — estado final

Ver tabla detallada en `PROJECT_CONTEXT.md` §11. Resumen:

| Prioridad | Estado |
|-----------|--------|
| P0 saldos, validación, bugs controllers | Hecho |
| P1 ReportService, DT unificado, timestamps | Hecho |
| P1 CSRF, roles delete, created_by, logout | Hecho |
| P2 nueva venta, saldo en cobro, export, filtros, recibo, i18n métodos | Hecho |
| P2 seeds, índices, tests unitarios, README, forcehttps | Hecho |
| P3 console.log negocio, errores AJAX, docs | Hecho |
| P3 borrar demos del tema | **No** (se dejan como referencia UI) |
| FK formales created_by → users | Pendiente |
| Tests integración con DB | Pendiente |

---

## 8. Archivos “fuente de verdad” post-hardening

```
app/Libraries/SaleBalanceService.php
app/Libraries/ReportService.php
app/Libraries/PaymentMethods.php
app/Helpers/app_helper.php
app/Controllers/{Home,ReportController,Sale,Payment,Client,Expense}.php
app/Models/{Sale,Payment,Client,Expense}Model.php
app/Config/{Routes,Filters,AuthGroups,Security}.php
public/js/app-common.js
app/Views/app.php
app/Views/components/**
docs/PROJECT_CONTEXT.md
docs/OPERATIONS.md
docs/CHANGELOG_HARDENING_2026-07-10.md   ← este archivo
```

---

## 9. Instrucciones para el próximo agente

1. Leer `docs/README.md` → este changelog + `PROJECT_CONTEXT.md` + `OPERATIONS.md`.
2. **No** recalcular pending a mano en el front; usar/respetar `SaleBalanceService`.
3. **No** restar `front_payment` + SUM(payments) (doble conteo).
4. **No** meter filas `colspan` en tbody de DataTables.
5. Casts de Model: usar `?integer` / `?float` si el campo puede ser NULL en BD.
6. AJAX mutadores: confiar en CSRF de `app-common.js` + `csrf_field()` en forms.
7. Cambios de negocio casi siempre: Model + Controller resource + vista `components/`.
8. Reportes: `ReportService` / `ReportController`, no reintroducir SQL en `Home`.
9. Antes de deploy: `php spark migrate` y checklist en `OPERATIONS.md`.

---

## 10. Comandos útiles

```bash
php spark migrate
php spark migrate:status
php spark db:seed DemoSeeder          # solo local
composer test                         # PHPUnit
php spark serve
docker compose up -d --build
```

---

*Fin del handoff 2026-07-10.*
