Skip to Content
UNIMAST

eCF - Facturación Electrónica

UNIMAST ERP implementa un sistema completo de facturación electrónica (eCF) que cumple con los estándares de la Dirección General de Impuestos Internos (DGII) de la República Dominicana, utilizando la librería oficial dgii-ecf para la generación, envío y gestión de comprobantes fiscales electrónicos.

🏗️ Arquitectura del Sistema eCF

Componentes Principales

app/ ├── lib/ecf/ # Lógica principal del sistema eCF │ ├── index.ts # Funciones principales de envío │ ├── actions.ts # Acciones del sistema eCF │ ├── sender/ # Envío de documentos │ │ ├── index.ts # Procesamiento principal │ │ ├── document-sender.ts # Envío de documentos │ │ ├── approval-sender.ts # Envío de aprobaciones │ │ └── void-sequence-sender.ts # Anulación de secuencias │ ├── receiver/ # Recepción de documentos │ │ ├── document-receiver.ts # Recepción de documentos │ │ ├── approval-receiver.ts # Recepción de aprobaciones │ │ └── validations.ts # Validaciones de documentos │ ├── converters/ # Conversores de datos │ ├── parsers/ # Parsers de documentos │ ├── types/ # Tipos y interfaces │ └── utils/ # Utilidades del sistema ├── app/(dashboard)/ecf/ # Interfaz de usuario │ ├── issued/ # Documentos emitidos │ ├── received/ # Documentos recibidos │ ├── types.ts # Tipos específicos de eCF │ ├── QRCodeImage.tsx # Generación de códigos QR │ └── ecf-detail-dialog.tsx # Diálogo de detalles └── lib/queue.ts # Colas de trabajo para eCF

Flujo de Trabajo del Sistema

🌍 Entornos y Configuración

Mapeo de Entornos

// app/lib/constants.ts import { ENVIRONMENT } from 'dgii-ecf/dist/networking/restClient'; const ENV_MAP: Record<string, ENVIRONMENT> = { TesteCF: ENVIRONMENT.DEV, // Entorno de desarrollo/pruebas CerteCF: ENVIRONMENT.CERT, // Entorno de certificación eCF: ENVIRONMENT.PROD // Entorno de producción }; export const ECF_ENV = ENV_MAP[process.env.ECF_ENV ?? ''] || ENVIRONMENT.DEV;

Variables de Entorno

# .env ECF_ENV=TesteCF # TesteCF | CerteCF | eCF REDIS_URL=redis://... # Para colas de trabajo ENCRYPTION_KEY=... # Clave de encriptación para certificados

Configuración de Build

// app/next.config.ts import CopyPlugin from 'copy-webpack-plugin'; export default { webpack: (config: any) => { config.plugins.push( new CopyPlugin({ patterns: [ { from: './lib/ecf/schemas', // Esquemas XML de eCF to: './schemas' // Copiados al build } ] }) ); return config; } };

📄 Tipos de Documentos eCF

Tipos de Comprobantes Soportados

// app/app/(dashboard)/ecf/types.ts import type { Prisma, TaxDocument, TaxDocumentReceived } from '@prisma/client'; export type EcfIssued = Prisma.TaxDocumentGetPayload<{ include: { creditNote: { include: { invoice: { include: { customer: true } } } }; invoice: { include: { customer: true } }; }; }>; export type EcfReceived = TaxDocumentReceived & { approvalId: string | null; }; export type AnyEcf = EcfIssued | EcfReceived;

Tipos de Documentos Fiscales

// Tipos de comprobantes según DGII enum TaxDocumentType { E31 = 'E31', // Factura Consumidor Final E32 = 'E32', // Factura Consumidor Final (Resumen) E33 = 'E33', // Nota de Débito E34 = 'E34', // Nota de Crédito E41 = 'E41', // Comprobante de Consumo E43 = 'E43', // Nota de Débito Consumo E44 = 'E44', // Nota de Crédito Consumo E45 = 'E45', // Comprobante de Regimen Especial E46 = 'E46', // Comprobante de Gubernamental E47 = 'E47' // Comprobante de Exportación }

Estados de Documentos

