feat(cpe): NC + ND end-to-end via sunat-direct, verified live#9
Merged
Conversation
Closes the SOAP CPE emission cycle: Factura ✅ + Boleta ✅ (PRs #1-#2) + now NC + ND ✅ verified accepted by SUNAT beta 2026-04-29: - NC: FC01-555 → cdrCode=0 "La Nota de Credito ... ha sido aceptada" - ND: FD01-777 → cdrCode=0 "La Nota de Debito ... ha sido aceptada" What's new: 1. UBL builders (src/cpe/ubl/nota-credito.ts + nota-debito.ts) - Distinct UBL roots: CreditNote-2 / DebitNote-2 (NOT Invoice-2) - NC uses CreditNoteLine + CreditedQuantity - ND uses DebitNoteLine + DebitedQuantity + RequestedMonetaryTotal (NOT LegalMonetaryTotal — SUNAT-specific divergence) - Both add cac:DiscrepancyResponse with refDoc + ResponseCode + motivo - Both add cac:BillingReference with DocumentTypeCode auto-detected from refSerie prefix (F→01 Factura, B→03 Boleta) - Reuse common.ts helpers (renderEmisorParty, renderReceptorParty, renderTaxAndTotals, renderInvoiceLine + tag swap) 2. Validation (src/cpe/validation/reglas.ts) - validateNotaCredito + validateNotaDebito sharing validateNotaCommon - Catálogo 09 (13 codes) for NC, Catálogo 10 (5 codes) for ND - Reject codes valid in 09 but not 10 (e.g. NC's "06 Devolución total" is NOT valid for ND) - Validates: serie format [FB]***, numero range, fechaEmision plazo, refSerie + refNumero, tipoNota in catalog, motivo length ≤250, RUC/DNI receptor, items+totales coherence, moneda 3. Driver wiring (src/cpe/drivers/sunat-direct.ts) - emitNotaCredito + emitNotaDebito share private emitNota(input, "07"|"08") - Reuses sendBill, idempotency cache, two-phase audit from factura/boleta - Filename per SUNAT: RUC-07-SERIE-NUMERO (NC) / RUC-08-SERIE-NUMERO (ND) - Distinct audit commands: "cpe nc emit" / "cpe nd emit" 4. Command wiring (src/commands/cpe/index.ts) - cpe nd emit now real (was stub returning notImplemented) - cpe nc emit was already wired in PR #1, no change Tests: 325 pass / 2 skip / 0 fail in 3.1s (was 283) - ubl-nota-credito.test.ts (12) — root NS, no Invoice tags, ID format, DiscrepancyResponse, BillingReference auto-detect F/B, line count - ubl-nota-debito.test.ts (8) — DebitNote root, RequestedMonetaryTotal, DebitNoteLine, BillingReference detect, multi-line - reglas-nota.test.ts (15) — happy paths NC/ND, all 13 NC codes accepted, all 5 ND codes accepted, code valid in 09 rejected for ND, ref* required, motivo length, RUC checksum, fechaEmision plazo, totales coherence LIMITATIONS.md updated: - Driver matrix: NC/ND for sunat-direct → 🔬 (verified) - Verified end-to-end section: 4 SUNAT beta successes catalogued
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the SOAP CPE emission cycle. Verified accepted by SUNAT beta 2026-04-29:
Now sunat-direct supports the full Factura → Boleta → NC → ND lifecycle. Driver matrix in LIMITATIONS.md flips NC/ND from 🚧 to 🔬.
What's in this PR
UBL builders
src/cpe/ubl/nota-credito.ts—CreditNote-2schema (NOTInvoice-2)src/cpe/ubl/nota-debito.ts—DebitNote-2schema withRequestedMonetaryTotal(NOTLegalMonetaryTotal— SUNAT-specific divergence)Differences from Factura that bite if you don't know:
CreditNoteLine+CreditedQuantityDebitNoteLine+DebitedQuantityRequestedMonetaryTotal(notLegalMonetaryTotal)cac:DiscrepancyResponse(refDoc + ResponseCode + motivo)cac:BillingReferencewithDocumentTypeCodeauto-detected fromrefSerieprefix (F→01 Factura, B→03 Boleta)Reuse
common.tshelpers + tag-swap onrenderInvoiceLineoutput.Validation
validateNotaCredito(Catálogo 09, 13 codes)validateNotaDebito(Catálogo 10, 5 codes)validateNotaCommoncovers serie format, ref fields, motivo length, RUC/DNI receptor, items/totales coherence, moneda06(Devolución total) is rejected for NDDriver
emitNotaCredito+emitNotaDebitoshare privateemitNota(input, "07"|"08")sendBill, idempotency cache, two-phase audit from factura/boletaRUC-07-...(NC) /RUC-08-...(ND)cpe nc emit/cpe nd emitCommands
cpe nd emitnow real (was stub).cpe nc emitalready wired in PR feat(cpe): CPE namespace + sunat-direct driver (Factura, verified end-to-end) #1.Tests
Verified end-to-end against SUNAT beta
Same Greenter test cert + RUC 20000000001 from PR #1. Both first-try.
LIMITATIONS.md updated
sunat-direct× NC/ND → 🔬 (verified)Out of scope (still in LIMITATIONS)
cpe voidT3 with intent token (today voiding goes via NC for facturas, Comunicación de Baja for boletas)facturador,nubefact,apisperufor NC/ND (mock + sunat-direct only)Test plan
bun testgreen (325/2 skip/0 fail)cpe nc emitaccepted by SUNAT beta (FC01-555, cdrCode=0)cpe nd emitaccepted by SUNAT beta (FD01-777, cdrCode=0)validateNotaDebitorejects code "06" (which is valid for NC but not ND)