abacatepay-ruby 1.0.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +18 -0
  3. data/CHANGELOG.md +132 -12
  4. data/README.md +122 -20
  5. data/abacatepay-ruby.gemspec +2 -1
  6. data/lib/abacate_pay/clients/billing_client.rb +13 -3
  7. data/lib/abacate_pay/clients/checkout_client.rb +25 -4
  8. data/lib/abacate_pay/clients/client.rb +171 -16
  9. data/lib/abacate_pay/clients/coupon_client.rb +1 -1
  10. data/lib/abacate_pay/clients/customer_client.rb +1 -1
  11. data/lib/abacate_pay/clients/payment_link_client.rb +3 -3
  12. data/lib/abacate_pay/clients/payout_client.rb +1 -1
  13. data/lib/abacate_pay/clients/pix_client.rb +1 -1
  14. data/lib/abacate_pay/clients/product_client.rb +1 -1
  15. data/lib/abacate_pay/clients/store_client.rb +3 -1
  16. data/lib/abacate_pay/clients/subscription_client.rb +36 -1
  17. data/lib/abacate_pay/clients/transparent_client.rb +78 -24
  18. data/lib/abacate_pay/clients/webhook_client.rb +2 -2
  19. data/lib/abacate_pay/clients.rb +1 -1
  20. data/lib/abacate_pay/collection.rb +98 -0
  21. data/lib/abacate_pay/configuration.rb +20 -4
  22. data/lib/abacate_pay/enums/billings/methods.rb +8 -1
  23. data/lib/abacate_pay/enums/webhooks/event_types.rb +7 -0
  24. data/lib/abacate_pay/resources/checkouts.rb +10 -2
  25. data/lib/abacate_pay/resources/customers.rb +62 -21
  26. data/lib/abacate_pay/resources/resource.rb +1 -1
  27. data/lib/abacate_pay/resources/transparents.rb +29 -5
  28. data/lib/abacate_pay/resources/webhook_endpoints.rb +1 -1
  29. data/lib/abacate_pay/resources.rb +1 -1
  30. data/lib/abacate_pay/version.rb +1 -1
  31. data/lib/abacate_pay/webhooks.rb +53 -7
  32. data/lib/abacate_pay.rb +1 -0
  33. data/lib/abacatepay-ruby.rb +14 -0
  34. metadata +19 -3
@@ -10,12 +10,19 @@ module AbacatePay
10
10
  # PIX payment method.
11
11
  # @return [String] Represents the PIX payment method, a popular instant payment system in Brazil
12
12
  PIX = "PIX"
13
+
14
+ # @return [String] Credit card payment
13
15
  CARD = "CARD"
14
16
 
17
+ # Boleto bancário. Supports a due date and late-payment interest/fine -
18
+ # see Resources::Checkouts#due_date, #interest and #fine.
19
+ # @return [String] Boleto payment method
20
+ BOLETO = "BOLETO"
21
+
15
22
  # Gets all valid method values
16
23
  # @return [Array<String>] List of all valid payment methods
17
24
  def self.values
18
- [PIX, CARD]
25
+ [PIX, CARD, BOLETO]
19
26
  end
20
27
 
21
28
  # Validates if a given value is a valid method
@@ -13,6 +13,12 @@ module AbacatePay
13
13
  SUBSCRIPTION_COMPLETED = "subscription.completed"
14
14
  SUBSCRIPTION_RENEWED = "subscription.renewed"
15
15
  SUBSCRIPTION_CANCELLED = "subscription.cancelled"
16
+
17
+ # Recurring charge failed, the dunning signal. Without handling this,
18
+ # a failing subscription looks identical to a healthy one.
19
+ SUBSCRIPTION_PAYMENT_FAILED = "subscription.payment_failed"
20
+
21
+ SUBSCRIPTION_TRIAL_STARTED = "subscription.trial_started"
16
22
  TRANSFER_COMPLETED = "transfer.completed"
17
23
  TRANSFER_FAILED = "transfer.failed"
18
24
  PAYOUT_COMPLETED = "payout.completed"
@@ -23,6 +29,7 @@ module AbacatePay
23
29
  CHECKOUT_COMPLETED, CHECKOUT_REFUNDED, CHECKOUT_DISPUTED,
24
30
  TRANSPARENT_COMPLETED, TRANSPARENT_REFUNDED, TRANSPARENT_DISPUTED,
