# SFM — Beneficiario/Destino + Subcategorías (opcionales)

> Estado: **Roadmap completo — Fases 1, 2, 3 y 4 implementadas.** Sin migraciones nuevas en
> Fase 4 (solo código). Suite completa (164 tests) verificada tras cada fase.
> Módulo: `Modules/SimpleFinancialManagement`

## Objetivo

Dos mejoras de clasificación para `/Transaction`, confirmadas con el usuario tras evaluar
una auditoría externa (ver contexto de sesión, 2026-08-06):

1. **Destino/Beneficiario**: separar "qué compré" (Bucket/Categoría) de "para quién o qué
   destino fue el dinero" (Yo, Casa, Padre, Fondo Familiar, Iglesia...). Hoy esa información
   se pierde porque categorías como "Casa" o "Padre" mezclan naturaleza y destino.
2. **Subcategorías**: recrear una tabla que existió (`sfm_subcategories`, migración
   `2025_12_30_173004`) pero fue **eliminada a propósito** el 2026-05-24
   (`database/migrations/2026_05_24_000001_drop_sfm_subcategories.php`, junto con la
   columna `subcategory_id` de `sfm_transactions`) al construir el Data Center — había un
   modelo `Subcategory.php` real y un `SubcategorySeeder.php`, sin UI nunca conectada, y se
   quitaron por no estar en uso. **Corrección sobre lo que decía esta misma sección antes**:
   no era "andamiaje dormido desde el día uno", fue un rechazo activo — consistente con lo
   que el usuario recordaba al abrir esta conversación. Ahora se reconstruye desde cero,
   con intención y UX definida. Es **opcional por categoría**: la mayoría de Buckets (ej.
   "Apoyo") no tendrán ninguna, y el campo de subcategoría debe desaparecer del modal
   cuando el Bucket elegido no tiene ninguna definida — no se agrega un campo obligatorio
   nuevo a todos los movimientos.

## Fuera de alcance (descartado tras la conversación previa)

- Flag "Recurrente/Ocasional" — ya cubierto por `RecurringExpense`.
- Campo "Es transferible" — ya resuelto por `Transaction.type = 'transfer'`.
- Campo "Responsable" por movimiento — bajo valor en app de un solo usuario; se solapa con
  Destino.
- Reorganizar los Buckets actuales (dividir "Gustos", eliminar "Gastos varios", etc.) — no
  requiere código, lo hace el usuario directamente en `/Bucket`.

## Diseño de datos

### Destino (`sfm_destinations`) — tabla NUEVA

Catálogo por usuario, mismo espíritu que `Bucket` pero sin `kind`:

```
sfm_destinations
  id
  user_id (FK users, cascadeOnDelete)
  name (string 100)
  icon (string, nullable — para el selector, igual que otros catálogos del proyecto)
  is_default (boolean, default false)
  timestamps
```

`sfm_transactions.destination_id` — FK nullable a `sfm_destinations`, `nullOnDelete()`.

Ambos cambios van en **una** migración nueva (tabla + columna).

### Subcategoría — completar lo existente, SIN migración de tabla

- `sfm_subcategories` ya tiene `bucket_id`, `user_id` (nullable — **revisar si conviene
  hacerlo `not null` en una migración de ajuste, ya que hoy permite filas huérfanas de
  usuario**), `name`, `description`.
- `sfm_transactions.subcategory_id` ya existe y apunta a `sfm_subcategories` con
  `nullOnDelete()`.
- Falta únicamente: modelo Eloquent, global scope `owner`, UI, wiring en el modal de
  movimientos, filtros y (opcional) reportes/CSV/API.
- Considerar un índice único `(bucket_id, name)` si no existe — evita duplicados por
  categoría. Verificar antes de asumir que hace falta.

## Fases de implementación

### Fase 1 — Modelos y catálogos base

1. Migración nueva: crear `sfm_destinations` + columna `destination_id` en
   `sfm_transactions`. Migración de ajuste opcional para `sfm_subcategories.user_id` (si se
   decide hacerlo obligatorio).
