FacturaGo
Librería Go para comprobantes electrónicos SUNAT (SEE Del Contribuyente). Esta página ejecuta la librería compilada a WebAssembly: no hay backend, no hay llamadas de red y ningún dato ingresado sale del navegador.
Arquitectura
Librería stateless, sin dependencias externas: solo la biblioteca estándar de Go. La API son funciones sobre structs planos.
- Input — un
Document(oSummarypara el resumen diario,Voidedpara la comunicación de baja), los datos del emisor y su certificado PKCS#12. - Output — el XML UBL 2.1 firmado con XMLDSig, el ZIP de transmisión, el nombre de archivo, el digest, la cadena del QR y, tras la transmisión, el CDR de SUNAT.
CompleteTotals, ValidateDocument, BuildXML y Emit son puras: sin red, sin disco y sin estado compartido. El único punto de
red es el cliente SOAP, invocado de forma explícita.
No hay persistencia, caché ni interfaces de almacenamiento. Correlativos, reintentos, permisos y almacenamiento corresponden al sistema que integra la librería. La separación es deliberada: las reglas de SUNAT son idénticas para todo emisor en Perú, mientras que el resto varía en cada implementación.
Qué ejecuta esta página
El mismo binario Go, compilado a WebAssembly, con los 30 documentos de las
pruebas cargados como fixtures editables. Cada edición dispara el ciclo completo: CompleteTotals → ValidateDocument → BuildXML → Emit.
El alcance de esta página termina en el ZIP firmado. No transmite: el envío requiere un servidor y las credenciales reales de un emisor.
Representación de importes
Los montos son enteros en céntimos dentro de la librería y cadenas decimales en el límite
JSON/WebAssembly. Motivo: number en JavaScript es un flotante binario
IEEE 754 y no representa valores decimales de forma exacta. 0.07 sumado diez
veces da 0.7000000000000002, y (2.675).toFixed(2) devuelve "2.67" en lugar de "2.68". Ninguna operación aritmética se
ejecuta en JavaScript.
Los campos de importe y cantidad son numéricos en la interfaz y el valor ingresado se
transmite sin transformación. 4200.005 llega íntegro y ParseScaled lo rechaza por exceder los 2 decimales del campo, en lugar de
redondearlo a 4200.01.
Demostración
- Seleccionar un fixture en la lista lateral. Están agrupados según lo que ejercitan: casos corrientes, importes y cantidades, descuentos y cargos, tipos de afectación, notas, envíos por lote y dos documentos con errores de validación intencionales.
- Campos editables: los del comprobante, los del cliente y las celdas de las líneas. El valor de venta y el IGV son derivados y de solo lectura.
- Pestañas: Documento, el input; XML, el UBL 2.1 firmado
con su nombre de archivo, digest, QR y ZIP descargable; Validación, el
resultado de
ValidateDocument; Emisor y Firma, el certificado en uso. - recalcular importes derivados controla el parámetro
Recompute. Desactivado,CompleteTotalspreserva los importes declarados y solo completa los que estén en cero.
Uso e integración
Sin dependencias externas. Requiere Go 1.26.
go get github.com/ivanjoz/facturago Cuatro paquetes, ordenados por lo que conoce cada uno:
| Paquete | Responsabilidad |
|---|---|
facturago | de documento a transmisión firmada: UBL, firma, archivo |
facturago/model | los tipos, los catálogos, los totales y las reglas |
facturago/sunat | el servicio web: transmisión e interpretación de la respuesta |
facturago/utils | los estándares: XML canónico, XMLDSig, PKCS#12, decimales de punto fijo |
Las dependencias van en un sentido: sunat y facturago dependen de model, facturago además de utils, y model y utils solo de la biblioteca estándar.
Emisión
import (
"github.com/ivanjoz/facturago"
"github.com/ivanjoz/facturago/model"
"github.com/ivanjoz/facturago/sunat"
)
issuer := model.Issuer{
RUC: "20000000001",
LegalName: "EMPRESA S.A.C.",
SolUser: "MODDATOS", // usuario SOL secundario, sin el RUC
SolPassword: "MODDATOS",
PKCS12: pfx, // los bytes del .pfx
PKCS12Password: password,
Environment: model.EnvBeta,
}
doc := model.Document{
Type: model.Factura,
Series: "F001",
Correlativo: 123,
IssuedAt: time.Now(),
Customer: model.Party{
DocType: model.IDDocRUC,
DocNumber: "20000000002",
LegalName: "CLIENTE S.A.C.",
},
Lines: []model.Line{{
Description: "PRODUCTO 1",
Quantity: model.Units(2),
UnitValue: 10_000, // S/ 100.00: los importes son céntimos
}},
}
// Completa totales, valida, arma el UBL, firma y empaqueta.
// Puro: sin red y sin disco. Todavía no se emite nada.
emission, err := facturago.Emit(issuer, &doc)
// Transmisión. El CDR es la respuesta de SUNAT y el archivo a conservar.
cdr, err := new(sunat.Client).SendBill(issuer, emission)
if !cdr.Accepted() {
// Un rechazo es definitivo; una falla de transporte se reintenta.
return cdr.Err()
} Emit invoca model.CompleteTotals y model.ValidateDocument. Para inspeccionar o cotizar un documento sin emitirlo,
se las invoca directamente.
model.Emission devuelve FileName (sin extensión), XML (firmado y canónico), Digest (el DigestValue de la firma,
último campo del QR) y Zip (lo que se transmite). Nada se cachea entre
llamadas, que es lo que hace segura a la librería en un proceso multi-tenant.
Tipos de importe
No hay float64 en la librería. Los importes son centésimas de la unidad monetaria.
| Tipo | Significado |
|---|---|
model.Cents (int32) | un importe: total de línea, tributo, descuento |
model.TotalCents (int64) | un importe que agrega otros: los totales del documento |
model.Quantity (int64) | escalado por un millón: Units(3) es 3, 2_500_000 es 2.5 |
model.Percent (int32) | centésimas de por ciento: Percentage(18) es 18 % |
El redondeo ocurre solo donde hay una división. Un valor unitario por debajo del céntimo no
se redondea en el documento: se fija el Value de la línea y el valor unitario
que lee SUNAT se deriva por división larga exacta, de modo que
cantidad × valor unitario = total de línea se cumple a los diez decimales que SUNAT admite.
Cobertura
| Documento | Código | Estado |
|---|---|---|
| Factura | 01 | aceptado en SUNAT beta |
| Boleta | 03 | aceptado en SUNAT beta |
| Nota de crédito | 07 | implementado, sin transmisión verificada |
| Nota de débito | 08 | implementado, sin transmisión verificada |
| Resumen diario | RC | aceptado en SUNAT beta |
| Comunicación de baja | RA | implementado, sin transmisión verificada |
Por documento: los seis tipos de afectación del catálogo 07, descuentos por ítem y globales, cargos, anticipos, cuotas de crédito, ICBPER, ISC, IVAP, exportación, transferencias gratuitas y las leyendas del catálogo 15.
Fuera de alcance por diseño: representación impresa (PDF o HTML), guías de remisión (GRE), retenciones y percepciones como documentos independientes, y persistencia de cualquier tipo. Ámbito: Perú.
Certificado
Los fixtures se firman con un PKCS#12 autofirmado incluido en el repositorio, válido únicamente contra el ambiente beta de SUNAT.
En Emisor y Firma se puede cargar otro certificado. Los bytes permanecen en memoria del módulo durante la sesión: no se escriben en localStorage, no se codifican en la URL y no viajan en ninguna petición, porque la página no tiene backend al que enviarlos.
Verificación
La firma se contrasta con implementaciones de referencia de los estándares, no consigo misma: la forma canónica se compara con C14N 1.0 de lxml, los documentos firmados se verifican con xmlsec1, PBKDF2 contra los vectores del RFC 6070 y el lector PKCS#12 contra contenedores generados por OpenSSL. Adicionalmente, los documentos se transmiten al ambiente beta de SUNAT y deben retornar aceptados con código 0.
Referencias
- Códigos SUNAT — catálogo de códigos de error, con búsqueda
- Repositorio en GitHub
go get github.com/ivanjoz/facturago— requiere Go 1.26
Proyecto en desarrollo. Esta página expone únicamente lo ya implementado.