trevol 0.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.
Files changed (231) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +15 -0
  3. data/GENERADO.md +14 -0
  4. data/LICENSE +21 -0
  5. data/README.md +173 -0
  6. data/lib/trevol/client.rb +94 -0
  7. data/lib/trevol/errors.rb +152 -0
  8. data/lib/trevol/llave.rb +21 -0
  9. data/lib/trevol/paginacion.rb +41 -0
  10. data/lib/trevol/transporte.rb +173 -0
  11. data/lib/trevol/webhook.rb +100 -0
  12. data/lib/trevol.rb +18 -0
  13. data/lib/trevol_generado/api/balance_api.rb +220 -0
  14. data/lib/trevol_generado/api/credit_notes_api.rb +671 -0
  15. data/lib/trevol_generado/api/debit_notes_api.rb +671 -0
  16. data/lib/trevol_generado/api/equivalent_document_adjustments_api.rb +691 -0
  17. data/lib/trevol_generado/api/equivalent_documents_api.rb +698 -0
  18. data/lib/trevol_generado/api/events_api.rb +231 -0
  19. data/lib/trevol_generado/api/general_api.rb +201 -0
  20. data/lib/trevol_generado/api/invoices_api.rb +736 -0
  21. data/lib/trevol_generado/api/payroll_adjustments_api.rb +637 -0
  22. data/lib/trevol_generado/api/payrolls_api.rb +637 -0
  23. data/lib/trevol_generado/api/radian_events_api.rb +431 -0
  24. data/lib/trevol_generado/api/requests_api.rb +97 -0
  25. data/lib/trevol_generado/api/support_document_adjustments_api.rb +565 -0
  26. data/lib/trevol_generado/api/support_documents_api.rb +565 -0
  27. data/lib/trevol_generado/api/webhook_endpoints_api.rb +556 -0
  28. data/lib/trevol_generado/api_client.rb +441 -0
  29. data/lib/trevol_generado/api_error.rb +58 -0
  30. data/lib/trevol_generado/api_model_base.rb +88 -0
  31. data/lib/trevol_generado/configuration.rb +392 -0
  32. data/lib/trevol_generado/models/acquirer.rb +209 -0
  33. data/lib/trevol_generado/models/air_collection.rb +205 -0
  34. data/lib/trevol_generado/models/air_extension.rb +216 -0
  35. data/lib/trevol_generado/models/allowance_charge.rb +250 -0
  36. data/lib/trevol_generado/models/alternative_currency.rb +180 -0
  37. data/lib/trevol_generado/models/balance.rb +244 -0
  38. data/lib/trevol_generado/models/balance_event_data.rb +172 -0
  39. data/lib/trevol_generado/models/balance_transaction.rb +265 -0
  40. data/lib/trevol_generado/models/balance_transaction_company.rb +172 -0
  41. data/lib/trevol_generado/models/balance_transaction_document.rb +173 -0
  42. data/lib/trevol_generado/models/balance_transaction_list.rb +191 -0
  43. data/lib/trevol_generado/models/carrier.rb +219 -0
  44. data/lib/trevol_generado/models/certificate_event_data.rb +180 -0
  45. data/lib/trevol_generado/models/cinema_extension.rb +214 -0
  46. data/lib/trevol_generado/models/cinema_screening.rb +256 -0
  47. data/lib/trevol_generado/models/company.rb +267 -0
  48. data/lib/trevol_generado/models/company_key.rb +182 -0
  49. data/lib/trevol_generado/models/company_numbering.rb +201 -0
  50. data/lib/trevol_generado/models/consumption.rb +246 -0
  51. data/lib/trevol_generado/models/consumption_adjustment.rb +204 -0
  52. data/lib/trevol_generado/models/contingency.rb +160 -0
  53. data/lib/trevol_generado/models/credit_note_list.rb +191 -0
  54. data/lib/trevol_generado/models/currency.rb +241 -0
  55. data/lib/trevol_generado/models/customer.rb +360 -0
  56. data/lib/trevol_generado/models/debit_note_list.rb +191 -0
  57. data/lib/trevol_generado/models/deleted_object.rb +178 -0
  58. data/lib/trevol_generado/models/delivery.rb +200 -0
  59. data/lib/trevol_generado/models/delivery_email.rb +181 -0
  60. data/lib/trevol_generado/models/delivery_terms.rb +169 -0
  61. data/lib/trevol_generado/models/dian_result.rb +182 -0
  62. data/lib/trevol_generado/models/dian_rule.rb +216 -0
  63. data/lib/trevol_generado/models/document.rb +432 -0
  64. data/lib/trevol_generado/models/document_event_data.rb +315 -0
  65. data/lib/trevol_generado/models/document_reference.rb +169 -0
  66. data/lib/trevol_generado/models/document_totals.rb +299 -0
  67. data/lib/trevol_generado/models/document_totals_currency.rb +197 -0
  68. data/lib/trevol_generado/models/document_totals_item.rb +196 -0
  69. data/lib/trevol_generado/models/document_totals_tax.rb +197 -0
  70. data/lib/trevol_generado/models/document_totals_withholding.rb +206 -0
  71. data/lib/trevol_generado/models/equivalent_adjustment_item.rb +374 -0
  72. data/lib/trevol_generado/models/equivalent_document.rb +470 -0
  73. data/lib/trevol_generado/models/equivalent_document_adjustment.rb +440 -0
  74. data/lib/trevol_generado/models/equivalent_document_adjustment_input.rb +388 -0
  75. data/lib/trevol_generado/models/equivalent_document_adjustment_list.rb +191 -0
  76. data/lib/trevol_generado/models/equivalent_document_contingency.rb +181 -0
  77. data/lib/trevol_generado/models/equivalent_document_contingency_backup.rb +171 -0
  78. data/lib/trevol_generado/models/equivalent_document_event_data.rb +313 -0
  79. data/lib/trevol_generado/models/equivalent_document_input.rb +365 -0
  80. data/lib/trevol_generado/models/equivalent_document_list.rb +191 -0
  81. data/lib/trevol_generado/models/equivalent_document_reference.rb +241 -0
  82. data/lib/trevol_generado/models/equivalent_item.rb +387 -0
  83. data/lib/trevol_generado/models/event.rb +223 -0
  84. data/lib/trevol_generado/models/event_claim.rb +170 -0
  85. data/lib/trevol_generado/models/event_delivery.rb +178 -0
  86. data/lib/trevol_generado/models/event_delivery_email.rb +161 -0
  87. data/lib/trevol_generado/models/event_dian_result.rb +173 -0
  88. data/lib/trevol_generado/models/event_dian_rule.rb +170 -0
  89. data/lib/trevol_generado/models/event_document_error.rb +170 -0
  90. data/lib/trevol_generado/models/event_equivalent_document_ref.rb +161 -0
  91. data/lib/trevol_generado/models/event_error.rb +170 -0
  92. data/lib/trevol_generado/models/event_invoice_ref.rb +161 -0
  93. data/lib/trevol_generado/models/event_list.rb +191 -0
  94. data/lib/trevol_generado/models/event_payroll_ref.rb +160 -0
  95. data/lib/trevol_generado/models/event_radian_invoice_ref.rb +181 -0
  96. data/lib/trevol_generado/models/event_resend_input.rb +177 -0
  97. data/lib/trevol_generado/models/event_support_document_ref.rb +161 -0
  98. data/lib/trevol_generado/models/extension.rb +215 -0
  99. data/lib/trevol_generado/models/foreign_address.rb +218 -0
  100. data/lib/trevol_generado/models/free_of_charge.rb +170 -0
  101. data/lib/trevol_generado/models/game.rb +203 -0
  102. data/lib/trevol_generado/models/game_credit.rb +204 -0
  103. data/lib/trevol_generado/models/games_extension.rb +216 -0
  104. data/lib/trevol_generado/models/games_venue.rb +204 -0
  105. data/lib/trevol_generado/models/interoperability.rb +205 -0
  106. data/lib/trevol_generado/models/interoperability_collection.rb +171 -0
  107. data/lib/trevol_generado/models/interoperability_data.rb +186 -0
  108. data/lib/trevol_generado/models/invoice_input.rb +364 -0
  109. data/lib/trevol_generado/models/invoice_list.rb +191 -0
  110. data/lib/trevol_generado/models/invoice_reference.rb +198 -0
  111. data/lib/trevol_generado/models/item.rb +439 -0
  112. data/lib/trevol_generado/models/item_allowance.rb +190 -0
  113. data/lib/trevol_generado/models/item_consignment.rb +286 -0
  114. data/lib/trevol_generado/models/item_mandator.rb +231 -0
  115. data/lib/trevol_generado/models/item_notary.rb +204 -0
  116. data/lib/trevol_generado/models/item_property.rb +223 -0
  117. data/lib/trevol_generado/models/item_tax.rb +246 -0
  118. data/lib/trevol_generado/models/item_withholding.rb +214 -0
  119. data/lib/trevol_generado/models/meter.rb +233 -0
  120. data/lib/trevol_generado/models/note_input.rb +329 -0
  121. data/lib/trevol_generado/models/numbering.rb +178 -0
  122. data/lib/trevol_generado/models/numbering_expiring_event_data.rb +209 -0
  123. data/lib/trevol_generado/models/numbering_low_event_data.rb +209 -0
  124. data/lib/trevol_generado/models/origin_note.rb +230 -0
  125. data/lib/trevol_generado/models/payment.rb +232 -0
  126. data/lib/trevol_generado/models/payment_method.rb +187 -0
  127. data/lib/trevol_generado/models/payroll.rb +455 -0
  128. data/lib/trevol_generado/models/payroll_adjustment.rb +467 -0
  129. data/lib/trevol_generado/models/payroll_adjustment_input.rb +362 -0
  130. data/lib/trevol_generado/models/payroll_adjustment_list.rb +191 -0
  131. data/lib/trevol_generado/models/payroll_basic.rb +204 -0
  132. data/lib/trevol_generado/models/payroll_compensated_vacation.rb +204 -0
  133. data/lib/trevol_generado/models/payroll_compensation.rb +205 -0
  134. data/lib/trevol_generado/models/payroll_deductions.rb +370 -0
  135. data/lib/trevol_generado/models/payroll_disability.rb +249 -0
  136. data/lib/trevol_generado/models/payroll_earnings.rb +406 -0
  137. data/lib/trevol_generado/models/payroll_employee.rb +437 -0
  138. data/lib/trevol_generado/models/payroll_employer.rb +238 -0
  139. data/lib/trevol_generado/models/payroll_epctv_bonus.rb +191 -0
  140. data/lib/trevol_generado/models/payroll_event_data.rb +303 -0
  141. data/lib/trevol_generado/models/payroll_files.rb +179 -0
  142. data/lib/trevol_generado/models/payroll_input.rb +420 -0
  143. data/lib/trevol_generado/models/payroll_leaves.rb +184 -0
  144. data/lib/trevol_generado/models/payroll_list.rb +191 -0
  145. data/lib/trevol_generado/models/payroll_loan.rb +204 -0
  146. data/lib/trevol_generado/models/payroll_novelty.rb +177 -0
  147. data/lib/trevol_generado/models/payroll_other_concept.rb +197 -0
  148. data/lib/trevol_generado/models/payroll_overtime.rb +262 -0
  149. data/lib/trevol_generado/models/payroll_paid_period.rb +222 -0
  150. data/lib/trevol_generado/models/payroll_payment.rb +215 -0
  151. data/lib/trevol_generado/models/payroll_percent_amount.rb +205 -0
  152. data/lib/trevol_generado/models/payroll_period.rb +247 -0
  153. data/lib/trevol_generado/models/payroll_place.rb +179 -0
  154. data/lib/trevol_generado/models/payroll_reference.rb +188 -0
  155. data/lib/trevol_generado/models/payroll_salary_amounts.rb +171 -0
  156. data/lib/trevol_generado/models/payroll_sanction.rb +205 -0
  157. data/lib/trevol_generado/models/payroll_service_bonus.rb +214 -0
  158. data/lib/trevol_generado/models/payroll_severance.rb +232 -0
  159. data/lib/trevol_generado/models/payroll_solidarity_fund.rb +191 -0
  160. data/lib/trevol_generado/models/payroll_totals.rb +188 -0
  161. data/lib/trevol_generado/models/payroll_transport.rb +181 -0
  162. data/lib/trevol_generado/models/payroll_unpaid_period.rb +195 -0
  163. data/lib/trevol_generado/models/payroll_vacation.rb +173 -0
  164. data/lib/trevol_generado/models/payroll_workplace.rb +188 -0
  165. data/lib/trevol_generado/models/period.rb +169 -0
  166. data/lib/trevol_generado/models/physical_document.rb +179 -0
  167. data/lib/trevol_generado/models/pos.rb +186 -0
  168. data/lib/trevol_generado/models/pos_buyer.rb +181 -0
  169. data/lib/trevol_generado/models/pos_register.rb +269 -0
  170. data/lib/trevol_generado/models/prepayment.rb +248 -0
  171. data/lib/trevol_generado/models/preview.rb +195 -0
  172. data/lib/trevol_generado/models/problem.rb +329 -0
  173. data/lib/trevol_generado/models/problem_error.rb +178 -0
  174. data/lib/trevol_generado/models/public_services_extension.rb +385 -0
  175. data/lib/trevol_generado/models/radian_event.rb +359 -0
  176. data/lib/trevol_generado/models/radian_event_data.rb +289 -0
  177. data/lib/trevol_generado/models/radian_event_input.rb +284 -0
  178. data/lib/trevol_generado/models/radian_event_invoice.rb +208 -0
  179. data/lib/trevol_generado/models/radian_event_list.rb +191 -0
  180. data/lib/trevol_generado/models/received_invoice.rb +200 -0
  181. data/lib/trevol_generado/models/received_invoice_issuer.rb +190 -0
  182. data/lib/trevol_generado/models/references.rb +182 -0
  183. data/lib/trevol_generado/models/request_list.rb +191 -0
  184. data/lib/trevol_generado/models/request_log.rb +275 -0
  185. data/lib/trevol_generado/models/seller.rb +385 -0
  186. data/lib/trevol_generado/models/shipment.rb +178 -0
  187. data/lib/trevol_generado/models/show_event.rb +276 -0
  188. data/lib/trevol_generado/models/show_extension.rb +203 -0
  189. data/lib/trevol_generado/models/show_producer.rb +212 -0
  190. data/lib/trevol_generado/models/standard_code.rb +169 -0
  191. data/lib/trevol_generado/models/stock_exchange_extension.rb +320 -0
  192. data/lib/trevol_generado/models/stock_exchange_prices.rb +169 -0
  193. data/lib/trevol_generado/models/support_adjustment_item.rb +295 -0
  194. data/lib/trevol_generado/models/support_adjustment_reference.rb +188 -0
  195. data/lib/trevol_generado/models/support_currency.rb +231 -0
  196. data/lib/trevol_generado/models/support_document.rb +401 -0
  197. data/lib/trevol_generado/models/support_document_adjustment.rb +409 -0
  198. data/lib/trevol_generado/models/support_document_adjustment_input.rb +331 -0
  199. data/lib/trevol_generado/models/support_document_adjustment_list.rb +191 -0
  200. data/lib/trevol_generado/models/support_document_delivery.rb +199 -0
  201. data/lib/trevol_generado/models/support_document_event_data.rb +305 -0
  202. data/lib/trevol_generado/models/support_document_input.rb +331 -0
  203. data/lib/trevol_generado/models/support_document_list.rb +191 -0
  204. data/lib/trevol_generado/models/support_document_reference.rb +198 -0
  205. data/lib/trevol_generado/models/support_item.rb +317 -0
  206. data/lib/trevol_generado/models/support_item_charge.rb +180 -0
  207. data/lib/trevol_generado/models/support_item_tax.rb +197 -0
  208. data/lib/trevol_generado/models/support_item_withholding.rb +205 -0
  209. data/lib/trevol_generado/models/tacit_acceptance.rb +224 -0
  210. data/lib/trevol_generado/models/tacit_acceptance_rule.rb +226 -0
  211. data/lib/trevol_generado/models/test_event_data.rb +171 -0
  212. data/lib/trevol_generado/models/third_party_charge.rb +277 -0
  213. data/lib/trevol_generado/models/third_party_company.rb +203 -0
  214. data/lib/trevol_generado/models/top_up.rb +199 -0
  215. data/lib/trevol_generado/models/top_up_buyer.rb +352 -0
  216. data/lib/trevol_generado/models/top_up_input.rb +187 -0
  217. data/lib/trevol_generado/models/top_up_wompi.rb +209 -0
  218. data/lib/trevol_generado/models/transport_extension.rb +281 -0
  219. data/lib/trevol_generado/models/webhook_delivery.rb +267 -0
  220. data/lib/trevol_generado/models/webhook_delivery_attempt.rb +192 -0
  221. data/lib/trevol_generado/models/webhook_delivery_list.rb +191 -0
  222. data/lib/trevol_generado/models/webhook_endpoint.rb +317 -0
  223. data/lib/trevol_generado/models/webhook_endpoint_input.rb +229 -0
  224. data/lib/trevol_generado/models/webhook_endpoint_list.rb +191 -0
  225. data/lib/trevol_generado/models/webhook_endpoint_rotate_input.rb +160 -0
  226. data/lib/trevol_generado/models/webhook_endpoint_update.rb +227 -0
  227. data/lib/trevol_generado/models/withholding.rb +224 -0
  228. data/lib/trevol_generado/tablas.rb +243 -0
  229. data/lib/trevol_generado/version.rb +15 -0
  230. data/lib/trevol_generado.rb +251 -0
  231. metadata +386 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ca3520036480d9eb0112a8740a7b691619b86d3a024553c5d512fa95616c2364
