abacatepay-ruby 1.2.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 65f1214bed1683aa366b68ef9055d85a318ca11901f90c36669fb3202c0b9be5
4
- data.tar.gz: 0f1374d3702f53204228345237a8072a23918151cb3e2b9d13bd374b558cb04a
3
+ metadata.gz: 6049a0d508a75825d5bd94835dd27ec3241a0040129299165cb122de1af4636c
4
+ data.tar.gz: 8d2e2a858ac296de096f69ebf9acbab31c220d127631bd7a9534f30aa5caf5a7
5
5
  SHA512:
6
- metadata.gz: de8e7e809c16a249b698f02ed6d7f5eb0d1df24e6e2cab086f32a49fe90c818665a31c1cd642fae49f9a57174482470d64ec681ee99a55cb35e400d2d9fa6188
7
- data.tar.gz: 22fd62f1e9e223d5d2e956c466b1d9de1d6128930d3b8651f4c72369e5827596e3a46c4c30db5171673eee95dd1bce49ef16a1739a351c64f1c73babeb37ff8a
6
+ metadata.gz: 7733ebca9a9176869461cdbf726933ff32836f8953df5cd4a4a4c64c1021261c0ec6d5a75e753b382071f68b93928a928db8364c0dc280e050aed59a0b9a534e
7
+ data.tar.gz: f90e64c56b1681d41ee820ba3ba05e9e7fa0b9ff61afd2d9adf1daf8d6debf68d62f9865f49f5d956744843f1eb51fecc9cfbc49146a7717c7bbbf7a39a7d078
data/.rubocop.yml CHANGED
@@ -134,6 +134,7 @@ RSpec/DescribeClass:
134
134
  - "spec/clients/logging_spec.rb"
135
135
  - "spec/clients/boleto_spec.rb"
136
136
  - "spec/packaging_spec.rb"
137
+ - "spec/api_conformance_spec.rb"
137
138
 
138
139
  # `has_more?` mirrors the API's own `hasMore` field and matches the convention
139
140
  # other payment SDKs use for cursor pagination. `more?` would read as a
data/CHANGELOG.md CHANGED
@@ -7,6 +7,53 @@ 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
+
10
57
  ## [1.2.1] - 2026-08-06
11
58
 
12
59
  Inclui tudo o que estava previsto para a 1.2.0, que nunca chegou a ser
@@ -239,6 +286,7 @@ versions and will not change without a major bump.
239
286
 
240
287
  - Initial release
241
288
 
289
+ [1.2.2]: https://github.com/AbacatePay/abacatepay-ruby-sdk/releases/tag/v1.2.2
242
290
  [1.2.1]: https://github.com/AbacatePay/abacatepay-ruby-sdk/releases/tag/v1.2.1
243
291
  [1.1.0]: https://github.com/AbacatePay/abacatepay-ruby-sdk/releases/tag/v1.1.0
244
292
  [1.0.0]: https://github.com/AbacatePay/abacatepay-ruby-sdk/releases/tag/v1.0.0
@@ -83,16 +83,33 @@ module AbacatePay
83
83
  parsed = JSON.parse(response.body)
84
84
  raise ApiError, "API error: #{parsed["error"]}" if parsed["error"]
85
85
 
86
- data = parsed.fetch("data")
87
- # Preserve the cursor when the API sends one; dropping it made paging
88
- # past the first 100 records impossible.
89
- parsed["pagination"] ? Collection.new(data, parsed["pagination"]) : data
86
+ extract_data(parsed)
90
87
  rescue Faraday::Error => e
91
88
  handle_request_error(e)
92
89
  rescue JSON::ParserError => e
93
90
  raise ApiError, "Malformed API response: #{e.message}"
94
- rescue KeyError
95
- raise ApiError, "API response is missing the 'data' field"
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
96
113
  end
97
114
 
98
115
  # Issues the HTTP call.
@@ -42,7 +42,10 @@ module AbacatePay
42
42
  # @param id [String] Coupon ID
