Saltar a contenido

TypeScript

Paquete: @deuriib/dgii-ecf

Pensado primero para Bun, fetch nativo, TypeScript estricto y patrón result (neverthrow). Resiliencia con cockatiel.

Autentícate

import { ECF, Environment, P12Reader } from '@deuriib/dgii-ecf';

const credentials = new P12Reader(
  process.env.CERT_PASSWORD ?? '',
).getKeyFromFile('/secure/identity.p12');
if (credentials.isErr()) throw credentials.error;

const ecf = new ECF(credentials.value, Environment.Cert);
const auth = await ecf.authenticate();
if (auth.isErr()) throw auth.error;

console.log(auth.value.token);

Firma un documento

import { Signature } from '@deuriib/dgii-ecf';

const signed = new Signature(
  credentials.value.key!,
  credentials.value.cert!,
).signXml(xml);
if (signed.isErr()) throw signed.error;

Envía el documento

const response = await ecf.sendElectronicDocument(
  signed.value,
  '130862346E310000000001.xml',
);
response.match(
  (invoice) => console.log(invoice.trackId),
  (error) => console.error(error.code, error.message),
);

Patrón Result

Toda operación que puede fallar devuelve un Result:

const result = await ecf.statusTrackId('track-id');
if (result.isOk()) {
  console.log(result.value.estado);
} else {
  console.error(result.error.code, result.error.message);
}

API

Método Devuelve
authenticate(buyerHost?) Result<AuthToken>
sendElectronicDocument(xml, fileName, buyerHost?) Result<InvoiceResponse>
sendSummary(xml, fileName) Result<InvoiceSummaryResponse>
sendDocument(xml, fileName, buyerHost?) Result<InvoiceResponse \| InvoiceSummaryResponse>
statusTrackId(trackId) Result<TrackingStatusResponse>
inquiryStatus(rnc, ncf, comprador?, codigo?) Result<InquiryStatusResponse>
trackStatuses(rnc, encf) Result<SummaryTrackingStatusResponse[]>
getCustomerDirectory(rnc) Result<ServiceDirectoryResponse[] \| undefined>
getDirectoryListing() Result<ServiceDirectoryResponse[]>
getSummaryInvoiceInquiry(rnc, encf, codigo) Result<InquiryInvoiceSummary>
sendCommercialApproval(xml, fileName, buyerHost?) Result<CommercialApprovalResponse \| B2BCommercialApprovalResponse>
voidENCF(xml, fileName) Result<VoidNCFResponse>
reconcileSubmission(xml) Result<ReconciliationResult>
getServiceStatus() Result<ServiceStatusEntry[]>
getMaintenanceWindows() Result<MaintenanceWindow[]>
checkServiceAvailability(ambiente) Result<ServiceAvailabilityResponse>

Enrutamiento con sendDocument

sendDocument lee el elemento raíz, TipoeCF y MontoTotal del XML firmado y elige el endpoint por ti: un RFCE va a la recepción de resúmenes, un ECF a la recepción general. Una E32 menor a RD$250,000 se rechaza con VALIDATION —construye primero el RFCE— y un RFCE no puede entregarse directo a un buyerHost.

Usa resolveReceptionRoute(signedXml) cuando necesites la decisión sin enviar el documento.

Estado de los servicios

getServiceStatus, getMaintenanceWindows y checkServiceAvailability hablan con el host statusecf.dgii.gov.do. Se autentica con una API key, no con el token bearer, así que pasa statusApiKey al constructor y omite authenticate():

const ecf = new ECF(credentials.value, Environment.Prod, {
  statusApiKey: process.env.DGII_STATUS_API_KEY,
});
const availability = await ecf.checkServiceAvailability(
  StatusEnvironment.Production,
);
if (availability.isOk() && availability.value.estado === 'No Disponible') {
  // Ventana de mantenimiento: aplaza el lote.
}

Aplicabilidad B2B

Aplicabilidad B2B

El envío directo Emisor↔Receptor (buyerHost / buyer_host) no aplica a todos los tipos. La DGII excluye la E32 menor a RD$250,000 (solo aplica el resumen RFCE) y los tipos E41, E43, E45, E46 y E47. La E31, E33, E34, E44 y una E32 igual o mayor al umbral sí pueden entregarse entre pares.

El cliente lo verifica antes de emitir la solicitud y falla con VALIDATION sin llamar al host del comprador. Desactiva la validación con enforceB2BRules: false (TypeScript) o enforce_b2b_rules=False (Python) cuando necesites reproducir un escenario de certificación:

const result = await ecf.sendElectronicDocument(xml, 'f.xml', buyerHost);
if (result.isErr() && result.error.code === 'VALIDATION') {
  // El tipo o el monto no son elegibles para envío directo.
}

Endpoints exclusivos de producción

getSummaryInvoiceInquiry (consulta de RFCE, fc.dgii.gov.do/consultarfce) solo lo expone la DGII en producción (Environment.Prod / eCF). En Dev o Cert devuelve un error VALIDATION sin emitir la solicitud:

const inquiry = await ecf.getSummaryInvoiceInquiry(rnc, encf, codigoSeguridad);
if (inquiry.isErr()) {
  console.error(inquiry.error.code); // 'VALIDATION' outside production
}

El envío directo de un ACECF va al endpoint fe/aprobacioncomercial/api/ecf del comprador y devuelve B2BCommercialApprovalResponse en lugar del payload {codigo, estado} de la DGII. La DGII documenta HTTP 200 como satisfactorio y HTTP 400 como insatisfactorio, así que un 400 se reporta como accepted: false y no como un fallo de transporte:

const ack = await ecf.sendCommercialApproval(acecf, 'acecf.xml', buyerHost);
if (ack.isOk() && !ack.value.accepted) {
  // El receptor rechazó la aprobación; el emisor debe responder con una E34.
}

Validación por esquema

validateDocument verifica un documento contra el XML Schema oficial de la DGII y reporta cada violación con la ruta del elemento. Usa xmllint por subprocess, así que no hay motor empaquetado ni dependencia nativa:

import { validateDocument } from '@deuriib/dgii-ecf';

const result = validateDocument(signedXml);
if (result.isOk()) {
  for (const violation of result.value) {
    console.log(violation.path, violation.message);
  }
}

validateBeforeSend: true ejecuta la misma verificación antes de un envío general de e-CF y falla con VALIDATION sin llamar a la DGII:

const ecf = new ECF(credentials.value, Environment.Prod, {
  validateBeforeSend: true,
});

El binario se busca en PATH; usa DGII_XMLLINT_BIN (o la opción xmllintBin del cliente) para apuntarlo. Si falta el binario es un error, nunca un pase silencioso.

Se empaquetan doce esquemas: e-CF 32, 33, 34, 41, 43, 44, 45, 46, 47, ANECF, ARECF y Semilla. Tres esquemas publicados no compilan — e-CF 31, RFCE 32 y ACECF — así que esos tipos se reportan como esquema no disponible. Ver examples/fixtures/schemas/README.md para las razones.

Redondeo monetario

Transformer.json2xml(data, true) redondea los valores decimales a dos posiciones con ROUND_HALF_UP (2.665 → 2.67, -2.675 → -2.68), calculado sobre la cadena decimal y no con toFixed (que pierde precisión: (2.675).toFixed(2) === '2.67'). Version y el texto no monetario nunca se redondean.

Consulta Firma, Resiliencia y el README del paquete.