mercadopago-sdk 3.2.1 → 3.4.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +35 -33
  3. data/.github/workflows/release.yml +5 -8
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +8 -0
  6. data/.rubocop_todo.yml +6 -0
  7. data/CHANGELOG.md +38 -0
  8. data/Gemfile.lock +2 -2
  9. data/examples/order/create_automatic_payment.rb +162 -0
  10. data/examples/payment/automatic_payment_credential_on_file.rb +194 -0
  11. data/lib/mercadopago/config/request_options.rb +30 -2
  12. data/lib/mercadopago/core/mp_base.rb +22 -7
  13. data/lib/mercadopago/errors/exceptions.rb +108 -0
  14. data/lib/mercadopago/errors/response.rb +58 -0
  15. data/lib/mercadopago/http/http_client.rb +45 -57
  16. data/lib/mercadopago/pagination/iterator.rb +75 -0
  17. data/lib/mercadopago/resources/card.rb +16 -0
  18. data/lib/mercadopago/resources/customer.rb +5 -0
  19. data/lib/mercadopago/resources/disbursement_refund.rb +1 -1
  20. data/lib/mercadopago/resources/order/request.rb +388 -0
  21. data/lib/mercadopago/resources/order.rb +12 -4
  22. data/lib/mercadopago/resources/payment.rb +28 -0
  23. data/lib/mercadopago/resources/preapproval.rb +5 -0
  24. data/lib/mercadopago/resources/preference.rb +13 -0
  25. data/lib/mercadopago/resources/refund.rb +14 -0
  26. data/lib/mercadopago/resources/subscription.rb +63 -0
  27. data/lib/mercadopago/sdk.rb +5 -0
  28. data/lib/mercadopago/webhook/validator.rb +2 -1
  29. data/lib/mercadopago.rb +13 -4
  30. data/mercadopago.gemspec +1 -1
  31. data/tests/base_client_test.rb +48 -0
  32. data/tests/test_advanced_payment.rb +67 -0
  33. data/tests/test_card.rb +36 -69
  34. data/tests/test_card_token.rb +18 -29
  35. data/tests/test_chargeback.rb +14 -11
  36. data/tests/test_customer.rb +40 -45
  37. data/tests/test_disbursement_refund.rb +30 -0
  38. data/tests/test_ergonomia.rb +123 -0
  39. data/tests/test_identification_type.rb +8 -9
  40. data/tests/test_invoice.rb +15 -11
  41. data/tests/test_merchant_order.rb +23 -111
  42. data/tests/test_oauth.rb +28 -17
  43. data/tests/test_order.rb +57 -447
  44. data/tests/test_order_request.rb +272 -0
  45. data/tests/test_order_transaction.rb +18 -126
  46. data/tests/test_payment.rb +44 -255
  47. data/tests/test_payment_methods.rb +9 -7
  48. data/tests/test_point.rb +28 -13
  49. data/tests/test_preapproval.rb +28 -66
  50. data/tests/test_preapproval_plan.rb +24 -83
  51. data/tests/test_preference.rb +24 -59
  52. data/tests/test_refund.rb +22 -36
  53. data/tests/test_subscription.rb +39 -0
  54. data/tests/test_user.rb +8 -10
  55. data/tests/test_webhook_signature_validator.rb +10 -2
  56. data/tests/tests.rb +11 -1
  57. metadata +20 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7dfcf7186c9d72f9b02200699acf4807a80aee9014b0c8b1905097850b6ed5fe
4
- data.tar.gz: 3cb25341c72de1d3686fe217c7fb81e3ec0bfa87a74cf80cee163b1c3ec6a0d6
3
+ metadata.gz: fb7488c81eb88390ec65887c4f7626f51fabd3a268fec20e37a21c4bcb857551
4
+ data.tar.gz: 80ffcd3be59f116220171f3b4b053c0b48a5e222a09c22e4e9063ef6d8514826
5
5
  SHA512:
6
- metadata.gz: c446ae477af4b97192b150ac3618b2e363d0d98dc8cb060aa08a26f4dc13811612fa69d6f618180fcd315f77cdd83757872f4c052b6689a86ab533411a7fd8bb
7
- data.tar.gz: '086a6f76e544b588f0f09a55d59946bd68f9eb373dbd3328841fe977168273e2b5a2d35d9e30cf0b9672d81fd31947b8fcdc1bfaca17d21a1b6529e55adbad7b'
6
+ metadata.gz: 6fa0b01c59f95ee75586f5685212a899fd717963230cafafd237b2b8d981f7ecb75327446419323f88933e50fcc85850efbddafcb0bdd6b630de1d043b77fd6a
7
+ data.tar.gz: 101c97f483a49e8317b8ab9748e2433f42a0334969185fa26f21ee61ef0d139883cb22aa80618565dcea6702195868ea886893132b650b4dd2287b5c8c8d59c0
@@ -1,43 +1,45 @@
1
- name: Ruby
1
+ name: CI
2
2
 
