frame_payments 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 +7 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +48 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/PRODUCTION_READINESS.md +148 -0
- data/README.md +506 -0
- data/Rakefile +10 -0
- data/lib/frame/api_operations/create.rb +16 -0
- data/lib/frame/api_operations/delete.rb +16 -0
- data/lib/frame/api_operations/list.rb +16 -0
- data/lib/frame/api_operations/request.rb +34 -0
- data/lib/frame/api_operations/save.rb +39 -0
- data/lib/frame/api_resource.rb +67 -0
- data/lib/frame/configuration.rb +27 -0
- data/lib/frame/error.rb +48 -0
- data/lib/frame/frame_client.rb +208 -0
- data/lib/frame/frame_object.rb +155 -0
- data/lib/frame/list_object.rb +166 -0
- data/lib/frame/resources/charge_intent.rb +115 -0
- data/lib/frame/resources/customer.rb +125 -0
- data/lib/frame/resources/customer_identity_verification.rb +60 -0
- data/lib/frame/resources/invoice.rb +134 -0
- data/lib/frame/resources/invoice_line_item.rb +80 -0
- data/lib/frame/resources/payment_method.rb +116 -0
- data/lib/frame/resources/product.rb +80 -0
- data/lib/frame/resources/product_phase.rb +80 -0
- data/lib/frame/resources/refund.rb +60 -0
- data/lib/frame/resources/subscription.rb +134 -0
- data/lib/frame/resources/subscription_phase.rb +80 -0
- data/lib/frame/resources/webhook_endpoint.rb +116 -0
- data/lib/frame/resources.rb +26 -0
- data/lib/frame/util.rb +87 -0
- data/lib/frame/version.rb +5 -0
- data/lib/frame_payments.rb +60 -0
- data/sig/frame.rbs +4 -0
- metadata +124 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frame
|
|
4
|
+
# Configuration management for the Frame SDK.
|
|
5
|
+
#
|
|
6
|
+
# Handles API keys, base URLs, timeouts, and other SDK settings.
|
|
7
|
+
# Typically accessed through the Frame module methods like Frame.api_key.
|
|
8
|
+
class Configuration
|
|
9
|
+
DEFAULT_API_BASE = "https://api.framepayments.com"
|
|
10
|
+
DEFAULT_OPEN_TIMEOUT = 30
|
|
11
|
+
DEFAULT_READ_TIMEOUT = 80
|
|
12
|
+
DEFAULT_VERIFY_SSL_CERTS = true
|
|
13
|
+
|
|
14
|
+
attr_accessor :api_key, :api_base, :open_timeout, :read_timeout, :verify_ssl_certs, :log_level, :logger
|
|
15
|
+
|
|
16
|
+
def self.setup
|
|
17
|
+
new.tap do |config|
|
|
18
|
+
config.api_base = DEFAULT_API_BASE
|
|
19
|
+
config.open_timeout = DEFAULT_OPEN_TIMEOUT
|
|
20
|
+
config.read_timeout = DEFAULT_READ_TIMEOUT
|
|
21
|
+
config.verify_ssl_certs = DEFAULT_VERIFY_SSL_CERTS
|
|
22
|
+
config.log_level = nil
|
|
23
|
+
config.logger = nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/frame/error.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frame
|
|
4
|
+
# Base error class for all Frame-related errors
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# API Error class for handling API responses
|
|
8
|
+
class APIError < Error
|
|
9
|
+
attr_reader :message
|
|
10
|
+
attr_reader :http_status
|
|
11
|
+
attr_reader :http_body
|
|
12
|
+
attr_reader :json_body
|
|
13
|
+
attr_reader :code
|
|
14
|
+
|
|
15
|
+
def initialize(message = nil, http_status: nil, http_body: nil, json_body: nil, code: nil)
|
|
16
|
+
@message = message
|
|
17
|
+
@http_status = http_status
|
|
18
|
+
@http_body = http_body
|
|
19
|
+
@json_body = json_body
|
|
20
|
+
@code = code
|
|
21
|
+
super(message)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_s
|
|
25
|
+
status_string = @http_status.nil? ? "" : "(Status #{@http_status}) "
|
|
26
|
+
code_string = @code.nil? ? "" : "(Code #{@code}) "
|
|
27
|
+
"#{status_string}#{code_string}#{@message}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Authentication error
|
|
32
|
+
class AuthenticationError < APIError; end
|
|
33
|
+
|
|
34
|
+
# Invalid request error
|
|
35
|
+
class InvalidRequestError < APIError; end
|
|
36
|
+
|
|
37
|
+
# API connection error
|
|
38
|
+
class APIConnectionError < APIError; end
|
|
39
|
+
|
|
40
|
+
# Rate limit error
|
|
41
|
+
class RateLimitError < APIError; end
|
|
42
|
+
|
|
43
|
+
# Resource not found error
|
|
44
|
+
class ResourceNotFoundError < APIError; end
|
|
45
|
+
|
|
46
|
+
# Invalid parameters error
|
|
47
|
+
class InvalidParameterError < APIError; end
|
|
48
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frame
|
|
4
|
+
# HTTP client for making requests to the Frame Payments API.
|
|
5
|
+
#
|
|
6
|
+
# Handles connection management, request execution, and response processing.
|
|
7
|
+
# Automatically handles authentication, error parsing, and response formatting.
|
|
8
|
+
class FrameClient
|
|
9
|
+
attr_accessor :conn, :config
|
|
10
|
+
|
|
11
|
+
@default_client_mutex = Mutex.new
|
|
12
|
+
|
|
13
|
+
def self.active_client
|
|
14
|
+
Thread.current[:frame_client] || default_client
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.default_client
|
|
18
|
+
return @default_client if @default_client
|
|
19
|
+
|
|
20
|
+
@default_client_mutex.synchronize do
|
|
21
|
+
@default_client ||= FrameClient.new(
|
|
22
|
+
api_key: Frame.api_key,
|
|
23
|
+
api_base: Frame.api_base,
|
|
24
|
+
open_timeout: Frame.open_timeout,
|
|
25
|
+
read_timeout: Frame.read_timeout,
|
|
26
|
+
verify_ssl_certs: Frame.verify_ssl_certs,
|
|
27
|
+
logger: Frame.logger,
|
|
28
|
+
log_level: Frame.log_level
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def initialize(api_key: nil, api_base: nil, open_timeout: nil, read_timeout: nil, verify_ssl_certs: nil, logger: nil, log_level: nil)
|
|
34
|
+
@config = {
|
|
35
|
+
api_key: api_key || Frame.api_key,
|
|
36
|
+
api_base: api_base || Frame.api_base,
|
|
37
|
+
open_timeout: open_timeout || Frame.open_timeout,
|
|
38
|
+
read_timeout: read_timeout || Frame.read_timeout,
|
|
39
|
+
verify_ssl_certs: verify_ssl_certs.nil? ? Frame.verify_ssl_certs : verify_ssl_certs,
|
|
40
|
+
logger: logger || Frame.logger,
|
|
41
|
+
log_level: log_level || Frame.log_level
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@conn = create_connection
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def request(method, path, params = {}, opts = {})
|
|
48
|
+
response = execute_request(method, path, params, opts)
|
|
49
|
+
process_response(response)
|
|
50
|
+
rescue Faraday::ConnectionFailed => e
|
|
51
|
+
raise APIConnectionError.new("Connection failed: #{e.message}")
|
|
52
|
+
rescue Faraday::TimeoutError => e
|
|
53
|
+
raise APIConnectionError.new("Request timed out: #{e.message}")
|
|
54
|
+
rescue Faraday::ClientError => e
|
|
55
|
+
raise APIConnectionError.new("Client error: #{e.message}")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def create_connection
|
|
61
|
+
Faraday.new(url: @config[:api_base]) do |faraday|
|
|
62
|
+
faraday.request :json
|
|
63
|
+
faraday.response :json, content_type: /\bjson$/
|
|
64
|
+
faraday.adapter Faraday.default_adapter
|
|
65
|
+
|
|
66
|
+
# SSL verification setting
|
|
67
|
+
faraday.ssl.verify = @config[:verify_ssl_certs]
|
|
68
|
+
|
|
69
|
+
faraday.options.timeout = @config[:read_timeout]
|
|
70
|
+
faraday.options.open_timeout = @config[:open_timeout]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def execute_request(method, path, params, opts)
|
|
75
|
+
unless @config[:api_key]
|
|
76
|
+
raise AuthenticationError.new(
|
|
77
|
+
"API key is required. Set Frame.api_key before making requests.",
|
|
78
|
+
http_status: nil
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
headers = {
|
|
83
|
+
"Authorization" => "Bearer #{@config[:api_key]}",
|
|
84
|
+
"Content-Type" => "application/json",
|
|
85
|
+
"User-Agent" => "FrameRuby/#{Frame::VERSION}"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
log_request(method, path, params, headers) if should_log?
|
|
89
|
+
|
|
90
|
+
response = case method.to_s.downcase.to_sym
|
|
91
|
+
when :get
|
|
92
|
+
@conn.get(path) do |req|
|
|
93
|
+
req.params = params
|
|
94
|
+
req.headers = headers
|
|
95
|
+
end
|
|
96
|
+
when :post
|
|
97
|
+
@conn.post(path) do |req|
|
|
98
|
+
req.body = params.to_json
|
|
99
|
+
req.headers = headers
|
|
100
|
+
end
|
|
101
|
+
when :patch
|
|
102
|
+
@conn.patch(path) do |req|
|
|
103
|
+
req.body = params.to_json
|
|
104
|
+
req.headers = headers
|
|
105
|
+
end
|
|
106
|
+
when :delete
|
|
107
|
+
@conn.delete(path) do |req|
|
|
108
|
+
req.params = params
|
|
109
|
+
req.headers = headers
|
|
110
|
+
end
|
|
111
|
+
else
|
|
112
|
+
raise APIConnectionError.new("Unrecognized HTTP method: #{method}")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
log_response(response) if should_log?
|
|
116
|
+
|
|
117
|
+
response
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def process_response(response)
|
|
121
|
+
case response.status
|
|
122
|
+
when 200, 201, 202
|
|
123
|
+
parsed_response = Util.symbolize_names(response.body)
|
|
124
|
+
when 204
|
|
125
|
+
parsed_response = {}
|
|
126
|
+
when 400, 404
|
|
127
|
+
error = Util.symbolize_names(response.body)
|
|
128
|
+
raise InvalidRequestError.new(
|
|
129
|
+
error[:error],
|
|
130
|
+
http_status: response.status,
|
|
131
|
+
http_body: response.body,
|
|
132
|
+
json_body: error
|
|
133
|
+
)
|
|
134
|
+
when 401
|
|
135
|
+
error = Util.symbolize_names(response.body)
|
|
136
|
+
raise AuthenticationError.new(
|
|
137
|
+
error[:error],
|
|
138
|
+
http_status: response.status,
|
|
139
|
+
http_body: response.body,
|
|
140
|
+
json_body: error
|
|
141
|
+
)
|
|
142
|
+
when 429
|
|
143
|
+
error = Util.symbolize_names(response.body)
|
|
144
|
+
raise RateLimitError.new(
|
|
145
|
+
error[:error],
|
|
146
|
+
http_status: response.status,
|
|
147
|
+
http_body: response.body,
|
|
148
|
+
json_body: error
|
|
149
|
+
)
|
|
150
|
+
else
|
|
151
|
+
error = Util.symbolize_names(response.body)
|
|
152
|
+
raise APIError.new(
|
|
153
|
+
error[:error] || "Unknown error",
|
|
154
|
+
http_status: response.status,
|
|
155
|
+
http_body: response.body,
|
|
156
|
+
json_body: error
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
parsed_response
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def should_log?
|
|
164
|
+
@config[:logger] && @config[:log_level]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def log_request(method, path, params, headers)
|
|
168
|
+
return unless should_log?
|
|
169
|
+
|
|
170
|
+
sanitized_headers = headers.dup
|
|
171
|
+
sanitized_headers["Authorization"] = "Bearer [REDACTED]" if sanitized_headers["Authorization"]
|
|
172
|
+
|
|
173
|
+
sanitized_params = sanitize_sensitive_data(params)
|
|
174
|
+
|
|
175
|
+
@config[:logger].public_send(@config[:log_level], "[Frame] #{method.to_s.upcase} #{path}")
|
|
176
|
+
@config[:logger].public_send(@config[:log_level], "[Frame] Headers: #{sanitized_headers.inspect}")
|
|
177
|
+
@config[:logger].public_send(@config[:log_level], "[Frame] Params: #{sanitized_params.inspect}") unless sanitized_params.empty?
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def log_response(response)
|
|
181
|
+
return unless should_log?
|
|
182
|
+
|
|
183
|
+
@config[:logger].public_send(@config[:log_level], "[Frame] Response: #{response.status} #{response.reason_phrase}")
|
|
184
|
+
if response.body&.is_a?(Hash)
|
|
185
|
+
sanitized_body = sanitize_sensitive_data(response.body)
|
|
186
|
+
@config[:logger].public_send(@config[:log_level], "[Frame] Body: #{sanitized_body.inspect}")
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def sanitize_sensitive_data(data)
|
|
191
|
+
return data unless data.is_a?(Hash)
|
|
192
|
+
|
|
193
|
+
sensitive_keys = %w[api_key secret key password card_number cvc cvv number]
|
|
194
|
+
data.each_with_object({}) do |(key, value), sanitized|
|
|
195
|
+
key_str = key.to_s.downcase
|
|
196
|
+
sanitized[key] = if sensitive_keys.any? { |sensitive| key_str.include?(sensitive) }
|
|
197
|
+
"[REDACTED]"
|
|
198
|
+
elsif value.is_a?(Hash)
|
|
199
|
+
sanitize_sensitive_data(value)
|
|
200
|
+
elsif value.is_a?(Array)
|
|
201
|
+
value.map { |v| v.is_a?(Hash) ? sanitize_sensitive_data(v) : v }
|
|
202
|
+
else
|
|
203
|
+
value
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frame
|
|
4
|
+
# Base object class for all Frame API responses.
|
|
5
|
+
#
|
|
6
|
+
# FrameObject provides dynamic attribute access and JSON serialization
|
|
7
|
+
# for API responses. All API resources inherit from this class.
|
|
8
|
+
#
|
|
9
|
+
# Attributes are dynamically created based on the API response, allowing
|
|
10
|
+
# for flexible access to all fields:
|
|
11
|
+
#
|
|
12
|
+
# customer.name # => "John Doe"
|
|
13
|
+
# customer["name"] # => "John Doe"
|
|
14
|
+
# customer[:name] # => "John Doe"
|
|
15
|
+
#
|
|
16
|
+
class FrameObject
|
|
17
|
+
include Enumerable
|
|
18
|
+
|
|
19
|
+
attr_reader :id, :original_values
|
|
20
|
+
|
|
21
|
+
def initialize(id = nil, opts = {})
|
|
22
|
+
@id = id
|
|
23
|
+
@values = {}
|
|
24
|
+
@original_values = {}
|
|
25
|
+
@unsaved_values = Set.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.construct_from(values, opts = {})
|
|
29
|
+
obj = new(values[:id])
|
|
30
|
+
obj.initialize_from(values, opts)
|
|
31
|
+
obj
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize_from(values, opts = {})
|
|
35
|
+
@original_values = values.dup
|
|
36
|
+
@values = values.dup
|
|
37
|
+
|
|
38
|
+
# Make sure all keys are symbols
|
|
39
|
+
@values.keys.each do |k|
|
|
40
|
+
@values[k.to_sym] = @values.delete(k) unless k.is_a?(Symbol)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Add accessors for all keys
|
|
44
|
+
remove_accessors(@values.keys)
|
|
45
|
+
add_accessors(@values.keys)
|
|
46
|
+
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def update_attributes(values)
|
|
51
|
+
values.each do |k, v|
|
|
52
|
+
@values[k] = Util.convert_to_frame_object(v)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def [](key)
|
|
57
|
+
@values[key.to_sym]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def []=(key, value)
|
|
61
|
+
send(:"#{key}=", value)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def keys
|
|
65
|
+
@values.keys
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def values
|
|
69
|
+
@values.values
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def to_s(*_args)
|
|
73
|
+
JSON.pretty_generate(@values)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def inspect
|
|
77
|
+
id_string = @id.nil? ? "" : " id=#{@id}"
|
|
78
|
+
"#<#{self.class}:0x#{object_id.to_s(16)}#{id_string}> JSON: " + JSON.pretty_generate(@values)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def to_hash
|
|
82
|
+
@values.each_with_object({}) do |(key, value), hash|
|
|
83
|
+
hash[key] = case value
|
|
84
|
+
when FrameObject
|
|
85
|
+
value.to_hash
|
|
86
|
+
when Array
|
|
87
|
+
value.map { |v| v.respond_to?(:to_hash) ? v.to_hash : v }
|
|
88
|
+
else
|
|
89
|
+
value
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def each(&blk)
|
|
95
|
+
@values.each(&blk)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def serialize_params(obj)
|
|
99
|
+
params = {}
|
|
100
|
+
|
|
101
|
+
obj.instance_variable_get(:@values).each do |key, value|
|
|
102
|
+
params[key] = if value.is_a?(FrameObject)
|
|
103
|
+
value.serialize_params(value)
|
|
104
|
+
elsif value.is_a?(Array)
|
|
105
|
+
value.map { |v| v.is_a?(FrameObject) ? v.serialize_params(v) : v }
|
|
106
|
+
else
|
|
107
|
+
value
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
params
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
protected
|
|
115
|
+
|
|
116
|
+
def metaclass
|
|
117
|
+
class << self; self; end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def remove_accessors(keys)
|
|
121
|
+
# Skip keys that should be ignored when adding/removing accessors
|
|
122
|
+
ignored_keys = [:id, :data]
|
|
123
|
+
|
|
124
|
+
metaclass.instance_eval do
|
|
125
|
+
keys.each do |k|
|
|
126
|
+
# Skip certain keys that have special handling
|
|
127
|
+
next if ignored_keys.include?(k.to_sym)
|
|
128
|
+
|
|
129
|
+
# Remove reader method if it exists
|
|
130
|
+
remove_method(k.to_sym) if method_defined?(k.to_sym)
|
|
131
|
+
|
|
132
|
+
# Remove writer method if it exists
|
|
133
|
+
remove_method(:"#{k}=") if method_defined?(:"#{k}=")
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def add_accessors(keys)
|
|
139
|
+
# Skip keys that should be ignored when adding/removing accessors
|
|
140
|
+
ignored_keys = [:id, :data]
|
|
141
|
+
|
|
142
|
+
metaclass.instance_eval do
|
|
143
|
+
keys.each do |k|
|
|
144
|
+
# Skip certain keys that have special handling
|
|
145
|
+
next if ignored_keys.include?(k.to_sym)
|
|
146
|
+
|
|
147
|
+
define_method(k) { @values[k] }
|
|
148
|
+
define_method(:"#{k}=") do |v|
|
|
149
|
+
@values[k] = v
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frame
|
|
4
|
+
class ListObject < FrameObject
|
|
5
|
+
include Enumerable
|
|
6
|
+
include Frame::APIOperations::Request
|
|
7
|
+
|
|
8
|
+
attr_accessor :filters
|
|
9
|
+
attr_reader :resource_url, :data
|
|
10
|
+
|
|
11
|
+
def initialize(data = {}, opts = {})
|
|
12
|
+
super(nil, opts)
|
|
13
|
+
@data = data[:data] || []
|
|
14
|
+
@filters = {}
|
|
15
|
+
@resource_url = opts[:resource_url]
|
|
16
|
+
@has_more = data[:meta] && data[:meta][:has_more]
|
|
17
|
+
@page = data[:meta] && data[:meta][:page] || 1
|
|
18
|
+
|
|
19
|
+
# Extract per_page from URL if available
|
|
20
|
+
@per_page = if data.dig(:meta, :url)&.include?("per_page=")
|
|
21
|
+
begin
|
|
22
|
+
data[:meta][:url].match(/per_page=(\d+)/)[1].to_i
|
|
23
|
+
rescue
|
|
24
|
+
10
|
|
25
|
+
end
|
|
26
|
+
else
|
|
27
|
+
10
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.construct_from(values, opts = {})
|
|
32
|
+
data = values || {}
|
|
33
|
+
|
|
34
|
+
# Initialize from the values - excluding the :data key to avoid accessor conflicts
|
|
35
|
+
# We'll handle data manually since it's declared as an attribute
|
|
36
|
+
obj = new(data, opts)
|
|
37
|
+
|
|
38
|
+
# Store original values except data
|
|
39
|
+
values_without_data = values.dup
|
|
40
|
+
data_array = values_without_data.delete(:data)
|
|
41
|
+
|
|
42
|
+
# Initialize from values without data first
|
|
43
|
+
obj.initialize_from(values_without_data, opts)
|
|
44
|
+
|
|
45
|
+
# Then process the data array
|
|
46
|
+
if data_array&.is_a?(Array)
|
|
47
|
+
converted_data = data_array.map { |item| Util.convert_to_frame_object(item, opts) }
|
|
48
|
+
obj.instance_variable_set(:@data, converted_data)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
obj
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.empty_list(opts = {})
|
|
55
|
+
construct_from({data: [], meta: {has_more: false, page: 1}}, opts)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def [](index)
|
|
59
|
+
@data[index]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def each(&blk)
|
|
63
|
+
@data.each(&blk)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def empty?
|
|
67
|
+
@data.empty?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def first
|
|
71
|
+
@data.first
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def last
|
|
75
|
+
@data.last
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def retrieve(id, opts = {})
|
|
79
|
+
resource_class = object_class_for_data
|
|
80
|
+
return resource_class.retrieve(id, opts) if resource_class
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def next_page(params = {}, opts = {})
|
|
85
|
+
return self.class.empty_list(opts) unless has_more?
|
|
86
|
+
|
|
87
|
+
params = filters.merge(params || {})
|
|
88
|
+
next_page_num = @page + 1 if @page
|
|
89
|
+
params[:page] = next_page_num
|
|
90
|
+
params[:per_page] = @per_page if @per_page
|
|
91
|
+
|
|
92
|
+
# Get the resource URL - try all possible fallbacks
|
|
93
|
+
url = resource_url ||
|
|
94
|
+
(self.class.respond_to?(:resource_url) ? self.class.resource_url : nil) ||
|
|
95
|
+
"/v1/customers" # Default if we can't determine it
|
|
96
|
+
|
|
97
|
+
response = request(:get, url, params, opts)
|
|
98
|
+
result = Util.convert_to_frame_object(response, opts)
|
|
99
|
+
|
|
100
|
+
# Update this object's state with the next page's data
|
|
101
|
+
if result && !result.empty?
|
|
102
|
+
@page = next_page_num
|
|
103
|
+
@data = result.data
|
|
104
|
+
@has_more = result.has_more?
|
|
105
|
+
|
|
106
|
+
self
|
|
107
|
+
else
|
|
108
|
+
result
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def has_more?
|
|
113
|
+
!!@has_more
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def total_count
|
|
117
|
+
@data.size
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def to_hash
|
|
121
|
+
{
|
|
122
|
+
data: @data.map { |i| i.is_a?(FrameObject) ? i.to_hash : i },
|
|
123
|
+
meta: {
|
|
124
|
+
has_more: has_more?,
|
|
125
|
+
page: @page,
|
|
126
|
+
per_page: @per_page
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def inspect
|
|
132
|
+
meta_info = "#<#{self.class.name}:0x#{object_id.to_s(16)} @page=#{@page} @per_page=#{@per_page} @has_more=#{@has_more} items=#{@data.size}>"
|
|
133
|
+
|
|
134
|
+
# If data is empty, just return meta info
|
|
135
|
+
return meta_info if @data.empty?
|
|
136
|
+
|
|
137
|
+
# Format each item in the data array
|
|
138
|
+
data_strings = @data.map do |item|
|
|
139
|
+
if item.is_a?(FrameObject) && item.respond_to?(:id) && item.id
|
|
140
|
+
obj_name = item.class.name.split("::").last
|
|
141
|
+
" #<#{obj_name}:#{item.id} #{item.inspect}>"
|
|
142
|
+
else
|
|
143
|
+
" #{item.inspect}"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
"#{meta_info}\ndata=[\n#{data_strings.join(",\n")}\n]"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def object_class_for_data
|
|
153
|
+
return nil if @data.empty?
|
|
154
|
+
|
|
155
|
+
# Get first item's object type, whether it's already a FrameObject or still a Hash
|
|
156
|
+
first_item = @data.first
|
|
157
|
+
object_name = if first_item.is_a?(FrameObject)
|
|
158
|
+
first_item[:object]
|
|
159
|
+
elsif first_item.is_a?(Hash)
|
|
160
|
+
first_item[:object]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
Util.object_classes_to_constants[object_name] if object_name
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frame
|
|
4
|
+
class ChargeIntent < APIResource
|
|
5
|
+
extend Frame::APIOperations::Create
|
|
6
|
+
extend Frame::APIOperations::List
|
|
7
|
+
include Frame::APIOperations::Save
|
|
8
|
+
|
|
9
|
+
OBJECT_NAME = "charge_intent"
|
|
10
|
+
|
|
11
|
+
def self.object_name
|
|
12
|
+
OBJECT_NAME
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.create(params = {}, opts = {})
|
|
16
|
+
request_object(
|
|
17
|
+
:post,
|
|
18
|
+
"/v1/charge_intents",
|
|
19
|
+
params,
|
|
20
|
+
opts
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.list(params = {}, opts = {})
|
|
25
|
+
request_object(
|
|
26
|
+
:get,
|
|
27
|
+
"/v1/charge_intents",
|
|
28
|
+
params,
|
|
29
|
+
opts
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.retrieve(id, opts = {})
|
|
34
|
+
id = Util.normalize_id(id)
|
|
35
|
+
request_object(
|
|
36
|
+
:get,
|
|
37
|
+
"/v1/charge_intents/#{CGI.escape(id)}",
|
|
38
|
+
{},
|
|
39
|
+
opts
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def authorize(params = {}, opts = {})
|
|
44
|
+
request_object(
|
|
45
|
+
:post,
|
|
46
|
+
"/v1/charge_intents/#{CGI.escape(self["id"])}/authorize",
|
|
47
|
+
params,
|
|
48
|
+
opts
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.authorize(id, params = {}, opts = {})
|
|
53
|
+
request_object(
|
|
54
|
+
:post,
|
|
55
|
+
"/v1/charge_intents/#{CGI.escape(id)}/authorize",
|
|
56
|
+
params,
|
|
57
|
+
opts
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def capture(params = {}, opts = {})
|
|
62
|
+
request_object(
|
|
63
|
+
:post,
|
|
64
|
+
"/v1/charge_intents/#{CGI.escape(self["id"])}/capture",
|
|
65
|
+
params,
|
|
66
|
+
opts
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.capture(id, params = {}, opts = {})
|
|
71
|
+
request_object(
|
|
72
|
+
:post,
|
|
73
|
+
"/v1/charge_intents/#{CGI.escape(id)}/capture",
|
|
74
|
+
params,
|
|
75
|
+
opts
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def cancel(params = {}, opts = {})
|
|
80
|
+
request_object(
|
|
81
|
+
:post,
|
|
82
|
+
"/v1/charge_intents/#{CGI.escape(self["id"])}/cancel",
|
|
83
|
+
params,
|
|
84
|
+
opts
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.cancel(id, params = {}, opts = {})
|
|
89
|
+
request_object(
|
|
90
|
+
:post,
|
|
91
|
+
"/v1/charge_intents/#{CGI.escape(id)}/cancel",
|
|
92
|
+
params,
|
|
93
|
+
opts
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def save(params = {}, opts = {})
|
|
98
|
+
values = serialize_params(self).merge(params)
|
|
99
|
+
|
|
100
|
+
if values.empty?
|
|
101
|
+
return self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
updated = request_object(
|
|
105
|
+
:patch,
|
|
106
|
+
"/v1/charge_intents/#{CGI.escape(self["id"])}",
|
|
107
|
+
values,
|
|
108
|
+
opts
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
initialize_from(updated)
|
|
112
|
+
self
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|