FacturaGo Comprobantes electrónicos SUNAT, en Go GitHub

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 (o Summary para el resumen diario, Voided para 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: CompleteTotalsValidateDocumentBuildXMLEmit.

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

  1. 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.
  2. 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.
  3. 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.
  4. recalcular importes derivados controla el parámetro Recompute. Desactivado, CompleteTotals preserva 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:

PaqueteResponsabilidad
facturagode documento a transmisión firmada: UBL, firma, archivo
facturago/modellos tipos, los catálogos, los totales y las reglas
facturago/sunatel servicio web: transmisión e interpretación de la respuesta
facturago/utilslos 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.

TipoSignificado
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

DocumentoCódigoEstado
Factura01aceptado en SUNAT beta
Boleta03aceptado en SUNAT beta
Nota de crédito07implementado, sin transmisión verificada
Nota de débito08implementado, sin transmisión verificada
Resumen diarioRCaceptado en SUNAT beta
Comunicación de bajaRAimplementado, 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

Proyecto en desarrollo. Esta página expone únicamente lo ya implementado.