3
3
  on:
4
4
  push:
5
- branches: [ master ]
5
+ branches: ["master"]
6
6
  pull_request:
7
- branches: [ master ]
7
+ branches: ["master"]
8
8
 
9
9
  jobs:
10
10
  test:
11
+ name: Test on Ruby ${{ matrix.ruby-version }}
11
12
  runs-on: ubuntu-latest
12
- container: ruby:3.4
13
+ container: ruby:${{ matrix.ruby-version }}
14
+
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ ruby-version: ["3.3", "3.4"]
13
19
 
14
20
  steps:
15
- - name: Checkout code
16
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
17
- with:
18
- ref: ${{ github.event.pull_request.head.ref }}
19
- repository: ${{ github.event.pull_request.head.repo.full_name }}
20
-
21
- - name: Cache gems
22
- uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v6
23
- with:
24
- path: vendor/bundle
25
- key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
26
- restore-keys: |
27
- ${{ runner.os }}-gems-
28
-
29
- - name: Install dependencies
30
- run: |
31
- bundle config set --local path vendor/bundle
32
- bundle install
33
-
34
- - name: Rubocop
35
- run: bundle exec rubocop lib
36
-
37
- # Integration tests disabled: they require a valid MercadoPago ACCESS_TOKEN
38
- # that is not available in the CI environment. To run them locally:
39
- # ACCESS_TOKEN=<your_token> bundle exec rake
40
- # - name: Run tests
41
- # run: bundle exec rake
42
- # env:
43
- # ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN_V2 }}
21
+ - name: Checkout code
22
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
23
+
24
+ - name: Configure git safe directory
25
+ run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
26
+
27
+ - name: Install dependencies
28
+ run: bundle install
29
+
30
+ - name: Lint with Rubocop
31
+ run: bundle exec rubocop lib
32
+
33
+ - name: Run unit tests
34
+ run: bundle exec rake test
35
+
36
+ - name: Dependency audit
37
+ run: |
38
+ gem install bundler-audit --no-document
39
+ bundle-audit check --update
40
+
41
+ - name: Dependency review
42
+ if: github.event_name == 'pull_request'
43
+ uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v4
44
+ with:
45
+ fail-on-severity: high
@@ -2,7 +2,7 @@ name: Release SDK - Publish Gem
2
2
 
3
3
  on:
4
4
  release:
5
- types: [created]
5
+ types: [published]
6
6
 
7
7
  jobs:
8
8
  release:
@@ -14,14 +14,14 @@ jobs:
14
14
 
15
15
  steps:
16
16
  - name: Checkout code
17
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
18
-
19
- - name: Install dependencies
20
- run: bundle install
17
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
21
18
 
22
19
  - name: Configure git safe directory
23
20
  run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
24
21
 
22
+ - name: Install dependencies
23
+ run: bundle install
24
+
25
25
  - name: Build gem
26
26
  run: gem build *.gemspec
27
27
 
@@ -29,6 +29,3 @@ jobs:
29
29
  env:
30
30
  GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
31
31
  run: gem push *.gem
32
-
33
- - name: Wait for release to propagate
34
- run: gem install rubygems-await
data/.gitignore CHANGED
@@ -2,5 +2,6 @@
2
2
  bin/