// app/lib/constants.ts export const STATUS_MAP = { Aceptado: TaxDocumentStatus.APPROVED, Rechazado: TaxDocumentStatus.REJECTED, ['Aceptado Condicional']: TaxDocumentStatus.CONDITIONALLY_APPROVED, ['En Proceso']: TaxDocumentStatus.IN_PROCESS }; export const TAX_DOCUMENT_STATUS_LABEL = { [TaxDocumentStatus.APPROVED]: 'Aceptado', [TaxDocumentStatus.REJECTED]: 'Rechazado', [TaxDocumentStatus.CONDITIONALLY_APPROVED]: 'Aceptado Condicional', [TaxDocumentStatus.IN_PROCESS]: 'En Proceso' };

🚀 Envío de Documentos eCF

Función Principal de Envío

// app/lib/ecf/index.ts export async function sendInvoiceECF({ invoiceId, tx = prisma, type = TaxDocumentType.E32, note }: { invoiceId: string; type: TaxDocumentType; note?: string; tx?: PrismaClient; }) { const session = await getSession(); if (!session) { throw new Error('No tienes permiso para realizar esta acción.'); } const settings = await getSettings(); const cert = await getCertificate(); if (!cert || !cert.key || !cert.cert) { throw new Error('No ECF certificate found'); } // Inicializar servicio eCF const ecf = new ECF(cert, ECF_ENV); await ecf.authenticate(); // Obtener siguiente NCF disponible const ecfResult = await getNextECF(type); if (!ecfResult) { throw new Error(`No existen secuencias disponible para el tipo ${type}`); } const { efc: noEcf } = ecfResult; const invoice = await prisma.invoice.findFirst({ where: { id: invoiceId }, include: { customer: true } }); // Información de referencia para notas de crédito/débito const referenceInfo = type === TaxDocumentType.E34 || type === TaxDocumentType.E33 ? { modifiedInvoiceNCF: invoice.ncf || '', modifiedInvoiceDate: invoice.issuedDate, modificationCode: ModificationCode.CANCELS_INVOICE, modificationReason: note } : undefined; // Convertir factura a formato electrónico const inputData = await convertToElectronicInvoice({ invoice, ncfType: type, settings, encf: noEcf, ...(referenceInfo && { referenceInfo }) }, tx); // Procesar envío del documento const taxDocument = await processDocumentSubmission(inputData, ecfResult, tx); // Actualizar factura con metadatos eCF await tx.invoice.update({ where: { id: invoiceId }, data: { ncf: invoice.ncf || noEcf, metadata: { ...(invoice.metadata as JsonObject), securityCode: taxDocument.metadata.securityCode, qrURL: taxDocument.qrURL }, updatedBy: session.user.id } }); return taxDocument; }

🔄 Procesamiento de Envío

Procesamiento Principal de Documentos