25
31
  SUBSCRIPTION_COMPLETED, SUBSCRIPTION_RENEWED, SUBSCRIPTION_CANCELLED,
32
+ SUBSCRIPTION_PAYMENT_FAILED, SUBSCRIPTION_TRIAL_STARTED,
26
33
  TRANSFER_COMPLETED, TRANSFER_FAILED,
27
34
  PAYOUT_COMPLETED, PAYOUT_FAILED
28
35
  ]
@@ -20,7 +20,13 @@ module AbacatePay
20
20
 
21
21
  attr_reader :id, :url, :amount, :status, :dev_mode, :methods,
22
22
  :products, :metadata, :customer, :coupons, :external_id,
23
- :frequency, :created_at, :updated_at
23
+ :frequency, :created_at, :updated_at,
24
+ # BOLETO-only: due date (YYYY-MM-DD) plus late-payment charges.
25
+ :due_date, :interest, :fine,
26
+ # CARD-only: maximum number of instalments offered.
27
+ :max_installments,
28
+ # Order bump shown at checkout, and free-form merchant data.
29
+ :up_sell_product_id, :custom_metadata
24
30
 
25
31
  def initialize(data)
26
32
  fill(data)
@@ -70,7 +76,9 @@ module AbacatePay
70
76
 
71
77
  attr_writer :id, :url, :amount, :status, :dev_mode, :methods,
72
78
  :products, :metadata, :customer, :coupons, :external_id,
73
- :frequency, :created_at, :updated_at
79
+ :frequency, :created_at, :updated_at,
80
+ :due_date, :interest, :fine, :max_installments,
81
+ :up_sell_product_id, :custom_metadata
74
82
  end
75
83
  end
76
84
  end
@@ -2,45 +2,86 @@
2
2
 
3
3
  module AbacatePay
4
4
  module Resources
5
- # Represents a customer resource in the AbacatePay payment system.
5
+ # Represents a customer in the AbacatePay system.
6
6
  #
7
- # This class handles customer data including identification and metadata,
8
- # providing a structured way to manage customer information within the system.
7
+ # The API returns the customer fields at the top level of the object:
8
+ #
9
+ # { "id": "cust_...", "name": "Ana", "email": "ana@example.com",
10
+ # "cellphone": "...", "taxId": "...", "metadata": {} }
11
+ #
12
+ # Earlier versions of this class only mapped `id` and a nested `metadata`,
13
+ # so every other field was silently dropped and `customer.metadata.name`
14
+ # came back nil for real API data.
15
+ #
16
+ # Both shapes work now: the fields are exposed directly, and `metadata`
17
+ # keeps answering for code written against the previous interface.
9
18
  class Customers < Resource
10
- # Maps property names to their corresponding resource classes
11
19
  RESOURCE_PROPERTIES = {
12
20
  metadata: "AbacatePay::Resources::Customers::Metadata"
13
21
  }.freeze
14
22
 
15
- # @return [String, nil] The unique identifier for the customer
16
- attr_reader :id
23
+ # Fields the API sends inside the customer object.
24
+ IDENTITY_FIELDS = %i[name email cellphone tax_id].freeze
17
25
 
18
- # @return [Metadata, nil] Additional customer information
19
- attr_reader :metadata
26
+ attr_reader :id, :country, :zip_code, :dev_mode
20
27
 
21
- # Creates a new Customer instance
22
- #
23
- # @param data [Hash] The raw customer data
24
- # @raise [ArgumentError] If the data is invalid
28
+ # @return [String, nil] Customer's name
29
+ attr_reader :name
30
+
31
+ # @return [String, nil] Customer's email address
32
+ attr_reader :email
33
+
34
+ # @return [String, nil] Customer's cellphone number
35
+ attr_reader :cellphone
36
+
37
+ # @return [String, nil] Customer's tax identification number
38
+ attr_reader :tax_id
39
+
40
+ # @param data [Hash] The customer properties
25
41
  def initialize(data)
26
42
  fill(data)
27
43
  end
28
44
 
29
- private
45
+ # @return [Boolean, nil] Whether this customer belongs to Dev mode
46
+ def dev_mode?
47
+ @dev_mode
48
+ end
30
49
 
31
- # Process a value based on its property
50
+ # The identity fields, wrapped.
32
51
  #
