Skip to content

feat(gre): Guía de Remisión Electrónica REST API#7

Merged
Railly merged 1 commit into
mainfrom
feat/gre-rest
Apr 29, 2026
Merged

feat(gre): Guía de Remisión Electrónica REST API#7
Railly merged 1 commit into
mainfrom
feat/gre-rest

Conversation

@Railly
Copy link
Copy Markdown
Contributor

@Railly Railly commented Apr 29, 2026

Summary

End-to-end emission of GRE 2022 (CPE tipo 09) via SUNAT's REST API. Third major SUNAT integration after CPE SOAP (PRs #1-#2) and SIRE (PRs #4, #6). Reuses the XAdES-BES signer from PR #1.

Why GRE matters: every empresa que mueve inventario (retail, ecommerce, distribución) needs guías de remisión per traslado. SUNAT requires GRE electrónica desde 2022. Hasta este PR el comando era un stub.

What's in this PR

OAuth scope + host (oauth.ts)

  • SUNAT_REST_BASES.cpe = https://api-cpe.sunat.gob.pe/v1
  • SCOPES.gre = https://api-cpe.sunat.gob.pe
  • callRestApi ahora soporta baseHost: "cpe" (además de "api" / "sire")
  • Password grant (igual que SIRE), pero con credenciales separadas por SOL menu URI = "GRE Emisión de Comprobantes"

UBL 2.1 DespatchAdvice builder (src/cpe/ubl/gre.ts)

  • Schema distinto a Invoice (Factura/Boleta)
  • DespatchSupplierParty (emisor) + DeliveryCustomerParty (destinatario)
  • Shipment con HandlingCode (catalog 20, motivo de traslado), TransportModeCode (catalog 18, modal), GrossWeightMeasure
  • ShipmentStage con TransitPeriod + DriverPerson (chofer)
  • Delivery con DeliveryAddress (llegada) + Despatch.DespatchAddress (partida)
  • TransportEquipment con placa
  • DespatchLine por item (UBL/GS1 commodity classification)
  • cac:Signature stub para inyección de XAdES (reusa helper común)

REST client (src/sunat-rest/gre.ts)

  • enviarGre({filename, signedXml}) — POST con body {archivo: {nomArchivo, arcGreZip(b64), hashZip(sha256)}}
  • consultarGreTicket(numTicket) — GET status
  • pollGreTicket() — backoff 2s/4s/8s/16s/30s, max 5min
  • Códigos: 0001 Aceptado · 0002 Anulado · 0003 Rechazado · 0098 En proceso

Commands

export SUNAT_GRE_CLIENT_ID=...   # SOL → Credenciales API SUNAT, URI "GRE Emisión de Comprobantes"
export SUNAT_GRE_CLIENT_SECRET=...
export CPE_SOL_USUARIO=MODDATOS
export CPE_SOL_PASSWORD='clave-sol'

sunat cpe gre emit --params '{
  "tipoDoc": "09", "serie": "T001", "numero": 1, "fechaEmision": "2026-04-29",
  "destinatario": {"tipoDoc":"6","numDoc":"20100070970","rznSocial":"CLIENTE SAC"},
  "envio": {
    "codTraslado": "01", "modTraslado": "02", "fecTraslado": "2026-04-29",
    "pesoTotal": 100, "undPesoTotal": "KGM", "numBultos": 2,
    "chofer": {"tipoDoc":"1","nroDoc":"12345678","nombres":"JUAN","apellidos":"PEREZ","licencia":"Q12345678"},
    "vehiculo": {"placa": "ABC-123"},
    "partida": {"ubigeo":"150101","direccion":"AV LIMA 123"},
    "llegada": {"ubigeo":"150114","direccion":"AV ALIVERTI 456"}
  },
  "items": [{"codigo":"P001","descripcion":"Cajas","cantidad":10,"unidad":"NIU"}]
}' --yes --wait

sunat cpe gre status --ticket 20240100000001 --wait

The old cpe guia stub now redirects to cpe gre with a clear notice.

Tests

265 pass / 2 skip / 0 fail in 3.1s (was 240)

ubl-gre.test.ts (16):
- XML UTF-8 declaration (no BOM)
- DespatchAdvice root + namespaces (DespatchAdvice-2 + cac/cbc/ds/ext)
- DespatchAdviceTypeCode = input tipoDoc
- ID format serie-numero
- ext:UBLExtensions placeholder for signature
- Emisor RUC in DespatchSupplierParty
- Destinatario in DeliveryCustomerParty
- Shipment HandlingCode + TransportModeCode
- DriverPerson rendered (chofer required for modal 02)
- TransportEquipment placa
- Ubigeos in delivery + despatch addresses
- DespatchLine count matches items
- GrossWeightMeasure formatted

gre-rest.test.ts (9):
- greCredentials password grant + api-cpe scope
- OAuth /clientessol with grant_type=password + scope encoded
- enviarGre POST to /v1/contribuyente/gem/comprobantes/{filename} on api-cpe
- Body has archivo.{nomArchivo, arcGreZip, hashZip}
- hashZip is sha256 of arcGreZip decoded (verified end-to-end via crypto module)
- consultarGreTicket GET /envios/{ticket}
- pollGreTicket: completed (0001) / rejected (0003) / still-processing (timeout)

