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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +15 -0
- data/GENERADO.md +14 -0
- data/LICENSE +21 -0
- data/README.md +173 -0
- data/lib/trevol/client.rb +94 -0
- data/lib/trevol/errors.rb +152 -0
- data/lib/trevol/llave.rb +21 -0
- data/lib/trevol/paginacion.rb +41 -0
- data/lib/trevol/transporte.rb +173 -0
- data/lib/trevol/webhook.rb +100 -0
- data/lib/trevol.rb +18 -0
- data/lib/trevol_generado/api/balance_api.rb +220 -0
- data/lib/trevol_generado/api/credit_notes_api.rb +671 -0
- data/lib/trevol_generado/api/debit_notes_api.rb +671 -0
- data/lib/trevol_generado/api/equivalent_document_adjustments_api.rb +691 -0
- data/lib/trevol_generado/api/equivalent_documents_api.rb +698 -0
- data/lib/trevol_generado/api/events_api.rb +231 -0
- data/lib/trevol_generado/api/general_api.rb +201 -0
- data/lib/trevol_generado/api/invoices_api.rb +736 -0
- data/lib/trevol_generado/api/payroll_adjustments_api.rb +637 -0
- data/lib/trevol_generado/api/payrolls_api.rb +637 -0
- data/lib/trevol_generado/api/radian_events_api.rb +431 -0
- data/lib/trevol_generado/api/requests_api.rb +97 -0
- data/lib/trevol_generado/api/support_document_adjustments_api.rb +565 -0
- data/lib/trevol_generado/api/support_documents_api.rb +565 -0
- data/lib/trevol_generado/api/webhook_endpoints_api.rb +556 -0
- data/lib/trevol_generado/api_client.rb +441 -0
- data/lib/trevol_generado/api_error.rb +58 -0
- data/lib/trevol_generado/api_model_base.rb +88 -0
- data/lib/trevol_generado/configuration.rb +392 -0
- data/lib/trevol_generado/models/acquirer.rb +209 -0
- data/lib/trevol_generado/models/air_collection.rb +205 -0
- data/lib/trevol_generado/models/air_extension.rb +216 -0
- data/lib/trevol_generado/models/allowance_charge.rb +250 -0
- data/lib/trevol_generado/models/alternative_currency.rb +180 -0
- data/lib/trevol_generado/models/balance.rb +244 -0
- data/lib/trevol_generado/models/balance_event_data.rb +172 -0
- data/lib/trevol_generado/models/balance_transaction.rb +265 -0
- data/lib/trevol_generado/models/balance_transaction_company.rb +172 -0
- data/lib/trevol_generado/models/balance_transaction_document.rb +173 -0
- data/lib/trevol_generado/models/balance_transaction_list.rb +191 -0
- data/lib/trevol_generado/models/carrier.rb +219 -0
- data/lib/trevol_generado/models/certificate_event_data.rb +180 -0
- data/lib/trevol_generado/models/cinema_extension.rb +214 -0
- data/lib/trevol_generado/models/cinema_screening.rb +256 -0
- data/lib/trevol_generado/models/company.rb +267 -0
- data/lib/trevol_generado/models/company_key.rb +182 -0
- data/lib/trevol_generado/models/company_numbering.rb +201 -0
- data/lib/trevol_generado/models/consumption.rb +246 -0
- data/lib/trevol_generado/models/consumption_adjustment.rb +204 -0
- data/lib/trevol_generado/models/contingency.rb +160 -0
- data/lib/trevol_generado/models/credit_note_list.rb +191 -0
- data/lib/trevol_generado/models/currency.rb +241 -0
- data/lib/trevol_generado/models/customer.rb +360 -0
- data/lib/trevol_generado/models/debit_note_list.rb +191 -0
- data/lib/trevol_generado/models/deleted_object.rb +178 -0
- data/lib/trevol_generado/models/delivery.rb +200 -0
- data/lib/trevol_generado/models/delivery_email.rb +181 -0
- data/lib/trevol_generado/models/delivery_terms.rb +169 -0
- data/lib/trevol_generado/models/dian_result.rb +182 -0
- data/lib/trevol_generado/models/dian_rule.rb +216 -0
- data/lib/trevol_generado/models/document.rb +432 -0
- data/lib/trevol_generado/models/document_event_data.rb +315 -0
- data/lib/trevol_generado/models/document_reference.rb +169 -0
- data/lib/trevol_generado/models/document_totals.rb +299 -0
- data/lib/trevol_generado/models/document_totals_currency.rb +197 -0
- data/lib/trevol_generado/models/document_totals_item.rb +196 -0
- data/lib/trevol_generado/models/document_totals_tax.rb +197 -0
- data/lib/trevol_generado/models/document_totals_withholding.rb +206 -0
- data/lib/trevol_generado/models/equivalent_adjustment_item.rb +374 -0
- data/lib/trevol_generado/models/equivalent_document.rb +470 -0
- data/lib/trevol_generado/models/equivalent_document_adjustment.rb +440 -0
- data/lib/trevol_generado/models/equivalent_document_adjustment_input.rb +388 -0
- data/lib/trevol_generado/models/equivalent_document_adjustment_list.rb +191 -0
- data/lib/trevol_generado/models/equivalent_document_contingency.rb +181 -0
- data/lib/trevol_generado/models/equivalent_document_contingency_backup.rb +171 -0
- data/lib/trevol_generado/models/equivalent_document_event_data.rb +313 -0
- data/lib/trevol_generado/models/equivalent_document_input.rb +365 -0
- data/lib/trevol_generado/models/equivalent_document_list.rb +191 -0
- data/lib/trevol_generado/models/equivalent_document_reference.rb +241 -0
- data/lib/trevol_generado/models/equivalent_item.rb +387 -0
- data/lib/trevol_generado/models/event.rb +223 -0
- data/lib/trevol_generado/models/event_claim.rb +170 -0
- data/lib/trevol_generado/models/event_delivery.rb +178 -0
- data/lib/trevol_generado/models/event_delivery_email.rb +161 -0
- data/lib/trevol_generado/models/event_dian_result.rb +173 -0
- data/lib/trevol_generado/models/event_dian_rule.rb +170 -0
- data/lib/trevol_generado/models/event_document_error.rb +170 -0
- data/lib/trevol_generado/models/event_equivalent_document_ref.rb +161 -0
- data/lib/trevol_generado/models/event_error.rb +170 -0
- data/lib/trevol_generado/models/event_invoice_ref.rb +161 -0
- data/lib/trevol_generado/models/event_list.rb +191 -0
- data/lib/trevol_generado/models/event_payroll_ref.rb +160 -0
- data/lib/trevol_generado/models/event_radian_invoice_ref.rb +181 -0
- data/lib/trevol_generado/models/event_resend_input.rb +177 -0
- data/lib/trevol_generado/models/event_support_document_ref.rb +161 -0
- data/lib/trevol_generado/models/extension.rb +215 -0
- data/lib/trevol_generado/models/foreign_address.rb +218 -0
- data/lib/trevol_generado/models/free_of_charge.rb +170 -0
- data/lib/trevol_generado/models/game.rb +203 -0
- data/lib/trevol_generado/models/game_credit.rb +204 -0
- data/lib/trevol_generado/models/games_extension.rb +216 -0
- data/lib/trevol_generado/models/games_venue.rb +204 -0
- data/lib/trevol_generado/models/interoperability.rb +205 -0
- data/lib/trevol_generado/models/interoperability_collection.rb +171 -0
- data/lib/trevol_generado/models/interoperability_data.rb +186 -0
- data/lib/trevol_generado/models/invoice_input.rb +364 -0
- data/lib/trevol_generado/models/invoice_list.rb +191 -0
- data/lib/trevol_generado/models/invoice_reference.rb +198 -0
- data/lib/trevol_generado/models/item.rb +439 -0
- data/lib/trevol_generado/models/item_allowance.rb +190 -0
- data/lib/trevol_generado/models/item_consignment.rb +286 -0
- data/lib/trevol_generado/models/item_mandator.rb +231 -0
- data/lib/trevol_generado/models/item_notary.rb +204 -0
- data/lib/trevol_generado/models/item_property.rb +223 -0
- data/lib/trevol_generado/models/item_tax.rb +246 -0
- data/lib/trevol_generado/models/item_withholding.rb +214 -0
- data/lib/trevol_generado/models/meter.rb +233 -0
- data/lib/trevol_generado/models/note_input.rb +329 -0
- data/lib/trevol_generado/models/numbering.rb +178 -0
- data/lib/trevol_generado/models/numbering_expiring_event_data.rb +209 -0
- data/lib/trevol_generado/models/numbering_low_event_data.rb +209 -0
- data/lib/trevol_generado/models/origin_note.rb +230 -0
- data/lib/trevol_generado/models/payment.rb +232 -0
- data/lib/trevol_generado/models/payment_method.rb +187 -0
- data/lib/trevol_generado/models/payroll.rb +455 -0
- data/lib/trevol_generado/models/payroll_adjustment.rb +467 -0
- data/lib/trevol_generado/models/payroll_adjustment_input.rb +362 -0
- data/lib/trevol_generado/models/payroll_adjustment_list.rb +191 -0
- data/lib/trevol_generado/models/payroll_basic.rb +204 -0
- data/lib/trevol_generado/models/payroll_compensated_vacation.rb +204 -0
- data/lib/trevol_generado/models/payroll_compensation.rb +205 -0
- data/lib/trevol_generado/models/payroll_deductions.rb +370 -0
- data/lib/trevol_generado/models/payroll_disability.rb +249 -0
- data/lib/trevol_generado/models/payroll_earnings.rb +406 -0
- data/lib/trevol_generado/models/payroll_employee.rb +437 -0
- data/lib/trevol_generado/models/payroll_employer.rb +238 -0
- data/lib/trevol_generado/models/payroll_epctv_bonus.rb +191 -0
- data/lib/trevol_generado/models/payroll_event_data.rb +303 -0
- data/lib/trevol_generado/models/payroll_files.rb +179 -0
- data/lib/trevol_generado/models/payroll_input.rb +420 -0
- data/lib/trevol_generado/models/payroll_leaves.rb +184 -0
- data/lib/trevol_generado/models/payroll_list.rb +191 -0
- data/lib/trevol_generado/models/payroll_loan.rb +204 -0
- data/lib/trevol_generado/models/payroll_novelty.rb +177 -0
- data/lib/trevol_generado/models/payroll_other_concept.rb +197 -0
- data/lib/trevol_generado/models/payroll_overtime.rb +262 -0
- data/lib/trevol_generado/models/payroll_paid_period.rb +222 -0
- data/lib/trevol_generado/models/payroll_payment.rb +215 -0
- data/lib/trevol_generado/models/payroll_percent_amount.rb +205 -0
- data/lib/trevol_generado/models/payroll_period.rb +247 -0
- data/lib/trevol_generado/models/payroll_place.rb +179 -0
- data/lib/trevol_generado/models/payroll_reference.rb +188 -0
- data/lib/trevol_generado/models/payroll_salary_amounts.rb +171 -0
- data/lib/trevol_generado/models/payroll_sanction.rb +205 -0
- data/lib/trevol_generado/models/payroll_service_bonus.rb +214 -0
- data/lib/trevol_generado/models/payroll_severance.rb +232 -0
- data/lib/trevol_generado/models/payroll_solidarity_fund.rb +191 -0
- data/lib/trevol_generado/models/payroll_totals.rb +188 -0
- data/lib/trevol_generado/models/payroll_transport.rb +181 -0
- data/lib/trevol_generado/models/payroll_unpaid_period.rb +195 -0
- data/lib/trevol_generado/models/payroll_vacation.rb +173 -0
- data/lib/trevol_generado/models/payroll_workplace.rb +188 -0
- data/lib/trevol_generado/models/period.rb +169 -0
- data/lib/trevol_generado/models/physical_document.rb +179 -0
- data/lib/trevol_generado/models/pos.rb +186 -0
- data/lib/trevol_generado/models/pos_buyer.rb +181 -0
- data/lib/trevol_generado/models/pos_register.rb +269 -0
- data/lib/trevol_generado/models/prepayment.rb +248 -0
- data/lib/trevol_generado/models/preview.rb +195 -0
- data/lib/trevol_generado/models/problem.rb +329 -0
- data/lib/trevol_generado/models/problem_error.rb +178 -0
- data/lib/trevol_generado/models/public_services_extension.rb +385 -0
- data/lib/trevol_generado/models/radian_event.rb +359 -0
- data/lib/trevol_generado/models/radian_event_data.rb +289 -0
- data/lib/trevol_generado/models/radian_event_input.rb +284 -0
- data/lib/trevol_generado/models/radian_event_invoice.rb +208 -0
- data/lib/trevol_generado/models/radian_event_list.rb +191 -0
- data/lib/trevol_generado/models/received_invoice.rb +200 -0
- data/lib/trevol_generado/models/received_invoice_issuer.rb +190 -0
- data/lib/trevol_generado/models/references.rb +182 -0
- data/lib/trevol_generado/models/request_list.rb +191 -0
- data/lib/trevol_generado/models/request_log.rb +275 -0
- data/lib/trevol_generado/models/seller.rb +385 -0
- data/lib/trevol_generado/models/shipment.rb +178 -0
- data/lib/trevol_generado/models/show_event.rb +276 -0
- data/lib/trevol_generado/models/show_extension.rb +203 -0
- data/lib/trevol_generado/models/show_producer.rb +212 -0
- data/lib/trevol_generado/models/standard_code.rb +169 -0
- data/lib/trevol_generado/models/stock_exchange_extension.rb +320 -0
- data/lib/trevol_generado/models/stock_exchange_prices.rb +169 -0
- data/lib/trevol_generado/models/support_adjustment_item.rb +295 -0
- data/lib/trevol_generado/models/support_adjustment_reference.rb +188 -0
- data/lib/trevol_generado/models/support_currency.rb +231 -0
- data/lib/trevol_generado/models/support_document.rb +401 -0
- data/lib/trevol_generado/models/support_document_adjustment.rb +409 -0
- data/lib/trevol_generado/models/support_document_adjustment_input.rb +331 -0
- data/lib/trevol_generado/models/support_document_adjustment_list.rb +191 -0
- data/lib/trevol_generado/models/support_document_delivery.rb +199 -0
- data/lib/trevol_generado/models/support_document_event_data.rb +305 -0
- data/lib/trevol_generado/models/support_document_input.rb +331 -0
- data/lib/trevol_generado/models/support_document_list.rb +191 -0
- data/lib/trevol_generado/models/support_document_reference.rb +198 -0
- data/lib/trevol_generado/models/support_item.rb +317 -0
- data/lib/trevol_generado/models/support_item_charge.rb +180 -0
- data/lib/trevol_generado/models/support_item_tax.rb +197 -0
- data/lib/trevol_generado/models/support_item_withholding.rb +205 -0
- data/lib/trevol_generado/models/tacit_acceptance.rb +224 -0
- data/lib/trevol_generado/models/tacit_acceptance_rule.rb +226 -0
- data/lib/trevol_generado/models/test_event_data.rb +171 -0
- data/lib/trevol_generado/models/third_party_charge.rb +277 -0
- data/lib/trevol_generado/models/third_party_company.rb +203 -0
- data/lib/trevol_generado/models/top_up.rb +199 -0
- data/lib/trevol_generado/models/top_up_buyer.rb +352 -0
- data/lib/trevol_generado/models/top_up_input.rb +187 -0
- data/lib/trevol_generado/models/top_up_wompi.rb +209 -0
- data/lib/trevol_generado/models/transport_extension.rb +281 -0
- data/lib/trevol_generado/models/webhook_delivery.rb +267 -0
- data/lib/trevol_generado/models/webhook_delivery_attempt.rb +192 -0
- data/lib/trevol_generado/models/webhook_delivery_list.rb +191 -0
- data/lib/trevol_generado/models/webhook_endpoint.rb +317 -0
- data/lib/trevol_generado/models/webhook_endpoint_input.rb +229 -0
- data/lib/trevol_generado/models/webhook_endpoint_list.rb +191 -0
- data/lib/trevol_generado/models/webhook_endpoint_rotate_input.rb +160 -0
- data/lib/trevol_generado/models/webhook_endpoint_update.rb +227 -0
- data/lib/trevol_generado/models/withholding.rb +224 -0
- data/lib/trevol_generado/tablas.rb +243 -0
- data/lib/trevol_generado/version.rb +15 -0
- data/lib/trevol_generado.rb +251 -0
- 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
|
data/lib/trevol/llave.rb
ADDED
|
@@ -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
|