// app/lib/ecf/sender/index.ts export async function processDocumentSubmission( inputData: ElectronicInvoice, ecfResult: NextECFResult, tx: PrismaClient ): Promise<TaxDocument> { const cert = await getCertificate(); if (!cert || !cert.key || !cert.cert) { throw new Error('No ECF certificate found'); } // Inicializar servicio eCF const ecf = new ECF(cert, ECF_ENV); await ecf.authenticate(); // Convertir datos a XML const transformer = new Transformer(); let jsonData = await ECFBuilder(inputData); const xmlDoc = transformer.json2xml(jsonData); // Firmar documento XML const signature = new Signature(cert.key, cert.cert); const fileName = `${inputData.issuerRNC}${inputData.encf}.xml`; const signedXml = signature.signXml(xmlDoc, 'ECF'); // Generar código de seguridad const securityCode = getCodeSixDigitfromSignature(signedXml); if (!securityCode) { throw new Error('Unable to get the first 6 digits of the SignatureValue'); } // Determinar si es resumen o documento completo const totalAmount = inputData?.totals?.totalAmount || 0; const isSummary = inputData.type === TaxDocumentType.E32 && totalAmount < E32_LIMIT; // Generar URL del código QR let qrURL; if (isSummary) { qrURL = generateFcQRCodeURL( inputData.issuerRNC, inputData.encf, totalAmount, securityCode, ECF_ENV ); } else { qrURL = generateEcfQRCodeURL( inputData.issuerRNC, inputData.buyer?.taxId!, inputData.encf, totalAmount.toString(), formatDate(inputData.issueDate), jsonData.ECF.FechaHoraFirma._text, securityCode, ECF_ENV ); } // Guardar copia del XML const filepath = await saveXMLCopy(signedXml, fileName); // Crear documento fiscal en base de datos const document = await tx.taxDocument.create({ data: { id: inputData.encf, type: inputData.type, status: TaxDocumentStatus.IN_PROCESS, filepath, metadata: { securityCode }, qrURL, totalAmount, sequenceId: ecfResult.sequence, dgiiDeliveryStatus: TaxDocumentDeliverStatus.PENDING, customerDeliveryStatus: TaxDocumentDeliverStatus.PENDING } }); // Enviar según tipo de documento if (isSummary) { queueSendSummaryTaxDocument(inputData.encf); return document; } // Enviar documento completo a DGII try { const response = await ecf.sendElectronicDocument(signedXml, fileName); if (response?.trackId) { // Verificar estado inmediatamente const checkStatus = await ecf.statusTrackId(response.trackId); if (checkStatus && checkStatus.estado in STATUS_MAP) { const status = STATUS_MAP[checkStatus.estado as keyof typeof STATUS_MAP]; await tx.taxDocument.update({ where: { id: inputData.encf }, data: { status, trackId: response.trackId, metadata: { ...document.metadata, response: checkStatus } } }); } } } catch (error) { console.error('Error sending document:', error); queueSendTaxDocument(inputData.encf); } // Programar verificación de estado queueCheckTaxDocumentStatus(inputData.encf); return document; }

🔄 Sistema de Colas de Trabajo

Colas para eCF

// app/lib/queue.ts export function queueSendTaxDocument( encf: string, customerDirectory?: ServiceDirectoryResponse ) { const connection = new IORedis(process.env.REDIS_URL as string); const queue = new Queue(sendTaxDocumentQueueName, { connection }); return queue.add( `send_tax_document:${encf}`, { encf, customerDirectory }, { ...commonJobOptions, delay: 1000 * 60 } // Esperar 1 minuto ); } export function queueSendSummaryTaxDocument(encf: string) { const connection = new IORedis(process.env.REDIS_URL as string); const queue = new Queue(sendSummaryTaxDocumentQueueName, { connection }); return queue.add( `send_summary_tax_document:${encf}`, { encf }, { ...commonJobOptions, delay: 1000 * 20 } // Esperar 20 segundos ); } export function queueCheckTaxDocumentStatus(encf: string) { const connection = new IORedis(process.env.REDIS_URL as string); const queue = new Queue(checkDocumentStatus, { connection }); return queue.add( `check_document_status:${encf}`, { encf }, { ...commonJobOptions, delay: 1000 * 30 } // Esperar 30 segundos ); }

Workers de Procesamiento

// app/lib/queue.ts export function mountWorkers() { const connection = new IORedis(process.env.REDIS_URL || '', { maxRetriesPerRequest: null }); // Worker para envío de documentos const sendTaxDocumentWorker = new Worker( sendTaxDocumentQueueName, async (job) => { const { encf, customerDirectory } = job.data; return sendTaxDocument(encf, customerDirectory); }, { connection } ); // Worker para verificación de estado const checkDocumentStatusWorker = new Worker( checkDocumentStatus, async (job) => { const { encf } = job.data; return checkTaxDocumentStatus(encf); }, { connection } ); // Worker para envío de resúmenes const sendSummaryTaxDocumentWorker = new Worker( sendSummaryTaxDocumentQueueName, async (job) => { const { encf } = job.data; return sendSummaryTaxDocument(encf); }, { connection } ); // Configurar event handlers [sendTaxDocumentWorker, checkDocumentStatusWorker, sendSummaryTaxDocumentWorker] .forEach(worker => { worker.on('completed', (job) => { console.info(`Job ${job.id} completed successfully`); }); worker.on('failed', (job) => { console.error(`Job ${job.id} failed: ${job.failedReason}`); }); }); }

📱 Interfaz de Usuario

Documentos Emitidos

