SaaS de control financiero diario para restaurantes
Aplicación web mobile-first para Medina Foods: registro de ingresos y gastos en tiempo real, períodos mensuales con historial navegable y exportación de reportes en Excel.
Contexto / Problema
El dueño de Medina Foods controlaba las finanzas diarias del restaurante en hojas de Excel: un archivo por mes, actualizado a mano, sin forma de saber el saldo disponible en tiempo real. Al cierre del día debía sumar columnas, cruzar categorías y calcular manualmente cuánto quedaba en caja. A fin de mes, reconstruir el período completo tomaba horas.
El problema real no era solo la incomodidad: era la falta de visibilidad. Sin un saldo actualizado al momento, las decisiones de compra y gasto se tomaban “a ojo”. Se necesitaba una herramienta que el dueño pudiera usar desde el teléfono, que mostrara el saldo del mes en tiempo real, y que al cerrar el mes generara automáticamente el reporte para el contador.
Decisiones técnicas
Next.js App Router con Server Components y Server Actions — el fetching de datos ocurre en servidor, sin necesidad de una API REST intermedia. Esto simplifica la arquitectura considerablemente: las acciones de registro van directas a la base de datos, y las páginas de historial se renderizan en servidor con los datos ya presentes. Menos código, menos puntos de falla, menos latencia percibida.
Navegación por query params para el historial — /historial?periodId=xxx permite acceder a cualquier período cerrado sin estado global ni contexto compartido entre rutas. Cada URL es compartible, bookmarkeable y funciona correctamente al refrescar la página. Es la solución más simple que resuelve el problema correctamente.
Decimal(12,2) en MySQL, nunca Float — para valores monetarios el tipo Float produce errores de precisión en punto flotante que se acumulan en cálculos de saldo. Usar Decimal en Prisma mapea a DECIMAL(12,2) en MySQL, garantizando exactitud aritmética en todos los cálculos financieros.
Auth.js con logo en /_next/static/media/ — durante el desarrollo el flujo de login generaba redirects 307 inesperados. La causa: el middleware de Auth.js interceptaba la petición al logo servido desde /public/, porque esa ruta requería autenticación. Moverlo a /_next/static/media/ (servido directamente por Next.js sin pasar por middleware) resolvió el problema sin modificar la lógica de protección de rutas.

Railway para el deploy — CI/CD automático conectado al repositorio. Cada push a main despliega automáticamente. La base de datos MySQL vive en el mismo proveedor, simplificando la configuración de red y la gestión de variables de entorno.
Implementación destacada
Sistema de períodos mensuales
Cada mes es una entidad independiente en la base de datos con estado OPEN o CLOSED. Solo puede existir un período abierto por negocio a la vez. Al cerrarlo, el sistema congela todos sus movimientos — nadie puede editar ni agregar registros a un período cerrado. Al mismo tiempo, abre automáticamente el período siguiente con el saldo de cierre del anterior como saldo inicial.
Este diseño evita un problema clásico en sistemas financieros: la modificación retroactiva de datos históricos que altera reportes ya entregados. Un período cerrado es inmutable por definición del esquema, no por validación de código.

Cálculo de saldo en tiempo real
El saldo disponible del período activo no se almacena como campo: se calcula en cada consulta como saldoInicial + SUM(ingresos) + SUM(aportaciones) − SUM(gastos) − SUM(retiros). Esto elimina el riesgo de inconsistencias entre el saldo almacenado y los movimientos registrados — la única fuente de verdad son los movimientos individuales.
El cálculo ocurre en una query SQL única con GROUP BY tipo, eficiente incluso con cientos de movimientos por período.
Reporte Excel con ExcelJS
El reporte mensual genera un archivo .xlsx con dos hojas: resumen financiero (saldo inicial, totales por tipo, saldo final) y detalle completo de movimientos con fecha, descripción, categoría, método de pago e importe. El archivo se genera en el servidor como stream y se descarga directamente — sin almacenamiento temporal en disco.
El formato visual incluye colores por tipo de movimiento (verde para ingresos, rojo para gastos), totales en negrita y encabezados con el nombre del negocio y el período. El contador del cliente recibe un archivo listo para usar sin procesamiento adicional.
Layout dual mobile / desktop
En móvil el sistema prioriza el flujo de registro rápido: navegación inferior con acceso a las secciones principales y un teclado numérico dedicado para ingresar importes. El flujo completo de registro — tipo, categoría, importe, descripción, método de pago — cabe en una sola pantalla sin scroll.
En desktop el layout cambia a sidebar con panel lateral de registro persistente, pensado para sesiones de revisión más largas donde el dueño o administrador consulta movimientos y registra varios en secuencia.
Resultado / impacto
El dueño de Medina Foods tiene visibilidad del saldo disponible en tiempo real desde el primer día de uso. El cierre mensual bajó de horas de trabajo manual a un clic: selecciona el período, descarga el Excel, lo envía al contador. El historial de cualquier mes anterior es accesible en segundos.
Un beneficio no anticipado: el desglose por categorías reveló patrones de gasto que no eran evidentes en el Excel plano. El dueño identificó una categoría de gasto operativo que representaba más del doble de lo que estimaba mentalmente.
Desafíos del desarrollo
El diseño del esquema de períodos requirió varias iteraciones. La primera versión relacionaba los movimientos directamente con fechas, sin entidad de período explícita. El problema: calcular el saldo de un mes con fecha de inicio variable (¿el 1 del mes? ¿el día que se abrió?) generaba ambigüedades. Introducir Period como entidad de primera clase con fechas explícitas de apertura y cierre resolvió la ambigüedad y simplificó todas las queries.
La validación de formularios con React Hook Form + Zod requirió atención especial en los campos de importe: el input recibe texto del teclado numérico, pero Zod necesita validar un número con dos decimales. El schema transforma el string a número antes de validar, evitando errores de tipo silenciosos en el submit.
Aprendizajes
El mayor aprendizaje fue sobre diseño de sistemas financieros: la inmutabilidad de datos históricos no es un detalle de implementación, es un requisito de negocio. Un sistema que permite editar movimientos de meses cerrados genera desconfianza — el usuario no puede saber si el reporte que imprimió ayer sigue siendo correcto hoy.
Diseñar la restricción como parte del esquema (períodos CLOSED sin endpoint de modificación) es más robusto que implementarla solo como validación de UI. La base de datos es el guardián final de la integridad de los datos.