2. `Models/Destination.php` — mismo patrón que `Bucket.php` (global scope owner,
   `WithAuditLog`, `Importable` con `csvImportFields/Rules/Transform`, `exportHeaders/Row`).
3. `Models/Subcategory.php` — `belongsTo(Bucket::class)`, global scope owner, `WithAuditLog`,
   `Importable` (mismo patrón).
4. `module.json` — agregar `destination` y `subcategory` al array `permissions`.

### Fase 2 — CRUD de catálogos

5. `Http/Livewire/DestinationIndex.php` + vista — copiar el patrón de `BucketIndex.php`
   (sin `kind`, mucho más simple: nombre + icono + default). Nueva ruta
   `GET /SfmDestinations` → `destination.index`. Entrada en `pages.json` (submenu
   `Advanced`, junto a Exchange Rates/Budget).
6. Gestión de subcategorías **anidada dentro de `/Bucket`**, no como página propia de menú
   (la mayoría de Buckets no tendrán ninguna — no amerita un ítem de menú aparte): botón
   "Gestionar subcategorías" por fila en `bucket-index.blade.php` que abre un modal con
   mini-CRUD (crear/renombrar/eliminar) contra `Subcategory::where('bucket_id', ...)`.
7. Opcional (mismo patrón que `SfmDataCenter::RECOMMENDED_*` / `applyRecommendedSetup()`):
   botón "Aplicar destinos sugeridos" que crea Yo/Casa/Padre/Fondo Familiar/Iglesia vía
   `firstOrCreate` — el usuario decide si lo quiere o prefiere crearlos a mano.

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

8. **Destino**: nuevo select "Destino" (opcional) junto al de Bucket en los modales
   Crear/Editar Movimiento (`movement-crud.blade.php` líneas ~481 y ~634, mismo bloque
   donde vive el select de Bucket). Propiedades `newMovementDestination`/
   `editMovementDestination`, reglas `nullable, Rule::exists('sfm_destinations','id')->where('user_id', Auth::id())`,
   incluido en `addMovement()`/`updateMovement()`.
   - **Mismo select en los otros 3 lugares que crean `Transaction` sin pasar por este
     modal** (hallazgo de la revisión de pantallas, ver tabla arriba — sin esto, esos
     movimientos quedan permanentemente sin Destino): `SfmDashboard::openPay()`/
     `recordPayment()`, `RecurringExpenseIndex::openPay()`/`recordPayment()` (mismo
     patrón duplicado), y `HouseholdManager::recordPayment()` (solo cuando
     `payLinkTransaction` está activo). Considerar también agregar `destination_id` a
     `RecurringExpense` mismo (como ya tiene `bucket_id`) para poder pre-asignarlo en
     `createExpense()`/`updateExpense()` y no repetirlo cada mes al pagar.
   - Subcategoría NO se agrega a estos 3 flujos secundarios (gastos fijos y pagos de
     grupo) — queda solo en el modal principal de `/Transaction`, ver justificación en
     la tabla de revisión.
9. **Subcategoría** — depende de qué Bucket esté elegido, así que necesita cascada:
   - Recomendado: hacer el select de Bucket `wire:model.live` (ya hay precedente en el
     mismo archivo, ej. `editMovementWallet`) y exponer una computed property
     `subcategoryOptions` que devuelve las subcategorías del Bucket actualmente
     seleccionado (vacío si no tiene ninguna).
   - El campo de subcategoría se envuelve en `@if(count($subcategoryOptions))` — **no se
     renderiza en absoluto** si el Bucket no tiene subcategorías, cumpliendo el
     requisito de no ensuciar el modal con un campo que casi nunca aplica.
   - Alternativa si el roundtrip de `.live` resulta molesto en la práctica: precargar
     `{bucket_id: [{id,name}, ...]}` completo en un `<script>` + `window.__sfmSubcats = @json(...)`
     (NO como JSON inline en `x-data`, ver `[[feedback-alpine-patterns]]` — falla
     silenciosamente con colecciones grandes) y filtrar client-side con Alpine. Empezar
     con el enfoque Livewire por consistencia con el resto del archivo; solo migrar a
     Alpine si se siente lento.