// app/app/(dashboard)/ecf/issued/page.tsx export default async function TransactionsPage(props: { searchParams: Promise<SearchParams>; }) { const searchParams = await props.searchParams; const { totalCount, documents, page, perPage } = await getTaxDocuments(searchParams); return ( <div> <div className="flex items-center justify-between mb-6"> <div> <h1 className="text-3xl font-bold tracking-tight"> Documentos eCF Emitidos </h1> <p className="text-muted-foreground"> Gestiona y visualiza todos los comprobantes electrónicos emitidos </p> </div> <div className="flex items-center gap-2"> <ExportButton params={{ docType: 'ecf-report', format: 'pdf' }} /> </div> </div> <DocumentsFilter /> <Card> <CardContent className="py-4"> <Table> <TableHeader> <TableRow> <TableHead>NCF</TableHead> <TableHead>Tipo de Comprobante</TableHead> <TableHead>Factura ID</TableHead> <TableHead>Cliente</TableHead> <TableHead>Monto</TableHead> <TableHead>Fecha y Hora</TableHead> <TableHead>Estado</TableHead> <TableHead>Acciones</TableHead> </TableRow> </TableHeader> <TableBody> {documents?.map((document) => { const invoice = document?.invoice || document?.creditNote?.invoice; const customerName = (invoice?.metadata as any)?.customerData?.name || invoice?.customer?.name; return ( <TableRow key={document.id}> <TableCell className="font-medium">{document.id}</TableCell> <TableCell>{TAX_TYPE_LABEL_MAP[document.type]}</TableCell> <TableCell>{invoice?.cid || `MW-${invoice?.legacySystemId}`}</TableCell> <TableCell>{customerName}</TableCell> <TableCell className="text-right"> {formatCurrency(document.totalAmount)} </TableCell> <TableCell> {dayjs(document.createdAt).format('DD/MM/YYYY hh:mm:ss A')} </TableCell> <TableCell> <Badge className={STATUS_BG_MAP[document.status]}> {TAX_DOCUMENT_STATUS_LABEL[document.status]} </Badge> </TableCell> <TableCell> <TransactionRowActions document={document} /> </TableCell> </TableRow> ); })} </TableBody> </Table> </CardContent> </Card> <Pagination totalCount={totalCount} currentPage={page} perPage={perPage} /> </div> ); }

Documentos Recibidos

// app/app/(dashboard)/ecf/received/page.tsx export default async function TransactionsPage(props: { searchParams: Promise<SearchParams>; }) { const searchParams = await props.searchParams; const { totalCount, page, perPage, rows } = await getTaxDocuments(searchParams); return ( <div> <div className="flex items-center justify-between mb-6"> <div> <h1 className="text-3xl font-bold tracking-tight"> Documentos eCF Recibidos </h1> <p className="text-muted-foreground"> Gestiona y visualiza todos los comprobantes electrónicos recibidos </p> </div> <div className="flex items-center gap-2"> <ExportButton params={{ docType: 'ecf-report', format: 'pdf' }} /> </div> </div> <DocumentsFilter /> <Card> <CardContent className="py-4"> <Table> <TableHeader> <TableRow> <TableHead>NCF</TableHead> <TableHead>Tipo de Comprobante</TableHead> <TableHead>RNC Emisor</TableHead> <TableHead>Emisor</TableHead> <TableHead>Monto</TableHead> <TableHead>Fecha y Hora</TableHead> <TableHead>Estado</TableHead> <TableHead>Acciones</TableHead> </TableRow> </TableHeader> <TableBody> {rows?.map((document) => ( <TableRow key={document.id}> <TableCell className="font-medium">{document.id}</TableCell> <TableCell>{TAX_TYPE_LABEL_MAP[document.type]}</TableCell> <TableCell>{document.fromRNC}</TableCell> <TableCell>{document.fromName}</TableCell> <TableCell className="text-right"> {formatCurrency(document?.amount)} </TableCell> <TableCell> {dayjs(document.createdAt).format('DD/MM/YYYY hh:mm:ss A')} </TableCell> <TableCell> <Badge variant={document.approvalId ? 'success' : 'secondary'}> {document.approvalId ? 'Aprobado' : 'Recibido'} </Badge> </TableCell> <TableCell> <ReceivedRowActions row={document} /> </TableCell> </TableRow> ))} </TableBody> </Table> </CardContent> </Card> <Pagination totalCount={totalCount} currentPage={page} perPage={perPage} /> </div> ); }

