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 eCFFlujo 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 certificadosConfiguració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.