52
+ # Kept because `customer.metadata.name` is what the README documented and
53
+ # what existing integrations call. When the API sends the fields at the
54
+ # top level, this builds the wrapper from them rather than returning the
55
+ # empty object the API puts in `metadata`.
56
+ #
57
+ # @return [Customers::Metadata, nil]
58
+ def metadata
59
+ return @metadata if metadata_populated?(@metadata)
60
+
61
+ identity = IDENTITY_FIELDS.to_h { |field| [field, public_send(field)] }.compact
62
+ return @metadata if identity.empty?
63
+
64
+ Customers::Metadata.new(identity)
65
+ end
66
+
67
+ private
68
+
69
+ # @param metadata [Customers::Metadata, nil]
70
+ # @return [Boolean] Whether the API actually filled the nested object
71
+ def metadata_populated?(metadata)
72
+ return false if metadata.nil?
73
+
74
+ IDENTITY_FIELDS.any? { |field| metadata.public_send(field) }
75
+ end
76
+
33
77
  # @param property [String] The property name
34
- # @param value [Object] The value to process
78
+ # @param value [Object] The raw value
35
79
  # @return [Object] The processed value
36
80
  def process_value(property, value)
37
81
  return nil if value.nil?
38
82
 
39
83
  if RESOURCE_PROPERTIES.key?(property.to_sym)
40
- initialize_resource(
41
- Object.const_get(RESOURCE_PROPERTIES[property.to_sym]),
42
- value
43
- )
84
+ initialize_resource(Object.const_get(RESOURCE_PROPERTIES[property.to_sym]), value)
44
85
  else
45
86
  value
46
87
  end
@@ -48,8 +89,8 @@ module AbacatePay
48
89
 
49
90
  protected
50
91
 
51
- # @!visibility private
52
- attr_writer :id, :metadata
92
+ attr_writer :id, :metadata, :name, :email, :cellphone, :tax_id,
93
+ :country, :zip_code, :dev_mode
53
94
  end
54
95
  end
55
96
  end
@@ -51,7 +51,7 @@ module AbacatePay
51
51
  resource_class.new(value)
52
52
  end
53
53
 
54
- # Default value processor returns the value as-is.
54
+ # Default value processor, returns the value as-is.
55
55
  # Subclasses override this to handle enums, datetimes, and nested resources.
56
56
  #
57
57
  # @param _property [String] The property name
@@ -2,22 +2,44 @@
2
2
 
3
3
  module AbacatePay
4
4
  module Resources
5
- # Represents a transparent PIX checkout in the AbacatePay system.
5
+ # Represents a transparent checkout (PIX or boleto) in the AbacatePay system.
6
6
  class Transparents < Resource
7
7
  RESOURCE_PROPERTIES = {
8
8
  customer: "AbacatePay::Resources::Customers"
9
9
  }.freeze
10
10
 
11
- DATETIME_PROPERTIES = %w[created_at updated_at].freeze
11
+ DATETIME_PROPERTIES = %w[created_at updated_at expires_at].freeze
12
12
 
13
+ # qr_code and qr_code_image are defined below rather than generated here:
14
+ # they fall back to the br_code fields the API actually sends.
13
15
  attr_reader :id, :amount, :status, :method, :description,
14
- :expires_in, :qr_code, :qr_code_image, :customer,
15
- :metadata, :dev_mode, :created_at, :updated_at
16
+ :expires_in, :customer,
17
+ :metadata, :dev_mode, :created_at, :updated_at,
18
+ # Boleto: due date sent on create, plus the payment slip the
19
+ # API returns: digitable line, viewing URL, and the PIX
20
+ # fallback issued for the same charge.
21
+ :due_date, :bar_code, :url, :br_code, :br_code_base64,
22
+ :expires_at, :platform_fee, :receipt_url
16
23
 
17
24
  def initialize(data)
18
25
  fill(data)
19
26
  end
20
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
+
21
43
  def dev_mode?
22
44
  @dev_mode
23
45
  end
@@ -40,7 +62,9 @@ module AbacatePay
40
62
 
41
63
  attr_writer :id, :amount, :status, :method, :description,
42
64
  :expires_in, :qr_code, :qr_code_image, :customer,
43
- :metadata, :dev_mode, :created_at, :updated_at
65
+ :metadata, :dev_mode, :created_at, :updated_at,
66
+ :due_date, :bar_code, :url, :br_code, :br_code_base64,
67
+ :expires_at, :platform_fee, :receipt_url
44
68
  end
45
69
  end
46
70
  end
@@ -4,7 +4,7 @@ module AbacatePay
4
4
  module Resources
5
5
  # Represents a registered webhook endpoint in the AbacatePay system.
