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 +4 -4
- data/.rubocop.yml +7 -0
- data/CHANGELOG.md +89 -19
- data/README.md +43 -24
- data/abacatepay-ruby.gemspec +1 -1
- data/lib/abacate_pay/clients/billing_client.rb +2 -2
- data/lib/abacate_pay/clients/checkout_client.rb +1 -1
- data/lib/abacate_pay/clients/client.rb +43 -9
- data/lib/abacate_pay/clients/payment_link_client.rb +2 -2
- data/lib/abacate_pay/clients/store_client.rb +3 -1
- data/lib/abacate_pay/clients/subscription_client.rb +1 -1
- data/lib/abacate_pay/clients/transparent_client.rb +4 -2
- data/lib/abacate_pay/clients/webhook_client.rb +1 -1
- data/lib/abacate_pay/clients.rb +1 -1
- data/lib/abacate_pay/configuration.rb +3 -3
- data/lib/abacate_pay/enums/billings/methods.rb +1 -1
- data/lib/abacate_pay/enums/webhooks/event_types.rb +1 -1
- data/lib/abacate_pay/resources/customers.rb +62 -21
- data/lib/abacate_pay/resources/resource.rb +1 -1
- data/lib/abacate_pay/resources/transparents.rb +21 -4
- data/lib/abacate_pay/resources/webhook_endpoints.rb +1 -1
- data/lib/abacate_pay/resources.rb +1 -1
- data/lib/abacate_pay/version.rb +1 -1
- data/lib/abacate_pay/webhooks.rb +53 -7
- data/lib/abacatepay-ruby.rb +14 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 65f1214bed1683aa366b68ef9055d85a318ca11901f90c36669fb3202c0b9be5
|
|
4
|
+
data.tar.gz: 0f1374d3702f53204228345237a8072a23918151cb3e2b9d13bd374b558cb04a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
23
|
-
Array-compatible, so existing code is unaffected
|
|
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
|
|
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
|
-
|
|
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
|
|
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`)
|
|
72
|
-
- `WebhookClient` (`AbacatePay.webhook_endpoints`)
|
|
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
|
-
|
|
76
|
-
- `SubscriptionClient#cancel
|
|
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
|
|
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
|
|
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
|
|
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
|
|
151
|
-
simulate transactions
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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`.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
data/abacatepay-ruby.gemspec
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
8
|
-
# raffles, sign-up forms
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
data/lib/abacate_pay/clients.rb
CHANGED
|
@@ -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
|
|
13
|
-
# different resource names (`pixQrCode`)
|
|
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
|
|
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
|
|
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
|
|
5
|
+
# Represents a customer in the AbacatePay system.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
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
|
-
#
|
|
16
|
-
|
|
23
|
+
# Fields the API sends inside the customer object.
|
|
24
|
+
IDENTITY_FIELDS = %i[name email cellphone tax_id].freeze
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
attr_reader :metadata
|
|
26
|
+
attr_reader :id, :country, :zip_code, :dev_mode
|
|
20
27
|
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# @
|
|
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
|
-
|
|
45
|
+
# @return [Boolean, nil] Whether this customer belongs to Dev mode
|
|
46
|
+
def dev_mode?
|
|
47
|
+
@dev_mode
|
|
48
|
+
end
|
|
30
49
|
|
|
31
|
-
#
|
|
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
|
|
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
|
-
|
|
52
|
-
|
|
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
|
|
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, :
|
|
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
|
|
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
|
|
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
|
data/lib/abacate_pay/version.rb
CHANGED
data/lib/abacate_pay/webhooks.rb
CHANGED
|
@@ -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
|
-
#
|
|
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]
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|