4
+ data.tar.gz: cad373fe775779a5c8775e5a7fe91d076ee9d04d16061cf6d1d56bd2593c9fb7
5
+ SHA512:
6
+ metadata.gz: 4c9107774f828c1d308589153110b2a7497bfb75b0b59b1d44abcfa4317319787d9ac9e5d4710ef557bdacd0a11e1e71086392edc4c0e5ca13763f42eecca937
7
+ data.tar.gz: 9df656532ba941372add5fada3acf0b47b5eadd621010d7e91be1b63ca88f2605348cbbdf0714d787b9502dc38f9ba96e7e38e34ac0abf92b29b77deb8fe2f73
data/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Registro de cambios
2
+
3
+ El formato sigue [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/) y el versionado, [SemVer](https://semver.org/lang/es/): antes
4
+ de la 1.0, un cambio incompatible sube el número menor.
5
+
6
+ ## [0.1.0] — sin publicar
7
+
8
+ Primera versión (API `2026-10-22`).
9
+
10
+ - `Trevol::Client` con un recurso por etiqueta de la API (`invoices`, `credit_notes`, `debit_notes`, `support_documents`, `payrolls`,
11
+ `radian_events`, `webhook_endpoints`, `events`, `requests`, `balance`, `general`…) y los modelos generados de la especificación
12
+ (`TrevolGenerado::*`).
13
+ - `Idempotency-Key` automática (la misma en los reintentos), reintentos con espera creciente que respetan `Retry-After`, errores tipados por
14
+ `code` (`Trevol::*Error`), entorno deducido del prefijo de la llave, `Trevol-Version` fijada.
15
+ - `paginate` (`Enumerator::Lazy`), descargas de PDF, XML y ZIP como `Tempfile`, `Trevol::Webhook#verify` y `#event_data` (Standard Webhooks).
data/GENERADO.md ADDED
@@ -0,0 +1,14 @@
1
+ # Código generado
2
+
3
+ **No edites `lib/trevol_generado/`**: `sdks/generar.sh` la borra y la vuelve a escribir entera. La capa propia (cliente,
4
+ reintentos, errores, paginación, webhooks) vive fuera de esa carpeta.
5
+
6
+ | | |
7
+ |---|---|
8
+ | Generador | OpenAPI Generator 7.26.0 (`ruby`) |
9
+ | Especificación | `especificacion/openapi.sdk.enums-abiertos.json` (variante 3.0.3 de `docs/api/openapi.json`, versión de la API 2026-10-22) |
10
+ | sha256 de la especificación | `ca431c24f76cc576b9f989aa4c9df300f5df85fdce051a0704558fae65374bd1` |
11
+ | Configuración | `openapi-generator.yaml` |
12
+
13
+ Regenerar (desde el monorepo de Trevol): `sdks/generar.sh ruby`. Comprobar que lo del repositorio es lo que sale de la
14
+ especificación: `sdks/generar.sh --comprobar ruby`.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joshua Montclair (Trevol)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # trevol (Ruby)
2
+
3
+ SDK de Ruby para la **API de Trevol**: facturación electrónica, documento soporte, documento equivalente, nómina electrónica y eventos
4
+ RADIAN ante la DIAN (Colombia). Documentación: <https://platform.trevol.si/docs>.
5
+
6
+ > **In English.** Ruby SDK for the Trevol API (Colombian e-invoicing, DIAN). Ruby ≥ 3.3, Faraday 2. Typed models and API clients generated from
7
+ > the OpenAPI spec (OpenAPI Generator, module `TrevolGenerado`), plus a thin hand-written layer (module `Trevol`): automatic `Idempotency-Key`
8
+ > (the same one on retries), retries with exponential backoff honouring `Retry-After`, typed errors by problem+json `code`, environment inferred
9
+ > from the key prefix (`tvl_test_` = DIAN test environment, `tvl_live_` = production), pinned `Trevol-Version`, Standard Webhooks signature
10
+ > verification, cursor pagination with `Enumerator::Lazy` and PDF/XML/ZIP downloads. Docs are in Spanish like the API.
11
+
12
+ **Versión 0.1.0** — API `2026-10-22`. Licencia MIT.
13
+
14
+ ## Instalación
15
+
16
+ ```sh
17
+ gem install trevol # o, en tu Gemfile: gem 'trevol'
18
+ ```
19
+
20
+ Necesita Ruby 3.3 o más nuevo. Depende de [Faraday](https://lostisland.github.io/faraday/) 2 y de la gema abierta
21
+ [`standardwebhooks`](https://rubygems.org/gems/standardwebhooks) (MIT).
22
+
23
+ ## Uso
24
+
25
+ La llave de la API se crea en la consola de desarrolladores (<https://platform.trevol.si/llaves>) y se guarda en el entorno de tu **servidor**:
26
+ `TREVOL_API_KEY`. Una llave `tvl_test_…` llama a la habilitación de la DIAN; una `tvl_live_…`, a producción. El SDK deduce el entorno del
27
+ prefijo (`trevol.environment`).
28
+
29
+ ```ruby
30
+ require 'trevol'
31
+
32
+ trevol = Trevol::Client.new # lee TREVOL_API_KEY; tvl_test_… → trevol.environment == :test
33
+
34
+ begin
35
+ factura = trevol.invoices.create_invoice(
36
+ {
37
+ external_id: 'VENTA-2026-000123',
38
+ customer: { final_consumer: true },
39
+ items: [{ description: 'Café especial 500 g', quantity: '1', unit_price: '32000', taxes: [{ code: '01', rate: '19' }] }],
40
+ payment: { form: 'cash', methods: [{ code: '10' }] }
41
+ },
42
+ idempotency_key: 'VENTA-2026-000123' # opcional: si falta, el SDK pone una y la repite en los reintentos
43
+ )
44
+ if factura.status == 'processing'
45
+ # 202: la DIAN no contestó; Trevol lo termina y avisa por webhook. NO crees otro documento para la misma venta.
46
+ end
47
+ rescue Trevol::ValidationError => e
48
+ e.errors.each { |x| warn "#{x[:pointer]} #{x[:message]}" }
49
+ rescue Trevol::Error => e
50
+ warn "#{e.code} #{e.request_id}"
51
+ end
52
+
53
+ # Todas las páginas de un listado, perezosamente (Enumerator::Lazy)
54
+ trevol.paginate { |cursor| trevol.invoices.list_invoices(limit: 100, starting_after: cursor) }.each do |f|
55
+ puts "#{f.id} #{f.status}"
56
+ end
57
+
58
+ # Los archivos llegan como Tempfile (ciérralo con close! cuando termines)
59
+ pdf = trevol.invoices.download_invoice_pdf('inv_…')
60
+ ```
61
+
62
+ Los nombres de los métodos son los del `operationId` de la [especificación](https://platform.trevol.si/docs/api) en `snake_case`
63
+ (`create_invoice`, `list_invoices`, `retrieve_invoice`, `correct_invoice`, `send_invoice`, `download_invoice_pdf`…). Los parámetros de ruta y
64
+ los de consulta obligatorios son posicionales; los opcionales (`limit:`, `starting_after:`, `idempotency_key:`) van con nombre. El cuerpo es un
65
+ `Hash` con las claves **iguales a las de la API** (`external_id`) o el modelo generado (`TrevolGenerado::InvoiceInput.new(...)`); los importes y
66
+ las fechas viajan como texto (`'32000.50'`, `'2026-10-31'`).
67
+
68
+ ## Lo que conviene saber
69
+
70
+ ### Idempotencia: manda `external_id` y tu propia clave
71
+
72
+ El SDK genera una `Idempotency-Key` por llamada y **la repite en todos los reintentos de esa llamada**. Pero si tu proceso muere y vuelves a
73
+ llamar, saldrá otra clave y, si no mandaste `external_id` (es opcional), podría duplicarse el documento. Por eso:
74
+
75
+ - manda siempre `external_id` (tu id de la venta), y
76
+ - pasa una `idempotency_key:` derivada de tu venta (`idempotency_key: 'VENTA-2026-000123'`).
77
+
78
+ Ante un `Trevol::ConflictError` con `code == 'external_id_in_use'`, el documento ya existe: usa `e.problem[:existing_id]`. Todo error lleva la
79
+ `idempotency_key` que se usó para reintentar más tarde con la misma. Un `APIConnectionError` también.
80
+
81
+ ### Respuestas 202 (`processing`, `sending`, `validating`, `contingency`)
82
+
83
+ No son errores ni se reintentan: se devuelven como el modelo de la respuesta. **No crees otro documento**: espera el webhook o consulta con
84
+ `retrieve_invoice`; repetir la misma llamada con la misma `Idempotency-Key` continúa el mismo documento. El PDF y el ZIP llegan después del
85
+ `201` (evento `invoice.delivered`, o `delivery.status == 'completed'`).
86
+
87
+ ### Reintentos
88
+
89
+ Hasta 2 reintentos (3 intentos) con espera creciente (0,5 s, 1 s, 2 s… con tope de 8 s y un poco de azar) ante un error de red, 429, 500,
90
+ 502, 503, 504, un 409 `request_in_progress` o `previous_event_pending`, y todo problema que diga `retryable: true` (salvo el 402). Respeta
91
+ `Retry-After`; si pide más de `max_retry_after` (60 s) no espera y lanza el error con `retry_after`. Las operaciones sin `Idempotency-Key`
92
+ (crear un endpoint, `…/send`, `DELETE`) solo se reintentan con 429. Los 202 y los errores finales (400, 401, 402, 403, 404, 422 y los 409
93
+ `invalid_state`, `external_id_in_use`, `duplicate_document`) **nunca** se reintentan. El SDK no usa `faraday-retry`: es el único que reintenta.
94
+
95
+ ### La versión de la API
96
+
97
+ El SDK fija `Trevol-Version: 2026-10-22` en cada petición. **Ojo:** una llave creada con la versión `2026-10-21` que empiece a usar este SDK
98
+ pasa a `2026-10-22` (el encabezado manda): recibe `202 processing` donde antes recibía `503`. Si todavía no quieres ese cambio:
99
+ `Trevol::Client.new(llave, api_version: '2026-10-21')`. Más en <https://platform.trevol.si/docs/versiones>.
100
+
101
+ ### Errores tipados
102
+
103
+ `Trevol::Error` es la base; las clases salen del `code` del problem+json: `AuthenticationError`, `PermissionDeniedError`, `NotFoundError`,
104
+ `InvalidRequestError`, `IdempotencyError`, `ValidationError` (con `errors`: `field`, `pointer`, `message`), `ConflictError`,
105
+ `PreconditionError`, `DianRejectedError` (con `problem[:document]` y `problem[:rules]`), `BalanceError`, `RateLimitError`,
106
+ `DianUnavailableError` e `InternalServerError`. Un `code` que el SDK todavía no conoce llega como `Trevol::Error` con ese `code`: nunca rompe.
107
+ Además: `APIConnectionError` (no hubo respuesta), `ConfigurationError` (mal uso, antes de llamar) y `WebhookVerificationError` (`reason`: `:firma`,
108
+ `:fecha`, `:cabeceras` o `:cuerpo`). Cada error tiene `status`, `code`, `type`, `title`, `detail`, `request_id`, `retryable?`, `retry_after`,
109
+ `errors`, `idempotency_key`, `problem` (el cuerpo completo, con claves símbolo) y `headers` (los de la respuesta). Ninguno incluye tu llave.
110
+
111
+ ### Webhooks
112
+
113
+ ```ruby
114
+ # Rails
115
+ class WebhooksController < ActionController::API
116
+ WH = Trevol::Webhook.new(ENV.fetch('TREVOL_WEBHOOK_SECRET')) # whsec_…
117
+
118
+ def trevol
119
+ evento = WH.verify(request.raw_post, request.headers) # el cuerpo CRUDO, no params
120
+ # Deduplica por evento.id (también llega en la cabecera webhook-id): la verificación no evita repeticiones.
121
+ datos = WH.event_data(evento) # el modelo según evento.type (p. ej. TrevolGenerado::DocumentEventData)
122
+ head :ok
123
+ rescue Trevol::WebhookVerificationError
124
+ head :bad_request
125
+ end
126
+ end
127
+ ```
128
+
129
+ En Sinatra usa `request.body.read`. La firma se calcula sobre los **bytes exactos**: no uses el cuerpo ya decodificado. `verify` acepta las
130
+ cabeceras como `Hash` (cualquier caja, también el formato `HTTP_WEBHOOK_ID` de Rack) o `request.headers`; la firma esperada la calcula la gema
131
+ de Standard Webhooks, y el SDK tolera 5 minutos hacia atrás y hacia adelante en `webhook-timestamp` y compara en tiempo constante.
132
+
133
+ ### Estado y cabeceras de la respuesta
134
+
135
+ Cada método tiene una variante `…_with_http_info` que devuelve `[modelo, estado, cabeceras]`:
136
+ `factura, estado, cabeceras = trevol.invoices.create_invoice_with_http_info(...)` (`cabeceras['request-id']`, `cabeceras['idempotent-replayed']`).
137
+
138
+ ### Cabeceras propias por llamada
139
+
140
+ Cada método acepta `header_params: { 'X-Mi-Cabecera' => '…' }` (se mezcla con las del SDK) en su último argumento.
141
+
142
+ ## Opciones
143
+
144
+ ```ruby
145
+ Trevol::Client.new(
146
+ llave,
147
+ api_version: '2026-10-22', # Trevol-Version en cada petición
148
+ max_retries: 2, # 0 los apaga
149
+ timeout: 65, # segundos por intento
150
+ max_retry_after: 60, # segundos
151
+ base_url: 'https://api.trevol.si/v1',
152
+ environment: :test, # si la llave no coincide: ConfigurationError antes de llamar
153
+ proxy: nil # p. ej. 'http://proxy.miempresa.co:3128'
154
+ )
155
+ ```
156
+
157
+ ## Cómo está hecho
158
+
159
+ - `lib/trevol_generado/` lo escribe [OpenAPI Generator](https://openapi-generator.tech) 7.26.0 (`ruby`, Faraday) desde la especificación
160
+ pública y **no se edita** (ver `GENERADO.md`). Para que un valor nuevo de un `enum` en lo que la API devuelve no rompa a quien ya integró,
161
+ los modelos de respuesta se generan sin `enum`.
162
+ - Lo propio (`lib/trevol/`) vive fuera y solo usa el punto de extensión del código generado: un middleware de Faraday (`Trevol::Transporte`),
163
+ que lanza el error tipado y reintenta. La gema también declara `base64` y `cgi` porque `standardwebhooks` 1.1.0 y el código generado los
164
+ usan sin que Ruby 3.4/4.0 los traiga ya en la biblioteca estándar.
165
+ - Las pruebas: `bundle install && bundle exec rake test` (unitarias sin red, contra un servidor HTTP local, con los mismos casos de los demás SDK
166
+ en `pruebas/datos/`); `TREVOL_PRISM_URL=http://127.0.0.1:4010 bundle exec rake prism` contra un servidor simulado
167
+ (`npx @stoplight/prism-cli mock pruebas/datos/openapi.json -p 4010`) que valida cada petición contra la especificación, y que además corre
168
+ como programa cada fragmento de código de la documentación.
169
+ - `bundle exec ruby pruebas/humo/humo.rb` es la prueba de humo **real** (solo con una llave `tvl_test_`; no emite documentos).
170
+
171
+ ## Licencia
172
+
173
+ MIT © 2026 Joshua Montclair (Trevol).
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'securerandom'
5
+ require 'rbconfig'
6
+ require_relative 'errors'
7
+ require_relative 'llave'
8
+ require_relative 'paginacion'
9
+ require_relative 'transporte'
10
+
11
+ module Trevol
12
+ URL_BASE = 'https://api.trevol.si/v1'
13
+
14
+ # El cliente de la API de Trevol: un recurso por etiqueta (`invoices`, `credit_notes`, …), cada uno con los métodos generados de la
15
+ # especificación (`create_invoice`, `list_invoices`, `retrieve_invoice`…). Documentación: https://platform.trevol.si/docs
16
+ #
17
+ # trevol = Trevol::Client.new # lee TREVOL_API_KEY; tvl_test_… → trevol.environment == :test
18
+ class Client
19
+ RECURSOS = {
20
+ balance: TrevolGenerado::BalanceApi, credit_notes: TrevolGenerado::CreditNotesApi, debit_notes: TrevolGenerado::DebitNotesApi,
21
+ equivalent_document_adjustments: TrevolGenerado::EquivalentDocumentAdjustmentsApi, equivalent_documents: TrevolGenerado::EquivalentDocumentsApi,
22
+ events: TrevolGenerado::EventsApi, general: TrevolGenerado::GeneralApi, invoices: TrevolGenerado::InvoicesApi,
23
+ payroll_adjustments: TrevolGenerado::PayrollAdjustmentsApi, payrolls: TrevolGenerado::PayrollsApi,
24
+ radian_events: TrevolGenerado::RadianEventsApi, requests: TrevolGenerado::RequestsApi,
25
+ support_document_adjustments: TrevolGenerado::SupportDocumentAdjustmentsApi, support_documents: TrevolGenerado::SupportDocumentsApi,
26
+ webhook_endpoints: TrevolGenerado::WebhookEndpointsApi
27
+ }.freeze
28
+
29
+ RECURSOS.each_key { |nombre| attr_reader nombre }
30
+
31
+ # @return [Symbol] :test (llave tvl_test_, habilitación de la DIAN) o :live (tvl_live_, producción)
32
+ attr_reader :environment
33
+ # @return [String] el valor de `Trevol-Version` que se manda en cada petición
34
+ attr_reader :api_version
35
+
36
+ # @param api_key [String, nil] `tvl_test_…` o `tvl_live_…`; si falta se lee `TREVOL_API_KEY`
37
+ # @param api_version [String, nil] el valor de `Trevol-Version` en cada petición (por defecto `Trevol::API_VERSION`)
38
+ # @param max_retries [Integer] reintentos tras el primer intento (3 intentos en total por defecto). 0 los apaga
39
+ # @param timeout [Numeric] segundos por intento (65 por defecto, como el playground)
40
+ # @param max_retry_after [Numeric] si `Retry-After` pide esperar más segundos que esto, no se espera: se lanza el error con `retry_after`
41
+ # @param base_url [String] por defecto https://api.trevol.si/v1 (para un servidor simulado en las pruebas)
42
+ # @param environment [Symbol, String, nil] :test o :live: si la llave no coincide, `ConfigurationError` ANTES de llamar
43
+ # @param proxy [String, nil] URL de un proxy (se pasa a Faraday)
44
+ # @param internos [Hash, nil] solo para pruebas: `dormir`, `azar`, `uuid` y `ahora`
45
+ # rubocop:disable Metrics/ParameterLists
46
+ def initialize(api_key = nil, api_version: nil, max_retries: 2, timeout: 65, max_retry_after: 60, base_url: URL_BASE, environment: nil,
47
+ proxy: nil, internos: nil)
48
+ llave = api_key.nil? ? ENV.fetch('TREVOL_API_KEY', nil) : api_key
49
+ if llave.nil? || llave.to_s.strip.empty?
50
+ raise ConfigurationError, 'Falta la llave de la API: pásala a Trevol::Client.new(...) o define la variable de entorno TREVOL_API_KEY.'
51
+ end
52
+
53
+ @environment = Llave.entorno(llave)
54
+ if environment && environment.to_sym != @environment
55
+ raise ConfigurationError, "Pediste el entorno «#{environment}» pero la llave es de «#{@environment}» " \
56
+ "(#{@environment == :test ? 'tvl_test_ = habilitación' : 'tvl_live_ = producción'})."
57
+ end
58
+ raise ConfigurationError, 'max_retries no puede ser negativo.' if max_retries.negative?
59
+
60
+ @api_version = api_version || API_VERSION
61
+ i = internos || {}
62
+ base_url = base_url.chomp('/')
63
+ opciones = Transporte::Opciones.new(
64
+ llave: llave.to_s.strip, api_version: @api_version, user_agent: "trevol-ruby/#{VERSION} (ruby #{RUBY_VERSION}; +https://platform.trevol.si/docs)",
65
+ max_reintentos: max_retries, max_retry_after: max_retry_after.to_f, ruta_base: URI.parse(base_url).path.chomp('/'),
66
+ dormir: i[:dormir] || ->(s) { sleep(s) }, azar: i[:azar] || -> { rand }, uuid: i[:uuid] || -> { SecureRandom.uuid }, ahora: i[:ahora] || -> { Time.now }
67
+ )
68
+ config = TrevolGenerado::Configuration.new
69
+ uri = URI.parse(base_url)
70
+ config.scheme = uri.scheme
71
+ config.host = uri.port && ![80, 443].include?(uri.port) ? "#{uri.host}:#{uri.port}" : uri.host
72
+ config.base_path = uri.path
73
+ config.timeout = timeout
74
+ config.logger = Logger.new(nil) # el generado escribe «Temp file written…» en cada descarga; la persona decide qué hacer con el archivo
75
+ config.proxy = proxy if proxy
76
+ config.use(Transporte, opciones)
77
+ config.configure_faraday_connection { |conexion| i[:conexion].call(conexion) } if i[:conexion]
78
+ @api_client = TrevolGenerado::ApiClient.new(config)
79
+ RECURSOS.each { |nombre, clase| instance_variable_set(:"@#{nombre}", clase.new(@api_client)) }
80
+ end
81
+ # rubocop:enable Metrics/ParameterLists
82
+
83
+ # Recorre todas las páginas de un listado, perezosamente (`Enumerator::Lazy`):
84
+ #
85
+ # trevol.paginate { |cursor| trevol.invoices.list_invoices(limit: 100, starting_after: cursor) }.each { |f| puts f.id }
86
+ def paginate(&)
87
+ Paginacion.paginar(&)
88
+ end
89
+
90
+ # La llave no se guarda en el objeto; un inspect nunca debe enseñar nada sensible.
91
+ def inspect = "#<Trevol::Client environment=#{environment.inspect} api_version=#{api_version.inspect}>"
92
+ alias_method :to_s, :inspect
93
+ end
94
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'time'
5
+
6
+ module Trevol
7
+ # La base de todos los errores del SDK (docs/api/43 §2.6.5): un `rescue Trevol::Error` ya lo atrapa todo.
8
+ #
9
+ # Toda respuesta que no es 2xx llega como una subclase, según el `code` del problem+json (el mismo vocabulario que
10
+ # https://platform.trevol.si/docs/errores). Ninguna lleva la llave ni las cabeceras de la PETICIÓN.
11
+ class Error < StandardError
12
+ # @return [Integer, nil] el estado HTTP (nil si no hubo respuesta)
13
+ attr_reader :status
14
+ # @return [String, nil] el `code` del problem+json
15
+ attr_reader :code
16
+ attr_reader :type, :title, :detail, :request_id, :retryable, :retry_after, :idempotency_key, :problem, :headers
17
+ # @return [Array<Hash>] las `errors[]` de un 422: `{field:, pointer:, message:}`
18
+ attr_reader :errors
19
+
20
+ # rubocop:disable Metrics/ParameterLists
21
+ def initialize(message = nil, status: nil, code: nil, type: nil, title: nil, detail: nil, request_id: nil, retryable: nil,
22
+ retry_after: nil, errors: [], idempotency_key: nil, problem: nil, headers: {})
23
+ super(message)
24
+ @status = status
25
+ @code = code
26
+ @type = type
27
+ @title = title
28
+ @detail = detail
29
+ @request_id = request_id
30
+ @retryable = retryable
31
+ @retry_after = retry_after # segundos
32
+ @errors = errors
33
+ # La Idempotency-Key de la llamada: úsala para reintentar más tarde con la MISMA clave.
34
+ @idempotency_key = idempotency_key
35
+ # El cuerpo problem+json completo, con los campos extra (existing_id, reason, document, rules…), con claves símbolo.
36
+ @problem = problem
37
+ # Las cabeceras de la RESPUESTA.
38
+ @headers = headers
39
+ end
40
+ # rubocop:enable Metrics/ParameterLists
41
+
42
+ def retryable? = retryable == true
43
+
44
+ # Clase, code, estado, detalle y request_id; nunca la llave ni cabeceras de la petición.
45
+ def to_s
46
+ partes = [self.class.name.to_s.split('::').last]
47
+ partes << code if code
48
+ partes << "HTTP #{status}" if status
49
+ cola = request_id ? " [request_id=#{request_id}]" : ''
50
+ "#{partes.join(' ')}: #{detail || title || super}#{cola}"
51
+ end
52
+
53
+ def inspect = "#<#{self.class} code=#{code.inspect} status=#{status.inspect} request_id=#{request_id.inspect}>"
54
+ end
55
+
56
+ class AuthenticationError < Error; end
57
+ class PermissionDeniedError < Error; end
58
+ class NotFoundError < Error; end
59
+ class InvalidRequestError < Error; end
60
+ class IdempotencyError < Error; end
61
+ # 422 `validation_error`: `errors` dice qué campo (`pointer`) y por qué.
62
+ class ValidationError < Error; end
63
+ class ConflictError < Error; end
64
+ class PreconditionError < Error; end
65
+ # 422 `dian_rejected`: `problem[:document]` trae el documento y `problem[:rules]` las reglas que la DIAN aplicó.
66
+ class DianRejectedError < Error; end
67
+ class BalanceError < Error; end
68
+ class RateLimitError < Error; end
69
+ class DianUnavailableError < Error; end
70
+ class InternalServerError < Error; end
71
+ # No hubo respuesta: red, DNS, TLS o tiempo agotado. Lleva la `idempotency_key` para reintentar sin duplicar.
72
+ class APIConnectionError < Error; end
73
+ # Mal uso del SDK, antes de llamar: llave mal formada, entorno que no coincide. Nunca incluye la llave.
74
+ class ConfigurationError < Error; end
75
+
76
+ # La firma, la fecha o las cabeceras de un webhook no son válidas. `reason`: :firma, :fecha, :cabeceras o :cuerpo.
77
+ class WebhookVerificationError < Error
78
+ attr_reader :reason
79
+
80
+ def initialize(message, reason)
81
+ super(message)
82
+ @reason = reason
83
+ end
84
+ end
85
+
86
+ # Convierte una respuesta que no es 2xx en su error tipado. @api private
87
+ module Errores
88
+ RECORTE = 2048
89
+
90
+ CLASES = {
91
+ 'AuthenticationError' => AuthenticationError, 'PermissionDeniedError' => PermissionDeniedError, 'NotFoundError' => NotFoundError,
92
+ 'InvalidRequestError' => InvalidRequestError, 'IdempotencyError' => IdempotencyError, 'ValidationError' => ValidationError,
93
+ 'ConflictError' => ConflictError, 'PreconditionError' => PreconditionError, 'DianRejectedError' => DianRejectedError,
94
+ 'BalanceError' => BalanceError, 'RateLimitError' => RateLimitError, 'DianUnavailableError' => DianUnavailableError,
95
+ 'InternalServerError' => InternalServerError
96
+ }.freeze
97
+
98
+ module_function
99
+
100
+ def numero(valor)
101
+ return nil if valor == true || valor == false || valor.nil?
102
+
103
+ n = valor.is_a?(String) ? Float(valor.strip, exception: false) : valor
104
+ n.is_a?(Numeric) && n.finite? && n >= 0 ? n.to_f : nil
105
+ end
106
+
107
+ # El valor de `Retry-After`: segundos o una fecha HTTP. nil si no está o no se entiende.
108
+ def leer_retry_after(valor, ahora = Time.now)
109
+ return nil if valor.nil? || valor.to_s.strip.empty?
110
+
111
+ n = numero(valor)
112
+ return n if n
113
+
114
+ fecha = Time.httpdate(valor.to_s.strip)
115
+ [(fecha - ahora).ceil, 0].max.to_f
116
+ rescue ArgumentError
117
+ nil
118
+ end
119
+
120
+ # El cuerpo se lee como un mapa TOLERANTE (nunca con el modelo generado: un `code` nuevo o un campo que falta no puede romper el
121
+ # manejo de errores).
122
+ def de_respuesta(estado, texto, cabeceras, idempotency_key, ahora = Time.now)
123
+ cab = cabeceras.to_h.transform_keys { |k| k.to_s.downcase }
124
+ cuerpo = begin
125
+ j = JSON.parse(texto.to_s, symbolize_names: true)
126
+ j.is_a?(Hash) ? j : nil
127
+ rescue JSON::ParserError
128
+ nil
129
+ end
130
+ cadena = ->(clave) { cuerpo && cuerpo[clave].is_a?(String) ? cuerpo[clave] : nil }
131
+ code = cadena.call(:code)
132
+ detail = cadena.call(:detail)
133
+ title = cadena.call(:title)
134
+ errores = cuerpo && cuerpo[:errors].is_a?(Array) ? cuerpo[:errors].grep(Hash).map { |x| { field: nil, pointer: nil, message: nil }.merge(x) } : []
135
+ retryable = cuerpo && [true, false].include?(cuerpo[:retryable]) ? cuerpo[:retryable] : nil
136
+ retry_after = leer_retry_after(cab['retry-after'], ahora) || numero(cuerpo && cuerpo[:retry_after])
137
+ mensaje = detail || title || (cuerpo ? "La API respondió #{estado}" : "La API respondió #{estado} sin problem+json")
138
+ nombre = if code
139
+ TrevolGenerado::Tablas::CLASE_POR_CODE[code] # un code que el SDK no conoce (la API sumó uno) da la base, nunca rompe
140
+ else
141
+ TrevolGenerado::Tablas::CLASE_POR_ESTADO[estado.to_s] || (estado >= 500 ? TrevolGenerado::Tablas::CLASE_POR_ESTADO['5xx'] : nil)
142
+ end
143
+ clase = CLASES.fetch(nombre, Error)
144
+ clase.new(
145
+ mensaje, status: estado, code: code, type: cadena.call(:type), title: title,
146
+ detail: cuerpo ? detail : (texto.to_s.strip[0, RECORTE].then { |t| t.empty? ? nil : t }),
147
+ request_id: cadena.call(:request_id) || cab['request-id'], retryable: retryable, retry_after: retry_after,
148
+ errors: errores, idempotency_key: idempotency_key, problem: cuerpo, headers: cabeceras.to_h
149
+ )
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors'
4
+
5
+ module Trevol
6
+ # La llave de la API: su formato y el entorno que se deduce de su prefijo (docs/api/43 §2.6.1).
7
+ module Llave
8
+ FORMATO = /\Atvl_(test|live)_[0-9a-f]{64}\z/
9
+
10
+ module_function
11
+
12
+ # Valida la llave y devuelve su entorno: `:test` (habilitación de la DIAN) o `:live` (producción). El mensaje NUNCA incluye la llave.
13
+ def entorno(llave)
14
+ m = llave.is_a?(String) ? FORMATO.match(llave.strip) : nil
15
+ raise ConfigurationError, 'La llave de la API no tiene el formato esperado (tvl_test_… o tvl_live_… con 64 caracteres hexadecimales). ' \
16
+ 'Créala en https://platform.trevol.si/llaves y pásala completa o define TREVOL_API_KEY.' unless m
17
+
18
+ m[1].to_sym
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trevol
4
+ # Paginación con iteradores (docs/api/43 §2.6.7): la API pagina por cursor (`starting_after`).
5
+ module Paginacion
6
+ module_function
7
+
8
+ # Recorre todas las páginas de un listado, perezosamente (devuelve un `Enumerator::Lazy`).
9
+ #
10
+ # El bloque recibe el cursor (`starting_after`; nil la primera vez), pide UNA página y devuelve el sobre de lista generado
11
+ # (`data`, `has_more`, `next_cursor`). Sigue con `next_cursor` mientras `has_more`; si la página no lo trae, usa el `id` del último
12
+ # elemento (así lo arma la API). Se detiene si el cursor no avanza.
13
+ def paginar(&pagina)
14
+ raise ArgumentError, 'paginate necesita un bloque { |cursor| ... }' unless pagina
15
+
16
+ Enumerator.new do |y|
17
+ cursor = nil
18
+ loop do
19
+ p = pagina.call(cursor)
20
+ datos = Array(tomar(p, :data))
21
+ datos.each { |d| y << d }
22
+ break if tomar(p, :has_more) != true || datos.empty?
23
+
24
+ siguiente = tomar(p, :next_cursor)
25
+ siguiente = tomar(datos.last, :id) if siguiente.nil? || siguiente.to_s.empty?
26
+ break if siguiente.nil? || siguiente.to_s.empty? || siguiente == cursor
27
+
28
+ cursor = siguiente
29
+ end
30
+ end.lazy
31
+ end
32
+
33
+ def tomar(objeto, clave)
34
+ if objeto.respond_to?(clave)
35
+ objeto.public_send(clave)
36
+ elsif objeto.is_a?(Hash)
37
+ objeto[clave] || objeto[clave.to_s]
38
+ end
39
+ end
40
+ end
41
+ end