Arquitectura y stack (Contabilidad)
Tecnologías
| Capa | Tecnología |
|---|---|
| App | Next.js 14 (App Router) · React 18 · TypeScript · Tailwind 3 |
| Hosting | Vercel |
| Datos | Firebase Firestore (cliente firebase + servidor firebase-admin) |
| Auth | Firebase Auth (con custom claims para los accesos POS) |
| Archivos | Firebase Storage |
| Background | Cloud Functions — solo paddleWebhook (suscripciones) |
| Pagos/Suscripción | Paddle (@paddle/paddle-js) |
| Parseo XML | fast-xml-parser + @xmldom/xmldom |
| Export | xlsx (Excel), pdf-lib (PDF) |
| Gráficos | recharts |
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./accountants→ lista de clientes (ClientsDashboard), bajoProtectedRoute+RequireSubscription+AppLayout./accountants/clients/[clientId]→ hub de módulos (ClientModules), bajoClientDetailProvider:/info— perfil + analítica (useClientBookSummaries+ Recharts)/bills—BillsView(subir/parsear XML)/books—BooksView→LibrosTab(libros + declaraciones)/customers,/suppliers—EntityTable/payments(PagosView),/receivables(CobrosView)/facturacion—FacturadorAccessPanel(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(FacturadorAccessPanel→app/api/facturador/access). Se crea con custom claims{ role: "facturador", ownerUid, clientId }+ índice enfacturadorAccess/{authUid}. - Las reglas de Firestore confinan al facturador a su
clientId: puede R/Wproductos,receptores,proveedores,comprobantes(metadata); no accede afacturacionConfig,consecutivosnicomprobantesXml. - App A lee la metadata de
comprobantes+ el estadoMH/MRpara 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: headerPaddle-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/.envde App A (el Firebase CLI lo carga aprocess.enval deployar; está gitignoreado):NEXT_PUBLIC_{MONTHLY,ANNUALLY}_PLAN_{PLATA,ORO,DIAMANTE}— price IDs de Paddle. Mismos valores que el.envde 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 confirebase deploy --only functions. Cambios de la web →.envde la raíz + deploy de Vercel. - ⚠️ La verificación es obligatoria: sin
PADDLE_WEBHOOK_SECRETseteado, los webhooks reales se rechazan con 401. Setealo antes de deployar.