# Handoff — Edición y eliminación de clientes (2026-07-23)

> Contexto para humanos y agentes. Complementa [PROJECT_CONTEXT.md](./PROJECT_CONTEXT.md) y el handoff [edit/delete + redesign 2026-07-21](./CHANGELOG_EDIT_DELETE_REDESIGN_2026-07-21.md).

---

## 1. Qué se hizo

1. **Editar cliente al patrón de venta/pago.** El modal `editClient` dejó de auto-guardar campo por campo (PUT vía `editThing`) y ahora es un formulario con botón **Guardar cliente** que envía `POST /client/update/{id}` + CSRF, igual que ventas y pagos.
2. **Eliminar cliente con regla de negocio.** `Client::delete` bloquea el borrado si el cliente tiene ventas con `pending > 0`:
   > "No se puede eliminar al cliente: tiene {N} venta(s) con saldo pendiente. Podrá eliminarse cuando la(s) venta(s) esté(n) pagada(s)."
   El mensaje llega al usuario como toast rojo vía `LoteApp.handleAjaxError` (sin JS extra). Si no hay saldo pendiente: soft-delete + `deleted_by` desde sesión.
3. **Permiso `clients.delete` para el grupo `user`** en `AuthGroups` (decisión de producto: los operadores también pueden borrar clientes, sujetos a la regla de saldo).
4. **`parseRequestInput()` movido a `BaseController`** (estaba duplicado en `Sale` y `Payment`; ahora los tres controllers lo heredan).
5. **Flag de UI `canDeleteClients`**: `Home::clients` lo calcula (`can('clients.delete') || admin/superadmin`) y `clients.php` solo pinta el botón de basura si es true. El servidor sigue siendo la autoridad.
6. **`withDeleted()` en lookups de cliente** de `Home::saleDetail`, `paymentReceipt` y `saleReceipt`: las ventas históricas de un cliente borrado (soft delete) seguían listándose en `/ventas`, pero su detalle/recibos tronaban con null. Ya no.
7. **Limpieza de código muerto en `saleDetail.php`**: se quitó el include de `modals/editClient` y `setupEditClientInputs()` (nunca fueron funcionales ahí y con el nuevo form-con-submit habrían roto la página).

## 2. Decisiones de diseño

- **La verificación de saldo vive en el controlador** (query directa a `SaleModel`, patrón de `Sale::byClient`), no en un callback `beforeDelete` del modelo: permite responder `failValidationErrors` con mensaje en español y conteo. Las ventas soft-deleted no bloquean (el modelo las excluye solo).
- **No se cascadean las ventas pagadas al borrar el cliente.** Son historia financiera que alimenta los reportes; se conservan intactas y `/ventas` sigue mostrándolas con el nombre del cliente.
- **`email` se actualiza con `array_key_exists` puro** (acepta cadena vacía): la regla es `permit_empty` y el usuario debe poder vaciar el correo. Los demás campos usan el patrón "presente y no vacío" de `Sale::update`.
- **Auth de Client update/delete usa el patrón preferido** `can('clients.*') || inGroup('admin','superadmin')` — a diferencia de Sale/Payment que siguen con `inGroup('user')` (ítem H, sigue pendiente para esas entidades).

## 3. Archivos tocados

| Archivo | Cambio |
|---------|--------|
| `app/Controllers/Client.php` | `update()` reescrito (POST + parseRequestInput + can()); `delete()` con regla de saldo + `deleted_by` |
| `app/Controllers/BaseController.php` | + `parseRequestInput()` compartido |
| `app/Controllers/Sale.php` / `Payment.php` | − copias locales de `parseRequestInput()` |
| `app/Controllers/Home.php` | `clients()` pasa `canDeleteClients`; `withDeleted()` en saleDetail/paymentReceipt/saleReceipt |
| `app/Config/Routes.php` | + `POST client/update/(:num)` |
| `app/Config/AuthGroups.php` | + `clients.delete` al grupo `user` |
| `app/Views/modals/editClient.php` | Rediseño: form + Guardar (patrón editSale) |
| `app/Views/components/clients.php` | Handler submit del form; − listener auto-guardado; botón borrar condicionado a `canDeleteClients` |
| `app/Views/components/saleDetail.php` | − modal editClient muerto y su JS |

## 4. Cómo probar

1. **Editar**: `/clientes` → lápiz → cambiar datos → Guardar → toast verde y tabla recargada. Vaciar teléfono → toast rojo de validación.
2. **Borrado bloqueado**: cliente con venta a crédito (`pending > 0`) → borrar → toast rojo con el mensaje de la regla; el cliente permanece.
3. **Pagar y borrar**: liquidar la venta → borrar → éxito; `deleted_at`/`deleted_by` en BD; `/venta/{id}` y recibos de sus ventas históricas abren sin error.
4. **Permisos**: usuario grupo `user` puede borrar (puede requerir re-login tras el cambio de matriz); usuario sin permiso no ve el botón y el DELETE directo devuelve 403.

## 5. Pendientes que deja este release

- Ítem H (2026-07-21) sigue: unificar Sale/Payment a `can()`.
- Tests PHPUnit de `Client::update/delete` (incluida la regla de saldo) — se suma al ítem J.