10. Filtros en el panel de `/Transaction`: `filterDestination` y `filterSubcategory`
    (este último también cascada — mismo criterio que el punto anterior, y limpiándose
    solo si cambia `filterBucket`). Incluir en `filteredQuery()` y en `clearFilters()`.
11. Botón "Ver desglose" en `bucket-index.blade.php` por categoría → deep-link a
    `/Transaction?bucket_id=X` (y opcionalmente `&subcategory_id=Y`), reutilizando
    exactamente el patrón de deep-link que ya existe para Saving Goals
    (`MovementCrud::mount()`, ver `[[project-sfm-savinggoals-2026-07-25]]`) — sin
    construir un mini-reporte nuevo.

### Fase 4 — Alcance ampliado (opcional, después de validar Fases 1–3 en uso real)

12. Reportes agregados: desglose "gasto por Destino" en `SfmDashboard`/`AnnualReport`
    (nuevo widget, no reemplaza el donut existente por `kind`). Solo si el filtro simple de
    la Fase 3 no es suficiente en la práctica.
13. Data Center (`SfmDataCenter.php` + vista): agregar `Destination` y `Subcategory` como
    entidades exportables/importables (mismo patrón que `RecurringExpense`/`SavingGoal`
    se agregaron antes); agregar columnas `destination_id`/`subcategory_id` a
    `Transaction::csvImportFields()/exportRow()`.
14. API móvil: `Http/Resources/{Destination,Subcategory}Resource.php` +
    `Http/Controllers/Api/{Destination,Subcategory}Controller.php` (mismo patrón que
    `BucketController`), agregar `destination_id`/`subcategory_id` a
    `TransactionResource`/`Api\TransactionController` (validación y payload) — recordar el
    bug ya encontrado una vez con `saving_goal_id` faltando en las reglas de update
    (`[[project-sfm-savinggoals-2026-07-25]]`), no repetirlo aquí.

## Archivos que toca este cambio (confirmado por grep, no estimado)

Catálogo/CRUD nuevos: `Models/Destination.php`, `Models/Subcategory.php`,
`Http/Livewire/DestinationIndex.php` + vista, cambios en `BucketIndex.php` +
`bucket-index.blade.php`.

Ya referencian `bucket_id`/`Bucket::` hoy y probablemente necesiten un vistazo en Fase 3–4:
`MovementCrud.php` + `movement-crud.blade.php`, `SfmDashboard.php` + `sfm-dashboard.blade.php`,
`AnnualReport.php`, `BudgetPlanner.php` + `budget-planner.blade.php`, `HouseholdManager.php`,
`SavingsPlan.php`, `SfmDataCenter.php` + `sfm-data-center.blade.php`,
`RecurringExpenseIndex.php`, `BankImportWizard.php`, `PdfExportController.php`,
`Api/BucketController.php`, `Api/TransactionController.php`, `BucketResource.php`,
`TransactionResource.php`.

No todos requieren cambios — la mayoría solo si se decide incluir Destino/Subcategoría en
esa pantalla específica. Las Fases 1–3 son el corazón del pedido del usuario (clasificar
bien un movimiento y poder filtrar/ver desglose); la Fase 4 es expansión posterior.

## Revisión completa de pantallas (2026-08-06)

Se leyó el código real de las 16 pantallas del módulo (`pages.json`) para decidir, con
evidencia concreta, cuáles necesitan ajuste. Veredictos:

| Pantalla | Veredicto | Evidencia |
|---|---|---|
| `SfmDashboard.php` (KPIs, donut, top buckets, presupuesto) | Sin impacto en Fase 1–3 | El donut agrupa por `kind` de `Bucket` (líneas 610–626), no por bucket individual — a propósito, ver `[[project-sfm]]` ("evita redundancia"). `budgetData()` (líneas 180–280) agrega por `bucket_id`. Ninguno de los dos necesita Destino/Subcategoría para seguir funcionando igual. Desglose por Destino queda en Fase 4 (opcional). |
| **`SfmDashboard::openPay()`/`recordPayment()`** (modal "Pagar" de gastos fijos, líneas 103–152) | **Ajuste necesario, mover a Fase 3** | Crea `Transaction` a mano con `bucket_id` (línea 140) fuera de `MovementCrud`. Si no se le agrega también el select de Destino, cualquier pago de gasto fijo hecho desde el Dashboard queda permanentemente sin Destino salvo que el usuario lo edite después en `/Transaction` — inconsistencia día uno. |
| **`RecurringExpenseIndex.php`** (`openPay`/`recordPayment` líneas 174–230, y `createExpense`/`updateExpense`/`openEdit` líneas 234–296) | **Ajuste necesario, mover a Fase 3** | Mismo patrón duplicado que el punto anterior — es literalmente el mismo modal de pago, código separado. Además `newBucketId`/`editBucketId` (líneas 33, 48) permiten pre-asignar Bucket al gasto fijo mismo; tiene sentido permitir pre-asignar Destino ahí también (ej. "Internet" fijo → Destino "Casa" siempre), no solo en el momento de pagar. |
| **`HouseholdManager::recordPayment()`** (líneas 345–389) | **Ajuste necesario, mover a Fase 3 (no Fase 4)** | Cuando `payLinkTransaction` está activo, crea un `Transaction` con `bucket_id` (línea 366) pero SIN pasar por `MovementCrud`. Este es exactamentente el caso de uso que motivó Destino ("cuánto destiné a mi Fondo Familiar/Padre") — dejarlo fuera de Fase 3 significa que los pagos de grupo, el escenario principal que dio origen a la idea, nunca podrían llevar Destino sin editarlos después. `HouseholdExpense`/`HouseholdPayment` en sí (el registro de "quién debe/pagó qué" dentro del grupo) no necesitan campo propio — el concepto de "destino" ya lo da el grupo (`household_id`) al que pertenece el gasto; solo la `Transaction` vinculada opcionalmente necesita el select. |
| `CreditCardIndex.php`, `CardCycleIndex.php`, `WalletIndex.php`, `ExchangeRateIndex.php`, `WalletLedger.php`, `Test.php` | Sin impacto — confirmado | Ninguno de estos 6 archivos referencia `bucket_id` ni `Bucket::` (confirmado por grep sobre todo `Http/` y `Resources/views/`); Destino/Subcategoría viven en `Transaction`/`Bucket`, no tienen ningún punto de contacto aquí. |
| `SavingsPlan.php` (barras 50/30/20) | Sin impacto | Agrupa `Transaction` por `bucket_id` → `kind` únicamente (líneas 38–69, `Bucket::KINDS`). Ni Destino ni Subcategoría alteran esa agregación. |
| `SavingGoalsIndex.php` | Sin impacto | No referencia `bucket_id` en ningún punto del archivo; las metas se enlazan por `saving_goal_id`, un eje totalmente distinto. El botón "Ver desglose" con Destino que se pidió es para Buckets, no para Goals — no hace falta replicar el deep-link aquí. |
| `AnnualReport.php` | Sin impacto en Fase 1–3 | `topBuckets` (líneas 98–107) agrega por bucket, no por destino. Un desglose anual por Destino es exactamente el tipo de ítem ya listado en Fase 4, punto 12 — no se encontró urgencia adicional. |
| `CashFlowProjection.php` | Sin impacto — confirmado | Solo distingue movimientos por `recurring_expense_id`/`saving_goal_id` nulos para estimar "gasto variable" (líneas 80–85); no toca `bucket_id` en ningún punto. |
| `BudgetPlanner.php` | Sin impacto | Presupuesto mensual es por `bucket_id` + `kind` (vía `Bucket::budgetProgress()`); no hay caso de uso pedido por el usuario para presupuestar por Destino o Subcategoría — abrirlo sería alcance no pedido. |
| **`BankImportWizard::runImport()`** (línea 289) | Sin impacto — confirmado, coherente con el patrón actual | Crea cada `Transaction` con `bucket_id: null` siempre — el wizard NUNCA asigna categoría por fila hoy; el usuario reconcilia manualmente después en `/Transaction`. Agregar Destino/Subcategoría al wizard sería más trabajo del que el propio Bucket recibe hoy — fuera de alcance, coherente con el patrón existente. |
| `PdfExportController::transactions()` | Ajuste menor, no urgente | Ya arma `$byBucket` (líneas 38–47) para el PDF; agregar Destino/Subcategoría como columna es un `with()` + un `groupBy` adicional, trivial de hacer junto con Fase 3 pero no bloquea nada si se deja para después. |
| **`SfmDataCenter.php`** | Ajuste necesario, ya previsto en Fase 4 — puntos exactos confirmados | `RECOMMENDED_BUCKETS` (líneas 44–53, sin tocar), `exportAllSections()` con un bloque `if ($this->canExport(...))` por entidad (líneas 283–345, agregar bloques `destination`/`subcategory`), `downloadTemplate()` con el array `$config` por entidad (líneas 414–439, agregar `destination`/`subcategory`). Ningún hallazgo nuevo, confirma lo que Fase 4 ya decía. |
| **`SfmManual.php`** (`sections()`, líneas 14–31) | Ajuste menor | Agregar una entrada `'destination' => [...]` al array (mismo formato que `exchange_rate`, grupo `advanced`). Subcategoría NO necesita entrada propia — se documenta como sub-sección dentro del texto de `bucket`, ya que vive anidada en esa pantalla (Fase 2, punto 6). |
| **`Api/TransactionController.php` + `TransactionResource.php`** | Ajuste necesario, ya previsto en Fase 4 — puntos exactos confirmados | Reglas de validación en `store()` (líneas 52–63) y `update()` (líneas 114–124): falta `destination_id`/`subcategory_id` en ambos arrays, más el payload de `Transaction::create()`/`update()` y los 4 `with()`/`load()`/`fresh()` que hoy solo cargan `bucket` (líneas 22, 93, 101, 149). `TransactionResource::toArray()` (líneas 12–34) necesita las claves `destination_id`/`destination_name`/`subcategory_id`/`subcategory_name`, mismo patrón que `bucket_id`/`bucket_name`. No se encontraron controllers/resources adicionales — la API de SFM solo tiene `Wallet`/`Bucket`/`Transaction` hoy (no hay endpoints para `RecurringExpense`, `SavingGoal`, etc.), así que Destino y Subcategoría necesitan sus propios `Http/Controllers/Api/{Destination,Subcategory}Controller.php` + `Resource.php` nuevos si se quiere exponerlos individualmente (no solo embebidos en `TransactionResource`). |
| `module.json` / `pages.json` | Ya cubierto en Fase 1–2, sin cambios adicionales | Confirmado: `permissions` solo necesita `destination`/`subcategory`; `pages.json` solo necesita la entrada de Destinos (Fase 2, punto 5) — no hay una entrada de menú de Subcategorías porque vive anidada en Buckets. |
| `es.json` del módulo | Ajuste menor, no bloqueante | Nota curiosa: `"Bucket": "Cuenta"` (línea 98) — el UI en español ya le dice "Cuenta" a Bucket, no "Categoría". Al nombrar el campo nuevo conviene mantener consistencia con esa etiqueta existente en vez de "Categoría"/"Subcategoría" a secas, para no confundir. Decisión de copy, no de arquitectura. |

