Mejores prácticas
Arquitectura y diseño
Separación de ambientes
La URL base es la misma para todos los ambientes (https://ecf.facturord.app); el ambiente de la DGII se elige con dgii_env en cada operación. Centraliza esa decisión en la configuración:
// config/facturo.js
const DGII_ENV_BY_NODE_ENV = {
development: 'testecf',
staging: 'certecf',
production: 'ecf',
};
module.exports = {
baseUrl: process.env.FACTURO_BASE_URL || 'https://ecf.facturord.app/',
apiKey: process.env.FACTURO_API_KEY,
companyId: process.env.FACTURO_COMPANY_ID,
dgiiEnv: process.env.FACTURO_DGII_ENV || DGII_ENV_BY_NODE_ENV[process.env.NODE_ENV || 'development'],
};
No uses datos reales de clientes en testecf, no hagas pruebas en ecf (los comprobantes de producción tienen validez fiscal) y no compartas la misma API Key entre desarrollo y producción.
Llama a la API solo desde tu backend
La API Key da acceso a la emisión fiscal de tu empresa. Úsala únicamente en servidores, workers o funciones serverless; nunca en el navegador ni en apps móviles.
Usa los clientes oficiales
Los clientes oficiales ya resuelven autenticación, rutas, timeouts y parseo de errores. Reutiliza una sola instancia del cliente por proceso.
Circuit breaker
Si la DGII o la red dejan de responder, un circuit breaker evita que tu aplicación siga acumulando peticiones fallidas:
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.failureCount = 0;
this.threshold = threshold;
this.timeout = timeout;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.nextAttempt = Date.now();
}
async execute(fn) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
}
this.state = 'HALF_OPEN';
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failureCount++;
if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
}
}
}
// Uso
const breaker = new CircuitBreaker();
const result = await breaker.execute(() => client.emitRichECF({ dgii_env: 'ecf', ecf }));
Gestión de secuencias (e-NCF)
Control centralizado
Un e-NCF tiene 13 caracteres: serie (1), tipo (2) y secuencia (10). Genera las secuencias de forma atómica en un único lugar:
-- PostgreSQL
CREATE TABLE ecf_sequences (
company_rnc VARCHAR(11) NOT NULL,
ecf_type VARCHAR(2) NOT NULL,
last_sequence BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (company_rnc, ecf_type)
);
// Generador atómico
async function getNextSequence(rnc, ecfType) {
const result = await db.query(`
INSERT INTO ecf_sequences (company_rnc, ecf_type, last_sequence)
VALUES ($1, $2, 1)
ON CONFLICT (company_rnc, ecf_type)
DO UPDATE SET last_sequence = ecf_sequences.last_sequence + 1,
updated_at = NOW()
RETURNING last_sequence
`, [rnc, ecfType]);
return result.rows[0].last_sequence;
}
// Formatear e-NCF
function formatENCF(ecfType, number, serie = 'E') {
return `${serie}${ecfType}${String(number).padStart(10, '0')}`;
}
// Uso
const next = await getNextSequence('123456789', '31');
const encf = formatENCF('31', next);
// Resultado: E310000000042
Manejo de concurrencia
Si no puedes usar un UPSERT atómico, bloquea la fila durante la transacción:
from sqlalchemy import select
from sqlalchemy.orm import Session
def get_next_sequence(session: Session, rnc: str, ecf_type: str) -> int:
# FOR UPDATE previene condiciones de carrera
stmt = (
select(EcfSequence)
.where(EcfSequence.company_rnc == rnc, EcfSequence.ecf_type == ecf_type)
.with_for_update()
)
sequence = session.execute(stmt).scalar_one_or_none()
if sequence is None:
sequence = EcfSequence(company_rnc=rnc, ecf_type=ecf_type, last_sequence=1)
session.add(sequence)
else:
sequence.last_sequence += 1
session.commit()
return sequence.last_sequence
Secuencias no utilizadas
- No reutilices e-NCF, ni siquiera de documentos rechazados.
- Anula con ANECF las secuencias saltadas, dañadas o que no se van a usar.
- Un comprobante ya aceptado por la DGII no se anula con ANECF: emite una Nota de Crédito (E34).
Emisión confiable
Guarda los identificadores de la emisión
Persiste document_id, encf y track_id en cuanto recibas la respuesta de emisión:
CREATE TABLE ecf_documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
facturo_doc_id UUID UNIQUE,
encf VARCHAR(13) NOT NULL,
ecf_type VARCHAR(2) NOT NULL,
dgii_env VARCHAR(10) NOT NULL,
track_id VARCHAR(100),
internal_status VARCHAR(50),
dgii_status VARCHAR(50),
security_code VARCHAR(20),
qr_url TEXT,
total_amount DECIMAL(15, 2),
payload JSONB NOT NULL,
error_message TEXT,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE (dgii_env, encf)
);
Reintentos con backoff
Reintenta solo errores de red y 5xx. Los 4xx indican datos o permisos incorrectos: corrígelos antes de reenviar.
async function requestWithBackoff(fn, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
// No reintentar errores 4xx
if (error.statusCode >= 400 && error.statusCode < 500) {
throw error;
}
// Backoff exponencial
const delay = Math.min(1000 * Math.pow(2, i), 10000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
Si un documento ya tiene document_id, usa reintentar emisión en lugar de volver a emitirlo. Tras un timeout, consulta el estado antes de reenviar.
Consulta de estado con intervalos crecientes
async function waitForDgiiStatus(trackId, isFinal, maxAttempts = 8) {
for (let i = 0; i < maxAttempts; i++) {
const status = await client.getTrackingStatus(trackId);
if (isFinal(status)) {
return status;
}
// 2 s, 4 s, 8 s ... hasta 60 s
await new Promise(resolve => setTimeout(resolve, Math.min(2000 * 2 ** i, 60000)));
}
throw new Error(`Estado no definitivo para el Track ID ${trackId}`);
}
Para volúmenes altos, mueve la consulta de estados a un job en segundo plano en lugar de bloquear la petición del usuario.
Procesamiento por lotes
Limita la concurrencia al emitir muchos documentos:
async function emitInBatches(payloads, batchSize = 10) {
const results = [];
for (let i = 0; i < payloads.length; i += batchSize) {
const batch = payloads.slice(i, i + batchSize);
const batchResults = await Promise.allSettled(
batch.map(ecf => client.emitRichECF({ dgii_env: 'ecf', ecf }))
);
results.push(...batchResults);
}
return {
successful: results.filter(r => r.status === 'fulfilled'),
failed: results.filter(r => r.status === 'rejected'),
};
}
Validación de datos
Validar antes de enviar
Validar en tu sistema antes de enviar evita rechazos y secuencias desperdiciadas:
function validateTotals(ecf) {
const t = ecf.Encabezado.Totales;
const calculated =
(t.MontoGravadoTotal || 0) +
(t.MontoExento || 0) +
(t.TotalITBIS || 0) +
(t.MontoImpuestoAdicional || 0);
if (Math.abs(calculated - t.MontoTotal) > 0.01) {
throw new Error('MontoTotal no coincide con gravado + exento + ITBIS');
}
}
function validateENCF(encf) {
// Serie E–Z (excepto P), tipo de 2 dígitos y secuencia de 10 dígitos
return /^[E-OQ-Z]\d{12}$/.test(encf);
}
Validar RNC y cédula
function validateRNC(rnc) {
const clean = rnc.replace(/[-\s]/g, '');
if (!/^\d{9}$/.test(clean)) return false;
const weights = [7, 9, 8, 6, 5, 4, 3, 2];
const sum = weights.reduce((acc, w, i) => acc + Number(clean[i]) * w, 0);
const remainder = sum % 11;
const checkDigit = remainder === 0 ? 2 : remainder === 1 ? 1 : 11 - remainder;
return Number(clean[8]) === checkDigit;
}
function validateCedula(cedula) {
const clean = cedula.replace(/[-\s]/g, '');
if (!/^\d{11}$/.test(clean)) return false;
// Algoritmo de Luhn
let sum = 0;
for (let i = 0; i < 10; i++) {
let n = Number(clean[i]) * (i % 2 === 0 ? 1 : 2);
if (n > 9) n -= 9;
sum += n;
}
return (10 - (sum % 10)) % 10 === Number(clean[10]);
}
Seguridad
Protección de credenciales
- Guarda la API Key en variables de entorno o en un gestor de secretos.
- Usa claves distintas por ambiente y por aplicación, con los permisos mínimos necesarios.
- Rota las claves periódicamente y revoca las que no uses.
- No incluyas la clave en el código, en repositorios ni en logs.
- No la envíes al navegador ni a apps móviles.
Cifrado de la API Key en base de datos
Si tienes que guardar la API Key en base de datos (por ejemplo, en un sistema multiempresa), cífrala:
const crypto = require('crypto');
class SecureStorage {
constructor(masterKeyHex) {
this.masterKey = Buffer.from(masterKeyHex, 'hex'); // 32 bytes
}
encrypt(text) {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', this.masterKey, iv);
const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
return {
iv: iv.toString('hex'),
encrypted: encrypted.toString('hex'),
authTag: cipher.getAuthTag().toString('hex'),
};
}
decrypt({ iv, encrypted, authTag }) {
const decipher = crypto.createDecipheriv('aes-256-gcm', this.masterKey, Buffer.from(iv, 'hex'));
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
return Buffer.concat([
decipher.update(Buffer.from(encrypted, 'hex')),
decipher.final(),
]).toString('utf8');
}
}
Monitoreo y observabilidad
Logs estructurados
Registra identificadores y códigos, nunca la API Key:
logger.info('e-CF emitido', {
documentId: res.document_id,
encf: res.encf,
trackId: res.track_id,
dgiiEnv: 'ecf',
});
logger.error('Falló la emisión', {
statusCode: error.statusCode,
message: error.message,
encf,
});
Métricas recomendadas
- Documentos emitidos por tipo y ambiente
- Documentos rechazados por la DGII
- Latencia de las llamadas a la API
- Errores por código HTTP
- Documentos pendientes de estado definitivo
Pruebas
Pruebas de integración contra testecf
const { FacturoClient } = require('../lib/facturo-client');
describe('Integración Facturo', () => {
const client = new FacturoClient({
apiKey: process.env.FACTURO_TEST_API_KEY,
defaultCompanyId: process.env.FACTURO_TEST_COMPANY_ID,
});
test('emite un e-CF en pruebas', async () => {
const res = await client.emitRichECF({ dgii_env: 'testecf', ecf: buildTestEcf() });
expect(res.encf).toMatch(/^E31\d{10}$/);
expect(res.track_id).toBeTruthy();
});
});
En pruebas unitarias, inyecta un fetch simulado con la opción customFetch del cliente Node.js para no depender de la red.
Checklist de producción
Seguridad
- API Key en variables de entorno o gestor de secretos
- Claves separadas por ambiente, con permisos mínimos
- La API Key nunca llega al navegador ni a los logs
- 2FA activado para usuarios administradores del portal
Emisión
- Integración validada en
testecfy certificada encertecf -
dgii_envconfigurado comoecfsolo en producción - Generación atómica de e-NCF
-
document_id,encfytrack_idpersistidos - Reintentos con backoff y uso del endpoint de reintento
- Proceso de anulación (ANECF) para secuencias no usadas
Operación
- Logs estructurados y métricas
- Alertas ante rechazos de la DGII y errores
5xx - Respaldo de los XML firmados
Si tienes dudas sobre la implementación, escribe a soporte@facturo.do o pregunta en Discord.