abacatepay-ruby 1.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 604226a94737477c55324bf08cb81736cce65fdf482453bd2667572c8986b010
4
- data.tar.gz: 4e292b8d7db21a444cc4348bd59bc2cf396849ff8d99db4d4463ab1b651962fe
3
+ metadata.gz: 65f1214bed1683aa366b68ef9055d85a318ca11901f90c36669fb3202c0b9be5
4
+ data.tar.gz: 0f1374d3702f53204228345237a8072a23918151cb3e2b9d13bd374b558cb04a
5
5
  SHA512:
6
- metadata.gz: 55eeee33fcf8a4b509c849f85d90ccb3f0fb4a2b4aad040325da4a6ed19bf2b60394190ce848ff0b03fa98ec56b0bddccf2fae677c19b1db1dfc7c744819286a
7
- data.tar.gz: e8258c5ba01696d7d76fb85b4314a047052b04f13e0a90c4a826fe83f14ce967ea817cb445d3158a61be27a6ec4af99da9fc0c4dd477449c1bf1b60412f7f612
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,8 +120,14 @@ 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"
125
132
  - "spec/clients/pagination_spec.rb"
126
133
  - "spec/clients/resilience_spec.rb"
data/CHANGELOG.md CHANGED
@@ -7,6 +7,75 @@ 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
+
10
79
  ## [1.1.0] - 2026-08-05
11
80
 
12
81
  ### Added
@@ -19,16 +88,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
19
88
  `br_code_base64` and `expires_at`.
20
89
  - **Cursor pagination.** List endpoints cap at 100 items and report `hasMore`
21
90
  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.
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.
25
94
  - **Retries with exponential backoff and jitter** on 429 and 5xx, configurable
26
95
  via `config.max_retries` (default 2). Only idempotent methods are retried;
27
96
  POST never is, because repeating `checkouts/create` after a timeout could
28
97
  charge a customer twice and the API exposes no idempotency key.
29
98
  - **Optional request logging** via `config.logger`, with the bearer token
30
99
  redacted and bodies never logged.
31
- - `SubscriptionClient#change_plan` and `#record_usage` the last two of the 45
100
+ - `SubscriptionClient#change_plan` and `#record_usage`: the last two of the 45
32
101
  documented v2 endpoints. All 45 are now covered.
33
102
  - `subscription.payment_failed` and `subscription.trial_started` webhook event
34
103
  types. `payment_failed` is the dunning signal.
@@ -50,9 +119,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
50
119
  ### Corrected
51
120
 
52
121
  - 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
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
56
125
  `/v1`. The fix itself stands: this SDK only ever spoke v2's dialect, so 10 of
57
126
  the 12 paths it calls do not exist on v1 and routing there produced 404s.
58
127
 
@@ -65,15 +134,15 @@ versions and will not change without a major bump.
65
134
 
66
135
  - Full API coverage: checkouts, coupons, customers, payouts, PIX transfers,
67
136
  products, store, subscriptions and transparent checkout.
68
- - `AbacatePay::Webhooks.construct_event` verifies the signature and parses the
137
+ - `AbacatePay::Webhooks.construct_event`, which verifies the signature and parses the
69
138
  body in a single call, so an unverified payload cannot be acted on.
70
139
  - `AbacatePay::Webhooks::PayloadError` for malformed or non-object webhook bodies.
71
- - `PaymentLinkClient` (`AbacatePay.payment_links`) reusable multi-payment links.
72
- - `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,
73
142
  with local HTTPS validation so a bad endpoint fails before the round trip.
74
- - `CheckoutClient#refund`, `TransparentClient#refund`, `PaymentLinkClient#refund` —
75
- refunds were previously impossible through the SDK.
76
- - `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
77
146
  be cancelled through it.
78
147
  - CI workflow running the suite on Ruby 3.2, 3.3, 3.4 and 4.0, plus RuboCop, a
79
148
  dependency audit and a gem build check on every pull request.
@@ -111,7 +180,7 @@ versions and will not change without a major bump.
111
180
  without `X-Webhook-Signature` reached `secure_compare` as `nil` and raised
112
181
  `NoMethodError`, so `valid?` returned neither `true` nor `false` and the
113
182
  endpoint returned a server error instead of rejecting the request. Missing and
114
- empty signatures and a missing secret are now `SignatureError`.
183
+ empty signatures, plus a missing secret, are now `SignatureError`.
115
184
  - **`Webhooks.parse` leaked parser internals.** Malformed JSON raised
116
185
  `JSON::ParserError` and a non-object JSON body raised `TypeError`; both now
117
186
  raise `PayloadError`.
@@ -121,7 +190,7 @@ versions and will not change without a major bump.
121
190
  - **Changing the token at runtime had no effect.** Clients were memoized on first
122
191
  use and never rebuilt, so `AbacatePay.configure` after a first API call kept
123
192
  sending the previous bearer token. `configure` now discards memoized clients.
124
- - `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
125
194
  method. The dead reader has been removed.
