ecf-dgii 1.0.2 → 1.1.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.
- checksums.yaml +4 -4
- data/README.md +214 -60
- data/lib/ecf-dgii.rb +2 -1
- data/lib/ecf_dgii/client.rb +300 -152
- data/lib/ecf_dgii/exceptions.rb +29 -0
- data/lib/ecf_dgii/frontend_client.rb +217 -0
- data/lib/ecf_dgii/polling.rb +78 -21
- data/lib/ecf_dgii/railtie.rb +4 -1
- data/lib/ecf_dgii/version.rb +2 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 283a36ead0fbc5599832378e0383bbcd60cb8a0424b07828dffa12c650d5140a
|
|
4
|
+
data.tar.gz: 57df5b04a64728d794fe5e8becc32dd76552a6d54de2b0dd5d75ba82a3ff47ad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d3289733f2ccbb63451a6c925fee997c8c297f4a4446f580f90e43a299430f41585d868cc3ee0b3afc3a87d4e7d858f78063dcbd7c9af1c1ce49a6225575ed86
|
|
7
|
+
data.tar.gz: e5515aaee00fadfcdb7b1564bcff01309073b28b0702b4aaa7eb0c1fd8822a02d2ef50ace74adebed735a529d320b593d2772c607b641be48f98dd4a94561ddc
|
data/README.md
CHANGED
|
@@ -5,6 +5,30 @@
|
|
|
5
5
|
|
|
6
6
|
SDK de Ruby para la **API ECF DGII** — Comprobantes Fiscales Electrónicos de la República Dominicana.
|
|
7
7
|
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Índice
|
|
11
|
+
|
|
12
|
+
- [Instalación](#instalación)
|
|
13
|
+
- [Inicio rápido](#inicio-rápido)
|
|
14
|
+
- [Configuración](#configuración)
|
|
15
|
+
- [Autenticación](#autenticación)
|
|
16
|
+
- [Ambientes de trabajo](#ambientes-de-trabajo)
|
|
17
|
+
- [Integración en Ruby on Rails](#integración-en-ruby-on-rails)
|
|
18
|
+
- [EcfClient — Backend (Full Access)](#ecfclient--backend-full-access)
|
|
19
|
+
- [Envío de ECF con polling automático](#envío-de-ecf-con-polling-automático)
|
|
20
|
+
- [Operaciones de empresa](#operaciones-de-empresa)
|
|
21
|
+
- [Certificados P12](#certificados-p12)
|
|
22
|
+
- [Consultas DGII](#consultas-dgii)
|
|
23
|
+
- [Recepción y aprobación comercial](#recepción-y-aprobación-comercial)
|
|
24
|
+
- [EcfFrontendClient — Frontend (Read-Only)](#ecffrontendclient--frontend-read-only)
|
|
25
|
+
- [Polling con backoff exponencial](#polling-con-backoff-exponencial)
|
|
26
|
+
- [Manejo de errores](#manejo-de-errores)
|
|
27
|
+
- [Arquitectura Backend / Frontend](#arquitectura-backend--frontend)
|
|
28
|
+
- [Licencia](#licencia)
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
8
32
|
## Instalación
|
|
9
33
|
|
|
10
34
|
Añade esta línea al `Gemfile` de tu aplicación:
|
|
@@ -25,6 +49,8 @@ O instálalo tú mismo con:
|
|
|
25
49
|
gem install ecf-dgii
|
|
26
50
|
```
|
|
27
51
|
|
|
52
|
+
---
|
|
53
|
+
|
|
28
54
|
## Inicio rápido
|
|
29
55
|
|
|
30
56
|
```ruby
|
|
@@ -118,10 +144,12 @@ ecf = EcfDgii::Generated::Ecf31ECF.new(
|
|
|
118
144
|
)
|
|
119
145
|
|
|
120
146
|
# 3. Enviar el eCF con enrutamiento automático y polling hasta completar
|
|
121
|
-
result = client.
|
|
147
|
+
result = client.send_ecf(ecf)
|
|
122
148
|
puts "eCF Procesado: #{result.encf} - Estado: #{result.progress}"
|
|
123
149
|
```
|
|
124
150
|
|
|
151
|
+
---
|
|
152
|
+
|
|
125
153
|
## Configuración
|
|
126
154
|
|
|
127
155
|
### Autenticación
|
|
@@ -184,65 +212,34 @@ Una vez configurado el inicializador, puedes acceder al cliente en cualquier par
|
|
|
184
212
|
client = EcfDgii.client
|
|
185
213
|
```
|
|
186
214
|
|
|
187
|
-
|
|
215
|
+
---
|
|
188
216
|
|
|
189
|
-
|
|
190
|
-
- **Enrutamiento dinámico** — selecciona el endpoint correcto según el atributo `tipoe_cf` de la cabecera.
|
|
191
|
-
- **Polling con backoff exponencial** — consulta repetidamente el estado de procesamiento del eCF hasta llegar a un estado terminal (`Completed`, `Failed` o `Rejected`).
|
|
192
|
-
- **Manejo de tiempos límite y reintentos** — lanza excepciones controladas si se excede el timeout de espera.
|
|
217
|
+
## EcfClient — Backend (Full Access)
|
|
193
218
|
|
|
194
|
-
|
|
219
|
+
El `EcfDgii::Client` ofrece acceso completo a la API de ECF: envío de comprobantes, consultas, operaciones de empresa y certificados, consultas DGII, etc.
|
|
195
220
|
|
|
196
|
-
|
|
197
|
-
options = EcfDgii::PollingOptions.new(
|
|
198
|
-
initial_delay: 2.0, # Segundos antes de la primera consulta
|
|
199
|
-
max_delay: 30.0, # Límite máximo de espera entre consultas
|
|
200
|
-
max_retries: 50, # Intentos máximos de consulta (0 para ilimitado)
|
|
201
|
-
backoff_multiplier: 1.5, # Multiplicador del retraso en cada intento
|
|
202
|
-
timeout: 300.0 # Timeout total en segundos
|
|
203
|
-
)
|
|
221
|
+
### Envío de ECF con polling automático
|
|
204
222
|
|
|
205
|
-
|
|
206
|
-
```
|
|
223
|
+
El método `send_ecf` (1:1 con el `sendEcf` del SDK de TypeScript) maneja de forma transparente:
|
|
207
224
|
|
|
208
|
-
|
|
225
|
+
- **Validación completa** — verifica que `tipoeCF`, `rncEmisor` y `encf` estén presentes (mismo comportamiento que TypeScript).
|
|
226
|
+
- **Enrutamiento dinámico** — selecciona el endpoint correcto según el atributo `tipoe_cf` de la cabecera.
|
|
227
|
+
- **Polling con backoff exponencial** — consulta repetidamente el estado de procesamiento del eCF hasta llegar a un estado terminal (`Finished` o `Error`).
|
|
228
|
+
- **EcfError en errores** — si el progreso termina en `Error`, lanza `EcfError` con la respuesta completa.
|
|
209
229
|
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
ECF-->>BE: { messageId }
|
|
221
|
-
BE-->>C: { messageId }
|
|
222
|
-
|
|
223
|
-
Note over C: No espera — puede continuar
|
|
224
|
-
|
|
225
|
-
alt Token en cache
|
|
226
|
-
C->>C: Usar token existente
|
|
227
|
-
else Sin token o expirado
|
|
228
|
-
C->>BE: GET /ecf-token
|
|
229
|
-
BE->>ECF: POST /apikey (solo lectura, scoped a RNC)
|
|
230
|
-
ECF-->>BE: { apiKey }
|
|
231
|
-
BE-->>C: { apiKey }
|
|
232
|
-
C->>C: Almacenar token en cache
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
loop Polling hasta completar
|
|
236
|
-
C->>ECF: GET /ecf/{rnc}/{encf} (token solo lectura)
|
|
237
|
-
ECF-->>C: { progress, codSec, ... }
|
|
238
|
-
end
|
|
230
|
+
```ruby
|
|
231
|
+
# Envío con polling automático (reemplaza a send_ecf_and_poll)
|
|
232
|
+
result = client.send_ecf(ecf)
|
|
233
|
+
|
|
234
|
+
# Con opciones de polling personalizadas
|
|
235
|
+
result = client.send_ecf(ecf, EcfDgii::PollingOptions.new(
|
|
236
|
+
initial_delay: 1.0,
|
|
237
|
+
max_retries: 60,
|
|
238
|
+
timeout: 120.0
|
|
239
|
+
))
|
|
239
240
|
```
|
|
240
241
|
|
|
241
|
-
|
|
242
|
-
2. La API de ECF responde de inmediato con un `messageId` para que el backend Rails responda al cliente web sin bloquearse.
|
|
243
|
-
3. El frontend del browser puede pedir un token de API de sólo lectura llamando a un endpoint en Rails (el cual usa `client.new_company_api_key`), para después hacer polling directamente contra la API de eCF de forma segura.
|
|
244
|
-
|
|
245
|
-
## Gestión de Empresas y Certificados
|
|
242
|
+
> **Nota:** `send_ecf_and_poll` sigue disponible como alias por compatibilidad, pero ahora `send_ecf` ya incluye el polling (1:1 con TypeScript).
|
|
246
243
|
|
|
247
244
|
### Operaciones de Empresa
|
|
248
245
|
|
|
@@ -269,36 +266,139 @@ client.delete_company("101001010")
|
|
|
269
266
|
|
|
270
267
|
```ruby
|
|
271
268
|
# Obtener el certificado actual de la empresa
|
|
272
|
-
certificate = client.
|
|
269
|
+
certificate = client.get_certificate("101001010")
|
|
273
270
|
|
|
274
271
|
# Subir/actualizar un certificado de firma digital P12
|
|
275
|
-
|
|
276
|
-
client.update_certificate_company("101001010", file, "clave-del-certificado")
|
|
272
|
+
client.update_certificate("101001010", File.open("ruta/al/certificado.p12", "rb"), "clave-del-certificado")
|
|
277
273
|
```
|
|
278
274
|
|
|
279
|
-
|
|
275
|
+
> Los métodos antiguos `get_current_certificate` y `update_certificate_company` siguen disponibles como alias.
|
|
276
|
+
|
|
277
|
+
### Consultas DGII
|
|
280
278
|
|
|
281
279
|
```ruby
|
|
282
280
|
# Consultar estado actual del eCF
|
|
283
281
|
estado = client.consulta_estado("101001010", "101001010", "E310000051630", "131880681", "ABC123")
|
|
284
282
|
|
|
285
283
|
# Consultar directorio de emisores electrónicos activos en DGII
|
|
286
|
-
directorio = client.
|
|
284
|
+
directorio = client.consulta_directorio_listado("101001010")
|
|
285
|
+
|
|
286
|
+
# Consultar RFCE (incluye código de seguridad, 1:1 con TypeScript)
|
|
287
|
+
rfce = client.consulta_rfce("101001010", "101001010", "E310000051630", "SEC123")
|
|
287
288
|
|
|
288
289
|
# Consultar estado de procesamiento mediante el track_id obtenido
|
|
289
290
|
resultado = client.consulta_resultado("101001010", "track-id-obtenido")
|
|
290
291
|
|
|
291
292
|
# Obtener estatus general de los servicios de la DGII
|
|
292
|
-
estatus = client.
|
|
293
|
+
estatus = client.estatus_servicios("101001010")
|
|
293
294
|
```
|
|
294
295
|
|
|
296
|
+
### Recepción y aprobación comercial
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
# Buscar solicitudes de recepción
|
|
300
|
+
requests = client.search_ecf_reception_requests(page: 1, limit: 10)
|
|
301
|
+
|
|
302
|
+
# Obtener una solicitud de recepción por RNC y messageId
|
|
303
|
+
request = client.get_ecf_reception_request("101001010", "msg_123")
|
|
304
|
+
|
|
305
|
+
# Enviar aprobación comercial (ACECF) para un messageId
|
|
306
|
+
client.aprobacion_comercial("msg_123", body)
|
|
307
|
+
|
|
308
|
+
# Anular rangos de comprobantes
|
|
309
|
+
client.anulacion_rangos("101001010", anulacion_request)
|
|
310
|
+
|
|
311
|
+
# Firmar semilla
|
|
312
|
+
client.firmar_semilla("101001010", xml_body)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## EcfFrontendClient — Frontend (Read-Only)
|
|
318
|
+
|
|
319
|
+
El `EcfDgii::FrontendClient` es un cliente restringido que solo expone endpoints **GET** (solo lectura), diseñado para usarse en frontends. El manejo del token es automático:
|
|
320
|
+
|
|
321
|
+
1. En cada petición, verifica si hay un token en caché. Si no, obtiene uno nuevo y lo almacena.
|
|
322
|
+
2. En respuestas `401`, obtiene un token nuevo, actualiza la caché y reintenta la petición.
|
|
323
|
+
|
|
324
|
+
**¡NUEVO!** — Ahora disponible en Ruby (1:1 con `EcfFrontendClient` del SDK de TypeScript).
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
# Crear un frontend client
|
|
328
|
+
frontend = EcfDgii::FrontendClient.new(
|
|
329
|
+
get_token: -> { fetch_fresh_token_from_backend }, # Requerido
|
|
330
|
+
environment: :test
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Solo operaciones de lectura disponibles
|
|
334
|
+
ecfs = frontend.search_ecfs("131460941", page: 1, limit: 10)
|
|
335
|
+
company = frontend.get_company_by_rnc("131460941")
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Factory method
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
frontend = EcfDgii.create_frontend_client(
|
|
342
|
+
get_token: -> { fetch_fresh_token_from_backend },
|
|
343
|
+
environment: :test
|
|
344
|
+
)
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Cache personalizado
|
|
348
|
+
|
|
349
|
+
Por defecto el token se guarda en `~/.ecf-dgii/token`. Puedes proveer tu propia lógica de caché:
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
frontend = EcfDgii::FrontendClient.new(
|
|
353
|
+
get_token: -> { fetch_fresh_token_from_backend },
|
|
354
|
+
cache_token: ->(token) { Redis.current.set("ecf-token", token) },
|
|
355
|
+
get_cached_token: -> { Redis.current.get("ecf-token") },
|
|
356
|
+
environment: :test
|
|
357
|
+
)
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Polling con backoff exponencial
|
|
363
|
+
|
|
364
|
+
El SDK incluye un módulo de polling genérico que puedes usar directamente:
|
|
365
|
+
|
|
366
|
+
```ruby
|
|
367
|
+
resultado = EcfDgii::Polling.poll_until_complete do
|
|
368
|
+
client.query_ecf(rnc, encf)
|
|
369
|
+
end
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Opciones de polling (1:1 con TypeScript)
|
|
373
|
+
|
|
374
|
+
```ruby
|
|
375
|
+
options = EcfDgii::PollingOptions.new(
|
|
376
|
+
initial_delay: 1.0, # Segundos antes de la primera consulta (default: 1.0)
|
|
377
|
+
max_delay: 30.0, # Límite máximo de espera entre consultas (default: 30.0)
|
|
378
|
+
max_retries: 60, # Intentos máximos (default: 60, 0 = ilimitado)
|
|
379
|
+
backoff_multiplier: 2.0, # Multiplicador del retraso en cada intento (default: 2.0)
|
|
380
|
+
timeout: nil, # Timeout total en segundos (nil = sin timeout)
|
|
381
|
+
cancellation: -> { stop_polling_flag } # Callable de cancelación opcional
|
|
382
|
+
)
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Valores terminales
|
|
386
|
+
|
|
387
|
+
El polling termina cuando el progreso es `"Finished"` (éxito) o `"Error"` (fallo), igual que el SDK de TypeScript y el contrato de la API.
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
295
391
|
## Manejo de errores
|
|
296
392
|
|
|
297
|
-
El SDK utiliza
|
|
393
|
+
El SDK utiliza una jerarquía de excepciones tipadas, 1:1 con el SDK de TypeScript:
|
|
298
394
|
|
|
299
395
|
```ruby
|
|
300
396
|
begin
|
|
301
|
-
result = client.
|
|
397
|
+
result = client.send_ecf(ecf)
|
|
398
|
+
rescue EcfDgii::EcfError => e
|
|
399
|
+
# Error de procesamiento del ECF — incluye la respuesta completa
|
|
400
|
+
puts "Error del ECF: #{e.message}"
|
|
401
|
+
puts "Respuesta completa: #{e.response.inspect}" if e.response
|
|
302
402
|
rescue EcfDgii::PollingTimeoutError => e
|
|
303
403
|
puts "El polling tomó más tiempo de lo permitido: #{e.message}"
|
|
304
404
|
rescue EcfDgii::PollingMaxRetriesError => e
|
|
@@ -306,11 +406,65 @@ rescue EcfDgii::PollingMaxRetriesError => e
|
|
|
306
406
|
rescue EcfDgii::Generated::ApiError => e
|
|
307
407
|
puts "Error de API de ECF (Estatus: #{e.code})"
|
|
308
408
|
puts "Respuesta: #{e.response_body}"
|
|
409
|
+
rescue ArgumentError => e
|
|
410
|
+
puts "Error de validación: #{e.message}"
|
|
309
411
|
rescue => e
|
|
310
412
|
puts "Ocurrió un error inesperado: #{e.message}"
|
|
311
413
|
end
|
|
312
414
|
```
|
|
313
415
|
|
|
416
|
+
### Jerarquía de errores
|
|
417
|
+
|
|
418
|
+
```
|
|
419
|
+
StandardError
|
|
420
|
+
└── EcfDgii::EcfError # Error base del SDK (incluye response)
|
|
421
|
+
├── EcfDgii::PollingTimeoutError # Timeout de polling
|
|
422
|
+
└── EcfDgii::PollingMaxRetriesError # Máximo de reintentos
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
> `EcfDgii::PollingError` se mantiene como alias de `EcfError` por compatibilidad.
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## Arquitectura Backend / Frontend
|
|
430
|
+
|
|
431
|
+
```mermaid
|
|
432
|
+
sequenceDiagram
|
|
433
|
+
participant C as Cliente (Browser/App)
|
|
434
|
+
participant BE as Backend (Rails)
|
|
435
|
+
participant ECF as ECF API
|
|
436
|
+
|
|
437
|
+
C->>BE: POST /invoice (datos de factura)
|
|
438
|
+
Note over BE: Valida, guarda y convierte a formato eCF
|
|
439
|
+
|
|
440
|
+
BE->>ECF: POST /ecf/{tipo} (enviar eCF)
|
|
441
|
+
ECF-->>BE: { messageId }
|
|
442
|
+
BE-->>C: { messageId }
|
|
443
|
+
|
|
444
|
+
Note over C: No espera — puede continuar
|
|
445
|
+
|
|
446
|
+
alt Token en cache
|
|
447
|
+
C->>C: Usar token existente
|
|
448
|
+
else Sin token o expirado
|
|
449
|
+
C->>BE: GET /ecf-token
|
|
450
|
+
BE->>ECF: POST /apikey (solo lectura, scoped a RNC)
|
|
451
|
+
ECF-->>BE: { apiKey }
|
|
452
|
+
BE-->>C: { apiKey }
|
|
453
|
+
C->>C: Almacenar token en cache
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
loop Polling hasta completar
|
|
457
|
+
C->>ECF: GET /ecf/{rnc}/{encf} (token solo lectura)
|
|
458
|
+
ECF-->>C: { progress, codSec, ... }
|
|
459
|
+
end
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
1. Tu backend en Rails convierte las facturas internas a los modelos del eCF y los transmite con `client.send_ecf(ecf)`.
|
|
463
|
+
2. La API de ECF responde de inmediato con un `messageId` para que el backend Rails responda al cliente web sin bloquearse.
|
|
464
|
+
3. El frontend usa `EcfDgii::FrontendClient` con un token de solo lectura (obtenido via `client.create_api_key`) para hacer polling directamente contra la API de eCF de forma segura.
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
314
468
|
## Licencia
|
|
315
469
|
|
|
316
470
|
La gema está disponible como software de código abierto bajo los términos de la [Licencia MIT](LICENSE).
|
data/lib/ecf-dgii.rb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
require "uri"
|
|
2
2
|
require_relative "ecf_dgii/version"
|
|
3
3
|
require_relative "ecf_dgii/generated"
|
|
4
|
+
require_relative "ecf_dgii/exceptions"
|
|
4
5
|
require_relative "ecf_dgii/client"
|
|
5
6
|
require_relative "ecf_dgii/polling"
|
|
7
|
+
require_relative "ecf_dgii/frontend_client"
|
|
6
8
|
|
|
7
9
|
module EcfDgii
|
|
8
10
|
class << self
|
|
@@ -37,4 +39,3 @@ module EcfDgii
|
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
require_relative "ecf_dgii/railtie" if defined?(Rails)
|
|
40
|
-
|