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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a2c79e13f75622c9eb910f445af2d434e748329886bab87b240aa45d3e22da3
4
- data.tar.gz: 4a54830804d256802668df2f0ad83d26016666e63f7ea1467f4d475e2b3c0ddd
3
+ metadata.gz: 283a36ead0fbc5599832378e0383bbcd60cb8a0424b07828dffa12c650d5140a
4
+ data.tar.gz: 57df5b04a64728d794fe5e8becc32dd76552a6d54de2b0dd5d75ba82a3ff47ad
5
5
  SHA512:
6
- metadata.gz: 89f604510ca590b437ccd68dc12ed21d2a8040528f00df0ea0d57526af7e35ed6410b82aa280e59581fa8b50a237764355a5679a4c5aa9266a438f60857e044d
7
- data.tar.gz: c7e3e1a40eb42669ccff5ecdd50c4936aaaf1ac6bb55f5f4dd1774a26835349be74bda8629784388139842590ccf114465c16294d27f70c70193acedf2c80af3
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.send_ecf_and_poll(ecf)
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
- ## Funcionalidades de envío y Polling
215
+ ---
188
216
 
189
- El método `send_ecf_and_poll` maneja de forma transparente:
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
- Para personalizar los tiempos de espera y el backoff exponencial:
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
- ```ruby
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
- result = client.send_ecf_and_poll(ecf, options)
206
- ```
223
+ El método `send_ecf` (1:1 con el `sendEcf` del SDK de TypeScript) maneja de forma transparente:
207
224
 
208
- ## Arquitectura Backend / Frontend
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
- ```mermaid
211
- sequenceDiagram
212
- participant C as Cliente (Browser/App)
213
- participant BE as Backend (Rails)
214
- participant ECF as ECF API
215
-
216
- C->>BE: POST /invoice (datos de factura)
217
- Note over BE: Valida, guarda y convierte a formato eCF
218
-
219
- BE->>ECF: POST /ecf/{tipo} (enviar eCF)
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
- 1. Tu backend en Rails convierte las facturas internas a los modelos del eCF y los transmite con `client.send_ecf(ecf)`.
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.get_current_certificate("101001010")
269
+ certificate = client.get_certificate("101001010")
273
270
 
274
271
  # Subir/actualizar un certificado de firma digital P12
275
- file = File.open("ruta/al/certificado.p12", "r+b")
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
- ## Consultas DGII
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.consulta_directorio("101001010")
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.estatus_servicio("101001010")
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 el control estructurado de excepciones. Puedes capturar los errores comunes del API Client y los tiempos límite del polling de la siguiente manera:
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.send_ecf_and_poll(ecf)
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
-