3
3
  config/*yml
4
4
  pkg/
5
+ vendor/
5
6
  *.log
6
7
  *.gem
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/.rubocop_todo.yml CHANGED
@@ -48,8 +48,14 @@ Metrics/MethodLength:
48
48
 
49
49
  # Offense count: 3
50
50
  # Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
51
+ Metrics/ModuleLength:
52
+ Exclude:
53
+ - 'lib/mercadopago/resources/order/request.rb'
54
+
51
55
  Metrics/ParameterLists:
52
56
  Max: 7
57
+ Exclude:
58
+ - 'lib/mercadopago/resources/order/request.rb'
53
59
 
54
60
  # Offense count: 1
55
61
  Naming/AccessorMethodName:
data/CHANGELOG.md CHANGED
@@ -5,6 +5,44 @@ 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.4.0] - 2026-08-11
9
+
10
+ ### Added
11
+ - **Automatic Payments example**: two-step recurring flow ([#156](https://github.com/mercadopago/sdk-ruby/pull/156))
12
+ - **Pagination**: support `data` key for Orders v2 API and string paging totals ([#156](https://github.com/mercadopago/sdk-ruby/pull/156))
13
+
14
+ ### Fixed
15
+ - **Stored credential**: rename `prev_transaction_ref` to `previous_transaction_reference` ([#156](https://github.com/mercadopago/sdk-ruby/pull/156))
16
+ - **`Iterator#call`**: use keyword args for Ruby 4.0 strict kwarg separation ([#156](https://github.com/mercadopago/sdk-ruby/pull/156))
17
+
18
+ ### CI
19
+ - Standardize CI/CD workflows ([#166](https://github.com/mercadopago/sdk-ruby/pull/166))
20
+ - Migrate tests to mock-based unit tests ([#166](https://github.com/mercadopago/sdk-ruby/pull/166))
21
+ - Fix SDK bugs discovered during mock test migration ([#166](https://github.com/mercadopago/sdk-ruby/pull/166))
22
+ - Fix Ruby CI: drop 3.2 matrix (gemspec requires >=3.3), add `safe.directory` ([#166](https://github.com/mercadopago/sdk-ruby/pull/166))
23
+
24
+ ## [3.3.0] - 2026-08-04
25
+
26
+ ### Added
27
+
28
+ - **SDK ergonomics**: typed exceptions, configurable retry, and auto-pagination ([#162](https://github.com/mercadopago/sdk-ruby/pull/162))
29
+ - `MercadoPago::ApiError` now has 12 specific subtypes per HTTP status code
30
+ - Request options gain optional `max_retries`, `retry_on`, `initial_delay_ms`, `max_delay_ms` and `on_retry` callback
31
+ - New auto-pagination support on search endpoints
32
+ - **Missing API methods** — `disbursement_refund.list`, `advanced_payment.update`, `customer_card.update`, `payment.update` ([#161](https://github.com/mercadopago/sdk-ruby/pull/161))
33
+ - **CREDENTIAL_ON_FILE messaging fields** on Payment types ([#158](https://github.com/mercadopago/sdk-ruby/pull/158)): `first_transaction`, `storage`, `transaction_initiator`, `reference`
34
+
35
+ ### Fixed
36
+
37
+ - Webhook `tolerance_seconds` unit mismatch — `ts` header value compared in seconds against a millisecond clock ([#163](https://github.com/mercadopago/sdk-ruby/pull/163))
38
+ - `constant_time_equals` error on multibyte v1 hash ([#163](https://github.com/mercadopago/sdk-ruby/pull/163))
39
+
40
+ ### Dependencies
41
+
42
+ - Bump `json` gem ([#159](https://github.com/mercadopago/sdk-ruby/pull/159))
43
+ - Bump `actions/cache` to `v6.1.0` ([#160](https://github.com/mercadopago/sdk-ruby/pull/160))
44
+ - Bump `actions/checkout` to `v7.0.1` ([#157](https://github.com/mercadopago/sdk-ruby/pull/157))
45
+
8
46
  ## [3.2.0] - 2026-06-30
9
47
 
10
48
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mercadopago-sdk (3.2.1)
4
+ mercadopago-sdk (3.4.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.1)
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,162 @@
1
+ require_relative '../../lib/mercadopago'
2
+
3
+ # Mercado Pago Create Order — Automatic Payments (recurring charges).
4
+ #
5
+ # Demonstrates the two-step Automatic Payments flow:
6
+ # 1. First payment — CVV-validated charge that registers the card credential.
7
+ # 2. Recurring charge — subsequent MIT charge without CVV, referencing step 1.
8
+ #
9
+ # Prerequisites:
10
+ # - A customer created via POST /v1/customers → customer_id
11
+ # - A payment profile created via POST /v1/customers/{id}/payment-profiles → payment_profile_id
12
+
13
+ sdk = Mercadopago::SDK.new('<ACCESS_TOKEN>')
14
+
15
+ # ── Step 1: First payment ─────────────────────────────────────────────────────
16
+ # Registers the card credential with first_payment: true.
17
+ # No prev_transaction_ref is needed on the first charge.
18
+ def create_first_payment(sdk, customer_id:, payment_profile_id:, payer_email:, card_token:)
19
+ request = {
20
+ type: 'online',
21
+ processing_mode: 'automatic',
22
+ total_amount: '100.00',
23
+ external_reference: 'subscription-001-payment-1',
24
+ payer: {
25
+ email: payer_email,
26
+ customer_id: customer_id
27
+ },
28
+ transactions: {
29
+ payments: [
30
+ {
31
+ amount: '100.00',
32
+ payment_method: {
33
+ id: 'master',
34
+ type: 'credit_card',
35
+ token: card_token,
36
+ installments: 1
37
+ },
38
+ automatic_payments: {
39
+ payment_profile_id: payment_profile_id
40
+ },
41
+ stored_credential: {
42
+ payment_initiator: 'customer',
43
+ reason: 'recurring',
44
+ first_payment: true
45
+ }
46
+ }
47
+ ]
48
+ }
49
+ }
50
+
51
+ custom_headers = { 'X-Idempotency-Key': '<IDEMPOTENCY_KEY_FIRST>' }
52
+ request_options = Mercadopago::RequestOptions.new(custom_headers: custom_headers)
53
+
54
+ sdk.order.create(request, request_options: request_options)
55
+ rescue StandardError => e
56
+ puts e.message
57
+ nil
58
+ end
59
+
60
+ # ── Step 2: Recurring charge ──────────────────────────────────────────────────
61
+ # Subsequent MIT charge — no card token needed, uses the payment profile.
62
+ # prev_transaction_ref links this charge to the original authorization.
63
+ def create_recurring_charge(sdk,
64
+ customer_id:,
65
+ payment_profile_id:,
66
+ payer_email:,
67
+ previous_transaction_reference:,
68
+ sequence_number:)
69
+ request = {
70
+ type: 'online',
71
+ processing_mode: 'automatic_async',
72
+ total_amount: '100.00',
73
+ external_reference: "subscription-001-payment-#{sequence_number}",
74
+ payer: {
75
+ email: payer_email,
76
+ customer_id: customer_id
77
+ },
78
+ transactions: {
79
+ payments: [
80
+ {
81
+ amount: '100.00',
82
+ automatic_payments: {
83
+ payment_profile_id: payment_profile_id,
84
+ retries: 3,
85
+ schedule_date: '2026-09-01T00:00:00.000-04:00',
86
+ due_date: '2026-09-05T00:00:00.000-04:00'
87
+ },
88
+ stored_credential: {
89
+ payment_initiator: 'merchant',
90
+ reason: 'recurring',
91
+ first_payment: false,
92
+ previous_transaction_reference: previous_transaction_reference
93
+ },
94
+ subscription_data: {
95
+ invoice_id: "INV-00#{sequence_number}",
96
+ billing_date: '2026-08-01',
97
+ subscription_sequence: {
98
+ number: sequence_number,
99
+ total: 12
100
+ },
101
+ invoice_period: {
102
+ type: 'monthly',
103
+ period: 1
104
+ }
105
+ }
106
+ }
107
+ ]
108
+ }
109
+ }
110
+
111
+ custom_headers = { 'X-Idempotency-Key': "<IDEMPOTENCY_KEY_RECURRING_#{sequence_number}>" }
112
+ request_options = Mercadopago::RequestOptions.new(custom_headers: custom_headers)
113
+
114
+ sdk.order.create(request, request_options: request_options)
115
+ rescue StandardError => e
116
+ puts e.message
117
+ nil
118
+ end
119
+
120
+ # ── Run the flow ──────────────────────────────────────────────────────────────
121
+ customer_id = '<CUSTOMER_ID>'
122
+ payment_profile_id = '<PAYMENT_PROFILE_ID>'
123
+ payer_email = '<PAYER_EMAIL>'
124
+ card_token = '<CARD_TOKEN>'
125
+
126
+ # First payment
127
+ first_result = create_first_payment(
128
+ sdk,
129
+ customer_id: customer_id,
130
+ payment_profile_id: payment_profile_id,
131
+ payer_email: payer_email,
132
+ card_token: card_token
133
+ )
134
+
135
+ if first_result
136
+ first_order = first_result[:response]
137
+ puts "First payment order ID: #{first_order['id']}"
138
+ puts "Status: #{first_order['status']}"
139
+
140
+ # Save the payment ID for the next recurring charge
141
+ first_payment_id = first_order.dig('transactions', 'payments', 0, 'id')
142
+ puts "First payment ID (save for next charge): #{first_payment_id}"
143
+
144
+ # Recurring charge
145
+ if first_payment_id
146
+ recurring_result = create_recurring_charge(
147
+ sdk,
148
+ customer_id: customer_id,
149
+ payment_profile_id: payment_profile_id,
150
+ payer_email: payer_email,
151
+ previous_transaction_reference: first_payment_id,
152
+ sequence_number: 2
153
+ )
154
+
155
+ if recurring_result
156
+ recurring_order = recurring_result[:response]
157
+ puts "\nRecurring charge order ID: #{recurring_order['id']}"
158
+ puts "Status: #{recurring_order['status']}"
159
+ puts "Status detail: #{recurring_order['status_detail']}"
160
+ end
161
+ end
162
+ end
@@ -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