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.
@@ -1,6 +1,12 @@
1
1
  require "uri"
2
+ require_relative "exceptions"
3
+ require_relative "polling"
2
4
 
3
5
  module EcfDgii
6
+ # High-level client for the ECF DGII API.
7
+ #
8
+ # Mirrors the TypeScript {EcfClient} 1:1 — same method names (snake_case),
9
+ # same validation, same error semantics.
4
10
  class Client
5
11
  ENVIRONMENT_URLS = {
6
12
  test: "https://api.test.ecfx.ssd.com.do",
@@ -8,21 +14,39 @@ module EcfDgii
8
14
  prod: "https://api.prod.ecfx.ssd.com.do"
9
15
  }.freeze
10
16
 
11
- attr_reader :api_client, :environment
17
+ ECF_TYPE_ROUTE_MAP = {
18
+ "FacturaDeCreditoFiscalElectronica" => "31",
19
+ "FacturaDeConsumoElectronica" => "32",
20
+ "NotaDeDebitoElectronica" => "33",
21
+ "NotaDeCreditoElectronica" => "34",
22
+ "ComprasElectronico" => "41",
23
+ "GastosMenoresElectronico" => "43",
24
+ "RegimenesEspecialesElectronico" => "44",
25
+ "GubernamentalElectronico" => "45",
26
+ "ComprobanteDeExportacionesElectronico" => "46",
27
+ "ComprobanteParaPagosAlExteriorElectronico" => "47"
28
+ }.freeze
29
+
30
+ # @return [EcfDgii::Generated::ApiClient] The underlying generated API client.
31
+ attr_reader :api_client
32
+
33
+ # @return [Symbol] The configured environment (:test, :cert, or :prod).
34
+ attr_reader :environment
12
35
 
13
36
  def initialize(api_key: nil, base_url: nil, environment: :test, timeout: 30)
14
37
  token = api_key || ENV["ECF_API_KEY"]
15
38
  resolved_url = base_url || ENV["ECF_API_URL"] || ENVIRONMENT_URLS[environment.to_sym]
39
+
16
40
  raise ArgumentError, "Se requiere un api_key o la variable de entorno ECF_API_KEY" if token.nil? || token.empty?
17
41
  raise ArgumentError, "El entorno especificado o la URL base no son válidos" if resolved_url.nil? || resolved_url.empty?
18
42
 
19
43
  config = EcfDgii::Generated::Configuration.new
20
44
  uri = URI.parse(resolved_url)
21
-
45
+
22
46
  config.scheme = uri.scheme
23
47
  config.host = uri.host
24
48
  config.base_path = uri.path.empty? ? "" : uri.path
25
-
49
+
26
50
  config.access_token = token
27
51
  config.timeout = timeout
28
52
 
@@ -30,9 +54,9 @@ module EcfDgii
30
54
  @environment = environment.to_sym
31
55
  end
32
56
 
33
- # ------------------------------------------------------------------
34
- # Base API Clients
35
- # ------------------------------------------------------------------
57
+ # ---------------------------------------------------------------------------
58
+ # Base API clients
59
+ # ---------------------------------------------------------------------------
36
60
 
37
61
  def ecf_api
38
62
  @ecf_api ||= EcfDgii::Generated::EcfApi.new(api_client)
@@ -58,9 +82,71 @@ module EcfDgii
58
82
  @api_key_api ||= EcfDgii::Generated::ApiKeyApi.new(api_client)
59
83
  end
60
84
 
