Modelo de base de datos
La aplicación usa IndexedDB en el navegador. Es una base local, mínima y sin servidor.
Resumen
- Base:
qofre_finanzas - Versión:
3 - Stores:
categories,transactions,budgets - Moneda de interfaz:
PEN - Fechas: texto ISO
YYYY-MM-DD - Fechas de creación: texto ISO completo, por ejemplo
2026-05-13T12:00:00.000Z
Diagrama lógico
categories
id PK
name UNIQUE
createdAt
categories 1 ---- * transactions
categoryId FK lógica
categories 1 ---- * budgets
categoryId FK lógica
budgets compara su monto contra transactions:
transactions.type = "expense"
transactions.categoryId = budgets.categoryId
transactions.date >= budgets.startsOn
transactions.date < finDelPeriodo(budgets.period, budgets.startsOn)
IndexedDB no impone llaves foráneas reales. categoryId funciona como relación lógica desde la aplicación.
Store: categories
Representa las categorías usadas por movimientos y presupuestos.
| Campo | Tipo | Requerido | Índice | Descripción |
|---|---|---|---|---|
id | number | Sí | PK | Identificador autoincremental. |
name | string | Sí | unique | Nombre único de la categoría. |
createdAt | string | Sí | No | Fecha/hora ISO en que se creó. |
{
"id": 1,
"name": "Comida",
"createdAt": "2026-05-13T12:00:00.000Z"
}
Store: transactions
Representa ingresos y gastos.
| Campo | Tipo | Requerido | Índice | Descripción |
|---|---|---|---|---|
id | number | Sí | PK | Identificador autoincremental. |
type | enum | Sí | Sí | income o expense. |
amount | number | Sí | No | Monto positivo del movimiento. |
categoryId | number | Sí | Sí | Categoría relacionada. |
categoryName | string | Sí | No | Copia del nombre para lectura rápida. |
date | string | Sí | Sí | Fecha del movimiento en formato YYYY-MM-DD. |
comment | string | No | No | Comentario propio del usuario. |
sourceApp | enum | Sí | Sí | Origen: manual, yape, plin u other. |
externalId | string | No | Sí | Identificador externo para importar o vincular operaciones. |
note | string | No | No | Campo legado. |
createdAt | string | Sí | No | Fecha/hora ISO en que se registró. |
{
"id": 10,
"type": "expense",
"amount": 42.5,
"categoryId": 1,
"categoryName": "Comida",
"date": "2026-05-13",
"comment": "Almuerzo",
"sourceApp": "yape",
"externalId": "YAPE-20260513-001",
"createdAt": "2026-05-13T12:10:00.000Z"
}
Store: budgets
Representa un presupuesto para una categoría y un periodo.
| Campo | Tipo | Requerido | Índice | Descripción |
|---|---|---|---|---|
id | number | Sí | PK | Identificador autoincremental. |
categoryId | number | Sí | Sí | Categoría presupuestada. |
categoryName | string | Sí | No | Copia del nombre para lectura rápida. |
amount | number | Sí | No | Monto máximo presupuestado para el periodo. |
period | enum | Sí | Sí | weekly, biweekly o monthly. |
startsOn | string | Sí | Sí | Inicio del periodo en formato YYYY-MM-DD. |
createdAt | string | Sí | No | Fecha/hora ISO en que se registró. |
{
"id": 3,
"categoryId": 1,
"categoryName": "Comida",
"amount": 500,
"period": "monthly",
"startsOn": "2026-05-01",
"createdAt": "2026-05-13T12:15:00.000Z"
}
Periodicidad de presupuestos
period | Nombre en interfaz | Rango incluido |
|---|---|---|
weekly | Semanal | Desde startsOn hasta antes de startsOn + 7 días. |
biweekly | Cada 2 semanas | Desde startsOn hasta antes de startsOn + 14 días. |
monthly | Mensual | Desde startsOn hasta antes de startsOn + 1 mes. |
Índices
| Store | Índice | Uso |
|---|---|---|
categories | name | Evitar nombres duplicados. |
transactions | date | Ordenar y filtrar por fecha. |
transactions | type | Separar ingresos y gastos. |
transactions | categoryId | Relacionar movimientos con categorías y presupuestos. |
transactions | sourceApp | Filtrar movimientos importados por origen. |
transactions | externalId | Ubicar o deduplicar operaciones importadas. |
budgets | categoryId | Relacionar presupuestos con categorías. |
budgets | period | Separar presupuestos por periodicidad. |
budgets | startsOn | Ordenar o ubicar periodos presupuestados. |
Reglas de validación
amountdebe ser mayor que0.typesolo aceptaincomeoexpense.sourceAppsolo aceptamanual,yape,plinuother.periodsolo aceptaweekly,biweeklyomonthly.dateystartsOnse guardan comoYYYY-MM-DD.categoryIddebe apuntar a una categoría existente.- Para importaciones futuras, la combinación recomendada para detectar duplicados es
sourceApp + externalId.
Cálculos
ingresos = suma(transactions.amount donde type = "income")
gastos = suma(transactions.amount donde type = "expense")
balance = ingresos - gastos
usadoPresupuesto = suma(transactions.amount donde:
type = "expense"
categoryId = budget.categoryId
date dentro del periodo del budget
)
porcentajePresupuesto = min(100, usadoPresupuesto / budget.amount * 100)