DGII / Fiscal
Cumplimiento fiscal dominicano — numeración NCF/eNCF, transmisión de e-CF a la DGII con la librería dgii-ecf, esquema de datos y ticket fiscal.
ixiclinic cumple el régimen fiscal dominicano de la DGII: numera comprobantes (NCF), transmite facturas electrónicas (e-CF) firmadas con certificado, y guarda el rastro de cada envío.
Importante: La lógica fiscal vive en el API, no en el admin. El antiguo directorio
apps/admin/src/lib/dgii/ya no existe. El admin solo presenta el formulario y consume los endpoints/api/ecf/*.
Las piezas y dónde viven
| Pieza | Ubicación | Rol |
|---|---|---|
| Módulo e-CF (rutas/handlers/service/schemas) | apps/api/src/modules/ecf/ | Configuración, certificado, transmisión y estado |
| Librería de firma + envío | dependencia dgii-ecf (^1.8.0) | P12, XML, firma digital y REST de la DGII |
| Función de numeración NCF/eNCF | apps/api/src/db/ensure-ncf-function.ts | next_fiscal_number() en PostgreSQL |
| Esquema fiscal | apps/api/src/db/schema/dgii.ts | fiscal_sequences, ecf_configurations, ecf_submissions, dgii_reports |
| Ticket / recibo fiscal | apps/api/src/lib/escpos/invoice-svg.ts | SVG del comprobante (se rasteriza a ESC/POS) |
Numeración NCF / eNCF
La función PostgreSQL next_fiscal_number(p_tenant_id, p_ncf_type) se crea (idempotente) al
arrancar el API desde ensureNcfFunction(), invocada en apps/api/src/index.ts antes de
app.listen. Garantiza una numeración gap-free y concurrente:
- Toma la secuencia activa del tenant para ese tipo (
SELECT ... FOR UPDATE— lock de fila). - Valida vencimiento (
SEQUENCE_EXPIRED) y rango (SEQUENCE_EXHAUSTED). - Incrementa
current_numbery compone el comprobante:- eNCF (prefijo que empieza por
E, p. ej.E31):prefix + LPAD(n, 10, '0')→E310000000001(13 chars). - NCF legacy (prefijo
B, p. ej.B02):prefix + LPAD(n, 8, '0')→B0200000001.
- eNCF (prefijo que empieza por
- B02 (Consumidor Final) no requiere autorización DGII: si el tenant no tiene secuencia, la auto-provisiona con un rango amplio. Otros tipos (B01, B15, B16…) sí requieren autorización y deben configurarse en Ajustes.
Un índice único parcial uq_fiscal_seq_active impide tener dos secuencias activas del mismo
tipo por tenant. La tabla fiscal_sequences (apps/api/src/db/schema/dgii.ts) guarda
ncfType, prefix, currentNumber, rangeFrom/To, authorizationNumber y expiryDate.
Nota: Existe también un contador genérico (no fiscal) —
next_generic_number()+ tablageneric_sequences— que produce números tipoFAC-000001para documentos que no se transmiten a la DGII (invoices.fiscal_regime = 'generic'). El régimen fiscal es la fuente de verdad: solofiscal_regime = 'fiscal'va a la DGII.
e-CF (facturación electrónica)
El servicio apps/api/src/modules/ecf/service.ts transmite las facturas fiscales a la DGII con
la librería dgii-ecf.
Gotcha (documentado en el código):
dgii-ecfes CommonJS; bajo ESM hay que importar el namedECF, no el default —el default es el namespace del módulo, no constructable, y pasaría typecheck pero lanzaría"ECF is not a constructor"en runtime.
Configuración y certificado
saveConfigguarda RNC, razón social y ambiente (TesteCF/CerteCF/eCF).saveCertificateguarda el P12 en base64 dentro de la DB (sobrevive a redeploys de Docker, sin dependencia del filesystem). Antes de guardar, lo lee conP12Readerpara validar la contraseña y extraer su fecha de expiración.setActive(true)exige que haya certificado y, al activar, auto-provisiona las secuencias e-CF estándar (provisionEcfSequences):B01→E31,B02→E32,B04→E34.getConfigPublicdevuelve una vista segura que nunca filtra el certificado ni la passphrase.
Transmisión
submitInvoice(tenantId, invoiceId):
- Corta si la factura ya fue aceptada o si es genérica (
fiscalRegime !== 'fiscal'): un documento genérico nunca se transmite. initClientconstruye el clientedgii-ecfdesde el certificado, fija el ambiente y haceecf.authenticate()(registralastAuthAt).- Construye el JSON del e-CF (
buildEcfData) mapeando el tipo NCF al tipo de documento DGII (DOCUMENT_TYPE_MAP:B01→31,B02→32,B03→33,B04→34,B11→41,B13→43,B14→44,B15→45,B16→46,B17→47). - Lo transforma a XML (
Transformer.json2xml) y lo firma (Signature.signXml(..., 'ECF')). - Envía:
- Consumo (E32) bajo RD$250,000 → resumen RFCE (
convertECF32ToRFCE+sendSummary), que devuelve un código de seguridad. - El resto →
sendElectronicDocument.
- Consumo (E32) bajo RD$250,000 → resumen RFCE (
- Persiste el envío en
ecf_submissionscontrackId,status(sent), XML firmado y metadata. Ante error, lo deja enerrorconlastErrore incrementaattempts.
Estado
checkStatus(tenantId, submissionId) consulta la DGII por trackId (ecf.statusTrackId) y
traduce el estado: Aceptado → accepted, Rechazado → rejected, otro → sent. Guarda código
y mensaje de respuesta en la fila.
Endpoints (apps/api/src/modules/ecf/routes.ts)
Todos gateados con requireModule('billing'):
| Método y ruta | Para qué |
|---|---|
GET /api/ecf/config | Config pública (sin secretos) |
POST /api/ecf/config | Guardar RNC / razón social / ambiente |
POST /api/ecf/certificate | Subir el P12 (base64) + passphrase |
POST /api/ecf/active | Activar/desactivar e-CF (provisiona secuencias) |
POST /api/ecf/submit/:invoiceId | Transmitir una factura a la DGII |
GET /api/ecf/status/:submissionId | Consultar estado por trackId |
GET /api/ecf/submissions | Listar envíos (filtro por estado) |
GET /api/ecf/submissions/invoice/:invoiceId | Último envío de una factura |
Datos persistidos
En apps/api/src/db/schema/dgii.ts:
fiscal_sequences— rangos NCF/eNCF por tipo y tenant.ecf_configurations— una fila por tenant: RNC, razón social, certificado base64 + passphrase, expiración, ambiente, activo,lastAuthAt.ecf_submissions— una fila por factura fiscal transmitida:documentType,encf, RNC emisor/receptor,trackId,securityCode,status, XML firmado, intentos y metadata.dgii_reports— tabla para los reportes mensuales 606 (compras), 607 (ventas) y 608 (anulaciones):reportType,period(YYYY-MM),data(jsonb) ystatus.
Discrepancia notable: La tabla
dgii_reportsexiste en el esquema, pero no se encontró un módulo ni endpoints en el API que generen los reportes 606/607/608 (no hay módulodgiienapps/api/src/modules/). La infraestructura de datos está, pero la generación de esos reportes aún no está implementada en el backend.
Ticket fiscal
El comprobante impreso se diseña como SVG en apps/api/src/lib/escpos/invoice-svg.ts
(buildInvoiceSvg) para tener control total de fuentes, logo y layout, y luego se rasteriza
a ESC/POS para la impresora térmica (ver Impresión + Bridge).
Siguiente: Impresión + Bridge.
Scheduling
Tareas recurrentes en background con @fastify/schedule (toad-scheduler) — recordatorios de citas, no-shows, limpieza de ocupación, resumen diario y canales WhatsApp/email.
Impresión + Bridge
Arquitectura del sistema de impresión cloud→LAN — módulo printing del API, render ESC/POS, plugin bridge-auth y el agente ixiclinic Connect.