🔐 Gestión de Certificados

Obtención de Certificados

// app/lib/ecf/actions.ts export async function getCertificate(): Promise<Certificate | null> { try { const settings = await getSettings(); if (!settings.ecfCertificatePath || !settings.ecfPrivateKeyPath) { return null; } const cert = await readFile(settings.ecfCertificatePath, 'utf-8'); const key = await readFile(settings.ecfPrivateKeyPath, 'utf-8'); return { cert, key }; } catch (error) { console.error('Error reading ECF certificate:', error); return null; } }

Configuración de Certificados

// app/app/(dashboard)/settings/general/actions.ts export async function updateECFCertificate( formData: FormData ): Promise<{ success: boolean; message: string }> { try { const session = await getSession(); if (!session) { throw new Error('No tienes permiso para realizar esta acción.'); } const certificateFile = formData.get('ecfCertificate') as File; const privateKeyFile = formData.get('ecfPrivateKey') as File; if (!certificateFile || !privateKeyFile) { throw new Error('Certificado y clave privada son requeridos'); } // Validar archivos if (!certificateFile.name.endsWith('.crt') && !certificateFile.name.endsWith('.pem')) { throw new Error('El certificado debe ser un archivo .crt o .pem'); } if (!privateKeyFile.name.endsWith('.key') && !privateKeyFile.name.endsWith('.pem')) { throw new Error('La clave privada debe ser un archivo .key o .pem'); } // Guardar archivos const certPath = path.join(process.cwd(), 'certs', 'ecf-certificate.crt'); const keyPath = path.join(process.cwd(), 'certs', 'ecf-private-key.key'); await writeFile(certPath, await certificateFile.arrayBuffer()); await writeFile(keyPath, await privateKeyFile.arrayBuffer()); // Actualizar configuración await prisma.settings.update({ where: { id: 'general' }, data: { ecfCertificatePath: certPath, ecfPrivateKeyPath: keyPath } }); return { success: true, message: 'Certificado eCF actualizado exitosamente' }; } catch (error) { console.error('Error updating ECF certificate:', error); return { success: false, message: 'Error al actualizar certificado eCF' }; } }

📊 Filtros y Búsquedas

Filtros de Documentos Emitidos

// app/app/(dashboard)/ecf/issued/documents-filter.tsx export default function DocumentsFilter() { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const [startDate, setStartDate] = useState<Date | undefined>( searchParams.get('startDate') ? new Date(searchParams.get('startDate')!) : undefined ); const [endDate, setEndDate] = useState<Date | undefined>( searchParams.get('endDate') ? new Date(searchParams.get('endDate')!) : undefined ); const [status, setStatus] = useState( searchParams.get('status') || 'ANY' ); const [type, setType] = useState( searchParams.get('type') || 'ANY' ); const applyFilters = () => { const params = new URLSearchParams(); if (startDate) params.set('startDate', startDate.toISOString()); if (endDate) params.set('endDate', endDate.toISOString()); if (status !== 'ANY') params.set('status', status); if (type !== 'ANY') params.set('type', type); router.replace(`${pathname}?${params.toString()}`); }; const clearFilters = () => { setStartDate(undefined); setEndDate(undefined); setStatus('ANY'); setType('ANY'); router.replace(pathname); }; return ( <Card className="mb-6"> <CardContent className="py-4"> <div className="flex flex-wrap gap-4 items-center"> <DatePicker selected={startDate} onChange={setStartDate} placeholderText="Fecha inicial" className="w-40" /> <DatePicker selected={endDate} onChange={setEndDate} placeholderText="Fecha final" className="w-40" /> <Select value={status} onValueChange={setStatus}> <SelectTrigger className="w-40"> <SelectValue placeholder="Estado" /> </SelectTrigger> <SelectContent> <SelectItem value="ANY">Cualquier estado</SelectItem> <SelectItem value="APPROVED">Aceptado</SelectItem> <SelectItem value="REJECTED">Rechazado</SelectItem> <SelectItem value="IN_PROCESS">En Proceso</SelectItem> <SelectItem value="CONDITIONALLY_APPROVED">Aceptado Condicional</SelectItem> </SelectContent> </Select> <Select value={type} onValueChange={setType}> <SelectTrigger className="w-40"> <SelectValue placeholder="Tipo" /> </SelectTrigger> <SelectContent> <SelectItem value="ANY">Cualquier tipo</SelectItem> <SelectItem value="E31">Factura Consumidor Final</SelectItem> <SelectItem value="E32">Factura Consumidor Final (Resumen)</SelectItem> <SelectItem value="E33">Nota de Débito</SelectItem> <SelectItem value="E34">Nota de Crédito</SelectItem> </SelectContent> </Select> <Button onClick={applyFilters}>Aplicar Filtros</Button> <Button variant="outline" onClick={clearFilters}> Limpiar </Button> </div> </CardContent> </Card> ); }

