dgii-ts 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -40,8 +40,9 @@ that pass validation despite failing the check-digit algorithm.
40
40
  ### Resilient client (DgiiClient)
41
41
 
42
42
  `DgiiClient` is the recommended entry point for real-time queries. It uses
43
- web scraping as its primary strategy with SOAP fallback, a circuit breaker
44
- to prevent cascading failures, and retry with exponential backoff.
43
+ web scraping with a circuit breaker to prevent cascading failures and retry
44
+ with exponential backoff. The SOAP fallback is opt-in (`soapFallback: true`)
45
+ and off by default since version 0.3.0.
45
46
 
46
47
  ### Web scraping
47
48
 
@@ -52,8 +53,8 @@ endpoint in January 2025.
52
53
  ### SOAP client (WSMovilDGII) — deprecated
53
54
 
54
55
  Typed wrapper around DGII's SOAP service. **Permanently blocked by DGII
55
- since January 2025.** Kept as an internal fallback but not recommended
56
- for direct use.
56
+ since January 2025.** Kept as an opt-in `DgiiClient` fallback (off by
57
+ default) and not recommended for direct use.
57
58
 
58
59
  ### Bulk data importer
59
60
 
@@ -74,6 +75,29 @@ pnpm add dgii-ts
74
75
  yarn add dgii-ts
75
76
  ```
76
77
 
78
+ ## Agent skill
79
+
80
+ dgii-ts ships a skill ([`skills/dgii-ts/SKILL.md`](./skills/dgii-ts/SKILL.md))
81
+ that teaches coding agents (Claude Code, Cursor, Codex, Copilot, and others
82
+ that support [Agent Skills](https://agentskills.io)) to use the library
83
+ correctly: what each validator checks, how to handle `DgiiClient` errors,
84
+ and how to avoid hammering DGII.
85
+
86
+ ```bash
87
+ # Any supported agent
88
+ npx skills add franklinmdev/dgii-ts --skill dgii-ts
89
+ ```
90
+
91
+ In Claude Code you can also install it as a plugin:
92
+
93
+ ```text
94
+ /plugin marketplace add franklinmdev/dgii-ts
95
+ /plugin install dgii-ts@dgii-ts
96
+ ```
97
+
98
+ The skill is also included in the npm package, under
99
+ `node_modules/dgii-ts/skills/`.
100
+
77
101
  ## Quick start
78
102
 
79
103
  ### Validate an RNC
@@ -141,8 +165,11 @@ const ncfResult = await client.getNCF('131098193', 'B0100000001');
141
165
  ### Validate an e-NCF online
142
166
 
143
167
  For an e-NCF (E-series) DGII requires the buyer's RNC. Without it DGII
144
- answers with a required-field message and `getNCF` throws
145
- `DgiiServiceError`. The security code is optional.
168
+ answers with a required-field message: `ScrapingClient.getNCF` throws
169
+ `DgiiServiceError` and `DgiiClient.getNCF` wraps it in
170
+ `AllStrategiesFailedError` after exhausting retries and the fallback
171
+ (the message keeps DGII's text). The same happens without the
172
+ security code: DGII requires it too (verified with an E31 e-NCF).
146
173
 
147
174
  ```typescript
