abacatepay-ruby 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +11 -0
- data/CHANGELOG.md +50 -0
- data/README.md +87 -4
- data/abacatepay-ruby.gemspec +1 -0
- data/lib/abacate_pay/clients/billing_client.rb +13 -3
- data/lib/abacate_pay/clients/checkout_client.rb +24 -3
- data/lib/abacate_pay/clients/client.rb +130 -9
- data/lib/abacate_pay/clients/coupon_client.rb +1 -1
- data/lib/abacate_pay/clients/customer_client.rb +1 -1
- data/lib/abacate_pay/clients/payment_link_client.rb +1 -1
- data/lib/abacate_pay/clients/payout_client.rb +1 -1
- data/lib/abacate_pay/clients/pix_client.rb +1 -1
- data/lib/abacate_pay/clients/product_client.rb +1 -1
- data/lib/abacate_pay/clients/subscription_client.rb +36 -1
- data/lib/abacate_pay/clients/transparent_client.rb +74 -22
- data/lib/abacate_pay/clients/webhook_client.rb +1 -1
- data/lib/abacate_pay/collection.rb +98 -0
- data/lib/abacate_pay/configuration.rb +19 -3
- data/lib/abacate_pay/enums/billings/methods.rb +8 -1
- data/lib/abacate_pay/enums/webhooks/event_types.rb +7 -0
- data/lib/abacate_pay/resources/checkouts.rb +10 -2
- data/lib/abacate_pay/resources/transparents.rb +11 -4
- data/lib/abacate_pay/version.rb +1 -1
- data/lib/abacate_pay.rb +1 -0
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 604226a94737477c55324bf08cb81736cce65fdf482453bd2667572c8986b010
|
|
4
|
+
data.tar.gz: 4e292b8d7db21a444cc4348bd59bc2cf396849ff8d99db4d4463ab1b651962fe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 55eeee33fcf8a4b509c849f85d90ccb3f0fb4a2b4aad040325da4a6ed19bf2b60394190ce848ff0b03fa98ec56b0bddccf2fae677c19b1db1dfc7c744819286a
|
|
7
|
+
data.tar.gz: e8258c5ba01696d7d76fb85b4314a047052b04f13e0a90c4a826fe83f14ce967ea817cb445d3158a61be27a6ec4af99da9fc0c4dd477449c1bf1b60412f7f612
|
data/.rubocop.yml
CHANGED
|
@@ -122,4 +122,15 @@ RSpec/SpecFilePathFormat:
|
|
|
122
122
|
RSpec/DescribeClass:
|
|
123
123
|
Exclude:
|
|
124
124
|
- "spec/clients/request_contract_spec.rb"
|
|
125
|
+
- "spec/clients/pagination_spec.rb"
|
|
126
|
+
- "spec/clients/resilience_spec.rb"
|
|
127
|
+
- "spec/clients/logging_spec.rb"
|
|
128
|
+
- "spec/clients/boleto_spec.rb"
|
|
125
129
|
- "spec/packaging_spec.rb"
|
|
130
|
+
|
|
131
|
+
# `has_more?` mirrors the API's own `hasMore` field and matches the convention
|
|
132
|
+
# other payment SDKs use for cursor pagination. `more?` would read as a
|
|
133
|
+
# different question.
|
|
134
|
+
Naming/PredicatePrefix:
|
|
135
|
+
AllowedMethods:
|
|
136
|
+
- has_more?
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.1.0] - 2026-08-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **BOLETO support.** The method was rejected outright by the `Billings::Methods`
|
|
15
|
+
enum even though it is a first-class payment method in the v2 API. Adds the
|
|
16
|
+
enum value, the boleto-only `due_date`/`interest`/`fine` fields on checkouts,
|
|
17
|
+
and a `method:` argument on `TransparentClient#create` (which previously
|
|
18
|
+
hard-coded PIX). Boleto responses now expose `bar_code`, `url`, `br_code`,
|
|
19
|
+
`br_code_base64` and `expires_at`.
|
|
20
|
+
- **Cursor pagination.** List endpoints cap at 100 items and report `hasMore`
|
|
21
|
+
plus a cursor; the SDK discarded that metadata, making record 101
|
|
22
|
+
unreachable. `list` now returns an `AbacatePay::Collection` — Enumerable and
|
|
23
|
+
Array-compatible, so existing code is unaffected — carrying `has_more?` and
|
|
24
|
+
`next_cursor`. `each_page` and `auto_paging_each` walk every page.
|
|
25
|
+
- **Retries with exponential backoff and jitter** on 429 and 5xx, configurable
|
|
26
|
+
via `config.max_retries` (default 2). Only idempotent methods are retried;
|
|
27
|
+
POST never is, because repeating `checkouts/create` after a timeout could
|
|
28
|
+
charge a customer twice and the API exposes no idempotency key.
|
|
29
|
+
- **Optional request logging** via `config.logger`, with the bearer token
|
|
30
|
+
redacted and bodies never logged.
|
|
31
|
+
- `SubscriptionClient#change_plan` and `#record_usage` — the last two of the 45
|
|
32
|
+
documented v2 endpoints. All 45 are now covered.
|
|
33
|
+
- `subscription.payment_failed` and `subscription.trial_started` webhook event
|
|
34
|
+
types. `payment_failed` is the dunning signal.
|
|
35
|
+
- Checkout fields the v2 API accepts but the SDK never sent: `max_installments`
|
|
36
|
+
(nested under `card`), `up_sell_product_id` and `custom_metadata`.
|
|
37
|
+
- A `User-Agent` identifying the SDK and Ruby version.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- `Webhooks.parse` aside, malformed API responses raised `JSON::ParserError`
|
|
42
|
+
and a missing `data` field raised `KeyError`; both are now `ApiError`.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- `BillingClient`'s deprecation warning now states that its `/billings/*`
|
|
47
|
+
endpoints do not exist on either API version, so every call fails. It will be
|
|
48
|
+
removed in 2.0.0.
|
|
49
|
+
|
|
50
|
+
### Corrected
|
|
51
|
+
|
|
52
|
+
- The 1.0.0 notes stated that the v1 API "has been retired and answers Not found
|
|
53
|
+
for every path". That is wrong: v1 is still served, under a different dialect
|
|
54
|
+
— singular paths (`/v1/billing/`, `/v1/customer/`) and different resource
|
|
55
|
+
names (`pixQrCode`). The original diagnosis tested v2-shaped paths against
|
|
56
|
+
`/v1`. The fix itself stands: this SDK only ever spoke v2's dialect, so 10 of
|
|
57
|
+
the 12 paths it calls do not exist on v1 and routing there produced 404s.
|
|
58
|
+
|
|
10
59
|
## [1.0.0] - 2026-08-05
|
|
11
60
|
|
|
12
61
|
First stable release. The public surface is now covered by CI on four Ruby
|
|
@@ -121,5 +170,6 @@ versions and will not change without a major bump.
|
|
|
121
170
|
|
|
122
171
|
- Initial release
|
|
123
172
|
|
|
173
|
+
[1.1.0]: https://github.com/AbacatePay/abacatepay-ruby-sdk/releases/tag/v1.1.0
|
|
124
174
|
[1.0.0]: https://github.com/AbacatePay/abacatepay-ruby-sdk/releases/tag/v1.0.0
|
|
125
175
|
[0.1.0]: https://github.com/AbacatePay/abacatepay-ruby-sdk/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -42,8 +42,10 @@ gem 'abacatepay-ruby'
|
|
|
42
42
|
|
|
43
43
|
```ruby
|
|
44
44
|
AbacatePay.configure do |config|
|
|
45
|
-
config.api_token
|
|
46
|
-
config.timeout
|
|
45
|
+
config.api_token = ENV['ABACATEPAY_TOKEN']
|
|
46
|
+
config.timeout = 30 # opcional, segundos (default 30)
|
|
47
|
+
config.max_retries = 2 # opcional, retry em 429/5xx (default 2, 0 desliga)
|
|
48
|
+
config.logger = Rails.logger # opcional, token é redigido
|
|
47
49
|
end
|
|
48
50
|
```
|
|
49
51
|
|
|
@@ -101,9 +103,26 @@ AbacatePay.checkouts.list(status: 'PAID', email: 'user@example.com')
|
|
|
101
103
|
|
|
102
104
|
<div align="center">
|
|
103
105
|
|
|
106
|
+
Listas retornam no máximo 100 itens. O resultado é uma `Collection` — funciona como Array, e ainda carrega o cursor:
|
|
107
|
+
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
page = AbacatePay.customers.list
|
|
112
|
+
page.first.id # funciona como Array
|
|
113
|
+
page.has_more? # => true
|
|
114
|
+
page.next_cursor # => "cust_abc123"
|
|
115
|
+
|
|
116
|
+
# Para percorrer tudo sem lidar com cursor:
|
|
117
|
+
AbacatePay.customers.auto_paging_each { |customer| puts customer.id }
|
|
118
|
+
AbacatePay.customers.each_page { |page| puts page.size }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
<div align="center">
|
|
122
|
+
|
|
104
123
|
## Versionamento
|
|
105
124
|
|
|
106
|
-
O SDK fala **exclusivamente a v2** — `https://api.abacatepay.com/v2`. A v1
|
|
125
|
+
O SDK fala **exclusivamente a v2** — `https://api.abacatepay.com/v2`. A v1 ainda existe para integrações legadas, mas usa outro dialeto (caminhos no singular como `/v1/billing/`, `/v1/customer/`) que este SDK nunca implementou. Se você precisa da v1, chame a API diretamente.
|
|
107
126
|
|
|
108
127
|
O ambiente (dev mode x produção) é definido **pela chave de API**, não por configuração: chaves de Dev mode geram transações simuladas. Por isso `config.environment` não faz nada — ela continua aceita para não quebrar initializers existentes, mas emite aviso de depreciação.
|
|
109
128
|
|
|
@@ -214,7 +233,7 @@ Todos os recursos são acessíveis pela fachada `AbacatePay.<recurso>`.
|
|
|
214
233
|
| `products` | `list` `get` `create` `delete` |
|
|
215
234
|
| `coupons` | `list` `get` `create` `delete` `toggle` |
|
|
216
235
|
| `checkouts` | `list` `get` `create` `refund` |
|
|
217
|
-
| `subscriptions` | `list` `create` `cancel` |
|
|
236
|
+
| `subscriptions` | `list` `create` `cancel` `change_plan` `record_usage` |
|
|
218
237
|
| `transparents` | `list` `create` `check` `simulate_payment` `refund` |
|
|
219
238
|
| `pix` | `list` `get` `send_pix` |
|
|
220
239
|
| `payouts` | `list` `get` `create` |
|
|
@@ -363,6 +382,64 @@ AbacatePay.payouts.create(
|
|
|
363
382
|
|
|
364
383
|
<div align="center">
|
|
365
384
|
|
|
385
|
+
### Boleto
|
|
386
|
+
|
|
387
|
+
Boleto tem vencimento, juros e multa próprios. Todos os valores em centavos.
|
|
388
|
+
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
```ruby
|
|
392
|
+
AbacatePay.checkouts.create(
|
|
393
|
+
AbacatePay::Resources::Checkouts.new(
|
|
394
|
+
methods: ['BOLETO'],
|
|
395
|
+
due_date: '2026-08-15', # opcional; default 3 dias úteis
|
|
396
|
+
interest: { value: 100 }, # juros ao mês
|
|
397
|
+
fine: { value: 200, type: 'PERCENTAGE' }, # ou type: 'FIXED'
|
|
398
|
+
products: [
|
|
399
|
+
AbacatePay::Resources::Billings::Product.new(external_id: 'prod_123', quantity: 1)
|
|
400
|
+
]
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
<div align="center">
|
|
406
|
+
|
|
407
|
+
No checkout transparente, o boleto exige nome e CPF/CNPJ do pagador — o SDK valida antes de chamar a API:
|
|
408
|
+
|
|
409
|
+
</div>
|
|
410
|
+
|
|
411
|
+
```ruby
|
|
412
|
+
charge = AbacatePay::Resources::Transparents.new(amount: 25_000, due_date: '2026-08-15')
|
|
413
|
+
# charge.customer precisa ter metadata.name e metadata.tax_id
|
|
414
|
+
|
|
415
|
+
boleto = AbacatePay.transparents.create(charge, method: 'BOLETO')
|
|
416
|
+
boleto.bar_code # linha digitável
|
|
417
|
+
boleto.url # PDF para impressão
|
|
418
|
+
boleto.br_code # PIX alternativo da mesma cobrança
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
<div align="center">
|
|
422
|
+
|
|
423
|
+
### Parcelamento e order bump
|
|
424
|
+
|
|
425
|
+
</div>
|
|
426
|
+
|
|
427
|
+
```ruby
|
|
428
|
+
AbacatePay.checkouts.create(
|
|
429
|
+
AbacatePay::Resources::Checkouts.new(
|
|
430
|
+
methods: ['CARD'],
|
|
431
|
+
max_installments: 12,
|
|
432
|
+
up_sell_product_id: 'prod_bump',
|
|
433
|
+
custom_metadata: { origem: 'app-mobile' },
|
|
434
|
+
products: [
|
|
435
|
+
AbacatePay::Resources::Billings::Product.new(external_id: 'prod_123', quantity: 1)
|
|
436
|
+
]
|
|
437
|
+
)
|
|
438
|
+
)
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
<div align="center">
|
|
442
|
+
|
|
366
443
|
### Links de pagamento
|
|
367
444
|
|
|
368
445
|
Um link reutilizável, pago por vários clientes de forma independente — vendas em massa, rifas, formulários de inscrição. Para uma cobrança por cliente, use `checkouts`.
|
|
@@ -407,6 +484,12 @@ Cancela imediatamente; parcelas futuras pendentes são canceladas junto.
|
|
|
407
484
|
|
|
408
485
|
```ruby
|
|
409
486
|
AbacatePay.subscriptions.cancel('subs_abc123xyz')
|
|
487
|
+
|
|
488
|
+
# Upgrade/downgrade — vale a partir do próximo ciclo
|
|
489
|
+
AbacatePay.subscriptions.change_plan('subs_abc123xyz', product_id: 'prod_pro', quantity: 1)
|
|
490
|
+
|
|
491
|
+
# Cobrança por uso — produto sem ciclo
|
|
492
|
+
AbacatePay.subscriptions.record_usage('subs_abc123xyz', product_id: 'prod_api', units: 50)
|
|
410
493
|
```
|
|
411
494
|
|
|
412
495
|
<div align="center">
|
data/abacatepay-ruby.gemspec
CHANGED
|
@@ -41,6 +41,7 @@ Gem::Specification.new do |spec|
|
|
|
41
41
|
# NestedParamsEncoder). The lockfile only protects this repo — consumers are
|
|
42
42
|
# protected by the constraint here.
|
|
43
43
|
spec.add_dependency "faraday", "~> 2.14", ">= 2.14.3"
|
|
44
|
+
spec.add_dependency "faraday-retry", "~> 2.3"
|
|
44
45
|
|
|
45
46
|
# Development dependencies
|
|
46
47
|
spec.add_development_dependency "bundler-audit", "~> 0.9"
|
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module AbacatePay
|
|
4
4
|
module Clients
|
|
5
|
-
#
|
|
5
|
+
# Deprecated client for the v1 billing endpoints.
|
|
6
|
+
#
|
|
7
|
+
# The endpoints this class calls (`/billings/create`, `/billings/list`) do
|
|
8
|
+
# not exist on either API version — v2 replaced them with `/checkouts/*`,
|
|
9
|
+
# and v1 uses the singular `/billing/*`. Every call raises ApiError.
|
|
10
|
+
#
|
|
11
|
+
# Kept only so existing code keeps loading; it will be removed in 2.0.0.
|
|
12
|
+
#
|
|
13
|
+
# @deprecated Use {CheckoutClient} instead.
|
|
6
14
|
class BillingClient < Client
|
|
7
15
|
# API endpoint for billing-related operations
|
|
8
16
|
URI = "billings"
|
|
@@ -10,7 +18,9 @@ module AbacatePay
|
|
|
10
18
|
# @param client [Faraday::Connection, nil] Optional Faraday client for custom configurations
|
|
11
19
|
# @deprecated Use {CheckoutClient} instead
|
|
12
20
|
def initialize(client = nil)
|
|
13
|
-
warn "[DEPRECATION] BillingClient
|
|
21
|
+
warn "[DEPRECATION] BillingClient calls /billings/* endpoints that do not exist on the " \
|
|
22
|
+
"AbacatePay API — every request will fail. Use AbacatePay.checkouts instead. " \
|
|
23
|
+
"This class will be removed in 2.0.0."
|
|
14
24
|
super(URI, client)
|
|
15
25
|
end
|
|
16
26
|
|
|
@@ -19,7 +29,7 @@ module AbacatePay
|
|
|
19
29
|
# @return [Array<Resources::Billing>] Array of Billing objects
|
|
20
30
|
def list
|
|
21
31
|
response = request("GET", "list")
|
|
22
|
-
|
|
32
|
+
build_list(response, Resources::Billings)
|
|
23
33
|
end
|
|
24
34
|
|
|
25
35
|
# Creates a new billing
|
|
@@ -17,7 +17,7 @@ module AbacatePay
|
|
|
17
17
|
# @return [Array<Resources::Checkouts>]
|
|
18
18
|
def list(**params)
|
|
19
19
|
response = request("GET", "list", params: params.empty? ? nil : params)
|
|
20
|
-
|
|
20
|
+
build_list(response, Resources::Checkouts)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# @param id [String] Checkout ID
|
|
@@ -62,8 +62,29 @@ module AbacatePay
|
|
|
62
62
|
items: data.products&.map { |product| { id: product.external_id, quantity: product.quantity } },
|
|
63
63
|
externalId: data.external_id,
|
|
64
64
|
coupons: data.coupons,
|
|
65
|
-
customerId: customer_id.to_s.empty? ? nil : customer_id
|
|
66
|
-
|
|
65
|
+
customerId: customer_id.to_s.empty? ? nil : customer_id,
|
|
66
|
+
upSellProductId: data.up_sell_product_id,
|
|
67
|
+
metadata: data.custom_metadata
|
|
68
|
+
}.merge(boleto_options(data)).merge(card_options(data)).compact
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# BOLETO-only fields. The API rejects them for other methods, so they are
|
|
72
|
+
# only sent when the caller actually set them.
|
|
73
|
+
#
|
|
74
|
+
# @param data [Resources::Checkouts] The checkout to serialize
|
|
75
|
+
# @return [Hash] The boleto payload fragment
|
|
76
|
+
def boleto_options(data)
|
|
77
|
+
{ dueDate: data.due_date, interest: data.interest, fine: data.fine }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# CARD-only instalment cap, which the API nests under `card`.
|
|
81
|
+
#
|
|
82
|
+
# @param data [Resources::Checkouts] The checkout to serialize
|
|
83
|
+
# @return [Hash] The card payload fragment
|
|
84
|
+
def card_options(data)
|
|
85
|
+
return {} unless data.max_installments
|
|
86
|
+
|
|
87
|
+
{ card: { maxInstallments: data.max_installments } }
|
|
67
88
|
end
|
|
68
89
|
end
|
|
69
90
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "faraday"
|
|
4
|
+
require "faraday/retry"
|
|
4
5
|
|
|
5
6
|
module AbacatePay
|
|
6
7
|
module Clients
|
|
@@ -9,12 +10,64 @@ module AbacatePay
|
|
|
9
10
|
# This class handles API requests using Faraday and provides a way to manage
|
|
10
11
|
# authentication and communication with the AbacatePay service.
|
|
11
12
|
class Client
|
|
13
|
+
# Statuses worth retrying. 429 is rate limiting and 5xx are transient —
|
|
14
|
+
# AbacatePay's own reference tells integrators to back off on both.
|
|
15
|
+
RETRIABLE_STATUSES = [429, 500, 502, 503, 504].freeze
|
|
16
|
+
|
|
17
|
+
# Only methods that are safe to repeat. POST is excluded: retrying
|
|
18
|
+
# `checkouts/create` after a timeout could charge a customer twice, and
|
|
19
|
+
# the API exposes no idempotency key to make that safe.
|
|
20
|
+
RETRIABLE_METHODS = %i[get head options].freeze
|
|
21
|
+
|
|
22
|
+
# Passing `exceptions` replaces faraday-retry's defaults rather than
|
|
23
|
+
# adding to them, and Faraday::RetriableResponse is what the middleware
|
|
24
|
+
# raises internally for a retriable status. Omitting it silently disables
|
|
25
|
+
# status-code retries altogether.
|
|
26
|
+
RETRIABLE_EXCEPTIONS = [
|
|
27
|
+
Faraday::RetriableResponse,
|
|
28
|
+
Faraday::TimeoutError,
|
|
29
|
+
Faraday::ConnectionFailed,
|
|
30
|
+
Errno::ETIMEDOUT
|
|
31
|
+
].freeze
|
|
32
|
+
|
|
12
33
|
# @param uri [String] The specific API endpoint to interact with
|
|
13
34
|
# @param client [Faraday::Connection, nil] Optional Faraday client for custom configurations
|
|
14
35
|
def initialize(uri, client = nil)
|
|
15
36
|
@client = client || build_client(uri)
|
|
16
37
|
end
|
|
17
38
|
|
|
39
|
+
# Yields every page of a list endpoint, following the cursor.
|
|
40
|
+
#
|
|
41
|
+
# @param params [Hash] Params forwarded to each `list` call
|
|
42
|
+
# @yield [AbacatePay::Collection] Each page in order
|
|
43
|
+
# @return [void]
|
|
44
|
+
def each_page(**params)
|
|
45
|
+
return to_enum(:each_page, **params) unless block_given?
|
|
46
|
+
|
|
47
|
+
cursor = params.delete(:after)
|
|
48
|
+
loop do
|
|
49
|
+
page = list(**params, **(cursor ? { after: cursor } : {}))
|
|
50
|
+
yield page
|
|
51
|
+
break unless page.respond_to?(:has_more?) && page.has_more? && page.next_cursor
|
|
52
|
+
|
|
53
|
+
cursor = page.next_cursor
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Yields every record across every page.
|
|
58
|
+
#
|
|
59
|
+
# Prefer this over `list` when the result set can exceed the 100-item
|
|
60
|
+
# page limit.
|
|
61
|
+
#
|
|
62
|
+
# @param params [Hash] Params forwarded to each `list` call
|
|
63
|
+
# @yield [Object] Each resource
|
|
64
|
+
# @return [void]
|
|
65
|
+
def auto_paging_each(**params, &)
|
|
66
|
+
return to_enum(:auto_paging_each, **params) unless block_given?
|
|
67
|
+
|
|
68
|
+
each_page(**params) { |page| page.each(&) }
|
|
69
|
+
end
|
|
70
|
+
|
|
18
71
|
private
|
|
19
72
|
|
|
20
73
|
# Sends an HTTP request to the API
|
|
@@ -22,7 +75,8 @@ module AbacatePay
|
|
|
22
75
|
# @param method [String] The HTTP method (e.g., GET, POST)
|
|
23
76
|
# @param uri [String] The endpoint URI relative to the base URI
|
|
24
77
|
# @param options [Hash] Optional settings and parameters for the request
|
|
25
|
-
# @return [Hash] The response data
|
|
78
|
+
# @return [Hash, AbacatePay::Collection] The response data — a Collection
|
|
79
|
+
# when the API reports pagination, the raw data otherwise
|
|
26
80
|
# @raise [ApiError] If an error occurs during the request
|
|
27
81
|
def request(method, uri, options = {})
|
|
28
82
|
response = @client.public_send(method.downcase) do |req|
|
|
@@ -34,11 +88,27 @@ module AbacatePay
|
|
|
34
88
|
parsed = JSON.parse(response.body)
|
|
35
89
|
raise ApiError, "API error: #{parsed["error"]}" if parsed["error"]
|
|
36
90
|
|
|
37
|
-
parsed.fetch("data")
|
|
91
|
+
data = parsed.fetch("data")
|
|
92
|
+
# Preserve the cursor when the API sends one; dropping it made paging
|
|
93
|
+
# past the first 100 records impossible.
|
|
94
|
+
parsed["pagination"] ? Collection.new(data, parsed["pagination"]) : data
|
|
38
95
|
rescue Faraday::Error => e
|
|
39
96
|
handle_request_error(e)
|
|
40
|
-
rescue
|
|
41
|
-
raise ApiError, "
|
|
97
|
+
rescue JSON::ParserError => e
|
|
98
|
+
raise ApiError, "Malformed API response: #{e.message}"
|
|
99
|
+
rescue KeyError
|
|
100
|
+
raise ApiError, "API response is missing the 'data' field"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Maps a list response into resources without losing the page cursor.
|
|
104
|
+
#
|
|
105
|
+
# @param response [Array, AbacatePay::Collection] The raw list response
|
|
106
|
+
# @param resource_class [Class] The resource to instantiate per item
|
|
107
|
+
# @return [Array, AbacatePay::Collection] Mapped items, still paginated
|
|
108
|
+
# when the API reported pagination
|
|
109
|
+
def build_list(response, resource_class)
|
|
110
|
+
items = Array(response).map { |data| resource_class.new(data) }
|
|
111
|
+
response.is_a?(Collection) ? response.with_items(items) : items
|
|
42
112
|
end
|
|
43
113
|
|
|
44
114
|
# Builds a new Faraday client with default configuration
|
|
@@ -51,17 +121,66 @@ module AbacatePay
|
|
|
51
121
|
|
|
52
122
|
Faraday.new(
|
|
53
123
|
url: base_url,
|
|
54
|
-
headers:
|
|
55
|
-
"Content-Type" => "application/json",
|
|
56
|
-
"Authorization" => "Bearer #{configuration.api_token}"
|
|
57
|
-
},
|
|
124
|
+
headers: build_headers(configuration),
|
|
58
125
|
# Without an explicit timeout a hung gateway blocks the caller's
|
|
59
126
|
# thread indefinitely — inside a Rails request, that is an outage.
|
|
60
127
|
request: {
|
|
61
128
|
timeout: configuration.timeout,
|
|
62
129
|
open_timeout: configuration.timeout
|
|
63
130
|
}
|
|
64
|
-
)
|
|
131
|
+
) do |builder|
|
|
132
|
+
configure_retries(builder, configuration)
|
|
133
|
+
configure_logging(builder, configuration)
|
|
134
|
+
builder.adapter Faraday.default_adapter
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @param configuration [AbacatePay::Configuration] The active configuration
|
|
139
|
+
# @return [Hash] Request headers
|
|
140
|
+
def build_headers(configuration)
|
|
141
|
+
{
|
|
142
|
+
"Content-Type" => "application/json",
|
|
143
|
+
"Authorization" => "Bearer #{configuration.api_token}",
|
|
144
|
+
"User-Agent" => "abacatepay-ruby/#{AbacatePay::VERSION} ruby/#{RUBY_VERSION}"
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# @param builder [Faraday::Connection] The connection being built
|
|
149
|
+
# @param configuration [AbacatePay::Configuration] The active configuration
|
|
150
|
+
# @return [void]
|
|
151
|
+
def configure_retries(builder, configuration)
|
|
152
|
+
return if configuration.max_retries.to_i <= 0
|
|
153
|
+
|
|
154
|
+
# `retry_if` is deliberately left at its default (never retry outside
|
|
155
|
+
# `methods`). Overriding it would re-enable retries for POST, which is
|
|
156
|
+
# exactly what must not happen for charge creation.
|
|
157
|
+
builder.request :retry,
|
|
158
|
+
max: configuration.max_retries,
|
|
159
|
+
interval: 0.5,
|
|
160
|
+
backoff_factor: 2,
|
|
161
|
+
max_interval: 8,
|
|
162
|
+
# Jitter: without it, every client that hit the same
|
|
163
|
+
# rate limit retries in lockstep and hits it again.
|
|
164
|
+
interval_randomness: 0.5,
|
|
165
|
+
retry_statuses: RETRIABLE_STATUSES,
|
|
166
|
+
methods: RETRIABLE_METHODS,
|
|
167
|
+
exceptions: RETRIABLE_EXCEPTIONS
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# @param builder [Faraday::Connection] The connection being built
|
|
171
|
+
# @param configuration [AbacatePay::Configuration] The active configuration
|
|
172
|
+
# @return [void]
|
|
173
|
+
def configure_logging(builder, configuration)
|
|
174
|
+
return unless configuration.logger
|
|
175
|
+
|
|
176
|
+
builder.response :logger, configuration.logger, headers: true, bodies: false do |logger|
|
|
177
|
+
# Faraday renders header values inspected, so the token appears as
|
|
178
|
+
# Authorization: "Bearer abc_live_..."
|
|
179
|
+
# Both spellings are filtered so a change in that formatting cannot
|
|
180
|
+
# silently start leaking the credential.
|
|
181
|
+
logger.filter(/(Authorization:\s*")Bearer\s+[^"]*(")/i, '\1Bearer [REDACTED]\2')
|
|
182
|
+
logger.filter(/(Authorization:\s*)Bearer\s+\S+/i, '\1Bearer [REDACTED]')
|
|
183
|
+
end
|
|
65
184
|
end
|
|
66
185
|
|
|
67
186
|
# Handles API request errors
|
|
@@ -75,6 +194,8 @@ module AbacatePay
|
|
|
75
194
|
end
|
|
76
195
|
|
|
77
196
|
raise ApiError, "Request error: #{error_message || error.message}"
|
|
197
|
+
rescue JSON::ParserError
|
|
198
|
+
raise ApiError, "Request error: #{error.message}"
|
|
78
199
|
end
|
|
79
200
|
end
|
|
80
201
|
end
|
|
@@ -17,7 +17,7 @@ module AbacatePay
|
|
|
17
17
|
# @return [Array<Resources::Coupons>]
|
|
18
18
|
def list(**params)
|
|
19
19
|
response = request("GET", "list", params: params.empty? ? nil : params)
|
|
20
|
-
|
|
20
|
+
build_list(response, Resources::Coupons)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# @param id [String] Coupon ID
|
|
@@ -18,7 +18,7 @@ module AbacatePay
|
|
|
18
18
|
# @return [Array<Resources::Customers>] Array of Customer objects
|
|
19
19
|
def list(**params)
|
|
20
20
|
response = request("GET", "list", params: params.empty? ? nil : params)
|
|
21
|
-
|
|
21
|
+
build_list(response, Resources::Customers)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# Retrieves a customer by ID
|
|
@@ -23,7 +23,7 @@ module AbacatePay
|
|
|
23
23
|
# @return [Array<Resources::Checkouts>]
|
|
24
24
|
def list(**params)
|
|
25
25
|
response = request("GET", "list", params: params.empty? ? nil : params)
|
|
26
|
-
|
|
26
|
+
build_list(response, Resources::Checkouts)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
# @param id [String] The payment link ID
|
|
@@ -16,7 +16,7 @@ module AbacatePay
|
|
|
16
16
|
# @return [Array<Resources::Payouts>]
|
|
17
17
|
def list(**params)
|
|
18
18
|
response = request("GET", "list", params: params.empty? ? nil : params)
|
|
19
|
-
|
|
19
|
+
build_list(response, Resources::Payouts)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# @param id [String] Payout ID
|
|
@@ -17,7 +17,7 @@ module AbacatePay
|
|
|
17
17
|
# @return [Array<Resources::PixTransfers>]
|
|
18
18
|
def list(**params)
|
|
19
19
|
response = request("GET", "list", params: params.empty? ? nil : params)
|
|
20
|
-
|
|
20
|
+
build_list(response, Resources::PixTransfers)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# @param id [String] PIX transfer ID
|
|
@@ -16,7 +16,7 @@ module AbacatePay
|
|
|
16
16
|
# @return [Array<Resources::Products>]
|
|
17
17
|
def list(**params)
|
|
18
18
|
response = request("GET", "list", params: params.empty? ? nil : params)
|
|
19
|
-
|
|
19
|
+
build_list(response, Resources::Products)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# @param id [String] Product ID or externalId
|
|
@@ -16,7 +16,7 @@ module AbacatePay
|
|
|
16
16
|
# @return [Array<Resources::Subscriptions>]
|
|
17
17
|
def list(**params)
|
|
18
18
|
response = request("GET", "list", params: params.empty? ? nil : params)
|
|
19
|
-
|
|
19
|
+
build_list(response, Resources::Subscriptions)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# @param data [Resources::Subscriptions]
|
|
@@ -50,6 +50,41 @@ module AbacatePay
|
|
|
50
50
|
response = request("POST", "cancel", json: { id: id })
|
|
51
51
|
Resources::Subscriptions.new(response)
|
|
52
52
|
end
|
|
53
|
+
|
|
54
|
+
# Changes the main product of an active subscription. The new price takes
|
|
55
|
+
# effect on the next billing cycle — the current cycle is untouched.
|
|
56
|
+
#
|
|
57
|
+
# @param id [String] The subscription ID (`subs_...`)
|
|
58
|
+
# @param product_id [String] The new product ID (`prod_...`), which must have a cycle
|
|
59
|
+
# @param quantity [Integer] Quantity of the product, minimum 1
|
|
60
|
+
# @return [Hash] The pending update object (`status: "PENDING"`)
|
|
61
|
+
# @raise [ArgumentError] if quantity is below 1
|
|
62
|
+
def change_plan(id, product_id:, quantity: 1)
|
|
63
|
+
raise ArgumentError, "quantity must be at least 1, got #{quantity.inspect}" if quantity.to_i < 1
|
|
64
|
+
|
|
65
|
+
request("POST", "change-plan", json: { id: id, productId: product_id, quantity: quantity.to_i })
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Records usage of a pay-as-you-go product on an active subscription. The
|
|
69
|
+
# amount is added to the next pending instalment of the cycle.
|
|
70
|
+
#
|
|
71
|
+
# @param id [String] The subscription ID (`subs_...`)
|
|
72
|
+
# @param product_id [String] The usage product ID (`prod_...`), which must NOT have a cycle
|
|
73
|
+
# @param units [Integer] Number of units, minimum 1
|
|
74
|
+
# @param action [String] "add" to add units, "subtract" to reverse units
|
|
75
|
+
# already recorded in the same cycle
|
|
76
|
+
# @return [Hash] The recorded usage
|
|
77
|
+
# @raise [ArgumentError] if units is below 1 or action is not add/subtract
|
|
78
|
+
def record_usage(id, product_id:, units:, action: "add")
|
|
79
|
+
raise ArgumentError, "units must be at least 1, got #{units.inspect}" if units.to_i < 1
|
|
80
|
+
|
|
81
|
+
unless %w[add subtract].include?(action.to_s)
|
|
82
|
+
raise ArgumentError, "action must be \"add\" or \"subtract\", got #{action.inspect}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
request("POST", "record-usage",
|
|
86
|
+
json: { id: id, productId: product_id, units: units.to_i, action: action.to_s })
|
|
87
|
+
end
|
|
53
88
|
end
|
|
54
89
|
end
|
|
55
90
|
end
|
|
@@ -17,31 +17,21 @@ module AbacatePay
|
|
|
17
17
|
# @return [Array<Resources::Transparents>]
|
|
18
18
|
def list(**params)
|
|
19
19
|
response = request("GET", "list", params: params.empty? ? nil : params)
|
|
20
|
-
|
|
20
|
+
build_list(response, Resources::Transparents)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
#
|
|
23
|
+
# Creates a transparent charge.
|
|
24
|
+
#
|
|
25
|
+
# @param data [Resources::Transparents] The charge to create
|
|
26
|
+
# @param method [String] "PIX" or "BOLETO"
|
|
24
27
|
# @return [Resources::Transparents]
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
description: data.description
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if data.customer
|
|
36
|
-
request_data[:customer] = {
|
|
37
|
-
name: data.customer.metadata&.name,
|
|
38
|
-
email: data.customer.metadata&.email,
|
|
39
|
-
cellphone: data.customer.metadata&.cellphone,
|
|
40
|
-
taxId: data.customer.metadata&.tax_id
|
|
41
|
-
}
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
response = request("POST", "create", json: request_data)
|
|
28
|
+
# @raise [ArgumentError] if the method is not supported, or if a BOLETO
|
|
29
|
+
# charge is missing the payer name/taxId the API requires
|
|
30
|
+
def create(data, method: Enums::Billings::Methods::PIX)
|
|
31
|
+
validate_transparent_method!(method)
|
|
32
|
+
validate_boleto_payer!(data) if method == Enums::Billings::Methods::BOLETO
|
|
33
|
+
|
|
34
|
+
response = request("POST", "create", json: build_create_payload(data, method))
|
|
45
35
|
Resources::Transparents.new(response)
|
|
46
36
|
end
|
|
47
37
|
|
|
@@ -68,6 +58,68 @@ module AbacatePay
|
|
|
68
58
|
response = request("POST", "refund", json: { id: id })
|
|
69
59
|
Resources::Transparents.new(response)
|
|
70
60
|
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# Only PIX and BOLETO are transparent-checkout methods; CARD goes through
|
|
65
|
+
# the hosted checkout.
|
|
66
|
+
#
|
|
67
|
+
# @param method [String] The requested method
|
|
68
|
+
# @return [void]
|
|
69
|
+
def validate_transparent_method!(method)
|
|
70
|
+
supported = [Enums::Billings::Methods::PIX, Enums::Billings::Methods::BOLETO]
|
|
71
|
+
return if supported.include?(method)
|
|
72
|
+
|
|
73
|
+
raise ArgumentError, "Transparent checkout supports #{supported.join(" and ")}, got #{method.inspect}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The API requires the payer's name and taxId for boleto. Failing here
|
|
77
|
+
# names the missing field instead of returning a generic 422.
|
|
78
|
+
#
|
|
79
|
+
# @param data [Resources::Transparents] The charge to validate
|
|
80
|
+
# @return [void]
|
|
81
|
+
def validate_boleto_payer!(data)
|
|
82
|
+
metadata = data.customer&.metadata
|
|
83
|
+
missing = []
|
|
84
|
+
missing << "customer.metadata.name" if metadata&.name.to_s.strip.empty?
|
|
85
|
+
missing << "customer.metadata.tax_id" if metadata&.tax_id.to_s.strip.empty?
|
|
86
|
+
return if missing.empty?
|
|
87
|
+
|
|
88
|
+
raise ArgumentError, "BOLETO requires #{missing.join(" and ")}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @param data [Resources::Transparents] The charge to serialize
|
|
92
|
+
# @param method [String] "PIX" or "BOLETO"
|
|
93
|
+
# @return [Hash] The request payload
|
|
94
|
+
def build_create_payload(data, method)
|
|
95
|
+
payload = {
|
|
96
|
+
method: method,
|
|
97
|
+
data: {
|
|
98
|
+
amount: data.amount,
|
|
99
|
+
dueDate: data.due_date
|
|
100
|
+
}.compact,
|
|
101
|
+
expiresIn: data.expires_in,
|
|
102
|
+
description: data.description
|
|
103
|
+
}.compact
|
|
104
|
+
|
|
105
|
+
customer = serialize_customer(data.customer)
|
|
106
|
+
payload[:data][:customer] = customer if customer
|
|
107
|
+
|
|
108
|
+
payload
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @param customer [Resources::Customers, nil] The payer
|
|
112
|
+
# @return [Hash, nil] The customer payload, or nil when absent
|
|
113
|
+
def serialize_customer(customer)
|
|
114
|
+
return nil unless customer
|
|
115
|
+
|
|
116
|
+
{
|
|
117
|
+
name: customer.metadata&.name,
|
|
118
|
+
email: customer.metadata&.email,
|
|
119
|
+
cellphone: customer.metadata&.cellphone,
|
|
120
|
+
taxId: customer.metadata&.tax_id
|
|
121
|
+
}.compact
|
|
122
|
+
end
|
|
71
123
|
end
|
|
72
124
|
end
|
|
73
125
|
end
|
|
@@ -20,7 +20,7 @@ module AbacatePay
|
|
|
20
20
|
# @return [Array<Resources::WebhookEndpoints>]
|
|
21
21
|
def list(**params)
|
|
22
22
|
response = request("GET", "list", params: params.empty? ? nil : params)
|
|
23
|
-
|
|
23
|
+
build_list(response, Resources::WebhookEndpoints)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
# @param id [String] The webhook ID
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AbacatePay
|
|
4
|
+
# A page of results plus the cursor needed to fetch the next one.
|
|
5
|
+
#
|
|
6
|
+
# List endpoints return at most 100 items and report whether more exist. The
|
|
7
|
+
# SDK used to return a bare Array and drop that metadata, which made it
|
|
8
|
+
# impossible to page past the first 100 records.
|
|
9
|
+
#
|
|
10
|
+
# Behaves like an Array everywhere an Array was returned before, so existing
|
|
11
|
+
# code keeps working:
|
|
12
|
+
#
|
|
13
|
+
# customers = AbacatePay.customers.list
|
|
14
|
+
# customers.each { |c| puts c.id } # Enumerable
|
|
15
|
+
# customers.size # items on this page
|
|
16
|
+
# customers.has_more? # is there another page?
|
|
17
|
+
#
|
|
18
|
+
# To walk every page without handling cursors:
|
|
19
|
+
#
|
|
20
|
+
# AbacatePay.customers.each_page { |page| page.each { |c| puts c.id } }
|
|
21
|
+
# AbacatePay.customers.auto_paging_each { |customer| puts customer.id }
|
|
22
|
+
class Collection
|
|
23
|
+
include Enumerable
|
|
24
|
+
|
|
25
|
+
# @return [Array] The items on this page
|
|
26
|
+
attr_reader :items
|
|
27
|
+
|
|
28
|
+
# @return [String, nil] Cursor for the next page, passed back as `after`
|
|
29
|
+
attr_reader :next_cursor
|
|
30
|
+
|
|
31
|
+
# @return [String, nil] Cursor for the previous page, passed back as `before`
|
|
32
|
+
attr_reader :before_cursor
|
|
33
|
+
|
|
34
|
+
# @param items [Array] The items on this page
|
|
35
|
+
# @param pagination [Hash, nil] The raw `pagination` object from the API
|
|
36
|
+
def initialize(items, pagination = nil)
|
|
37
|
+
@items = Array(items)
|
|
38
|
+
pagination = {} unless pagination.is_a?(Hash)
|
|
39
|
+
@has_more = pagination["hasMore"] || false
|
|
40
|
+
@next_cursor = pagination["next"]
|
|
41
|
+
@before_cursor = pagination["before"]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @yield [Object] Each item on this page
|
|
45
|
+
# @return [Enumerator, self]
|
|
46
|
+
def each(&)
|
|
47
|
+
return to_enum(:each) unless block_given?
|
|
48
|
+
|
|
49
|
+
items.each(&)
|
|
50
|
+
self
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Whether the API reported further pages after this one.
|
|
54
|
+
#
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
def has_more?
|
|
57
|
+
@has_more
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @return [Integer] Number of items on this page
|
|
61
|
+
def size
|
|
62
|
+
items.size
|
|
63
|
+
end
|
|
64
|
+
alias length size
|
|
65
|
+
alias count size
|
|
66
|
+
|
|
67
|
+
# @return [Boolean]
|
|
68
|
+
def empty?
|
|
69
|
+
items.empty?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @param index [Integer, Range] Index into this page
|
|
73
|
+
# @return [Object, Array, nil]
|
|
74
|
+
def [](index)
|
|
75
|
+
items[index]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @return [Array] A plain Array copy of this page
|
|
79
|
+
def to_a
|
|
80
|
+
items.dup
|
|
81
|
+
end
|
|
82
|
+
alias to_ary to_a
|
|
83
|
+
|
|
84
|
+
# Returns a new Collection with different items and the same cursor.
|
|
85
|
+
# Used to turn raw hashes into resources without losing pagination.
|
|
86
|
+
#
|
|
87
|
+
# @param new_items [Array] The mapped items
|
|
88
|
+
# @return [Collection]
|
|
89
|
+
def with_items(new_items)
|
|
90
|
+
self.class.new(new_items, "hasMore" => has_more?, "next" => next_cursor, "before" => before_cursor)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @return [String]
|
|
94
|
+
def inspect
|
|
95
|
+
"#<#{self.class.name} size=#{size} has_more=#{has_more?} next=#{next_cursor.inspect}>"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -8,9 +8,11 @@ module AbacatePay
|
|
|
8
8
|
#
|
|
9
9
|
# @api public
|
|
10
10
|
class Configuration
|
|
11
|
-
# The only base URL
|
|
12
|
-
#
|
|
13
|
-
#
|
|
11
|
+
# The only base URL this SDK speaks. v1 still exists, but under a
|
|
12
|
+
# different dialect — singular paths (`/v1/billing/`, `/v1/customer/`) and
|
|
13
|
+
# different resource names (`pixQrCode`) — which this SDK has never
|
|
14
|
+
# implemented. Deriving a base URL from the token prefix only produced 404s
|
|
15
|
+
# against v1 while sending v2-shaped paths.
|
|
14
16
|
API_BASE_URL = "https://api.abacatepay.com/v2"
|
|
15
17
|
|
|
16
18
|
# @return [String] API token for authentication
|
|
@@ -19,11 +21,25 @@ module AbacatePay
|
|
|
19
21
|
# @return [Integer] Request timeout in seconds
|
|
20
22
|
attr_accessor :timeout
|
|
21
23
|
|
|
24
|
+
# Retries apply to idempotent requests only (GET/HEAD/OPTIONS) on 429 and
|
|
25
|
+
# 5xx, with exponential backoff and jitter. Set to 0 to disable.
|
|
26
|
+
#
|
|
27
|
+
# @return [Integer] Maximum retry attempts
|
|
28
|
+
attr_accessor :max_retries
|
|
29
|
+
|
|
30
|
+
# Optional logger. Request and response headers are logged with the bearer
|
|
31
|
+
# token redacted; bodies are never logged, since they carry customer PII.
|
|
32
|
+
#
|
|
33
|
+
# @return [Logger, nil]
|
|
34
|
+
attr_accessor :logger
|
|
35
|
+
|
|
22
36
|
# Initialize a new configuration with default values
|
|
23
37
|
#
|
|
24
38
|
# @api public
|
|
25
39
|
def initialize
|
|
26
40
|
@timeout = 30
|
|
41
|
+
@max_retries = 2
|
|
42
|
+
@logger = nil
|
|
27
43
|
@api_token = nil
|
|
28
44
|
end
|
|
29
45
|
|
|
@@ -10,12 +10,19 @@ module AbacatePay
|
|
|
10
10
|
# PIX payment method.
|
|
11
11
|
# @return [String] Represents the PIX payment method, a popular instant payment system in Brazil
|
|
12
12
|
PIX = "PIX"
|
|
13
|
+
|
|
14
|
+
# @return [String] Credit card payment
|
|
13
15
|
CARD = "CARD"
|
|
14
16
|
|
|
17
|
+
# Boleto bancário. Supports a due date and late-payment interest/fine —
|
|
18
|
+
# see Resources::Checkouts#due_date, #interest and #fine.
|
|
19
|
+
# @return [String] Boleto payment method
|
|
20
|
+
BOLETO = "BOLETO"
|
|
21
|
+
|
|
15
22
|
# Gets all valid method values
|
|
16
23
|
# @return [Array<String>] List of all valid payment methods
|
|
17
24
|
def self.values
|
|
18
|
-
[PIX, CARD]
|
|
25
|
+
[PIX, CARD, BOLETO]
|
|
19
26
|
end
|
|
20
27
|
|
|
21
28
|
# Validates if a given value is a valid method
|
|
@@ -13,6 +13,12 @@ module AbacatePay
|
|
|
13
13
|
SUBSCRIPTION_COMPLETED = "subscription.completed"
|
|
14
14
|
SUBSCRIPTION_RENEWED = "subscription.renewed"
|
|
15
15
|
SUBSCRIPTION_CANCELLED = "subscription.cancelled"
|
|
16
|
+
|
|
17
|
+
# Recurring charge failed — the dunning signal. Without handling this,
|
|
18
|
+
# a failing subscription looks identical to a healthy one.
|
|
19
|
+
SUBSCRIPTION_PAYMENT_FAILED = "subscription.payment_failed"
|
|
20
|
+
|
|
21
|
+
SUBSCRIPTION_TRIAL_STARTED = "subscription.trial_started"
|
|
16
22
|
TRANSFER_COMPLETED = "transfer.completed"
|
|
17
23
|
TRANSFER_FAILED = "transfer.failed"
|
|
18
24
|
PAYOUT_COMPLETED = "payout.completed"
|
|
@@ -23,6 +29,7 @@ module AbacatePay
|
|
|
23
29
|
CHECKOUT_COMPLETED, CHECKOUT_REFUNDED, CHECKOUT_DISPUTED,
|
|
24
30
|
TRANSPARENT_COMPLETED, TRANSPARENT_REFUNDED, TRANSPARENT_DISPUTED,
|
|
25
31
|
SUBSCRIPTION_COMPLETED, SUBSCRIPTION_RENEWED, SUBSCRIPTION_CANCELLED,
|
|
32
|
+
SUBSCRIPTION_PAYMENT_FAILED, SUBSCRIPTION_TRIAL_STARTED,
|
|
26
33
|
TRANSFER_COMPLETED, TRANSFER_FAILED,
|
|
27
34
|
PAYOUT_COMPLETED, PAYOUT_FAILED
|
|
28
35
|
]
|
|
@@ -20,7 +20,13 @@ module AbacatePay
|
|
|
20
20
|
|
|
21
21
|
attr_reader :id, :url, :amount, :status, :dev_mode, :methods,
|
|
22
22
|
:products, :metadata, :customer, :coupons, :external_id,
|
|
23
|
-
:frequency, :created_at, :updated_at
|
|
23
|
+
:frequency, :created_at, :updated_at,
|
|
24
|
+
# BOLETO-only: due date (YYYY-MM-DD) plus late-payment charges.
|
|
25
|
+
:due_date, :interest, :fine,
|
|
26
|
+
# CARD-only: maximum number of instalments offered.
|
|
27
|
+
:max_installments,
|
|
28
|
+
# Order bump shown at checkout, and free-form merchant data.
|
|
29
|
+
:up_sell_product_id, :custom_metadata
|
|
24
30
|
|
|
25
31
|
def initialize(data)
|
|
26
32
|
fill(data)
|
|
@@ -70,7 +76,9 @@ module AbacatePay
|
|
|
70
76
|
|
|
71
77
|
attr_writer :id, :url, :amount, :status, :dev_mode, :methods,
|
|
72
78
|
:products, :metadata, :customer, :coupons, :external_id,
|
|
73
|
-
:frequency, :created_at, :updated_at
|
|
79
|
+
:frequency, :created_at, :updated_at,
|
|
80
|
+
:due_date, :interest, :fine, :max_installments,
|
|
81
|
+
:up_sell_product_id, :custom_metadata
|
|
74
82
|
end
|
|
75
83
|
end
|
|
76
84
|
end
|
|
@@ -2,17 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
module AbacatePay
|
|
4
4
|
module Resources
|
|
5
|
-
# Represents a transparent PIX
|
|
5
|
+
# Represents a transparent checkout (PIX or boleto) in the AbacatePay system.
|
|
6
6
|
class Transparents < Resource
|
|
7
7
|
RESOURCE_PROPERTIES = {
|
|
8
8
|
customer: "AbacatePay::Resources::Customers"
|
|
9
9
|
}.freeze
|
|
10
10
|
|
|
11
|
-
DATETIME_PROPERTIES = %w[created_at updated_at].freeze
|
|
11
|
+
DATETIME_PROPERTIES = %w[created_at updated_at expires_at].freeze
|
|
12
12
|
|
|
13
13
|
attr_reader :id, :amount, :status, :method, :description,
|
|
14
14
|
:expires_in, :qr_code, :qr_code_image, :customer,
|
|
15
|
-
:metadata, :dev_mode, :created_at, :updated_at
|
|
15
|
+
:metadata, :dev_mode, :created_at, :updated_at,
|
|
16
|
+
# Boleto: due date sent on create, plus the payment slip the
|
|
17
|
+
# API returns — digitable line, viewing URL, and the PIX
|
|
18
|
+
# fallback issued for the same charge.
|
|
19
|
+
:due_date, :bar_code, :url, :br_code, :br_code_base64,
|
|
20
|
+
:expires_at
|
|
16
21
|
|
|
17
22
|
def initialize(data)
|
|
18
23
|
fill(data)
|
|
@@ -40,7 +45,9 @@ module AbacatePay
|
|
|
40
45
|
|
|
41
46
|
attr_writer :id, :amount, :status, :method, :description,
|
|
42
47
|
:expires_in, :qr_code, :qr_code_image, :customer,
|
|
43
|
-
:metadata, :dev_mode, :created_at, :updated_at
|
|
48
|
+
:metadata, :dev_mode, :created_at, :updated_at,
|
|
49
|
+
:due_date, :bar_code, :url, :br_code, :br_code_base64,
|
|
50
|
+
:expires_at
|
|
44
51
|
end
|
|
45
52
|
end
|
|
46
53
|
end
|
data/lib/abacate_pay/version.rb
CHANGED
data/lib/abacate_pay.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: abacatepay-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matheus Cardoso
|
|
@@ -29,6 +29,20 @@ dependencies:
|
|
|
29
29
|
- - ">="
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: 2.14.3
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: faraday-retry
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '2.3'
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '2.3'
|
|
32
46
|
- !ruby/object:Gem::Dependency
|
|
33
47
|
name: bundler-audit
|
|
34
48
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -130,6 +144,7 @@ files:
|
|
|
130
144
|
- lib/abacate_pay/clients/subscription_client.rb
|
|
131
145
|
- lib/abacate_pay/clients/transparent_client.rb
|
|
132
146
|
- lib/abacate_pay/clients/webhook_client.rb
|
|
147
|
+
- lib/abacate_pay/collection.rb
|
|
133
148
|
- lib/abacate_pay/configuration.rb
|
|
134
149
|
- lib/abacate_pay/enums.rb
|
|
135
150
|
- lib/abacate_pay/enums/billings/frequencies.rb
|