126
195
  - `CustomerClient#get` and `#delete` had no test coverage at all.
127
196
  - The `customer` and `billing` specs stubbed singular endpoint paths while the
@@ -142,13 +211,13 @@ versions and will not change without a major bump.
142
211
  ### Changed
143
212
 
144
213
  - **BREAKING: `required_ruby_version` is now `>= 3.2.0`** (was `>= 2.6.0`). The
145
- 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
146
215
  no working installation is losing support, but a `bundle update` on Ruby 2.6
147
216
  or 3.1 will now refuse to resolve instead of failing later.
148
217
  - **`config.environment` is deprecated and now warns.** It was declared,
149
218
  defaulted, and validated, but read by nothing. Per AbacatePay's own
150
- documentation the environment is decided by the API key Dev mode keys
151
- 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
152
221
  an accepted no-op so existing initializers keep loading.
153
222
  - `validate!` no longer rejects unknown `environment` values, and now rejects an
154
223
  empty or whitespace-only token.
@@ -163,13 +232,14 @@ versions and will not change without a major bump.
163
232
  - Publishing to GitHub Packages. The step never executed successfully in any run
164
233
  and nothing consumed the gem from that registry.
165
234
  - `sig/abacatepay/rails.rbs`. Leftover `bundle gem` scaffolding declaring an
166
- `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
167
236
  shipping inside the published gem, where a type checker would read it.
168
237
 
169
238
  ## [0.1.0] - 2024-12-13
170
239
 
171
240
  - Initial release
172
241
 
242
+ [1.2.1]: https://github.com/AbacatePay/abacatepay-ruby-sdk/releases/tag/v1.2.1
173
243
  [1.1.0]: https://github.com/AbacatePay/abacatepay-ruby-sdk/releases/tag/v1.1.0
174
244
  [1.0.0]: https://github.com/AbacatePay/abacatepay-ruby-sdk/releases/tag/v1.0.0
175
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
 
@@ -56,7 +56,7 @@ Nunca utilize sua API key diretamente no código.
56
56
 
57
57
  Em Rails, coloque isso em `config/initializers/abacatepay.rb`.
58
58
 
59
- 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`.
60
60
 
61
61
  ### Criando uma cobrança
62
62
 
@@ -103,42 +103,53 @@ AbacatePay.checkouts.list(status: 'PAID', email: 'user@example.com')
103
103
 
104
104
  <div align="center">
105
105
 
106
- Listas retornam no máximo 100 itens. O resultado é uma `Collection` — funciona como Array, e ainda carrega o cursor:
106
+ Listas retornam no máximo 100 itens.
107
107
 
108
108
  </div>
109
109
 
110
110
  ```ruby
111
111
  page = AbacatePay.customers.list
112
112
  page.first.id # funciona como Array
113
- page.has_more? # => true
114
- page.next_cursor # => "cust_abc123"
115
113
 
116
114
  # Para percorrer tudo sem lidar com cursor:
117
115
  AbacatePay.customers.auto_paging_each { |customer| puts customer.id }
118
- AbacatePay.customers.each_page { |page| puts page.size }
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
119
128
  ```
120
129
 
121
130
  <div align="center">
122
131
 
123
132
  ## Versionamento
124
133
 
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.
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.
126
135
 
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.
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.
128
137
 
129
- 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:
130
139
 
131
140
  </div>
132
141
 
133
142
  ```
134
- [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.
135
146
  ```
136
147
 
137
148
  <div align="center">
138
149
 
139
150
  ## Tratamento de erros
140
151
 
141
- 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.
142
153
 
143
154
  </div>
144
155
 
@@ -166,18 +177,26 @@ Erros de rede e timeout são normalizados para `ApiError`, com a mensagem da API
166
177
 
167
178
  ## Webhooks
168
179
 
169
- 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.
170
181
 
171
182
  </div>
172
183
 