6
6
  #
7
- # This is the endpoint *registration* the URL AbacatePay delivers events
7
+ # This is the endpoint *registration*, the URL AbacatePay delivers events
8
8
  # to. For verifying and parsing an inbound delivery, see {AbacatePay::Webhooks}.
9
9
  class WebhookEndpoints < Resource
10
10
  # @return [String, nil] Webhook ID
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Resource must load first every other resource inherits from it.
3
+ # Resource must load first: every other resource inherits from it.
4
4
  require "abacate_pay/resources/resource"
5
5
 
6
6
  require "abacate_pay/resources/billings"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AbacatePay
4
- VERSION = "1.0.0"
4
+ VERSION = "1.2.1"
5
5
  end
@@ -17,33 +17,65 @@ module AbacatePay
17
17
  # Raised when a webhook body is not a JSON object the SDK can interpret.
18
18
  class PayloadError < AbacatePay::Error; end
19
19
 
20
- # Verifies a webhook signature using HMAC-SHA256.
20
+ # AbacatePay signs every delivery with this fixed key, published at
21
+ # https://docs.abacatepay.com/pages/webhooks/security and hard-coded in the
22
+ # Node, Python and Go samples there.
23
+ #
24
+ # It is public and global, so it proves only that the body was not altered
25
+ # in transit, it does NOT prove the request came from AbacatePay, since
26
+ # anyone can compute a valid signature with it. Origin is authenticated by
27
+ # the `webhookSecret` query parameter; see {verify_secret!}. Use both.
28
+ PUBLIC_KEY = "t9dXRhHHo3yDEj5pVDYz0frf7q6bMKyMRmxxCPIPp3RCplBfXRxqlC6ZpiWmOqj4L63qEaeUOtrCI8P0VMU" \
29
+ "go6iIga2ri9ogaHFs0WIIywSMg0q7RmBfybe1E5XJcfC4IW3alNqym0tXoAKkzvfEjZxV6bE0oG2zJrNNYmU" \
30
+ "CKZyV0KZ3JS8Votf9EAWWYdiDkMkpbMdPggfh1EqHlVkMiTady6jOR3hyzGEHrIz2Ret0xHKMbiqkr9HS1Jh" \
31
+ "NHDX9"
32
+
33
+ # Verifies the `X-Webhook-Signature` header: HMAC-SHA256 over the raw body,
34
+ # base64-encoded.
21
35
  #
22
36
  # @param payload [String] The raw request body
23
37
  # @param signature [String] The X-Webhook-Signature header value
24
- # @param secret [String] Your webhook secret/public key
38
+ # @param secret [String] HMAC key. Defaults to {PUBLIC_KEY}, which is what
39
+ # AbacatePay signs with.
25
40
  # @return [true] if signature is valid
26
41
  # @raise [SignatureError] if the signature is missing or invalid
27
- def self.verify!(payload:, signature:, secret:)
42
+ def self.verify!(payload:, signature:, secret: PUBLIC_KEY)
28
43
  raise SignatureError, "Missing webhook signature" if signature.nil? || signature.to_s.empty?
29
44
  raise SignatureError, "Missing webhook secret" if secret.nil? || secret.to_s.empty?
30
45
 
31
- expected = OpenSSL::HMAC.hexdigest("SHA256", secret.to_s, payload.to_s)
46
+ expected = base64_hmac(secret.to_s, payload.to_s)
32
47
  raise SignatureError, "Invalid webhook signature" unless secure_compare(expected, signature.to_s)
33
48
 
34
49
  true
35
50
  end
36
51
 
52
+ # Verifies the `webhookSecret` query parameter, which is what actually
53
+ # authenticates the request as coming from AbacatePay.
54
+ #
55
+ # The HMAC signature alone cannot do this: it is computed with a public key.
56
+ #
57
+ # @param received [String] The `webhookSecret` query parameter as received
58
+ # @param expected [String] The secret you configured on the webhook
59
+ # @return [true] if they match
60
+ # @raise [SignatureError] if either is missing or they differ
61
+ def self.verify_secret!(received:, expected:)
62
+ raise SignatureError, "Missing webhook secret parameter" if received.nil? || received.to_s.empty?
63
+ raise SignatureError, "Missing expected webhook secret" if expected.nil? || expected.to_s.empty?
64
+ raise SignatureError, "Invalid webhook secret" unless secure_compare(expected.to_s, received.to_s)
65
+
66
+ true
67
+ end
68
+
37
69
  # Checks if a webhook signature is valid.
