# SFM — Terminar de cablear Tarjetas de Crédito a Transacciones

> Estado: **Implementado (Fases 1-3 completas, Fase 4 parcial por decisión del usuario).**
> Migración `2026_08_07_100000_add_balance_to_sfm_credit_cards.php` aún NO corrida contra
> la BD real — el usuario debe ejecutar `php artisan migrate`.
> Módulo: `Modules/SimpleFinancialManagement`
> Disparador: el usuario quiere empezar a usar más su tarjeta y hoy no tiene control
> automático — de paso, quiere poder retirar una `Wallet` que creó llamada "Crédito" que
> no sabía cómo usar bien.

## Objetivo

El módulo ya tiene `CreditCard` (límite, día de corte, día de pago, tasa de interés
anual/mensual) y `CardCycle` (una fila por estado de cuenta: monto, fecha límite, pagado,
interés cobrado) — pero **están desconectados de `/Transaction`**. Hoy:
- `Transaction.credit_card_id` y `Transaction.card_tx_type` existen en la tabla desde el
  diseño original pero **ningún componente Livewire los usa** (confirmado por grep en todo
  el módulo — el único lugar que los toca es el modelo mismo y los seeders de demo).
- `CardCycleIndex::createCycle()`/`updateCycle()` reciben `statement_amount`,
  `paid_amount`, `interest_charged` como **campos numéricos escritos a mano** — no se
  derivan de ningún movimiento real.
- `CreditCard.annual_rate_pct`/`monthly_rate_pct` se calculan entre sí en el formulario
  (`CreditCardIndex::updatedNewAnnualRatePct()`) pero **no se usan en ningún cálculo** más
  allá de eso — son decorativos hoy.

Meta: registrar una compra con tarjeta como cualquier otro movimiento, ver cuánto debes en
tiempo real, y que el monto del estado de cuenta + el interés se sugieran solos en vez de
escribirse a mano.

## Decisión de diseño confirmada con el usuario

**No** se modela la tarjeta como una `Wallet` con saldo negativo (se evaluó y se descartó
en conversación — ver razones abajo). `CreditCard` sigue siendo su propia entidad; se
termina de cablear, no se reemplaza por Wallet.

**Por qué no Wallet-como-tarjeta** (razones concretas, no genéricas):
- `SfmDashboard::totalBalance` suma el saldo de todas las billeteras — una billetera en
  negativo se restaría ahí en silencio, cambiando el significado de "Balance Total" (hoy
  = efectivo disponible) sin que el usuario lo pida explícitamente.
- `Wallet.low_balance_threshold` está pensado al revés para una tarjeta: lo que importa
  ahí es acercarse al `credit_limit`, no a cero.
- `CashFlowProjection` usa "saldo inicial = suma de billeteras hoy" — mezclaría deuda con
  efectivo real en una proyección que debe representar solo caja.
- `CardCycle` ya modela ciclo de facturación/fecha de pago/interés — replicarlo sobre
  `Wallet` sería reconstruir lo que ya existe, con otro nombre.

**Mecánica del ciclo completo (confirmada con el usuario, evita doble conteo):**
1. **Compra con tarjeta** → `type: expense`, `wallet_id: null`, `credit_card_id` +
   `card_tx_type: 'charge'`. Ya es gasto real en ese momento (tu patrimonio baja aunque no
   salga efectivo de ninguna billetera). Sube el saldo adeudado de la tarjeta.
2. **Pago de la tarjeta desde una billetera de débito** → **transferencia**, no
   gasto+ingreso. Baja el saldo de la billetera de origen Y baja lo que debes en la
   tarjeta; no debe contar en los KPIs de Ingreso/Gasto/Neto (mismo criterio que las
   transferencias entre billeteras hoy — si se contara como gasto, el mismo dinero
   aparecería gastado dos veces, ya que el gasto real se contó en el paso 1).
3. **Interés cobrado** (si no pagaste todo) → su propio `type: expense`,
   `credit_card_id` + `card_tx_type: 'interest'`, igual que la compra original. Aparte del
   pago.

## Diseño de datos

### `sfm_credit_cards` — agregar saldo vivo

```
sfm_credit_cards
  + balance decimal(14,2) not null default 0   -- lo que debes ahora mismo, mismo patrón
                                                 -- que sfm_wallets.balance (mantenido por
                                                 -- el observer, no recalculado al vuelo)
```

### `sfm_transactions` — SIN columnas nuevas

