Arquitectura y stack (Contabilidad)

Tecnologías

CapaTecnología
AppNext.js 14 (App Router) · React 18 · TypeScript · Tailwind 3
HostingVercel
DatosFirebase Firestore (cliente firebase + servidor firebase-admin)
AuthFirebase Auth (con custom claims para los accesos POS)
ArchivosFirebase Storage
BackgroundCloud Functions — solo paddleWebhook (suscripciones)
Pagos/SuscripciónPaddle (@paddle/paddle-js)
Parseo XMLfast-xml-parser + @xmldom/xmldom
Exportxlsx (Excel), pdf-lib (PDF)
Gráficosrecharts

Todo el cálculo fiscal es client-side (no hay Cloud Functions de cómputo). Las Cloud Functions solo manejan el webhook de Paddle.

Estructura de la app (App Router)

  • / landing (i18n ES/EN) · /login, /register, /dashboard, /subscription, /settings.
  • /accountantslista de clientes (ClientsDashboard), bajo ProtectedRoute + RequireSubscription + AppLayout.
  • /accountants/clients/[clientId]hub de módulos (ClientModules), bajo ClientDetailProvider:
    • /info — perfil + analítica (useClientBookSummaries + Recharts)
    • /billsBillsView (subir/parsear XML)
    • /booksBooksViewLibrosTab (libros + declaraciones)
    • /customers, /suppliersEntityTable
    • /payments (PagosView), /receivables (CobrosView)
    • /facturacionFacturadorAccessPanel (accesos a App B)
    • /taxi — módulo odómetro → D-104

Contextos (jerarquía en app/providers.tsx)

AuthProvider → LanguageProvider → ThemeProvider → FeedbackMessageProvider. Además:

  • AccountantDataCacheContext — caché en memoria de clientes y facturas (TTL 5 min, máx 4 entradas/cliente); claves vía buildFacturasCacheKey({year,dateRange,scope}).
  • ClientDetailContext — perfil del cliente, estado Hacienda (moroso/omiso), isBlocked (límite de plan), regimenKeys, actividadesEconomicas, taxiEnabled.

Hooks clave

useHaciendaLookup (contribuyente), useClientBookSummaries (analítica), useLibroCuentaTree (árbol de cuentas contables + overrides), useLibroProrrata (prorrata), useSubscriptionStatus, useVisibleModules.

Integración con Hacienda

App A no emite ni firma. Su única integración externa con Hacienda es el lookup de contribuyente:

GET https://api.hacienda.go.cr/fe/ae?identificacion={id}
→ { nombre, regimen.codigo, actividades[] (CIIU), tipoIdentificacion, situacion }

Se usa al crear/editar un cliente (autorelleno de régimen y actividades) y para el badge de moroso/omiso. El tipo de cambio se toma del propio XML (ResumenFactura.CodigoTipoMoneda.TipoCambio), no de una API.

Relación con App B (Facturación)

Comparten el mismo proyecto Firebase/Firestore:

  • El contador crea un usuario POS desde /accountants/clients/[clientId]/facturacion (FacturadorAccessPanelapp/api/facturador/access). Se crea con custom claims { role: "facturador", ownerUid, clientId } + índice en facturadorAccess/{authUid}.
  • Las reglas de Firestore confinan al facturador a su clientId: puede R/W productos, receptores, proveedores, comprobantes (metadata); no accede a facturacionConfig, consecutivos ni comprobantesXml.
  • App A lee la metadata de comprobantes + el estado MH/MR para la contabilidad.

Suscripciones

Planes (Plata/Oro/Diamante, mensual/anual) con límites de clientes/facturas, vía Paddle. La Cloud Function paddleWebhook actualiza subscriptions/{uid}_accountants. Rutas: app/api/trial/activate, paddle/subscription, paddle/cancel.

Webhook de Paddle — seguridad y variables

  • paddleWebhook (functions/src/paddleWebhook.ts, App A) verifica la firma de Paddle Billing antes de procesar: header Paddle-Signature → HMAC-SHA256 sobre ${ts}:${rawBody}, comparación en tiempo constante + anti-replay (rechaza timestamps de más de 5 min). Firma inválida o ausente → 401 y no procesa. Cierra el riesgo de eventos de suscripción falsificados.
  • Solo App A deploya paddleWebhook. App B tenía una copia (mismo proyecto Firebase) y se eliminó para no pisar la versión firmada al deployar.
  • Variables de la función viven en functions/.env de App A (el Firebase CLI lo carga a process.env al deployar; está gitignoreado):
    • NEXT_PUBLIC_{MONTHLY,ANNUALLY}_PLAN_{PLATA,ORO,DIAMANTE} — price IDs de Paddle. Mismos valores que el .env de la raíz (la raíz alimenta la web/checkout en Vercel; functions/.env, el webhook). Mantener ambos en sync.
    • PADDLE_WEBHOOK_SECRET — “Secret key” del notification destination (Paddle → Developer Tools → Notifications).
  • Dónde/cuándo se cambia: editás functions/.env (App A) y redeployás con firebase deploy --only functions. Cambios de la web → .env de la raíz + deploy de Vercel.
  • ⚠️ La verificación es obligatoria: sin PADDLE_WEBHOOK_SECRET seteado, los webhooks reales se rechazan con 401. Setealo antes de deployar.