**Cambio de prioridad resultante**: los tres flujos que crean `Transaction` manualmente
fuera de `MovementCrud` (`SfmDashboard::recordPayment`, `RecurringExpenseIndex::recordPayment`,
`HouseholdManager::recordPayment`) se mueven de "no mencionados" a **Fase 3**, agregando el
select de Destino (no Subcategoría — para gastos fijos/pagos de grupo la Subcategoría es
poco útil y añadiría fricción sin el beneficio de cascada visual que sí tiene en el modal
principal). El punto 8 de Fase 3 se actualiza para cubrir los 3 lugares.

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

- Nombre visible: **"Destino"**. Ya aplicado en `es.json`; de paso se corrigió una key
  `"Destination": "Destinatario"` que no se usaba en ningún lugar del código (leftover de
  antes de este feature) y se eliminó un duplicado `"Add Destination"` que sobraba de la
  característica de Tags ya eliminada del módulo (`2026_05_17_600000_drop_sfm_tag_tables.php`).
- **Sí** se sembraron Destinos sugeridos, pero como botón manual "Aplicar sugeridos" en
  `/Destination` (`DestinationIndex::applyRecommended()`), solo visible si el usuario no
  tiene ningún Destino creado todavía. Nunca se ejecuta automáticamente.
- `sfm_subcategories.user_id` → **NOT NULL**. La migración `2026_08_06_100001` hace backfill
  desde `bucket.user_id` antes de aplicar la constraint (no debería tocar nada real ya que
  la tabla nunca se usó desde código).