🔄 Acciones de Documentos

Acciones de Documentos Emitidos

// app/app/(dashboard)/ecf/issued/issued-documents-row-actions.tsx export default function TransactionRowActions({ document }: { document: EcfIssued }) { const [isDetailOpen, setIsDetailOpen] = useState(false); return ( <> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" className="h-8 w-8 p-0"> <MoreHorizontal className="h-4 w-4" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuItem onClick={() => setIsDetailOpen(true)}> <Eye className="mr-2 h-4 w-4" /> Ver Detalle </DropdownMenuItem> <DropdownMenuItem> <Download className="mr-2 h-4 w-4" /> Descargar XML </DropdownMenuItem> <DropdownMenuItem> <QrCode className="mr-2 h-4 w-4" /> Ver QR </DropdownMenuItem> {document.status === TaxDocumentStatus.REJECTED && ( <DropdownMenuItem> <RefreshCw className="mr-2 h-4 w-4" /> Reintentar Envío </DropdownMenuItem> )} </DropdownMenuContent> </DropdownMenu> <EcfDetailDialog document={document} open={isDetailOpen} onOpenChange={setIsDetailOpen} /> </> ); }

Acciones de Documentos Recibidos

// app/app/(dashboard)/ecf/received/received-documents-row-actions.tsx export default function ReceivedRowActions({ row }: { row: EcfReceived }) { const [isSendApprovalOpen, setIsSendApprovalOpen] = useState(false); const [isDetailOpen, setIsDetailOpen] = useState(false); return ( <> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" className="h-8 w-8 p-0"> <MoreHorizontal className="h-4 w-4" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuItem onClick={() => setIsDetailOpen(true)}> <Eye className="mr-2 h-4 w-4" /> Ver Detalle </DropdownMenuItem> <DropdownMenuItem onClick={() => setIsSendApprovalOpen(true)}> <Send className="mr-2 h-4 w-4" /> Enviar Aprobación </DropdownMenuItem> <DropdownMenuItem> <Download className="mr-2 h-4 w-4" /> Descargar XML </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> <SendApprovalDialog document={row} open={isSendApprovalOpen} onOpenChange={setIsSendApprovalOpen} /> <EcfDetailDialog document={row} open={isDetailOpen} onOpenChange={setIsDetailOpen} /> </> ); }

🚀 Mejores Prácticas

1. Manejo de Errores

// ✅ CORRECTO: Manejo robusto de errores export async function sendInvoiceECF(invoiceId: string) { try { const session = await getSession(); if (!session) { throw new Error('No tienes permiso para realizar esta acción.'); } const cert = await getCertificate(); if (!cert || !cert.key || !cert.cert) { throw new Error('Certificado eCF no encontrado. Configure el certificado en Configuración > General.'); } const ecfResult = await getNextECF(type); if (!ecfResult) { throw new Error(`No existen secuencias disponibles para el tipo de documento ${type}. Contacte al administrador.`); } // Procesar envío const result = await processDocumentSubmission(inputData, ecfResult); toast.success('Documento eCF enviado exitosamente'); return result; } catch (error) { console.error('Error sending eCF document:', error); if (error.message.includes('Certificado')) { toast.error('Error de certificado: ' + error.message); } else if (error.message.includes('secuencias')) { toast.error('Error de secuencias: ' + error.message); } else { toast.error('Error al enviar documento eCF. Inténtelo de nuevo.'); } throw error; } } // ❌ INCORRECTO: Manejo básico de errores export async function sendInvoiceECF(invoice: Invoice) { // Enviar directamente sin validar return processDocumentSubmission(invoice); }

