Modelo de base de datos

La aplicación usa IndexedDB en el navegador. Es una base local, mínima y sin servidor.

Resumen

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.

CampoTipoRequeridoÍndiceDescripción
idnumberPKIdentificador autoincremental.
namestringuniqueNombre único de la categoría.
createdAtstringNoFecha/hora ISO en que se creó.
{
  "id": 1,
  "name": "Comida",
  "createdAt": "2026-05-13T12:00:00.000Z"
}

Store: transactions

Representa ingresos y gastos.

CampoTipoRequeridoÍndiceDescripción
idnumberPKIdentificador autoincremental.
typeenumincome o expense.
amountnumberNoMonto positivo del movimiento.
categoryIdnumberCategoría relacionada.
categoryNamestringNoCopia del nombre para lectura rápida.
datestringFecha del movimiento en formato YYYY-MM-DD.
commentstringNoNoComentario propio del usuario.
sourceAppenumOrigen: manual, yape, plin u other.
externalIdstringNoIdentificador externo para importar o vincular operaciones.
notestringNoNoCampo legado.
createdAtstringNoFecha/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.

CampoTipoRequeridoÍndiceDescripción
idnumberPKIdentificador autoincremental.
categoryIdnumberCategoría presupuestada.
categoryNamestringNoCopia del nombre para lectura rápida.
amountnumberNoMonto máximo presupuestado para el periodo.
periodenumweekly, biweekly o monthly.
startsOnstringInicio del periodo en formato YYYY-MM-DD.
createdAtstringNoFecha/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

periodNombre en interfazRango incluido
weeklySemanalDesde startsOn hasta antes de startsOn + 7 días.
biweeklyCada 2 semanasDesde startsOn hasta antes de startsOn + 14 días.
monthlyMensualDesde startsOn hasta antes de startsOn + 1 mes.

Índices

StoreÍndiceUso
categoriesnameEvitar nombres duplicados.
transactionsdateOrdenar y filtrar por fecha.
transactionstypeSeparar ingresos y gastos.
transactionscategoryIdRelacionar movimientos con categorías y presupuestos.
transactionssourceAppFiltrar movimientos importados por origen.
transactionsexternalIdUbicar o deduplicar operaciones importadas.
budgetscategoryIdRelacionar presupuestos con categorías.
budgetsperiodSeparar presupuestos por periodicidad.
budgetsstartsOnOrdenar o ubicar periodos presupuestados.

Reglas de validación

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)