elements-pay 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/.rspec +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +19 -0
- data/README.md +123 -0
- data/Rakefile +23 -0
- data/bin/elements-console +18 -0
- data/elements-pay.gemspec +33 -0
- data/lib/elements/certs/cacert.pem +3138 -0
- data/lib/elements/elements_client.rb +256 -0
- data/lib/elements/elements_configuration.rb +70 -0
- data/lib/elements/elements_object.rb +57 -0
- data/lib/elements/elements_response.rb +14 -0
- data/lib/elements/errors.rb +113 -0
- data/lib/elements/logging.rb +43 -0
- data/lib/elements/object_types.rb +18 -0
- data/lib/elements/resources/api_resource.rb +93 -0
- data/lib/elements/resources/charge.rb +13 -0
- data/lib/elements/resources/checkout_session.rb +11 -0
- data/lib/elements/resources/client_token.rb +12 -0
- data/lib/elements/resources/customer.rb +9 -0
- data/lib/elements/resources/dispute.rb +10 -0
- data/lib/elements/resources/payment_method.rb +10 -0
- data/lib/elements/resources/payment_method_gateway.rb +7 -0
- data/lib/elements/resources/refund.rb +11 -0
- data/lib/elements/resources/token.rb +9 -0
- data/lib/elements/resources.rb +3 -0
- data/lib/elements/util.rb +51 -0
- data/lib/elements/version.rb +5 -0
- data/lib/elements.rb +31 -0
- metadata +109 -0
@@ -0,0 +1,256 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elements
|
4
|
+
# ElementsClient executes HTTP requests against the Elements API, converts the response
|
5
|
+
# into a resource object or an error object accordingly.
|
6
|
+
class ElementsClient
|
7
|
+
include ::Elements::Logging
|
8
|
+
|
9
|
+
attr_reader :config
|
10
|
+
|
11
|
+
def initialize(config_arg = {})
|
12
|
+
@config = Elements.config.reverse_duplicate_merge(config_arg)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.current_thread_context
|
16
|
+
Thread.current[:elements_client__internal_use_only] ||= ThreadContext.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.active_client
|
20
|
+
current_thread_context.active_client || default_client
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.default_client
|
24
|
+
current_thread_context.default_client ||= ElementsClient.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def execute_request(method, path, params: {}, headers: {}, opts: {})
|
28
|
+
api_base = opts[:api_base] || config.api_base
|
29
|
+
api_key = opts[:api_key] || config.api_key
|
30
|
+
|
31
|
+
body_params, query_params = if %i[get head delete].include?(method)
|
32
|
+
[nil, params]
|
33
|
+
else
|
34
|
+
[params, nil]
|
35
|
+
end
|
36
|
+
|
37
|
+
query_params, path = merge_query_params(query_params, path)
|
38
|
+
query = encode_query_params(query_params)
|
39
|
+
|
40
|
+
headers = request_headers(api_key, method).update(headers)
|
41
|
+
|
42
|
+
body = body_params ? JSON.generate(body_params) : nil
|
43
|
+
|
44
|
+
url = api_url(path, query, api_base)
|
45
|
+
|
46
|
+
context = {
|
47
|
+
method: method,
|
48
|
+
path: path,
|
49
|
+
query: query,
|
50
|
+
body: body_params,
|
51
|
+
idempotency_key: headers['Idempotency-Key']
|
52
|
+
}
|
53
|
+
|
54
|
+
log_request(context)
|
55
|
+
resp = with_retries do
|
56
|
+
execute_request_with_rescues(method, url, headers, body, context)
|
57
|
+
end
|
58
|
+
log_response(context, resp)
|
59
|
+
resp
|
60
|
+
end
|
61
|
+
|
62
|
+
private def with_retries(config = Elements.config)
|
63
|
+
attempts = 0
|
64
|
+
begin
|
65
|
+
resp = yield
|
66
|
+
rescue StandardError => e
|
67
|
+
raise e unless self.class.should_retry?(e, attempts, config)
|
68
|
+
|
69
|
+
attempts += 1
|
70
|
+
sleep sleep_duration(attempts, config)
|
71
|
+
retry
|
72
|
+
end
|
73
|
+
resp
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.should_retry?(error, attempts, config)
|
77
|
+
return false if attempts >= config.max_network_retries
|
78
|
+
# retry if it is a connection issue
|
79
|
+
return true if error.is_a?(Elements::APIConnectionError)
|
80
|
+
# retry if it is a conflict, e.g., record not saved
|
81
|
+
return true if error.is_a?(Elements::ElementsError) && error.http_status == 409
|
82
|
+
# retry if service is temporarily unavailable
|
83
|
+
return true if error.is_a?(Elements::ElementsError) && error.http_status == 503
|
84
|
+
|
85
|
+
false
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.sleep_duration(attempts, config)
|
89
|
+
duration = config.min_network_retry_delay * (2**(attempts - 1))
|
90
|
+
# adding jitter, https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
|
91
|
+
duration = rand(duration)
|
92
|
+
duration = [config.min_network_retry_delay, duration].max
|
93
|
+
[config.max_network_retry_delay, duration].min
|
94
|
+
end
|
95
|
+
|
96
|
+
private def execute_request_with_rescues(method, url, headers, body, context)
|
97
|
+
resp, err = begin
|
98
|
+
context[:request_start] = Util.monotonic_time
|
99
|
+
[RestClient::Request.execute(method: method, url: url, headers: headers, payload: body, **request_options), nil]
|
100
|
+
rescue RestClient::ExceptionWithResponse => e
|
101
|
+
[e.response, e]
|
102
|
+
rescue StandardError => e
|
103
|
+
[nil, e]
|
104
|
+
end
|
105
|
+
|
106
|
+
if resp && resp.code >= 400
|
107
|
+
handle_error_with_response(resp, err, context)
|
108
|
+
elsif err
|
109
|
+
handle_error_without_response(err, context)
|
110
|
+
end
|
111
|
+
resp
|
112
|
+
end
|
113
|
+
|
114
|
+
private def handle_error_without_response(err, context)
|
115
|
+
if APIConnectionError::CAUSE_ERROR_CLASSES.any? { |cause| err.is_a?(cause) }
|
116
|
+
log_error('Network error',
|
117
|
+
error_message: err.message,
|
118
|
+
idempotency_key: context[:idempotency_key],
|
119
|
+
method: context[:method],
|
120
|
+
path: context[:path])
|
121
|
+
|
122
|
+
raise APIConnectionError,
|
123
|
+
"Encountered unexpected error #{err.class.name} " \
|
124
|
+
"while sending request, error message: #{err.message}"
|
125
|
+
end
|
126
|
+
|
127
|
+
# an unexpected error was raised
|
128
|
+
log_error('Unknown request error',
|
129
|
+
error_message: err.message,
|
130
|
+
idempotency_key: context[:idempotency_key],
|
131
|
+
method: context[:method],
|
132
|
+
path: context[:path])
|
133
|
+
raise err
|
134
|
+
end
|
135
|
+
|
136
|
+
private def handle_error_with_response(http_resp, err, context)
|
137
|
+
log_response(context, http_resp)
|
138
|
+
|
139
|
+
begin
|
140
|
+
resp = ElementsResponse.new(http_resp)
|
141
|
+
error_data = resp.data[:error]
|
142
|
+
|
143
|
+
raise ElementsError, 'Unknown error' unless error_data
|
144
|
+
rescue JSON::ParserError, ElementsError
|
145
|
+
raise GenericAPIError.new("Invalid response from Elements API: #{http_resp.body}, " \
|
146
|
+
"error message: #{err.message}",
|
147
|
+
http_status: http_resp.code,
|
148
|
+
http_headers: http_resp.headers,
|
149
|
+
http_body: http_resp.body)
|
150
|
+
end
|
151
|
+
|
152
|
+
log_error('Elements API error',
|
153
|
+
status: resp.status,
|
154
|
+
error_code: error_data[:code],
|
155
|
+
error_type: error_data[:type],
|
156
|
+
error_message: error_data[:message],
|
157
|
+
error_params: error_data[:data],
|
158
|
+
trace_id: error_data[:trace_id],
|
159
|
+
idempotency_key: context[:idempotency_key])
|
160
|
+
|
161
|
+
specific_error = api_error(http_resp, error_data)
|
162
|
+
specific_error.response = resp
|
163
|
+
raise specific_error
|
164
|
+
end
|
165
|
+
|
166
|
+
private def api_error(resp, error_data)
|
167
|
+
opts = {
|
168
|
+
http_body: resp.body,
|
169
|
+
http_headers: resp.headers,
|
170
|
+
http_status: resp.code
|
171
|
+
}
|
172
|
+
|
173
|
+
err_type = error_data[:type] || 'api_error'
|
174
|
+
error_class = Elements::Errors::ERROR_CODES_TO_TYPES[err_type.to_sym] || GenericAPIError
|
175
|
+
error_class.new(error_data[:message], **opts)
|
176
|
+
end
|
177
|
+
|
178
|
+
private def log_request(context)
|
179
|
+
log_info('Request info',
|
180
|
+
method: context[:method],
|
181
|
+
path: context[:path],
|
182
|
+
idempotency_key: context[:idempotency_key])
|
183
|
+
log_debug('Request details',
|
184
|
+
method: context[:method],
|
185
|
+
path: context[:path],
|
186
|
+
query: context[:query],
|
187
|
+
body: context[:body],
|
188
|
+
idempotency_key: context[:idempotency_key])
|
189
|
+
end
|
190
|
+
|
191
|
+
private def log_response(context, resp)
|
192
|
+
log_info('Response info',
|
193
|
+
method: context[:method],
|
194
|
+
path: context[:path],
|
195
|
+
idempotency_key: context[:idempotency_key],
|
196
|
+
elapsed: "#{(Util.monotonic_time - context[:request_start]) * 1_000}ms",
|
197
|
+
status: resp.code)
|
198
|
+
log_debug('Response details',
|
199
|
+
method: context[:method],
|
200
|
+
path: context[:path],
|
201
|
+
query: context[:query],
|
202
|
+
idempotency_key: context[:idempotency_key],
|
203
|
+
status: resp.code,
|
204
|
+
body: resp.body,
|
205
|
+
headers: resp.headers)
|
206
|
+
end
|
207
|
+
|
208
|
+
private def encode_query_params(params)
|
209
|
+
return nil if params.nil?
|
210
|
+
|
211
|
+
params.map { |k, v| "#{k}=#{v}" }.join('&')
|
212
|
+
end
|
213
|
+
|
214
|
+
private def request_headers(api_key, method)
|
215
|
+
headers = {}
|
216
|
+
headers['Authorization'] = "Bearer #{api_key}"
|
217
|
+
headers['Content-Type'] = 'application/json'
|
218
|
+
headers['Idempotency-Key'] ||= SecureRandom.uuid if %i[post delete].include?(method)
|
219
|
+
headers['User-Agent'] = "elements-ruby-sdk/#{Elements::VERSION}"
|
220
|
+
headers
|
221
|
+
end
|
222
|
+
|
223
|
+
private def request_options
|
224
|
+
options = {}
|
225
|
+
if config.ssl_verify_certs
|
226
|
+
options[:verify_ssl] = OpenSSL::SSL::VERIFY_PEER
|
227
|
+
options[:ssl_ca_file] = config.ssl_ca_file
|
228
|
+
else
|
229
|
+
options[:verify_ssl] = false
|
230
|
+
end
|
231
|
+
options
|
232
|
+
end
|
233
|
+
|
234
|
+
private def merge_query_params(query_params, path)
|
235
|
+
u = URI.parse(path)
|
236
|
+
return query_params, path if u.query.nil?
|
237
|
+
|
238
|
+
query_params ||= {}
|
239
|
+
query_params = query_params.merge(Hash[URI.decode_www_form(u.query)])
|
240
|
+
[query_params, u.path]
|
241
|
+
end
|
242
|
+
|
243
|
+
private def api_url(url, query, api_base = nil)
|
244
|
+
api_base ||= config.api_base
|
245
|
+
if query
|
246
|
+
"#{api_base}#{url}?#{query}"
|
247
|
+
else
|
248
|
+
"#{api_base}#{url}"
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
class ThreadContext
|
253
|
+
attr_accessor :active_client, :default_client
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elements
|
4
|
+
# Configurations:
|
5
|
+
#
|
6
|
+
# api_base: The base URL of the Elements API, see https://elements-pay.readme.io/reference/set-up-elements.
|
7
|
+
#
|
8
|
+
# api_key: The client token required to execute requests, see https://elements-pay.readme.io/reference/fetch-client-token.
|
9
|
+
#
|
10
|
+
# max_network_retries, min_network_retry_delay, max_network_retry_delay:
|
11
|
+
# The client has the ability to perform automatic retries with exponential backoff,
|
12
|
+
# you may configure how the client retries with these variables, setting max_network_retries to 0 disable retries.
|
13
|
+
#
|
14
|
+
# ssl_ca_file: The location of a file containing a bundle of CA certificates,
|
15
|
+
# the library included one under lib/elements/certs and will use that as default.
|
16
|
+
#
|
17
|
+
# ssl_verify_certs: You may disable SSL verification by setting this to false.
|
18
|
+
#
|
19
|
+
# logger: When set, the library will log execution information to the prompt location.
|
20
|
+
# You may change the verbosity by supplying a log_level config.
|
21
|
+
#
|
22
|
+
class ElementsConfiguration
|
23
|
+
attr_accessor :api_base, :api_key, :max_network_retries, :min_network_retry_delay, :max_network_retry_delay,
|
24
|
+
:ssl_ca_file, :ssl_verify_certs, :logger
|
25
|
+
attr_reader :log_level
|
26
|
+
|
27
|
+
SANDBOX_URL = 'https://api.elements-sandbox.io'
|
28
|
+
PRODUCTION_URL = 'https://api.elements.io'
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
@api_base = PRODUCTION_URL
|
32
|
+
# disable log by default
|
33
|
+
@logger = Logger.new('/dev/null')
|
34
|
+
@log_level = Logger::DEBUG
|
35
|
+
@max_network_retries = 0
|
36
|
+
@min_network_retry_delay = 0.5
|
37
|
+
@max_network_retry_delay = 2.0
|
38
|
+
@ssl_verify_certs = true
|
39
|
+
@ssl_ca_file = File.expand_path('certs/cacert.pem', __dir__)
|
40
|
+
end
|
41
|
+
|
42
|
+
def log_level=(severity)
|
43
|
+
if severity.is_a?(Integer)
|
44
|
+
@log_level = severity
|
45
|
+
else
|
46
|
+
log_level_map = {
|
47
|
+
'debug' => Logger::DEBUG,
|
48
|
+
'info' => Logger::INFO,
|
49
|
+
'warn' => Logger::WARN,
|
50
|
+
'error' => Logger::ERROR,
|
51
|
+
'fatal' => Logger::FATAL,
|
52
|
+
'unknown' => Logger::UNKNOWN
|
53
|
+
}
|
54
|
+
severity = severity.to_s.downcase
|
55
|
+
raise ArgumentError, "invalid log level: #{severity}" unless
|
56
|
+
log_level_map.key?(severity)
|
57
|
+
|
58
|
+
@log_level = log_level_map[severity]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def reverse_duplicate_merge(hash)
|
63
|
+
dup.tap do |instance|
|
64
|
+
hash.each do |option, value|
|
65
|
+
instance.public_send("#{option}=", value)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elements
|
4
|
+
# A mapping from the resource JSON to a PORO.
|
5
|
+
# Assumes the existence of attributes such as `id`, and encourages immutability.
|
6
|
+
class ElementsObject
|
7
|
+
PERMANENT_ATTRIBUTES = [:id].freeze
|
8
|
+
RESERVED_FIELD_NAMES = [:class].freeze
|
9
|
+
|
10
|
+
undef :id if method_defined?(:id)
|
11
|
+
|
12
|
+
def initialize(id, attributes = nil)
|
13
|
+
@id = id
|
14
|
+
@attributes = attributes || {}
|
15
|
+
add_readers(attributes)
|
16
|
+
end
|
17
|
+
|
18
|
+
def [](key)
|
19
|
+
@attributes[key.to_sym]
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_json(*opts)
|
23
|
+
JSON.generate(@attributes, opts)
|
24
|
+
end
|
25
|
+
|
26
|
+
def as_json(*opts)
|
27
|
+
@attributes.as_json(*opts)
|
28
|
+
end
|
29
|
+
|
30
|
+
protected def metaclass
|
31
|
+
class << self; self; end
|
32
|
+
end
|
33
|
+
|
34
|
+
protected def add_readers(attributes)
|
35
|
+
metaclass.instance_eval do
|
36
|
+
attributes.each_key do |k|
|
37
|
+
next if RESERVED_FIELD_NAMES.include?(k)
|
38
|
+
next if PERMANENT_ATTRIBUTES.include?(k)
|
39
|
+
|
40
|
+
if k == :method # avoid method name collision
|
41
|
+
define_method(k) { |*args| args.empty? ? @attributes[k] : super(*args) }
|
42
|
+
else
|
43
|
+
define_method(k) { @attributes[k] }
|
44
|
+
end
|
45
|
+
|
46
|
+
define_method(:"#{k}?") { @attributes[k] } if [FalseClass, TrueClass].include?(attributes[k].class)
|
47
|
+
end
|
48
|
+
|
49
|
+
define_method(:id) { @id }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
protected def respond_to_missing?(symbol, include_private = false)
|
54
|
+
@attributes && @attributes.key?(symbol) || super
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elements
|
4
|
+
class ElementsResponse
|
5
|
+
attr_reader :data, :body, :headers, :status
|
6
|
+
|
7
|
+
def initialize(http_resp)
|
8
|
+
@body = http_resp.body
|
9
|
+
@headers = http_resp.headers
|
10
|
+
@status = http_resp.code
|
11
|
+
@data = JSON.parse(http_resp.body, symbolize_names: true)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elements
|
4
|
+
# Contains the detail error information received from the Elements API,
|
5
|
+
# refer to https://elements-pay.readme.io/reference/errors for more information.
|
6
|
+
class ErrorDetails < ElementsObject
|
7
|
+
def type
|
8
|
+
@attributes[:type]
|
9
|
+
end
|
10
|
+
|
11
|
+
def code
|
12
|
+
@attributes[:code]
|
13
|
+
end
|
14
|
+
|
15
|
+
def message
|
16
|
+
@attributes[:message]
|
17
|
+
end
|
18
|
+
|
19
|
+
def trace_id
|
20
|
+
@attributes[:trace_id]
|
21
|
+
end
|
22
|
+
|
23
|
+
def psp_reference
|
24
|
+
@attributes[:psp_reference]
|
25
|
+
end
|
26
|
+
|
27
|
+
def param
|
28
|
+
@attributes[:param]
|
29
|
+
end
|
30
|
+
|
31
|
+
def decline_code
|
32
|
+
@attributes[:decline_code]
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.from_http_body(body)
|
36
|
+
return nil if body.nil?
|
37
|
+
|
38
|
+
json_body = JSON.parse(body, symbolize_names: true)
|
39
|
+
return nil unless json_body.key?(:error)
|
40
|
+
|
41
|
+
error = json_body[:error]
|
42
|
+
Util.convert_to_elements_object(error)
|
43
|
+
rescue JSON::ParserError
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class ElementsError < StandardError
|
49
|
+
attr_reader :message, :http_status, :http_headers, :http_body, :error
|
50
|
+
|
51
|
+
attr_accessor :response
|
52
|
+
|
53
|
+
def initialize(message = nil, http_status: nil, http_headers: nil, http_body: nil)
|
54
|
+
super(message)
|
55
|
+
@message = message
|
56
|
+
@http_status = http_status
|
57
|
+
@http_headers = http_headers
|
58
|
+
@http_body = http_body
|
59
|
+
@error = ErrorDetails.from_http_body(http_body)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class APIConnectionError < ElementsError
|
64
|
+
CAUSE_ERROR_CLASSES = [
|
65
|
+
EOFError,
|
66
|
+
Errno::ECONNREFUSED,
|
67
|
+
Errno::ECONNRESET,
|
68
|
+
Errno::EHOSTUNREACH,
|
69
|
+
Errno::ETIMEDOUT,
|
70
|
+
SocketError,
|
71
|
+
|
72
|
+
Net::OpenTimeout,
|
73
|
+
Net::ReadTimeout,
|
74
|
+
|
75
|
+
OpenSSL::SSL::SSLError,
|
76
|
+
|
77
|
+
RestClient::ServerBrokeConnection,
|
78
|
+
RestClient::SSLCertificateNotVerified,
|
79
|
+
RestClient::Exceptions::OpenTimeout,
|
80
|
+
RestClient::Exceptions::ReadTimeout
|
81
|
+
].freeze
|
82
|
+
end
|
83
|
+
|
84
|
+
class GenericAPIError < ElementsError; end
|
85
|
+
|
86
|
+
class InvalidRequestError < ElementsError; end
|
87
|
+
|
88
|
+
class IdempotencyError < ElementsError; end
|
89
|
+
|
90
|
+
class AuthenticationError < ElementsError; end
|
91
|
+
|
92
|
+
class PermissionError < ElementsError; end
|
93
|
+
|
94
|
+
class RateLimitError < ElementsError; end
|
95
|
+
|
96
|
+
class CardError < ElementsError; end
|
97
|
+
|
98
|
+
class InternalServerError < ElementsError; end
|
99
|
+
|
100
|
+
module Errors
|
101
|
+
ERROR_CODES_TO_TYPES = {
|
102
|
+
"api_error": Elements::GenericAPIError,
|
103
|
+
"api_connection_error": Elements::APIConnectionError,
|
104
|
+
"authentication_error": Elements::AuthenticationError,
|
105
|
+
"invalid_request_error": Elements::InvalidRequestError,
|
106
|
+
"card_error": Elements::CardError,
|
107
|
+
"idempotency_error": Elements::IdempotencyError,
|
108
|
+
"rate_limit_error": Elements::RateLimitError,
|
109
|
+
"more_permissions_required": Elements::PermissionError,
|
110
|
+
"internal_server_error": Elements::InternalServerError
|
111
|
+
}.freeze
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elements
|
4
|
+
module Logging
|
5
|
+
def log_error(message, data = {})
|
6
|
+
opts = data.delete(:opts) || {}
|
7
|
+
logger = opts.delete(:logger) || Elements.config.logger
|
8
|
+
log_level = Elements.config.log_level
|
9
|
+
|
10
|
+
log(message, data, level: Logger::ERROR, logger: logger) if logger && log_level <= Logger::ERROR
|
11
|
+
end
|
12
|
+
|
13
|
+
def log_warn(message, data = {})
|
14
|
+
opts = data.delete(:opts) || {}
|
15
|
+
logger = opts.delete(:logger) || Elements.config.logger
|
16
|
+
log_level = Elements.config.log_level
|
17
|
+
|
18
|
+
log(message, data, level: Logger::WARN, logger: logger) if logger && log_level <= Logger::WARN
|
19
|
+
end
|
20
|
+
|
21
|
+
def log_info(message, data = {})
|
22
|
+
opts = data.delete(:opts) || {}
|
23
|
+
logger = opts.delete(:logger) || Elements.config.logger
|
24
|
+
log_level = Elements.config.log_level
|
25
|
+
|
26
|
+
log(message, data, level: Logger::INFO, logger: logger) if logger && log_level <= Logger::INFO
|
27
|
+
end
|
28
|
+
|
29
|
+
def log_debug(message, data = {})
|
30
|
+
opts = data.delete(:opts) || {}
|
31
|
+
logger = opts.delete(:logger) || Elements.config.logger
|
32
|
+
log_level = Elements.config.log_level
|
33
|
+
|
34
|
+
log(message, data, level: Logger::DEBUG, logger: logger) if logger && log_level <= Logger::DEBUG
|
35
|
+
end
|
36
|
+
|
37
|
+
private def log(message, data = {}, level:, logger:)
|
38
|
+
data_str = data.map { |(k, v)| "#{k}=#{v.inspect}" }.join(' ')
|
39
|
+
log_str = "message=\"#{message}\" #{data_str}"
|
40
|
+
logger.log(level, log_str)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elements
|
4
|
+
module ObjectTypes
|
5
|
+
def self.object_names_to_classes
|
6
|
+
{
|
7
|
+
Charge::OBJECT_NAME => Charge,
|
8
|
+
Dispute::OBJECT_NAME => Dispute,
|
9
|
+
Refund::OBJECT_NAME => Refund,
|
10
|
+
PaymentMethod::OBJECT_NAME => PaymentMethod,
|
11
|
+
PaymentMethodGateway::OBJECT_NAME => PaymentMethodGateway,
|
12
|
+
Token::OBJECT_NAME => Token,
|
13
|
+
CheckoutSession::OBJECT_NAME => CheckoutSession,
|
14
|
+
Customer::OBJECT_NAME => Customer
|
15
|
+
}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elements
|
4
|
+
# All API resources, such as Charges, Refunds, are defined under lib/elements/resources.
|
5
|
+
#
|
6
|
+
# Each API resource should have an OBJECT_NAME, corresponding to the "type" in a JSON response of that resource.
|
7
|
+
#
|
8
|
+
# API endpoints are defined with `api_method`, e.g.,
|
9
|
+
#
|
10
|
+
# class MyAPIResource < APIResource
|
11
|
+
# api_method :create, method: :post, path: '/api/v1/my_api_resource'
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# To break it down:
|
15
|
+
# - `:create` defines the name of the class method that invokes the API request
|
16
|
+
# - `method: :post` defines the HTTP verb that is associated with this endpoint
|
17
|
+
# - `path: '/api/v1/my_api_resource'` defines the API path
|
18
|
+
#
|
19
|
+
# To invoke this API method:
|
20
|
+
#
|
21
|
+
# params = {
|
22
|
+
# foo: "bar"
|
23
|
+
# }
|
24
|
+
# headers = {
|
25
|
+
# MyHeaderVar: "value"
|
26
|
+
# }
|
27
|
+
# my_api_resource = MyAPIResource.create(params, headers)
|
28
|
+
#
|
29
|
+
# Here, `params` are the API params that will be encoded automatically according to the HTTP verb.
|
30
|
+
# You may also supply optional headers, common headers such as `Idempotency-Key`
|
31
|
+
# will be supplied with a default.
|
32
|
+
class APIResource < ElementsObject
|
33
|
+
class << self
|
34
|
+
def execute_resource_request(method, url, params = {}, headers = {}, opts = {})
|
35
|
+
client = opts[:client] || ElementsClient.active_client
|
36
|
+
|
37
|
+
client.execute_request(method, url,
|
38
|
+
params: params, headers: headers, opts: opts)
|
39
|
+
end
|
40
|
+
|
41
|
+
def api_method(name, method:, path:, parser: nil)
|
42
|
+
unless %i[get post delete].include?(method)
|
43
|
+
raise ArgumentError,
|
44
|
+
"Invalid method value: #{method.inspect}. Should be one " \
|
45
|
+
'of :get, :post or :delete.'
|
46
|
+
end
|
47
|
+
|
48
|
+
url_template = Addressable::Template.new(path)
|
49
|
+
if url_template.variables.include?('id')
|
50
|
+
define_resource_method(name, method, path, parser)
|
51
|
+
else
|
52
|
+
define_api_method(name, method, path, parser)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private def define_api_method(name, method, path, parser)
|
57
|
+
define_singleton_method(name) do |params = {}, headers = {}, opts = {}|
|
58
|
+
params = Elements::Util.symbolize_names(params)
|
59
|
+
execute_request(path, params, method, parser, headers, opts)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private def define_resource_method(name, method, path, parser)
|
64
|
+
define_singleton_method(name) do |id = nil, params = {}, headers = {}, opts = {}|
|
65
|
+
raise ArgumentError 'Missing source id' if id.nil?
|
66
|
+
|
67
|
+
params = Elements::Util.symbolize_names(params.merge!(id: id))
|
68
|
+
execute_request(path, params, method, parser, headers, opts)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private def execute_request(path, params, method, parser, headers, opts)
|
73
|
+
url_template = Addressable::Template.new(path)
|
74
|
+
path_params = {}
|
75
|
+
url_template.variables.each do |var|
|
76
|
+
raise ArgumentError, "Missing path param #{var}" unless params.key? var.to_sym
|
77
|
+
|
78
|
+
val = params.delete(var.to_sym)
|
79
|
+
path_params[var] = val
|
80
|
+
end
|
81
|
+
url = Addressable::Template.new(path).expand(path_params)&.to_str
|
82
|
+
|
83
|
+
resp = execute_resource_request(method, url, params, headers, opts)
|
84
|
+
json_body = JSON.parse(resp.body, symbolize_names: true)
|
85
|
+
if parser
|
86
|
+
parser.call(json_body)
|
87
|
+
else
|
88
|
+
Util.convert_to_elements_object(json_body)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elements
|
4
|
+
class Charge < APIResource
|
5
|
+
OBJECT_NAME = 'charge'
|
6
|
+
|
7
|
+
api_method :create, method: :post, path: '/api/v1/charges'
|
8
|
+
api_method :retrieve, method: :get, path: '/api/v1/charges/{id}'
|
9
|
+
api_method :list, method: :get, path: '/api/v1/charges'
|
10
|
+
api_method :capture, method: :post, path: '/api/v1/charges/{id}/capture'
|
11
|
+
api_method :cancel, method: :post, path: '/api/v1/charges/{id}/cancel'
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elements
|
4
|
+
class CheckoutSession < APIResource
|
5
|
+
OBJECT_NAME = 'checkout_session'
|
6
|
+
|
7
|
+
api_method :create, method: :post, path: '/api/v1/checkout/sessions'
|
8
|
+
api_method :retrieve, method: :get, path: '/api/v1/checkout/sessions/{id}'
|
9
|
+
api_method :list, method: :get, path: '/api/v1/checkout/sessions'
|
10
|
+
end
|
11
|
+
end
|