# Numeración de créditos por empresa (código 001, 002, …)

- **Fecha:** 2026-08-01
- **Rama:** `feat/credit-numbering`
- **Estado:** diseño aprobado en brainstorming, pendiente de plan.
- **Motivación:** el "código" visible del crédito es hoy el **`id` de la BD** (`Crédito #{{ credit.id }}`), una secuencia **global** (todas las empresas + todos los clientes). Por eso el primer crédito de una empresa puede mostrarse como #500. Los prestamistas esperan que su libro empiece en **001** y numere corrido (001, 002, 003…). Se introduce un **código por empresa**.

## Cómo funciona hoy (auditado)

- `Credit` **no tiene** ningún campo `code`/`number`/`correlativo` — solo `installment_number` (por cuota). El "número" mostrado es el PK global `id`.
- El `id` se muestra como "Crédito #{id}" en (PWA): `views/CreditDetailView.vue:5` (header), `views/PaymentView.vue:20`, `views/ClientDetailView.vue:96`, `components/payments/PaymentReceipt.vue:56,202` (recibo), y fallbacks offline en `db/index.js:233` y `views/PaymentsHistoryView.vue:154,229`. (También, probablemente, el panel Filament de créditos.)
- **No existe** ninguna secuencia por empresa que reusar: el `receipt_number` de pagos también es solo el `id` global con padding (`Api/Pwa/PaymentController.php:450`).
- **Concurrencia — patrón establecido:** `CreditOperationService` (líneas 144/233/340) y `CollectorCreditOrderService` (comentario: *"lockForUpdate() serializa inserciones concurrentes"*) ya usan `lockForUpdate()` dentro de transacciones para asignar secuencias/órdenes sin colisiones. Se **reutiliza ese patrón**.

## Decisiones (del brainstorming)

1. **Esquema: por empresa.** Cada empresa numera corrido: 001, 002, 003… (único dentro de la empresa; el más antiguo = 001). No per-cliente.
2. **Backfill cronológico** de los créditos existentes (una sola vez).

## Diseño

### 1. Campo de secuencia
Migración: `company_credit_number` (unsigned integer, **nullable** durante el backfill) en `credits`, con **índice único `(company_id, company_credit_number)`**. `id` (PK) no cambia — rutas y FKs siguen igual.

### 2. Asignación al crear (concurrency-safe)
En el **punto de creación de créditos** (el plan confirma cuál: `CreditOperationService::createNewCredit()` — el que usa la app y el seeder demo — y/o `Credits\CreditOperationOrchestrator`; ambos ya operan en transacción con `lockForUpdate`): dentro de la transacción, **bloquear** las filas de créditos de esa empresa (o una fila-ancla) y asignar `company_credit_number = (max de esa empresa) + 1` (arranca en 1). Mirror del patrón de `CollectorCreditOrderService`. Si hay más de un punto de creación, cubrir todos o centralizar la asignación en un helper compartido llamado por cada uno.
- **Robustez:** el índice único `(company_id, company_credit_number)` es la red final — una colisión (carrera improbable pese al lock) fallaría el insert en vez de duplicar en silencio.

### 3. Backfill de los existentes
Migración de datos (en el mismo release, tras crear la columna): por cada empresa, tomar sus créditos ordenados por `created_at` asc (desempate por `id` asc) y asignar 1, 2, 3… secuencialmente. Los **anulados/cancelados conservan** su número (sin renumerar; los huecos son válidos, como un talonario). La migración es idempotente-segura (solo asigna donde `company_credit_number IS NULL`).

### 4. Formato + display
- **Formato:** `str_pad((string) $n, 3, '0', STR_PAD_LEFT)` → **001** (mínimo 3 dígitos; crece natural: 999 → 1000). Un accessor en el modelo (`Credit::getCodeAttribute()` / `formattedCode`) centraliza el formato.
- **API/serializers:** exponer el campo (crudo + formateado) en las respuestas de crédito de la PWA (donde hoy se envía el crédito) para que el front lo muestre online **y** offline (IndexedDB debe guardar el nuevo campo).
- **Reemplazar** `Crédito #{{ credit.id }}` por el código formateado en: `CreditDetailView`, `PaymentView`, `ClientDetailView`, `PaymentReceipt` (+ el texto de compartir) y los fallbacks offline (`db/index.js`, `PaymentsHistoryView`). En el fallback offline sin dato enriquecido, usar el nuevo campo si está en IndexedDB; si no, degradar al id (como hoy).
- **Filament:** mostrar el código por empresa en la tabla/vista de créditos del panel (donde hoy se ve el id).
- **Routing y FKs siguen por `id`** (interno) — no cambian.

## No-objetivos / límites
- **No** cambiar el `id` (PK), rutas, ni FKs.
- **No** renumerar al anular/eliminar (los números son permanentes; huecos OK).
- **No** tocar el `receipt_number` de pagos (issue relacionado pero aparte).
- **No** numeración per-cliente ni código compuesto (decisión: por empresa).

## Riesgos / gotchas
- **Concurrencia:** el `lockForUpdate` serializa la asignación; el índice único `(company_id, company_credit_number)` es la red de seguridad. **TDD:** un test que cree varios créditos en una empresa y verifique 1,2,3 correlativos; otro que confirme que dos empresas numeran independientes (ambas desde 1).
- **Backfill sobre datos de prod:** una sola vez en la migración; test que verifique el orden cronológico por empresa. Ojo con empresas grandes (J&A tiene cientos de créditos) — la migración debe ser eficiente (por empresa, en lotes si hace falta).
- **Offline/PWA:** el nuevo campo debe viajar en el payload y guardarse en IndexedDB, o el crédito recién creado offline mostrará el id hasta sincronizar. Degradar con gracia.
- **PHPStan level 5** sobre el backend nuevo; **sin tests JS** de la PWA → build + revisión + validación en dispositivo para el display. Ver [[credify-ci-phpstan-preflight]], [[credify-test-db]].
- **Deploy full** (assets + backend + **migración con backfill**): build + ship assets + `git pull` + `migrate --force` + `optimize` + reload. Ver [[credify-prod-vm]].

## Verificación
- Tests backend: asignación correlativa por empresa (con lock), independencia entre empresas, y backfill cronológico correcto; el índice único impide duplicados. Suite completa + PHPStan + Pint.
- `npm run build` sin errores; el crédito muestra el código formateado (001…) en las 6+ ubicaciones, online y offline; el routing sigue por id.
- Validación en dispositivo: crear un crédito nuevo en una empresa → recibe el siguiente número; el recibo y las vistas muestran el código por empresa.
- CHANGELOG.

## Secuencia con el demo
Es una mejora **general** (toda empresa), en su propia rama. Como el seeder del demo crea créditos vía el mismo servicio, conviene **mergear/desplegar la numeración antes o junto con el demo** para que los créditos demo se vean 001+ (no #517). Se coordina el orden de deploy con el usuario.