61
- # ------------------------------------------------------------------
62
- # ECF send operations (per-type)
63
- # ------------------------------------------------------------------
85
+ # ---------------------------------------------------------------------------
86
+ # ECF send + poll (mirrors TypeScript EcfClient.sendEcf)
87
+ # ---------------------------------------------------------------------------
88
+
89
+ # Send an ECF and poll until processing completes.
90
+ #
91
+ # Determines the correct endpoint from +ecf.encabezado.idDoc.tipoeCF+,
92
+ # posts the ECF, then polls until +progress+ is +Finished+ or +Error+.
93
+ #
94
+ # @param ecf [Object] Any ECF object (Ecf31ECF … Ecf47ECF) or Hash
95
+ # @param polling_options [PollingOptions, nil] Polling configuration
96
+ # @return [Object] The final EcfResponse when processing is complete
97
+ # @raise [ArgumentError] If required fields (tipoeCF, rncEmisor, encf) are missing
98
+ # @raise [EcfError] If the ECF type is unknown
99
+ # @raise [EcfError] If processing finishes with progress "Error"
100
+ # @raise [PollingTimeoutError] If total timeout is exceeded
101
+ # @raise [PollingMaxRetriesError] If max retries is exceeded
102
+ def send_ecf(ecf, polling_options = nil)
103
+ # 1. Extract tipoeCF
104
+ tipoe_cf = extract_tipoe_cf(ecf)
105
+ raise ArgumentError, "ECF must have encabezado.idDoc.tipoeCF" if tipoe_cf.nil? || tipoe_cf.to_s.empty?
106
+
107
+ # 2. Resolve route
108
+ route = ECF_TYPE_ROUTE_MAP[tipoe_cf.to_s]
109
+ raise ArgumentError, "Unknown tipoeCF: #{tipoe_cf}" if route.nil?
110
+
111
+ # 3. Extract rncEmisor (for polling)
112
+ rnc = extract_rncemisor(ecf)
113
+ raise ArgumentError, "ECF must have encabezado.emisor.rncEmisor" if rnc.nil? || rnc.to_s.empty?
114
+
115
+ # 4. Extract encf (for polling)
116
+ encf = extract_encf(ecf)
117
+ raise ArgumentError, "ECF must have encabezado.idDoc.encf" if encf.nil? || encf.to_s.empty?
118
+
119
+ # 5. POST to the correct endpoint
120
+ response = post_ecf(route, ecf)
121
+
122
+ # 6. Poll until complete
123
+ result = poll_until_complete(response, rnc, encf, polling_options)
124
+
125
+ # 7. Throw EcfError if progress is Error (matching TS behavior)
126
+ progress = extract_progress_value(result)
127
+ if progress == "Error"
128
+ error_msg = nil
129
+ if result.respond_to?(:errors)
130
+ error_msg = result.errors
131
+ elsif result.respond_to?(:mensaje)
132
+ error_msg = result.mensaje
133
+ end
134
+ error_msg ||= result[:errors] || result[:mensaje] || result["errors"] || result["mensaje"] || "ECF processing failed"
135
+ raise EcfError.new(error_msg, result)
136
+ end
137
+
138
+ result
139
+ end
140
+
141
+ # Convenience alias matching older Ruby SDK API.
142
+ # @deprecated Use {#send_ecf} instead (which now includes polling 1:1 with TS).
143
+ def send_ecf_and_poll(ecf, options = nil)
144
+ send_ecf(ecf, options)
145
+ end
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # Individual ECF type send methods (kept for backward compatibility)
149
+ # ---------------------------------------------------------------------------
64
150
 
65
151
  def send_ecf31(ecf)
66
152
  ecf_api.recepcion_ecf_31(ecf)
@@ -102,242 +188,304 @@ module EcfDgii
102
188
  ecf_api.recepcion_ecf_47(ecf)
103
189
  end
104
190
 
105
- # ------------------------------------------------------------------
106
- # Polling send operations
107
- # ------------------------------------------------------------------
108
-
109
- def send_ecf31_and_poll(ecf, options = nil)
110
- _send_and_poll(send_ecf31(ecf), options)
111
- end
112
-
113
- def send_ecf32_and_poll(ecf, options = nil)
114
- _send_and_poll(send_ecf32(ecf), options)
115
- end
116
-
117
- def send_ecf33_and_poll(ecf, options = nil)
118
- _send_and_poll(send_ecf33(ecf), options)
119
- end
191
+ # ---------------------------------------------------------------------------
192
+ # Company operations
193
+ # ---------------------------------------------------------------------------
120
194
 
121
- def send_ecf34_and_poll(ecf, options = nil)
122
- _send_and_poll(send_ecf34(ecf), options)
195
+ # List companies with optional filters.
196
+ def get_companies(opts = {})
197
+ company_api.get_companies(opts)
123
198
  end
124
199
 
125
- def send_ecf41_and_poll(ecf, options = nil)
126
- _send_and_poll(send_ecf41(ecf), options)
200
+ # Get a company by RNC.
201
+ def get_company_by_rnc(rnc)
202
+ company_api.get_company_by_rnc(rnc)
127
203
  end