38
70
  #
39
- # Never raises for untrusted input a missing header, an empty secret, or a
71
+ # Never raises for untrusted input, a missing header, an empty secret, or a
40
72
  # forged signature all return false.
41
73
  #
42
74
  # @param payload [String] The raw request body
43
75
  # @param signature [String] The X-Webhook-Signature header value
44
76
  # @param secret [String] Your webhook secret/public key
45
77
  # @return [Boolean]
46
- def self.valid?(payload:, signature:, secret:)
78
+ def self.valid?(payload:, signature:, secret: PUBLIC_KEY)
47
79
  verify!(payload: payload, signature: signature, secret: secret)
48
80
  true
49
81
  rescue SignatureError
@@ -78,11 +110,25 @@ module AbacatePay
78
110
  # @return [Event] The verified, parsed event
79
111
  # @raise [SignatureError] if the signature is missing or invalid
80
112
  # @raise [PayloadError] if the body is not a JSON object
81
- def self.construct_event(payload:, signature:, secret:)
113
+ def self.construct_event(payload:, signature:, secret: PUBLIC_KEY)
82
114
  verify!(payload: payload, signature: signature, secret: secret)
83
115
  parse(payload)
84
116
  end
85
117
 
118
+ # Base64-encoded HMAC-SHA256, matching what AbacatePay sends.
119
+ #
120
+ # Uses Array#pack rather than the base64 gem: base64 stopped being a default
121
+ # gem in Ruby 3.4, and requiring it would add a runtime dependency for one
122
+ # call.
123
+ #
124
+ # @param secret [String] The HMAC key
125
+ # @param payload [String] The raw body
126
+ # @return [String] The base64 signature
127
+ def self.base64_hmac(secret, payload)
128
+ [OpenSSL::HMAC.digest("SHA256", secret, payload)].pack("m0")
129
+ end
130
+ private_class_method :base64_hmac
131
+
86
132
  # Constant-time comparison to prevent timing attacks.
87
133
  #
88
134
  # @param expected [String] The signature computed from the payload
data/lib/abacate_pay.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "abacate_pay/version"
4
4
  require "abacate_pay/configuration"
5
+ require "abacate_pay/collection"
5
6
 
6
7
  # Main module for AbacatePay SDK integration
7
8
  module AbacatePay
@@ -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.0.0
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matheus Cardoso
@@ -29,6 +29,20 @@ dependencies:
29
29
  - - ">="
30
30
  - !ruby/object:Gem::Version
31
31
  version: 2.14.3
32
+ - !ruby/object:Gem::Dependency
33
+ name: faraday-retry
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '2.3'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '2.3'
32
46
  - !ruby/object:Gem::Dependency
33
47
  name: bundler-audit
34
48
  requirement: !ruby/object:Gem::Requirement
@@ -91,14 +105,14 @@ dependencies:
91
105
  requirements:
92
106
  - - "~>"
93
107
  - !ruby/object:Gem::Version
94
- version: '0.22'
108
+ version: '1.0'
95
109
  type: :development
96
110
  prerelease: false
97
111
  version_requirements: !ruby/object:Gem::Requirement
98
112
  requirements:
99
113
  - - "~>"
100
114
  - !ruby/object:Gem::Version
101
- version: '0.22'
115
+ version: '1.0'
102
116
  description: The easiest way to integrate your Ruby application with AbacatePay Gateway
103
117
  for payments, subscriptions, PIX transfers, and more.
104
118
  email:
@@ -130,6 +144,7 @@ files:
130
144
  - lib/abacate_pay/clients/subscription_client.rb
131
145
  - lib/abacate_pay/clients/transparent_client.rb
132
146
  - lib/abacate_pay/clients/webhook_client.rb
147
+ - lib/abacate_pay/collection.rb
133
148
  - lib/abacate_pay/configuration.rb
134
149
  - lib/abacate_pay/enums.rb
135
150
  - lib/abacate_pay/enums/billings/frequencies.rb
@@ -163,6 +178,7 @@ files:
163
178
  - lib/abacate_pay/version.rb
164
179
  - lib/abacate_pay/webhooks.rb
165
180
  - lib/abacate_pay/webhooks/event.rb
181
+ - lib/abacatepay-ruby.rb
166
182
  homepage: https://www.abacatepay.com/
167
183
  licenses:
168
184
  - MIT