checkout-intents 0.0.2 → 0.1.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/CHANGELOG.md +21 -0
- data/README.md +80 -1
- data/lib/checkout_intents/client.rb +34 -5
- data/lib/checkout_intents/errors.rb +33 -0
- data/lib/checkout_intents/internal/transport/base_client.rb +33 -0
- data/lib/checkout_intents/resources/checkout_intents.rb +222 -0
- data/lib/checkout_intents/version.rb +1 -1
- data/rbi/checkout_intents/errors.rbi +33 -0
- data/rbi/checkout_intents/internal/transport/base_client.rbi +12 -0
- data/rbi/checkout_intents/resources/checkout_intents.rbi +92 -0
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8cf5e6cd633f4f7ee6338c4bea928549742449f2866fa6bbd2ae92b6421ef31c
|
|
4
|
+
data.tar.gz: 852728890666c85d7d1a0e8adeda2d59a5b2ce29c3cee5e3799044f740f9e543
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0034c8b0230278a365e8d1193ba49d1cd32e9fc03a69794436e2ef407dcd73b6b7637e40587951695621099bc0e71a619725f896e91d0b1e335bef346cbae651
|
|
7
|
+
data.tar.gz: 2f2dfb10980745a1f0f3b127a4e31faac529af16e3091c753d7f53e9341e29ac558c3308ca68cfcd33a1033cab7299fc47f05e78e1ba7a85938fc472384596df
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0 (2026-01-12)
|
|
4
|
+
|
|
5
|
+
Full Changelog: [v0.0.2...v0.1.0](https://github.com/rye-com/checkout-intents-ruby/compare/v0.0.2...v0.1.0)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
* **api:** add polling helpers ([5209a5f](https://github.com/rye-com/checkout-intents-ruby/commit/5209a5fcb449acf6e4ab52e1711b5c95a5acfa63))
|
|
10
|
+
* **api:** auto infer environment value ([d666fbb](https://github.com/rye-com/checkout-intents-ruby/commit/d666fbb211955c2c02c9fb5c88de0dea6240adf1))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Chores
|
|
14
|
+
|
|
15
|
+
* **internal:** add request_with_headers method ([89dc817](https://github.com/rye-com/checkout-intents-ruby/commit/89dc817a36404e5231e65ab7f05ebe1e5766c61f))
|
|
16
|
+
* move `cgi` into dependencies for ruby 4 ([7674f56](https://github.com/rye-com/checkout-intents-ruby/commit/7674f56727d3e5d940c110eb173d91148224c21b))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Documentation
|
|
20
|
+
|
|
21
|
+
* **api:** polling helpers ([465f0a0](https://github.com/rye-com/checkout-intents-ruby/commit/465f0a06848b014fd4e3526ff5c2a01b68f3b7ec))
|
|
22
|
+
|
|
3
23
|
## 0.0.2 (2026-01-07)
|
|
4
24
|
|
|
5
25
|
Full Changelog: [v0.0.1...v0.0.2](https://github.com/rye-com/checkout-intents-ruby/compare/v0.0.1...v0.0.2)
|
|
@@ -13,4 +33,5 @@ Full Changelog: [v0.0.1...v0.0.2](https://github.com/rye-com/checkout-intents-ru
|
|
|
13
33
|
### Chores
|
|
14
34
|
|
|
15
35
|
* configure new SDK language ([ee73dec](https://github.com/rye-com/checkout-intents-ruby/commit/ee73dec601f3c6286045a97a8c900ade97e9099f))
|
|
36
|
+
* sync repo ([c2a3795](https://github.com/rye-com/checkout-intents-ruby/commit/c2a379536364e5dbdfc5aabfaf20383817a1e2b1))
|
|
16
37
|
* update SDK settings ([a11c510](https://github.com/rye-com/checkout-intents-ruby/commit/a11c51015c6bcdbbc51353813a10b4421f9ebf44))
|
data/README.md
CHANGED
|
@@ -17,7 +17,7 @@ To use this gem, install via Bundler by adding the following to your application
|
|
|
17
17
|
<!-- x-release-please-start-version -->
|
|
18
18
|
|
|
19
19
|
```ruby
|
|
20
|
-
gem "checkout-intents", "~> 0.0
|
|
20
|
+
gem "checkout-intents", "~> 0.1.0"
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
<!-- x-release-please-end -->
|
|
@@ -81,6 +81,63 @@ if page.next_page?
|
|
|
81
81
|
end
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
### Polling Helpers
|
|
85
|
+
|
|
86
|
+
This SDK includes helper methods for the asynchronous checkout flow. The recommended pattern follows Rye's two-phase checkout:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
# Phase 1: Create and wait for offer
|
|
90
|
+
intent = checkout_intents.checkout_intents.create_and_poll(
|
|
91
|
+
buyer: {
|
|
92
|
+
address1: "123 Main St",
|
|
93
|
+
city: "New York",
|
|
94
|
+
country: "US",
|
|
95
|
+
email: "john.doe@example.com",
|
|
96
|
+
firstName: "John",
|
|
97
|
+
lastName: "Doe",
|
|
98
|
+
phone: "5555555555",
|
|
99
|
+
postalCode: "10001",
|
|
100
|
+
province: "NY"
|
|
101
|
+
},
|
|
102
|
+
product_url: "https://example.com/product",
|
|
103
|
+
quantity: 1
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Handle failure during offer retrieval
|
|
107
|
+
if intent.state == CheckoutIntents::CheckoutIntent::FailedCheckoutIntent::State::FAILED
|
|
108
|
+
puts "Failed: #{intent.failure_reason.message}"
|
|
109
|
+
else
|
|
110
|
+
# Review pricing with user
|
|
111
|
+
puts "Total: #{intent.offer.cost.total}"
|
|
112
|
+
|
|
113
|
+
# Phase 2: Confirm and wait for completion
|
|
114
|
+
completed = checkout_intents.checkout_intents.confirm_and_poll(
|
|
115
|
+
intent.id,
|
|
116
|
+
payment_method: { type: :stripe_token, stripeToken: "tok_visa" }
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
puts "Status: #{completed.state}"
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Available polling methods:
|
|
124
|
+
|
|
125
|
+
- `create_and_poll` - Create and poll until offer is ready (awaiting_confirmation or failed)
|
|
126
|
+
- `confirm_and_poll` - Confirm and poll until completion (completed or failed)
|
|
127
|
+
- `poll_until_completed` - Poll until completed or failed
|
|
128
|
+
- `poll_until_awaiting_confirmation` - Poll until offer is ready or failed
|
|
129
|
+
|
|
130
|
+
All polling methods support customizable timeouts:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
# Configure polling behavior
|
|
134
|
+
intent = checkout_intents.checkout_intents.poll_until_completed(
|
|
135
|
+
intent_id,
|
|
136
|
+
poll_interval: 5.0, # Poll every 5 seconds (default)
|
|
137
|
+
max_attempts: 120 # Try up to 120 times, ~10 minutes (default)
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
84
141
|
### Handling errors
|
|
85
142
|
|
|
86
143
|
When the library is unable to connect to the API, or if the API returns a non-success status code (i.e., 4xx or 5xx response), a subclass of `CheckoutIntents::Errors::APIError` will be thrown:
|
|
@@ -128,6 +185,28 @@ Error codes are as follows:
|
|
|
128
185
|
| Other HTTP error | `APIStatusError` |
|
|
129
186
|
| Timeout | `APITimeoutError` |
|
|
130
187
|
| Network error | `APIConnectionError` |
|
|
188
|
+
| Polling timeout | `PollTimeoutError` |
|
|
189
|
+
|
|
190
|
+
### Polling Timeout Errors
|
|
191
|
+
|
|
192
|
+
When using polling helper methods, if the operation exceeds the configured `max_attempts`, a `PollTimeoutError` is raised with helpful context:
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
begin
|
|
196
|
+
intent = checkout_intents.checkout_intents.poll_until_completed(
|
|
197
|
+
"intent_id",
|
|
198
|
+
poll_interval: 5.0,
|
|
199
|
+
max_attempts: 60
|
|
200
|
+
)
|
|
201
|
+
rescue CheckoutIntents::Errors::PollTimeoutError => e
|
|
202
|
+
puts "Polling timed out for intent: #{e.intent_id}"
|
|
203
|
+
puts "Attempted #{e.attempts} times over #{e.attempts * e.poll_interval}s"
|
|
204
|
+
|
|
205
|
+
# You can retrieve the current state manually
|
|
206
|
+
current_intent = checkout_intents.checkout_intents.retrieve(e.intent_id)
|
|
207
|
+
puts "Current state: #{current_intent.state}"
|
|
208
|
+
end
|
|
209
|
+
```
|
|
131
210
|
|
|
132
211
|
### Retries
|
|
133
212
|
|
|
@@ -42,6 +42,19 @@ module CheckoutIntents
|
|
|
42
42
|
{"authorization" => "Bearer #{@api_key}"}
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# Extracts the environment from a Rye API key.
|
|
46
|
+
# API keys follow the format: RYE/{environment}-{key}
|
|
47
|
+
#
|
|
48
|
+
# @api private
|
|
49
|
+
#
|
|
50
|
+
# @param api_key [String] The API key to parse
|
|
51
|
+
#
|
|
52
|
+
# @return [Symbol, nil] The extracted environment (:staging or :production), or nil if format doesn't match
|
|
53
|
+
private_class_method def self.extract_environment_from_api_key(api_key)
|
|
54
|
+
match = api_key.match(%r{\ARYE/(staging|production)-})
|
|
55
|
+
match ? match[1].to_sym : nil
|
|
56
|
+
end
|
|
57
|
+
|
|
45
58
|
# Creates and returns a new client for interacting with the API.
|
|
46
59
|
#
|
|
47
60
|
# @param api_key [String, nil] Rye API key. Format: `RYE/{environment}-abcdef` Defaults to
|
|
@@ -73,17 +86,33 @@ module CheckoutIntents
|
|
|
73
86
|
initial_retry_delay: self.class::DEFAULT_INITIAL_RETRY_DELAY,
|
|
74
87
|
max_retry_delay: self.class::DEFAULT_MAX_RETRY_DELAY
|
|
75
88
|
)
|
|
76
|
-
base_url ||= CheckoutIntents::Client::ENVIRONMENTS.fetch(environment&.to_sym || :production) do
|
|
77
|
-
message = "environment must be one of #{CheckoutIntents::Client::ENVIRONMENTS.keys}, got #{environment}"
|
|
78
|
-
raise ArgumentError.new(message)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
89
|
if api_key.nil?
|
|
82
90
|
raise ArgumentError.new("api_key is required, and can be set via environ: \"CHECKOUT_INTENTS_API_KEY\"")
|
|
83
91
|
end
|
|
84
92
|
|
|
85
93
|
@api_key = api_key.to_s
|
|
86
94
|
|
|
95
|
+
# Auto-infer environment from API key
|
|
96
|
+
inferred_environment = self.class.send(:extract_environment_from_api_key, @api_key)
|
|
97
|
+
|
|
98
|
+
# Validate environment option matches API key (if both provided)
|
|
99
|
+
if environment && inferred_environment && environment.to_sym != inferred_environment
|
|
100
|
+
raise ArgumentError.new(
|
|
101
|
+
"Environment mismatch: API key is for '#{inferred_environment}' environment " \
|
|
102
|
+
"but 'environment' option is set to '#{environment}'. Please use an API key that " \
|
|
103
|
+
"matches your desired environment or omit the 'environment' option to auto-detect " \
|
|
104
|
+
"from the API key (only auto-detectable with the RYE/{environment}-abcdef api key format)."
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Resolve environment: explicit > inferred > default (staging)
|
|
109
|
+
resolved_environment = environment&.to_sym || inferred_environment || :staging
|
|
110
|
+
|
|
111
|
+
base_url ||= CheckoutIntents::Client::ENVIRONMENTS.fetch(resolved_environment) do
|
|
112
|
+
message = "environment must be one of #{CheckoutIntents::Client::ENVIRONMENTS.keys}, got #{resolved_environment}"
|
|
113
|
+
raise ArgumentError.new(message)
|
|
114
|
+
end
|
|
115
|
+
|
|
87
116
|
super(
|
|
88
117
|
base_url: base_url,
|
|
89
118
|
timeout: timeout,
|
|
@@ -224,5 +224,38 @@ module CheckoutIntents
|
|
|
224
224
|
class InternalServerError < CheckoutIntents::Errors::APIStatusError
|
|
225
225
|
HTTP_STATUS = (500..)
|
|
226
226
|
end
|
|
227
|
+
|
|
228
|
+
class PollTimeoutError < CheckoutIntents::Errors::Error
|
|
229
|
+
# @return [String]
|
|
230
|
+
attr_reader :intent_id
|
|
231
|
+
|
|
232
|
+
# @return [Integer]
|
|
233
|
+
attr_reader :attempts
|
|
234
|
+
|
|
235
|
+
# @return [Float]
|
|
236
|
+
attr_reader :poll_interval
|
|
237
|
+
|
|
238
|
+
# @return [Integer]
|
|
239
|
+
attr_reader :max_attempts
|
|
240
|
+
|
|
241
|
+
# @api private
|
|
242
|
+
#
|
|
243
|
+
# @param intent_id [String]
|
|
244
|
+
# @param attempts [Integer]
|
|
245
|
+
# @param poll_interval [Float]
|
|
246
|
+
# @param max_attempts [Integer]
|
|
247
|
+
# @param message [String, nil]
|
|
248
|
+
def initialize(intent_id:, attempts:, poll_interval:, max_attempts:, message: nil)
|
|
249
|
+
@intent_id = intent_id
|
|
250
|
+
@attempts = attempts
|
|
251
|
+
@poll_interval = poll_interval
|
|
252
|
+
@max_attempts = max_attempts
|
|
253
|
+
|
|
254
|
+
message ||= "Polling timeout for checkout intent '#{intent_id}': " \
|
|
255
|
+
"condition not met after #{attempts} attempts (#{(max_attempts * poll_interval).round(1)}s). " \
|
|
256
|
+
"Consider increasing max_attempts or poll_interval."
|
|
257
|
+
super(message)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
227
260
|
end
|
|
228
261
|
end
|
|
@@ -518,6 +518,39 @@ module CheckoutIntents
|
|
|
518
518
|
end
|
|
519
519
|
end
|
|
520
520
|
|
|
521
|
+
# @api private
|
|
522
|
+
#
|
|
523
|
+
# Like `request`, but returns both the parsed model and response headers.
|
|
524
|
+
# Used internally for polling helpers that need to inspect headers.
|
|
525
|
+
#
|
|
526
|
+
# @param req [Hash{Symbol=>Object}]
|
|
527
|
+
#
|
|
528
|
+
# @raise [CheckoutIntents::Errors::APIError]
|
|
529
|
+
# @return [Hash{Symbol=>Object}] Hash with :data and :headers keys
|
|
530
|
+
def request_with_headers(req)
|
|
531
|
+
self.class.validate!(req)
|
|
532
|
+
model = req.fetch(:model) { CheckoutIntents::Internal::Type::Unknown }
|
|
533
|
+
opts = req[:options].to_h
|
|
534
|
+
unwrap = req[:unwrap]
|
|
535
|
+
CheckoutIntents::RequestOptions.validate!(opts)
|
|
536
|
+
request = build_request(req.except(:options), opts)
|
|
537
|
+
|
|
538
|
+
send_retry_header = request.fetch(:headers)["x-stainless-retry-count"] == "0"
|
|
539
|
+
_status, response, stream = send_request(
|
|
540
|
+
request,
|
|
541
|
+
redirect_count: 0,
|
|
542
|
+
retry_count: 0,
|
|
543
|
+
send_retry_header: send_retry_header
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
headers = CheckoutIntents::Internal::Util.normalized_headers(response.each_header.to_h)
|
|
547
|
+
decoded = CheckoutIntents::Internal::Util.decode_content(headers, stream: stream)
|
|
548
|
+
unwrapped = CheckoutIntents::Internal::Util.dig(decoded, unwrap)
|
|
549
|
+
data = CheckoutIntents::Internal::Type::Converter.coerce(model, unwrapped)
|
|
550
|
+
|
|
551
|
+
{data: data, headers: headers}
|
|
552
|
+
end
|
|
553
|
+
|
|
521
554
|
# @api private
|
|
522
555
|
#
|
|
523
556
|
# @return [String]
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
module CheckoutIntents
|
|
4
4
|
module Resources
|
|
5
5
|
class CheckoutIntents
|
|
6
|
+
# Default polling interval in seconds
|
|
7
|
+
DEFAULT_POLL_INTERVAL = 5.0
|
|
8
|
+
|
|
9
|
+
# Default maximum polling attempts
|
|
10
|
+
DEFAULT_MAX_ATTEMPTS = 120
|
|
6
11
|
# Create a checkout intent with the given request body.
|
|
7
12
|
#
|
|
8
13
|
# @overload create(buyer:, product_url:, quantity:, constraints: nil, promo_codes: nil, variant_selections: nil, request_options: {})
|
|
@@ -162,12 +167,229 @@ module CheckoutIntents
|
|
|
162
167
|
)
|
|
163
168
|
end
|
|
164
169
|
|
|
170
|
+
# Poll a checkout intent until it reaches a completed state (completed or failed).
|
|
171
|
+
#
|
|
172
|
+
# @param id [String] The checkout intent ID to poll
|
|
173
|
+
# @param poll_interval [Float] Seconds between polling attempts (default: 5.0)
|
|
174
|
+
# @param max_attempts [Integer] Maximum polling attempts before timeout (default: 120)
|
|
175
|
+
# @param request_options [::CheckoutIntents::RequestOptions, Hash{Symbol=>Object}, nil]
|
|
176
|
+
#
|
|
177
|
+
# @return [::CheckoutIntents::Models::CheckoutIntent::CompletedCheckoutIntent, ::CheckoutIntents::Models::CheckoutIntent::FailedCheckoutIntent]
|
|
178
|
+
#
|
|
179
|
+
# @raise [::CheckoutIntents::Errors::PollTimeoutError] If max attempts reached without reaching terminal state
|
|
180
|
+
#
|
|
181
|
+
# @example
|
|
182
|
+
# intent = client.checkout_intents.poll_until_completed("ci_123")
|
|
183
|
+
# if intent.state == :completed
|
|
184
|
+
# puts "Order placed successfully!"
|
|
185
|
+
# else
|
|
186
|
+
# puts "Order failed: #{intent.failure_reason.message}"
|
|
187
|
+
# end
|
|
188
|
+
def poll_until_completed(
|
|
189
|
+
id,
|
|
190
|
+
poll_interval: DEFAULT_POLL_INTERVAL,
|
|
191
|
+
max_attempts: DEFAULT_MAX_ATTEMPTS,
|
|
192
|
+
request_options: {}
|
|
193
|
+
)
|
|
194
|
+
poll_until(
|
|
195
|
+
id,
|
|
196
|
+
->(intent) { [:completed, :failed].include?(intent.state) },
|
|
197
|
+
poll_interval: poll_interval,
|
|
198
|
+
max_attempts: max_attempts,
|
|
199
|
+
request_options: request_options
|
|
200
|
+
)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Poll a checkout intent until it's ready for confirmation (awaiting_confirmation or failed).
|
|
204
|
+
#
|
|
205
|
+
# This is typically used after creating a checkout intent to wait for the offer
|
|
206
|
+
# to be retrieved from the merchant. The intent can reach awaiting_confirmation
|
|
207
|
+
# (success - ready to confirm) or failed (offer retrieval failed).
|
|
208
|
+
#
|
|
209
|
+
# @param id [String] The checkout intent ID to poll
|
|
210
|
+
# @param poll_interval [Float] Seconds between polling attempts (default: 5.0)
|
|
211
|
+
# @param max_attempts [Integer] Maximum polling attempts before timeout (default: 120)
|
|
212
|
+
# @param request_options [::CheckoutIntents::RequestOptions, Hash{Symbol=>Object}, nil]
|
|
213
|
+
#
|
|
214
|
+
# @return [::CheckoutIntents::Models::CheckoutIntent::AwaitingConfirmationCheckoutIntent, ::CheckoutIntents::Models::CheckoutIntent::FailedCheckoutIntent]
|
|
215
|
+
#
|
|
216
|
+
# @raise [::CheckoutIntents::Errors::PollTimeoutError] If max attempts reached without reaching target state
|
|
217
|
+
#
|
|
218
|
+
# @example
|
|
219
|
+
# intent = client.checkout_intents.poll_until_awaiting_confirmation("ci_123")
|
|
220
|
+
# if intent.state == :awaiting_confirmation
|
|
221
|
+
# puts "Total: #{intent.offer.cost.total}"
|
|
222
|
+
# else
|
|
223
|
+
# puts "Failed: #{intent.failure_reason.message}"
|
|
224
|
+
# end
|
|
225
|
+
def poll_until_awaiting_confirmation(
|
|
226
|
+
id,
|
|
227
|
+
poll_interval: DEFAULT_POLL_INTERVAL,
|
|
228
|
+
max_attempts: DEFAULT_MAX_ATTEMPTS,
|
|
229
|
+
request_options: {}
|
|
230
|
+
)
|
|
231
|
+
poll_until(
|
|
232
|
+
id,
|
|
233
|
+
->(intent) { [:awaiting_confirmation, :failed].include?(intent.state) },
|
|
234
|
+
poll_interval: poll_interval,
|
|
235
|
+
max_attempts: max_attempts,
|
|
236
|
+
request_options: request_options
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Create a checkout intent and poll until it's ready for confirmation.
|
|
241
|
+
#
|
|
242
|
+
# This follows the Rye documented flow: create -> poll until awaiting_confirmation.
|
|
243
|
+
# After this method completes, you should review the offer (pricing, shipping, taxes)
|
|
244
|
+
# with the user before calling confirm().
|
|
245
|
+
#
|
|
246
|
+
# @overload create_and_poll(buyer:, product_url:, quantity:, constraints: nil, promo_codes: nil, variant_selections: nil, poll_interval: 5.0, max_attempts: 120, request_options: {})
|
|
247
|
+
#
|
|
248
|
+
# @param buyer [::CheckoutIntents::Models::Buyer]
|
|
249
|
+
# @param product_url [String]
|
|
250
|
+
# @param quantity [Float]
|
|
251
|
+
# @param constraints [::CheckoutIntents::Models::CheckoutIntentCreateParams::Constraints]
|
|
252
|
+
# @param promo_codes [Array<String>]
|
|
253
|
+
# @param variant_selections [Array<::CheckoutIntents::Models::VariantSelection>]
|
|
254
|
+
# @param poll_interval [Float] Seconds between polling attempts (default: 5.0)
|
|
255
|
+
# @param max_attempts [Integer] Maximum polling attempts before timeout (default: 120)
|
|
256
|
+
# @param request_options [::CheckoutIntents::RequestOptions, Hash{Symbol=>Object}, nil]
|
|
257
|
+
#
|
|
258
|
+
# @return [::CheckoutIntents::Models::CheckoutIntent::AwaitingConfirmationCheckoutIntent, ::CheckoutIntents::Models::CheckoutIntent::FailedCheckoutIntent]
|
|
259
|
+
#
|
|
260
|
+
# @raise [::CheckoutIntents::Errors::PollTimeoutError] If max attempts reached without reaching target state
|
|
261
|
+
#
|
|
262
|
+
# @example
|
|
263
|
+
# intent = client.checkout_intents.create_and_poll(
|
|
264
|
+
# buyer: { address1: "123 Main St", city: "New York", ... },
|
|
265
|
+
# product_url: "https://example.com/product",
|
|
266
|
+
# quantity: 1
|
|
267
|
+
# )
|
|
268
|
+
# puts "Total: #{intent.offer.cost.total}"
|
|
269
|
+
def create_and_poll(
|
|
270
|
+
params,
|
|
271
|
+
poll_interval: DEFAULT_POLL_INTERVAL,
|
|
272
|
+
max_attempts: DEFAULT_MAX_ATTEMPTS
|
|
273
|
+
)
|
|
274
|
+
intent = create(params)
|
|
275
|
+
poll_until_awaiting_confirmation(
|
|
276
|
+
intent.id,
|
|
277
|
+
poll_interval: poll_interval,
|
|
278
|
+
max_attempts: max_attempts,
|
|
279
|
+
request_options: params[:request_options] || {}
|
|
280
|
+
)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Confirm a checkout intent and poll until it reaches a completed state (completed or failed).
|
|
284
|
+
#
|
|
285
|
+
# @overload confirm_and_poll(id, payment_method:, poll_interval: 5.0, max_attempts: 120, request_options: {})
|
|
286
|
+
#
|
|
287
|
+
# @param id [String] The id of the checkout intent to confirm
|
|
288
|
+
# @param payment_method [::CheckoutIntents::Models::PaymentMethod::StripeTokenPaymentMethod, ::CheckoutIntents::Models::PaymentMethod::BasisTheoryPaymentMethod, ::CheckoutIntents::Models::PaymentMethod::NekudaPaymentMethod]
|
|
289
|
+
# @param poll_interval [Float] Seconds between polling attempts (default: 5.0)
|
|
290
|
+
# @param max_attempts [Integer] Maximum polling attempts before timeout (default: 120)
|
|
291
|
+
# @param request_options [::CheckoutIntents::RequestOptions, Hash{Symbol=>Object}, nil]
|
|
292
|
+
#
|
|
293
|
+
# @return [::CheckoutIntents::Models::CheckoutIntent::CompletedCheckoutIntent, ::CheckoutIntents::Models::CheckoutIntent::FailedCheckoutIntent]
|
|
294
|
+
#
|
|
295
|
+
# @raise [::CheckoutIntents::Errors::PollTimeoutError] If max attempts reached without reaching terminal state
|
|
296
|
+
#
|
|
297
|
+
# @example
|
|
298
|
+
# intent = client.checkout_intents.confirm_and_poll(
|
|
299
|
+
# "ci_123",
|
|
300
|
+
# payment_method: { type: "stripe_token", stripe_token: "tok_visa" }
|
|
301
|
+
# )
|
|
302
|
+
# if intent.state == :completed
|
|
303
|
+
# puts "Order placed successfully!"
|
|
304
|
+
# else
|
|
305
|
+
# puts "Order failed: #{intent.failure_reason.message}"
|
|
306
|
+
# end
|
|
307
|
+
def confirm_and_poll(
|
|
308
|
+
id,
|
|
309
|
+
params,
|
|
310
|
+
poll_interval: DEFAULT_POLL_INTERVAL,
|
|
311
|
+
max_attempts: DEFAULT_MAX_ATTEMPTS
|
|
312
|
+
)
|
|
313
|
+
intent = confirm(id, params)
|
|
314
|
+
poll_until_completed(
|
|
315
|
+
intent.id,
|
|
316
|
+
poll_interval: poll_interval,
|
|
317
|
+
max_attempts: max_attempts,
|
|
318
|
+
request_options: params[:request_options] || {}
|
|
319
|
+
)
|
|
320
|
+
end
|
|
321
|
+
|
|
165
322
|
# @api private
|
|
166
323
|
#
|
|
167
324
|
# @param client [::CheckoutIntents::Client]
|
|
168
325
|
def initialize(client:)
|
|
169
326
|
@client = client
|
|
170
327
|
end
|
|
328
|
+
|
|
329
|
+
private
|
|
330
|
+
|
|
331
|
+
# Core polling implementation.
|
|
332
|
+
#
|
|
333
|
+
# @param id [String] The checkout intent ID
|
|
334
|
+
# @param condition [Proc] Returns true when polling should stop
|
|
335
|
+
# @param poll_interval [Float] Seconds between polls
|
|
336
|
+
# @param max_attempts [Integer] Maximum attempts
|
|
337
|
+
# @param request_options [Hash] Additional request options
|
|
338
|
+
#
|
|
339
|
+
# @return [::CheckoutIntents::Models::CheckoutIntent]
|
|
340
|
+
def poll_until(id, condition, poll_interval:, max_attempts:, request_options:)
|
|
341
|
+
if max_attempts < 1
|
|
342
|
+
warn(
|
|
343
|
+
"[Checkout Intents SDK] Invalid max_attempts value: #{max_attempts}. " \
|
|
344
|
+
"max_attempts must be >= 1. Defaulting to 1."
|
|
345
|
+
)
|
|
346
|
+
max_attempts = 1
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
attempts = 0
|
|
350
|
+
poll_headers = {
|
|
351
|
+
"x-stainless-poll-helper" => "true",
|
|
352
|
+
"x-stainless-custom-poll-interval" => (poll_interval * 1000).to_i.to_s
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
merged_options = (request_options || {}).merge(
|
|
356
|
+
extra_headers: ((request_options || {})[:extra_headers] || {}).merge(poll_headers)
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
while attempts < max_attempts
|
|
360
|
+
result = @client.request_with_headers(
|
|
361
|
+
method: :get,
|
|
362
|
+
path: ["api/v1/checkout-intents/%1$s", id],
|
|
363
|
+
model: ::CheckoutIntents::CheckoutIntent,
|
|
364
|
+
options: merged_options
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
intent = result[:data]
|
|
368
|
+
headers = result[:headers]
|
|
369
|
+
|
|
370
|
+
return intent if condition.call(intent)
|
|
371
|
+
|
|
372
|
+
attempts += 1
|
|
373
|
+
|
|
374
|
+
if attempts >= max_attempts
|
|
375
|
+
raise ::CheckoutIntents::Errors::PollTimeoutError.new(
|
|
376
|
+
intent_id: id,
|
|
377
|
+
attempts: attempts,
|
|
378
|
+
poll_interval: poll_interval,
|
|
379
|
+
max_attempts: max_attempts
|
|
380
|
+
)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Check for server-suggested polling interval
|
|
384
|
+
sleep_interval = poll_interval
|
|
385
|
+
if (header_interval = headers["retry-after-ms"])
|
|
386
|
+
header_interval_ms = Integer(header_interval, exception: false)
|
|
387
|
+
sleep_interval = header_interval_ms / 1000.0 if header_interval_ms
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
sleep(sleep_interval)
|
|
391
|
+
end
|
|
392
|
+
end
|
|
171
393
|
end
|
|
172
394
|
end
|
|
173
395
|
end
|
|
@@ -201,5 +201,38 @@ module CheckoutIntents
|
|
|
201
201
|
class InternalServerError < CheckoutIntents::Errors::APIStatusError
|
|
202
202
|
HTTP_STATUS = T.let((500..), T::Range[Integer])
|
|
203
203
|
end
|
|
204
|
+
|
|
205
|
+
class PollTimeoutError < CheckoutIntents::Errors::Error
|
|
206
|
+
sig { returns(String) }
|
|
207
|
+
attr_reader :intent_id
|
|
208
|
+
|
|
209
|
+
sig { returns(Integer) }
|
|
210
|
+
attr_reader :attempts
|
|
211
|
+
|
|
212
|
+
sig { returns(Float) }
|
|
213
|
+
attr_reader :poll_interval
|
|
214
|
+
|
|
215
|
+
sig { returns(Integer) }
|
|
216
|
+
attr_reader :max_attempts
|
|
217
|
+
|
|
218
|
+
# @api private
|
|
219
|
+
sig do
|
|
220
|
+
params(
|
|
221
|
+
intent_id: String,
|
|
222
|
+
attempts: Integer,
|
|
223
|
+
poll_interval: Float,
|
|
224
|
+
max_attempts: Integer,
|
|
225
|
+
message: T.nilable(String)
|
|
226
|
+
).returns(T.attached_class)
|
|
227
|
+
end
|
|
228
|
+
def self.new(
|
|
229
|
+
intent_id:,
|
|
230
|
+
attempts:,
|
|
231
|
+
poll_interval:,
|
|
232
|
+
max_attempts:,
|
|
233
|
+
message: nil
|
|
234
|
+
)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
204
237
|
end
|
|
205
238
|
end
|
|
@@ -299,6 +299,18 @@ module CheckoutIntents
|
|
|
299
299
|
)
|
|
300
300
|
end
|
|
301
301
|
|
|
302
|
+
# @api private
|
|
303
|
+
#
|
|
304
|
+
# Like `request`, but returns both the parsed model and response headers.
|
|
305
|
+
# Used internally for polling helpers that need to inspect headers.
|
|
306
|
+
sig do
|
|
307
|
+
params(
|
|
308
|
+
req: CheckoutIntents::Internal::Transport::BaseClient::RequestComponents
|
|
309
|
+
).returns({ data: T.anything, headers: T::Hash[String, String] })
|
|
310
|
+
end
|
|
311
|
+
def request_with_headers(req)
|
|
312
|
+
end
|
|
313
|
+
|
|
302
314
|
# @api private
|
|
303
315
|
sig { returns(String) }
|
|
304
316
|
def inspect
|
|
@@ -156,6 +156,98 @@ module CheckoutIntents
|
|
|
156
156
|
)
|
|
157
157
|
end
|
|
158
158
|
|
|
159
|
+
# Default polling interval in seconds
|
|
160
|
+
DEFAULT_POLL_INTERVAL = T.let(5.0, Float)
|
|
161
|
+
|
|
162
|
+
# Default maximum polling attempts
|
|
163
|
+
DEFAULT_MAX_ATTEMPTS = T.let(120, Integer)
|
|
164
|
+
|
|
165
|
+
# Poll a checkout intent until it reaches a completed state (completed or failed).
|
|
166
|
+
sig do
|
|
167
|
+
params(
|
|
168
|
+
id: String,
|
|
169
|
+
poll_interval: Float,
|
|
170
|
+
max_attempts: Integer,
|
|
171
|
+
request_options: ::CheckoutIntents::RequestOptions::OrHash
|
|
172
|
+
).returns(
|
|
173
|
+
T.any(
|
|
174
|
+
::CheckoutIntents::CheckoutIntent::CompletedCheckoutIntent,
|
|
175
|
+
::CheckoutIntents::CheckoutIntent::FailedCheckoutIntent
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
def poll_until_completed(
|
|
180
|
+
id,
|
|
181
|
+
poll_interval: DEFAULT_POLL_INTERVAL,
|
|
182
|
+
max_attempts: DEFAULT_MAX_ATTEMPTS,
|
|
183
|
+
request_options: {}
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Poll a checkout intent until it's ready for confirmation (awaiting_confirmation or failed).
|
|
188
|
+
sig do
|
|
189
|
+
params(
|
|
190
|
+
id: String,
|
|
191
|
+
poll_interval: Float,
|
|
192
|
+
max_attempts: Integer,
|
|
193
|
+
request_options: ::CheckoutIntents::RequestOptions::OrHash
|
|
194
|
+
).returns(
|
|
195
|
+
T.any(
|
|
196
|
+
::CheckoutIntents::CheckoutIntent::AwaitingConfirmationCheckoutIntent,
|
|
197
|
+
::CheckoutIntents::CheckoutIntent::FailedCheckoutIntent
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
def poll_until_awaiting_confirmation(
|
|
202
|
+
id,
|
|
203
|
+
poll_interval: DEFAULT_POLL_INTERVAL,
|
|
204
|
+
max_attempts: DEFAULT_MAX_ATTEMPTS,
|
|
205
|
+
request_options: {}
|
|
206
|
+
)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Create a checkout intent and poll until it's ready for confirmation.
|
|
210
|
+
sig do
|
|
211
|
+
params(
|
|
212
|
+
params: T::Hash[Symbol, T.anything],
|
|
213
|
+
poll_interval: Float,
|
|
214
|
+
max_attempts: Integer
|
|
215
|
+
).returns(
|
|
216
|
+
T.any(
|
|
217
|
+
::CheckoutIntents::CheckoutIntent::AwaitingConfirmationCheckoutIntent,
|
|
218
|
+
::CheckoutIntents::CheckoutIntent::FailedCheckoutIntent
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
end
|
|
222
|
+
def create_and_poll(
|
|
223
|
+
params,
|
|
224
|
+
poll_interval: DEFAULT_POLL_INTERVAL,
|
|
225
|
+
max_attempts: DEFAULT_MAX_ATTEMPTS
|
|
226
|
+
)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Confirm a checkout intent and poll until it reaches a completed state.
|
|
230
|
+
sig do
|
|
231
|
+
params(
|
|
232
|
+
id: String,
|
|
233
|
+
params: T::Hash[Symbol, T.anything],
|
|
234
|
+
poll_interval: Float,
|
|
235
|
+
max_attempts: Integer
|
|
236
|
+
).returns(
|
|
237
|
+
T.any(
|
|
238
|
+
::CheckoutIntents::CheckoutIntent::CompletedCheckoutIntent,
|
|
239
|
+
::CheckoutIntents::CheckoutIntent::FailedCheckoutIntent
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
end
|
|
243
|
+
def confirm_and_poll(
|
|
244
|
+
id,
|
|
245
|
+
params,
|
|
246
|
+
poll_interval: DEFAULT_POLL_INTERVAL,
|
|
247
|
+
max_attempts: DEFAULT_MAX_ATTEMPTS
|
|
248
|
+
)
|
|
249
|
+
end
|
|
250
|
+
|
|
159
251
|
# @api private
|
|
160
252
|
sig { params(client: ::CheckoutIntents::Client).returns(T.attached_class) }
|
|
161
253
|
def self.new(client:)
|
metadata
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: checkout-intents
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0
|
|
4
|
+
version: 0.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Checkout Intents
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-01-
|
|
11
|
+
date: 2026-01-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: cgi
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: connection_pool
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|