- Índice único `(bucket_id, name)` en subcategorías → **agregado**. Duplicados se
  previenen también en el modal (regla `Rule::unique(...)->where('bucket_id', ...)`).
- **Permiso de Subcategoría**: se decidió NO crear un permiso `subcategory.*` propio —
  vive gateada por `bucket.update` ya que solo es accesible desde el modal anidado en
  `/Bucket`, consistente con que no tiene ruta ni ítem de menú propios.

## Incidente al migrar (2026-08-06) — corregido

Primer intento de `php artisan migrate` falló: `sfm_destinations` "already exists". Causa:
un intento previo había creado la tabla pero falló después en el `ALTER TABLE
sfm_transactions ADD destination_id` (el DDL de MySQL no es transaccional, así que la
tabla quedó creada sin que Laravel registrara la migración como corrida). Al investigar
también se descubrió que la premisa sobre `sfm_subcategories` era incorrecta (ver arriba,
sección Objetivo) — la tabla no existía en absoluto, había sido eliminada en 2026-05-24.

Ambas migraciones (aún no corridas, `migrate:status` las mostraba `Pending`) se reescribieron:
- `2026_08_06_100000_create_sfm_destinations_table.php` — ahora usa `Schema::hasTable()`/
  `hasColumn()` como guardas, así puede re-ejecutarse sin importar en qué punto haya
  quedado a medias un intento anterior. `destination_id` se posiciona `->after('bucket_id')`
  (ya no `->after('subcategory_id')`, para no depender del orden entre esta migración y la
  de subcategorías).
- `2026_08_06_100001_complete_sfm_subcategories.php` → renombrada a
  `2026_08_06_100001_recreate_sfm_subcategories.php` — ya no es un backfill sobre datos
  existentes (no hay ninguno), es una recreación completa de la tabla + columna, con
  `user_id` NOT NULL y el índice único desde el `CREATE TABLE` original, sin pasos previos.

## Implementado — Fase 1 (2026-08-06)

- `Database/migrations/2026_08_06_100000_create_sfm_destinations_table.php` — tabla
  `sfm_destinations` (`user_id`, `name`, `icon`, `is_default`, único `(user_id, name)`) +
  columna `sfm_transactions.destination_id`. Idempotente, ver incidente arriba.
- `Database/migrations/2026_08_06_100001_recreate_sfm_subcategories.php` — recrea la tabla
  eliminada en 2026-05-24 + columna `subcategory_id`, con `user_id` NOT NULL y único
  `(bucket_id, name)` desde el `CREATE TABLE`. Idempotente, ver incidente arriba.