128
204
 
129
- def send_ecf43_and_poll(ecf, options = nil)
130
- _send_and_poll(send_ecf43(ecf), options)
205
+ # Create or update a company.
206
+ def upsert_company(body)
207
+ company_api.upsert_company(body)
131
208
  end
132
209
 
133
- def send_ecf44_and_poll(ecf, options = nil)
134
- _send_and_poll(send_ecf44(ecf), options)
210
+ # Delete a company by RNC.
211
+ def delete_company(rnc)
212
+ company_api.delete_company(rnc)
135
213
  end
136
214
 
137
- def send_ecf45_and_poll(ecf, options = nil)
138
- _send_and_poll(send_ecf45(ecf), options)
139
- end
215
+ # ---------------------------------------------------------------------------
216
+ # Certificate operations
217
+ # ---------------------------------------------------------------------------
140
218
 
141
- def send_ecf46_and_poll(ecf, options = nil)
142
- _send_and_poll(send_ecf46(ecf), options)
219
+ # Get the current certificate for a company.
220
+ def get_certificate(rnc)
221
+ company_api.get_current_certificate(rnc)
143
222
  end
144
223
 
145
- def send_ecf47_and_poll(ecf, options = nil)
146
- _send_and_poll(send_ecf47(ecf), options)
224
+ # Update a company's certificate.
225
+ #
226
+ # @param rnc [String] Company RNC
227
+ # @param certificate [String, File] Path to the .p12 file or a File object
228
+ # @param password [String] Certificate password
229
+ def update_certificate(rnc, certificate, password)
230
+ company_api.update_certificate_company(rnc, certificate, password)
147
231
  end
148
232
 
149
- # ------------------------------------------------------------------
150
- # Dynamic send_ecf routing (similar to TS sendEcf)
151
- # ------------------------------------------------------------------
152
-
153
- ECF_TYPE_ROUTE_MAP = {
154
- "FacturaDeCreditoFiscalElectronica" => "31",
155
- "FacturaDeConsumoElectronica" => "32",
156
- "NotaDeDebitoElectronica" => "33",
157
- "NotaDeCreditoElectronica" => "34",
158
- "ComprasElectronico" => "41",
159
- "GastosMenoresElectronico" => "43",
160
- "RegimenesEspecialesElectronico" => "44",
161
- "GubernamentalElectronico" => "45",
162
- "ComprobanteDeExportacionesElectronico" => "46",
163
- "ComprobanteParaPagosAlExteriorElectronico" => "47"
164
- }.freeze
165
-
166
- def send_ecf(ecf)
167
- tipoe_cf = nil
168
- if ecf.respond_to?(:encabezado) && ecf.encabezado.respond_to?(:id_doc)
169
- tipoe_cf = ecf.encabezado.id_doc.tipoe_cf
170
- elsif ecf.is_a?(Hash)
171
- tipoe_cf = ecf.dig(:encabezado, :id_doc, :tipoe_cf) || ecf.dig("encabezado", "idDoc", "tipoeCF")
172
- end
173
-
174
- raise ArgumentError, "El objeto ECF debe contener encabezado.id_doc.tipoe_cf" if tipoe_cf.nil? || tipoe_cf.to_s.empty?
175
-
176
- route = ECF_TYPE_ROUTE_MAP[tipoe_cf.to_s]
177
- raise ArgumentError, "Tipo de eCF desconocido: #{tipoe_cf}" if route.nil?
178
-
179
- send("send_ecf#{route}", ecf)
180
- end
233
+ # @deprecated Use {#get_certificate} instead.
234
+ alias get_current_certificate get_certificate
181
235
 
182
- def send_ecf_and_poll(ecf, options = nil)
183
- _send_and_poll(send_ecf(ecf), options)
184
- end
236
+ # @deprecated Use {#update_certificate} instead.
237
+ alias update_certificate_company update_certificate
185
238
 
186
- # ------------------------------------------------------------------
239
+ # ---------------------------------------------------------------------------
187
240
  # ECF query & search operations
188
- # ------------------------------------------------------------------
241
+ # ---------------------------------------------------------------------------
189
242
 