`credit_card_id` y `card_tx_type` ya existen. Reutilizarlos evita otra migración:
- `card_tx_type` pasa a tener 3 valores reales: `charge` / `payment` / `interest` (hoy no
  se usa ninguno).
- Para el **pago** (paso 2 arriba): la fila de `type='transfer'` lleva `wallet_id` (origen,
  billetera de débito) Y `credit_card_id` (destino, la tarjeta) al mismo tiempo — en vez de
  agregar una columna `transfer_credit_card_id` aparte. `transfer_wallet_id` y
  `credit_card_id` quedan mutuamente excluyentes en una transferencia: o el destino es otra
  billetera, o es una tarjeta, nunca ambos.

### `TransactionObserver` — extender, no reescribir

- `applyEffect()`: cuando `type='expense'` y `credit_card_id` está seteado (sin
  `wallet_id`) → `CreditCard::increment('balance', $amount)` en vez de tocar una Wallet.
  Mismo patrón para `card_tx_type='interest'`.
- `applyTransfer()`: cuando la transferencia tiene `credit_card_id` en vez de
  `transfer_wallet_id` como destino → `decrement('balance', ...)` sobre la `CreditCard` en
  vez de `increment` sobre una `Wallet`. El lado origen (`wallet_id`) no cambia.
- Los métodos `created()`/`updated()`/`deleted()` ya están estructurados alrededor de
  "reversar con el snapshot viejo, aplicar el nuevo" — el mismo esqueleto sirve, solo hay
  que enrutar a `CreditCard` cuando corresponda en vez de asumir siempre `Wallet`.

## Fases de implementación

### Fase 1 — Saldo vivo de la tarjeta

1. Migración: `balance` en `sfm_credit_cards`.
2. `TransactionObserver` extendido según el diseño de arriba.
3. `Models/CreditCard.php` — método `owedAmount()` o similar si se prefiere no exponer
   `balance` crudo en vistas (consistencia con cómo `Bucket`/`Wallet` exponen helpers).

### Fase 2 — Wiring en `/Transaction` (`MovementCrud`)

4. En el modal de Crear/Editar Movimiento: cuando el tipo es `expense`, opción de elegir
   **Billetera O Tarjeta de crédito** (no ambas) — mutuamente excluyente, similar al toggle
   que ya existe para `movementType !== 'transfer'` en el mismo archivo. Si se elige
   tarjeta, `wallet_id` queda `null` y se setea `credit_card_id` + `card_tx_type: 'charge'`.
5. En el toggle de Transferencia: agregar opción de destino "Tarjeta de crédito" además de
   billetera — mismo select, cambia qué columna se llena (`transfer_wallet_id` vs
   `credit_card_id`).
6. Filtro nuevo en `/Transaction`: `filterCreditCard`, mismo patrón que `filterWallet`.

### Fase 3 — Conectar con `CardCycle` (el "cálculo automático" que pidió el usuario)

7. `CardCycleIndex::createCycle()`/`generateCycles()`: al crear o cerrar un ciclo, sugerir
   `statement_amount` = suma de transacciones (`charge` + `interest`) de esa tarjeta con
   fecha dentro de `[period_start, period_end]` — sigue siendo editable a mano por si el
   usuario quiere ajustar, pero ya no parte de cero. No requiere FK nueva
   (`card_cycle_id` en `Transaction`): se deriva por rango de fechas, igual que ya hace
   `AnnualReport`/`WalletLedger` con otros totales del periodo.
8. `interest_charged` sugerido = `monthly_rate_pct × (statement_amount del ciclo anterior −
   paid_amount del ciclo anterior)`, solo si quedó saldo sin pagar — **a confirmar con el
   usuario cómo cobra intereses su banco real** (algunos cobran sobre el total si no pagas
   el 100%, no solo sobre el saldo revolvente; varía por emisor) antes de dar esto por
   sentado como fórmula única.

### Fase 4 — Opcional, después de validar en uso real

9. `SfmDashboard`: `activeCards` (hoy solo cuenta tarjetas activas) podría mostrar también
   "total adeudado" sumando `CreditCard.balance` — nuevo widget, no reemplaza nada.
10. `credit_limit` — alerta cuando el saldo se acerca al límite (paralelo conceptual al
    `low_balance_threshold` de Wallet, pero en la dirección correcta para deuda).
11. `minimum_payment_pct` en `CreditCard` (muchos bancos exigen un % mínimo, no el 100%) —
    solo si el usuario lo quiere; no es indispensable para el objetivo original.
12. Data Center / API móvil: exponer `CreditCard`/`CardCycle` si algún día se quiere
    exportar/importar o ver desde la app — hoy ninguno de los dos implementa `Importable`.