148
175
  const ecfResult = await client.getNCF('101010632', 'E310125217173', {
@@ -185,7 +212,7 @@ ignored for a B-series NCF.
185
212
 
186
213
  | Class/Method | Description |
187
214
  | --- | --- |
188
- | `DgiiClient` | Scraping + SOAP fallback, circuit breaker, retry |
215
+ | `DgiiClient` | Scraping, circuit breaker, retry (optional SOAP fallback) |
189
216
  | `client.getContribuyente(rnc)` | Looks up taxpayer data by RNC |
190
217
  | `client.getNCF(rnc, ncf, options?)` | Validates a fiscal receipt against DGII (`options` for e-NCF) |
191
218
 
@@ -227,7 +254,7 @@ accepts an `encoding` option (defaults to `latin1`).
227
254
  ├─────────────────────────────────────────────────┤
228
255
  │ Layer 2: DgiiClient (resilient) │
229
256
  │ ✓ Web scraping (primary) │
230
- │ ✓ SOAP fallback ✓ Circuit breaker ✓ Retry │
257
+ │ ✓ Circuit breaker ✓ Retry │
231
258
  ├─────────────────────────────────────────────────┤
232
259
  │ Layer 3: Bulk data (DGII_RNC.zip) │
233
260
  │ ✓ Daily download ✓ TXT parsing │
@@ -235,8 +262,7 @@ accepts an `encoding` option (defaults to `latin1`).
235
262
  ```
236
263
 
237
264
  **Layer 1** is instant and works offline. **Layer 2** provides real-time data
238
- using web scraping as the primary strategy, with SOAP fallback, circuit
239
- breaker, and retry with exponential backoff. **Layer 3** is ideal for batch
265
+ using web scraping, a circuit breaker, and retry with exponential backoff. **Layer 3** is ideal for batch
240
266
  operations where you need to look up thousands of RNCs quickly.
241
267
 
242
268
  ## Project status
package/README.md CHANGED
@@ -43,8 +43,9 @@ pasan validación aunque no cumplan el algoritmo de dígito verificador.
43
43
  ### Cliente resiliente (DgiiClient)
44
44
 
45
45
  `DgiiClient` es el punto de entrada recomendado para consultas en tiempo real.
46
- Usa web scraping como estrategia primaria con fallback a SOAP, circuit breaker
47
- para evitar cascadas de errores, y retry con backoff exponencial.
46
+ Usa web scraping con circuit breaker para evitar cascadas de errores y retry
47
+ con backoff exponencial. El fallback a SOAP es opcional (`soapFallback: true`)
48
+ y está apagado por defecto desde la versión 0.3.0.
48
49
 
49
50
  ### Web scraping
50
51
 
@@ -56,7 +57,8 @@ bloqueó el endpoint SOAP en enero 2025.
56
57
 
57
58
  Wrapper tipado alrededor del servicio SOAP de la DGII. **Bloqueado
58
59
  permanentemente por la DGII en enero 2025.** Se mantiene como fallback
59
- interno pero no se recomienda su uso directo.
60
+ opcional de `DgiiClient` (apagado por defecto) y no se recomienda su uso
61
+ directo.
60
62
 
61
63
  ### Importador de datos masivos
62
64
 
@@ -77,6 +79,29 @@ pnpm add dgii-ts
77
79
  yarn add dgii-ts
78
80
  ```
79
81
 
82
+ ## Skill para agentes de IA
83
+
84
+ dgii-ts incluye un skill ([`skills/dgii-ts/SKILL.md`](./skills/dgii-ts/SKILL.md))
85
+ que enseña a los agentes de código (Claude Code, Cursor, Codex, Copilot y
86
+ otros compatibles con [Agent Skills](https://agentskills.io)) a usar la
87
+ librería correctamente: qué valida cada validador, cómo manejar los
88
+ errores de `DgiiClient` y cómo no saturar a la DGII.
89
+
90
+ ```bash
91
+ # Cualquier agente compatible
92
+ npx skills add franklinmdev/dgii-ts --skill dgii-ts
93
+ ```
94
+
95
+ En Claude Code también se puede instalar como plugin:
96
+
97
+ ```text
98
+ /plugin marketplace add franklinmdev/dgii-ts
99
+ /plugin install dgii-ts@dgii-ts
100
+ ```
101
+
102
+ El skill viene además dentro del paquete de npm, en
103
+ `node_modules/dgii-ts/skills/`.
104
+
80
105
  ## Uso rápido
81
106
 
82
107
  ### Validar un RNC
@@ -144,8 +169,11 @@ const ncfResult = await client.getNCF('131098193', 'B0100000001');
144
169
  ### Validar e-NCF en línea
145
170
 
146
171
  Para un e-NCF (serie E) la DGII exige el RNC del comprador. Sin él
147
- responde con un mensaje de campo requerido y `getNCF` lanza
148
- `DgiiServiceError`. El código de seguridad es opcional.
172
+ responde con un mensaje de campo requerido: `ScrapingClient.getNCF`
173
+ lanza `DgiiServiceError` y `DgiiClient.getNCF` lo envuelve en
174
+ `AllStrategiesFailedError` tras agotar reintentos y fallback (el
175
+ mensaje conserva el texto de la DGII). Lo mismo pasa sin el código
176
+ de seguridad: la DGII también lo exige (verificado con un e-NCF E31).
149
177
 
150
178
  ```typescript
151
179
  const ecfResult = await client.getNCF('101010632', 'E310125217173', {
@@ -189,7 +217,7 @@ serie B.
189
217
 
190
218
  | Clase/Método | Descripción |
191
219
  | --- | --- |
192
- | `DgiiClient` | Cliente con scraping + SOAP fallback, circuit breaker y retry |
220
+ | `DgiiClient` | Cliente con scraping, circuit breaker y retry (SOAP fallback opcional) |
193
221
  | `client.getContribuyente(rnc)` | Consulta datos de un contribuyente por RNC |
194
222
  | `client.getNCF(rnc, ncf, options?)` | Valida un comprobante fiscal contra la DGII (`options` para e-NCF) |
195
223
 
@@ -231,7 +259,7 @@ Acepta `encoding` en las opciones (`latin1` por defecto).
231
259
  ├─────────────────────────────────────────────────┤
232
260
  │ Capa 2: DgiiClient (resiliente) │
233
261
  │ ✓ Web scraping (primario) │
234
- │ ✓ SOAP fallback ✓ Circuit breaker ✓ Retry │
262
+ │ ✓ Circuit breaker ✓ Retry │
235
263
  ├─────────────────────────────────────────────────┤
236
264
  │ Capa 3: Datos masivos (DGII_RNC.zip) │
237
265
  │ ✓ Descarga diaria ✓ Parseo TXT │
@@ -239,8 +267,8 @@ Acepta `encoding` en las opciones (`latin1` por defecto).
239
267
  ```
240
268
 
241
269
  La **capa 1** es instantánea y funciona sin conexión. La **capa 2** ofrece
242
- datos en tiempo real con web scraping como estrategia primaria, fallback a
243
- SOAP, circuit breaker y retry con backoff exponencial. La **capa 3** es
270
+ datos en tiempo real con web scraping, circuit breaker y retry con backoff
271
+ exponencial. La **capa 3** es
244
272
  ideal para operaciones en lote donde necesitas buscar miles de RNC rápidamente.
245
273
 
246
274
  ## Estado del proyecto
@@ -94,7 +94,7 @@ function parseContribuyenteHtml(html) {
94
94
  rnc: _chunkQH4BK5X3cjs.stripNonDigits.call(void 0, rnc),
95
95
  nombre: _chunkQH4BK5X3cjs.collapseSpaces.call(void 0, nombre),
96
96
  nombreComercial: _chunkQH4BK5X3cjs.collapseSpaces.call(void 0, nombreComercial),
97
- estado: rawEstado.toUpperCase().includes("ACTIVO") ? "ACTIVO" : "INACTIVO",
97
+ estado: _chunkQH4BK5X3cjs.collapseSpaces.call(void 0, rawEstado).toUpperCase() === "ACTIVO" ? "ACTIVO" : "INACTIVO",
98
98
  categoria: _chunkQH4BK5X3cjs.collapseSpaces.call(void 0, categoria),
99
99
  esFacturadorElectronico: rawFacturadorElectronico.trim().toUpperCase().replace(/[ÍI]/g, "I").startsWith("SI"),
100
100
  actividadEconomica: actividadEconomica ? _chunkQH4BK5X3cjs.collapseSpaces.call(void 0, actividadEconomica) : void 0,
@@ -210,13 +210,13 @@ function extractTableFields(tableHtml) {
210
210
  const fields = /* @__PURE__ */ new Map();
211
211
  for (const match of tableHtml.matchAll(BOLD_STYLE_PATTERN)) {
212
212
  const label = normalizeLabel(_nullishCoalesce(match[1], () => ( "")));
213
- const value = stripHtmlTags(_nullishCoalesce(match[2], () => ( "")));
213
+ const value = decodeHtmlEntities(stripHtmlTags(_nullishCoalesce(match[2], () => ( ""))));
214
214
  if (label) fields.set(label, value);
215
215
  }
216
216
  if (fields.size === 0) {
217
217
  for (const match of tableHtml.matchAll(BOLD_TAG_PATTERN)) {
218
218
  const label = normalizeLabel(_nullishCoalesce(match[1], () => ( "")));
219
- const value = stripHtmlTags(_nullishCoalesce(match[2], () => ( "")));
219
+ const value = decodeHtmlEntities(stripHtmlTags(_nullishCoalesce(match[2], () => ( ""))));
220
220
  if (label) fields.set(label, value);
221
221
  }
222
222
  }
@@ -3,7 +3,7 @@
3
3
  var _chunkDLB5RJ7Ecjs = require('./chunk-DLB5RJ7E.cjs');
4
4
 
5
5
 
6
- var _chunkFRM5XREFcjs = require('./chunk-FRM5XREF.cjs');
6
+ var _chunk6REHWG3Rcjs = require('./chunk-6REHWG3R.cjs');
7
7
 
8
8
 
9
9
 
@@ -52,7 +52,11 @@ var ConsecutiveBreaker = class {
52
52
  this._onSuccess();
53
53
  return result;
54
54
  } catch (error) {
55
- this._onFailure();
55
+ if (error instanceof _chunkNASWEFHNcjs.DgiiNotFoundError) {
56
+ this._onSuccess();
57
+ } else {
58
+ this._onFailure();
59
+ }
56
60
  throw error;
57
61
  }
58
62
  }
@@ -123,7 +127,7 @@ async function withRetry(fn, options = DEFAULT_RETRY_OPTIONS) {
123
127
  var DgiiClient = class {
124
128
  constructor(options) {
125
129
  const timeout = _optionalChain([options, 'optionalAccess', _ => _.timeout]);
126
- this._scraping = new (0, _chunkFRM5XREFcjs.ScrapingClient)(
130
+ this._scraping = new (0, _chunk6REHWG3Rcjs.ScrapingClient)(
127
131
  timeout ? { timeout } : void 0
128
132
  );
129
133
  this._soap = new (0, _chunkDLB5RJ7Ecjs.DgiiSoapClient)(
@@ -139,7 +143,7 @@ var DgiiClient = class {
139
143
  ...DEFAULT_RETRY_OPTIONS,
140
144
  ..._optionalChain([options, 'optionalAccess', _4 => _4.retry])
141
145
  };
142
- this._soapFallback = _nullishCoalesce(_optionalChain([options, 'optionalAccess', _5 => _5.soapFallback]), () => ( true));
146
+ this._soapFallback = _nullishCoalesce(_optionalChain([options, 'optionalAccess', _5 => _5.soapFallback]), () => ( false));
143
147
  }
144
148
  /**
145
149
  * Consulta datos de un contribuyente por RNC o cédula.
@@ -156,6 +160,11 @@ var DgiiClient = class {
156
160
  * @param ncf - NCF o e-NCF a validar
157
161
  * @param options - Datos adicionales del e-NCF (`rncComprador`,
158
162
  * `codigoSeguridad`). Se ignoran cuando `ncf` no es un e-NCF válido.
163
+ *
164
+ * Para un e-NCF sin `rncComprador` la DGII responde con un mensaje de
165
+ * campo requerido: `ScrapingClient.getNCF` lanza `DgiiServiceError` y
166
+ * este método lo envuelve en `AllStrategiesFailedError` tras agotar
167
+ * reintentos y fallback (el mensaje conserva el texto de la DGII).
159
168
  */
160
169
  async getNCF(rnc, ncf, options) {
161
170
  return this._executeWithFallback(
@@ -94,7 +94,7 @@ function parseContribuyenteHtml(html) {
94
94
  rnc: stripNonDigits(rnc),
95
95
  nombre: collapseSpaces(nombre),
96
96
  nombreComercial: collapseSpaces(nombreComercial),
97
- estado: rawEstado.toUpperCase().includes("ACTIVO") ? "ACTIVO" : "INACTIVO",
97
+ estado: collapseSpaces(rawEstado).toUpperCase() === "ACTIVO" ? "ACTIVO" : "INACTIVO",
98
98
  categoria: collapseSpaces(categoria),
99
99
  esFacturadorElectronico: rawFacturadorElectronico.trim().toUpperCase().replace(/[ÍI]/g, "I").startsWith("SI"),
100
100
  actividadEconomica: actividadEconomica ? collapseSpaces(actividadEconomica) : void 0,
@@ -210,13 +210,13 @@ function extractTableFields(tableHtml) {
210
210
  const fields = /* @__PURE__ */ new Map();
211
211
  for (const match of tableHtml.matchAll(BOLD_STYLE_PATTERN)) {
212
212
  const label = normalizeLabel(match[1] ?? "");
213
- const value = stripHtmlTags(match[2] ?? "");
213
+ const value = decodeHtmlEntities(stripHtmlTags(match[2] ?? ""));
214
214
  if (label) fields.set(label, value);
215
215
  }
216
216
  if (fields.size === 0) {
217
217
  for (const match of tableHtml.matchAll(BOLD_TAG_PATTERN)) {
218
218
  const label = normalizeLabel(match[1] ?? "");
219
- const value = stripHtmlTags(match[2] ?? "");
219
+ const value = decodeHtmlEntities(stripHtmlTags(match[2] ?? ""));
220
220
  if (label) fields.set(label, value);
221
221
  }
222
222
  }
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-QTN5ZT4P.js";
4
4
  import {
5
5
  ScrapingClient
6
- } from "./chunk-7ILLRYE4.js";
6
+ } from "./chunk-LRPZFX62.js";
7
7
  import {
8
8
  AllStrategiesFailedError,
9
9
  DgiiConnectionError,
@@ -52,7 +52,11 @@ var ConsecutiveBreaker = class {
52
52
  this._onSuccess();
53
53
  return result;
54
54
  } catch (error) {
55
- this._onFailure();
55
+ if (error instanceof DgiiNotFoundError) {
56
+ this._onSuccess();
57
+ } else {
58
+ this._onFailure();
59
+ }
56
60
  throw error;
57
61
  }
58
62
  }
@@ -139,7 +143,7 @@ var DgiiClient = class {
139
143
  ...DEFAULT_RETRY_OPTIONS,
140
144
  ...options?.retry
141
145
  };
142
- this._soapFallback = options?.soapFallback ?? true;
146
+ this._soapFallback = options?.soapFallback ?? false;
143
147
  }
144
148
  /**
145
149
  * Consulta datos de un contribuyente por RNC o cédula.
@@ -156,6 +160,11 @@ var DgiiClient = class {
156
160
  * @param ncf - NCF o e-NCF a validar
157
161
  * @param options - Datos adicionales del e-NCF (`rncComprador`,
158
162
  * `codigoSeguridad`). Se ignoran cuando `ncf` no es un e-NCF válido.
163
+ *
164
+ * Para un e-NCF sin `rncComprador` la DGII responde con un mensaje de
165
+ * campo requerido: `ScrapingClient.getNCF` lanza `DgiiServiceError` y
166
+ * este método lo envuelve en `AllStrategiesFailedError` tras agotar
167
+ * reintentos y fallback (el mensaje conserva el texto de la DGII).
159
168
  */
160
169
  async getNCF(rnc, ncf, options) {
161
170
  return this._executeWithFallback(
package/dist/client.cjs CHANGED
@@ -3,9 +3,9 @@
3
3
 
4
4
 
5
5
 
6
- var _chunk6ZRPVQPPcjs = require('./chunk-6ZRPVQPP.cjs');
6
+ var _chunkDUKBAJTMcjs = require('./chunk-DUKBAJTM.cjs');
7
7
  require('./chunk-DLB5RJ7E.cjs');
8
- require('./chunk-FRM5XREF.cjs');
8
+ require('./chunk-6REHWG3R.cjs');
9
9
  require('./chunk-OSGHCQFA.cjs');
10
10
  require('./chunk-WMFHPNFP.cjs');
11
11
  require('./chunk-QH4BK5X3.cjs');
@@ -15,4 +15,4 @@ require('./chunk-NASWEFHN.cjs');
15
15
 
16
16
 
17
17
 
18
- exports.ConsecutiveBreaker = _chunk6ZRPVQPPcjs.ConsecutiveBreaker; exports.DgiiClient = _chunk6ZRPVQPPcjs.DgiiClient; exports.isRetryableError = _chunk6ZRPVQPPcjs.isRetryableError; exports.withRetry = _chunk6ZRPVQPPcjs.withRetry;
18
+ exports.ConsecutiveBreaker = _chunkDUKBAJTMcjs.ConsecutiveBreaker; exports.DgiiClient = _chunkDUKBAJTMcjs.DgiiClient; exports.isRetryableError = _chunkDUKBAJTMcjs.isRetryableError; exports.withRetry = _chunkDUKBAJTMcjs.withRetry;
package/dist/client.d.cts CHANGED
@@ -59,7 +59,11 @@ declare class ConsecutiveBreaker {
59
59
  interface ClientOptions {
60
60
  /** Tiempo de espera en milisegundos (por defecto: 15000) */
61
61
  timeout?: number;
62
- /** Habilitar fallback a SOAP (por defecto: true) */
62
+ /**
63
+ * Habilitar fallback a SOAP (por defecto: false). La DGII bloqueó el
64
+ * endpoint SOAP en enero 2025; activarlo solo agrega una petición
65
+ * inútil tras cada falla del scraping.
66
+ */
63
67
  soapFallback?: boolean;
64
68
  /** Opciones de reintentos */
65
69
  retry?: Partial<RetryOptions>;
@@ -70,8 +74,9 @@ interface ClientOptions {
70
74
  /**
71
75
  * Cliente resiliente para consultas a la DGII.
72
76
  *
73
- * Usa web scraping como estrategia principal y SOAP como
74
- * fallback (con circuit breaker y reintentos automáticos).
77
+ * Usa web scraping con circuit breaker y reintentos automáticos. El
78
+ * fallback a SOAP es opcional (`soapFallback: true`) y está apagado por
79
+ * defecto: la DGII bloqueó ese endpoint en enero 2025.
75
80
  */
76
81
  declare class DgiiClient {
77
82
  private readonly _scraping;
@@ -92,6 +97,11 @@ declare class DgiiClient {
92
97
  * @param ncf - NCF o e-NCF a validar
93
98
  * @param options - Datos adicionales del e-NCF (`rncComprador`,
94
99
  * `codigoSeguridad`). Se ignoran cuando `ncf` no es un e-NCF válido.
100
+ *
101
+ * Para un e-NCF sin `rncComprador` la DGII responde con un mensaje de
102
+ * campo requerido: `ScrapingClient.getNCF` lanza `DgiiServiceError` y
103
+ * este método lo envuelve en `AllStrategiesFailedError` tras agotar
104
+ * reintentos y fallback (el mensaje conserva el texto de la DGII).
95
105
  */
96
106
  getNCF(rnc: string, ncf: string, options?: NcfQueryOptions): Promise<NcfQueryResult>;
97
107
  private _executeWithFallback;
package/dist/client.d.ts CHANGED
@@ -59,7 +59,11 @@ declare class ConsecutiveBreaker {
59
59
  interface ClientOptions {
60
60
  /** Tiempo de espera en milisegundos (por defecto: 15000) */
61
61
  timeout?: number;
62
- /** Habilitar fallback a SOAP (por defecto: true) */
62
+ /**
63
+ * Habilitar fallback a SOAP (por defecto: false). La DGII bloqueó el
64
+ * endpoint SOAP en enero 2025; activarlo solo agrega una petición
65
+ * inútil tras cada falla del scraping.
66
+ */
63
67
  soapFallback?: boolean;
64
68
  /** Opciones de reintentos */
65
69
  retry?: Partial<RetryOptions>;
@@ -70,8 +74,9 @@ interface ClientOptions {
70
74
  /**
71
75
  * Cliente resiliente para consultas a la DGII.
72
76
  *
73
- * Usa web scraping como estrategia principal y SOAP como
74
- * fallback (con circuit breaker y reintentos automáticos).
77
+ * Usa web scraping con circuit breaker y reintentos automáticos. El
78
+ * fallback a SOAP es opcional (`soapFallback: true`) y está apagado por
79
+ * defecto: la DGII bloqueó ese endpoint en enero 2025.
75
80
  */
76
81
  declare class DgiiClient {
77
82
  private readonly _scraping;
@@ -92,6 +97,11 @@ declare class DgiiClient {
92
97
  * @param ncf - NCF o e-NCF a validar
93
98
  * @param options - Datos adicionales del e-NCF (`rncComprador`,
94
99
  * `codigoSeguridad`). Se ignoran cuando `ncf` no es un e-NCF válido.
100
+ *
101
+ * Para un e-NCF sin `rncComprador` la DGII responde con un mensaje de
102
+ * campo requerido: `ScrapingClient.getNCF` lanza `DgiiServiceError` y
103
+ * este método lo envuelve en `AllStrategiesFailedError` tras agotar
104
+ * reintentos y fallback (el mensaje conserva el texto de la DGII).
95
105
  */
96
106
  getNCF(rnc: string, ncf: string, options?: NcfQueryOptions): Promise<NcfQueryResult>;
97
107
  private _executeWithFallback;
package/dist/client.js CHANGED
@@ -3,9 +3,9 @@ import {
3
3
  DgiiClient,
4
4
  isRetryableError,
5
5
  withRetry
6
- } from "./chunk-JV4HWHYP.js";
6
+ } from "./chunk-YV4IPACJ.js";
7
7
  import "./chunk-QTN5ZT4P.js";
8
- import "./chunk-7ILLRYE4.js";
8
+ import "./chunk-LRPZFX62.js";
9
9
  import "./chunk-H42U4GI5.js";
10
10
  import "./chunk-KW4ZV6FJ.js";
11
11
  import "./chunk-D26S6EXD.js";
package/dist/index.cjs CHANGED
@@ -17,7 +17,7 @@ require('./chunk-2ST6QKHA.cjs');
17
17
 
18
18
 
19
19
 
20
- var _chunk6ZRPVQPPcjs = require('./chunk-6ZRPVQPP.cjs');
20
+ var _chunkDUKBAJTMcjs = require('./chunk-DUKBAJTM.cjs');
21
21
 
22
22
 
23
23
 
@@ -27,7 +27,7 @@ var _chunkDLB5RJ7Ecjs = require('./chunk-DLB5RJ7E.cjs');
27
27
 
28
28
 
29
29
 
30
- var _chunkFRM5XREFcjs = require('./chunk-FRM5XREF.cjs');
30
+ var _chunk6REHWG3Rcjs = require('./chunk-6REHWG3R.cjs');
31
31
 
32
32
 
33
33
 
@@ -69,4 +69,4 @@ var _chunkNASWEFHNcjs = require('./chunk-NASWEFHN.cjs');
69
69
 
70
70
 
71
71
 
72
- exports.AllStrategiesFailedError = _chunkNASWEFHNcjs.AllStrategiesFailedError; exports.BulkFormatError = _chunkNASWEFHNcjs.BulkFormatError; exports.ConsecutiveBreaker = _chunk6ZRPVQPPcjs.ConsecutiveBreaker; exports.DGII_BULK_URL = _chunkVIZWYFHPcjs.DGII_BULK_URL; exports.DGII_ESTADOS = _chunkVIZWYFHPcjs.DGII_ESTADOS; exports.DGII_NCF_URL = _chunkFRM5XREFcjs.DGII_NCF_URL; exports.DGII_RNC_URL = _chunkFRM5XREFcjs.DGII_RNC_URL; exports.DGII_SOAP_BASE_URL = _chunkDLB5RJ7Ecjs.DGII_SOAP_BASE_URL; exports.DgiiClient = _chunk6ZRPVQPPcjs.DgiiClient; exports.DgiiConnectionError = _chunkNASWEFHNcjs.DgiiConnectionError; exports.DgiiError = _chunkNASWEFHNcjs.DgiiError; exports.DgiiNotFoundError = _chunkNASWEFHNcjs.DgiiNotFoundError; exports.DgiiServiceError = _chunkNASWEFHNcjs.DgiiServiceError; exports.DgiiSoapClient = _chunkDLB5RJ7Ecjs.DgiiSoapClient; exports.ECF_TYPE_NAMES = _chunkOSGHCQFAcjs.ECF_TYPE_NAMES; exports.NCF_TYPE_NAMES = _chunkUW6W2JHWcjs.NCF_TYPE_NAMES; exports.SOAP_ACTIONS = _chunkDLB5RJ7Ecjs.SOAP_ACTIONS; exports.ScrapingClient = _chunkFRM5XREFcjs.ScrapingClient; exports.downloadBulkFile = _chunkVIZWYFHPcjs.downloadBulkFile; exports.isRetryableError = _chunk6ZRPVQPPcjs.isRetryableError; exports.parseBulkFile = _chunkVIZWYFHPcjs.parseBulkFile; exports.validateCedula = _chunkUW6W2JHWcjs.validateCedula; exports.validateEcf = _chunkOSGHCQFAcjs.validateEcf; exports.validateNcf = _chunkUW6W2JHWcjs.validateNcf; exports.validateRnc = _chunkUW6W2JHWcjs.validateRnc; exports.withRetry = _chunk6ZRPVQPPcjs.withRetry;
72
+ exports.AllStrategiesFailedError = _chunkNASWEFHNcjs.AllStrategiesFailedError; exports.BulkFormatError = _chunkNASWEFHNcjs.BulkFormatError; exports.ConsecutiveBreaker = _chunkDUKBAJTMcjs.ConsecutiveBreaker; exports.DGII_BULK_URL = _chunkVIZWYFHPcjs.DGII_BULK_URL; exports.DGII_ESTADOS = _chunkVIZWYFHPcjs.DGII_ESTADOS; exports.DGII_NCF_URL = _chunk6REHWG3Rcjs.DGII_NCF_URL; exports.DGII_RNC_URL = _chunk6REHWG3Rcjs.DGII_RNC_URL; exports.DGII_SOAP_BASE_URL = _chunkDLB5RJ7Ecjs.DGII_SOAP_BASE_URL; exports.DgiiClient = _chunkDUKBAJTMcjs.DgiiClient; exports.DgiiConnectionError = _chunkNASWEFHNcjs.DgiiConnectionError; exports.DgiiError = _chunkNASWEFHNcjs.DgiiError; exports.DgiiNotFoundError = _chunkNASWEFHNcjs.DgiiNotFoundError; exports.DgiiServiceError = _chunkNASWEFHNcjs.DgiiServiceError; exports.DgiiSoapClient = _chunkDLB5RJ7Ecjs.DgiiSoapClient; exports.ECF_TYPE_NAMES = _chunkOSGHCQFAcjs.ECF_TYPE_NAMES; exports.NCF_TYPE_NAMES = _chunkUW6W2JHWcjs.NCF_TYPE_NAMES; exports.SOAP_ACTIONS = _chunkDLB5RJ7Ecjs.SOAP_ACTIONS; exports.ScrapingClient = _chunk6REHWG3Rcjs.ScrapingClient; exports.downloadBulkFile = _chunkVIZWYFHPcjs.downloadBulkFile; exports.isRetryableError = _chunkDUKBAJTMcjs.isRetryableError; exports.parseBulkFile = _chunkVIZWYFHPcjs.parseBulkFile; exports.validateCedula = _chunkUW6W2JHWcjs.validateCedula; exports.validateEcf = _chunkOSGHCQFAcjs.validateEcf; exports.validateNcf = _chunkUW6W2JHWcjs.validateNcf; exports.validateRnc = _chunkUW6W2JHWcjs.validateRnc; exports.withRetry = _chunkDUKBAJTMcjs.withRetry;
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  DgiiClient,
18
18
  isRetryableError,
19
19
  withRetry
20
- } from "./chunk-JV4HWHYP.js";
20
+ } from "./chunk-YV4IPACJ.js";
21
21
  import {
22
22
  DGII_SOAP_BASE_URL,
23
23
  DgiiSoapClient,
@@ -27,7 +27,7 @@ import {
27
27
  DGII_NCF_URL,
28
28
  DGII_RNC_URL,
29
29
  ScrapingClient
30
- } from "./chunk-7ILLRYE4.js";
30
+ } from "./chunk-LRPZFX62.js";
31
31
  import {
32
32
  ECF_TYPE_NAMES,
33
33
  validateEcf
package/dist/scraping.cjs CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
 
6
6
 
7
- var _chunkFRM5XREFcjs = require('./chunk-FRM5XREF.cjs');
7
+ var _chunk6REHWG3Rcjs = require('./chunk-6REHWG3R.cjs');
8
8
  require('./chunk-OSGHCQFA.cjs');
9
9
  require('./chunk-WMFHPNFP.cjs');
10
10
  require('./chunk-QH4BK5X3.cjs');
@@ -14,4 +14,4 @@ require('./chunk-NASWEFHN.cjs');
14
14
 
15
15
 
16
16
 
17
- exports.DGII_NCF_URL = _chunkFRM5XREFcjs.DGII_NCF_URL; exports.DGII_RNC_URL = _chunkFRM5XREFcjs.DGII_RNC_URL; exports.FORM_FIELDS = _chunkFRM5XREFcjs.FORM_FIELDS; exports.ScrapingClient = _chunkFRM5XREFcjs.ScrapingClient;
17
+ exports.DGII_NCF_URL = _chunk6REHWG3Rcjs.DGII_NCF_URL; exports.DGII_RNC_URL = _chunk6REHWG3Rcjs.DGII_RNC_URL; exports.FORM_FIELDS = _chunk6REHWG3Rcjs.FORM_FIELDS; exports.ScrapingClient = _chunk6REHWG3Rcjs.ScrapingClient;
package/dist/scraping.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  DGII_RNC_URL,
5
5
  FORM_FIELDS,
6
6
  ScrapingClient
7
- } from "./chunk-7ILLRYE4.js";
7
+ } from "./chunk-LRPZFX62.js";
8
8
  import "./chunk-H42U4GI5.js";
9
9
  import "./chunk-KW4ZV6FJ.js";
10
10
  import "./chunk-D26S6EXD.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dgii-ts",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Librería TypeScript para validación e integración con la DGII de República Dominicana — RNC, Cédula, NCF, e-NCF",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -80,7 +80,8 @@
80
80
  },
81
81
  "sideEffects": false,
82
82
  "files": [
83
- "dist"
83
+ "dist",
84
+ "skills"
84
85
  ],
85
86
  "scripts": {
86
87
  "build": "tsup",
@@ -0,0 +1,273 @@
1
+ ---
2
+ name: dgii-ts
3
+ description: Write correct TypeScript or JavaScript with the dgii-ts library for Dominican Republic tax data from the DGII (Dirección General de Impuestos Internos). Covers validating RNC and cédula numbers, checking NCF and e-NCF (e-CF) invoice numbers, looking up taxpayers (contribuyentes) live, verifying an e-NCF's status, and importing the DGII_RNC.zip taxpayer registry. Use this whenever code touches RNC, cédula, NCF, e-NCF, comprobantes fiscales, facturación electrónica, or Dominican tax IDs, even if the user never names dgii-ts, and before writing any mod-11 or Luhn check or any DGII scraper by hand.
4
+ ---
5
+
6
+ # dgii-ts
7
+
8
+ dgii-ts validates Dominican tax identifiers offline and queries the DGII
9
+ online. Most mistakes with it come from three misunderstandings: treating
10
+ an offline `valid: true` as "DGII confirmed this", using the wrong entry
11
+ point for live queries, and handling errors in a way that either hides
12
+ real outages or hammers DGII's servers. This skill exists to prevent
13
+ those.
14
+
15
+ If the project does not depend on it yet: `npm install dgii-ts`
16
+ (Node 18 or later).
17
+
18
+ ## Pick the entry point
19
+
20
+ | Need | Use | Network |
21
+ | --- | --- | --- |
22
+ | Is this RNC, cédula, NCF or e-NCF well formed? (forms, imports, and always before a live lookup) | `validateRnc`, `validateCedula`, `validateNcf`, `validateEcf` from `dgii-ts/validators` | none |
23
+ | Does DGII know this taxpayer, what is its name and status? Is this invoice number real? | one shared `DgiiClient` from `dgii-ts/client` | DGII web pages |
24
+ | Many RNCs at once, an offline copy of the registry, search by name | `downloadBulkFile` and `parseBulkFile` from `dgii-ts/bulk` | one ~23 MB download |
25
+
26
+ Three things the library also exports that you should not reach for:
27
+
28
+ - `DgiiSoapClient` (`dgii-ts/soap`) is deprecated. DGII shut the SOAP
29
+ service off in January 2025 and it currently answers without data.
30
+ - `ScrapingClient` (`dgii-ts/scraping`) is the low-level layer that
31
+ `DgiiClient` wraps. Used directly it has no retries and no circuit
32
+ breaker.
33
+ - Hand-written check digits or regexes. DGII has real identifiers that
34
+ fail the checksum; the validators carry a whitelist of them, so a
35
+ hand-rolled mod-11 or Luhn rejects real taxpayers.
36
+
37
+ ## Offline validators
38
+
39
+ ```ts
40
+ import { validateRnc, validateCedula, validateNcf, validateEcf } from 'dgii-ts/validators';
41
+
42
+ validateRnc('131-09819-3'); // { valid: true, formatted: '1-31-09819-3' }
43
+ validateCedula('00114272360'); // { valid: true, formatted: '001-1427236-0' }
44
+ validateNcf('b0100000001'); // { valid: true, type: 'CREDITO_FISCAL', serie: 'B01' }
45
+ validateEcf('E310000000001'); // { valid: true, type: 'CREDITO_FISCAL_ELECTRONICA', serie: 'E31' }
46
+ validateRnc('131098194'); // { valid: false }
47
+ ```
48
+
49
+ - **`valid: true` only means well formed.** It does not mean the RNC is
50
+ registered or active, or that the NCF was ever issued or belongs to that
51
+ issuer. Only `DgiiClient` can say that. Word UI messages accordingly
52
+ ("invalid format" vs "not registered with DGII").
53
+ - RNC and cédula: every non-digit is stripped first, so dashes and spaces
54
+ are fine. RNC is 9 digits (mod-11), cédula is 11 digits (Luhn).
55
+ `formatted` is `X-XX-XXXXX-X` or `XXX-XXXXXXX-X`.
56
+ - NCF and e-NCF: only uppercased and trimmed; separators are **not**
57
+ stripped, so `B01-00000001` is invalid. NCF is `B` + 2-digit type + 8
58
+ digits (11 chars). e-NCF is `E` + 2-digit type + 10 digits (13 chars).
59
+ Unknown type codes are invalid. Valid NCF types: 01, 02, 03, 04, 11,
60
+ 12, 13, 14, 15, 16, 17. Valid e-NCF types: 31, 32, 33, 34, 41, 43, 44,
61
+ 45, 46, 47. `NCF_TYPE_NAMES` and `ECF_TYPE_NAMES` (exported from the
62
+ same subpath, keyed by the 2-digit code) give Spanish display names.
63
+ - Non-string input returns `{ valid: false }` and never throws, so the
64
+ validators are safe on raw form data.
65
+ - The result is a discriminated union: check `valid` before reading
66
+ `formatted`, `type` or `serie`.
67
+ - A Dominican taxpayer ID is either an RNC (companies, 9 digits) or a
68
+ cédula (people, 11 digits). A field that accepts "RNC o cédula" should
69
+ accept either validator's `valid: true`. `getContribuyente` accepts
70
+ both.
71
+ - Store the digits (`value.replace(/\D/g, '')`) and use `formatted` for
72
+ display.
73
+
74
+ ## Live lookups with DgiiClient
75
+
76
+ Run it on the server. DGII's pages send no CORS headers, so a browser
77
+ cannot call them; use an API route, server action or backend job.
78
+
79
+ Create **one** client and reuse it:
80
+
81
+ ```ts
82
+ // dgii.ts
83
+ import { DgiiClient } from 'dgii-ts/client';
84
+
85
+ export const dgii = new DgiiClient({ soapFallback: false });
86
+ ```
87
+
88
+ Retry and circuit-breaker state live inside the instance. A new client per
89
+ request never trips its breaker, so during a DGII outage every request
90
+ keeps hitting DGII. `soapFallback` is `false` by default from dgii-ts
91
+ 0.3.0; older versions default to `true`, so pass `false` explicitly. The
92
+ SOAP service is shut off: the fallback only adds a wasted request after
93
+ every failed lookup, and an empty SOAP reply would be reported as
94
+ `DgiiNotFoundError`, making an outage look like "not registered". Do not
95
+ turn it on.
96
+
97
+ Options and defaults: `timeout` 15000 ms (clamped to 1 to 120 s);
98
+ `retry: { maxRetries: 2, baseDelayMs: 500, maxDelayMs: 10000 }`
99
+ (exponential backoff with jitter); `circuitBreaker: { failureThreshold: 5,
100
+ recoveryTimeoutMs: 60000, successThreshold: 2 }`. Pass partial objects;
101
+ the rest keeps its default.
102
+
103
+ ### Taxpayer: `getContribuyente(rncOrCedula)`
104
+
105
+ ```ts
106
+ import type { Contribuyente } from 'dgii-ts';
107
+ import { DgiiNotFoundError } from 'dgii-ts/errors';
108
+
109
+ async function findTaxpayer(digits: string): Promise<Contribuyente | null> {
110
+ try {
111
+ return await dgii.getContribuyente(digits);
112
+ } catch (err) {
113
+ // Not registered with DGII: a normal answer, not an outage
114
+ if (err instanceof DgiiNotFoundError) return null;
115
+ throw err;
116
+ }
117
+ }
118
+ ```
119
+
120
+ A `Contribuyente` has `rnc` (digits only), `nombre`, `nombreComercial`,
121
+ `estado`, `categoria`, `esFacturadorElectronico`, and the optional
122
+ `actividadEconomica`, `regimenDePagos` and `administracionLocal`.
123
+
124
+ - `estado` is `'ACTIVO'` or `'INACTIVO'`. Every other DGII status
125
+ (suspended, cancelled, and so on) is reported as `'INACTIVO'`.
126
+ - An unregistered RNC or cédula **throws** `DgiiNotFoundError`. It is not
127
+ retried and does not count against the circuit breaker.
128
+ - Pass the digits you validated, not user input as typed.
129
+
130
+ ### Invoice number: `getNCF(rncEmisor, ncf, options?)`
131
+
132
+ For a B-series NCF:
133
+
134
+ ```ts
135
+ const r = await dgii.getNCF('131098193', 'B0100000001');
136
+ // found: { valid: true, rnc, ncf, nombreComercial? }
137
+ // not found: { valid: false, rnc: '', ncf: '' } (other fields undefined)
138
+ ```
139
+
140
+ Unlike `getContribuyente`, a missing or invalid NCF does **not** throw; it
141
+ returns `valid: false`.
142
+
143
+ For an e-NCF (E-series), DGII needs the buyer's RNC **and** the 6-character
144
+ security code printed on the invoice. The types mark both optional because
145
+ B-series lookups ignore them, but without either one DGII refuses to answer
146
+ and `DgiiClient` throws `AllStrategiesFailedError` whose message contains
147
+ DGII's "es necesario completar el campo..." text.
148
+
149
+ ```ts
150
+ const e = await dgii.getNCF('101010632', 'E310125217173', {
151
+ rncComprador: '131262414',
152
+ codigoSeguridad: 'KrOLI0',
153
+ });
154
+ // { valid: true, rnc: '101010632', ncf: 'E310125217173',
155
+ // rncComprador: '131262414', codigoSeguridad: 'KrOLI0',
156
+ // estado: 'Aceptado', montoTotal: 230677.74, totalItbis: 35188.13,
157
+ // fechaEmision: '2026-02-11', fechaFirma: '2026-02-11' }
158
+ ```
159
+
160
+ - For an e-NCF, `valid: true` means DGII **found** it, not that it is
161
+ accepted. Check `estado`.
162
+ - A wrong security code gives `valid: false`.
163
+ - `montoTotal` and `totalItbis` are numbers; `fechaEmision` and
164
+ `fechaFirma` are strings exactly as DGII shows them.
165
+ - The library decides "e-NCF or not" with `validateEcf`, so run the
166
+ offline validator first and only pass the options for an e-NCF.
167
+ - DGII also accepts a foreign ID in place of the buyer's RNC, but the
168
+ library has no option for it.
169
+
170
+ ## Errors
171
+
172
+ `DgiiClient` only ever throws two things:
173
+
174
+ | Error (`code`) | Meaning | What to do |
175
+ | --- | --- | --- |
176
+ | `DgiiNotFoundError` (`DGII_NOT_FOUND`) | `getContribuyente`: DGII has no such RNC or cédula | Tell the user it is not registered. Do not retry. |
177
+ | `AllStrategiesFailedError` (`DGII_ALL_STRATEGIES_FAILED`) | Everything else, after the built-in retries: network down, timeout, DGII HTTP error, circuit open, DGII page changed, missing e-NCF fields, empty input | Report "DGII is unavailable, try later" and log `err.errors` (one error per strategy tried; `err.cause` is the first). |
178
+
179
+ The underlying `DgiiConnectionError` (`DGII_CONNECTION_ERROR`) and
180
+ `DgiiServiceError` (`DGII_SERVICE_ERROR`, with optional `statusCode`) are
181
+ inside `err.errors`; they are only thrown directly by the low-level
182
+ clients. `BulkFormatError` (`DGII_BULK_FORMAT_ERROR`) comes from
183
+ `parseBulkFile`. All of them extend `DgiiError` and are exported from
184
+ `dgii-ts/errors` (and the root). Matching on `err.code` also works when
185
+ `instanceof` cannot, for example when an app loads the package both as
186
+ ESM and as CommonJS.
187
+
188
+ Do not wrap calls in your own retry loop. The client already retries
189
+ connection errors and HTTP 5xx with backoff, and deliberately does not
190
+ retry 4xx or unexpected pages, because repeating those will not help.
191
+
192
+ Because empty input and missing e-NCF fields also surface as
193
+ `AllStrategiesFailedError`, validate before calling. Otherwise a
194
+ programming error looks like a DGII outage.
195
+
196
+ ## Be gentle with DGII
197
+
198
+ DGII publishes no rate limit and these are its public web pages. Each
199
+ lookup is two HTTP requests (load the form, submit it).
200
+
201
+ - Validate offline first. Never send DGII an identifier that fails the
202
+ validator.
203
+ - Share one client (above).
204
+ - Cache results. Taxpayer names and statuses rarely change within a day.
205
+ - For batches, go one at a time or with very low concurrency. For more
206
+ than a few dozen RNCs, use the bulk file instead of live lookups.
207
+ - Keep the default retry and circuit-breaker settings unless you have a
208
+ measured reason; raising retries multiplies load during an outage.
209
+
210
+ ## Bulk registry: DGII_RNC.zip
211
+
212
+ Node only (it uses `node:https` and `node:fs`).
213
+
214
+ The library downloads the ZIP but does not unzip it, and it has no zip
215
+ dependency. Extract the single entry `TMP/DGII_RNC.TXT` with the zip tool
216
+ the project already uses, or `adm-zip` (`npm install adm-zip`, plus
217
+ `@types/adm-zip` for TypeScript):
218
+
219
+ ```ts
220
+ import { writeFile } from 'node:fs/promises';
221
+ import AdmZip from 'adm-zip';
222
+ import { downloadBulkFile, parseBulkFile } from 'dgii-ts/bulk';
223
+
224
+ const zipPath = await downloadBulkFile({ outputDir: '/var/data/dgii' });
225
+
226
+ const entry = new AdmZip(zipPath).getEntry('TMP/DGII_RNC.TXT');
227
+ if (!entry) throw new Error('DGII_RNC.zip no longer contains TMP/DGII_RNC.TXT');
228
+ const txtPath = '/var/data/dgii/DGII_RNC.TXT';
229
+ await writeFile(txtPath, entry.getData());
230
+
231
+ const rows = await parseBulkFile({ filePath: txtPath });
232
+ const byRnc = new Map(rows.map((row) => [row.rnc, row]));
233
+ ```
234
+
235
+ - `downloadBulkFile` writes `<outputDir>/DGII_RNC.zip` (overwriting it) and
236
+ returns that path, relative if `outputDir` was relative. `timeout`
237
+ defaults to 60 s (clamped 5 s to 10 min). It rejects with a plain
238
+ `Error`, not a `DgiiError`.
239
+ - The ZIP holds one file, `TMP/DGII_RNC.TXT`: about 90 MB, pipe-delimited,
240
+ latin-1. The parser defaults to `latin1`; do not pass `utf8` or names
241
+ like MUÑOZ get corrupted.
242
+ - `parseBulkFile` returns the whole registry as one array (about 790,000
243
+ rows in September 2026, roughly 0.5 GB of memory at peak). Load it in a
244
+ scheduled job and write it to a database or index; do not parse it per
245
+ request.
246
+ - To check a list (suppliers, customers), validate each ID with
247
+ `validateRnc` **or** `validateCedula` (people appear by cédula), report
248
+ the ones that fail as bad format rather than "not registered", and look
249
+ the rest up by their digits.
250
+ - Each row: `rnc` (bare digits: 9 for an RNC, 11 for a cédula), `nombre`,
251
+ `nombreComercial`, `actividad` (DGII truncates it), `estado`, `regimen`,
252
+ `fechaConstitucion` (`dd/mm/yyyy` or empty).
253
+ - Bulk `estado` is DGII's full vocabulary, `DGII_ESTADOS`: `ACTIVO`,
254
+ `SUSPENDIDO`, `DADO DE BAJA`, `CESE TEMPORAL`, `ANULADO`, `RECHAZADO`.
255
+ That is not comparable with `getContribuyente`'s `ACTIVO`/`INACTIVO`.
256
+ - Rows without exactly 11 columns are skipped. If most sampled rows have
257
+ an unknown `estado`, it throws `BulkFormatError`: DGII changed the
258
+ layout, so fail loudly rather than import garbage.
259
+
260
+ ## Imports
261
+
262
+ | Subpath | Contents | Runs in |
263
+ | --- | --- | --- |
264
+ | `dgii-ts/validators` | the four validators, `NCF_TYPE_NAMES`, `ECF_TYPE_NAMES` | anywhere, including the browser |
265
+ | `dgii-ts/client` | `DgiiClient` | server (Node 18+) |
266
+ | `dgii-ts/errors` | the error classes | anywhere |
267
+ | `dgii-ts/bulk` | `downloadBulkFile`, `parseBulkFile`, `DGII_ESTADOS`, `DGII_BULK_URL` | Node |
268
+ | `dgii-ts` | everything above plus the low-level clients | server |
269
+
270
+ Types (`ValidationResult`, `NcfValidationResult`, `Contribuyente`,
271
+ `NcfQueryOptions`, `NcfQueryResult`, `BulkContribuyente`, `ClientOptions`)
272
+ are exported from the root. Both ESM `import` and CommonJS `require` work.
273
+ In client-side bundles import from `dgii-ts/validators` only.