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.
@@ -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