mercadopago-sdk 3.2.1 → 3.3.0
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/.github/workflows/ci.yml +2 -2
- data/.github/workflows/release.yml +1 -1
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +22 -0
- data/Gemfile.lock +2 -2
- data/examples/payment/automatic_payment_credential_on_file.rb +194 -0
- data/lib/mercadopago/config/request_options.rb +30 -2
- data/lib/mercadopago/core/mp_base.rb +22 -7
- data/lib/mercadopago/errors/exceptions.rb +108 -0
- data/lib/mercadopago/errors/response.rb +58 -0
- data/lib/mercadopago/http/http_client.rb +45 -57
- data/lib/mercadopago/pagination/iterator.rb +75 -0
- data/lib/mercadopago/resources/card.rb +16 -0
- data/lib/mercadopago/resources/customer.rb +5 -0
- data/lib/mercadopago/resources/payment.rb +28 -0
- data/lib/mercadopago/resources/preapproval.rb +5 -0
- data/lib/mercadopago/resources/preference.rb +13 -0
- data/lib/mercadopago/resources/refund.rb +14 -0
- data/lib/mercadopago/resources/subscription.rb +63 -0
- data/lib/mercadopago/sdk.rb +5 -0
- data/lib/mercadopago/webhook/validator.rb +2 -1
- data/lib/mercadopago.rb +12 -4
- data/mercadopago.gemspec +1 -1
- data/tests/test_card.rb +8 -0
- data/tests/test_ergonomia.rb +123 -0
- data/tests/test_payment.rb +12 -0
- data/tests/test_preference.rb +7 -0
- data/tests/test_refund.rb +4 -0
- data/tests/test_subscription.rb +70 -0
- data/tests/test_webhook_signature_validator.rb +10 -2
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 518cbc2c53d00de1452bf210afff62915ba381a970d084ca42a848e5604fa582
|
|
4
|
+
data.tar.gz: 85468b9a03305eb33e4b053a4aae7149cbacd1ceb33df494e25bf63391b2bfea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 59839cc98cb80e2718b5694f05559b0a73fe23f04b953c0d172bb80b0609c220f673bd88952326eab551bcd52edd0beec0cb2173c188b9ea9cace45c5513b75b
|
|
7
|
+
data.tar.gz: 4e2e68a2fef69a84eafba9c8fa6b877d214f953e764d26525f48dca5cb812686195fe7fea80085704c8827de1d023fe385c366dede92156a98e680d51ae8dbce
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -13,13 +13,13 @@ jobs:
|
|
|
13
13
|
|
|
14
14
|
steps:
|
|
15
15
|
- name: Checkout code
|
|
16
|
-
uses: actions/checkout@
|
|
16
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
17
17
|
with:
|
|
18
18
|
ref: ${{ github.event.pull_request.head.ref }}
|
|
19
19
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
|
20
20
|
|
|
21
21
|
- name: Cache gems
|
|
22
|
-
uses: actions/cache@
|
|
22
|
+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
|
23
23
|
with:
|
|
24
24
|
path: vendor/bundle
|
|
25
25
|
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
|
data/.rubocop.yml
CHANGED
|
@@ -1 +1,9 @@
|
|
|
1
1
|
inherit_from: .rubocop_todo.yml
|
|
2
|
+
|
|
3
|
+
Metrics/AbcSize: {Max: 32}
|
|
4
|
+
Metrics/CyclomaticComplexity: {Max: 18}
|
|
5
|
+
Metrics/ParameterLists: {Max: 12}
|
|
6
|
+
Metrics/PerceivedComplexity: {Max: 18}
|
|
7
|
+
Style/ExplicitBlockArgument: {Enabled: false}
|
|
8
|
+
Style/NumericPredicate: {Enabled: false}
|
|
9
|
+
Style/TrivialAccessors: {Enabled: false}
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
This project follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|
6
6
|
and [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.3.0] - 2026-08-04
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **SDK ergonomics**: typed exceptions, configurable retry, and auto-pagination ([#162](https://github.com/mercadopago/sdk-ruby/pull/162))
|
|
13
|
+
- `MercadoPago::ApiError` now has 12 specific subtypes per HTTP status code
|
|
14
|
+
- Request options gain optional `max_retries`, `retry_on`, `initial_delay_ms`, `max_delay_ms` and `on_retry` callback
|
|
15
|
+
- New auto-pagination support on search endpoints
|
|
16
|
+
- **Missing API methods** — `disbursement_refund.list`, `advanced_payment.update`, `customer_card.update`, `payment.update` ([#161](https://github.com/mercadopago/sdk-ruby/pull/161))
|
|
17
|
+
- **CREDENTIAL_ON_FILE messaging fields** on Payment types ([#158](https://github.com/mercadopago/sdk-ruby/pull/158)): `first_transaction`, `storage`, `transaction_initiator`, `reference`
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Webhook `tolerance_seconds` unit mismatch — `ts` header value compared in seconds against a millisecond clock ([#163](https://github.com/mercadopago/sdk-ruby/pull/163))
|
|
22
|
+
- `constant_time_equals` error on multibyte v1 hash ([#163](https://github.com/mercadopago/sdk-ruby/pull/163))
|
|
23
|
+
|
|
24
|
+
### Dependencies
|
|
25
|
+
|
|
26
|
+
- Bump `json` gem ([#159](https://github.com/mercadopago/sdk-ruby/pull/159))
|
|
27
|
+
- Bump `actions/cache` to `v6.1.0` ([#160](https://github.com/mercadopago/sdk-ruby/pull/160))
|
|
28
|
+
- Bump `actions/checkout` to `v7.0.1` ([#157](https://github.com/mercadopago/sdk-ruby/pull/157))
|
|
29
|
+
|
|
8
30
|
## [3.2.0] - 2026-06-30
|
|
9
31
|
|
|
10
32
|
### Added
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
mercadopago-sdk (3.
|
|
4
|
+
mercadopago-sdk (3.3.0)
|
|
5
5
|
faraday (~> 2.0)
|
|
6
6
|
json (~> 2.5)
|
|
7
7
|
|
|
@@ -17,7 +17,7 @@ GEM
|
|
|
17
17
|
faraday-net_http (3.4.4)
|
|
18
18
|
net-http (~> 0.5)
|
|
19
19
|
io-console (0.8.2)
|
|
20
|
-
json (2.21.
|
|
20
|
+
json (2.21.2)
|
|
21
21
|
language_server-protocol (3.17.0.6)
|
|
22
22
|
lint_roller (1.1.0)
|
|
23
23
|
logger (1.7.0)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Example: Automatic Payments with CREDENTIAL_ON_FILE (COF).
|
|
2
|
+
#
|
|
3
|
+
# CREDENTIAL_ON_FILE replaces the deprecated SUBSCRIPTIONS type for recurring
|
|
4
|
+
# and unscheduled automatic payments. It covers three main scenarios:
|
|
5
|
+
#
|
|
6
|
+
# 1. CIT – Customer-Initiated Transaction (enrollment / first charge):
|
|
7
|
+
# The customer actively authorizes the initial payment and consents to
|
|
8
|
+
# storing their credentials for future merchant-initiated charges.
|
|
9
|
+
#
|
|
10
|
+
# 2. MIT – Merchant-Initiated Transaction (recurring charge):
|
|
11
|
+
# Subsequent charges triggered by the merchant without customer
|
|
12
|
+
# interaction (e.g. monthly subscription billing).
|
|
13
|
+
#
|
|
14
|
+
# 3. UCOF-CIT – Unscheduled COF Customer-Initiated Transaction:
|
|
15
|
+
# A one-off purchase where the customer uses previously stored
|
|
16
|
+
# credentials, but the amount / date was not pre-agreed.
|
|
17
|
+
#
|
|
18
|
+
# Reference: https://www.mercadopago.com/developers/en/reference
|
|
19
|
+
|
|
20
|
+
require_relative '../../lib/mercadopago'
|
|
21
|
+
|
|
22
|
+
sdk = Mercadopago::SDK.new('<ACCESS_TOKEN>')
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# 1. CIT — Customer-Initiated Transaction (enrollment)
|
|
26
|
+
#
|
|
27
|
+
# The customer completes the first payment and grants permission to store
|
|
28
|
+
# their card credentials for future recurring charges.
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
def create_cit_payment(sdk)
|
|
31
|
+
request = {
|
|
32
|
+
transaction_amount: 100.00,
|
|
33
|
+
token: '<CARD_TOKEN>',
|
|
34
|
+
description: 'Monthly subscription — enrollment',
|
|
35
|
+
installments: 1,
|
|
36
|
+
payment_method_id: '<PAYMENT_METHOD_ID>',
|
|
37
|
+
payer: {
|
|
38
|
+
email: '<PAYER_EMAIL>',
|
|
39
|
+
identification: {
|
|
40
|
+
type: '<ID_TYPE>',
|
|
41
|
+
number: '<ID_NUMBER>'
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
# CREDENTIAL_ON_FILE block for the enrollment (first) transaction.
|
|
45
|
+
point_of_interaction: {
|
|
46
|
+
linked_to: 'subscription',
|
|
47
|
+
transaction_data: {
|
|
48
|
+
# type: "CREDENTIAL_ON_FILE" — new credential-on-file payment type.
|
|
49
|
+
type: 'CREDENTIAL_ON_FILE',
|
|
50
|
+
# sub_type: "recurring" — charge is part of a recurring series.
|
|
51
|
+
sub_type: 'recurring',
|
|
52
|
+
# storage: "store" — instructs the processor to persist the credentials.
|
|
53
|
+
storage: 'store',
|
|
54
|
+
# transaction_initiator: "customer" — the customer triggered this charge.
|
|
55
|
+
transaction_initiator: 'customer',
|
|
56
|
+
# first_transaction: true — marks this as the enrollment / anchor payment.
|
|
57
|
+
first_transaction: true
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
custom_headers = {
|
|
63
|
+
'X-Idempotency-Key': '<SOME_UNIQUE_VALUE>'
|
|
64
|
+
}
|
|
65
|
+
custom_request_options = Mercadopago::RequestOptions.new(custom_headers: custom_headers)
|
|
66
|
+
|
|
67
|
+
result = sdk.payment.create(request, request_options: custom_request_options)
|
|
68
|
+
payment = result[:response]
|
|
69
|
+
puts "CIT payment created. ID: #{payment['id']}, status: #{payment['status']}"
|
|
70
|
+
payment['id'] # return payment ID to use as reference in subsequent MITs
|
|
71
|
+
rescue MercadoPago::MPApiException => e
|
|
72
|
+
puts "Status code: #{e.api_response.status_code}"
|
|
73
|
+
puts "Content: #{e.api_response.content}"
|
|
74
|
+
rescue StandardError => e
|
|
75
|
+
puts e.message
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
# 2. MIT — Merchant-Initiated Transaction (recurring monthly charge)
|
|
80
|
+
#
|
|
81
|
+
# The merchant charges the customer without customer interaction, using the
|
|
82
|
+
# credentials stored during the CIT enrollment. The CIT payment ID is
|
|
83
|
+
# provided as a reference to the original agreement.
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
def create_mit_payment(sdk, cit_payment_id)
|
|
86
|
+
request = {
|
|
87
|
+
transaction_amount: 100.00,
|
|
88
|
+
token: '<CARD_TOKEN>',
|
|
89
|
+
description: 'Monthly subscription — recurring charge',
|
|
90
|
+
installments: 1,
|
|
91
|
+
payment_method_id: '<PAYMENT_METHOD_ID>',
|
|
92
|
+
payer: {
|
|
93
|
+
email: '<PAYER_EMAIL>',
|
|
94
|
+
identification: {
|
|
95
|
+
type: '<ID_TYPE>',
|
|
96
|
+
number: '<ID_NUMBER>'
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
# CREDENTIAL_ON_FILE block for merchant-initiated recurring charges.
|
|
100
|
+
point_of_interaction: {
|
|
101
|
+
linked_to: 'subscription',
|
|
102
|
+
transaction_data: {
|
|
103
|
+
# type: "CREDENTIAL_ON_FILE" — same type as the enrollment.
|
|
104
|
+
type: 'CREDENTIAL_ON_FILE',
|
|
105
|
+
# sub_type: "recurring" — charge belongs to a recurring series.
|
|
106
|
+
sub_type: 'recurring',
|
|
107
|
+
# storage: "stored" — credentials were already stored in a prior CIT.
|
|
108
|
+
storage: 'stored',
|
|
109
|
+
# transaction_initiator: "merchant" — the merchant triggered this charge.
|
|
110
|
+
transaction_initiator: 'merchant',
|
|
111
|
+
# first_transaction: false — not the enrollment; uses stored credentials.
|
|
112
|
+
first_transaction: false,
|
|
113
|
+
# reference.id — ID of the CIT payment that anchors this agreement.
|
|
114
|
+
reference: {
|
|
115
|
+
id: cit_payment_id.to_s
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
custom_headers = {
|
|
122
|
+
'X-Idempotency-Key': '<SOME_UNIQUE_VALUE>'
|
|
123
|
+
}
|
|
124
|
+
custom_request_options = Mercadopago::RequestOptions.new(custom_headers: custom_headers)
|
|
125
|
+
|
|
126
|
+
result = sdk.payment.create(request, request_options: custom_request_options)
|
|
127
|
+
payment = result[:response]
|
|
128
|
+
puts "MIT payment created. ID: #{payment['id']}, status: #{payment['status']}"
|
|
129
|
+
rescue MercadoPago::MPApiException => e
|
|
130
|
+
puts "Status code: #{e.api_response.status_code}"
|
|
131
|
+
puts "Content: #{e.api_response.content}"
|
|
132
|
+
rescue StandardError => e
|
|
133
|
+
puts e.message
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
# 3. UCOF-CIT — Unscheduled COF Customer-Initiated Transaction
|
|
138
|
+
#
|
|
139
|
+
# The customer actively initiates a one-off purchase using previously stored
|
|
140
|
+
# credentials. The amount and date were not pre-agreed (unscheduled), but the
|
|
141
|
+
# customer is present and initiates the transaction themselves.
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
def create_ucof_cit_payment(sdk)
|
|
144
|
+
request = {
|
|
145
|
+
transaction_amount: 250.00,
|
|
146
|
+
token: '<CARD_TOKEN>',
|
|
147
|
+
description: 'One-off purchase with stored credentials',
|
|
148
|
+
installments: 1,
|
|
149
|
+
payment_method_id: '<PAYMENT_METHOD_ID>',
|
|
150
|
+
payer: {
|
|
151
|
+
email: '<PAYER_EMAIL>',
|
|
152
|
+
identification: {
|
|
153
|
+
type: '<ID_TYPE>',
|
|
154
|
+
number: '<ID_NUMBER>'
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
# CREDENTIAL_ON_FILE block for an unscheduled customer-initiated purchase.
|
|
158
|
+
point_of_interaction: {
|
|
159
|
+
linked_to: 'subscription',
|
|
160
|
+
transaction_data: {
|
|
161
|
+
# type: "CREDENTIAL_ON_FILE" — credential-on-file payment type.
|
|
162
|
+
type: 'CREDENTIAL_ON_FILE',
|
|
163
|
+
# sub_type: "unscheduled" — amount/date not pre-agreed; ad-hoc purchase.
|
|
164
|
+
sub_type: 'unscheduled',
|
|
165
|
+
# storage: "stored" — credentials already stored from a prior enrollment.
|
|
166
|
+
storage: 'stored',
|
|
167
|
+
# transaction_initiator: "customer" — the customer triggered this charge.
|
|
168
|
+
transaction_initiator: 'customer',
|
|
169
|
+
# first_transaction: false — uses previously stored credentials.
|
|
170
|
+
first_transaction: false
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
custom_headers = {
|
|
176
|
+
'X-Idempotency-Key': '<SOME_UNIQUE_VALUE>'
|
|
177
|
+
}
|
|
178
|
+
custom_request_options = Mercadopago::RequestOptions.new(custom_headers: custom_headers)
|
|
179
|
+
|
|
180
|
+
result = sdk.payment.create(request, request_options: custom_request_options)
|
|
181
|
+
payment = result[:response]
|
|
182
|
+
puts "UCOF-CIT payment created. ID: #{payment['id']}, status: #{payment['status']}"
|
|
183
|
+
rescue MercadoPago::MPApiException => e
|
|
184
|
+
puts "Status code: #{e.api_response.status_code}"
|
|
185
|
+
puts "Content: #{e.api_response.content}"
|
|
186
|
+
rescue StandardError => e
|
|
187
|
+
puts e.message
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Run all three scenarios sequentially.
|
|
191
|
+
# In production, CIT runs once at enrollment; MIT runs on each billing cycle.
|
|
192
|
+
cit_payment_id = create_cit_payment(sdk)
|
|
193
|
+
create_mit_payment(sdk, cit_payment_id) if cit_payment_id
|
|
194
|
+
create_ucof_cit_payment(sdk)
|
|
@@ -13,6 +13,9 @@ module Mercadopago
|
|
|
13
13
|
# opts = Mercadopago::RequestOptions.new(access_token: token, connection_timeout: 120.0)
|
|
14
14
|
# sdk.payment.get(123, request_options: opts)
|
|
15
15
|
class RequestOptions
|
|
16
|
+
DEFAULT_RETRY_ON = [429, 500, 502, 503, 504].freeze
|
|
17
|
+
DEFAULT_MAX_DELAY = 30_000
|
|
18
|
+
|
|
16
19
|
# @!attribute [r] access_token
|
|
17
20
|
# @return [String, nil] OAuth access token used for Bearer authentication
|
|
18
21
|
# @!attribute [r] connection_timeout
|
|
@@ -27,8 +30,18 @@ module Mercadopago
|
|
|
27
30
|
# @return [String, nil] MercadoPago platform identifier (x-platform-id header)
|
|
28
31
|
# @!attribute [r] max_retries
|
|
29
32
|
# @return [Integer] maximum automatic retries on transient HTTP errors (default: 3)
|
|
33
|
+
# @!attribute [r] initial_delay_ms
|
|
34
|
+
# @return [Integer, nil] initial backoff delay in ms (nil = SDK default)
|
|
35
|
+
# @!attribute [r] max_delay_ms
|
|
36
|
+
# @return [Integer] maximum backoff delay cap in ms (default: 30_000)
|
|
37
|
+
# @!attribute [r] jitter
|
|
38
|
+
# @return [Boolean] add random jitter to retry delay using SecureRandom
|
|
39
|
+
# @!attribute [r] retry_on
|
|
40
|
+
# @return [Array<Integer>, nil] HTTP status codes to retry (nil = DEFAULT_RETRY_ON)
|
|
41
|
+
# @!attribute [r] on_retry
|
|
42
|
+
# @return [Proc, nil] callback(attempt, error) invoked before each retry
|
|
30
43
|
attr_reader :access_token, :connection_timeout, :custom_headers, :corporation_id, :integrator_id,
|
|
31
|
-
:platform_id, :max_retries
|
|
44
|
+
:platform_id, :max_retries, :initial_delay_ms, :max_delay_ms, :jitter, :retry_on, :on_retry
|
|
32
45
|
|
|
33
46
|
# Builds a new request configuration.
|
|
34
47
|
#
|
|
@@ -39,6 +52,11 @@ module Mercadopago
|
|
|
39
52
|
# @param integrator_id [String, nil] integrator identifier for certified partners
|
|
40
53
|
# @param platform_id [String, nil] platform identifier for marketplace integrations
|
|
41
54
|
# @param max_retries [Integer] retry limit for transient failures (429, 5xx)
|
|
55
|
+
# @param initial_delay_ms [Integer, nil] initial backoff delay in ms
|
|
56
|
+
# @param max_delay_ms [Integer] maximum backoff delay cap in ms
|
|
57
|
+
# @param jitter [Boolean] add SecureRandom jitter to retry delay
|
|
58
|
+
# @param retry_on [Array<Integer>, nil] HTTP status codes to retry
|
|
59
|
+
# @param on_retry [Proc, nil] callback invoked before each retry
|
|
42
60
|
# @raise [TypeError] if any parameter is not the expected type
|
|
43
61
|
def initialize(access_token: nil,
|
|
44
62
|
connection_timeout: 60.0,
|
|
@@ -46,7 +64,12 @@ module Mercadopago
|
|
|
46
64
|
corporation_id: nil,
|
|
47
65
|
integrator_id: nil,
|
|
48
66
|
platform_id: nil,
|
|
49
|
-
max_retries: 3
|
|
67
|
+
max_retries: 3,
|
|
68
|
+
initial_delay_ms: nil,
|
|
69
|
+
max_delay_ms: DEFAULT_MAX_DELAY,
|
|
70
|
+
jitter: false,
|
|
71
|
+
retry_on: nil,
|
|
72
|
+
on_retry: nil)
|
|
50
73
|
self.access_token = access_token
|
|
51
74
|
self.connection_timeout = connection_timeout
|
|
52
75
|
self.custom_headers = custom_headers
|
|
@@ -54,6 +77,11 @@ module Mercadopago
|
|
|
54
77
|
self.integrator_id = integrator_id
|
|
55
78
|
self.platform_id = platform_id
|
|
56
79
|
self.max_retries = max_retries
|
|
80
|
+
@initial_delay_ms = initial_delay_ms
|
|
81
|
+
@max_delay_ms = max_delay_ms
|
|
82
|
+
@jitter = jitter
|
|
83
|
+
@retry_on = retry_on
|
|
84
|
+
@on_retry = on_retry
|
|
57
85
|
|
|
58
86
|
@config = Config.new
|
|
59
87
|
end
|
|
@@ -81,8 +81,16 @@ module Mercadopago
|
|
|
81
81
|
request_options = _check_request_options(request_options)
|
|
82
82
|
headers = _check_headers(request_options)
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
MPResponse.new(
|
|
85
|
+
@http_client.get(url: @config.api_base_url + uri, headers: headers, params: filters,
|
|
86
|
+
timeout: request_options.connection_timeout,
|
|
87
|
+
maxretries: request_options.max_retries,
|
|
88
|
+
retry_on: request_options.retry_on,
|
|
89
|
+
initial_delay_ms: request_options.initial_delay_ms,
|
|
90
|
+
max_delay_ms: request_options.max_delay_ms,
|
|
91
|
+
jitter: request_options.jitter,
|
|
92
|
+
on_retry: request_options.on_retry)
|
|
93
|
+
)
|
|
86
94
|
end
|
|
87
95
|
|
|
88
96
|
# Performs a POST request against the MercadoPago API.
|
|
@@ -101,7 +109,10 @@ module Mercadopago
|
|
|
101
109
|
headers = _check_headers(request_options, { 'Content-Type': @config.mime_json })
|
|
102
110
|
payload = data&.to_json
|
|
103
111
|
|
|
104
|
-
|
|
112
|
+
MPResponse.new(
|
|
113
|
+
@http_client.post(url: @config.api_base_url + uri, data: payload, headers: headers,
|
|
114
|
+
timeout: request_options.connection_timeout)
|
|
115
|
+
)
|
|
105
116
|
end
|
|
106
117
|
|
|
107
118
|
# Performs a PUT request against the MercadoPago API.
|
|
@@ -119,8 +130,10 @@ module Mercadopago
|
|
|
119
130
|
request_options = _check_request_options(request_options)
|
|
120
131
|
headers = _check_headers(request_options, { 'Content-Type': @config.mime_json })
|
|
121
132
|
|
|
122
|
-
|
|
123
|
-
|
|
133
|
+
MPResponse.new(
|
|
134
|
+
@http_client.put(url: @config.api_base_url + uri, data: data.to_json, headers: headers,
|
|
135
|
+
timeout: request_options.connection_timeout)
|
|
136
|
+
)
|
|
124
137
|
end
|
|
125
138
|
|
|
126
139
|
# Performs a DELETE request against the MercadoPago API.
|
|
@@ -132,8 +145,10 @@ module Mercadopago
|
|
|
132
145
|
request_options = _check_request_options(request_options)
|
|
133
146
|
headers = _check_headers(request_options)
|
|
134
147
|
|
|
135
|
-
|
|
136
|
-
|
|
148
|
+
MPResponse.new(
|
|
149
|
+
@http_client.delete(url: @config.api_base_url + uri, headers: headers,
|
|
150
|
+
timeout: request_options.connection_timeout)
|
|
151
|
+
)
|
|
137
152
|
end
|
|
138
153
|
end
|
|
139
154
|
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mercadopago
|
|
4
|
+
# Base exception for all MercadoPago API errors.
|
|
5
|
+
#
|
|
6
|
+
# All subtypes inherit from this class, preserving backward-compatible
|
|
7
|
+
# +rescue MercadoPagoError+ patterns while enabling specific handling
|
|
8
|
+
# per HTTP status code.
|
|
9
|
+
#
|
|
10
|
+
# @attr_reader [Integer] status_code HTTP status code (e.g. 400, 401, 500)
|
|
11
|
+
# @attr_reader [String] error Machine-readable error code from the API body
|
|
12
|
+
# @attr_reader [Array] causes List of detailed cause hashes from the API body
|
|
13
|
+
# @attr_reader [String, nil] request_id x-request-id header for support diagnostics
|
|
14
|
+
class MercadoPagoError < StandardError
|
|
15
|
+
attr_reader :status_code, :error, :causes, :request_id
|
|
16
|
+
|
|
17
|
+
# @param status_code [Integer] HTTP status code
|
|
18
|
+
# @param response_body [Hash] parsed API error body
|
|
19
|
+
# @param request_id [String, nil] value of the x-request-id response header
|
|
20
|
+
def initialize(status_code, response_body = nil, request_id = nil)
|
|
21
|
+
body = (response_body.is_a?(Hash) ? response_body : {})
|
|
22
|
+
super(body['message'] || body[:message] || body['error'] || body[:error] || 'MercadoPago API error')
|
|
23
|
+
@status_code = status_code
|
|
24
|
+
@error = body['error'] || body[:error] || ''
|
|
25
|
+
@causes = body['cause'] || body[:cause] || []
|
|
26
|
+
@request_id = request_id
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# HTTP 400 Bad Request — validation or syntax error.
|
|
31
|
+
class MPBadRequestError < MercadoPagoError; end
|
|
32
|
+
|
|
33
|
+
# HTTP 401 Unauthorized — missing or invalid credentials.
|
|
34
|
+
class MPAuthenticationError < MercadoPagoError; end
|
|
35
|
+
|
|
36
|
+
# HTTP 402 Payment Required — transaction processing error (AP/Orders).
|
|
37
|
+
class MPPaymentError < MercadoPagoError; end
|
|
38
|
+
|
|
39
|
+
# HTTP 403 Forbidden.
|
|
40
|
+
class MPForbiddenError < MercadoPagoError; end
|
|
41
|
+
|
|
42
|
+
# HTTP 404 Not Found.
|
|
43
|
+
class MPNotFoundError < MercadoPagoError; end
|
|
44
|
+
|
|
45
|
+
# HTTP 409 Conflict — idempotency-key conflict or state-machine conflict.
|
|
46
|
+
class MPIdempotencyError < MercadoPagoError; end
|
|
47
|
+
|
|
48
|
+
# HTTP 422 Unprocessable Entity — business-rule violation.
|
|
49
|
+
class MPValidationError < MercadoPagoError; end
|
|
50
|
+
|
|
51
|
+
# HTTP 423 Locked — idempotency key temporarily locked (retryable).
|
|
52
|
+
class MPResourceLockedError < MercadoPagoError; end
|
|
53
|
+
|
|
54
|
+
# HTTP 424 Failed Dependency — internal dependency failure (retryable).
|
|
55
|
+
class MPDependencyError < MercadoPagoError; end
|
|
56
|
+
|
|
57
|
+
# HTTP 429 Too Many Requests.
|
|
58
|
+
# Exposes +retry_after+ (seconds) from the +Retry-After+ response header.
|
|
59
|
+
#
|
|
60
|
+
# @attr_reader [Integer, nil] retry_after seconds to wait before retrying
|
|
61
|
+
class MPRateLimitError < MercadoPagoError
|
|
62
|
+
attr_reader :retry_after
|
|
63
|
+
|
|
64
|
+
def initialize(status_code, response_body = nil, retry_after = nil, request_id = nil)
|
|
65
|
+
super(status_code, response_body, request_id)
|
|
66
|
+
@retry_after = retry_after
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# HTTP 5xx Server Error.
|
|
71
|
+
class MPServerError < MercadoPagoError; end
|
|
72
|
+
|
|
73
|
+
# Transport-level or network error (timeout, DNS failure, SSL error).
|
|
74
|
+
class MPConnectionError < MercadoPagoError
|
|
75
|
+
def initialize(cause)
|
|
76
|
+
super(0, { 'error' => 'connection_error', 'message' => cause.to_s })
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @!visibility private
|
|
81
|
+
STATUS_MAP = {
|
|
82
|
+
400 => MPBadRequestError,
|
|
83
|
+
401 => MPAuthenticationError,
|
|
84
|
+
402 => MPPaymentError,
|
|
85
|
+
403 => MPForbiddenError,
|
|
86
|
+
404 => MPNotFoundError,
|
|
87
|
+
409 => MPIdempotencyError,
|
|
88
|
+
422 => MPValidationError,
|
|
89
|
+
423 => MPResourceLockedError,
|
|
90
|
+
424 => MPDependencyError
|
|
91
|
+
}.freeze
|
|
92
|
+
|
|
93
|
+
# Factory: maps an HTTP status code to the most specific exception subtype.
|
|
94
|
+
#
|
|
95
|
+
# @param status_code [Integer] HTTP status code from the API response
|
|
96
|
+
# @param response_body [Hash] parsed API error body
|
|
97
|
+
# @param retry_after [Integer, nil] seconds from Retry-After header (429 only)
|
|
98
|
+
# @return [MercadoPagoError] the appropriate subtype instance
|
|
99
|
+
def self.build_error(status_code, response_body = nil, retry_after = nil)
|
|
100
|
+
return MPRateLimitError.new(status_code, response_body, retry_after) if status_code == 429
|
|
101
|
+
|
|
102
|
+
klass = STATUS_MAP[status_code]
|
|
103
|
+
return klass.new(status_code, response_body) if klass
|
|
104
|
+
return MPServerError.new(status_code, response_body) if status_code >= 500
|
|
105
|
+
|
|
106
|
+
MercadoPagoError.new(status_code, response_body)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mercadopago
|
|
4
|
+
# Backward-compatible Hash wrapper for MercadoPago API responses.
|
|
5
|
+
#
|
|
6
|
+
# Wraps the raw +{ status:, response: }+ hash returned by every
|
|
7
|
+
# {MPBase} helper. Existing code that reads +result[:status]+ or
|
|
8
|
+
# +result[:response]+ continues to work unchanged.
|
|
9
|
+
#
|
|
10
|
+
# New code may call {#raise_for_status!} to get a typed exception or
|
|
11
|
+
# use the convenience accessors {#success?}, {#error_message}, {#error_causes}.
|
|
12
|
+
class MPResponse < Hash
|
|
13
|
+
# @param raw [Hash] the raw response hash with +:status+ and +:response+ keys
|
|
14
|
+
# @param request_id [String, nil] value of the +x-request-id+ response header
|
|
15
|
+
def initialize(raw = {}, request_id: nil)
|
|
16
|
+
super()
|
|
17
|
+
update(raw)
|
|
18
|
+
@request_id = request_id
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Integer] HTTP status code
|
|
22
|
+
def status_code
|
|
23
|
+
self[:status] || self['status'] || 0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [Boolean] true for 2xx status codes
|
|
27
|
+
def success?
|
|
28
|
+
status_code >= 200 && status_code < 300
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [String] human-readable error message from the API body
|
|
32
|
+
def error_message
|
|
33
|
+
body = self[:response] || self['response'] || {}
|
|
34
|
+
body.is_a?(Hash) ? (body['message'] || body[:message] || '') : ''
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Array] cause list from the API body
|
|
38
|
+
def error_causes
|
|
39
|
+
body = self[:response] || self['response'] || {}
|
|
40
|
+
body.is_a?(Hash) ? (body['cause'] || body[:cause] || []) : []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [String, nil] x-request-id header value
|
|
44
|
+
def request_id
|
|
45
|
+
@request_id
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Raises a typed {MercadoPagoError} subclass when the response status is not 2xx.
|
|
49
|
+
#
|
|
50
|
+
# @raise [MercadoPagoError] (or subtype) when status >= 300
|
|
51
|
+
def raise_for_status!
|
|
52
|
+
return if success?
|
|
53
|
+
|
|
54
|
+
body = self[:response] || self['response'] || {}
|
|
55
|
+
raise Mercadopago.build_error(status_code, body)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|