Out of scope (deferred per LIMITATIONS.md)

  • ⚠️ Untested live — needs SUNAT_GRE_CLIENT_ID/SECRET from real RUC's SOL menu (test cert from PR feat(cpe): CPE namespace + sunat-direct driver (Factura, verified end-to-end) #1 doesn't have these). Code matches Greenter twig template byte-for-byte.
  • 🚧 Modal 01 (transporte público) — requires <cac:CarrierParty> + RUC + nroMtc. Only modal 02 (privado) shipped.
  • 🚧 GRE Transportista (tipo doc 31) — different schema entirely
  • 🚧 BuyerCustomerParty / SellerSupplierParty / AdditionalDocumentReference — easy adds when needed
  • 🚧 Multiple choferes — schema accepts loop, only one supported

Test plan

  • bun test green (265/2 skip/0 fail)
  • sunat cpe gre --help lists emit + status
  • sunat cpe gre emit requires --params (E2E gate test)
  • sunat cpe guia (legacy) prints redirect notice (E2E test)
  • LIMITATIONS.md updated (sub-limitations enumerated)
  • CI green on push
  • Verify with real RUC + GRE credentials (Hunter post-merge)

End-to-end emission of GRE 2022 (CPE tipo 09) via SUNAT's REST API,
the third major SUNAT integration after CPE SOAP (PRs #1-#2) and SIRE
(PRs #4, #6). Reuses the XAdES-BES signer.

Why GRE matters: every empresa que mueve inventario (retail, ecommerce,
distribución) needs guías de remisión per traslado. SUNAT requires GRE
electrónica since 2022. Until this PR the command was a stub.

What's new:

1. OAuth scope + host (oauth.ts)
   - SUNAT_REST_BASES.cpe = "https://api-cpe.sunat.gob.pe/v1"
   - SCOPES.gre = "https://api-cpe.sunat.gob.pe"
   - callRestApi gains baseHost: "cpe" (in addition to "api" / "sire")
   - Uses password grant (same as SIRE), separate client_id/secret per
     SOL menu URI = "GRE Emisión de Comprobantes"

2. UBL 2.1 DespatchAdvice builder (src/cpe/ubl/gre.ts)
   - Distinct schema from Invoice (Factura/Boleta)
   - DespatchSupplierParty (emisor) + DeliveryCustomerParty (destinatario)
   - Shipment block with HandlingCode (catalog 20, motivo de traslado),
     TransportModeCode (catalog 18, modal), GrossWeightMeasure
   - ShipmentStage with TransitPeriod + DriverPerson (chofer)
   - Delivery with DeliveryAddress (llegada) + Despatch.DespatchAddress (partida)
   - TransportEquipment with placa
   - DespatchLine per item (UBL/GS1 commodity classification)
   - cac:Signature stub for XAdES injection (reuses common.ts helper)

3. REST client (src/sunat-rest/gre.ts)
   - greCredentials() — password grant for api-cpe scope
   - enviarGre({filename, signedXml}) — POST with body:
     {archivo: {nomArchivo, arcGreZip(b64), hashZip(sha256)}}
   - consultarGreTicket(numTicket) — GET status
   - pollGreTicket() — backoff polling (2s/4s/8s/16s/30s, max 5min)
   - Codes: 0001=Aceptado, 0002=Anulado, 0003=Rechazado, 0098=En proceso

4. Commands
   - sunat cpe gre emit --params '{...}' [--dry-run] [--yes] [--wait] [--timeout]
   - sunat cpe gre status --ticket <id> [--wait]
   - Old "cpe guia" stub now redirects to "cpe gre" with a clear notice

5. Scope of THIS PR (deliberate)
   - tipoDoc 09 (Guía Remitente). GRE Transportista (31) is a separate
     schema, deferred.
   - modTraslado 02 (transporte privado, emisor mueve los bienes).
     Modal 01 (transporte público / CarrierParty + nroMtc) deferred.
   - codTraslado 01 (Venta) primary use case; other catalog 20 codes
     accepted by typing.
   - No BuyerCustomerParty (when buyer != destinatario), no SellerSupplierParty
     (tercero), no AdditionalDocumentReference. Easy adds when needed.

Tests: 265 pass / 2 skip / 0 fail in 3.1s (was 240)
- ubl-gre.test.ts (16) — XML structure assertions, ID format, namespaces,
  party blocks, shipment, transport equipment, weight, line count, BOM check
- gre-rest.test.ts (9) — OAuth password grant for api-cpe scope, POST URL
  shape, JSON body (archivo.nomArchivo/arcGreZip/hashZip), sha256 hash
  matches zip bytes, GET status URL, polling state transitions

LIMITATIONS.md updated:
- Move "Guía de Remisión" out of "shaped" — now a partial implementation
  with clear sub-limitations (modal 01, transportista doc 31, buyer/tercero,
  additional refs, multiple choferes)
@Railly Railly marked this pull request as ready for review April 29, 2026 06:03
@Railly Railly merged commit c818162 into main Apr 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant