Skip to main content

Architecture Overview

Sistema Financiero follows a full-stack Next.js architecture with clear separation of concerns:
┌─────────────────────────────────────────────────────┐
│         Next.js 15 (Full-Stack Framework)           │
├─────────────────────────────────────────────────────┤
│  FRONTEND (Client-Side)                             │
│  ├─ app/page.tsx          → Dashboard               │
│  ├─ app/registro/         → Manual forms            │
│  ├─ app/agente-mejorado/  → AI chat                 │
│  └─ components/*.tsx      → Reusable UI             │
├─────────────────────────────────────────────────────┤
│  BACKEND (Server-Side)                              │
│  └─ app/api/              → API Routes              │
│      ├─ transacciones/route.ts  → GET transactions │
│      ├─ chat/stream/route.ts    → AI streaming     │
│      └─ upload-image/route.ts   → OCR processing   │
├─────────────────────────────────────────────────────┤
│  DATABASE (Supabase)                                │
│  └─ transacciones table                             │
└─────────────────────────────────────────────────────┘

Common Patterns

Pattern 1: Adding a New API Endpoint

1

Create Route File

Create a new route handler in app/api/:
// File: app/api/stats/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

export async function GET(request: NextRequest) {
  const { data, error } = await supabase
    .from('transacciones')
    .select('tipo, monto')

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }

  const stats = data.reduce(
    (acc, t) => {
      if (t.tipo === 'ingreso') {
        acc.totalIngresos += Number(t.monto)
      } else {
        acc.totalGastos += Number(t.monto)
      }
      return acc
    },
    { totalIngresos: 0, totalGastos: 0 }
  )

  return NextResponse.json({ data: stats })
}
2

Call from Frontend

Use the endpoint in your component:
const [stats, setStats] = useState({ totalIngresos: 0, totalGastos: 0 })

useEffect(() => {
  fetch('/api/stats')
    .then(res => res.json())
    .then(json => setStats(json.data))
}, [])
API routes in app/api/ are automatically server-side and have access to environment variables.

Pattern 2: Adding a New Category

Categories are hardcoded in multiple files. Update all locations to avoid inconsistencies.
1

Update AI Chat Categories

File: app/api/chat/stream/route.ts:26
📋 CATEGORÍAS VÁLIDAS:
**Gastos:** Alimentación, Transporte, Vivienda, Salud, Entretenimiento, Educación, Mascotas, Otros Gastos
**Ingresos:** Salario, Ventas, Servicios, Inversiones, Otros Ingresos
2

Update Function Definitions

File: app/api/chat/stream/route.ts:75
categoria: {
  type: 'string',
  enum: [
    'Alimentación',
    'Transporte',
    'Vivienda',
    'Salud',
    'Entretenimiento',
    'Educación',
    'Mascotas', // ← New category
    'Otros Gastos'
  ]
}
3

Update Manual Form

File: app/registro/page.tsx (add to category dropdown)
const CATEGORIAS_GASTOS = [
  'Alimentación',
  'Transporte',
  'Vivienda',
  'Salud',
  'Entretenimiento',
  'Educación',
  'Mascotas', // ← New category
  'Otros Gastos'
]
4

Update OCR Analysis

File: app/api/upload-image/route.ts:64
**CATEGORÍAS VÁLIDAS DEL SISTEMA:**
- Gastos: Alimentación, Transporte, Vivienda, Salud, Entretenimiento, Educación, Mascotas, Otros Gastos

Pattern 3: Adding a Database Column

1

Add Column to Supabase

Run in Supabase SQL Editor:
ALTER TABLE transacciones ADD COLUMN notas TEXT;
2

Update TypeScript Interface

File: components/DataViews.tsx:8
interface Transaccion {
  id: string
  fecha: string
  tipo: 'gasto' | 'ingreso'
  categoria: string
  monto: number
  descripcion: string
  metodo_pago: string
  notas: string | null // ← New field
}
3

Update API Insert Logic

File: app/api/chat/stream/route.ts:208
const { error } = await supabase.from('transacciones').insert({
  tipo,
  monto: functionArgs.monto,
  categoria: functionArgs.categoria,
  concepto: functionArgs.descripcion || `${tipo} - ${functionArgs.categoria}`,
  descripcion: functionArgs.descripcion || null,
  metodo_pago: functionArgs.metodo_pago || 'Efectivo',
  registrado_por: functionArgs.registrado_por || 'Usuario',
  notas: functionArgs.notas || null, // ← New field
  fecha: new Date().toISOString(),
})
4

Update AI Function Schema

Add to function parameters:
parameters: {
  type: 'object',
  properties: {
    monto: { type: 'number' },
    categoria: { type: 'string', enum: [...] },
    descripcion: { type: 'string' },
    metodo_pago: { type: 'string', enum: [...] },
    registrado_por: { type: 'string' },
    notas: { type: 'string', description: 'Notas adicionales' } // ← New
  },
  required: ['monto', 'categoria']
}

Pattern 4: Adding a New Component

1

Create Component File

// File: components/ExpenseChart.tsx
'use client'

import { useState, useEffect } from 'react'
import { Pie } from 'react-chartjs-2'

export function ExpenseChart() {
  const [chartData, setChartData] = useState(null)

  useEffect(() => {
    fetch('/api/transacciones?vista=mensual')
      .then(res => res.json())
      .then(json => {
        const gastos = json.data.filter(t => t.tipo === 'gasto')
        const byCategory = gastos.reduce((acc, t) => {
          acc[t.categoria] = (acc[t.categoria] || 0) + Number(t.monto)
          return acc
        }, {})

        setChartData({
          labels: Object.keys(byCategory),
          datasets: [{
            data: Object.values(byCategory),
            backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0']
          }]
        })
      })
  }, [])

  if (!chartData) return <div>Loading...</div>

  return <Pie data={chartData} />
}
2

Import and Use

// File: app/page.tsx
import { ExpenseChart } from '@/components/ExpenseChart'

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <ExpenseChart />
    </div>
  )
}

Feature Examples

Example 1: Add Budget Tracking

-- Add budget column to transacciones
ALTER TABLE transacciones ADD COLUMN presupuesto NUMERIC(10, 2);

-- Create budget alerts table
CREATE TABLE presupuestos (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  usuario_id UUID REFERENCES auth.users(id),
  categoria TEXT NOT NULL,
  monto_limite NUMERIC(10, 2) NOT NULL,
  periodo TEXT CHECK (periodo IN ('mensual', 'semanal', 'diario')),
  created_at TIMESTAMP DEFAULT NOW()
);

Example 2: Add Recurring Transactions

CREATE TABLE transacciones_recurrentes (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  usuario_id UUID REFERENCES auth.users(id),
  tipo TEXT CHECK (tipo IN ('ingreso', 'gasto')) NOT NULL,
  monto NUMERIC(10, 2) NOT NULL,
  categoria TEXT NOT NULL,
  descripcion TEXT,
  frecuencia TEXT CHECK (frecuencia IN ('diaria', 'semanal', 'mensual', 'anual')),
  dia_mes INTEGER CHECK (dia_mes BETWEEN 1 AND 31),
  activo BOOLEAN DEFAULT true,
  proxima_fecha DATE NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_recurrentes_proxima_fecha ON transacciones_recurrentes(proxima_fecha);

Changing AI Model

File: app/api/chat/stream/route.ts:54
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
  body: JSON.stringify({
    model: 'anthropic/claude-3.5-sonnet', // ← Change model here
    // Available models:
    // - 'google/gemini-2.5-flash' (default, fastest)
    // - 'anthropic/claude-3.5-sonnet' (best quality)
    // - 'openai/gpt-4o' (balanced)
    // - 'meta-llama/llama-3.1-70b-instruct' (open source)
  })
})
Check OpenRouter Models for full list and pricing.

Custom Hooks

Example: useTransactions Hook

// File: hooks/useTransactions.ts
import { useState, useEffect } from 'react'
import { supabase } from '@/lib/supabase'

interface Transaccion {
  id: string
  fecha: string
  tipo: 'gasto' | 'ingreso'
  categoria: string
  monto: number
}

export function useTransactions(vista: 'diaria' | 'semanal' | 'mensual' = 'mensual') {
  const [transacciones, setTransacciones] = useState<Transaccion[]>([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    async function fetchData() {
      try {
        const { data, error } = await supabase
          .from('transacciones')
          .select('*')
          .order('fecha', { ascending: false })

        if (error) throw error

        setTransacciones(data || [])
      } catch (err: any) {
        setError(err.message)
      } finally {
        setLoading(false)
      }
    }

    fetchData()
  }, [vista])

  return { transacciones, loading, error }
}

Testing New Features

1

Local Testing

npm run dev
Test in browser at http://localhost:3000
2

Check Console Logs

Open DevTools → Console to see errors or API responses
3

Test Database Changes

Use Supabase Dashboard → Table Editor to verify inserts
4

Test API Endpoints

Use curl or Postman:
curl http://localhost:3000/api/stats

Next Steps

Database Schema

Learn about the database structure

Customization Guide

Customize categories, styles, and behavior

Build docs developers (and LLMs) love