Skip to content

feat(rest): consulta CPE OAuth + padrón RUC local#3

Merged
Railly merged 1 commit into
mainfrom
feat/sunat-rest-padron-consulta-tc
Apr 29, 2026
Merged

feat(rest): consulta CPE OAuth + padrón RUC local#3
Railly merged 1 commit into
mainfrom
feat/sunat-rest-padron-consulta-tc

Conversation

@Railly
Copy link
Copy Markdown
Contributor

@Railly Railly commented Apr 29, 2026

Summary

Adds two new SUNAT capabilities, both orthogonal to the SOAP CPE emission flow shipped in #1 and #2:

  • sunat cpe consulta — validate any CPE (yours or vendor's) against SUNAT records via the official REST OAuth 2.0 API
  • sunat padron — local mirror of the SUNAT Padrón Reducido del RUC (~370MB ZIP, 6.1M entries), no auth, instant lookups

Both verified end-to-end against real SUNAT infrastructure 2026-04-29.

What's in this PR

REST OAuth 2.0 client_credentials wrapper (src/sunat-rest/oauth.ts)

  • Token fetch + in-process cache (refreshes 60s before SUNAT-reported expiry)
  • Single callRestApi<T>() helper — auto-retries once on 401 with fresh token
  • Two SUNAT host families: api-seguridad.sunat.gob.pe (token), api.sunat.gob.pe (ops)
  • Reusable for upcoming GRE / SIRE PRs (same OAuth shape)

Consulta Integrada CPE (sunat cpe consulta)

export SUNAT_API_CLIENT_ID=...   # from SOL → Mi RUC → Credenciales API
export SUNAT_API_CLIENT_SECRET=...

sunat cpe consulta \
  --ruc-emisor 20131312955 --tipo 01 --serie F001 --numero 1234 \
  --fecha 2026-04-29 --monto 118
  • POSTs to /v1/contribuyente/contribuyentes/{rucConsultante}/validarcomprobante
  • Auto-converts ISO date → DD/MM/YYYY (SUNAT format)
  • Maps cryptic codes to human descriptions: estadoCp (Aceptado/Anulado), estadoRuc (Activo/Baja), condDomiRuc (Habido/No Habido)
  • Both raw codes and friendly descriptions in JSON output (agent-friendly)

Padrón Reducido del RUC local (sunat padron *)

sunat padron status                 # cache status + freshness
sunat padron sync [--force]         # 12s download, ~370MB ZIP → 1GB TXT
sunat padron ruc 20131312955        # ~1s streaming lookup
sunat padron batch --file rucs.csv  # one scan for N RUCs
echo "20131312955" | sunat padron batch
  • 6.1M entries indexed by 11-char RUC prefix
  • Streams raw bytes to disk (no encoding conversion in hot path) — was 12+ minutes when we tried UTF-8 conversion in flight, now 12s
  • Pipe-separated TXT, ISO-8859-1 encoded, decoded only at lookup time
  • 24h freshness check, --force to re-sync earlier
  • No auth, no captcha, no third-party API, no dependency

Tests

209 pass / 2 skip / 0 fail in 2.6s (was 182)

New unit tests (27):
- oauth.test.ts (13) — token cache, 401 retry, query params, custom scope, error paths
- consulta-cpe.test.ts (6) — ISO→DD/MM/YYYY, body shape, code mappings, monto formatting
- padron-local.test.ts (8) — line parser, isStale (24h boundary), edge cases

Smoke tests

bun smoke:sunat   # Factura individual (PR #1)
bun smoke:boleta  # Boleta >= S/700 (PR #2)
bun smoke:padron  # NEW — downloads padrón + looks up SUNAT's own RUC

All three verified passing 2026-04-29.

Verified end-to-end

$ time bun run bin/sunat.ts -o json padron sync
{ synced: true, durationMs: 12306, zipSize: 385365406, entries: 6179818 }

$ time bun run bin/sunat.ts -o json padron ruc 20131312955
{
  "ruc": "20131312955",
  "found": true,
  "razonSocial": "SUPERINTENDENCIA NACIONAL DE ADUANAS Y DE ADMINISTRACION TRIBUTARIA - SUNAT",
  "estado": "ACTIVO",
  "condicion": "HABIDO",
  ...
}
real  0m1.155s

Out of scope (deliberately deferred)

Capability Why Path forward
Tipo de cambio SUNAT + SBS portal endpoints both blocked by WAF (verified — both return "Request Rejected") Future PR with --driver agent-browser (same pattern as RHE/F616)
Padrón RUC puntual via portal Now requires numRnd token + reCAPTCHA on e-consultaruc.sunat.gob.pe Local padrón is a strictly better answer for batch/scriptable use
GRE (Guía de Remisión REST) Same OAuth shape — oauth.ts is reusable Separate PR; minimal new code (UBL builder + 1 endpoint)
SIRE (RVIE/RCE) Distinct host api-sire.sunat.gob.pe, mandatory monthly filing High-value next PR — automates dolor mensual de contadores
sqlite index for padrón Streaming lookup is 1s — good enough for now Future PR if batch >1000 RUCs becomes common

Test plan

  • bun test green (209/2 skip/0 fail)
  • bun smoke:padron green (downloads + lookup verified)
  • sunat cpe consulta --help parses correctly
  • sunat padron status reports cache state
  • sunat padron ruc 20131312955 returns SUNAT row in <2s
  • CI green on push (will verify after PR opens)
  • Reviewer to verify their SUNAT_API_CLIENT_ID/SECRET works for cpe consulta

Adds two new capabilities orthogonal to the SOAP CPE emission flow:

1. SUNAT REST OAuth 2.0 client_credentials wrapper
   - src/sunat-rest/oauth.ts — token fetch + in-process cache + 401 retry
   - Token cached for ~1h (60s before SUNAT-reported expiry)
   - Single postSoap-style helper callRestApi<T>() unifies all REST calls
   - Two SUNAT host families: api-seguridad (token), api.sunat.gob.pe (ops)

2. Consulta Integrada CPE — sunat cpe consulta
   - POST /v1/contribuyente/contribuyentes/{ruc}/validarcomprobante
   - Validates ANY CPE against SUNAT records (yours or vendor's)
   - Auto-converts ISO date (2026-04-29) → DD/MM/YYYY (29/04/2026)
   - Friendly mapping of SUNAT codes (estadoCp, estadoRuc, condDomiRuc) to
     human descriptions (Aceptado, Activo, Habido, etc)
   - Required env: SUNAT_API_CLIENT_ID, SUNAT_API_CLIENT_SECRET (from SOL menu)

3. Padrón Reducido del RUC local — sunat padron sync|status|ruc|batch
   - Downloads ~370MB ZIP from www2.sunat.gob.pe (no auth, daily updated)
   - Streams raw bytes to disk (no encoding conversion in hot path — 12s
     instead of 12+ minutes when we tried UTF-8 conversion in flight)
   - 6.1M entries indexed by 11-char RUC prefix
   - Streaming lookup: ~1s per single RUC on 1GB TXT
   - Batch lookup: one scan for any number of RUCs
   - 24h freshness check; --force to re-sync earlier
   - Pipe-separated TXT, ISO-8859-1 encoded

Tests: 209 pass / 2 skip / 0 fail (was 182)
- oauth.test.ts (13) — token cache, 401 retry, query params, scope
- consulta-cpe.test.ts (6) — ISO→DD/MM/YYYY, code mappings, monto formatting
- padron-local.test.ts (8) — line parser, isStale, edge cases

Smoke: bun smoke:padron — downloads padrón, looks up SUNAT's own RUC,
verifies razon social. Verified passing 2026-04-29.

Out of scope (next PRs):
- Tipo de cambio: SUNAT/SBS WAF blocks fetch directly. Needs agent-browser.
- Padrón RUC consulta puntual via portal: requires numRnd token + reCAPTCHA.
  Local padrón is a strictly better answer for batch/scriptable use.
- GRE (Guía de Remisión REST API): same OAuth shape; oauth.ts is reusable.
- SIRE (RVIE/RCE) REST API: distinct host api-sire.sunat.gob.pe.
- sqlite index for sub-ms padrón lookups.

See src/sunat-rest/RESEARCH.md for full notes including the WAF observations
and SUNAT code → friendly description maps.
@Railly Railly marked this pull request as ready for review April 29, 2026 05:17
@Railly Railly merged commit 627d8d3 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