## Archivos que toca este cambio

`Models/CreditCard.php`, `Observers/TransactionObserver.php`, `Http/Livewire/MovementCrud.php`
+ `movement-crud.blade.php`, `Http/Livewire/CardCycleIndex.php` + `card-cycle-index.blade.php`,
una migración nueva. `Http/Livewire/CreditCardIndex.php` no necesita cambios de fondo (el
CRUD del catálogo ya está completo, solo ganaría mostrar el `balance` en la tabla).

## Nota sobre la Wallet "Crédito" existente

Antes de que el usuario borre esa `Wallet`: `WalletIndex::deleteWallet()` ya bloquea el
borrado si tiene movimientos asociados (protección existente, no hay que agregar nada). Si
tiene transacciones históricas, hay que decidir —cuando se llegue a esta fase— si se dejan
como registro histórico en esa Wallet o se re-registran contra la `CreditCard` real; no es
un paso automático, se revisa a mano. **Ahora que el sistema real ya existe**, el usuario
puede evaluar esto cuando quiera — no es parte de esta implementación.

## Implementado — Fase 1: saldo vivo (2026-08-07)

- `Database/migrations/2026_08_07_100000_add_balance_to_sfm_credit_cards.php` — columna
  `balance` en `sfm_credit_cards`, idempotente (`Schema::hasColumn` como guarda). **NO
  corrida aún contra la BD real.**
- `Models/CreditCard.php` — `balance` en `$fillable` y `$casts`.
- `Observers/TransactionObserver.php` — extendido sin tocar la lógica existente de
  transferencias billetera↔billetera:
  - `saving()` ahora también guarda `credit_card_id` en el snapshot para reversas.
  - Métodos nuevos `applyCardEffect()` (compra/interés directo a la tarjeta, sube/baja
    deuda según `expense`/`income`) y `applyCardPayment()` (pago vía transferencia —
    **no** se puede revertir con el truco de intercambiar origen/destino que usa
    `applyTransfer()`, porque una tarjeta nunca es origen de fondos; se revierte con
    signo explícito `+1`/`-1`).
  - `created()`/`updated()`/`deleted()` ramifican a estos métodos nuevos cuando
    `credit_card_id` está presente, sin cambiar el camino existente de `Wallet`.
- **Verificado con test nuevo** `tests/Feature/SimpleFinancialManagement/CreditCardBalanceTest.php`
  (6 casos, contra `lara_roles_test`, no la BD real): compra sube deuda, pago baja
  billetera y deuda sin duplicar el gasto, delete de compra revierte, delete de pago
  revierte ambos lados, editar el monto de una compra ajusta la deuda, interés se
  comporta como una compra. Este test queda como cobertura permanente del módulo.

## Implementado — Fase 2: wiring en `/Transaction` (2026-08-07)

- `MovementCrud.php` + `movement-crud.blade.php`:
  - Toggle "Billetera / Tarjeta de crédito" visible solo cuando el tipo es `expense`
    (`newMovementExpenseSource`/`editMovementExpenseSource`, Alpine `x-show` entangled
    junto a `movementType`) — al elegir tarjeta, oculta el select de Billetera y muestra
    uno de Tarjeta; `wallet_id` queda `null`, se graba `credit_card_id` +
    `card_tx_type: 'charge'`.
  - Mismo patrón en el bloque de Transferencia: toggle "Billetera / Tarjeta de crédito"
    como **destino** (`newMovementTransferTarget`/`editMovementTransferTarget`). Si se
    elige tarjeta: oculta Destination Wallet (y su bloque de conversión de moneda, que
    no aplica), muestra select de Tarjeta, graba `type: transfer` +
    `credit_card_id: <tarjeta>` + `card_tx_type: 'payment'`, y **oculta por completo**
    los campos Bucket/Subcategoría/Destino (ver decisión resuelta arriba — evita doble
    conteo en presupuesto).
  - Validación con `Rule::requiredIf(...)` (no `required_if:campo,valor` de string, que
    no puede expresar la condición compuesta tipo+origen/destino) para Billetera,
    Tarjeta, Billetera Destino y Tarjeta Destino, mutuamente excluyentes según el caso.
  - Filtro nuevo `filterCreditCard` en el panel de filtros.
  - Columna `Credit Card` agregada a los exports propios de `/Transaction` (XLSX/CSV/PDF
    del componente, no el Data Center). La celda "Billetera" de la tabla ahora muestra el
    ícono + nombre de la tarjeta cuando corresponde (compra directa o pago), incluyendo
    el caso `Billetera → 💳 Tarjeta` en la vista de transferencias.
  - Modal de ayuda: nueva sección explicando el flujo compra/pago con tarjeta.