173
184
  ```ruby
174
185
  payload = request.body.read
175
186
  signature = request.headers['X-Webhook-Signature']
176
- secret = ENV['ABACATEPAY_WEBHOOK_SECRET']
177
187
 
178
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.
179
198
  event = AbacatePay::Webhooks.construct_event(
180
- payload: payload, signature: signature, secret: secret
199
+ payload: payload, signature: signature
181
200
  )
182
201
  rescue AbacatePay::Webhooks::SignatureError
183
202
  return head :unauthorized
@@ -194,7 +213,7 @@ end
194
213
 
195
214
  <div align="center">
196
215
 
197
- 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.
198
217
 
199
218
  Os métodos de baixo nível continuam disponíveis:
200
219
 
@@ -204,7 +223,7 @@ Os métodos de baixo nível continuam disponíveis:
204
223
  # Levanta SignatureError se a assinatura estiver ausente ou inválida
205
224
  AbacatePay::Webhooks.verify!(payload: payload, signature: signature, secret: secret)
206
225
 
207
- # Contraparte booleana nunca levanta exceção
226
+ # Contraparte booleana. Nunca levanta exceção
208
227
  AbacatePay::Webhooks.valid?(payload: payload, signature: signature, secret: secret)
209
228
 
210
229
  # Faz parse de um corpo já verificado
@@ -219,7 +238,7 @@ AbacatePay::Webhooks.parse(payload)
219
238
  |---|---|
220
239
  | Checkout | `checkout.completed`, `checkout.refunded`, `checkout.disputed` |
221
240
  | Transparent | `transparent.completed`, `transparent.refunded`, `transparent.disputed` |
222
- | Subscription | `subscription.completed`, `subscription.renewed`, `subscription.cancelled` |
241
+ | Subscription | `subscription.completed`, `subscription.renewed`, `subscription.cancelled`, `subscription.payment_failed`, `subscription.trial_started` |
223
242
  | Transfer | `transfer.completed`, `transfer.failed` |
224
243
  | Payout | `payout.completed`, `payout.failed` |
225
244
 
@@ -404,7 +423,7 @@ AbacatePay.checkouts.create(
404
423
 
405
424
  <div align="center">
406
425
 
407
- No checkout transparente, o boleto exige nome e CPF/CNPJ do pagador o SDK valida antes de chamar a API:
426
+ No checkout transparente, o boleto exige nome e CPF/CNPJ do pagador, o SDK valida antes de chamar a API:
408
427
 
409
428
  </div>
410
429
 
@@ -442,7 +461,7 @@ AbacatePay.checkouts.create(
442
461
 
443
462
  ### Links de pagamento
444
463
 
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`.
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`.
446
465
 
447
466
  </div>
448
467
 
@@ -464,7 +483,7 @@ puts link.url # compartilhe esta URL
464
483
 
465
484
  ### Estornos
466
485
 
467
- O estorno é sempre integral a AbacatePay não faz estorno parcial.
486
+ O estorno é sempre integral, a AbacatePay não faz estorno parcial.
468
487
 
469
488
  </div>
470
489
 
@@ -485,10 +504,10 @@ Cancela imediatamente; parcelas futuras pendentes são canceladas junto.
485
504
  ```ruby
486
505
  AbacatePay.subscriptions.cancel('subs_abc123xyz')
487
506
 
488
- # Upgrade/downgrade vale a partir do próximo ciclo
507
+ # Upgrade/downgrade, vale a partir do próximo ciclo
489
508
  AbacatePay.subscriptions.change_plan('subs_abc123xyz', product_id: 'prod_pro', quantity: 1)
490
509
 
491
- # Cobrança por uso produto sem ciclo
510
+ # Cobrança por uso, produto sem ciclo
492
511
  AbacatePay.subscriptions.record_usage('subs_abc123xyz', product_id: 'prod_api', units: 50)
