jamm 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,249 @@
1
+ module Jamm
2
+ class JammObject
3
+ include Enumerable
4
+
5
+ @@permanent_attributes = Set.new([:id]) # rubocop:disable Style/ClassVars
6
+
7
+ # The default :id method is deprecated and isn't useful to us
8
+ undef :id if method_defined?(:id)
9
+
10
+ def initialize(id = nil)
11
+ # parameter overloading!
12
+ if id.is_a?(Hash)
13
+ @retrieve_params = id.dup
14
+ @retrieve_params.delete(:id)
15
+ id = id[:id]
16
+ else
17
+ @retrieve_params = {}
18
+ end
19
+
20
+ @values = {}
21
+ # This really belongs in APIResource, but not putting it there allows us
22
+ # to have a unified inspect method
23
+ @unsaved_values = Set.new
24
+ @transient_values = Set.new
25
+ @values[:id] = id if id
26
+ end
27
+
28
+ def self.construct_from(values)
29
+ new(values[:id]).refresh_from(values)
30
+ end
31
+
32
+ def to_s(*_args)
33
+ JSON.pretty_generate(@values)
34
+ end
35
+
36
+ def inspect
37
+ id_string = respond_to?(:id) && !id.nil? ? " id=#{id}" : ''
38
+ "#<#{self.class}:0x#{object_id.to_s(16)}#{id_string}> JSON: " + JSON.pretty_generate(@values)
39
+ end
40
+
41
+ def refresh_from(values, partial: false)
42
+ @original_values = Marshal.load(Marshal.dump(values)) # deep copy
43
+ removed = partial ? Set.new : Set.new(@values.keys - values.keys)
44
+ added = Set.new(values.keys - @values.keys)
45
+ # Wipe old state before setting new. This is useful for e.g. updating a
46
+ # customer, where there is no persistent card parameter. Mark those values
47
+ # which don't persist as transient
48
+
49
+ instance_eval do
50
+ remove_accessors(removed)
51
+ add_accessors(added)
52
+ end
53
+ removed.each do |k|
54
+ @values.delete(k)
55
+ @transient_values.add(k)
56
+ @unsaved_values.delete(k)
57
+ end
58
+ values.each do |k, v|
59
+ @values[k] = Util.convert_to_jamm_object(v)
60
+ @transient_values.delete(k)
61
+ @unsaved_values.delete(k)
62
+ end
63
+
64
+ self
65
+ end
66
+
67
+ def [](k)
68
+ @values[k.to_sym]
69
+ end
70
+
71
+ def []=(k, v)
72
+ send(:"#{k}=", v)
73
+ end
74
+
75
+ def keys
76
+ @values.keys
77
+ end
78
+
79
+ def values
80
+ @values.values
81
+ end
82
+
83
+ def to_json(*_a)
84
+ JSON.generate(@values)
85
+ end
86
+
87
+ def as_json(*a)
88
+ @values.as_json(*a)
89
+ end
90
+
91
+ def to_hash
92
+ @values.transform_values do |value|
93
+ value.respond_to?(:to_hash) ? value.to_hash : value
94
+ end
95
+ end
96
+
97
+ def each(&blk)
98
+ @values.each(&blk)
99
+ end
100
+
101
+ def _dump(_level)
102
+ Marshal.dump(@values)
103
+ end
104
+
105
+ def self._load(args)
106
+ values = Marshal.load(args)
107
+ construct_from(values)
108
+ end
109
+
110
+ def serialize_nested_object(key)
111
+ new_value = @values[key]
112
+ return {} if new_value.is_a?(APIResource)
113
+
114
+ if @unsaved_values.include?(key)
115
+ # the object has been reassigned
116
+ # e.g. as object.key = {foo => bar}
117
+ update = new_value
118
+ new_keys = update.keys.map(&:to_sym)
119
+
120
+ # remove keys at the server, but not known locally
121
+ if @original_values.include?(key)
122
+ keys_to_unset = @original_values[key].keys - new_keys
123
+ keys_to_unset.each { |unset_key| update[unset_key] = '' }
124
+ end
125
+
126
+ update
127
+ else
128
+ # can be serialized normally
129
+ self.class.serialize_params(new_value)
130
+ end
131
+ end
132
+
133
+ def self.serialize_params(obj, original_value = nil)
134
+ case obj
135
+ when nil
136
+ ''
137
+ when JammObject
138
+ unsaved_keys = obj.instance_variable_get(:@unsaved_values)
139
+ obj_values = obj.instance_variable_get(:@values)
140
+ update_hash = {}
141
+
142
+ unsaved_keys.each do |k|
143
+ update_hash[k] = serialize_params(obj_values[k])
144
+ end
145
+
146
+ obj_values.each do |k, v|
147
+ if v.is_a?(JammObject) || v.is_a?(Hash)
148
+ update_hash[k] = obj.serialize_nested_object(k)
149
+ elsif v.is_a?(Array)
150
+ original_value = obj.instance_variable_get(:@original_values)[k]
151
+ if original_value && original_value.length > v.length
152
+ # url params provide no mechanism for deleting an item in an array,
153
+ # just overwriting the whole array or adding new items. So let's not
154
+ # allow deleting without a full overwrite until we have a solution.
155
+ raise ArgumentError, 'You cannot delete an item from an array, you must instead set a new array'
156
+ end
157
+
158
+ update_hash[k] = serialize_params(v, original_value)
159
+ end
160
+ end
161
+
162
+ update_hash
163
+ when Array
164
+ update_hash = {}
165
+ obj.each_with_index do |value, index|
166
+ update = serialize_params(value)
167
+ update_hash[index] = update if update != {} && (!original_value || update != original_value[index])
168
+ end
169
+
170
+ if update_hash == {}
171
+ nil
172
+ else
173
+ update_hash
174
+ end
175
+ else
176
+ obj
177
+ end
178
+ end
179
+
180
+ protected
181
+
182
+ def metaclass
183
+ class << self; self; end
184
+ end
185
+
186
+ def remove_accessors(keys)
187
+ metaclass.instance_eval do
188
+ keys.each do |k|
189
+ next if @@permanent_attributes.include?(k)
190
+
191
+ k_eq = :"#{k}="
192
+ remove_method(k) if method_defined?(k)
193
+ remove_method(k_eq) if method_defined?(k_eq)
194
+ end
195
+ end
196
+ end
197
+
198
+ def add_accessors(keys)
199
+ metaclass.instance_eval do
200
+ keys.each do |k|
201
+ next if @@permanent_attributes.include?(k)
202
+
203
+ k_eq = :"#{k}="
204
+ define_method(k) { @values[k] }
205
+ define_method(k_eq) do |v|
206
+ if v == ''
207
+ raise ArgumentError, "You cannot set #{k} to an empty string." \
208
+ 'We interpret empty strings as nil in requests.' \
209
+ "You may set #{self}.#{k} = nil to delete the property."
210
+ end
211
+ @values[k] = v
212
+ @unsaved_values.add(k)
213
+ end
214
+ end
215
+ end
216
+ end
217
+
218
+ def method_missing(name, *args)
219
+ if name.to_s.end_with?('=')
220
+ attr = name.to_s[0...-1].to_sym
221
+ add_accessors([attr])
222
+ begin
223
+ mth = method(name)
224
+ rescue NameError
225
+ raise NoMethodError,
226
+ "Cannot set #{attr} on this object. HINT: you can't set: #{@@permanent_attributes.to_a.join(', ')}"
227
+ end
228
+ return mth.call(args[0])
229
+ elsif @values.key?(name)
230
+ return @values[name]
231
+ end
232
+
233
+ begin
234
+ super
235
+ rescue NoMethodError => e
236
+ if @transient_values.include?(name)
237
+ raise NoMethodError,
238
+ e.message + ". HINT: The '#{name}' attribute was set in the past, however. It was then wiped when refreshing the object with the result returned by Jamm's API, probably as a result of a save(). The attributes currently available on this object are: #{@values.keys.join(', ')}"
239
+ end
240
+
241
+ raise
242
+ end
243
+ end
244
+
245
+ def respond_to_missing?(symbol, include_private = false)
246
+ @values&.key?(symbol) || super
247
+ end
248
+ end
249
+ end
data/lib/jamm/oauth.rb ADDED
@@ -0,0 +1,66 @@
1
+ require 'rest-client'
2
+ require 'json'
3
+ require 'base64'
4
+ require 'jamm/errors'
5
+
6
+ module Jamm
7
+ module OAuth
8
+ def self.oauth_token
9
+ client_id = Jamm.client_id
10
+ client_secret = Jamm.client_secret
11
+ if client_id.nil? || client_secret.nil?
12
+ raise OAuthError, 'No client_id or client_secret is set. ' \
13
+ 'Set your merchant client id and client secret using' \
14
+ 'Jamm.client_id=<your client id> and Jamm=<your client secret>'
15
+ end
16
+
17
+ fetch_oauth_token(client_id, client_secret)
18
+ end
19
+
20
+ def self.fetch_oauth_token(client_id, client_secret)
21
+ oauth_base = Jamm.oauth_base
22
+ oauth_endpoint = "#{oauth_base}/oauth2/token"
23
+ read_timeout = Jamm.read_timeout
24
+ open_timeout = Jamm.open_timeout
25
+
26
+ headers = {
27
+ authorization: "Basic #{Base64.strict_encode64("#{client_id}:#{client_secret}")}",
28
+ content_type: 'application/x-www-form-urlencoded'
29
+ }
30
+ payload = {
31
+ grant_type: 'client_credentials',
32
+ client_id: client_id
33
+ }
34
+ begin
35
+ response = Jamm.execute_request(method: :post, url: oauth_endpoint, payload: payload, headers: headers,
36
+ read_timeout: read_timeout, open_timeout: open_timeout)
37
+ rescue SocketError
38
+ raise OAuthError, 'An unexpected error happens while communicating to OAuth server. Check your network setting'
39
+ rescue RestClient::RequestTimeout
40
+ raise OAuthError, "Timed out over #{read_timeout} sec."
41
+ rescue RestClient::ExceptionWithResponse => e
42
+ raise OAuthError.new 'An unsuccessful response was returned', http_status: e.http_code, http_body: e.http_body
43
+ rescue RestClient::Exception => e
44
+ raise OAuthError, "An unexpected error happens while communicating to OAuth server. #{e}"
45
+ end
46
+
47
+ parse_oauth_response(response)
48
+ end
49
+
50
+ def self.parse_oauth_response(response)
51
+ begin
52
+ body = JSON.parse(response.body)
53
+ rescue JSON::ParserError
54
+ raise OAuthError.new 'Failed to parse the response from the OAuth server', http_status: response.code,
55
+ http_body: response.body
56
+ end
57
+ access_token = body['access_token']
58
+ if access_token.nil?
59
+ raise OAuthError.new 'An access token was not found in the response from the OAuth server',
60
+ http_status: response.code, http_body: response.body
61
+ end
62
+
63
+ access_token
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,18 @@
1
+ module Jamm
2
+ class Payment < APIResource
3
+ def self.create(params = {}, headers = {})
4
+ params[:redirect][:expired_at] = TimeUtil.relative_time_to_iso(params[:redirect][:expired_in]) if params[:redirect] && params[:redirect][:expired_in]
5
+
6
+ request_jamm_api(
7
+ method: :post,
8
+ path: resource_url,
9
+ params: params,
10
+ headers: headers
11
+ )
12
+ end
13
+
14
+ def self.resource_url
15
+ '/payments'
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ module Jamm
2
+ module Request
3
+ module ClassMethods
4
+ def request_jamm_api(method:, path:, params: {}, headers: {})
5
+ Jamm.request_jamm_api(method, path, params, headers)
6
+ end
7
+ end
8
+
9
+ def self.included(base)
10
+ base.extend(ClassMethods)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,41 @@
1
+ require 'time'
2
+ module Jamm
3
+ class TimeUtil
4
+ def self.relative_time_to_iso(relative_time)
5
+ # Check if the input string is in ISO format
6
+ return relative_time if iso_format?(relative_time)
7
+
8
+ # Parse the relative time string
9
+ quantity, unit = relative_time.split
10
+ quantity = quantity.to_i
11
+
12
+ # Calculate the time delta
13
+ case unit.downcase
14
+ when 'second', 'seconds'
15
+ time_delta = quantity
16
+ when 'minute', 'minutes'
17
+ time_delta = quantity * 60
18
+ when 'hour', 'hours'
19
+ time_delta = quantity * 60 * 60
20
+ when 'day', 'days'
21
+ time_delta = quantity * 60 * 60 * 24
22
+ when 'week', 'weeks'
23
+ time_delta = quantity * 60 * 60 * 24 * 7
24
+ else
25
+ raise ArgumentError, "Unknown time unit: #{unit}"
26
+ end
27
+
28
+ # Calculate the ISO datetime string
29
+ iso_time = Time.now + time_delta
30
+ iso_time.strftime('%Y-%m-%dT%H:%M:%SZ')
31
+ end
32
+
33
+ def self.iso_format?(string)
34
+ # Check if the string matches the ISO 8601 datetime format
35
+
36
+ !!Time.iso8601(string)
37
+ rescue StandardError
38
+ false
39
+ end
40
+ end
41
+ end
data/lib/jamm/token.rb ADDED
@@ -0,0 +1,10 @@
1
+ module Jamm
2
+ class Token < APIResource
3
+ extend Jamm::APIOperations::List
4
+ extend Jamm::APIOperations::Get
5
+
6
+ def self.resource_url
7
+ '/payments/tokens'
8
+ end
9
+ end
10
+ end
data/lib/jamm/util.rb ADDED
@@ -0,0 +1,76 @@
1
+ module Jamm
2
+ module Util
3
+ def self.convert_to_jamm_object(response)
4
+ case response
5
+ when Array
6
+ response.map { |i| convert_to_jamm_object(i) }
7
+ when Hash
8
+ JammObject.construct_from(response)
9
+ else
10
+ response
11
+ end
12
+ end
13
+
14
+ def self.symbolize_names(object)
15
+ case object
16
+ when Hash
17
+ new_hash = {}
18
+ object.each do |key, value|
19
+ key = begin
20
+ key.to_sym
21
+ rescue StandardError
22
+ key
23
+ end || key
24
+ new_hash[key] = symbolize_names(value)
25
+ end
26
+ new_hash
27
+ when Array
28
+ object.map { |value| symbolize_names(value) }
29
+ else
30
+ object
31
+ end
32
+ end
33
+
34
+ def self.url_encode(key)
35
+ # URI.escape is obsolete, so just use the code fragment in URI library
36
+ # (from URI::RFC2396_Parser#escape)
37
+ key.to_s.gsub(Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")) do
38
+ us = ::Regexp.last_match(0)
39
+ tmp = ''
40
+ us.each_byte do |uc|
41
+ tmp << format('%%%02X', uc)
42
+ end
43
+ tmp
44
+ end.force_encoding(Encoding::US_ASCII)
45
+ end
46
+
47
+ def self.flatten_params(params, parent_key = nil)
48
+ result = []
49
+ params.each do |key, value|
50
+ calculated_key = parent_key ? "#{parent_key}[#{url_encode(key)}]" : url_encode(key)
51
+ if value.is_a?(Hash)
52
+ result += flatten_params(value, calculated_key)
53
+ elsif value.is_a?(Array)
54
+ result += flatten_params_array(value, calculated_key)
55
+ else
56
+ result << [calculated_key, value]
57
+ end
58
+ end
59
+ result
60
+ end
61
+
62
+ def self.flatten_params_array(value, calculated_key)
63
+ result = []
64
+ value.each do |elem|
65
+ if elem.is_a?(Hash)
66
+ result += flatten_params(elem, calculated_key)
67
+ elsif elem.is_a?(Array)
68
+ result += flatten_params_array(elem, calculated_key)
69
+ else
70
+ result << ["#{calculated_key}[]", elem]
71
+ end
72
+ end
73
+ result
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,3 @@
1
+ module Jamm
2
+ VERSION = '0.0.1'
3
+ end
data/lib/jamm.rb ADDED
@@ -0,0 +1,146 @@
1
+ # FIXME: remove this line once manual testing completed
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rest-client'
4
+ require 'openssl'
5
+ require 'json'
6
+
7
+ require 'jamm/api_operations/create'
8
+ require 'jamm/api_operations/get'
9
+ require 'jamm/api_operations/list'
10
+ require 'jamm/api_operations/update'
11
+ require 'jamm/version'
12
+ require 'jamm/oauth'
13
+ require 'jamm/request'
14
+ require 'jamm/api_resource'
15
+ require 'jamm/token'
16
+ require 'jamm/payment'
17
+ require 'jamm/charge'
18
+ require 'jamm/util'
19
+ require 'jamm/jamm_object'
20
+ require 'jamm/errors'
21
+ require 'jamm/time_util'
22
+
23
+ module Jamm
24
+ @api_base = 'http://api.jamm-pay.jp/api/v0'
25
+ @oauth_base = 'https://jamm-merchant-sandbox.auth.ap-northeast-1.amazoncognito.com'
26
+ @open_timeout = 30
27
+ @read_timeout = 90
28
+ @max_retry = 0
29
+ @retry_initial_delay = 0.1
30
+ @retry_max_delay = 32
31
+
32
+ class << self
33
+ attr_accessor :client_id, :client_secret, :api_base, :oauth_base, :api_version, :connect_base,
34
+ :open_timeout, :read_timeout, :max_retry, :retry_initial_delay, :retry_max_delay
35
+ end
36
+
37
+ def self.request_jamm_api(method, path, params = {}, headers = {})
38
+ access_token = OAuth.oauth_token
39
+ api_base_url = @api_base
40
+ open_timeout = @open_timeout
41
+ read_timeout = @read_timeout
42
+ max_retry = @max_retry
43
+ retry_initial_delay = @retry_initial_delay
44
+ retry_max_delay = @retry_max_delay
45
+
46
+ headers = {
47
+ authorization: "Bearer #{access_token}",
48
+ content_type: 'application/json',
49
+ X_JAMM_SDK_VERSION: "Ruby_#{VERSION}"
50
+ }.update(headers)
51
+ error_on_invalid_params(params)
52
+
53
+ url = api_url(path, api_base_url)
54
+
55
+ case method.to_s.downcase.to_sym
56
+ when :get, :head, :delete
57
+ # Make params into GET parameters
58
+ url + "#{URI.parse(url).query ? '&' : '?'}#{uri_encode(params)}" if params&.any?
59
+ payload = nil
60
+ else
61
+ payload = params.to_json
62
+ end
63
+
64
+ retry_count = 1
65
+
66
+ begin
67
+ response = execute_request(method: method, url: url, payload: payload, headers: headers,
68
+ read_timeout: read_timeout, open_timeout: open_timeout)
69
+ rescue SocketError
70
+ raise ApiConnectionError,
71
+ 'An unexpected error happens while communicating to OAuth server. Check your network setting'
72
+ rescue RestClient::RequestTimeout
73
+ raise ApiConnectionError, "Timed out over #{read_timeout} sec."
74
+ rescue RestClient::ExceptionWithResponse => e
75
+ if (e.http_code == 429) && (retry_count <= max_retry)
76
+ sleep get_retry_delay(retry_count, retry_initial_delay, retry_max_delay)
77
+ retry_count += 1
78
+ retry
79
+ end
80
+
81
+ handle_api_error(e.http_code, e.http_body)
82
+ rescue RestClient::Exception => e
83
+ raise ApiError, "An unexpected error happens while communicating to Jamm server. #{e}"
84
+ end
85
+
86
+ parse(response)
87
+ end
88
+
89
+ def self.handle_api_error(http_code, http_body)
90
+ begin
91
+ error_obj = JSON.parse(http_body)
92
+ error_obj = Util.symbolize_names(error_obj)
93
+ error = error_obj[:error] or raise(ApiError) # escape from parsing
94
+ rescue JSON::ParserError, ApiError => e
95
+ raise ApiError.new "An unexpected error happens while parsing the error response. #{e}", http_status: http_code,
96
+ http_body: http_body
97
+ end
98
+
99
+ case http_code
100
+ when 400, 404
101
+ raise InvalidRequestError.new error, http_status: http_code, http_body: http_body, json_body: error_obj
102
+ when 401
103
+ raise AuthenticationError.new error, http_status: http_code, http_body: http_body, json_body: error_obj
104
+ else
105
+ raise ApiError.new error, http_status: http_code, http_body: http_body, json_body: error_obj
106
+ end
107
+ end
108
+
109
+ def self.parse(response)
110
+ begin
111
+ response = JSON.parse(response.body)
112
+ rescue JSON::ParserError
113
+ raise ApiError.new "Invalid response object from API: #{response.inspect}. HTTP response code: #{response.code})",
114
+ http_status: response.code, http_body: response.body
115
+ end
116
+
117
+ converted_response = Util.symbolize_names(response)
118
+ Util.convert_to_jamm_object(converted_response)
119
+ end
120
+
121
+ def self.execute_request(option)
122
+ RestClient::Request.execute(option)
123
+ end
124
+
125
+ def self.get_retry_delay(retry_count, retry_initial_delay, retry_max_delay)
126
+ # Exponential backoff
127
+ [retry_max_delay, retry_initial_delay * 2**retry_count].min
128
+ end
129
+
130
+ def self.uri_encode(params)
131
+ Util.flatten_params(params)
132
+ .map { |k, v| "#{k}=#{Util.url_encode(v)}" }.join('&')
133
+ end
134
+
135
+ def self.api_url(url, api_base_url)
136
+ api_base_url + url
137
+ end
138
+
139
+ def self.error_on_invalid_params(params)
140
+ return if params.nil? || params.is_a?(Hash)
141
+
142
+ raise ArgumentError,
143
+ 'request params should be either a Hash or nil ' \
144
+ "(was a #{params.class})"
145
+ end
146
+ end
@@ -0,0 +1,56 @@
1
+ require File.expand_path('../test_helper', __dir__)
2
+
3
+ module Jamm
4
+ class ChargeTest < Test::Unit::TestCase
5
+ should 'return a list of charge when Charge.list return valid response' do
6
+ # Arrange
7
+ @mock.expects(:post).once.returns(test_response(test_cognito_response))
8
+ @mock.expects(:get).once.returns(test_response(test_charges))
9
+ # Act
10
+ actual = Jamm::Charge.list
11
+ # Assert
12
+ assert actual.is_a? Array
13
+ assert_equal actual[0].charge_id, 'ct_h5e599d3-0000-4d46-9a52-1a37e7b5b8ef'
14
+ assert_equal actual[0].contract_id, 'dt_f5e599d3-8b9e-4d46-9a52-1a37e7b5b8ef'
15
+ assert_equal actual[0].status, 'CREATED'
16
+ assert_equal actual[0].description, 'Gold Gym'
17
+ assert_equal actual[0].currency, 'JPY'
18
+ assert_equal actual[0].amount, 1000
19
+ assert_equal actual[0].amount_refunded, 800
20
+ assert_equal actual[0].merchant_metadata.key1, 'value1'
21
+ assert_equal actual[0].merchant_metadata.key2, 'value2'
22
+ assert_equal actual[0].created_at, '2023-11-07T15:30:00.000+03:00'
23
+ assert_equal actual[0].updated_at, '2023-11-11T15:30:00.000+03:00'
24
+ end
25
+
26
+ should 'raise an error when Charge.list return timeout' do
27
+ # Arrange
28
+ @mock.expects(:post).once.returns(test_response(test_cognito_response))
29
+ @mock.expects(:get).once.raises(RestClient::RequestTimeout)
30
+ # Assert
31
+ assert_raise ApiConnectionError do
32
+ Jamm::Charge.list
33
+ end
34
+ end
35
+
36
+ should 'return a charge when Charge.get return valid response' do
37
+ # Arrange
38
+ @mock.expects(:post).once.returns(test_response(test_cognito_response))
39
+ @mock.expects(:get).once.returns(test_response(test_charge))
40
+ # Act
41
+ actual = Jamm::Charge.get('ct_h5e599d3-0000-4d46-9a52-1a37e7b5b8ef')
42
+ # Assert
43
+ assert_equal actual.charge_id, 'ct_h5e599d3-0000-4d46-9a52-1a37e7b5b8ef'
44
+ assert_equal actual.contract_id, 'dt_f5e599d3-8b9e-4d46-9a52-1a37e7b5b8ef'
45
+ assert_equal actual.status, 'CREATED'
46
+ assert_equal actual.description, 'Gold Gym'
47
+ assert_equal actual.currency, 'JPY'
48
+ assert_equal actual.amount, 1000
49
+ assert_equal actual.amount_refunded, 800
50
+ assert_equal actual.merchant_metadata.key1, 'value1'
51
+ assert_equal actual.merchant_metadata.key2, 'value2'
52
+ assert_equal actual.created_at, '2023-11-07T15:30:00.000+03:00'
53
+ assert_equal actual.updated_at, '2023-11-11T15:30:00.000+03:00'
54
+ end
55
+ end
56
+ end