jamm 0.0.1

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,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