- `SfmDashboard::openPay`/`RecurringExpenseIndex::openPay`/`HouseholdManager::recordPayment`
  (los 3 flujos secundarios de pago) **NO se tocaron** — pagan gastos fijos/de grupo desde
  una billetera, no desde tarjeta; no forma parte de este roadmap.

## Implementado — Fase 3: sugerencia de `statement_amount` (2026-08-07)

- `CardCycleIndex.php` — método público `suggestedStatementAmount(cardId, from, to)`:
  suma `Transaction` con `card_tx_type IN ('charge','interest')` en el rango de fechas del
  ciclo. Expuesto en Crear (`newCardId`/`newPeriodStart`/`newPeriodEnd` ahora
  `wire:model.live` para recalcular al vuelo) y Editar (`editCreditCardId` nuevo,
  guardado en `openEdit()` desde el ciclo — el formulario de edición no permite cambiar de
  tarjeta, así que no hace falta selector). Botón "Usar" junto al campo, **nunca se aplica
  sola** — el usuario decide si la acepta o ajusta a mano.
- `interest_charged` sigue 100% manual (ver decisión resuelta arriba).

## Implementado — Fase 4 parcial (2026-08-07)

- **Punto 9 (total adeudado)**: `SfmDashboard.php` — `$totalCardDebt` (suma de `balance`
  de tarjetas activas, **sin caché** a diferencia de `$activeCards`, porque cambia con
  cada movimiento y debe verse al instante, igual que los saldos de Wallet). Se muestra
  junto al conteo de tarjetas activas existente.
- **Punto 10 (alerta de límite)**: `$cardsNearLimit` — tarjetas activas con
  `balance >= credit_limit * 0.9`, alerta visual (`bg-warning`) por tarjeta en el
  Dashboard. Umbral de 90% fijo por ahora (no configurable, a diferencia de
  `low_balance_threshold` de Wallet que sí lo es) — evaluar si hace falta configurar por
  tarjeta si el usuario lo pide.
- `CreditCardIndex.php` + vista: columna `Owed` (balance) agregada a la tabla, en rojo si
  hay saldo pendiente.
- **Puntos 11 y 12: descartados/diferidos** — ver "Decisiones resueltas" arriba.

## Decisiones — resueltas (2026-08-07)

- **Fórmula de interés (punto 8)**: el usuario prefirió **dejarlo manual por ahora** —
  `interest_charged` en `CardCycle` se sigue escribiendo a mano. Se implementó todo lo
  demás de la Fase 3 (sugerencia de `statement_amount` desde movimientos reales). Retomar
  cuando el usuario tenga un estado de cuenta real a la vista para verificar cómo cobra su
  banco específicamente.
- **Nombre en UI**: "Tarjeta de crédito" a secas (no "Pagar tarjeta: {nombre}").
- **Widget de "total adeudado"**: se dejó en la Fase 4, como planteaba el roadmap
  originalmente (no se adelantó a la Fase 1).
- **Aclaración importante que surgió en la conversación** (antes de implementar): el
  usuario preguntó cómo trataría el sistema un pago de tarjeta en los reportes. Se
  confirmó y **se implementó así**: el pago (`type: transfer` con `credit_card_id` como
  destino) **no lleva Bucket/Subcategoría/Destino** — esa clasificación ya vive en la
  compra original (`type: expense`, `card_tx_type: charge`). Si el pago también llevara
  Bucket, el presupuesto por categoría contaría el mismo gasto dos veces (una transferencia
  con Bucket asignado sí suma al presupuesto, mismo mecanismo que ya se usa para el
  patrón de ahorro). El modal de `/Transaction` NO ofrece esos campos cuando el destino de
  la transferencia es una tarjeta.
- **`minimum_payment_pct` (punto 11)**: descartado. Nada en el plan lo iba a usar en
  ningún cálculo — agregarlo habría sido un campo muerto. Si en el futuro se construye una
  sugerencia de "pago mínimo", se agrega junto con esa feature, no antes.
- **Data Center / API móvil para `CreditCard`/`CardCycle` (punto 12)**: diferido, tal como
  ya decía el roadmap original ("opcional, después de validar en uso real") — la feature
  es nueva, no tiene todavía uso real que validar. Revisar si vale la pena cuando el
  usuario lleve un tiempo registrando compras con tarjeta.
