abacatepay-ruby 1.0.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +18 -0
  3. data/CHANGELOG.md +132 -12
  4. data/README.md +122 -20
  5. data/abacatepay-ruby.gemspec +2 -1
  6. data/lib/abacate_pay/clients/billing_client.rb +13 -3
  7. data/lib/abacate_pay/clients/checkout_client.rb +25 -4
  8. data/lib/abacate_pay/clients/client.rb +171 -16
  9. data/lib/abacate_pay/clients/coupon_client.rb +1 -1
  10. data/lib/abacate_pay/clients/customer_client.rb +1 -1
  11. data/lib/abacate_pay/clients/payment_link_client.rb +3 -3
  12. data/lib/abacate_pay/clients/payout_client.rb +1 -1
  13. data/lib/abacate_pay/clients/pix_client.rb +1 -1
  14. data/lib/abacate_pay/clients/product_client.rb +1 -1
  15. data/lib/abacate_pay/clients/store_client.rb +3 -1
  16. data/lib/abacate_pay/clients/subscription_client.rb +36 -1
  17. data/lib/abacate_pay/clients/transparent_client.rb +78 -24
  18. data/lib/abacate_pay/clients/webhook_client.rb +2 -2
  19. data/lib/abacate_pay/clients.rb +1 -1
  20. data/lib/abacate_pay/collection.rb +98 -0
  21. data/lib/abacate_pay/configuration.rb +20 -4
  22. data/lib/abacate_pay/enums/billings/methods.rb +8 -1
  23. data/lib/abacate_pay/enums/webhooks/event_types.rb +7 -0
  24. data/lib/abacate_pay/resources/checkouts.rb +10 -2
  25. data/lib/abacate_pay/resources/customers.rb +62 -21
  26. data/lib/abacate_pay/resources/resource.rb +1 -1
  27. data/lib/abacate_pay/resources/transparents.rb +29 -5
  28. data/lib/abacate_pay/resources/webhook_endpoints.rb +1 -1
  29. data/lib/abacate_pay/resources.rb +1 -1
  30. data/lib/abacate_pay/version.rb +1 -1
  31. data/lib/abacate_pay/webhooks.rb +53 -7
  32. data/lib/abacate_pay.rb +1 -0
  33. data/lib/abacatepay-ruby.rb +14 -0
  34. metadata +19 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8249cc166bb8f5534c7299d4da8aac7c08193e8a33df8e7a7618737adb44a08c
4
- data.tar.gz: f939f4979946f82d9e5a189d634ca4a7c99aa11e555646aec213b2f34dd4b817
3
+ metadata.gz: 65f1214bed1683aa366b68ef9055d85a318ca11901f90c36669fb3202c0b9be5
4
+ data.tar.gz: 0f1374d3702f53204228345237a8072a23918151cb3e2b9d13bd374b558cb04a
5
5
  SHA512:
6
- metadata.gz: 4a14de54241b2901c7873becb95bab7bcceee8e7208b60b02114138eba6c72b1b6c16d5167c9f26f1b96fa9343bbe40097153f8b801b927b5c5dff02b3c0585c
7
- data.tar.gz: e70fc7b7efea2223fbe1093f6f792e394f697da22fe92f01c738bec6a016df8116d09c5d4eec56accd4dbd5cd63a4628a8b6bb721742b76695f1436edc1139e7
6
+ metadata.gz: de8e7e809c16a249b698f02ed6d7f5eb0d1df24e6e2cab086f32a49fe90c818665a31c1cd642fae49f9a57174482470d64ec681ee99a55cb35e400d2d9fa6188
7
+ data.tar.gz: 22fd62f1e9e223d5d2e956c466b1d9de1d6128930d3b8651f4c72369e5827596e3a46c4c30db5171673eee95dd1bce49ef16a1739a351c64f1c73babeb37ff8a
data/.rubocop.yml CHANGED
@@ -58,6 +58,7 @@ Naming/MethodParameterName:
58
58
  Naming/PredicateMethod:
59
59
  AllowedMethods:
60
60
  - verify!
61
+ - verify_secret!
61
62
 
62
63
  # Faraday errors carry a response payload, not just a message; the compact
