inttegro 1.0.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 +7 -0
- data/.dockerignore +9 -0
- data/CHANGELOG.md +17 -0
- data/Dockerfile +23 -0
- data/LICENSE +21 -0
- data/README.md +104 -0
- data/Rakefile +50 -0
- data/lib/inttegro/client.rb +159 -0
- data/lib/inttegro/enums.rb +6 -0
- data/lib/inttegro/errors.rb +128 -0
- data/lib/inttegro/file_download.rb +29 -0
- data/lib/inttegro/generated/enums.rb +693 -0
- data/lib/inttegro/generated/models.rb +3840 -0
- data/lib/inttegro/generated/operations.rb +369 -0
- data/lib/inttegro/http_client.rb +639 -0
- data/lib/inttegro/models.rb +132 -0
- data/lib/inttegro/resources/apps.rb +24 -0
- data/lib/inttegro/resources/balance_transactions.rb +28 -0
- data/lib/inttegro/resources/balances.rb +16 -0
- data/lib/inttegro/resources/broadcasts.rb +28 -0
- data/lib/inttegro/resources/chimes.rb +32 -0
- data/lib/inttegro/resources/customers.rb +28 -0
- data/lib/inttegro/resources/file_links.rb +50 -0
- data/lib/inttegro/resources/file_references.rb +20 -0
- data/lib/inttegro/resources/files.rb +39 -0
- data/lib/inttegro/resources/financial_accounts.rb +104 -0
- data/lib/inttegro/resources/keys.rb +60 -0
- data/lib/inttegro/resources/message_templates.rb +78 -0
- data/lib/inttegro/resources/orders.rb +422 -0
- data/lib/inttegro/resources/otp.rb +30 -0
- data/lib/inttegro/resources/payment_methods.rb +250 -0
- data/lib/inttegro/resources/payouts.rb +200 -0
- data/lib/inttegro/resources/prices.rb +40 -0
- data/lib/inttegro/resources/products.rb +48 -0
- data/lib/inttegro/resources/purchase_intents.rb +44 -0
- data/lib/inttegro/resources/refunds.rb +36 -0
- data/lib/inttegro/resources/schedules.rb +28 -0
- data/lib/inttegro/resources/spec.rb +16 -0
- data/lib/inttegro/resources/upload_requests.rb +66 -0
- data/lib/inttegro/response_object.rb +86 -0
- data/lib/inttegro/transport_response.rb +65 -0
- data/lib/inttegro/types.rb +32 -0
- data/lib/inttegro/version.rb +6 -0
- data/lib/inttegro.rb +14 -0
- data/rbi/inttegro/generated.rbi +529 -0
- metadata +107 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
require "uri"
|
|
8
|
+
|
|
9
|
+
require_relative "errors"
|
|
10
|
+
require_relative "file_download"
|
|
11
|
+
require_relative "models"
|
|
12
|
+
require_relative "generated/operations"
|
|
13
|
+
require_relative "response_object"
|
|
14
|
+
require_relative "transport_response"
|
|
15
|
+
require_relative "types"
|
|
16
|
+
require_relative "version"
|
|
17
|
+
|
|
18
|
+
module Inttegro
|
|
19
|
+
DEFAULT_BASE_URL = T.let("https://api.inttegro.com", String)
|
|
20
|
+
|
|
21
|
+
# Minimal HTTP client that wraps Net::HTTP with JSON parsing and API-friendly errors.
|
|
22
|
+
class HTTPClient
|
|
23
|
+
extend T::Sig
|
|
24
|
+
|
|
25
|
+
USER_AGENT = T.let("inttegro-sdk-ruby/#{Inttegro::VERSION}", String)
|
|
26
|
+
ResponseValue = T.type_alias do
|
|
27
|
+
T.any(T::Struct, ResponseObject, Types::Scalar, T::Array[Object])
|
|
28
|
+
end
|
|
29
|
+
ErrorPayload = T.type_alias { T::Hash[Symbol, T.nilable(String)] }
|
|
30
|
+
|
|
31
|
+
sig do
|
|
32
|
+
params(
|
|
33
|
+
api_key: T.nilable(String),
|
|
34
|
+
base_url: T.nilable(String),
|
|
35
|
+
read_timeout: T.nilable(Numeric),
|
|
36
|
+
open_timeout: T.nilable(Numeric),
|
|
37
|
+
adapter: T.nilable(Types::Adapter)
|
|
38
|
+
).void
|
|
39
|
+
end
|
|
40
|
+
def initialize(api_key:, base_url: DEFAULT_BASE_URL, read_timeout: 30, open_timeout: 10, adapter: nil)
|
|
41
|
+
@api_key = T.let(api_key || "", String)
|
|
42
|
+
raise ArgumentError, "api_key is required" if @api_key.strip.empty?
|
|
43
|
+
|
|
44
|
+
@base_url = T.let(base_url&.sub(%r{/*$}, "") || DEFAULT_BASE_URL, String)
|
|
45
|
+
@read_timeout = T.let(read_timeout, T.nilable(Numeric))
|
|
46
|
+
@open_timeout = T.let(open_timeout, T.nilable(Numeric))
|
|
47
|
+
@adapter = T.let(adapter, T.nilable(Types::Adapter))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sig { params(path: String, headers: Types::Headers, query: T.nilable(Types::Query)).returns(ResponseValue) }
|
|
51
|
+
def get(path, headers: {}, query: nil)
|
|
52
|
+
request(:get, path, headers: headers, query: query)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sig do
|
|
56
|
+
params(
|
|
57
|
+
path: String,
|
|
58
|
+
body: T.nilable(Types::RequestBody),
|
|
59
|
+
headers: Types::Headers,
|
|
60
|
+
query: T.nilable(Types::Query),
|
|
61
|
+
body_keywords: Object
|
|
62
|
+
).returns(ResponseValue)
|
|
63
|
+
end
|
|
64
|
+
def post(path, body = nil, headers: {}, query: nil, **body_keywords)
|
|
65
|
+
body = body_keywords unless body_keywords.empty?
|
|
66
|
+
request(:post, path, body: body, headers: headers, query: query)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
sig do
|
|
70
|
+
type_parameters(:Model)
|
|
71
|
+
.params(
|
|
72
|
+
path: String,
|
|
73
|
+
model: T::Class[T.type_parameter(:Model)],
|
|
74
|
+
body: Types::RequestBody,
|
|
75
|
+
headers: Types::Headers,
|
|
76
|
+
query: T.nilable(Types::Query)
|
|
77
|
+
)
|
|
78
|
+
.returns(T.type_parameter(:Model))
|
|
79
|
+
end
|
|
80
|
+
def post_model(path, model, body, headers: {}, query: nil)
|
|
81
|
+
unless model <= T::Struct
|
|
82
|
+
raise TypeError, "response model for #{path} must inherit from T::Struct"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
response = request(
|
|
86
|
+
:post,
|
|
87
|
+
path,
|
|
88
|
+
body: body,
|
|
89
|
+
headers: headers,
|
|
90
|
+
query: query,
|
|
91
|
+
response_model: model
|
|
92
|
+
)
|
|
93
|
+
return T.cast(response, T.type_parameter(:Model)) if response.is_a?(model)
|
|
94
|
+
|
|
95
|
+
raise TypeError, "expected #{model} from #{path}, got #{response.class}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
sig do
|
|
99
|
+
params(
|
|
100
|
+
path: String,
|
|
101
|
+
body: Types::RequestBody,
|
|
102
|
+
headers: Types::Headers,
|
|
103
|
+
query: T.nilable(Types::Query)
|
|
104
|
+
).returns(ResponseObject)
|
|
105
|
+
end
|
|
106
|
+
def post_object(path, body, headers: {}, query: nil)
|
|
107
|
+
expect_response_object(request(:post, path, body: body, headers: headers, query: query), path)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
sig do
|
|
111
|
+
params(path: String, body: Types::RequestBody, headers: Types::Headers).returns(ResponseObject)
|
|
112
|
+
end
|
|
113
|
+
def post_with_headers(path, body, headers: {})
|
|
114
|
+
post_object(path, body, headers: headers)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
sig do
|
|
118
|
+
params(
|
|
119
|
+
path: String,
|
|
120
|
+
fields: Types::Payload,
|
|
121
|
+
files: Types::Payload,
|
|
122
|
+
headers: Types::Headers,
|
|
123
|
+
authenticated: T::Boolean
|
|
124
|
+
).returns(ResponseObject)
|
|
125
|
+
end
|
|
126
|
+
def post_multipart(path, fields: {}, files: {}, headers: {}, authenticated: true)
|
|
127
|
+
expect_response_object(
|
|
128
|
+
perform_multipart(path, fields, files, headers, authenticated, nil),
|
|
129
|
+
path
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
sig do
|
|
134
|
+
type_parameters(:Model)
|
|
135
|
+
.params(
|
|
136
|
+
path: String,
|
|
137
|
+
model: T::Class[T.type_parameter(:Model)],
|
|
138
|
+
fields: Types::Payload,
|
|
139
|
+
files: Types::Payload,
|
|
140
|
+
headers: Types::Headers,
|
|
141
|
+
authenticated: T::Boolean
|
|
142
|
+
)
|
|
143
|
+
.returns(T.type_parameter(:Model))
|
|
144
|
+
end
|
|
145
|
+
def post_multipart_model(path, model, fields: {}, files: {}, headers: {}, authenticated: true)
|
|
146
|
+
unless model <= T::Struct
|
|
147
|
+
raise TypeError, "response model for #{path} must inherit from T::Struct"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
response = perform_multipart(path, fields, files, headers, authenticated, model)
|
|
151
|
+
return T.cast(response, T.type_parameter(:Model)) if response.is_a?(model)
|
|
152
|
+
|
|
153
|
+
raise TypeError, "expected #{model} from #{path}, got #{response.class}"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
sig do
|
|
157
|
+
params(
|
|
158
|
+
path: String,
|
|
159
|
+
fields: Types::Payload,
|
|
160
|
+
files: Types::Payload,
|
|
161
|
+
headers: Types::Headers,
|
|
162
|
+
authenticated: T::Boolean,
|
|
163
|
+
model: T.nilable(T::Class[T::Struct])
|
|
164
|
+
).returns(ResponseValue)
|
|
165
|
+
end
|
|
166
|
+
def perform_multipart(path, fields, files, headers, authenticated, model)
|
|
167
|
+
uri = build_uri(path, nil)
|
|
168
|
+
request = Net::HTTP::Post.new(uri)
|
|
169
|
+
request["Accept"] = "application/json"
|
|
170
|
+
request["User-Agent"] = USER_AGENT
|
|
171
|
+
request["Authorization"] = "Bearer #{@api_key}" if authenticated
|
|
172
|
+
headers = headers.dup
|
|
173
|
+
if authenticated && idempotent_mutation_path?(path) && !header_present?(headers, "Idempotency-Key")
|
|
174
|
+
headers["Idempotency-Key"] = generate_idempotency_key
|
|
175
|
+
end
|
|
176
|
+
headers.each { |key, value| request[key] = value }
|
|
177
|
+
|
|
178
|
+
form = []
|
|
179
|
+
fields.each do |key, value|
|
|
180
|
+
next if value.nil?
|
|
181
|
+
|
|
182
|
+
form << [key.to_s, value.is_a?(Hash) || value.is_a?(Array) ? JSON.generate(value) : value.to_s]
|
|
183
|
+
end
|
|
184
|
+
files.each do |key, path|
|
|
185
|
+
raise ArgumentError, "file path for #{key} must be a String" unless path.is_a?(String)
|
|
186
|
+
|
|
187
|
+
form << [key.to_s, File.open(path)]
|
|
188
|
+
end
|
|
189
|
+
request.set_form(form, "multipart/form-data")
|
|
190
|
+
|
|
191
|
+
raw_response = @adapter ? @adapter.call(uri, request) : build_http(uri).request(request)
|
|
192
|
+
response = TransportResponse.new(raw_response)
|
|
193
|
+
handle_response(path, response, model)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
sig { params(path: String, body: Types::RequestBody).returns(FileDownload) }
|
|
197
|
+
def post_binary_json(path, body)
|
|
198
|
+
body = with_request_meta_idempotency(:post, path, coerce_body(body), {})
|
|
199
|
+
response = raw_request(:post, path, JSON.dump(body), {
|
|
200
|
+
"Accept" => "application/octet-stream",
|
|
201
|
+
"Content-Type" => "application/json",
|
|
202
|
+
"Authorization" => "Bearer #{@api_key}"
|
|
203
|
+
})
|
|
204
|
+
return FileDownload.new(response.body, response.headers) if response.code.to_i < 400
|
|
205
|
+
|
|
206
|
+
raise_response_error(response)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
sig { params(url: String).returns(FileDownload) }
|
|
210
|
+
def get_binary_public(url)
|
|
211
|
+
uri = build_uri(url, nil)
|
|
212
|
+
request = Net::HTTP::Get.new(uri)
|
|
213
|
+
request["User-Agent"] = USER_AGENT
|
|
214
|
+
raw_response = @adapter ? @adapter.call(uri, request) : build_http(uri).request(request)
|
|
215
|
+
response = TransportResponse.new(raw_response)
|
|
216
|
+
return FileDownload.new(response.body, response.headers) if response.code.to_i < 400
|
|
217
|
+
|
|
218
|
+
raise_response_error(response)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
sig do
|
|
222
|
+
params(
|
|
223
|
+
method: T.any(String, Symbol),
|
|
224
|
+
path: String,
|
|
225
|
+
body: T.nilable(Types::RequestBody),
|
|
226
|
+
headers: Types::Headers,
|
|
227
|
+
query: T.nilable(Types::Query),
|
|
228
|
+
response_model: T.nilable(T::Class[T::Struct])
|
|
229
|
+
).returns(ResponseValue)
|
|
230
|
+
end
|
|
231
|
+
def request(method, path, body: nil, headers: {}, query: nil, response_model: nil)
|
|
232
|
+
uri = build_uri(path, query)
|
|
233
|
+
body = validate_body(path, coerce_body(body))
|
|
234
|
+
body = with_request_meta_idempotency(method, path, body, headers)
|
|
235
|
+
request = build_request(method, uri, body, headers)
|
|
236
|
+
|
|
237
|
+
raw_response =
|
|
238
|
+
if @adapter
|
|
239
|
+
@adapter.call(uri, request)
|
|
240
|
+
else
|
|
241
|
+
http = build_http(uri)
|
|
242
|
+
http.request(request)
|
|
243
|
+
end
|
|
244
|
+
response = TransportResponse.new(raw_response)
|
|
245
|
+
handle_response(path, response, response_model)
|
|
246
|
+
rescue Timeout::Error, Errno::ETIMEDOUT => e
|
|
247
|
+
raise TimeoutError.new("Request timed out", e)
|
|
248
|
+
rescue SocketError, Errno::ECONNREFUSED, Errno::ECONNRESET => e
|
|
249
|
+
raise NetworkError.new("Network request failed", e)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
private
|
|
253
|
+
|
|
254
|
+
sig { params(path: String, query: T.nilable(Types::Query)).returns(URI::Generic) }
|
|
255
|
+
def build_uri(path, query)
|
|
256
|
+
if path.start_with?("http://", "https://")
|
|
257
|
+
uri = URI.parse(path)
|
|
258
|
+
uri.query = URI.encode_www_form(query) if query && !query.empty?
|
|
259
|
+
return uri
|
|
260
|
+
end
|
|
261
|
+
normalized = path.start_with?("/") ? path : "/#{path}"
|
|
262
|
+
uri = URI.parse(@base_url + normalized)
|
|
263
|
+
uri.query = URI.encode_www_form(query) if query && !query.empty?
|
|
264
|
+
uri
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
sig do
|
|
268
|
+
params(
|
|
269
|
+
method: T.any(String, Symbol),
|
|
270
|
+
path: String,
|
|
271
|
+
body: T.nilable(String),
|
|
272
|
+
headers: Types::Headers
|
|
273
|
+
).returns(TransportResponse)
|
|
274
|
+
end
|
|
275
|
+
def raw_request(method, path, body, headers)
|
|
276
|
+
uri = build_uri(path, nil)
|
|
277
|
+
request = build_request(method, uri, nil, headers)
|
|
278
|
+
request.body = body if body
|
|
279
|
+
raw_response = @adapter ? @adapter.call(uri, request) : build_http(uri).request(request)
|
|
280
|
+
TransportResponse.new(raw_response)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
sig { params(uri: URI::Generic).returns(Net::HTTP) }
|
|
284
|
+
def build_http(uri)
|
|
285
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
286
|
+
http.use_ssl = uri.scheme == "https"
|
|
287
|
+
http.read_timeout = @read_timeout if @read_timeout
|
|
288
|
+
http.open_timeout = @open_timeout if @open_timeout
|
|
289
|
+
http
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
sig do
|
|
293
|
+
params(
|
|
294
|
+
method: T.any(String, Symbol),
|
|
295
|
+
uri: URI::Generic,
|
|
296
|
+
body: T.nilable(Types::Payload),
|
|
297
|
+
headers: Types::Headers
|
|
298
|
+
).returns(Net::HTTPRequest)
|
|
299
|
+
end
|
|
300
|
+
def build_request(method, uri, body, headers)
|
|
301
|
+
klass =
|
|
302
|
+
case method.to_s.downcase
|
|
303
|
+
when "get" then Net::HTTP::Get
|
|
304
|
+
when "post" then Net::HTTP::Post
|
|
305
|
+
when "delete" then Net::HTTP::Delete
|
|
306
|
+
when "put" then Net::HTTP::Put
|
|
307
|
+
when "patch" then Net::HTTP::Patch
|
|
308
|
+
else
|
|
309
|
+
raise ArgumentError, "Unsupported HTTP method: #{method}"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
req = klass.new(uri)
|
|
313
|
+
req["Accept"] = "application/json"
|
|
314
|
+
req["Authorization"] = "Bearer #{@api_key}"
|
|
315
|
+
req["User-Agent"] = USER_AGENT
|
|
316
|
+
|
|
317
|
+
headers.each { |key, value| req[key] = value }
|
|
318
|
+
|
|
319
|
+
if body
|
|
320
|
+
req["Content-Type"] ||= "application/json"
|
|
321
|
+
req.body = JSON.dump(body)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
req
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
sig do
|
|
328
|
+
params(
|
|
329
|
+
method: T.any(String, Symbol),
|
|
330
|
+
path: String,
|
|
331
|
+
body: T.nilable(Types::Payload),
|
|
332
|
+
headers: Types::Headers
|
|
333
|
+
).returns(T.nilable(Types::Payload))
|
|
334
|
+
end
|
|
335
|
+
def with_request_meta_idempotency(method, path, body, headers)
|
|
336
|
+
return body unless body.is_a?(Hash)
|
|
337
|
+
|
|
338
|
+
payload = body.dup
|
|
339
|
+
payload.delete(:idempotency_key)
|
|
340
|
+
payload.delete("idempotency_key")
|
|
341
|
+
return payload unless method.to_s.downcase == "post" && idempotent_mutation_path?(path)
|
|
342
|
+
return payload if header_present?(headers, "Idempotency-Key")
|
|
343
|
+
|
|
344
|
+
request_meta_value = payload.delete(:request_meta) || payload.delete("request_meta")
|
|
345
|
+
request_meta = request_meta_value.is_a?(Hash) ? request_meta_value.dup : {}
|
|
346
|
+
existing = request_meta[:idempotency_key] || request_meta["idempotency_key"]
|
|
347
|
+
request_meta["idempotency_key"] = generate_idempotency_key if existing.to_s.strip.empty?
|
|
348
|
+
payload["request_meta"] = request_meta
|
|
349
|
+
payload
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
sig { params(path_or_url: String).returns(T::Boolean) }
|
|
353
|
+
def idempotent_mutation_path?(path_or_url)
|
|
354
|
+
path =
|
|
355
|
+
if path_or_url.start_with?("http://", "https://")
|
|
356
|
+
URI.parse(path_or_url).path || ""
|
|
357
|
+
else
|
|
358
|
+
path_or_url
|
|
359
|
+
end
|
|
360
|
+
action = path.split("/").reject(&:empty?).last
|
|
361
|
+
!["", nil, "lookup", "page", "settings", "countries", "contents", "balances", "render_preview"].include?(action)
|
|
362
|
+
rescue URI::InvalidURIError
|
|
363
|
+
false
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
sig { params(headers: Types::Headers, name: String).returns(T::Boolean) }
|
|
367
|
+
def header_present?(headers, name)
|
|
368
|
+
headers.any? { |key, value| key.to_s.casecmp?(name) && !value.to_s.strip.empty? }
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
sig { returns(String) }
|
|
372
|
+
def generate_idempotency_key
|
|
373
|
+
random = SecureRandom.bytes(10).bytes
|
|
374
|
+
timestamp = (Time.now.to_f * 1000).to_i & ((1 << 48) - 1)
|
|
375
|
+
rand_a = ((random[0] << 8) | random[1]) & 0x0fff
|
|
376
|
+
rand_b = random[2..].reduce(0) { |value, byte| (value << 8) | byte } & ((1 << 62) - 1)
|
|
377
|
+
value = (timestamp << 80) | (0x7 << 76) | (rand_a << 64) | (0x2 << 62) | rand_b
|
|
378
|
+
hex = value.to_s(16).rjust(32, "0")
|
|
379
|
+
"#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
sig do
|
|
383
|
+
params(
|
|
384
|
+
path: String,
|
|
385
|
+
response: TransportResponse,
|
|
386
|
+
model_class: T.nilable(T::Class[T::Struct])
|
|
387
|
+
).returns(ResponseValue)
|
|
388
|
+
end
|
|
389
|
+
def handle_response(path, response, model_class = nil)
|
|
390
|
+
status = response.code.to_i
|
|
391
|
+
data = parse_json(response.body)
|
|
392
|
+
|
|
393
|
+
return wrap_response(data, model_class || response_model_for(path)) if status < 400
|
|
394
|
+
|
|
395
|
+
raise_response_error(response)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
sig { params(response: TransportResponse).returns(T.noreturn) }
|
|
399
|
+
def raise_response_error(response)
|
|
400
|
+
status = response.code.to_i
|
|
401
|
+
body = response.body
|
|
402
|
+
data = parse_json(body)
|
|
403
|
+
payload = extract_error_payload(data)
|
|
404
|
+
message = payload[:message] || payload[:detail] || extract_error_message(data, response)
|
|
405
|
+
code = payload[:code]
|
|
406
|
+
type = payload[:type]
|
|
407
|
+
url = payload[:url]
|
|
408
|
+
detail = payload[:detail]
|
|
409
|
+
fix_code = payload[:fix_code]
|
|
410
|
+
cause = payload[:cause]
|
|
411
|
+
|
|
412
|
+
case status
|
|
413
|
+
when 401
|
|
414
|
+
raise AuthenticationError.new(
|
|
415
|
+
message,
|
|
416
|
+
status: status,
|
|
417
|
+
code: code,
|
|
418
|
+
type: type,
|
|
419
|
+
url: url,
|
|
420
|
+
detail: detail,
|
|
421
|
+
fix_code: fix_code,
|
|
422
|
+
cause: cause,
|
|
423
|
+
body: body,
|
|
424
|
+
data: data
|
|
425
|
+
)
|
|
426
|
+
when 429
|
|
427
|
+
retry_after = response["Retry-After"]&.to_i
|
|
428
|
+
raise RateLimitError.new(
|
|
429
|
+
message,
|
|
430
|
+
status: status,
|
|
431
|
+
code: code,
|
|
432
|
+
type: type,
|
|
433
|
+
url: url,
|
|
434
|
+
detail: detail,
|
|
435
|
+
fix_code: fix_code,
|
|
436
|
+
cause: cause,
|
|
437
|
+
body: body,
|
|
438
|
+
data: data,
|
|
439
|
+
retry_after: retry_after
|
|
440
|
+
)
|
|
441
|
+
else
|
|
442
|
+
raise APIError.new(
|
|
443
|
+
message,
|
|
444
|
+
status: status,
|
|
445
|
+
code: code,
|
|
446
|
+
type: type,
|
|
447
|
+
url: url,
|
|
448
|
+
detail: detail,
|
|
449
|
+
fix_code: fix_code,
|
|
450
|
+
cause: cause,
|
|
451
|
+
body: body,
|
|
452
|
+
data: data
|
|
453
|
+
)
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
sig { params(body: String).returns(Types::WireValue) }
|
|
458
|
+
def parse_json(body)
|
|
459
|
+
return {} if body.empty?
|
|
460
|
+
|
|
461
|
+
normalize_wire_value(JSON.parse(body))
|
|
462
|
+
rescue JSON::ParserError
|
|
463
|
+
body
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
sig { params(data: Object, response: TransportResponse).returns(String) }
|
|
467
|
+
def extract_error_message(data, response)
|
|
468
|
+
return "HTTP #{response.code} #{response.message}" unless data.is_a?(Hash)
|
|
469
|
+
|
|
470
|
+
error = data["error"]
|
|
471
|
+
candidate = error.is_a?(Hash) ? error["message"] : data["message"] || error
|
|
472
|
+
candidate.is_a?(String) && !candidate.empty? ? candidate : "HTTP #{response.code}"
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
sig { params(data: Object).returns(ErrorPayload) }
|
|
476
|
+
def extract_error_payload(data)
|
|
477
|
+
return {} unless data.is_a?(Hash)
|
|
478
|
+
|
|
479
|
+
payload = data["error"].is_a?(Hash) ? data["error"] : data
|
|
480
|
+
{
|
|
481
|
+
message: string_or_nil(payload["message"]),
|
|
482
|
+
detail: string_or_nil(payload["detail"]),
|
|
483
|
+
code: string_or_nil(payload["code"]),
|
|
484
|
+
type: string_or_nil(payload["type"]),
|
|
485
|
+
url: string_or_nil(payload["url"]),
|
|
486
|
+
fix_code: string_or_nil(payload["fix_code"]),
|
|
487
|
+
cause: string_or_nil(payload["cause"])
|
|
488
|
+
}
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
sig do
|
|
492
|
+
params(data: Object, model_class: T.nilable(T::Class[T::Struct])).returns(ResponseValue)
|
|
493
|
+
end
|
|
494
|
+
def wrap_response(data, model_class = nil)
|
|
495
|
+
case data
|
|
496
|
+
when Hash
|
|
497
|
+
payload = normalize_payload(data)
|
|
498
|
+
model_class ? Inttegro::Models.deserialize(payload, model_class) : ResponseObject.new(payload)
|
|
499
|
+
when Array
|
|
500
|
+
data.each_with_object(T.let([], T::Array[Object])) do |item, values|
|
|
501
|
+
values << wrap_response(item, model_class)
|
|
502
|
+
end
|
|
503
|
+
when String, Integer, Float, TrueClass, FalseClass, NilClass
|
|
504
|
+
data
|
|
505
|
+
else
|
|
506
|
+
raise TypeError, "unsupported response value: #{data.class}"
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
sig { params(value: T.nilable(Types::RequestBody)).returns(T.nilable(Types::Payload)) }
|
|
511
|
+
def coerce_body(value)
|
|
512
|
+
return nil if value.nil?
|
|
513
|
+
|
|
514
|
+
normalize_payload(value.is_a?(T::Struct) ? value.serialize : value)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
sig { params(path: String).returns(T.nilable(T::Class[T::Struct])) }
|
|
518
|
+
def response_model_for(path)
|
|
519
|
+
return Inttegro::Models::CancelOTPResponse if path == "/otp/cancel"
|
|
520
|
+
return Inttegro::Models::OrderResponse if path == "/orders/new"
|
|
521
|
+
|
|
522
|
+
Inttegro::Operations::RESPONSE_MODELS[path]
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
sig do
|
|
526
|
+
params(path: String, body: T.nilable(Types::Payload)).returns(T.nilable(Types::Payload))
|
|
527
|
+
end
|
|
528
|
+
def validate_body(path, body)
|
|
529
|
+
return body unless body.is_a?(Hash)
|
|
530
|
+
|
|
531
|
+
case path
|
|
532
|
+
when "/financial_accounts/create", "/financial_accounts/connect"
|
|
533
|
+
require_keys(body, %w[label type reference currency owner], path)
|
|
534
|
+
when "/financial_accounts/lookup"
|
|
535
|
+
require_keys(body, %w[account_id], path)
|
|
536
|
+
when "/customers/create"
|
|
537
|
+
require_keys(body, %w[name], path)
|
|
538
|
+
when "/customers/lookup"
|
|
539
|
+
require_keys(body, %w[customer_id], path)
|
|
540
|
+
when "/products/create"
|
|
541
|
+
require_keys(body, %w[type name], path)
|
|
542
|
+
when "/products/add_price"
|
|
543
|
+
require_keys(body, %w[product_id amount], path)
|
|
544
|
+
when "/products/set_default_unit_price"
|
|
545
|
+
require_keys(body, %w[product_id price_id], path)
|
|
546
|
+
when "/products/lookup", "/products/update", "/products/publish", "/products/unpublish", "/products/archive"
|
|
547
|
+
require_keys(body, %w[product_id], path)
|
|
548
|
+
when "/orders/create", "/orders/new"
|
|
549
|
+
require_any(body, %w[customer_data customer_id], path)
|
|
550
|
+
require_keys(body, %w[line_items], path)
|
|
551
|
+
when "/orders/lookup", "/orders/update", "/orders/request_confirmation", "/orders/finalize",
|
|
552
|
+
"/orders/cancel", "/orders/refund", "/orders/complete"
|
|
553
|
+
require_keys(body, %w[order_id], path)
|
|
554
|
+
when "/orders/confirm_payment"
|
|
555
|
+
require_keys(body, %w[order_id token], path)
|
|
556
|
+
when "/orders/pay"
|
|
557
|
+
require_keys(body, %w[order_id], path)
|
|
558
|
+
when "/payment_methods/tokenize"
|
|
559
|
+
require_keys(body, %w[customer_id payment_method_data], path)
|
|
560
|
+
when "/payment_methods/verify", "/payment_methods/confirm_verification",
|
|
561
|
+
"/payment_methods/lookup", "/payment_methods/delete"
|
|
562
|
+
require_keys(body, %w[payment_method_id], path)
|
|
563
|
+
when "/payouts/set_destinations"
|
|
564
|
+
require_keys(body, %w[destinations], path)
|
|
565
|
+
when "/payouts/cancel"
|
|
566
|
+
require_keys(body, %w[payout_id], path)
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
body
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
sig { params(body: Types::Payload, keys: T::Array[String], path: String).void }
|
|
573
|
+
def require_keys(body, keys, path)
|
|
574
|
+
missing = keys.reject { |key| present?(body[key]) || present?(body[key.to_sym]) }
|
|
575
|
+
return if missing.empty?
|
|
576
|
+
|
|
577
|
+
raise ArgumentError, "Missing required fields for #{path}: #{missing.join(', ')}"
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
sig { params(body: Types::Payload, keys: T::Array[String], path: String).void }
|
|
581
|
+
def require_any(body, keys, path)
|
|
582
|
+
present = keys.any? { |key| present?(body[key]) || present?(body[key.to_sym]) }
|
|
583
|
+
return if present
|
|
584
|
+
|
|
585
|
+
raise ArgumentError, "Missing required fields for #{path}: one of #{keys.join(', ')}"
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
sig { params(value: Object).returns(T::Boolean) }
|
|
589
|
+
def present?(value)
|
|
590
|
+
return false if value.nil?
|
|
591
|
+
return !value.empty? if value.is_a?(String) || value.is_a?(Array) || value.is_a?(Hash)
|
|
592
|
+
|
|
593
|
+
true
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
sig { params(value: Object).returns(T.nilable(String)) }
|
|
597
|
+
def string_or_nil(value)
|
|
598
|
+
value.is_a?(String) ? value : nil
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
sig { params(value: Object).returns(Types::Payload) }
|
|
602
|
+
def normalize_payload(value)
|
|
603
|
+
raise TypeError, "request and response payloads must be hashes" unless value.is_a?(Hash)
|
|
604
|
+
|
|
605
|
+
value.each_with_object({}) do |(key, item), output|
|
|
606
|
+
unless key.is_a?(String) || key.is_a?(Symbol)
|
|
607
|
+
raise TypeError, "payload keys must be strings or symbols, got #{key.class}"
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
output[key] = normalize_wire_value(item)
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
sig { params(value: Object).returns(Types::WireValue) }
|
|
615
|
+
def normalize_wire_value(value)
|
|
616
|
+
case value
|
|
617
|
+
when T::Struct
|
|
618
|
+
normalize_payload(value.serialize)
|
|
619
|
+
when T::Enum
|
|
620
|
+
value.serialize
|
|
621
|
+
when Hash
|
|
622
|
+
normalize_payload(value)
|
|
623
|
+
when Array
|
|
624
|
+
value.map { |item| normalize_wire_value(item) }
|
|
625
|
+
when String, Integer, Float, TrueClass, FalseClass, NilClass
|
|
626
|
+
value
|
|
627
|
+
else
|
|
628
|
+
raise TypeError, "unsupported JSON value: #{value.class}"
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
sig { params(value: ResponseValue, path: String).returns(ResponseObject) }
|
|
633
|
+
def expect_response_object(value, path)
|
|
634
|
+
return value if value.is_a?(ResponseObject)
|
|
635
|
+
|
|
636
|
+
raise TypeError, "expected an object response from #{path}, got #{value.class}"
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
end
|