ixiclinicDocs
Desarrolladores

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

PiezaUbicaciónRol
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íodependencia dgii-ecf (^1.8.0)P12, XML, firma digital y REST de la DGII
Función de numeración NCF/eNCFapps/api/src/db/ensure-ncf-function.tsnext_fiscal_number() en PostgreSQL
Esquema fiscalapps/api/src/db/schema/dgii.tsfiscal_sequences, ecf_configurations, ecf_submissions, dgii_reports
Ticket / recibo fiscalapps/api/src/lib/escpos/invoice-svg.tsSVG 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_number y 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.
  • 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…) 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() + tabla generic_sequences — que produce números tipo FAC-000001 para documentos que no se transmiten a la DGII (invoices.fiscal_regime = 'generic'). El régimen fiscal es la fuente de verdad: solo fiscal_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-ecf es CommonJS; bajo ESM hay que importar el named ECF, 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

  • saveConfig guarda RNC, razón social y ambiente (TesteCF / CerteCF / eCF).
  • saveCertificate guarda el P12 en base64 dentro de la DB (sobrevive a redeploys de Docker, sin dependencia del filesystem). Antes de guardar, lo lee con P12Reader para 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.
  • getConfigPublic devuelve una vista segura que nunca filtra el certificado ni la passphrase.

Transmisión

submitInvoice(tenantId, invoiceId):

  1. Corta si la factura ya fue aceptada o si es genérica (fiscalRegime !== 'fiscal'): un documento genérico nunca se transmite.
  2. initClient construye el cliente dgii-ecf desde el certificado, fija el ambiente y hace ecf.authenticate() (registra lastAuthAt).
  3. 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).
  4. Lo transforma a XML (Transformer.json2xml) y lo firma (Signature.signXml(..., 'ECF')).
  5. Envía:
    • Consumo (E32) bajo RD$250,000 → resumen RFCE (convertECF32ToRFCE + sendSummary), que devuelve un código de seguridad.
    • El resto → sendElectronicDocument.
  6. Persiste el envío en ecf_submissions con trackId, status (sent), XML firmado y metadata. Ante error, lo deja en error con lastError e incrementa attempts.

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 rutaPara qué
GET /api/ecf/configConfig pública (sin secretos)
POST /api/ecf/configGuardar RNC / razón social / ambiente
POST /api/ecf/certificateSubir el P12 (base64) + passphrase
POST /api/ecf/activeActivar/desactivar e-CF (provisiona secuencias)
POST /api/ecf/submit/:invoiceIdTransmitir una factura a la DGII
GET /api/ecf/status/:submissionIdConsultar estado por trackId
GET /api/ecf/submissionsListar 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) y status.

Discrepancia notable: La tabla dgii_reports existe 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ódulo dgii en apps/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.

On this page