43
43
  # @return [Resources::Coupons]
44
44
  def delete(id)
45
- response = request("POST", "delete", json: { id: id })
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
- response = request("POST", "delete", json: { id: id })
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
@@ -32,7 +32,13 @@ module AbacatePay
32
32
  response = request("POST", "create", json: {
33
33
  amount: data.amount,
34
34
  externalId: data.external_id,
35
- description: data.description
35
+ description: data.description,
36
+ # The API nests the destination under `pix`, with
37
+ # `key` and `type`. Sending pixKey/pixKeyType at the
38
+ # top level fails with "Property 'pix' is missing";
39
+ # nesting the wrong names fails with
40
+ # "Property 'pix.type' is missing".
41
+ pix: { key: data.pix_key, type: data.pix_key_type }
36
42
  })
37
43
  Resources::Payouts.new(response)
38
44
  end
@@ -13,7 +13,11 @@ module AbacatePay
13
13
  super(URI, client)
14
14
  end
15
15
 
16
- # @param params [Hash] Optional pagination params (after, before, limit)
16
+ # The API requires an `id` on this endpoint: a bare call fails with
17
+ # "Expected property 'id' to be string but found: undefined". Despite the
18
+ # name it filters rather than lists.
19
+ #
20
+ # @param params [Hash] Params forwarded to the API; `id` is required
17
21
  # @return [Array<Resources::PixTransfers>]
18
22
  def list(**params)
19
23
  response = request("GET", "list", params: params.empty? ? nil : params)
@@ -37,8 +41,12 @@ module AbacatePay
37
41
  amount: data.amount,
38
42
  externalId: data.external_id,
39
43
  description: data.description,
40
- key: data.key,
41
- keyType: data.key_type
44
+ # The API nests the destination under `pix`, with
45
+ # `key` and `type`. Sending pixKey/pixKeyType at the
46
+ # top level fails with "Property 'pix' is missing";
47
+ # nesting the wrong names fails with
48
+ # "Property 'pix.type' is missing".
49
+ pix: { key: data.key, type: data.key_type }
42
50
  })
43
51
  Resources::PixTransfers.new(response)
44
52
  end
@@ -46,7 +46,10 @@ module AbacatePay
46
46
  # @param id [String] Product ID
47
47
  # @return [Resources::Products]
48
48
  def delete(id)
49
- response = request("POST", "delete", json: { id: id })
49
+ # The API reads the id from the query string on delete endpoints.
50
+ # Sending it only in the body fails with
51
+ # "Expected property 'id' to be string but found: undefined".
52
+ response = request("POST", "delete", params: { id: id }, json: {})
50
53
  Resources::Products.new(response)
51
54
  end
52
55
  end
@@ -26,15 +26,10 @@ module AbacatePay
26
26
  methods: data.methods,
27
27
  externalId: data.external_id,
28
28
  customerId: data.customer&.id,
29
- items: data.products&.map do |product|
30
- {
31
- externalId: product.external_id,
32
- name: product.name,
33
- description: product.description,
34
- quantity: product.quantity,
35
- price: product.price
36
- }
37
- end
29
+ # The API expects the product id here, the same shape checkouts use.
30
+ # Sending externalId fails with
31
+ # "Expected property 'items.0.id' to be string".
32
+ items: data.products&.map { |product| { id: product.external_id, quantity: product.quantity } }
38
33
  }
39
34
 
40
35
  response = request("POST", "create", json: request_data)
@@ -11,6 +11,9 @@ module AbacatePay
11
11
  class WebhookClient < Client
12
12
  URI = "webhooks"
13
13
 
14
+ # Shortest signing secret the API accepts.
15
+ MINIMUM_SECRET_LENGTH = 32
16
+
14
17
  # @param client [Faraday::Connection, nil] Optional Faraday client
15
18
  def initialize(client = nil)
16
19
  super(URI, client)
@@ -40,11 +43,19 @@ module AbacatePay
40
43
  # @param secret [String] Signing secret