63
64
  # form is how Faraday itself documents constructing them.
@@ -119,7 +120,24 @@ RSpec/SpecFilePathFormat:
119
120
 
120
121
  # These specs deliberately cut across the whole library rather than describing
121
122
  # a single class.
123
+ # The file name must match the gem name so Bundler's default require works.
124
+ Naming/FileName:
125
+ Exclude:
126
+ - "lib/abacatepay-ruby.rb"
127
+
122
128
  RSpec/DescribeClass:
123
129
  Exclude:
130
+ - "spec/load_spec.rb"
124
131
  - "spec/clients/request_contract_spec.rb"
132
+ - "spec/clients/pagination_spec.rb"
133
+ - "spec/clients/resilience_spec.rb"
134
+ - "spec/clients/logging_spec.rb"
135
+ - "spec/clients/boleto_spec.rb"
125
136
  - "spec/packaging_spec.rb"
137
+
138
+ # `has_more?` mirrors the API's own `hasMore` field and matches the convention
139
+ # other payment SDKs use for cursor pagination. `more?` would read as a
140
+ # different question.
141
+ Naming/PredicatePrefix:
142
+ AllowedMethods:
143
+ - has_more?
data/CHANGELOG.md CHANGED
@@ -7,6 +7,124 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.1] - 2026-08-06
11
+
12
+ Inclui tudo o que estava previsto para a 1.2.0, que nunca chegou a ser
13
+ publicada. Todas as correções abaixo foram encontradas exercitando o SDK
14
+ contra o sandbox da AbacatePay e contra uma aplicação Rails real, e não contra
15
+ a própria suíte de testes.
16
+
17
+ ### Fixed
18
+
19
+ - **Webhook signature verification rejected every genuine delivery.** The SDK
20
+ compared against `OpenSSL::HMAC.hexdigest`, but AbacatePay sends the HMAC
21
+ base64-encoded. The [security
22
+ spec](https://docs.abacatepay.com/pages/webhooks/security) and its Node,
23
+ Python and Go samples all use base64. `verify!` therefore failed every real
24
+ webhook while accepting a hex signature nobody sends, making webhook
25
+ verification inoperative since it was introduced. Reported by @danieldenis01
26
+ in #6, found while building a `pay-rails/pay` adapter against the sandbox and
27
+ production, along with the three fixes above.
28
+ - **`qr_code` and `qr_code_image` always returned nil for transparent PIX.** The
29
+ API sends the copy-and-paste payload as `brCode` and the image as
30
+ `brCodeBase64`; the readers looked for the older `qrCode`/`qrCodeImage`
31
+ spelling. Both are accepted now, preferring `qr*` when present. `platform_fee`
32
+ and `receipt_url` were missing entirely.
33
+ - **`simulate_payment` sent the id in the body.** The API reads it from the
34
+ query string on this endpoint, like `check`, and body-only requests fail with
35
+ "Expected property 'id'".
36
+ - **Optional fields were sent as explicit nulls.** The API rejects
37
+ `{"cellphone": null}` with HTTP 400 `Expected property 'cellphone' to be
38
+ string but found: null`, and payload builders emit nils for anything the
39
+ caller left unset. Payloads are now compacted recursively at the request
40
+ boundary, so this cannot be forgotten by a future endpoint. Also reported in
41
+ #6; the fix covers `products`, `coupons`, `payouts`, `pix` and
42
+ `subscriptions`, which had the same defect.
43
+
44
+ - **`gem "abacatepay-ruby"` did not load the SDK.** Bundler requires a gem by
45
+ its own name, so Rails called `require "abacatepay-ruby"`, and the entry
46
+ point was `lib/abacate_pay.rb`. The require failed, but the gemspec had
47
+ already defined `AbacatePay` with nothing but `VERSION` in it, so the first
48
+ call raised `undefined method 'configure' for module AbacatePay` instead of a
49
+ missing-constant error. Every Rails user had to discover `require:
50
+ "abacate_pay"` on their own. There is now an entry point matching the gem
51
+ name.
52
+ - **Customer responses dropped every field except the id.** The API returns
53
+ `name`, `email`, `cellphone` and `taxId` at the top level of the customer
54
+ object with an empty `metadata`; the resource only mapped a nested
55
+ `metadata`, so `customers.list` and `customers.get` returned objects with
56
+ nothing usable and `customer.metadata.name` was always nil. The fields are
57
+ exposed directly now, and `metadata` keeps answering for code written against
58
+ the previous interface.
59
+ - **`AbacatePay.store.get` always failed.** The API serves this as
60
+ `stores/get`, plural. The singular path documented in the reference answers
61
+ HTTP 400.
62
+
63
+ ### Added
64
+
65
+ - `AbacatePay::Webhooks::PUBLIC_KEY`, the fixed key AbacatePay signs with, now
66
+ the default for `secret:`. It is public and global, so it proves body
67
+ integrity only: anyone can compute a valid signature with it.
68
+ - `AbacatePay::Webhooks.verify_secret!(received:, expected:)` for the
69
+ `webhookSecret` query parameter, which is what actually authenticates the
70
+ origin. The docs instruct using both mechanisms together; the SDK previously
71
+ offered only the HMAC half.
72
+
73
+ ### Changed
74
+
75
+ - The README no longer implies `has_more?` is always available. List responses
76
+ only carry pagination metadata when the API sends it; otherwise they are
77
+ plain Arrays, exactly as before.
78
+
79
+ ## [1.1.0] - 2026-08-05
80
+
81
+ ### Added
82
+
83
+ - **BOLETO support.** The method was rejected outright by the `Billings::Methods`
84
+ enum even though it is a first-class payment method in the v2 API. Adds the
85
+ enum value, the boleto-only `due_date`/`interest`/`fine` fields on checkouts,
86
+ and a `method:` argument on `TransparentClient#create` (which previously
87
+ hard-coded PIX). Boleto responses now expose `bar_code`, `url`, `br_code`,
88
+ `br_code_base64` and `expires_at`.
89
+ - **Cursor pagination.** List endpoints cap at 100 items and report `hasMore`
90
+ plus a cursor; the SDK discarded that metadata, making record 101
91
+ unreachable. `list` now returns an `AbacatePay::Collection`. It is Enumerable and
92
+ Array-compatible, so existing code is unaffected, and it carries `has_more?`
93
+ and `next_cursor`. `each_page` and `auto_paging_each` walk every page.
94
+ - **Retries with exponential backoff and jitter** on 429 and 5xx, configurable
95
+ via `config.max_retries` (default 2). Only idempotent methods are retried;
96
+ POST never is, because repeating `checkouts/create` after a timeout could
97
+ charge a customer twice and the API exposes no idempotency key.
98
+ - **Optional request logging** via `config.logger`, with the bearer token
99
+ redacted and bodies never logged.
100
+ - `SubscriptionClient#change_plan` and `#record_usage`: the last two of the 45
101
+ documented v2 endpoints. All 45 are now covered.
102
+ - `subscription.payment_failed` and `subscription.trial_started` webhook event
103
+ types. `payment_failed` is the dunning signal.
104
+ - Checkout fields the v2 API accepts but the SDK never sent: `max_installments`
105
+ (nested under `card`), `up_sell_product_id` and `custom_metadata`.
106
+ - A `User-Agent` identifying the SDK and Ruby version.
107
+
108
+ ### Fixed
109
+
110
+ - `Webhooks.parse` aside, malformed API responses raised `JSON::ParserError`
111
+ and a missing `data` field raised `KeyError`; both are now `ApiError`.
112
+
113
+ ### Changed
114
+
115
+ - `BillingClient`'s deprecation warning now states that its `/billings/*`
116
+ endpoints do not exist on either API version, so every call fails. It will be
117
+ removed in 2.0.0.
118
+
119
+ ### Corrected
120
+
121
+ - The 1.0.0 notes stated that the v1 API "has been retired and answers Not found
122
+ for every path". That is wrong: v1 is still served, under a different dialect.
123
+ It uses singular paths (`/v1/billing/`, `/v1/customer/`) and different
124
+ resource names (`pixQrCode`). The original diagnosis tested v2-shaped paths against
125
+ `/v1`. The fix itself stands: this SDK only ever spoke v2's dialect, so 10 of
126
+ the 12 paths it calls do not exist on v1 and routing there produced 404s.
127
+
10
128
  ## [1.0.0] - 2026-08-05
11
129
 
12
130
  First stable release. The public surface is now covered by CI on four Ruby
@@ -16,15 +134,15 @@ versions and will not change without a major bump.
16
134
 
17
135
  - Full API coverage: checkouts, coupons, customers, payouts, PIX transfers,
18
136
  products, store, subscriptions and transparent checkout.
19
- - `AbacatePay::Webhooks.construct_event` verifies the signature and parses the
137
+ - `AbacatePay::Webhooks.construct_event`, which verifies the signature and parses the
20
138
  body in a single call, so an unverified payload cannot be acted on.
21
139
  - `AbacatePay::Webhooks::PayloadError` for malformed or non-object webhook bodies.
22
- - `PaymentLinkClient` (`AbacatePay.payment_links`) reusable multi-payment links.
23
- - `WebhookClient` (`AbacatePay.webhook_endpoints`) webhook endpoint registration,
140
+ - `PaymentLinkClient` (`AbacatePay.payment_links`) for reusable multi-payment links.
141
+ - `WebhookClient` (`AbacatePay.webhook_endpoints`) for webhook endpoint registration,
24
142
  with local HTTPS validation so a bad endpoint fails before the round trip.
25
- - `CheckoutClient#refund`, `TransparentClient#refund`, `PaymentLinkClient#refund` —
26
- refunds were previously impossible through the SDK.
27
- - `SubscriptionClient#cancel` a subscription created through the SDK could not
143
+ - `CheckoutClient#refund`, `TransparentClient#refund`, `PaymentLinkClient#refund`.
144
+ Refunds were previously impossible through the SDK.
145
+ - `SubscriptionClient#cancel`. A subscription created through the SDK could not
28
146
  be cancelled through it.
29
147
  - CI workflow running the suite on Ruby 3.2, 3.3, 3.4 and 4.0, plus RuboCop, a
30
148
  dependency audit and a gem build check on every pull request.
@@ -62,7 +180,7 @@ versions and will not change without a major bump.
62
180
  without `X-Webhook-Signature` reached `secure_compare` as `nil` and raised
63
181
  `NoMethodError`, so `valid?` returned neither `true` nor `false` and the
64
182
  endpoint returned a server error instead of rejecting the request. Missing and
65
- empty signatures and a missing secret are now `SignatureError`.
183
+ empty signatures, plus a missing secret, are now `SignatureError`.
66
184
  - **`Webhooks.parse` leaked parser internals.** Malformed JSON raised
67
185
  `JSON::ParserError` and a non-object JSON body raised `TypeError`; both now
68
186
  raise `PayloadError`.
@@ -72,7 +190,7 @@ versions and will not change without a major bump.
72
190
  - **Changing the token at runtime had no effect.** Clients were memoized on first
73
191
  use and never rebuilt, so `AbacatePay.configure` after a first API call kept
74
192
  sending the previous bearer token. `configure` now discards memoized clients.
75
- - `Configuration#api_url` was declared twice as an `attr_reader` and as a
193
+ - `Configuration#api_url` was declared twice, as an `attr_reader` and as a
76
194
  method. The dead reader has been removed.
77
195
  - `CustomerClient#get` and `#delete` had no test coverage at all.
78
196
  - The `customer` and `billing` specs stubbed singular endpoint paths while the
@@ -93,13 +211,13 @@ versions and will not change without a major bump.
93
211
  ### Changed
94
212
 
95
213
  - **BREAKING: `required_ruby_version` is now `>= 3.2.0`** (was `>= 2.6.0`). The
96
- old floor was never installable faraday 2.x requires Ruby 3.0 or newer so
214
+ old floor was never installable: faraday 2.x requires Ruby 3.0 or newer, so
97
215
  no working installation is losing support, but a `bundle update` on Ruby 2.6
98
216
  or 3.1 will now refuse to resolve instead of failing later.
99
217
  - **`config.environment` is deprecated and now warns.** It was declared,
100
218
  defaulted, and validated, but read by nothing. Per AbacatePay's own
101
- documentation the environment is decided by the API key Dev mode keys
102
- simulate transactions so the setting could never have worked. It is kept as
219
+ documentation the environment is decided by the API key (Dev mode keys
220
+ simulate transactions), so the setting could never have worked. It is kept as
103
221
  an accepted no-op so existing initializers keep loading.
104
222
  - `validate!` no longer rejects unknown `environment` values, and now rejects an
105
223
  empty or whitespace-only token.
@@ -114,12 +232,14 @@ versions and will not change without a major bump.
114
232
  - Publishing to GitHub Packages. The step never executed successfully in any run
115
233
  and nothing consumed the gem from that registry.
116
234
  - `sig/abacatepay/rails.rbs`. Leftover `bundle gem` scaffolding declaring an
117
- `Abacatepay::Rails` module that does not exist in this codebase and it was
235
+ `Abacatepay::Rails` module that does not exist in this codebase, and that was
118
236
  shipping inside the published gem, where a type checker would read it.
119
237
 
120
238
  ## [0.1.0] - 2024-12-13
121
239
 
122
240
  - Initial release
123
241
 
242
+ [1.2.1]: https://github.com/AbacatePay/abacatepay-ruby-sdk/releases/tag/v1.2.1
243
+ [1.1.0]: https://github.com/AbacatePay/abacatepay-ruby-sdk/releases/tag/v1.1.0
124
244
  [1.0.0]: https://github.com/AbacatePay/abacatepay-ruby-sdk/releases/tag/v1.0.0
125
245
  [0.1.0]: https://github.com/AbacatePay/abacatepay-ruby-sdk/releases/tag/v0.1.0
data/README.md CHANGED
@@ -8,7 +8,7 @@ O [`abacatepay-ruby`](https://rubygems.org/gems/abacatepay-ruby) é um **wrapper
8
8
 
9
9
  <img src="https://res.cloudinary.com/dkok1obj5/image/upload/v1767631413/avo_clhmaf.png" width="100%" alt="AbacatePay Open Source"/>
10
10
 
11
- Funciona em qualquer aplicação Ruby Rails, Sinatra, Hanami ou Ruby puro.
11
+ Funciona em qualquer aplicação Ruby: Rails, Sinatra, Hanami ou Ruby puro.
12
12
 
13
13
  Referência completa da API [aqui](https://abacatepay.readme.io/reference).
14
14
 
@@ -42,8 +42,10 @@ gem 'abacatepay-ruby'
42
42
 
43
43
  ```ruby
44
44
  AbacatePay.configure do |config|
45
- config.api_token = ENV['ABACATEPAY_TOKEN']
46
- config.timeout = 30 # opcional, em segundos
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
 
@@ -54,7 +56,7 @@ Nunca utilize sua API key diretamente no código.
54
56
 
55
57
  Em Rails, coloque isso em `config/initializers/abacatepay.rb`.
56
58
 
57
- Trocar o token em runtime tem efeito imediato os clients são reconstruídos a cada `configure`.
59
+ Trocar o token em runtime tem efeito imediato: os clients são reconstruídos a cada `configure`.
58
60
 
59
61
  ### Criando uma cobrança
60
62
 
@@ -101,25 +103,53 @@ 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.
107
+
108
+ </div>
109
+
110
+ ```ruby
111
+ page = AbacatePay.customers.list
112
+ page.first.id # funciona como Array
113
+
114
+ # Para percorrer tudo sem lidar com cursor:
115
+ AbacatePay.customers.auto_paging_each { |customer| puts customer.id }
116
+ ```
117
+
118
+ <div align="center">
119
+
120
+ Quando a API envia metadados de paginação, o resultado é uma `Collection` que carrega o cursor. Quando não envia, é um Array puro, então cheque antes de usar:
121
+
122
+ </div>
123
+
124
+ ```ruby
125
+ if page.respond_to?(:has_more?) && page.has_more?
126
+ proxima = AbacatePay.customers.list(after: page.next_cursor)
127
+ end
128
+ ```
129
+
130
+ <div align="center">
131
+
104
132
  ## Versionamento
105
133
 
106
- O SDK fala **exclusivamente a v2** `https://api.abacatepay.com/v2`. A v1 foi desligada pela AbacatePay e responde `{"error":"Not found"}` em toda rota, então não o que negociar.
134
+ O SDK fala **exclusivamente a v2**, em `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
135
 
108
- 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.
136
+ 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
137
 
110
- O `BillingClient` também está descontinuado, substituído pelo `CheckoutClient`. Ele emite um aviso ao ser instanciado, e seus endpoints `/billings/*` não existem na v2:
138
+ O `BillingClient` também está descontinuado, substituído pelo `CheckoutClient`. Seus endpoints `/billings/*` não existem em nenhuma versão da API: toda chamada falha. Será removido na 2.0.0:
111
139
 
112
140
  </div>
113
141
 
114
142
  ```
115
- [DEPRECATION] BillingClient is deprecated. Use CheckoutClient instead.
143
+ [DEPRECATION] BillingClient calls /billings/* endpoints that do not exist on the
144
+ AbacatePay API, every request will fail. Use AbacatePay.checkouts instead.
145
+ This class will be removed in 2.0.0.
116
146
  ```
117
147
 
118
148
  <div align="center">
119
149
 
120
150
  ## Tratamento de erros
121
151
 
122
- Diferente do SDK de Node, **este SDK levanta exceções** ele não retorna `{ data, error, success }`. Toda falha vira uma exceção tipada que herda de `AbacatePay::Error`, então você pode capturar tudo de uma vez ou tratar caso a caso.
152
+ Diferente do SDK de Node, **este SDK levanta exceções**. Ele não retorna `{ data, error, success }`. Toda falha vira uma exceção tipada que herda de `AbacatePay::Error`, então você pode capturar tudo de uma vez ou tratar caso a caso.
123
153
 
124
154
  </div>
125
155
 
@@ -147,18 +177,26 @@ Erros de rede e timeout são normalizados para `ApiError`, com a mensagem da API
147
177
 
148
178
  ## Webhooks
149
179
 
150
- Endpoints de webhook são públicos e não autenticados. Use `construct_event`, que **verifica a assinatura antes de fazer o parse** é o único ponto de entrada que não permite agir sobre um payload não verificado.
180
+ Endpoints de webhook são públicos. A AbacatePay usa **dois mecanismos**, e a documentação orienta usar os dois: o `webhookSecret` na query autentica a origem, e a assinatura HMAC garante que o corpo não foi alterado. A chave HMAC é pública e global: ela sozinha não prova origem.
151
181
 
152
182
  </div>
153
183
 
154
184
  ```ruby
155
185
  payload = request.body.read
156
186
  signature = request.headers['X-Webhook-Signature']
157
- secret = ENV['ABACATEPAY_WEBHOOK_SECRET']
158
187
 
159
188
  begin
189
+ # 1. Autentica a origem com o secret que você definiu ao criar o webhook,
190
+ # enviado pela AbacatePay como query parameter.
191
+ AbacatePay::Webhooks.verify_secret!(
192
+ received: params[:webhookSecret],
193
+ expected: ENV['ABACATEPAY_WEBHOOK_SECRET']
194
+ )
195
+
196
+ # 2. Verifica a integridade do corpo. A chave HMAC é pública, então este
197
+ # passo sozinho não prova origem. Por isso os dois juntos.
160
198
  event = AbacatePay::Webhooks.construct_event(
161
- payload: payload, signature: signature, secret: secret
199
+ payload: payload, signature: signature
162
200
  )
163
201
  rescue AbacatePay::Webhooks::SignatureError
164
202
  return head :unauthorized
@@ -175,7 +213,7 @@ end
175
213
 
176
214
  <div align="center">
177
215
 
178
- Header ausente, secret vazio, assinatura forjada e corpo malformado são todos tratados como casos esperados levantam erro tipado em vez de derrubar o endpoint. A comparação de assinatura é feita em tempo constante.
216
+ Header ausente, secret vazio, assinatura forjada e corpo malformado são todos tratados como casos esperados: levantam erro tipado em vez de derrubar o endpoint. A comparação de assinatura é feita em tempo constante.
179
217
 
180
218
  Os métodos de baixo nível continuam disponíveis:
181
219
 
@@ -185,7 +223,7 @@ Os métodos de baixo nível continuam disponíveis:
185
223
  # Levanta SignatureError se a assinatura estiver ausente ou inválida
186
224
  AbacatePay::Webhooks.verify!(payload: payload, signature: signature, secret: secret)
187
225
 
188
- # Contraparte booleana nunca levanta exceção
226
+ # Contraparte booleana. Nunca levanta exceção
189
227
  AbacatePay::Webhooks.valid?(payload: payload, signature: signature, secret: secret)
190
228
 
191
229
  # Faz parse de um corpo já verificado
@@ -200,7 +238,7 @@ AbacatePay::Webhooks.parse(payload)
200
238
  |---|---|
201
239
  | Checkout | `checkout.completed`, `checkout.refunded`, `checkout.disputed` |
202
240
  | Transparent | `transparent.completed`, `transparent.refunded`, `transparent.disputed` |
203
- | Subscription | `subscription.completed`, `subscription.renewed`, `subscription.cancelled` |
241
+ | Subscription | `subscription.completed`, `subscription.renewed`, `subscription.cancelled`, `subscription.payment_failed`, `subscription.trial_started` |
204
242
  | Transfer | `transfer.completed`, `transfer.failed` |
205
243
  | Payout | `payout.completed`, `payout.failed` |
206
244
 
@@ -214,7 +252,7 @@ Todos os recursos são acessíveis pela fachada `AbacatePay.<recurso>`.
214
252
  | `products` | `list` `get` `create` `delete` |
215
253
  | `coupons` | `list` `get` `create` `delete` `toggle` |
216
254
  | `checkouts` | `list` `get` `create` `refund` |
217
- | `subscriptions` | `list` `create` `cancel` |
255
+ | `subscriptions` | `list` `create` `cancel` `change_plan` `record_usage` |
218
256
  | `transparents` | `list` `create` `check` `simulate_payment` `refund` |
219
257
  | `pix` | `list` `get` `send_pix` |
220
258
  | `payouts` | `list` `get` `create` |
@@ -363,9 +401,67 @@ AbacatePay.payouts.create(
363
401
 
364
402
  <div align="center">
365
403
 
404
+ ### Boleto
405
+
406
+ Boleto tem vencimento, juros e multa próprios. Todos os valores em centavos.
407
+
408
+ </div>
409
+
410
+ ```ruby
411
+ AbacatePay.checkouts.create(
412
+ AbacatePay::Resources::Checkouts.new(
413
+ methods: ['BOLETO'],
414
+ due_date: '2026-08-15', # opcional; default 3 dias úteis
415
+ interest: { value: 100 }, # juros ao mês
416
+ fine: { value: 200, type: 'PERCENTAGE' }, # ou type: 'FIXED'
417
+ products: [
418
+ AbacatePay::Resources::Billings::Product.new(external_id: 'prod_123', quantity: 1)
419
+ ]
420
+ )
421
+ )
422
+ ```
423
+
424
+ <div align="center">
425
+
426
+ No checkout transparente, o boleto exige nome e CPF/CNPJ do pagador, o SDK valida antes de chamar a API:
427
+
428
+ </div>
429
+
430
+ ```ruby
431
+ charge = AbacatePay::Resources::Transparents.new(amount: 25_000, due_date: '2026-08-15')
432
+ # charge.customer precisa ter metadata.name e metadata.tax_id
433
+
434
+ boleto = AbacatePay.transparents.create(charge, method: 'BOLETO')
435
+ boleto.bar_code # linha digitável
436
+ boleto.url # PDF para impressão
437
+ boleto.br_code # PIX alternativo da mesma cobrança
438
+ ```
439
+
440
+ <div align="center">
441
+
442
+ ### Parcelamento e order bump
443
+
444
+ </div>
445
+
446
+ ```ruby
447
+ AbacatePay.checkouts.create(
448
+ AbacatePay::Resources::Checkouts.new(
449
+ methods: ['CARD'],
450
+ max_installments: 12,
451
+ up_sell_product_id: 'prod_bump',
452
+ custom_metadata: { origem: 'app-mobile' },
453
+ products: [
454
+ AbacatePay::Resources::Billings::Product.new(external_id: 'prod_123', quantity: 1)
455
+ ]
456
+ )
457
+ )
458
+ ```
459
+
460
+ <div align="center">
461
+
366
462
  ### Links de pagamento
367
463
 
368
- 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`.
464
+ 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`.
369
465
 
370
466
  </div>
371
467
 
@@ -387,7 +483,7 @@ puts link.url # compartilhe esta URL
387
483
 
388
484
  ### Estornos
389
485
 
390
- O estorno é sempre integral a AbacatePay não faz estorno parcial.
486
+ O estorno é sempre integral, a AbacatePay não faz estorno parcial.
391
487
 
392
488
  </div>
393
489
 
@@ -407,6 +503,12 @@ Cancela imediatamente; parcelas futuras pendentes são canceladas junto.
407
503
 
408
504
  ```ruby
409
505
  AbacatePay.subscriptions.cancel('subs_abc123xyz')
506
+
507
+ # Upgrade/downgrade, vale a partir do próximo ciclo
508
+ AbacatePay.subscriptions.change_plan('subs_abc123xyz', product_id: 'prod_pro', quantity: 1)
509
+
510
+ # Cobrança por uso, produto sem ciclo
511
+ AbacatePay.subscriptions.record_usage('subs_abc123xyz', product_id: 'prod_api', units: 50)
410
512
  ```
411
513
 
412
514
  <div align="center">
@@ -448,7 +550,7 @@ AbacatePay.store.revenue(start_date: '2026-01-01', end_date: '2026-03-30')
448
550
 
449
551
  ## Enums
450
552
 
451
- Os valores são validados na construção do recurso um valor inválido levanta `ArgumentError` antes de qualquer chamada de rede.
553
+ Os valores são validados na construção do recurso, um valor inválido levanta `ArgumentError` antes de qualquer chamada de rede.
452
554
 
453
555
  | Enum | Valores |
454
556
  |---|---|
@@ -475,7 +577,7 @@ bundle exec rake # specs + rubocop
475
577
 
476
578
  <div align="center">
477
579
 
478
- Antes de abrir um PR, garanta que `bundle exec rake` passa e que a cobertura não caiu o CI roda os specs em Ruby 3.2, 3.3, 3.4 e 4.0, mais RuboCop, auditoria de dependências e build do gem.
580
+ Antes de abrir um PR, garanta que `bundle exec rake` passa e que a cobertura não caiu, o CI roda os specs em Ruby 3.2, 3.3, 3.4 e 4.0, mais RuboCop, auditoria de dependências e build do gem.
479
581
 
480
582
  ## Licença
481
583
 
@@ -41,13 +41,14 @@ 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"
47
48
  spec.add_development_dependency "rspec", "~> 3.12"
48
49
  spec.add_development_dependency "rubocop", "~> 1.57"
49
50
  spec.add_development_dependency "rubocop-rspec", "~> 3.0"
50
- spec.add_development_dependency "simplecov", "~> 0.22"
51
+ spec.add_development_dependency "simplecov", "~> 1.0"
51
52
 
52
53
  # For more information and examples about making a new gem, check out our
53
54
  # guide at: https://bundler.io/guides/creating_gem.html
@@ -2,7 +2,15 @@
2
2
 
3
3
  module AbacatePay
4
4
  module Clients
5
- # Client class for managing billing-related operations in the AbacatePay API.
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 is deprecated. Use CheckoutClient instead."
21
+ warn "[DEPRECATION] BillingClient calls /billings/* endpoints that do not exist on the " \
22
+ "AbacatePay API, so every request fails. 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
- Array(response).map { |data| Resources::Billings.new(data) }
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
- Array(response).map { |data| Resources::Checkouts.new(data) }
20
+ build_list(response, Resources::Checkouts)
21
21
  end
22
22
 
23
23
  # @param id [String] Checkout ID
@@ -35,7 +35,7 @@ module AbacatePay
35
35
  end
36
36
 
37
37
  # Refunds a paid checkout in full. AbacatePay does not support partial
38
- # refunds the original amount is always returned.
38
+ # refunds, the original amount is always returned.
39
39
  #
40
40
  # @param id [String] Public checkout ID (`bill_...`) or charge ID
41
41
  # (`char_...`, `pix_char_...`, `card_...`)
@@ -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
- }.compact
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