243
+ # Query ECFs by RNC and eNCF.
190
244
  def query_ecf(rnc, encf, opts = {})
191
245
  ecf_api.query_ecf(rnc, encf, opts)
192
246
  end
193
247
 
248
+ # Search ECFs for a specific RNC.
194
249
  def search_ecfs(rnc, opts = {})
195
250
  ecf_api.search_ecfs(rnc, opts)
196
251
  end
197
252
 
253
+ # Search all ECFs across all companies.
198
254
  def search_all_ecfs(opts = {})
199
255
  ecf_api.search_all_ecfs(opts)
200
256
  end
201
257
 
202
- def get_ecf_by_id(id)
203
- ecf_api.get_ecf_by_id(id)
258
+ # Get a specific ECF by RNC and message ID.
259
+ def get_ecf_by_id(rnc, id)
260
+ ecf_api.get_ecf_by_id(rnc, id)
204
261
  end
205
262
 
206
- def anulacion_rangos(body)
207
- ecf_api.anulacion_rangos(body)
263
+ # ---------------------------------------------------------------------------
264
+ # Anulación rangos
265
+ # ---------------------------------------------------------------------------
266
+
267
+ # Request range annulment.
268
+ def anulacion_rangos(rnc, body)
269
+ ecf_api.anulacion_rangos(rnc, body)
208
270
  end
209
271
 
272
+ # List annulments.
210
273
  def list_anulaciones(opts = {})
211
274
  ecf_api.list_anulaciones(opts)
212
275
  end
213
276
 
214
- def send_aprobacion_comercial(body)
215
- recepcion_api.send_aprobacion_comercial(body)
216
- end
277
+ # ---------------------------------------------------------------------------
278
+ # Firmar semilla
279
+ # ---------------------------------------------------------------------------
217
280
 
218
- def firmar_semilla(body)
219
- ecf_api.firmar_semilla(body)
281
+ # Sign a seed for a company.
282
+ def firmar_semilla(rnc, body)
283
+ ecf_api.firmar_semilla(rnc, body)
220
284
  end
221
285
 
222
- # ------------------------------------------------------------------
223
- # Company operations
224
- # ------------------------------------------------------------------
286
+ # ---------------------------------------------------------------------------
287
+ # Reception operations
288
+ # ---------------------------------------------------------------------------
225
289
 
226
- def get_companies(opts = {})
227
- company_api.get_companies(opts)
290
+ # Search ECF reception requests.
291
+ def search_ecf_reception_requests(opts = {})
292
+ recepcion_api.search_ecf_reception_requests(opts)
228
293
  end
229
294
 
230
- def get_company_by_rnc(rnc)
231
- company_api.get_company_by_rnc(rnc)
295
+ # Search ACECF reception requests.
296
+ def search_acecf_reception_requests(opts = {})
297
+ aprobacion_comercial_api.search_acecf_reception_requests(opts)
232
298
  end
233
299
 
234
- def upsert_company(body)
235
- company_api.upsert_company(body)
300
+ # Search ECF reception requests by RNC.
301
+ def search_ecf_reception_requests_by_rnc(rnc, opts = {})
302
+ recepcion_api.search_ecf_reception_requests_by_rnc(rnc, opts)
236
303
  end
237
304
 
238
- def delete_company(rnc)
239
- company_api.delete_company(rnc)
305
+ # Get a specific ECF reception request by RNC and messageId.
306
+ def get_ecf_reception_request(rnc, message_id)
307
+ recepcion_api.get_ecf_reception_request(rnc, message_id)
240
308
  end
241
309
 
242
- def get_current_certificate(rnc)
243
- company_api.get_current_certificate(rnc)
310
+ # Get a specific ACECF reception request by messageId.
311
+ def get_acecf_reception_request(message_id)
312
+ aprobacion_comercial_api.get_acecf_reception_request(message_id)
244
313
  end
245
314
 
246
- def update_certificate_company(rnc, opts = {})
247
- company_api.update_certificate_company(rnc, opts)
315
+ # ---------------------------------------------------------------------------
316
+ # Aprobación comercial
317
+ # ---------------------------------------------------------------------------
318
+
319
+ # Send aprobación comercial (ACECF) for a given ECF reception messageId.
320
+ def aprobacion_comercial(message_id, body)
321
+ recepcion_api.aprobacion_comercial(message_id, body)
248
322
  end
