Producto: Aplicación web personal de control financiero integral.
Usuario: Único (Fernando Rodríguez Memije) — sin necesidad de sistema de autenticación multi-usuario en MVP.
Problema: Llevar un control completo de finanzas personales adaptado al ciclo quincenal mexicano, con seguimiento de deudas, ingresos variables (empleo + freelance), presupuesto por categorías y visualización histórica.
Stack: Next.js 14+ (App Router), PostgreSQL (Docker), Prisma ORM, Tailwind CSS, desplegable en Vercel o servidor local.
| Campo | Tipo | Descripción |
|---|---|---|
| id | UUID | PK |
| name | String | Nombre de la fuente (ej: "TerSoft", "Freelance") |
| defaultAmount | BigInt | Monto habitual por periodo (en centavos) |
| frequency | Enum | QUINCENAL, MENSUAL, SEMANAL, VARIABLE |
| type | Enum | EMPLOYMENT, FREELANCE, OTHER |
| isActive | Boolean | Si la fuente está activa |
| createdAt | DateTime | |
| updatedAt | DateTime |
| Campo | Tipo | Descripción |
|---|---|---|
| id | UUID | PK |
| type | Enum | INCOME, EXPENSE |
| amount | BigInt | Monto en centavos (siempre positivo, ej: $150.00 = 15000) |
| description | String? | Descripción opcional |
| categoryId | UUID | FK a Category |
| incomeSourceId | UUID? | FK a IncomeSource (solo si type=INCOME) |
| date | Date | Fecha de la transacción |
| paymentMethod | Enum? | EFECTIVO, DEBITO, CREDITO, TRANSFERENCIA |
| periodId | UUID | FK a Period |
| notes | String? | Notas adicionales |
| createdAt | DateTime | |
| updatedAt | DateTime |
| Campo | Tipo | Descripción |
|---|---|---|
| id | UUID | PK |
| name | String | Nombre (ej: "Comida") |
| icon | String | Nombre del ícono de Lucide (ej: "utensils", "zap") |
| color | String | Color hex para gráficas |
| type | Enum | EXPENSE, INCOME, BOTH |
| isDefault | Boolean | Si es categoría del sistema |
| isActive | Boolean | |
| sortOrder | Int | Orden de aparición |
Categorías iniciales (seed):
| Nombre | Ícono (Lucide) | Color | Tipo |
|---|---|---|---|
| Comida | utensils | #fb923c | EXPENSE |
| Servicios | zap | #60a5fa | EXPENSE |
| Entretenimiento | clapperboard | #a78bfa | EXPENSE |
| Suscripciones | smartphone | #f472b6 | EXPENSE |
| Transporte | car | #fbbf24 | EXPENSE |
| Otros | package | #94a3b8 | EXPENSE |
| Empleo | briefcase | #34d399 | INCOME |
| Freelance | laptop | #22d3ee | INCOME |
| Campo | Tipo | Descripción |
|---|---|---|
| id | UUID | PK |
| name | String | Nombre descriptivo |
| type | Enum | CREDIT_CARD, PERSONAL_LOAN, AUTO_LOAN, OTHER |
| currentBalance | BigInt | Saldo actual (en centavos) |
| creditLimit | BigInt? | Solo para tarjetas de crédito (en centavos) |
| annualRate | Int | Tasa de interés anual en basis points (ej: 4500 = 45.00%) |
| minimumPayment | BigInt? | Pago mínimo en centavos (tarjetas) |
| monthlyPayment | BigInt? | Mensualidad fija en centavos (préstamos) |
| originalAmount | BigInt? | Monto original en centavos (préstamos) |
| remainingMonths | Int? | Meses restantes (préstamos) |
| cutOffDay | Int? | Día de corte (tarjetas, 1-31) |
| paymentDueDay | Int? | Día límite de pago (tarjetas, 1-31) |
| isActive | Boolean | |
| createdAt | DateTime | |
| updatedAt | DateTime |
Campos calculados (no almacenados, derivados en la app):
utilizationRate: currentBalance / creditLimit (solo tarjetas)estimatedMonthlyInterest: currentBalance * (annualRate / 12)percentPaid: 1 - (currentBalance / originalAmount) (solo préstamos)totalRemainingPayment: monthlyPayment * remainingMonths (solo préstamos)
| Campo | Tipo | Descripción |
|---|---|---|
| id | UUID | PK |
| categoryId | UUID | FK a Category |
| quincenalAmount | BigInt | Monto asignado por quincena (en centavos) |
| periodId | UUID | FK a Period |
| createdAt | DateTime | |
| updatedAt | DateTime |
Campos calculados:
monthlyAmount: quincenalAmount * 2semesterAmount: monthlyAmount * 6annualAmount: monthlyAmount * 12spent: SUM(transactions del periodo en esa categoría)remaining: monthlyAmount - spentpercentUsed: spent / monthlyAmount
| Campo | Tipo | Descripción |
|---|---|---|
| id | UUID | PK |
| month | Int | Mes (1-12) |
| year | Int | Año |
| startDate | Date | Primer día del mes |
| endDate | Date | Último día del mes |
| isClosed | Boolean | Si el periodo está cerrado |
| closedAt | DateTime? | Fecha en que se cerró |
| createdAt | DateTime |
Constraint: UNIQUE(month, year)
| Campo | Tipo | Descripción |
|---|---|---|
| id | UUID | PK |
| periodId | UUID | FK a Period (UNIQUE) |
| totalIncome | BigInt | Total ingresos del mes (en centavos) |
| totalExpenses | BigInt | Total gastos del mes (en centavos) |
| totalSavings | BigInt | Ingresos - Gastos (en centavos) |
| savingsRate | Int | Tasa de ahorro en basis points (ej: 2050 = 20.50%) |
| debtAtClose | BigInt | Deuda total al cierre (en centavos) |
| debtPayments | BigInt | Pagos a deudas en el mes (en centavos) |
| notes | String? | Notas del cierre |
| createdAt | DateTime |
Permite rastrear activos en unidades distintas a MXN (UDI, UMA, USD, etc.) con conversión automática vía proveedores de tasa.
| Campo | Tipo | Descripción |
|---|---|---|
| id | UUID | PK |
| code | String | Código único (ej: "UDI", "UMA", "USD"). UNIQUE |
| name | String | Nombre legible (ej: "Unidad de Inversión") |
| precision | Int | Decimales nativos de la unidad (UDI=6, UMA=2, USD=2) |
| symbol | String? | Símbolo de display (ej: "UDI", "US$") |
| providerUrl | String? | URL del API para obtener la tasa (ej: Banxico API) |
| providerPath | String? | JSONPath para extraer el valor de la respuesta del API |
| providerHeaders | Json? | Headers requeridos por el API (ej: token de Banxico) |
| refreshInterval | Int | Intervalo de refresco en horas (UDI=24, UMA=8760 para anual) |
| isActive | Boolean | |
| createdAt | DateTime | |
| updatedAt | DateTime |
Unidades iniciales (seed):
| Código | Nombre | Precisión | Refresh | Provider |
|---|---|---|---|---|
| MXN | Peso Mexicano | 2 | — | Moneda base, no requiere API |
| UDI | Unidad de Inversión | 6 | 24h | Banxico SIE API (serie SP68257) |
| UMA | Unidad de Medida y Actualización | 2 | 8760h (anual) | INEGI / DOF |
Historial de tasas de conversión de cada unidad a MXN centavos.
| Campo | Tipo | Descripción |
|---|---|---|
| id | UUID | PK |
| unitId | UUID | FK a ValueUnit |
| date | Date | Fecha de la tasa |
| rateToMxnCents | BigInt | Cuántos centavos MXN equivale 1 unidad entera (ej: 1 UDI ≈ 829 centavos → 829) |
| rateRaw | String | Valor original tal como lo devolvió el API (para auditoría) |
| source | String | Origen: "api", "manual" |
| createdAt | DateTime |
Constraint: UNIQUE(unitId, date)
Ejemplo de cálculo:
- Tengo 50,000.123456 UDIs almacenados como
50000123456(precision=6, o sea amount / 10^6) - Tasa del día: 1 UDI = $8.29 MXN → rateToMxnCents = 829
- Valor en MXN centavos:
(amount * rateToMxnCents) / 10^precision=(50000123456 * 829) / 1000000=41450102centavos = $414,501.02 MXN
Cualquier cosa cuyo valor se rastrea en una unidad distinta a MXN.
| Campo | Tipo | Descripción |
|---|---|---|
| id | UUID | PK |
| name | String | Nombre descriptivo (ej: "PPR Profuturo", "CETES 28d") |
| unitId | UUID | FK a ValueUnit |
| amount | BigInt | Cantidad en la subdivisión mínima de la unidad (según precision) |
| category | Enum | PPR, INVESTMENT, SAVINGS, CRYPTO, OTHER |
| institution | String? | Institución (ej: "GBM", "Profuturo", "Cetesdirecto") |
| notes | String? | |
| isActive | Boolean | |
| createdAt | DateTime | |
| updatedAt | DateTime |
Campos calculados (no almacenados):
displayAmount: amount / 10^unit.precision (ej: 50000123456 / 10^6 = 50,000.123456 UDIs)mxnValue: (amount × latestRate.rateToMxnCents) / 10^unit.precision (en centavos MXN)mxnDisplay: formatMoney(mxnValue)
KPIs superiores (cards):
- Ingreso mensual (calculado de fuentes activas)
- Gastos del mes actual (SUM transacciones EXPENSE del periodo actual)
- Disponible (ingreso - gastos)
- Deuda total (SUM saldos de deudas activas)
- Tasa de ahorro (disponible / ingreso, en %)
- Ratio deuda/ingreso (deuda total / ingreso mensual, en %)
Gráficas:
- Barras horizontales: Presupuesto vs Gastado por categoría (mes actual)
- Área/Línea: Tendencia 6 meses (ingresos vs gastos) — usa datos de MonthlySummary
- Dona/Pie: Distribución de gastos por categoría (mes actual)
Últimos movimientos: Lista de las 5-8 transacciones más recientes con ícono de categoría, descripción, fecha y monto.
Formulario rápido (objetivo: < 30 segundos por registro):
- Tipo: toggle Gasto/Ingreso
- Monto: input numérico con prefijo $
- Categoría: selector con íconos (si es gasto) o fuente de ingreso (si es ingreso)
- Descripción: input texto (opcional)
- Fecha: date picker, default a hoy
- Forma de pago: selector (Efectivo, Débito, Crédito, Transferencia) — solo para gastos
- Notas: textarea corto (opcional, colapsado por default)
Lista de transacciones:
- Vista tabla/lista del mes actual
- Filtros: por categoría, tipo, rango de fechas, forma de pago
- Ordenamiento: por fecha (default desc), monto
- Acción rápida: eliminar con confirmación
- Editar: abre el mismo formulario precargado
Vista por deuda (card expandible):
- Nombre y tipo (ícono diferenciado)
- Saldo actual (editable inline o vía modal)
- Para tarjetas: barra de utilización (verde < 30%, naranja 30-70%, rojo > 70%), pago mínimo, fechas corte/pago, interés mensual estimado
- Para préstamos: barra de progreso (% pagado), mensualidad, meses restantes, total por pagar
- Botón para actualizar saldo manualmente
Resumen:
- Deuda total
- Pago mensual total de deudas
- Ratio deuda/ingreso
Funcionalidades futuras (no MVP):
- Simulador de estrategia de pago (avalancha vs bola de nieve)
- Alertas de fechas de corte/pago
Configuración:
- Tabla editable con cada categoría
- Input principal: monto quincenal
- Columnas calculadas: mensual (×2), semestral (×6), anual (×12)
- Fila de total
- Comparación: ingreso quincenal vs total presupuestado quincenal → sobrante/faltante
Vista de avance (mes actual):
- Barra de progreso por categoría con % usado
- Color dinámico: verde (< 80%), naranja (80-100%), rojo (> 100%)
- Monto gastado vs presupuestado
Vistas temporales:
- Toggle para ver presupuesto en modo quincenal, mensual o anual
Lista de fuentes:
- Cards con nombre, monto, frecuencia
- Editable
- Para frecuencia quincenal: mostrar equivalente mensual
- Para variable: mostrar promedio de los últimos 3 meses
Resumen:
- Ingreso quincenal estimado
- Ingreso mensual estimado
- Ingreso semestral y anual
Tabla pivote:
- Filas: Ingresos, Gastos, Ahorro, % Ahorro, Deuda (cierre), Pagos a deudas
- Columnas: Ene-Dic + Total Anual
- Se llena automáticamente con datos de MonthlySummary
- Ahorro y % Ahorro son calculados
Cierre de mes:
- Botón "Cerrar mes" que:
- Calcula totales del periodo
- Crea/actualiza el registro MonthlySummary
- Marca el periodo como cerrado (isClosed = true)
- Crea el siguiente periodo si no existe
- Un periodo cerrado no se puede editar (las transacciones quedan bloqueadas) a menos que se reabra explícitamente
Vista principal:
- Lista de activos como cards, cada una muestra: nombre, institución, cantidad en unidad nativa (ej: "50,000.12 UDIs"), equivalente MXN al tipo de cambio más reciente, y fecha de última actualización de tasa
- Total de portafolio en MXN (suma de todos los activos convertidos)
- Indicador de cambio desde la última actualización de tasa
CRUD de activos:
- Formulario: nombre, unidad de valor (selector), cantidad (input en formato de la unidad), categoría (PPR, Inversión, Ahorro, Crypto, Otro), institución (opcional)
- Editar cantidad manualmente (ej: al recibir estado de cuenta del PPR)
Administración de unidades (Settings):
- Lista de ValueUnits configuradas
- Formulario para agregar/editar unidad: código, nombre, precisión decimal, URL del API, JSONPath para extraer la tasa, headers del API, intervalo de refresco
- Botón "Probar conexión" que hace un request al API y muestra la tasa extraída
- Botón "Actualizar tasa ahora" por unidad
- Historial de tasas por unidad (tabla con fecha, valor, fuente)
Actualización automática de tasas:
- Cron job o API route invocable (para llamar desde un cron externo o Vercel Cron) que recorre todas las ValueUnits activas cuya última tasa tenga más horas que su refreshInterval
- Fetch al providerUrl, extrae el valor con providerPath, almacena en UnitRate
- Log de éxito/error por unidad
- Administración de unidades de valor (ver 3.7)
- Administración de categorías personalizadas
- Exportar/importar datos (JSON)
Sidebar fija (desktop) / Bottom tab bar (mobile):
- Dashboard (ícono: LayoutDashboard)
- Movimientos (ícono: Receipt)
- Deudas (ícono: CreditCard)
- Presupuesto (ícono: Target)
- Ingresos (ícono: Wallet)
- Activos (ícono: TrendingUp) — v2.0
- Historial (ícono: BarChart)
- Configuración (ícono: Settings) — v2.0
Header:
- Nombre del periodo actual (ej: "Abril 2026")
- Botón flotante "+" para registro rápido de transacción (siempre visible)
- Selector de periodo (para navegar meses previos en modo solo lectura)
- El ingreso base se captura como monto quincenal y se multiplica ×2 para el mensual
- El presupuesto se define por quincena como unidad base
- La app no separa primera y segunda quincena en MVP; el presupuesto es mensual (quincenal × 2) pero el input es quincenal para coincidir con el flujo de cobro
- Un periodo = un mes calendario (día 1 al último día del mes)
- El periodo actual se crea automáticamente si no existe al abrir la app
- Solo se pueden agregar transacciones al periodo actual (a menos que el anterior esté abierto)
- El cierre de mes genera el snapshot para MonthlySummary
- Los saldos de deudas se actualizan manualmente (no hay conexión bancaria)
- El interés mensual estimado es informativo, no modifica el saldo automáticamente
- La deuda total en el Dashboard es la suma de currentBalance de todas las deudas activas
- Si no hay presupuesto configurado para el periodo actual, se copia el del periodo anterior
- Las categorías de presupuesto incluyen categorías de gasto + "Ahorro/Emergencia" + "Pago Deudas" (estos dos no son categorías de transacción, son asignaciones de presupuesto)
- Toda la app opera en MXN como moneda base
- Almacenamiento: Todos los montos se guardan como
BigInten centavos (ej: $1,500.75 → 150075). Esto elimina errores de punto flotante y permite montos de hasta ~$922 billones - Tasas de interés: Se almacenan como
Inten basis points (ej: 45.00% → 4500). Esto permite representar hasta dos decimales de precisión sin flotantes - Serialización API: Prisma devuelve
BigIntcomo tipo nativo de JS. Los API routes serializan aStringen el JSON response (JSON.stringify no soporta BigInt nativamente). El frontend parseaString → Numbersolo para formateo de display - UI (entrada): El usuario escribe en formato decimal normal ($1,500.75). La conversión a centavos ocurre en la capa de validación/API antes de persistir
- UI (salida): Se divide entre 100 y se formatea con
Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN' }) - API: Los endpoints reciben y devuelven montos como
String(representación del BigInt en centavos). La conversión String ↔ display es responsabilidad exclusiva de la capa de presentación - No se requiere soporte multi-moneda en MVP (pero ver sección 2.9 para sistema de unidades de valor)
- Tema: Dark mode como default (fondo oscuro tipo #0a0f1a, cards #111827)
- Acento principal: Cyan/Teal (#22d3ee)
- Paleta semántica: Verde para ingresos/positivo, Rojo para gastos/deuda/negativo, Naranja para alertas, Azul para información neutral
- Iconografía: Lucide React exclusivamente. No emojis en ninguna parte de la UI. Los íconos de categorías se renderizan dinámicamente por nombre almacenado en BD
- Tipografía: DM Sans o similar sans-serif moderna
- Border radius: 12-16px en cards, 8px en inputs
- Sombras: Sutiles, orientadas a profundidad (no drop shadow genérico)
- Mobile-first
- Breakpoints: móvil (<768px), tablet (768-1024px), desktop (>1024px)
- Sidebar colapsable en móvil, fija en desktop
- Cards de KPI: 2 columnas en móvil, 3-4 en desktop
- Formulario de transacción rápida: full screen modal en móvil, slide-over en desktop
POST /api/transactions → Crear transacción
GET /api/transactions → Listar (con filtros via query params)
PUT /api/transactions/[id] → Actualizar
DELETE /api/transactions/[id] → Eliminar
GET /api/income-sources → Listar fuentes
POST /api/income-sources → Crear
PUT /api/income-sources/[id] → Actualizar
DELETE /api/income-sources/[id] → Eliminar
GET /api/debts → Listar deudas
POST /api/debts → Crear
PUT /api/debts/[id] → Actualizar saldo y datos
DELETE /api/debts/[id] → Eliminar
GET /api/budgets → Presupuesto del periodo actual
POST /api/budgets → Crear/actualizar presupuesto
PUT /api/budgets/[id] → Actualizar categoría individual
GET /api/categories → Listar categorías
POST /api/categories → Crear categoría personalizada
GET /api/periods → Listar periodos
GET /api/periods/current → Periodo actual con resumen
POST /api/periods/[id]/close → Cerrar periodo
GET /api/dashboard → KPIs + datos para gráficas del periodo actual
GET /api/dashboard/trend → Datos de tendencia (últimos 6 meses)
GET /api/history/[year] → Historial anual (MonthlySummary del año)
# ─── v2.0: Activos y Unidades de Valor ───
GET /api/assets → Listar activos con valor MXN calculado
POST /api/assets → Crear activo
PUT /api/assets/[id] → Actualizar (cantidad, datos)
DELETE /api/assets/[id] → Eliminar
GET /api/units → Listar unidades de valor
POST /api/units → Crear unidad personalizada
PUT /api/units/[id] → Actualizar configuración
DELETE /api/units/[id] → Eliminar
POST /api/units/[id]/test → Probar conexión al API del proveedor
POST /api/units/[id]/refresh → Forzar actualización de tasa
GET /api/units/[id]/rates → Historial de tasas
POST /api/cron/refresh-rates → Actualizar tasas vencidas (invocable por cron)
- PostgreSQL en contenedor Docker para desarrollo
- Prisma como ORM con migraciones versionadas
- UUIDs como primary keys (cuid2 generados por Prisma)
- Montos monetarios como
BigInten centavos (mapea abigintde PostgreSQL, 8 bytes). Serialización aStringen JSON responses - Tasas de interés como
Inten basis points (4500 = 45.00%) - Índices: Transaction(periodId, date), Transaction(categoryId), Budget(periodId, categoryId), Debt(isActive), UnitRate(unitId, date), Asset(unitId)
- Zod para validación de inputs en API routes y formularios
- La API recibe montos como String (BigInt centavos). La conversión de la entrada decimal del usuario a centavos String ocurre en la capa de presentación antes de enviar al API
- Montos siempre positivos (≥ 0), el tipo (INCOME/EXPENSE) determina el signo en cálculos
- Server Components por default, Client Components solo donde hay interactividad
- Las gráficas (Recharts) son Client Components
- Los cálculos de KPIs se hacen en queries SQL agregadas, no en el cliente
- ISR no aplica (datos personales, siempre fresh)
- Docker Compose para desarrollo local: app Next.js + PostgreSQL
- Producción: Vercel (app) + PostgreSQL externo, O servidor local con reverse proxy
- Variables de entorno: DATABASE_URL, NEXTAUTH_SECRET (si se agrega auth después)
- Dashboard con KPIs y 3 gráficas
- CRUD de transacciones con formulario rápido
- Control de deudas (tarjeta + préstamo)
- Presupuesto quincenal/mensual con avance
- Ingresos (2 fuentes + configurable)
- Historial anual con cierre de mes
- Categorías predefinidas + personalizables
- Dark mode
- Responsive
- Sistema de unidades de valor (UDI, UMA, USD) con proveedores de tasa configurables
- Tracking de activos e inversiones (PPR en UDIs, CETES, fondos, crypto) con conversión MXN automática
- Cron job para actualización automática de tasas (Vercel Cron o cron externo)
- Autenticación (NextAuth/Clerk) si se comparte el servidor
- PWA con soporte offline
- Notificaciones de fechas de corte/pago
- Importación de estados de cuenta (CSV/PDF parsing)
- Simulador de pago de deudas (avalancha/bola de nieve)
- Metas de ahorro con tracking
- Exportar reportes PDF/Excel
- API para integrar con bots de Telegram/WhatsApp para registro rápido
- Multi-moneda para ingresos freelance en USD