41
44
  # @param events [Array<String>] Event types to subscribe to
42
45
  # @return [Resources::WebhookEndpoints] The created webhook
43
- # @raise [ArgumentError] if the endpoint is not an HTTPS URL
46
+ # @raise [ArgumentError] if the endpoint is not HTTPS, the event list is
47
+ # empty, or the secret is shorter than the API accepts
44
48
  def create(name:, endpoint:, secret:, events:)
45
49
  raise ArgumentError, "endpoint must be an HTTPS URL, got #{endpoint.inspect}" unless https_url?(endpoint)
46
50
  raise ArgumentError, "events must not be empty" if Array(events).empty?
47
51
 
52
+ # The API rejects anything shorter with
53
+ # "Expected string length greater or equal to 32".
54
+ if secret.to_s.length < MINIMUM_SECRET_LENGTH
55
+ raise ArgumentError,
56
+ "secret must be at least #{MINIMUM_SECRET_LENGTH} characters, got #{secret.to_s.length}"
57
+ end
58
+
48
59
  response = request("POST", "create", json: {
49
60
  name: name,
50
61
  endpoint: endpoint,
@@ -55,9 +66,15 @@ module AbacatePay
55
66
  end
56
67
 
57
68
  # @param id [String] The webhook ID
58
- # @return [Resources::WebhookEndpoints] The deleted webhook
69
+ # @return [Resources::WebhookEndpoints, nil] The deleted webhook, or nil
70
+ # when the API confirms the deletion without echoing the object
59
71
  def delete(id)
60
- response = request("POST", "delete", json: { id: id })
72
+ # The API reads the id from the query string here, like
73
+ # transparents/simulate-payment. Sending it in the body fails with
74
+ # "Expected property 'id' to be string but found: undefined".
75
+ response = request("POST", "delete", params: { id: id }, json: {})
76
+ return nil if response.nil?
77
+
61
78
  Resources::WebhookEndpoints.new(response)
62
79
  end
63
80
 
@@ -10,8 +10,11 @@ module AbacatePay
10
10
  PAID = "PAID"
11
11
  REFUNDED = "REFUNDED"
12
12
 
13
+ # Reusable payment links are created ACTIVE rather than PENDING.
14
+ ACTIVE = "ACTIVE"
15
+
13
16
  def self.values
14
- [PENDING, EXPIRED, CANCELLED, PAID, REFUNDED]
17
+ [PENDING, EXPIRED, CANCELLED, PAID, REFUNDED, ACTIVE]
15
18
  end
16
19
 
17
20
  def self.valid?(value)
@@ -8,8 +8,11 @@ module AbacatePay
8
8
  INACTIVE = "INACTIVE"
9
9
  EXPIRED = "EXPIRED"
10
10
 
11
+ # What the API returns for a coupon turned off through `toggle`.
12
+ DISABLED = "DISABLED"
13
+
11
14
  def self.values
12
- [ACTIVE, INACTIVE, EXPIRED]
15
+ [ACTIVE, INACTIVE, EXPIRED, DISABLED]
13
16
  end
14
17
 
15
18
  def self.valid?(value)
@@ -11,13 +11,38 @@ module AbacatePay
11
11
 
12
12
  DATETIME_PROPERTIES = %w[created_at updated_at].freeze
13
13
 
14
- attr_reader :id, :code, :discount, :discount_kind, :max_redeems,
15
- :current_redeems, :status, :created_at, :updated_at
14
+ attr_reader :id, :discount, :discount_kind, :max_redeems,
15
+ :status, :notes, :dev_mode, :created_at, :updated_at
16
+
17
+ # @return [Integer, nil] Times the coupon has been redeemed
18
+ attr_reader :redeems_count
16
19
 
17
20
  def initialize(data)
18
21
  fill(data)
19
22
  end
20
23
 
24
+ # The coupon code.
25
+ #
26
+ # The API uses the code as the object's `id` and sends no separate
27
+ # `code` field, so this reader was always nil for API data.
28
+ #
29
+ # @return [String, nil]
30
+ def code
31
+ @code || @id
32
+ end
33
+
34
+ # @return [Integer, nil] Times the coupon has been redeemed
35
+ #
36
+ # @deprecated The API calls this `redeemsCount`; use {#redeems_count}.
37
+ def current_redeems
38
+ @current_redeems || @redeems_count
39
+ end
40
+
41
+ # @return [Boolean, nil] Whether the coupon belongs to Dev mode
42
+ def dev_mode?
43
+ @dev_mode
44
+ end
45
+
21
46
  private
22
47
 
23
48
  def process_value(property, value)
@@ -35,7 +60,8 @@ module AbacatePay
35
60
  protected
36
61
 
37
62
  attr_writer :id, :code, :discount, :discount_kind, :max_redeems,
38
- :current_redeems, :status, :created_at, :updated_at
63
+ :current_redeems, :redeems_count, :status, :notes,
64
+ :dev_mode, :created_at, :updated_at
39
65
  end
40
66
  end
41
67
  end
@@ -10,6 +10,12 @@ module AbacatePay
10
10
 
11
11
  DATETIME_PROPERTIES = %w[created_at updated_at].freeze
12
12
 
13
+ # @return [String, nil] Destination PIX key
14
+ attr_accessor :pix_key
15
+
16
+ # @return [String, nil] Destination PIX key type
17
+ attr_accessor :pix_key_type
18
+
13
19
  attr_reader :id, :amount, :external_id, :description,
14
20
  :status, :created_at, :updated_at
15
21
 
@@ -21,16 +21,28 @@ module AbacatePay
21
21
  raise ArgumentError, "Invalid datetime value: #{value}"
22
22
  end
23
23
 
24
- # Initialize an enum from a string or enum object
24
+ # Initialize an enum from a string or enum object.
25
+ #
26
+ # Unknown values pass through with a warning rather than raising. This
27
+ # method runs while parsing API responses, and AbacatePay adds statuses
28
+ # over time: rejecting an unrecognised one would turn every new value
29
+ # into a crash inside `list` and `get` for every integration at once.
30
+ # That happened with the coupon status `DISABLED` and the payment-link
31
+ # status `ACTIVE`, neither of which the SDK knew about.
32
+ #
33
+ # Use {AbacatePay::Enums} `validate!` directly when you want a hard
34
+ # failure on a value you are about to send.
25
35
  #
26
36
  # @param enum_class [Class] The enum class to initialize
27
37
  # @param value [String, Object] The value to initialize
28
- # @return [Object, nil] The initialized enum or nil
29
- # @raise [ArgumentError] If the value is invalid
38
+ # @return [Object, nil] The value, validated when known
30
39
  def initialize_enum(enum_class, value)
31
40
  return nil if value.nil? || value.empty?
41
+ return value if enum_class.valid?(value)
32
42
 
33
- enum_class.validate!(value)
43
+ warn "[AbacatePay] unknown #{enum_class.name.split("::").last} value #{value.inspect}. " \
44
+ "Passing it through; upgrade the gem if this is a new API value."
45
+ value
34
46
  end
35
47
 
36
48
  # Initialize a resource from a hash or resource object
@@ -66,6 +78,10 @@ module AbacatePay
66
78
  # @param data [Hash] The data to fill with
67
79
  # @return [void]
68
80
  def fill(data)
81
+ # Some endpoints answer a successful call with no payload, so the
82
+ # resource is built from nil. An empty object beats a NoMethodError.
83
+ return if data.nil?
84
+
69
85
  data.each do |key, value|
70
86
  property = camel_to_snake(key)
71
87
  next unless respond_to?("#{property}=", true)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AbacatePay
4
- VERSION = "1.2.1"
4
+ VERSION = "1.2.2"
5
5
  end
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.2.1
4
+ version: 1.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matheus Cardoso