249
323
 
250
- # ------------------------------------------------------------------
251
- # Api Key operations
252
- # ------------------------------------------------------------------
324
+ # @deprecated Use {#aprobacion_comercial} instead.
325
+ alias send_aprobacion_comercial aprobacion_comercial
253
326
 
254
- def new_company_api_key(body)
327
+ # ---------------------------------------------------------------------------
328
+ # ApiKey operations
329
+ # ---------------------------------------------------------------------------
330
+
331
+ # Create a new API key (read-only, scoped token for frontend use).
332
+ def create_api_key(body)
255
333
  api_key_api.new_company_api_key(body)
256
334
  end
257
335
 
258
- # ------------------------------------------------------------------
259
- # DGII operations
260
- # ------------------------------------------------------------------
336
+ # @deprecated Use {#create_api_key} instead.
337
+ alias new_company_api_key create_api_key
261
338
 
262
- def consulta_estado(rnc, rnc_emisor, ncf_electronico, rnc_comprador, codigo_seguridad)
263
- dgii_api.consulta_estado(rnc, rnc_emisor, ncf_electronico, rnc_comprador, codigo_seguridad)
264
- end
339
+ # ---------------------------------------------------------------------------
340
+ # DGII operations
341
+ # ---------------------------------------------------------------------------
265
342
 
266
- def consulta_track_id(rnc, rnc_emisor, encf)
267
- dgii_api.consulta_track_id(rnc, rnc_emisor, encf)
343
+ # Consulta directorio — listado.
344
+ def consulta_directorio_listado(rnc)
345
+ dgii_api.consulta_directorio_listado(rnc)
268
346
  end
269
347
 
270
- def consulta_timbre(rnc, rnc_emisor, ncf_electronico, rnc_comprador, codigo_seguridad, fecha_emision, monto_total, uid_timbre)
271
- dgii_api.consulta_timbre(rnc, rnc_emisor, ncf_electronico, rnc_comprador, codigo_seguridad, fecha_emision, monto_total, uid_timbre)
348
+ # Consulta directorio obtener directorio por RNC.
349
+ def consulta_directorio_por_rnc(rnc, target_rnc)
350
+ dgii_api.consulta_directorio_obtener_directorio_por_rnc(rnc, target_rnc)
272
351
  end
273
352
 
274
- def consulta_timbre_fc(rnc, rnc_emisor, ncf_electronico, rnc_comprador, fecha_emision, monto_total)
275
- dgii_api.consulta_timbre_fc(rnc, rnc_emisor, ncf_electronico, rnc_comprador, fecha_emision, monto_total)
353
+ # Consulta estado.
354
+ def consulta_estado(rnc, rnc_emisor, ncf_electronico, rnc_comprador, codigo_seguridad)
355
+ dgii_api.consulta_estado(rnc, rnc_emisor, ncf_electronico, rnc_comprador, codigo_seguridad)
276
356
  end
277
357
 
358
+ # Consulta resultado.
278
359
  def consulta_resultado(rnc, track_id)
279
360
  dgii_api.consulta_resultado(rnc, track_id)
280
361
  end
281
362
 
282
- def consulta_rfce(rnc, rnc_emisor, ncf_electronico)
283
- dgii_api.consulta_rfce(rnc, rnc_emisor, ncf_electronico)
363
+ # Consulta RFCE.
364
+ def consulta_rfce(rnc, rnc_emisor, encf, codigo_seguridad)
365
+ dgii_api.consulta_rfce(rnc, rnc_emisor, encf, codigo_seguridad)
284
366
  end
285
367
 
286
- def consulta_directorio(rnc)
287
- dgii_api.consulta_directorio_listado(rnc)
368
+ # Consulta timbre.
369
+ def consulta_timbre(rnc, rnc_emisor, ncf_electronico, rnc_comprador, codigo_seguridad, fecha_emision, monto_total, uid_timbre)
370
+ dgii_api.consulta_timbre(rnc, rnc_emisor, ncf_electronico, rnc_comprador, codigo_seguridad, fecha_emision, monto_total, uid_timbre)
288
371
  end
