Arquitectura y stack (Facturación)
Tecnologías
| Capa | Tecnología |
|---|---|
| App | Next.js (App Router) · TypeScript · Tailwind CSS |
| Hosting app | Vercel |
| Datos | Firebase Firestore |
| Auth | Firebase Auth (ID token en cada API route) |
| Archivos | Firebase Storage (XML firmado + XML respuesta) |
| Background | Cloud Functions (Node 22) + Cloud Tasks |
| Hacienda | API REST Comprobantes Electrónicos v4.4 + IDP OAuth2 (Keycloak) |
| Firma | XAdES-EPES (SHA256 + RSA), certificado .p12 del emisor |
pdf-lib (server-side, sin headless browser) |
Mapa de módulos
lib/facturacion/
├── server/
│ ├── emitir.ts # emitirComprobante(): orquesta todo
│ ├── calculo.ts # cálculo de líneas, IVA, normalización de unidades
│ ├── emisorStore.ts # Firestore (ComprobanteDoc) + Storage (XML)
│ ├── notificacion.ts # correo durable al cliente (XML+acuse+PDF)
│ └── pdf.ts # generarFacturaPdf() con pdf-lib
├── xml/
│ └── facturaElectronica.ts # arma el XML v4.4 (todos los tipos)
├── hacienda/
│ ├── oauth.ts # getAccessToken() (OAuth2 IDP)
│ └── recepcion.ts # enviarComprobante() / consultarEstado() / parseEstadoJson()
├── client/
│ └── api.ts # fcApi: llamadas a las API routes con ID token
└── clave.ts, catalogos.ts, types.tsAPI routes en app/api/facturacion/* (emitir, estado, comprobante, comprobante/pdf, comprobante/reenviar, exoneracion, cabys, contribuyente, tipocambio, warmup, seguridad…).
Pipeline de emisión
Modelo durable (Cloud Tasks)
Dos colas garantizan que nada se pierda ante caídas:
fe-envio-hacienda— envía el XML firmado a Hacienda y consulta el estado; reintenta si Hacienda está caída. El comprobante quedaprocesandoy se puede reenviar manualmente.fe-correo— envía al cliente el XML, el acuse de Hacienda y el PDF.
Patrón: encolar + fallback + idempotencia. La emisión local responde de inmediato (clave/consecutivo) y la confirmación de Hacienda + el correo ocurren en segundo plano. No se bloquea la venta esperando a Hacienda.
Autenticación con Hacienda (oauth.ts)
OAuth2 contra el IDP (Keycloak) de Hacienda: se obtiene un access_token (password grant / refresh) con las credenciales del API del emisor (sacadas de ATV). Endpoints distintos para sandbox y producción:
recepción sandbox: https://api-sandbox.comprobanteselectronicos.go.cr/recepcion/v1
recepción producción: https://api.comprobanteselectronicos.go.cr/recepcion/v1PDF y costos
- El XML firmado y el XML respuesta se guardan en Storage (
emisorStore.saveComprobanteXml). - El PDF se genera on-demand por link (
app/api/facturacion/comprobante/pdf), no se almacena → ahorra costos. pdf.tsusapdf-lib(fuentes estándar embebidas, corre en Vercel sin navegador).
Seguridad
- Cada API route valida el ID token de Firebase (
authedFetchenclient/api.ts). - Escrituras server-authoritative: consecutivos, estados y montos finales los decide el servidor.
- Candado NC/ND: opcional; requiere una clave interna para emitir notas (el hash/salt nunca sale al cliente). API en
app/api/facturacion/seguridad. - Certificado
.p12y credenciales del API cifrados server-side; nunca llegan al navegador.
App Check (anti-bot)
- El cliente inicializa Firebase App Check (reCAPTCHA v3) (
lib/firebase/client.ts), condicionado aNEXT_PUBLIC_RECAPTCHA_SITE_KEY. Hace que Firebase rechace requests que no vengan de la app real. - El enforcement se activa por producto en la consola (Firestore, Auth, Storage). El código del cliente solo protege si el backend está en Enforce (no en Monitor).
- Los links del correo NO dependen de App Check: el PDF es una Cloud Function con HMAC (
FE_PDF_LINK_SECRET) + Admin SDK, y los archivos de Storage se sirven por URL tokenizada o Admin SDK → App Check no los bloquea. Por eso se puede activar enforcement en Storage sin romper los downloads (recomendado: Monitor → Enforce).
Rate-limit por negocio
- Los endpoints caros tienen rate-limit por negocio (
ownerUid:clientId) —lib/facturacion/server/rateLimit.ts. Conteo por ventana de 1 minuto (el límite es por minuto); al superar el tope, el negocio queda bloqueado 5 minutos (penalidad). Contador en Firestore (rateLimits/{owner}__{client}__{accion}:windowStart,count,override?,blockedUntil?), respuesta 429 +Retry-After. - Cada negocio es un balde independiente (los clientes no se afectan entre sí); todas las PCs/locales de un mismo negocio comparten su balde, sizeado holgado.
- Defaults por minuto:
emitir300 ·pdf200 ·contribuyente/exoneracion120 ·reenviar/reenviar-hacienda60 ·export20. Cubren cadenas de varios locales con margen; frenan loops/abuso (miles/min). - Override por cliente: poné el campo
override(número) en el docrateLimits/{owner}__{client}__{accion}para subir el tope de un negocio grande sin redeploy.
Endpoints públicos (sin login) — protegidos
| Endpoint | Protección |
|---|---|
tasks/correo, tasks/enviar (target de Cloud Tasks) | header X-Tasks-Secret → 403 sin el secreto |
callback (Hacienda) | token + clave de 50 dígitos; junk → 200 e ignora |
descargarPdfComprobante (Function) | HMAC FE_PDF_LINK_SECRET → 403 antes de generar el PDF |
paddleWebhook (Function) | validación de firma de Paddle |
tipocambio | público pero cacheado (barato) |
Un bot sin credenciales no puede disparar trabajo caro: todo rechaza barato (401 token / 403 secreto / 200-ignore). El riesgo real es el abuso autenticado, cubierto por el rate-limit por negocio. Refuerzo de plataforma: Vercel (mitigación DDoS) + Cloud Functions
maxInstances: 10.