abacatepay-ruby 1.1.0 → 1.2.2
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 +8 -0
- data/CHANGELOG.md +137 -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 +66 -15
- data/lib/abacate_pay/clients/coupon_client.rb +4 -1
- data/lib/abacate_pay/clients/customer_client.rb +4 -1
- data/lib/abacate_pay/clients/payment_link_client.rb +2 -2
- data/lib/abacate_pay/clients/payout_client.rb +7 -1
- data/lib/abacate_pay/clients/pix_client.rb +11 -3
- data/lib/abacate_pay/clients/product_client.rb +4 -1
- data/lib/abacate_pay/clients/store_client.rb +3 -1
- data/lib/abacate_pay/clients/subscription_client.rb +5 -10
- data/lib/abacate_pay/clients/transparent_client.rb +4 -2
- data/lib/abacate_pay/clients/webhook_client.rb +21 -4
- 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/checkouts/statuses.rb +4 -1
- data/lib/abacate_pay/enums/coupons/statuses.rb +4 -1
- data/lib/abacate_pay/enums/webhooks/event_types.rb +1 -1
- data/lib/abacate_pay/resources/coupons.rb +29 -3
- data/lib/abacate_pay/resources/customers.rb +62 -21
- data/lib/abacate_pay/resources/payouts.rb +6 -0
- data/lib/abacate_pay/resources/resource.rb +21 -5
- 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: 6049a0d508a75825d5bd94835dd27ec3241a0040129299165cb122de1af4636c
|
|
4
|
+
data.tar.gz: 8d2e2a858ac296de096f69ebf9acbab31c220d127631bd7a9534f30aa5caf5a7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7733ebca9a9176869461cdbf726933ff32836f8953df5cd4a4a4c64c1021261c0ec6d5a75e753b382071f68b93928a928db8364c0dc280e050aed59a0b9a534e
|
|
7
|
+
data.tar.gz: f90e64c56b1681d41ee820ba3ba05e9e7fa0b9ff61afd2d9adf1daf8d6debf68d62f9865f49f5d956744843f1eb51fecc9cfbc49146a7717c7bbbf7a39a7d078
|
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,14 +120,21 @@ 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"
|
|
127
134
|
- "spec/clients/logging_spec.rb"
|
|
128
135
|
- "spec/clients/boleto_spec.rb"
|
|
129
136
|
- "spec/packaging_spec.rb"
|
|
137
|
+
- "spec/api_conformance_spec.rb"
|
|
130
138
|
|
|
131
139
|
# `has_more?` mirrors the API's own `hasMore` field and matches the convention
|
|
132
140
|
# other payment SDKs use for cursor pagination. `more?` would read as a
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,122 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.2] - 2026-08-06
|
|
11
|
+
|
|
12
|
+
Found by exercising all 39 public calls against the live sandbox.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **`customers.delete`, `products.delete` and `coupons.delete` always failed.**
|
|
17
|
+
Every delete endpoint reads the id from the query string; sending it only in
|
|
18
|
+
the body fails with "Expected property 'id' to be string but found:
|
|
19
|
+
undefined". Same defect as `webhook_endpoints.delete`.
|
|
20
|
+
- **`pix.send_pix` and `payouts.create` always failed.** The API nests the
|
|
21
|
+
destination under `pix` as `{ key, type }`. The SDK sent `key`/`keyType` at
|
|
22
|
+
the top level, which fails with "Property 'pix' is missing". `Payouts` gained
|
|
23
|
+
`pix_key` and `pix_key_type`, without which a payout has no destination at
|
|
24
|
+
all.
|
|
25
|
+
- **`subscriptions.create` always failed.** Line items take the product `id`,
|
|
26
|
+
the same shape checkouts use; the SDK sent `externalId`, which fails with
|
|
27
|
+
"Expected property 'items.0.id' to be string".
|
|
28
|
+
- **Parsing a response could crash on a value the SDK did not know.**
|
|
29
|
+
`initialize_enum` validated while reading API responses, so any status
|
|
30
|
+
AbacatePay introduced turned into an `ArgumentError` inside `list` and `get`
|
|
31
|
+
for every integration at once. It happened with the coupon status `DISABLED`
|
|
32
|
+
and the payment-link status `ACTIVE`, which made `coupons.toggle` and
|
|
33
|
+
`payment_links.create` fail outright. Unknown values now pass through with a
|
|
34
|
+
warning; use the enum's `validate!` when you want a hard failure on a value
|
|
35
|
+
you are about to send.
|
|
36
|
+
- **`Coupons#code` and `#current_redeems` were always nil.** The API uses the
|
|
37
|
+
coupon code as the object's `id` and sends the counter as `redeemsCount`.
|
|
38
|
+
Adds `redeems_count`, `notes` and `dev_mode?`; `code` falls back to the id
|
|
39
|
+
and `current_redeems` keeps answering.
|
|
40
|
+
- **`webhook_endpoints.delete` always failed.** The API reads the id from the
|
|
41
|
+
query string on that endpoint, and answers `{"success":true,"error":null}`
|
|
42
|
+
with no `data` key, which the SDK then rejected as malformed. A successful
|
|
43
|
+
response carrying no payload is no longer an error.
|
|
44
|
+
- Building a resource from an empty payload raised `NoMethodError`.
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- `webhook_endpoints.create` refuses a secret shorter than 32 characters
|
|
49
|
+
locally, which is what the API requires.
|
|
50
|
+
- `pix.list` documents that the API requires an `id`; despite the name it
|
|
51
|
+
filters rather than lists.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- The coupon and checkout status enums gained `DISABLED` and `ACTIVE`.
|
|
56
|
+
|
|
57
|
+
## [1.2.1] - 2026-08-06
|
|
58
|
+
|
|
59
|
+
Inclui tudo o que estava previsto para a 1.2.0, que nunca chegou a ser
|
|
60
|
+
publicada. Todas as correções abaixo foram encontradas exercitando o SDK
|
|
61
|
+
contra o sandbox da AbacatePay e contra uma aplicação Rails real, e não contra
|
|
62
|
+
a própria suíte de testes.
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- **Webhook signature verification rejected every genuine delivery.** The SDK
|
|
67
|
+
compared against `OpenSSL::HMAC.hexdigest`, but AbacatePay sends the HMAC
|
|
68
|
+
base64-encoded. The [security
|
|
69
|
+
spec](https://docs.abacatepay.com/pages/webhooks/security) and its Node,
|
|
70
|
+
Python and Go samples all use base64. `verify!` therefore failed every real
|
|
71
|
+
webhook while accepting a hex signature nobody sends, making webhook
|
|
72
|
+
verification inoperative since it was introduced. Reported by @danieldenis01
|
|
73
|
+
in #6, found while building a `pay-rails/pay` adapter against the sandbox and
|
|
74
|
+
production, along with the three fixes above.
|
|
75
|
+
- **`qr_code` and `qr_code_image` always returned nil for transparent PIX.** The
|
|
76
|
+
API sends the copy-and-paste payload as `brCode` and the image as
|
|
77
|
+
`brCodeBase64`; the readers looked for the older `qrCode`/`qrCodeImage`
|
|
78
|
+
spelling. Both are accepted now, preferring `qr*` when present. `platform_fee`
|
|
79
|
+
and `receipt_url` were missing entirely.
|
|
80
|
+
- **`simulate_payment` sent the id in the body.** The API reads it from the
|
|
81
|
+
query string on this endpoint, like `check`, and body-only requests fail with
|
|
82
|
+
"Expected property 'id'".
|
|
83
|
+
- **Optional fields were sent as explicit nulls.** The API rejects
|
|
84
|
+
`{"cellphone": null}` with HTTP 400 `Expected property 'cellphone' to be
|
|
85
|
+
string but found: null`, and payload builders emit nils for anything the
|
|
86
|
+
caller left unset. Payloads are now compacted recursively at the request
|
|
87
|
+
boundary, so this cannot be forgotten by a future endpoint. Also reported in
|
|
88
|
+
#6; the fix covers `products`, `coupons`, `payouts`, `pix` and
|
|
89
|
+
`subscriptions`, which had the same defect.
|
|
90
|
+
|
|
91
|
+
- **`gem "abacatepay-ruby"` did not load the SDK.** Bundler requires a gem by
|
|
92
|
+
its own name, so Rails called `require "abacatepay-ruby"`, and the entry
|
|
93
|
+
point was `lib/abacate_pay.rb`. The require failed, but the gemspec had
|
|
94
|
+
already defined `AbacatePay` with nothing but `VERSION` in it, so the first
|
|
95
|
+
call raised `undefined method 'configure' for module AbacatePay` instead of a
|
|
96
|
+
missing-constant error. Every Rails user had to discover `require:
|
|
97
|
+
"abacate_pay"` on their own. There is now an entry point matching the gem
|
|
98
|
+
name.
|
|
99
|
+
- **Customer responses dropped every field except the id.** The API returns
|
|
100
|
+
`name`, `email`, `cellphone` and `taxId` at the top level of the customer
|
|
101
|
+
object with an empty `metadata`; the resource only mapped a nested
|
|
102
|
+
`metadata`, so `customers.list` and `customers.get` returned objects with
|
|
103
|
+
nothing usable and `customer.metadata.name` was always nil. The fields are
|
|
104
|
+
exposed directly now, and `metadata` keeps answering for code written against
|
|
105
|
+
the previous interface.
|
|
106
|
+
- **`AbacatePay.store.get` always failed.** The API serves this as
|
|
107
|
+
`stores/get`, plural. The singular path documented in the reference answers
|
|
108
|
+
HTTP 400.
|
|
109
|
+
|
|
110
|
+
### Added
|
|
111
|
+
|
|
112
|
+
- `AbacatePay::Webhooks::PUBLIC_KEY`, the fixed key AbacatePay signs with, now
|
|
113
|
+
the default for `secret:`. It is public and global, so it proves body
|
|
114
|
+
integrity only: anyone can compute a valid signature with it.
|
|
115
|
+
- `AbacatePay::Webhooks.verify_secret!(received:, expected:)` for the
|
|
116
|
+
`webhookSecret` query parameter, which is what actually authenticates the
|
|
117
|
+
origin. The docs instruct using both mechanisms together; the SDK previously
|
|
118
|
+
offered only the HMAC half.
|
|
119
|
+
|
|
120
|
+
### Changed
|
|
121
|
+
|
|
122
|
+
- The README no longer implies `has_more?` is always available. List responses
|
|
123
|
+
only carry pagination metadata when the API sends it; otherwise they are
|
|
124
|
+
plain Arrays, exactly as before.
|
|
125
|
+
|
|
10
126
|
## [1.1.0] - 2026-08-05
|
|
11
127
|
|
|
12
128
|
### Added
|
|
@@ -19,16 +135,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
19
135
|
`br_code_base64` and `expires_at`.
|
|
20
136
|
- **Cursor pagination.** List endpoints cap at 100 items and report `hasMore`
|
|
21
137
|
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.
|
|
138
|
+
unreachable. `list` now returns an `AbacatePay::Collection`. It is Enumerable and
|
|
139
|
+
Array-compatible, so existing code is unaffected, and it carries `has_more?`
|
|
140
|
+
and `next_cursor`. `each_page` and `auto_paging_each` walk every page.
|
|
25
141
|
- **Retries with exponential backoff and jitter** on 429 and 5xx, configurable
|
|
26
142
|
via `config.max_retries` (default 2). Only idempotent methods are retried;
|
|
27
143
|
POST never is, because repeating `checkouts/create` after a timeout could
|
|
28
144
|
charge a customer twice and the API exposes no idempotency key.
|
|
29
145
|
- **Optional request logging** via `config.logger`, with the bearer token
|
|
30
146
|
redacted and bodies never logged.
|
|
31
|
-
- `SubscriptionClient#change_plan` and `#record_usage
|
|
147
|
+
- `SubscriptionClient#change_plan` and `#record_usage`: the last two of the 45
|
|
32
148
|
documented v2 endpoints. All 45 are now covered.
|
|
33
149
|
- `subscription.payment_failed` and `subscription.trial_started` webhook event
|
|
34
150
|
types. `payment_failed` is the dunning signal.
|
|
@@ -50,9 +166,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
50
166
|
### Corrected
|
|
51
167
|
|
|
52
168
|
- 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
|
|
169
|
+
for every path". That is wrong: v1 is still served, under a different dialect.
|
|
170
|
+
It uses singular paths (`/v1/billing/`, `/v1/customer/`) and different
|
|
171
|
+
resource names (`pixQrCode`). The original diagnosis tested v2-shaped paths against
|
|
56
172
|
`/v1`. The fix itself stands: this SDK only ever spoke v2's dialect, so 10 of
|
|
57
173
|
the 12 paths it calls do not exist on v1 and routing there produced 404s.
|
|
58
174
|
|
|
@@ -65,15 +181,15 @@ versions and will not change without a major bump.
|
|
|
65
181
|
|
|
66
182
|
- Full API coverage: checkouts, coupons, customers, payouts, PIX transfers,
|
|
67
183
|
products, store, subscriptions and transparent checkout.
|
|
68
|
-
- `AbacatePay::Webhooks.construct_event
|
|
184
|
+
- `AbacatePay::Webhooks.construct_event`, which verifies the signature and parses the
|
|
69
185
|
body in a single call, so an unverified payload cannot be acted on.
|
|
70
186
|
- `AbacatePay::Webhooks::PayloadError` for malformed or non-object webhook bodies.
|
|
71
|
-
- `PaymentLinkClient` (`AbacatePay.payment_links`)
|
|
72
|
-
- `WebhookClient` (`AbacatePay.webhook_endpoints`)
|
|
187
|
+
- `PaymentLinkClient` (`AbacatePay.payment_links`) for reusable multi-payment links.
|
|
188
|
+
- `WebhookClient` (`AbacatePay.webhook_endpoints`) for webhook endpoint registration,
|
|
73
189
|
with local HTTPS validation so a bad endpoint fails before the round trip.
|
|
74
|
-
- `CheckoutClient#refund`, `TransparentClient#refund`, `PaymentLinkClient#refund
|
|
75
|
-
|
|
76
|
-
- `SubscriptionClient#cancel
|
|
190
|
+
- `CheckoutClient#refund`, `TransparentClient#refund`, `PaymentLinkClient#refund`.
|
|
191
|
+
Refunds were previously impossible through the SDK.
|
|
192
|
+
- `SubscriptionClient#cancel`. A subscription created through the SDK could not
|
|
77
193
|
be cancelled through it.
|
|
78
194
|
- CI workflow running the suite on Ruby 3.2, 3.3, 3.4 and 4.0, plus RuboCop, a
|
|
79
195
|
dependency audit and a gem build check on every pull request.
|
|
@@ -111,7 +227,7 @@ versions and will not change without a major bump.
|
|
|
111
227
|
without `X-Webhook-Signature` reached `secure_compare` as `nil` and raised
|
|
112
228
|
`NoMethodError`, so `valid?` returned neither `true` nor `false` and the
|
|
113
229
|
endpoint returned a server error instead of rejecting the request. Missing and
|
|
114
|
-
empty signatures
|
|
230
|
+
empty signatures, plus a missing secret, are now `SignatureError`.
|
|
115
231
|
- **`Webhooks.parse` leaked parser internals.** Malformed JSON raised
|
|
116
232
|
`JSON::ParserError` and a non-object JSON body raised `TypeError`; both now
|
|
117
233
|
raise `PayloadError`.
|
|
@@ -121,7 +237,7 @@ versions and will not change without a major bump.
|
|
|
121
237
|
- **Changing the token at runtime had no effect.** Clients were memoized on first
|
|
122
238
|
use and never rebuilt, so `AbacatePay.configure` after a first API call kept
|
|
123
239
|
sending the previous bearer token. `configure` now discards memoized clients.
|
|
124
|
-
- `Configuration#api_url` was declared twice
|
|
240
|
+
- `Configuration#api_url` was declared twice, as an `attr_reader` and as a
|
|
125
241
|
method. The dead reader has been removed.
|
|
126
242
|
- `CustomerClient#get` and `#delete` had no test coverage at all.
|
|
127
243
|
- The `customer` and `billing` specs stubbed singular endpoint paths while the
|
|
@@ -142,13 +258,13 @@ versions and will not change without a major bump.
|
|
|
142
258
|
### Changed
|
|
143
259
|
|
|
144
260
|
- **BREAKING: `required_ruby_version` is now `>= 3.2.0`** (was `>= 2.6.0`). The
|
|
145
|
-
old floor was never installable
|
|
261
|
+
old floor was never installable: faraday 2.x requires Ruby 3.0 or newer, so
|
|
146
262
|
no working installation is losing support, but a `bundle update` on Ruby 2.6
|
|
147
263
|
or 3.1 will now refuse to resolve instead of failing later.
|
|
148
264
|
- **`config.environment` is deprecated and now warns.** It was declared,
|
|
149
265
|
defaulted, and validated, but read by nothing. Per AbacatePay's own
|
|
150
|
-
documentation the environment is decided by the API key
|
|
151
|
-
simulate transactions
|
|
266
|
+
documentation the environment is decided by the API key (Dev mode keys
|
|
267
|
+
simulate transactions), so the setting could never have worked. It is kept as
|
|
152
268
|
an accepted no-op so existing initializers keep loading.
|
|
153
269
|
- `validate!` no longer rejects unknown `environment` values, and now rejects an
|
|
154
270
|
empty or whitespace-only token.
|
|
@@ -163,13 +279,15 @@ versions and will not change without a major bump.
|
|
|
163
279
|
- Publishing to GitHub Packages. The step never executed successfully in any run
|
|
164
280
|
and nothing consumed the gem from that registry.
|
|
165
281
|
- `sig/abacatepay/rails.rbs`. Leftover `bundle gem` scaffolding declaring an
|
|
166
|
-
`Abacatepay::Rails` module that does not exist in this codebase
|
|
282
|
+
`Abacatepay::Rails` module that does not exist in this codebase, and that was
|
|
167
283
|
shipping inside the published gem, where a type checker would read it.
|
|
168
284
|
|
|
169
285
|
## [0.1.0] - 2024-12-13
|
|
170
286
|
|
|
171
287
|
- Initial release
|
|
172
288
|
|
|
289
|
+
[1.2.2]: https://github.com/AbacatePay/abacatepay-ruby-sdk/releases/tag/v1.2.2
|
|
290
|
+
[1.2.1]: https://github.com/AbacatePay/abacatepay-ruby-sdk/releases/tag/v1.2.1
|
|
173
291
|
[1.1.0]: https://github.com/AbacatePay/abacatepay-ruby-sdk/releases/tag/v1.1.0
|
|
174
292
|
[1.0.0]: https://github.com/AbacatePay/abacatepay-ruby-sdk/releases/tag/v1.0.0
|
|
175
293
|
[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,29 +75,80 @@ 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
|
|
|
91
|
-
|
|
92
|
-
# Preserve the cursor when the API sends one; dropping it made paging
|
|
93
|
-
# past the first 100 records impossible.
|
|
94
|
-
parsed["pagination"] ? Collection.new(data, parsed["pagination"]) : data
|
|
86
|
+
extract_data(parsed)
|
|
95
87
|
rescue Faraday::Error => e
|
|
96
88
|
handle_request_error(e)
|
|
97
89
|
rescue JSON::ParserError => e
|
|
98
90
|
raise ApiError, "Malformed API response: #{e.message}"
|
|
99
|
-
|
|
100
|
-
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Pulls the payload out of the `{data, error, success}` envelope.
|
|
94
|
+
#
|
|
95
|
+
# Some endpoints answer a successful call with no `data` at all, for
|
|
96
|
+
# example `webhooks/delete` returning `{"success":true,"error":null}`.
|
|
97
|
+
# Treating that as malformed turned a working call into an ApiError.
|
|
98
|
+
#
|
|
99
|
+
# @param parsed [Hash] The decoded response body
|
|
100
|
+
# @return [Object, nil] The payload, or nil when the call carries none
|
|
101
|
+
# @raise [ApiError] if the envelope has neither data nor a success flag
|
|
102
|
+
def extract_data(parsed)
|
|
103
|
+
unless parsed.key?("data")
|
|
104
|
+
return nil if parsed["success"]
|
|
105
|
+
|
|
106
|
+
raise ApiError, "Unexpected API response: #{parsed.inspect[0, 120]}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
data = parsed["data"]
|
|
110
|
+
# Preserve the cursor when the API sends one; dropping it made paging
|
|
111
|
+
# past the first 100 records impossible.
|
|
112
|
+
parsed["pagination"] ? Collection.new(data, parsed["pagination"]) : data
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Issues the HTTP call.
|
|
116
|
+
#
|
|
117
|
+
# @param method [String] The HTTP method
|
|
118
|
+
# @param uri [String] The endpoint URI relative to the base URI
|
|
119
|
+
# @param options [Hash] Params and JSON body
|
|
120
|
+
# @return [Faraday::Response]
|
|
121
|
+
def send_request(method, uri, options)
|
|
122
|
+
@client.public_send(method.downcase) do |req|
|
|
123
|
+
req.url uri
|
|
124
|
+
req.params = options[:params] if options[:params]
|
|
125
|
+
req.body = compact_payload(options[:json]).to_json if options[:json]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Removes nil values from an outgoing payload, at every depth.
|
|
130
|
+
#
|
|
131
|
+
# The API rejects explicit nulls: `{"cellphone": null}` comes back as
|
|
132
|
+
# HTTP 400 "Expected property 'cellphone' to be string but found: null",
|
|
133
|
+
# and payload builders naturally produce them for optional fields the
|
|
134
|
+
# caller left unset. Doing this at the boundary means no endpoint, present
|
|
135
|
+
# or future, can forget it.
|
|
136
|
+
#
|
|
137
|
+
# @param payload [Object] The payload about to be serialised
|
|
138
|
+
# @return [Object] The payload without nil entries
|
|
139
|
+
def compact_payload(payload)
|
|
140
|
+
case payload
|
|
141
|
+
when Hash
|
|
142
|
+
payload.each_with_object({}) do |(key, value), result|
|
|
143
|
+
next if value.nil?
|
|
144
|
+
|
|
145
|
+
result[key] = compact_payload(value)
|
|
146
|
+
end
|
|
147
|
+
when Array
|
|
148
|
+
payload.compact.map { |item| compact_payload(item) }
|
|
149
|
+
else
|
|
150
|
+
payload
|
|
151
|
+
end
|
|
101
152
|
end
|
|
102
153
|
|
|
103
154
|
# Maps a list response into resources without losing the page cursor.
|
|
@@ -123,7 +174,7 @@ module AbacatePay
|
|
|
123
174
|
url: base_url,
|
|
124
175
|
headers: build_headers(configuration),
|
|
125
176
|
# Without an explicit timeout a hung gateway blocks the caller's
|
|
126
|
-
# thread indefinitely
|
|
177
|
+
# thread indefinitely, inside a Rails request, that is an outage.
|
|
127
178
|
request: {
|
|
128
179
|
timeout: configuration.timeout,
|
|
129
180
|
open_timeout: configuration.timeout
|
|
@@ -42,7 +42,10 @@ module AbacatePay
|
|
|
42
42
|
# @param id [String] Coupon ID
|
|
43
43
|
# @return [Resources::Coupons]
|
|
44
44
|
def delete(id)
|
|
45
|
-
|
|
45
|
+
# The API reads the id from the query string on delete endpoints.
|
|
46
|
+
# Sending it only in the body fails with
|
|
47
|
+
# "Expected property 'id' to be string but found: undefined".
|
|
48
|
+
response = request("POST", "delete", params: { id: id }, json: {})
|
|
46
49
|
Resources::Coupons.new(response)
|
|
47
50
|
end
|
|
48
51
|
|
|
@@ -50,7 +50,10 @@ module AbacatePay
|
|
|
50
50
|
# @param id [String] The customer ID
|
|
51
51
|
# @return [Resources::Customers] The deleted Customer object
|
|
52
52
|
def delete(id)
|
|
53
|
-
|
|
53
|
+
# The API reads the id from the query string on delete endpoints.
|
|
54
|
+
# Sending it only in the body fails with
|
|
55
|
+
# "Expected property 'id' to be string but found: undefined".
|
|
56
|
+
response = request("POST", "delete", params: { id: id }, json: {})
|
|
54
57
|
Resources::Customers.new(response)
|
|
55
58
|
end
|
|
56
59
|
end
|
|
@@ -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"
|