289
372
 
290
- def consulta_directorio_por_rnc(rnc, target_rnc)
291
- dgii_api.consulta_directorio_obtener_directorio_por_rnc(rnc, target_rnc)
373
+ # Consulta timbre FC.
374
+ def consulta_timbre_fc(rnc, rnc_emisor, ncf_electronico, rnc_comprador, fecha_emision, monto_total)
375
+ dgii_api.consulta_timbre_fc(rnc, rnc_emisor, ncf_electronico, rnc_comprador, fecha_emision, monto_total)
292
376
  end
293
377
 
294
- def estatus_servicio(rnc)
378
+ # Consulta track IDs.
379
+ def consulta_track_id(rnc, rnc_emisor, encf)
380
+ dgii_api.consulta_track_id(rnc, rnc_emisor, encf)
381
+ end
382
+
383
+ # Estatus servicios — obtener estatus.
384
+ def estatus_servicios(rnc)
295
385
  dgii_api.estatus_servicios_obtener_estatus(rnc)
296
386
  end
297
387
 
388
+ # Estatus servicios — obtener ventanas de mantenimiento.
298
389
  def ventanas_mantenimiento(rnc)
299
390
  dgii_api.estatus_servicios_obtener_ventanas_mantenimiento(rnc)
300
391
  end
301
392
 
302
- # ------------------------------------------------------------------
303
- # Reception operations
304
- # ------------------------------------------------------------------
305
-
306
- def get_ecf_reception_request(message_id)
307
- recepcion_api.get_ecf_reception_request(message_id)
308
- end
393
+ # @deprecated Use {#estatus_servicios} instead.
394
+ alias estatus_servicio estatus_servicios
309
395
 
310
- def get_ecf_receptor_by_message_id(message_id)
311
- recepcion_api.get_ecf_receptor_by_message_id(message_id)
312
- end
396
+ # @deprecated Use {#consulta_directorio_listado} instead.
397
+ alias consulta_directorio consulta_directorio_listado
313
398
 
314
- def search_ecf_reception_requests(opts = {})
315
- recepcion_api.search_ecf_reception_requests(opts)
316
- end
399
+ private
317
400
 
318
- def search_ecf_reception_requests_by_rnc(rnc, opts = {})
319
- recepcion_api.search_ecf_reception_requests_by_rnc(rnc, opts)
401
+ # Extract tipoeCF from ecf object or hash.
402
+ def extract_tipoe_cf(ecf)
403
+ if ecf.respond_to?(:encabezado) && ecf.encabezado.respond_to?(:id_doc)
404
+ id_doc = ecf.encabezado.id_doc
405
+ return id_doc.tipoe_cf if id_doc.respond_to?(:tipoe_cf)
406
+ return id_doc.tipoeCF if id_doc.respond_to?(:tipoeCF)
407
+ elsif ecf.is_a?(Hash)
408
+ return ecf.dig(:encabezado, :id_doc, :tipoe_cf) ||
409
+ ecf.dig(:encabezado, :idDoc, :tipoeCF) ||
410
+ ecf.dig("encabezado", "idDoc", "tipoeCF")
411
+ end
412
+ nil
320
413
  end
321
414
 
322
- # ------------------------------------------------------------------
323
- # Aprobacion Comercial operations
324
- # ------------------------------------------------------------------
325
-
326
- def get_acecf_reception_request(message_id)
327
- aprobacion_comercial_api.get_acecf_reception_request(message_id)
415
+ # Extract rncEmisor from ecf object or hash.
416
+ def extract_rncemisor(ecf)
417
+ if ecf.respond_to?(:encabezado) && ecf.encabezado.respond_to?(:emisor)
418
+ emisor = ecf.encabezado.emisor
419
+ return emisor.rnc_emisor if emisor.respond_to?(:rnc_emisor)
420
+ return emisor.rncEmisor if emisor.respond_to?(:rncEmisor)
421
+ elsif ecf.is_a?(Hash)
422
+ return ecf.dig(:encabezado, :emisor, :rnc_emisor) ||
423
+ ecf.dig(:encabezado, :emisor, :rncEmisor) ||
424
+ ecf.dig("encabezado", "emisor", "rncEmisor")
425
+ end
426
+ nil
328
427
  end
