Skip to content

feat(cpe): NC + ND end-to-end via sunat-direct, verified live#9

Merged
Railly merged 1 commit into
mainfrom
feat/cpe-nc-nd
Apr 29, 2026
Merged

feat(cpe): NC + ND end-to-end via sunat-direct, verified live#9
Railly merged 1 commit into
mainfrom
feat/cpe-nc-nd

Conversation

@Railly
Copy link
Copy Markdown
Contributor

@Railly Railly commented Apr 29, 2026

Summary

Closes the SOAP CPE emission cycle. 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"

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.tsCreditNote-2 schema (NOT Invoice-2)
  • src/cpe/ubl/nota-debito.tsDebitNote-2 schema with RequestedMonetaryTotal (NOT LegalMonetaryTotal — SUNAT-specific divergence)

Differences from Factura that bite if you don't know:

  • NC line tag: CreditNoteLine + CreditedQuantity
  • ND line tag: DebitNoteLine + DebitedQuantity
  • ND totals: RequestedMonetaryTotal (not LegalMonetaryTotal)
  • Both add cac:DiscrepancyResponse (refDoc + ResponseCode + motivo)
  • Both add cac:BillingReference with DocumentTypeCode auto-detected from refSerie prefix (F→01 Factura, B→03 Boleta)

Reuse common.ts helpers + tag-swap on renderInvoiceLine output.

Validation

  • validateNotaCredito (Catálogo 09, 13 codes)
  • validateNotaDebito (Catálogo 10, 5 codes)
  • Shared validateNotaCommon covers serie format, ref fields, motivo length, RUC/DNI receptor, items/totales coherence, moneda
  • Catalog 09 ↔ 10 enforcement: NC's 06 (Devolución total) is rejected for ND

Driver

  • emitNotaCredito + emitNotaDebito share private emitNota(input, "07"|"08")
  • Reuses sendBill, idempotency cache, two-phase audit from factura/boleta
  • Filename: RUC-07-... (NC) / RUC-08-... (ND)
  • Audit commands: cpe nc emit / cpe nd emit

Commands

Tests

325 pass / 2 skip / 0 fail in 3.1s (was 283)

ubl-nota-credito.test.ts (12):
- Root NS CreditNote-2 + no Invoice tags
- ID format serie-numero
- DiscrepancyResponse + BillingReference (F→01, B→03 auto-detect)
- CreditNoteLine + CreditedQuantity (no InvoiceLine/InvoicedQuantity)
- ext:UBLExtensions placeholder
- LegalMonetaryTotal carries totals (NC keeps this, ND doesn't)
- Multi-line count

ubl-nota-debito.test.ts (8):
- Root NS DebitNote-2
- DebitNoteLine + DebitedQuantity
- RequestedMonetaryTotal (NOT LegalMonetaryTotal)
- DiscrepancyResponse + BillingReference detect
- ID format
- ext placeholder
- Multi-line

reglas-nota.test.ts (15):
- Happy paths NC + ND
- All 13 codes in Catálogo 09 accepted for NC
- All 5 codes in Catálogo 10 accepted for ND
- Code valid in 09 ("06" Devolución) rejected for ND
- Series format both F* and B*
- ref* required
- motivo length ≤ 250
- RUC checksum, fechaEmision plazo, totales coherence

Verified end-to-end against SUNAT beta

$ CPE_PROFILE=test ... bun run bin/sunat.ts cpe --driver sunat-direct nc emit --params '...' --yes
{ success: true, status: "accepted", cdrCode: "0",
  cdrDesc: "La Nota de Credito numero FC01-555, ha sido aceptada", ... }

$ ... cpe --driver sunat-direct nd emit --params '...' --yes
{ success: true, status: "accepted", cdrCode: "0",
  cdrDesc: "La Nota de Debito numero FD01-777, ha sido aceptada", ... }

Same Greenter test cert + RUC 20000000001 from PR #1. Both first-try.

LIMITATIONS.md updated

  • Driver matrix: sunat-direct × NC/ND → 🔬 (verified)
  • Active limitations section: NC/ND moved from "shaped" to "shipped + verified"
  • Verified end-to-end list: 4 SUNAT beta successes catalogued (factura, boleta, NC, ND)

Out of scope (still in LIMITATIONS)

  • 🚧 cpe void T3 with intent token (today voiding goes via NC for facturas, Comunicación de Baja for boletas)
  • 🚧 Drivers facturador, nubefact, apisperu for NC/ND (mock + sunat-direct only)
  • 🚧 NC special cases: line-level (codes 05/07), bonificación (08), exportación (11), IVAP (12) — schema accepts them but no targeted validation yet

Test plan

  • bun test green (325/2 skip/0 fail)
  • cpe nc emit accepted by SUNAT beta (FC01-555, cdrCode=0)
  • cpe nd emit accepted by SUNAT beta (FD01-777, cdrCode=0)
  • validateNotaDebito rejects code "06" (which is valid for NC but not ND)
  • BillingReference detects F→01 and B→03 from refSerie prefix
  • LIMITATIONS.md driver matrix updated to 🔬
  • CI green on push

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
@Railly Railly marked this pull request as ready for review April 29, 2026 06:26
@Railly Railly merged commit b3e3f8e 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