- `Models/Destination.php`, `Models/Subcategory.php` — nuevos, mismo patrón que `Bucket`
  (global scope owner, `WithAuditLog`, `Importable`).
- `Models/Transaction.php` — `subcategory_id`/`destination_id` en `$fillable`, relaciones
  `subcategory()`/`destination()`.
- `module.json` — permiso `destination` agregado (subcategoría reutiliza `bucket`, ver
  decisión arriba).

## Implementado — Fase 2 (2026-08-06)

- `Http/Controllers/DestinationController.php` + `Http/Livewire/DestinationIndex.php` +
  vistas (`Destination/DestinationIndex.blade.php`, `livewire/destination-index.blade.php`)
  — CRUD completo, mismo patrón que `BucketIndex` (sin `kind`). Botón "Aplicar sugeridos"
  (`RECOMMENDED` const: Yo/Casa/Padre/Fondo Familiar/Iglesia, vía `firstOrCreate`, solo
  visible si el catálogo está vacío).
- Ruta `GET /Destination` → `destination.index`; entrada en `pages.json` (submenu
  `Advanced`, icon `o-map-pin`).
- `Http/Livewire/BucketIndex.php` — nuevas propiedades/métodos para subcategorías
  anidadas: `openSubcategories()`, `addSubcategory()`, `openEditSubcategory()`,
  `updateSubcategory()`, `deleteSubcategory()`. `render()` agrega `subcategoryCounts`
  (badge por bucket en la tabla) y la lista de subcategorías del bucket abierto.
- `bucket-index.blade.php` — botón nuevo en la fila de cada bucket (icono
  `o-rectangle-stack`, resaltado si ya tiene subcategorías) + modal con lista
  editable/eliminable inline y form de alta rápida.
- `Resources/lang/es.json` — ~20 claves nuevas (Destino y Subcategorías), sin pisar
  ninguna existente (validado por script, 0 duplicados tras la limpieza mencionada arriba).

## Implementado — Fase 3 (2026-08-06)

- `MovementCrud.php` + `movement-crud.blade.php`: selects de Destino y Subcategoría
  (cascada real — `newMovementBucket`/`editMovementBucket` ahora `wire:model.live`,
  `subcategoryOptionsFor()` calcula las opciones del Bucket actual, el campo no se
  renderiza si está vacío) en los 4 puntos donde ya vivía el select de Bucket (bloque
  transferencia + bloque normal, Crear y Editar). Filtros `filterDestination`/
  `filterSubcategory` (este último también condicional al Bucket filtrado, se limpia solo
  si cambia `filterBucket`). Deep-link nuevo en `mount()`: `?bucket_id=` (+ opcional
  `&subcategory_id=`) reutiliza el mismo patrón que el deep-link de Metas de Ahorro.
  Columnas nuevas `Subcategory`/`Destination` en la tabla y en los exports XLSX/CSV/PDF
  propios de `/Transaction` (no en Data Center, eso sigue en Fase 4).
- `bucket-index.blade.php` + `BucketIndex.php`: botón "Ver desglose" por Bucket
  (`o-chart-pie`) y por cada subcategoría dentro del modal, ambos deep-link a
  `/Transaction` con los filtros ya aplicados.
- **Los 3 flujos secundarios que crean `Transaction` sin pasar por `MovementCrud`**
  (hallazgo de la revisión de pantallas) ganaron el select de Destino — no Subcategoría,
  según lo decidido: `SfmDashboard::openPay()`/`recordPayment()`,
  `RecurringExpenseIndex::openPay()`/`recordPayment()`, y
  `HouseholdManager::recordPayment()` (solo cuando `payLinkTransaction` está activo).
- `RecurringExpense` ganó su propio `destination_id` (migración
  `2026_08_06_100002_add_destination_id_to_sfm_recurring_expenses.php`, mismo patrón que
  `bucket_id`) para pre-asignarlo una sola vez en el gasto fijo — `quickPayExpense()` en
  `MovementCrud` y `openPay()` en los otros dos flujos lo heredan automáticamente al pagar.