2. Validaciones de Datos

// ✅ CORRECTO: Validaciones completas antes del envío export async function validateInvoiceForECF(invoice: Invoice): Promise<ValidationResult> { const errors: string[] = []; // Validar cliente if (!invoice.customer?.taxId) { errors.push('El cliente debe tener un RNC válido'); } // Validar items if (!invoice.items || invoice.items.length === 0) { errors.push('La factura debe tener al menos un item'); } // Validar montos if (invoice.totalAmount <= 0) { errors.push('El monto total debe ser mayor a 0'); } // Validar fechas if (!invoice.issuedDate) { errors.push('La fecha de emisión es requerida'); } return { isValid: errors.length === 0, errors }; } // ❌ INCORRECTO: Sin validaciones export async function sendInvoiceECF(invoice: Invoice) { // Enviar directamente sin validar return processDocumentSubmission(invoice); }

3. Logging y Monitoreo

// ✅ CORRECTO: Logging detallado para debugging export async function processDocumentSubmission(inputData: ElectronicInvoice) { const startTime = Date.now(); console.log(`[eCF] Starting document submission for ${inputData.encf}`, { type: inputData.type, amount: inputData.totals?.totalAmount, customer: inputData.buyer?.taxId }); try { // Procesar documento const result = await sendToDGII(inputData); const duration = Date.now() - startTime; console.log(`[eCF] Document ${inputData.encf} processed successfully in ${duration}ms`, { trackId: result.trackId, status: result.status }); return result; } catch (error) { const duration = Date.now() - startTime; console.error(`[eCF] Error processing document ${inputData.encf} after ${duration}ms:`, { error: error.message, stack: error.stack, inputData: { encf: inputData.encf, type: inputData.type, amount: inputData.totals?.totalAmount } }); throw error; } }

🔧 Troubleshooting

Problemas Comunes

1. Error de Certificado

// Verificar configuración del certificado const cert = await getCertificate(); if (!cert) { console.error('Certificate not found. Check settings.'); // Verificar rutas en configuración const settings = await getSettings(); console.log('ECF Certificate Path:', settings.ecfCertificatePath); console.log('ECF Private Key Path:', settings.ecfPrivateKeyPath); }

2. Error de Secuencias

// Verificar secuencias disponibles const sequences = await prisma.taxSequence.findMany({ where: { type: documentType, isActive: true } }); if (sequences.length === 0) { console.error(`No sequences available for type ${documentType}`); // Crear nueva secuencia o activar existente }

3. Error de Envío a DGII

// Verificar conectividad y configuración try { const ecf = new ECF(cert, ECF_ENV); await ecf.authenticate(); console.log('Authentication successful'); } catch (error) { console.error('Authentication failed:', error); // Verificar certificado, entorno y conectividad }

Debug de Documentos eCF

// Función de debug para documentos export async function debugECFDocument(encf: string) { console.log(`[eCF Debug] Document: ${encf}`); // Verificar documento en base de datos const doc = await prisma.taxDocument.findUnique({ where: { id: encf }, include: { invoice: true } }); if (!doc) { console.error('Document not found in database'); return; } console.log('Document status:', doc.status); console.log('Document metadata:', doc.metadata); console.log('Document filepath:', doc.filepath); // Verificar archivo XML try { const xmlContent = await readFile(doc.filepath, 'utf-8'); console.log('XML content length:', xmlContent.length); console.log('XML starts with:', xmlContent.substring(0, 100)); } catch (error) { console.error('Error reading XML file:', error); } // Verificar colas de trabajo const queueStatus = await checkQueueStatus(encf); console.log('Queue status:', queueStatus); }

¿Necesitas más detalles sobre algún aspecto específico? Revisa la documentación de background jobs para entender cómo funcionan las colas de trabajo del sistema eCF, o la documentación de APIs para aprender sobre los endpoints relacionados con facturación electrónica.