sunat-engine-nest
v2.0.0
Published
Motor de Facturación Electrónica SUNAT UBL 2.1 para NestJS
Readme
sunat-engine-nest
Motor de Facturación Electrónica SUNAT UBL 2.1 para NestJS.
Soporta Facturas, Boletas, Notas de Crédito/Débito, Guías de Remisión (GRE), Resúmenes Diarios y Comunicaciones de Baja — firma XML, envío a SUNAT/OSE, parseo de CDR y generación de PDF.
Índice
- Instalación
- Configuración
- Uso
- Generación de PDF (función paga)
- Testing / Modo simulado
- Respuesta del motor
- Endpoints SUNAT
- Soporte y actualizaciones
- Licencia
Instalación
npm install sunat-engine-nest
# o
pnpm add sunat-engine-nestConfiguración
Registra el módulo en tu AppModule. Puedes definir las credenciales de tu empresa una sola vez en el módulo usando el campo sunat, evitando repetirlas en cada llamada.
Registro síncrono
import { SunatEngineModule } from 'sunat-engine-nest';
@Module({
imports: [
SunatEngineModule.forRoot({
sunat: {
ruc: '20123456789',
solUser: 'MODDATOS',
solPass: 'moddatos',
certPem: 'BASE64_DEL_P12_O_PEM',
endpointMode: 'beta', // 'beta' | 'produccion'
},
gre: {
authUrl: 'https://gre-test.nubefact.com/v1',
apiUrl: 'https://gre-test.nubefact.com/v1',
clientId: 'TU_CLIENT_ID',
clientSecret: 'TU_CLIENT_SECRET',
},
}),
],
})
export class AppModule {}Registro asíncrono
Recomendado cuando las credenciales dependen de variables de entorno.
import { SunatEngineModule } from 'sunat-engine-nest';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
SunatEngineModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
provider: config.get('PROVIDER'), // 'sunat' | 'ose'
sunat: {
ruc: config.get('SUNAT_RUC'),
solUser: config.get('SUNAT_SOL_USER'),
solPass: config.get('SUNAT_SOL_PASS'),
certPem: config.get('SUNAT_CERT_PEM'),
endpointMode: config.get('SUNAT_MODE') ?? 'beta',
},
gre: {
authUrl: config.get('GRE_AUTH_URL'),
apiUrl: config.get('GRE_API_URL'),
clientId: config.get('GRE_CLIENT_ID'),
clientSecret: config.get('GRE_CLIENT_SECRET'),
},
ose: {
url: config.get('OSE_URL'),
token: config.get('OSE_TOKEN'),
},
}),
}),
],
})
export class AppModule {}Proveedor: SUNAT directo vs OSE
Puedes elegir enviar tus comprobantes directamente a SUNAT o a través de un OSE (Operador de Servicios Electrónicos) autorizado como Nubefact, EFACT, DigiFlow, Bizlinks, entre otros.
// Envío directo a SUNAT (por defecto)
SunatEngineModule.forRoot({
provider: 'sunat',
sunat: { ruc: '...', solUser: '...', solPass: '...', certPem: '...' },
})
// Envío a través de un OSE
SunatEngineModule.forRoot({
provider: 'ose',
sunat: { ruc: '...', certPem: '...' }, // solo se usa para firmar el XML
ose: {
url: 'https://api.tuose.com/v1/documents',
token: 'TU_TOKEN_OSE', // Bearer token
// o en lugar de token:
// username: 'usuario',
// password: 'clave',
},
})El
OseClientes genérico y compatible con cualquier OSE que acepte{ fileName, contentFile }(ZIP en base64) vía POST con autenticación Bearer o Basic.
Certificado digital
El motor acepta el certificado en tres formatos:
// Formato 1: ruta al archivo .p12 o .pem en disco
const credentials: CompanyCredentials = {
certPem: '/ruta/al/certificado.p12',
};
// Formato 2: ruta a cert PEM + ruta a clave privada PEM por separado
const credentials: CompanyCredentials = {
certPem: '/ruta/al/cert.pem',
certKey: '/ruta/al/key.pem',
};
// Formato 3: contenido directo en base64 (.p12) o texto PEM
const credentials: CompanyCredentials = {
certPem: 'MIIKJAIBAzCCCd4GCSqGSI...', // .p12 en base64
};Variables de entorno
# Credenciales SUNAT
SUNAT_RUC=20123456789
SUNAT_SOL_USER=MODDATOS
SUNAT_SOL_PASS=moddatos
SUNAT_CERT_PEM=BASE64_DEL_P12
SUNAT_MODE=beta
# GRE (Guía de Remisión Electrónica)
GRE_AUTH_URL=https://gre-test.nubefact.com/v1
GRE_API_URL=https://gre-test.nubefact.com/v1
GRE_CLIENT_ID=tu_client_id
GRE_CLIENT_SECRET=tu_client_secret
# OSE (solo si provider=ose)
PROVIDER=ose
OSE_URL=https://api.tuose.com/v1/documents
OSE_TOKEN=tu_token_aquiUso
Enviar un comprobante
Inyecta SunatEngineService en cualquier servicio o controlador. Si configuraste sunat:{} en el módulo, no necesitas pasar credenciales en cada llamada:
import { Injectable } from '@nestjs/common';
import { SunatEngineService, InvoicePayload } from 'sunat-engine-nest';
@Injectable()
export class InvoicesService {
constructor(private readonly engine: SunatEngineService) {}
async sendInvoice() {
// Sin credenciales — usa las del módulo automáticamente
const payload: InvoicePayload = {
tipoOperacion: '0101',
tipoDoc: '01', // 01=Factura, 03=Boleta
serie: 'F001',
correlativo: '00000001',
fechaEmision: '2026-07-20T00:00:00-05:00',
tipoMoneda: 'PEN',
company: {
ruc: '20123456789',
razonSocial: 'MI EMPRESA S.A.C.',
address: { direccion: 'Av. Principal 123, Lima' },
},
client: {
tipoDoc: '6',
numDoc: '20987654321',
rznSocial: 'CLIENTE S.A.C.',
},
details: [
{
unidad: 'NIU',
cantidad: 2,
descripcion: 'Producto de prueba',
mtoValorUnitario: 100,
mtoValorVenta: 200,
mtoBaseIgv: 200,
porcentajeIgv: 18,
igv: 36,
tipAfeIgv: '10',
totalImpuestos: 36,
mtoPrecioUnitario: 118,
},
],
legends: [{ code: '1000', value: 'DOSCIENTOS TREINTA Y SEIS Y 00/100 SOLES' }],
mtoOperGravadas: 200,
mtoIGV: 36,
totalImpuestos: 36,
valorVenta: 200,
subTotal: 236,
mtoImpVenta: 236,
};
return this.engine.sendInvoice(payload); // credenciales del módulo
}
}También puedes sobreescribir las credenciales por llamada cuando manejas múltiples empresas:
// Credenciales específicas para esta llamada (tienen prioridad sobre el módulo)
return this.engine.sendInvoice(payload, {
ruc: '20987654321',
solUser: 'OTRO_USUARIO',
solPass: 'otra_clave',
certPem: 'OTRO_CERT_BASE64',
});Documentos soportados
| Tipo | Método de envío | Descripción | PDF |
|------|------------------|-------------|-----|
| Factura (01) | sendInvoice() | Factura electrónica — SOAP síncrono | ✅ generateInvoice() |
| Boleta (03) | sendInvoice() | Boleta de venta electrónica — SOAP síncrono | ✅ generateInvoice() |
| Nota de Crédito (07) | sendNote() | NC electrónica — SOAP síncrono | ✅ generateNote() |
| Nota de Débito (08) | sendNote() | ND electrónica — SOAP síncrono | ✅ generateNote() |
| Guía de Remisión (09) | sendDespatch() | GRE 2022 — REST/OAuth asíncrono | ✅ generateDespatch() |
| Resumen Diario (RC) | sendSummary() | Resumen de boletas — SOAP asíncrono | — (comunicación interna a SUNAT) |
| Comunicación de Baja (RA) | sendVoided() | Baja de comprobantes — SOAP asíncrono | — (comunicación interna a SUNAT) |
| Consulta de ticket | getTicketStatus() | Estado de procesos asíncronos | — |
Generación de PDF (función paga)
El envío a SUNAT (XML, firma, CDR, GRE) sigue siendo MIT / gratuito. La generación de PDF es una función paga: necesitas una license key para usarla — escríbeme por WhatsApp para adquirir una.
La licencia se valida localmente (firma Ed25519), sin llamadas a ningún servidor externo — tus datos de facturación nunca salen de tu infraestructura.
SunatEngineModule.forRoot({
sunat: { ruc: '...', solUser: '...', solPass: '...', certPem: '...' },
pdf: {
licenseKey: 'LA_LICENSE_KEY_QUE_TE_ENTREGO', // o desde tu ConfigService/env
},
})import { DocumentPdfService } from 'sunat-engine-nest';
@Injectable()
export class InvoicesService {
constructor(
private readonly engine: SunatEngineService,
private readonly pdf: DocumentPdfService,
) {}
async getPdf(payload: InvoicePayload): Promise<Buffer> {
return this.pdf.generateInvoice(payload, 'A4'); // 'A4' | 'TICKET_80MM' | 'STICKER_A6'
}
}Sin pdf.licenseKey configurada (o con una key inválida/expirada), los métodos de PDF lanzan un PdfLicenseError.
Personalizar diseño (logo y color de marca)
El formato A4 acepta branding opcional. Si no se especifica, usa un azul corporativo por defecto.
return this.pdf.generateInvoice(payload, 'A4', {
licenseKey: 'OTRA_LICENSE_KEY', // opcional — tiene prioridad sobre la del módulo
branding: {
color: '#0f766e', // hex, opcional
logoBase64: 'data:image/png;base64,...', // opcional
},
});Nota de Crédito / Débito
Mismo diseño y misma licencia que las facturas.
await this.pdf.generateNote(notePayload, 'A4'); // 'A4' | 'TICKET_80MM' | 'STICKER_A6'Guía de Remisión (GRE)
Solo formato A4 — incluye destinatario, datos de traslado, punto de partida/llegada, transportista/vehículo/conductor.
await this.pdf.generateDespatch(despatchPayload, {
branding: { color: '#0f766e' }, // opcional
});Resumen Diario (RC) y Comunicación de Baja (RA) no tienen representación en PDF — son comunicaciones internas a SUNAT, no documentos que se entreguen a un cliente ni que viajen con la mercadería.
Testing / Modo simulado
SUNAT beta rechaza sendSummary (RC/RA) con el RUC genérico de prueba 20000000001 — ese RUC solo funciona de forma confiable para sendBill (facturas y boletas). Para probar el flujo completo sin depender del entorno beta, el módulo incluye un cliente SOAP simulado que devuelve respuestas válidas sin realizar ninguna llamada HTTP.
Opción 1 — useFake: true en el módulo (recomendado)
// app.module.ts (para tests)
SunatEngineModule.forRoot({
useFake: true, // ← activa FakeSunatSoapClient automáticamente
sunat: {
ruc: '20000000001',
solUser: 'MODDATOS',
solPass: 'moddatos',
certPem: 'BASE64_DEL_P12',
},
})
// También con forRootAsync:
SunatEngineModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
useFake: config.get('NODE_ENV') === 'test',
sunat: { ruc: config.get('SUNAT_RUC'), ... },
}),
})Opción 2 — overrideProvider en NestJS Testing
import { Test } from '@nestjs/testing';
import { SunatSoapClient, FakeSunatSoapClient } from 'sunat-engine-nest';
const moduleRef = await Test.createTestingModule({
imports: [SunatEngineModule.forRoot({ sunat: { ... } })],
})
.overrideProvider(SunatSoapClient)
.useClass(FakeSunatSoapClient)
.compile();Comportamiento del cliente simulado
| Método | Respuesta simulada |
|--------|-------------------|
| sendBill | CDR ZIP con ResponseCode=0 (ACEPTADA) |
| sendSummary | Ticket numérico incremental (1000000001, 1000000002, ...) |
| getStatus | CDR ZIP con ResponseCode=0 (ACEPTADA) |
No se realiza ninguna llamada HTTP — el XML se genera y firma normalmente, solo el envío a SUNAT es simulado.
Respuesta del motor
interface EngineResponse {
xml?: string; // XML firmado en base64
hash?: string; // SHA256 del XML
sunatResponse?: {
success?: boolean;
ticket?: string; // procesos asíncronos (RC/RA/GRE)
cdrZip?: string; // ZIP CDR en base64 (procesos síncronos)
cdrResponse?: {
code?: string; // 0=aceptado, 2xxx=rechazado, 4xxx=obs
description?: string;
notes?: string[];
};
error?: { code?: string | number; message?: string };
};
}Endpoints SUNAT
| Modo | Factura/Boleta/NC/ND | GRE |
|------|----------------------|-----|
| beta | e-beta.sunat.gob.pe | gre-test.nubefact.com |
| produccion | e-factura.sunat.gob.pe | api-cpe.sunat.gob.pe |
Soporte y actualizaciones
SUNAT actualiza continuamente sus esquemas, validaciones y normativas de facturación electrónica. Este paquete recibe mantenimiento activo para mantenerse alineado con cada cambio oficial, garantizando que tu integración siga funcionando sin interrupciones.
Si este proyecto te ha sido útil y quieres apoyar su desarrollo continuo, puedes hacerlo de las siguientes formas:
Yape / WhatsApp
Número: +51 907 596 305
- Escríbeme por WhatsApp para consultas, soporte o donaciones.
Cualquier aporte, por pequeño que sea, ayuda a mantener la librería actualizada y en funcionamiento. ¡Gracias por tu apoyo!
Licencia
MIT — excepto la generación de PDF (DocumentPdfService), que requiere una license key comercial. Ver Generación de PDF.