- `SfmCache::forgetDestinations()` nuevo, enganchado al `booted()` de `Destination` — mismo
  patrón de invalidación de caché que ya tenían Buckets/Cards/Recurring.
- Verificado: `php -l` en los 16 archivos tocados, `es.json` sin duplicados (661 claves), y
  **suite completa de tests corrida contra `lara_roles_test`** (BD aislada, no la real) —
  164 passed, incluye los tests propios de SFM (`DataCenterExportTest`,
  `DataIsolationTest`) sin romper nada.

## Pasos ejecutados por el usuario

1. ✅ `php artisan migrate` — corrido, con un incidente en el primer intento (ver sección
   arriba) ya corregido y re-verificado limpio.
2. ✅ `php artisan module:sync-permissions SimpleFinancialManagement` — corrido, crea
   `destination.*` (subcategoría no tiene permiso propio, ver decisión arriba).
3. Pendiente: verificar visualmente en el navegador — crear un Destino (o usar "Aplicar
   sugeridos"), asignarlo a un movimiento y a un gasto fijo, filtrar por él; crear una
   subcategoría en un Bucket y confirmar que el campo solo aparece en el modal para ese
   Bucket específico, y que "Ver desglose" lleva a `/Transaction` ya filtrado.

Recordatorio activo: `[[feedback-never-touch-real-db]]` — todas las migraciones las corrió
el usuario mismo contra su BD real; yo solo verifiqué con comandos de solo lectura y la
suite de tests contra la BD aislada.

## Implementado — Fase 4 (2026-08-06)

Sin migraciones nuevas — solo código sobre lo ya migrado en Fases 1-3.

- **Data Center** (`SfmDataCenter.php` + vista): `Destination` y `Subcategory` agregadas
  como entidades exportables/importables (CSV/XLSX + plantilla descargable), mismo patrón
  que `RecurringExpense`/`SavingGoal`. Incluidas en "Exportar todo" (`exportAllSections()`).
  Subcategoría reutiliza el permiso `bucket.export`/`bucket.create` (sin uno propio, misma
  decisión que en la web). `Transaction::csvImportFields()/csvImportTransform()/exportRow()`
  ganaron `subcategory_id`/`destination_id` con resolución de nombre → ID (la subcategoría
  se resuelve dentro del `bucket_id` ya resuelto de esa fila, porque el nombre es único por
  categoría, no global).
- **API móvil**: `Http/Resources/{Destination,Subcategory}Resource.php` +
  `Http/Controllers/Api/{Destination,Subcategory}Controller.php` nuevos (mismo patrón que
  `BucketController`), rutas `sfm/destinations` y `sfm/subcategories` en `Routes/api.php`.
  `TransactionResource`/`Api\TransactionController` ganaron `destination_id`/
  `destination_name`/`subcategory_id`/`subcategory_name` — validación en `store()` **y**
  `update()`, y las 4 cargas de relación (`with()`/`load()`×2/`fresh()`) actualizadas para
  no repetir el bug que ya se encontró una vez con `saving_goal_id`
  (`[[project-sfm-savinggoals-2026-07-25]]`).
- **Reportes agregados**: widget "Top Destinos" nuevo en `SfmDashboard` (respeta los 3
  modos Mes/Año/General, mismo patrón que "Top Buckets" vía `withSum` sobre
  `Destination::transactions()`) y en `AnnualReport` (barra de progreso por destino, mismo
  patrón que su "Top buckets" anual). Ambos **solo se muestran si hay datos** — no
  aparecen para quien no usa el campo Destino, sin agregar ruido.
- Verificado: `php -l` en los 10 archivos nuevos/tocados, `es.json` sin duplicados (665
  claves), y **suite completa vuelta a correr** (164 passed) tras cerrar la Fase 4.