493
512
  ```
494
513
 
@@ -531,7 +550,7 @@ AbacatePay.store.revenue(start_date: '2026-01-01', end_date: '2026-03-30')
531
550
 
532
551
  ## Enums
533
552
 
534
- 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.
535
554
 
536
555
  | Enum | Valores |
537
556
  |---|---|
@@ -558,7 +577,7 @@ bundle exec rake # specs + rubocop
558
577
 
559
578
  <div align="center">
560
579
 
561
- 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.
562
581
 
563
582
  ## Licença
564
583
 
@@ -48,7 +48,7 @@ Gem::Specification.new do |spec|
48
48
  spec.add_development_dependency "rspec", "~> 3.12"
49
49
  spec.add_development_dependency "rubocop", "~> 1.57"
50
50
  spec.add_development_dependency "rubocop-rspec", "~> 3.0"
51
- spec.add_development_dependency "simplecov", "~> 0.22"
51
+ spec.add_development_dependency "simplecov", "~> 1.0"
52
52
 
53
53
  # For more information and examples about making a new gem, check out our
54
54
  # guide at: https://bundler.io/guides/creating_gem.html
@@ -5,7 +5,7 @@ module AbacatePay
5
5
  # Deprecated client for the v1 billing endpoints.
6
6
  #
7
7
  # The endpoints this class calls (`/billings/create`, `/billings/list`) do
8
- # not exist on either API version v2 replaced them with `/checkouts/*`,
8
+ # not exist on either API version, v2 replaced them with `/checkouts/*`,
9
9
  # and v1 uses the singular `/billing/*`. Every call raises ApiError.
10
10
  #
11
11
  # Kept only so existing code keeps loading; it will be removed in 2.0.0.
@@ -19,7 +19,7 @@ module AbacatePay
19
19
  # @deprecated Use {CheckoutClient} instead
20
20
  def initialize(client = nil)
21
21
  warn "[DEPRECATION] BillingClient calls /billings/* endpoints that do not exist on the " \
22
- "AbacatePay API every request will fail. Use AbacatePay.checkouts instead. " \
22
+ "AbacatePay API, so every request fails. Use AbacatePay.checkouts instead. " \
23
23
  "This class will be removed in 2.0.0."
24
24
  super(URI, client)
25
25
  end
@@ -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_...`)
@@ -10,7 +10,7 @@ module AbacatePay
10
10
  # This class handles API requests using Faraday and provides a way to manage
11
11
  # authentication and communication with the AbacatePay service.
12
12
  class Client
13
- # Statuses worth retrying. 429 is rate limiting and 5xx are transient
13
+ # Statuses worth retrying. 429 is rate limiting and 5xx are transient -
14
14
  # AbacatePay's own reference tells integrators to back off on both.
15
15
  RETRIABLE_STATUSES = [429, 500, 502, 503, 504].freeze
16
16
 
@@ -75,16 +75,11 @@ module AbacatePay
75
75
  # @param method [String] The HTTP method (e.g., GET, POST)
76
76
  # @param uri [String] The endpoint URI relative to the base URI
77
77
  # @param options [Hash] Optional settings and parameters for the request
78
- # @return [Hash, AbacatePay::Collection] The response data a Collection
78
+ # @return [Hash, AbacatePay::Collection] The response data, a Collection
79
79
  # when the API reports pagination, the raw data otherwise
80
80
  # @raise [ApiError] If an error occurs during the request
81
81
  def request(method, uri, options = {})
82
- response = @client.public_send(method.downcase) do |req|
83
- req.url uri
84
- req.params = options[:params] if options[:params]
85
- req.body = options[:json].to_json if options[:json]
86
- end
87
-
82
+ response = send_request(method, uri, options)
88
83
  parsed = JSON.parse(response.body)
89
84
  raise ApiError, "API error: #{parsed["error"]}" if parsed["error"]
90
85
 
@@ -100,6 +95,45 @@ module AbacatePay
100
95
  raise ApiError, "API response is missing the 'data' field"
101
96
  end
102
97
 
98
+ # Issues the HTTP call.
99
+ #
100
+ # @param method [String] The HTTP method
101
+ # @param uri [String] The endpoint URI relative to the base URI
102
+ # @param options [Hash] Params and JSON body
103
+ # @return [Faraday::Response]
104
+ def send_request(method, uri, options)
105
+ @client.public_send(method.downcase) do |req|
106
+ req.url uri
107
+ req.params = options[:params] if options[:params]
108
+ req.body = compact_payload(options[:json]).to_json if options[:json]
109
+ end
110
+ end
111
+
112
+ # Removes nil values from an outgoing payload, at every depth.
113
+ #
114
+ # The API rejects explicit nulls: `{"cellphone": null}` comes back as
115
+ # HTTP 400 "Expected property 'cellphone' to be string but found: null",
116
+ # and payload builders naturally produce them for optional fields the
117
+ # caller left unset. Doing this at the boundary means no endpoint, present
118
+ # or future, can forget it.
119
+ #
120
+ # @param payload [Object] The payload about to be serialised
121
+ # @return [Object] The payload without nil entries
122
+ def compact_payload(payload)
123
+ case payload
124
+ when Hash
125
+ payload.each_with_object({}) do |(key, value), result|
126
+ next if value.nil?
127
+
128
+ result[key] = compact_payload(value)
129
+ end
130
+ when Array
131
+ payload.compact.map { |item| compact_payload(item) }
132
+ else
133
+ payload
134
+ end
135
+ end
136
+
103
137
  # Maps a list response into resources without losing the page cursor.
104
138
  #
105
139
  # @param response [Array, AbacatePay::Collection] The raw list response
@@ -123,7 +157,7 @@ module AbacatePay
123
157
  url: base_url,
124
158
  headers: build_headers(configuration),
125
159
  # Without an explicit timeout a hung gateway blocks the caller's
126
- # thread indefinitely inside a Rails request, that is an outage.
160
+ # thread indefinitely, inside a Rails request, that is an outage.
127
161
  request: {
128
162
  timeout: configuration.timeout,
129
163
  open_timeout: configuration.timeout
@@ -4,8 +4,8 @@ module AbacatePay
4
4
  module Clients
5
5
  # Client for reusable payment links in the AbacatePay API.
6
6
  #
7
- # A payment link can be paid by many customers independently mass sales,
8
- # raffles, sign-up forms without creating one checkout per customer.
7
+ # A payment link can be paid by many customers independently, mass sales,
8
+ # raffles, sign-up forms, without creating one checkout per customer.
9
9
  # Use CheckoutClient when each customer needs their own charge.
10
10
  class PaymentLinkClient < Client
11
11
  URI = "payment-links"
@@ -12,7 +12,9 @@ module AbacatePay
12
12
 
13
13
  # @return [Resources::Store]
14
14
  def get
15
- response = request("GET", "store/get")
15
+ # The API serves this as `stores/get`, plural, even though the reference
16
+ # documents `store/get`. The singular path answers HTTP 400.
17
+ response = request("GET", "stores/get")
16
18
  Resources::Store.new(response)
17
19
  end
18
20
 
@@ -52,7 +52,7 @@ module AbacatePay
52
52
  end
53
53
 
54
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.
55
+ # effect on the next billing cycle, the current cycle is untouched.
56
56
  #
57
57
  # @param id [String] The subscription ID (`subs_...`)
58
58
  # @param product_id [String] The new product ID (`prod_...`), which must have a cycle
@@ -45,12 +45,14 @@ module AbacatePay
45
45
  # @param id [String] QR code ID (dev mode only)
46
46
  # @return [Resources::Transparents]
47
47
  def simulate_payment(id)
48
- response = request("POST", "simulate-payment", json: { id: id })
48
+ # The API reads the id from the query string here, like #check. Sending
49
+ # it only in the body fails with "Expected property 'id'".
50
+ response = request("POST", "simulate-payment", params: { id: id }, json: {})
49
51
  Resources::Transparents.new(response)
50
52
  end
51
53
 
52
54
  # Refunds a transparent payment in full. AbacatePay does not support
53
- # partial refunds the original amount is always returned.
55
+ # partial refunds, the original amount is always returned.
54
56
  #
55
57
  # @param id [String] Public charge ID (`pix_char_...`, `card_...`, `char_...`)
56
58
  # @return [Resources::Transparents] The refunded charge
@@ -32,7 +32,7 @@ module AbacatePay
32
32
 
33
33
  # Registers a new webhook endpoint.
34
34
  #
35
- # The secret is what AbacatePay signs deliveries with pass the same
35
+ # The secret is what AbacatePay signs deliveries with, pass the same
36
36
  # value to {AbacatePay::Webhooks.construct_event} when handling them.
37
37
  #
38
38
  # @param name [String] Identifying name
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Client must load first every other client inherits from it.
3
+ # Client must load first: every other client inherits from it.
4
4
  require "abacate_pay/clients/client"
5
5
 
6
6
  require "abacate_pay/clients/billing_client"
@@ -9,8 +9,8 @@ module AbacatePay
9
9
  # @api public
10
10
  class Configuration
11
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
12
+ # different dialect, singular paths (`/v1/billing/`, `/v1/customer/`) and
13
+ # different resource names (`pixQrCode`), which this SDK has never
14
14
  # implemented. Deriving a base URL from the token prefix only produced 404s
15
15
  # against v1 while sending v2-shaped paths.
16
16
  API_BASE_URL = "https://api.abacatepay.com/v2"
@@ -43,7 +43,7 @@ module AbacatePay
43
43
  @api_token = nil
44
44
  end
45
45
 
46
- # @deprecated The environment is determined by the API key itself keys
46
+ # @deprecated The environment is determined by the API key itself, keys
47
47
  # created in Dev mode produce simulated transactions, production keys
48
48
  # produce real ones. This setting has never had any effect and is kept
49
49
  # only so existing initializers keep loading.
@@ -14,7 +14,7 @@ module AbacatePay
14
14
  # @return [String] Credit card payment
15
15
  CARD = "CARD"
16
16
 
17
- # Boleto bancário. Supports a due date and late-payment interest/fine
17
+ # Boleto bancário. Supports a due date and late-payment interest/fine -
18
18
  # see Resources::Checkouts#due_date, #interest and #fine.
19
19
  # @return [String] Boleto payment method
20
20
  BOLETO = "BOLETO"
@@ -14,7 +14,7 @@ module AbacatePay
14
14
  SUBSCRIPTION_RENEWED = "subscription.renewed"
15
15
  SUBSCRIPTION_CANCELLED = "subscription.cancelled"
16
16
 
17
- # Recurring charge failed the dunning signal. Without handling this,
17
+ # Recurring charge failed, the dunning signal. Without handling this,
18
18
  # a failing subscription looks identical to a healthy one.
19
19
  SUBSCRIPTION_PAYMENT_FAILED = "subscription.payment_failed"
20
20
 
@@ -2,45 +2,86 @@
2
2
 
3
3
  module AbacatePay
4
4
  module Resources
5
- # Represents a customer resource in the AbacatePay payment system.
5
+ # Represents a customer in the AbacatePay system.
6
6
  #
7
- # This class handles customer data including identification and metadata,
8
- # providing a structured way to manage customer information within the system.
7
+ # The API returns the customer fields at the top level of the object:
8
+ #
9
+ # { "id": "cust_...", "name": "Ana", "email": "ana@example.com",
10
+ # "cellphone": "...", "taxId": "...", "metadata": {} }
11
+ #
12
+ # Earlier versions of this class only mapped `id` and a nested `metadata`,
13
+ # so every other field was silently dropped and `customer.metadata.name`
14
+ # came back nil for real API data.
15
+ #
16
+ # Both shapes work now: the fields are exposed directly, and `metadata`
17
+ # keeps answering for code written against the previous interface.
9
18
  class Customers < Resource
10
- # Maps property names to their corresponding resource classes
11
19
  RESOURCE_PROPERTIES = {
12
20
  metadata: "AbacatePay::Resources::Customers::Metadata"
13
21
  }.freeze
14
22
 
15
- # @return [String, nil] The unique identifier for the customer
16
- attr_reader :id
23
+ # Fields the API sends inside the customer object.
24
+ IDENTITY_FIELDS = %i[name email cellphone tax_id].freeze
17
25
 
18
- # @return [Metadata, nil] Additional customer information
19
- attr_reader :metadata
26
+ attr_reader :id, :country, :zip_code, :dev_mode
20
27
 
21
- # Creates a new Customer instance
22
- #
23
- # @param data [Hash] The raw customer data
24
- # @raise [ArgumentError] If the data is invalid
28
+ # @return [String, nil] Customer's name
29
+ attr_reader :name
30
+
31
+ # @return [String, nil] Customer's email address
32
+ attr_reader :email
33
+
34
+ # @return [String, nil] Customer's cellphone number
35
+ attr_reader :cellphone
36
+
37
+ # @return [String, nil] Customer's tax identification number
38
+ attr_reader :tax_id
39
+
40
+ # @param data [Hash] The customer properties
25
41
  def initialize(data)
26
42
  fill(data)
27
43
  end
28
44
 
29
- private
45
+ # @return [Boolean, nil] Whether this customer belongs to Dev mode
46
+ def dev_mode?
47
+ @dev_mode
48
+ end
30
49
 
31
- # Process a value based on its property
50
+ # The identity fields, wrapped.
32
51
  #
52
+ # Kept because `customer.metadata.name` is what the README documented and
53
+ # what existing integrations call. When the API sends the fields at the
54
+ # top level, this builds the wrapper from them rather than returning the
55
+ # empty object the API puts in `metadata`.
56
+ #
57
+ # @return [Customers::Metadata, nil]
58
+ def metadata
59
+ return @metadata if metadata_populated?(@metadata)
60
+
61
+ identity = IDENTITY_FIELDS.to_h { |field| [field, public_send(field)] }.compact
62
+ return @metadata if identity.empty?
63
+
64
+ Customers::Metadata.new(identity)
65
+ end
66
+
67
+ private
68
+
69
+ # @param metadata [Customers::Metadata, nil]
70
+ # @return [Boolean] Whether the API actually filled the nested object
71
+ def metadata_populated?(metadata)
72
+ return false if metadata.nil?
73
+
74
+ IDENTITY_FIELDS.any? { |field| metadata.public_send(field) }
75
+ end
76
+
33
77
  # @param property [String] The property name
34
- # @param value [Object] The value to process
78
+ # @param value [Object] The raw value
35
79
  # @return [Object] The processed value
36
80
  def process_value(property, value)
37
81
  return nil if value.nil?
38
82
 
39
83
  if RESOURCE_PROPERTIES.key?(property.to_sym)
40
- initialize_resource(
41
- Object.const_get(RESOURCE_PROPERTIES[property.to_sym]),
42
- value
43
- )
84
+ initialize_resource(Object.const_get(RESOURCE_PROPERTIES[property.to_sym]), value)
44
85
  else
45
86
  value
46
87
  end
@@ -48,8 +89,8 @@ module AbacatePay
48
89
 
49
90
  protected
50
91
 
51
- # @!visibility private
52
- attr_writer :id, :metadata
92
+ attr_writer :id, :metadata, :name, :email, :cellphone, :tax_id,
93
+ :country, :zip_code, :dev_mode
53
94
  end
54
95
  end
55
96
  end
@@ -51,7 +51,7 @@ module AbacatePay
51
51
  resource_class.new(value)
52
52
  end
53
53
 
54
- # Default value processor returns the value as-is.
54
+ # Default value processor, returns the value as-is.
55
55
  # Subclasses override this to handle enums, datetimes, and nested resources.
56
56
  #
57
57
  # @param _property [String] The property name
@@ -10,19 +10,36 @@ module AbacatePay
10
10
 
11
11
  DATETIME_PROPERTIES = %w[created_at updated_at expires_at].freeze
12
12
 
13
+ # qr_code and qr_code_image are defined below rather than generated here:
14
+ # they fall back to the br_code fields the API actually sends.
13
15
  attr_reader :id, :amount, :status, :method, :description,
14
- :expires_in, :qr_code, :qr_code_image, :customer,
16
+ :expires_in, :customer,
15
17
  :metadata, :dev_mode, :created_at, :updated_at,
16
18
  # Boleto: due date sent on create, plus the payment slip the
17
- # API returns digitable line, viewing URL, and the PIX
19
+ # API returns: digitable line, viewing URL, and the PIX
18
20
  # fallback issued for the same charge.
19
21
  :due_date, :bar_code, :url, :br_code, :br_code_base64,
20
- :expires_at
22
+ :expires_at, :platform_fee, :receipt_url
21
23
 
22
24
  def initialize(data)
23
25
  fill(data)
24
26
  end
25
27
 
28
+ # The API returns the copy-and-paste payload as `brCode` and the image as
29
+ # `brCodeBase64`. Older documentation used `qrCode`/`qrCodeImage`, so both
30
+ # spellings are accepted: the reader prefers the `qr*` value when the API
31
+ # sends one and falls back to `br*`.
32
+ #
33
+ # @return [String, nil] PIX copy-and-paste payload
34
+ def qr_code
35
+ @qr_code || br_code
36
+ end
37
+
38
+ # @return [String, nil] PIX QR code image as a data URI
39
+ def qr_code_image
40
+ @qr_code_image || br_code_base64
41
+ end
42
+
26
43
  def dev_mode?
27
44
  @dev_mode
28
45
  end
@@ -47,7 +64,7 @@ module AbacatePay
47
64
  :expires_in, :qr_code, :qr_code_image, :customer,
48
65
  :metadata, :dev_mode, :created_at, :updated_at,
49
66
  :due_date, :bar_code, :url, :br_code, :br_code_base64,
50
- :expires_at
67
+ :expires_at, :platform_fee, :receipt_url
51
68
  end
52
69
  end
53
70
  end
@@ -4,7 +4,7 @@ module AbacatePay
4
4
  module Resources
5
5
  # Represents a registered webhook endpoint in the AbacatePay system.
6
6
  #
7
- # This is the endpoint *registration* the URL AbacatePay delivers events
7
+ # This is the endpoint *registration*, the URL AbacatePay delivers events
8
8
  # to. For verifying and parsing an inbound delivery, see {AbacatePay::Webhooks}.
9
9
  class WebhookEndpoints < Resource
10
10
  # @return [String, nil] Webhook ID
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Resource must load first every other resource inherits from it.
3
+ # Resource must load first: every other resource inherits from it.
4
4
  require "abacate_pay/resources/resource"
5
5
 
6
6
  require "abacate_pay/resources/billings"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AbacatePay
4
- VERSION = "1.1.0"
4
+ VERSION = "1.2.1"
5
5
  end
@@ -17,33 +17,65 @@ module AbacatePay
17
17
  # Raised when a webhook body is not a JSON object the SDK can interpret.
18
18
  class PayloadError < AbacatePay::Error; end
19
19
 
20
- # Verifies a webhook signature using HMAC-SHA256.
20
+ # AbacatePay signs every delivery with this fixed key, published at
21
+ # https://docs.abacatepay.com/pages/webhooks/security and hard-coded in the
22
+ # Node, Python and Go samples there.
23
+ #
24
+ # It is public and global, so it proves only that the body was not altered
25
+ # in transit, it does NOT prove the request came from AbacatePay, since
26
+ # anyone can compute a valid signature with it. Origin is authenticated by
27
+ # the `webhookSecret` query parameter; see {verify_secret!}. Use both.
28
+ PUBLIC_KEY = "t9dXRhHHo3yDEj5pVDYz0frf7q6bMKyMRmxxCPIPp3RCplBfXRxqlC6ZpiWmOqj4L63qEaeUOtrCI8P0VMU" \
29
+ "go6iIga2ri9ogaHFs0WIIywSMg0q7RmBfybe1E5XJcfC4IW3alNqym0tXoAKkzvfEjZxV6bE0oG2zJrNNYmU" \
30
+ "CKZyV0KZ3JS8Votf9EAWWYdiDkMkpbMdPggfh1EqHlVkMiTady6jOR3hyzGEHrIz2Ret0xHKMbiqkr9HS1Jh" \
31
+ "NHDX9"
32
+
33
+ # Verifies the `X-Webhook-Signature` header: HMAC-SHA256 over the raw body,
34
+ # base64-encoded.
21
35
  #
22
36
  # @param payload [String] The raw request body
23
37
  # @param signature [String] The X-Webhook-Signature header value
24
- # @param secret [String] Your webhook secret/public key
38
+ # @param secret [String] HMAC key. Defaults to {PUBLIC_KEY}, which is what
39
+ # AbacatePay signs with.
25
40
  # @return [true] if signature is valid
26
41
  # @raise [SignatureError] if the signature is missing or invalid
27
- def self.verify!(payload:, signature:, secret:)
42
+ def self.verify!(payload:, signature:, secret: PUBLIC_KEY)
28
43
  raise SignatureError, "Missing webhook signature" if signature.nil? || signature.to_s.empty?
29
44
  raise SignatureError, "Missing webhook secret" if secret.nil? || secret.to_s.empty?
30
45
 
31
- expected = OpenSSL::HMAC.hexdigest("SHA256", secret.to_s, payload.to_s)
46
+ expected = base64_hmac(secret.to_s, payload.to_s)
32
47
  raise SignatureError, "Invalid webhook signature" unless secure_compare(expected, signature.to_s)
33
48
 
34
49
  true
35
50
  end
36
51
 
52
+ # Verifies the `webhookSecret` query parameter, which is what actually
53
+ # authenticates the request as coming from AbacatePay.
54
+ #
55
+ # The HMAC signature alone cannot do this: it is computed with a public key.
56
+ #
57
+ # @param received [String] The `webhookSecret` query parameter as received
58
+ # @param expected [String] The secret you configured on the webhook
59
+ # @return [true] if they match
60
+ # @raise [SignatureError] if either is missing or they differ
61
+ def self.verify_secret!(received:, expected:)
62
+ raise SignatureError, "Missing webhook secret parameter" if received.nil? || received.to_s.empty?
63
+ raise SignatureError, "Missing expected webhook secret" if expected.nil? || expected.to_s.empty?
64
+ raise SignatureError, "Invalid webhook secret" unless secure_compare(expected.to_s, received.to_s)
65
+
66
+ true
67
+ end
68
+
37
69
  # Checks if a webhook signature is valid.
38
70
  #
39
- # Never raises for untrusted input a missing header, an empty secret, or a
71
+ # Never raises for untrusted input, a missing header, an empty secret, or a
40
72
  # forged signature all return false.
41
73
  #
42
74
  # @param payload [String] The raw request body
43
75
  # @param signature [String] The X-Webhook-Signature header value
44
76
  # @param secret [String] Your webhook secret/public key
45
77
  # @return [Boolean]
46
- def self.valid?(payload:, signature:, secret:)
78
+ def self.valid?(payload:, signature:, secret: PUBLIC_KEY)
47
79
  verify!(payload: payload, signature: signature, secret: secret)
48
80
  true
49
81
  rescue SignatureError
@@ -78,11 +110,25 @@ module AbacatePay
78
110
  # @return [Event] The verified, parsed event
79
111
  # @raise [SignatureError] if the signature is missing or invalid
80
112
  # @raise [PayloadError] if the body is not a JSON object
81
- def self.construct_event(payload:, signature:, secret:)
113
+ def self.construct_event(payload:, signature:, secret: PUBLIC_KEY)
82
114
  verify!(payload: payload, signature: signature, secret: secret)
83
115
  parse(payload)
84
116
  end
85
117
 
118
+ # Base64-encoded HMAC-SHA256, matching what AbacatePay sends.
119
+ #
120
+ # Uses Array#pack rather than the base64 gem: base64 stopped being a default
121
+ # gem in Ruby 3.4, and requiring it would add a runtime dependency for one
122
+ # call.
123
+ #
124
+ # @param secret [String] The HMAC key
125
+ # @param payload [String] The raw body
126
+ # @return [String] The base64 signature
127
+ def self.base64_hmac(secret, payload)
128
+ [OpenSSL::HMAC.digest("SHA256", secret, payload)].pack("m0")
129
+ end
130
+ private_class_method :base64_hmac
131
+
86
132
  # Constant-time comparison to prevent timing attacks.
87
133
  #
88
134
  # @param expected [String] The signature computed from the payload
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Entry point matching the gem name.
4
+ #
5
+ # Bundler requires a gem by its own name, so `gem "abacatepay-ruby"` in a
6
+ # Gemfile makes Rails call `require "abacatepay-ruby"` (and then
7
+ # `require "abacatepay/ruby"`). Neither matched `lib/abacate_pay.rb`, so the SDK
8
+ # silently failed to load and the first call raised
9
+ # `undefined method 'configure' for module AbacatePay` — the module existed with
10
+ # only VERSION in it, defined as a side effect of the gemspec.
11
+ #
12
+ # This file makes the default `gem "abacatepay-ruby"` work without a `require:`
13
+ # option. `require "abacate_pay"` keeps working for anyone already using it.
14
+ require_relative "abacate_pay"
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.1.0
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matheus Cardoso
@@ -105,14 +105,14 @@ dependencies:
105
105
  requirements:
106
106
  - - "~>"
107
107
  - !ruby/object:Gem::Version
108
- version: '0.22'
108
+ version: '1.0'
109
109
  type: :development
110
110
  prerelease: false
111
111
  version_requirements: !ruby/object:Gem::Requirement
112
112
  requirements:
113
113
  - - "~>"
114
114
  - !ruby/object:Gem::Version
115
- version: '0.22'
115
+ version: '1.0'
116
116
  description: The easiest way to integrate your Ruby application with AbacatePay Gateway
117
117
  for payments, subscriptions, PIX transfers, and more.
118
118
  email:
@@ -178,6 +178,7 @@ files:
178
178
  - lib/abacate_pay/version.rb
179
179
  - lib/abacate_pay/webhooks.rb
180
180
  - lib/abacate_pay/webhooks/event.rb
181
+ - lib/abacatepay-ruby.rb
181
182
  homepage: https://www.abacatepay.com/
182
183
  licenses:
183
184
  - MIT