329
428
 
330
- def search_acecf_reception_requests(opts = {})
331
- aprobacion_comercial_api.search_acecf_reception_requests(opts)
429
+ # Extract encf from ecf object or hash.
430
+ def extract_encf(ecf)
431
+ if ecf.respond_to?(:encabezado) && ecf.encabezado.respond_to?(:id_doc)
432
+ id_doc = ecf.encabezado.id_doc
433
+ return id_doc.encf if id_doc.respond_to?(:encf)
434
+ elsif ecf.is_a?(Hash)
435
+ return ecf.dig(:encabezado, :id_doc, :encf) ||
436
+ ecf.dig(:encabezado, :idDoc, :encf) ||
437
+ ecf.dig("encabezado", "idDoc", "encf")
438
+ end
439
+ nil
440
+ end
441
+
442
+ # Extract progress value from a response object.
443
+ def extract_progress_value(result)
444
+ if result.respond_to?(:progress)
445
+ p = result.progress
446
+ p = p.value if p.respond_to?(:value)
447
+ return p.to_s
448
+ elsif result.is_a?(Hash)
449
+ p = result[:progress] || result["progress"]
450
+ return p.to_s if p
451
+ end
452
+ ""
453
+ end
454
+
455
+ # Internal: POST to the correct /ecf/{route} endpoint.
456
+ def post_ecf(route, body)
457
+ case route
458
+ when "31" then ecf_api.recepcion_ecf_31(body)
459
+ when "32" then ecf_api.recepcion_ecf_32(body)
460
+ when "33" then ecf_api.recepcion_ecf_33(body)
461
+ when "34" then ecf_api.recepcion_ecf_34(body)
462
+ when "41" then ecf_api.recepcion_ecf_41(body)
463
+ when "43" then ecf_api.recepcion_ecf_43(body)
464
+ when "44" then ecf_api.recepcion_ecf_44(body)
465
+ when "45" then ecf_api.recepcion_ecf_45(body)
466
+ when "46" then ecf_api.recepcion_ecf_46(body)
467
+ when "47" then ecf_api.recepcion_ecf_47(body)
468
+ else raise ArgumentError, "Unknown ECF route: #{route}"
469
+ end
332
470
  end
333
471
 
334
- private
335
-
336
- def _send_and_poll(initial, options)
337
- EcfDgii::Polling.poll_until_complete(options) do
338
- results = query_ecf(initial.rnc_emisor, initial.encf, include_ecf_content: false)
339
- results && !results.empty? ? results.first : initial
472
+ # Internal: poll until the ECF reaches a terminal state.
473
+ #
474
+ # rubocop:disable Metrics/MethodLength
475
+ def poll_until_complete(initial_response, rnc, encf, polling_options)
476
+ opts = polling_options || EcfDgii::PollingOptions.new
477
+
478
+ EcfDgii::Polling.poll_until_complete(opts) do
479
+ results = query_ecf(rnc, encf, include_ecf_content: false)
480
+ if results.respond_to?(:first)
481
+ results.first
482
+ elsif results.is_a?(Array)
483
+ results.first
484
+ else
485
+ results
486
+ end
340
487
  end
341
488
  end
489
+ # rubocop:enable Metrics/MethodLength
342
490
  end
343
491
  end
@@ -0,0 +1,29 @@
1
+ module EcfDgii
2
+ # Base error class for ECF SDK errors.
3
+ class EcfError < StandardError
4
+ # The full EcfResponse object containing details about the error.
5
+ attr_reader :response
6
+
7
+ def initialize(message, response = nil)
8
+ super(message)
9
+ @response = response
10
+ end
11
+ end
12
+
13
+ # Raised when polling exceeds the total timeout.
14
+ class PollingTimeoutError < EcfError
15
+ def initialize(message = "Polling timed out")
16
+ super(message)
17
+ end
18
+ end
19
+
20
+ # Raised when polling exceeds the maximum number of retries.
21
+ class PollingMaxRetriesError < EcfError
22
+ def initialize(retries)
23
+ super("Polling exceeded maximum retries (#{retries})")
24
+ end
25
+ end
26
+
27
+ # @deprecated Use {EcfError} instead. Kept for backward compatibility.
28
+ PollingError = EcfError
29
+ end