active_merchant_gmo_pg 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.
@@ -0,0 +1,642 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'gmo_pg_error_messages'
4
+
5
+ module ActiveMerchant
6
+ module Billing
7
+ class GmoPgGateway < Gateway
8
+ self.test_url = 'https://pt01.mul-pay.jp'
9
+ self.live_url = 'https://p01.mul-pay.jp'
10
+
11
+ self.supported_countries = [ 'JP' ]
12
+ self.default_currency = 'JPY'
13
+ self.supported_cardtypes = %i[visa master american_express jcb diners_club]
14
+ self.money_format = :cents
15
+
16
+ self.homepage_url = 'https://www.gmo-pg.com/'
17
+ self.display_name = 'GMO Payment Gateway'
18
+
19
+ STANDARD_ERROR_CODE_MAPPING = {
20
+ 'E01' => STANDARD_ERROR_CODE[:invalid_number],
21
+ 'E11' => STANDARD_ERROR_CODE[:card_declined],
22
+ 'E61' => STANDARD_ERROR_CODE[:invalid_cvc],
23
+ 'E91' => STANDARD_ERROR_CODE[:processing_error]
24
+ }.freeze
25
+
26
+ # GMO-PGエラーコード
27
+ MEMBER_NOT_FOUND_ERROR = 'E01390002'
28
+
29
+ # GMO-PGゲートウェイを初期化します
30
+ #
31
+ # @param options [Hash] 認証情報を含むオプション
32
+ # @option options [String] :shop_id ショップID(必須)
33
+ # @option options [String] :shop_pass ショップパスワード(必須)
34
+ # @option options [String] :site_id サイトID(必須)
35
+ # @option options [String] :site_pass サイトパスワード(必須)
36
+ # @option options [Boolean] :test テスト環境を使用する場合はtrue
37
+ def initialize(options = {})
38
+ requires!(options, :shop_id, :shop_pass, :site_id, :site_pass)
39
+ super
40
+
41
+ host = test? ? 'pt01.mul-pay.jp' : 'p01.mul-pay.jp'
42
+
43
+ @site_client = GMO::Payment::SiteAPI.new(
44
+ site_id: @options[:site_id],
45
+ site_pass: @options[:site_pass],
46
+ host: host
47
+ )
48
+
49
+ @shop_client = GMO::Payment::ShopAPI.new(
50
+ shop_id: @options[:shop_id],
51
+ shop_pass: @options[:shop_pass],
52
+ host: host
53
+ )
54
+ end
55
+
56
+ # クレジットカード情報を登録します
57
+ # 会員が存在しない場合は自動的に会員登録も行います
58
+ #
59
+ # @param payment [String, CreditCard] トークンまたはクレジットカードオブジェクト
60
+ # @param options [Hash] オプション
61
+ # @option options [String] :member_id 会員ID(省略時は自動生成)
62
+ # @option options [String] :member_name 会員名(省略時はmember_idを使用)
63
+ # @option options [String] :card_pass カードパスワード
64
+ # @return [Response] レスポンスオブジェクト。authorizationに"member_id|card_seq"形式の識別子が含まれます
65
+ #
66
+ # @example トークンを使用したカード登録
67
+ # gateway.store("token_string", { member_id: "user_001" })
68
+ #
69
+ # @example クレジットカードオブジェクトを使用したカード登録
70
+ # card = ActiveMerchant::Billing::CreditCard.new(
71
+ # number: "4111111111111111",
72
+ # month: 12,
73
+ # year: 2025,
74
+ # verification_value: "123",
75
+ # first_name: "Taro",
76
+ # last_name: "Yamada"
77
+ # )
78
+ # gateway.store(card, { member_id: "user_001" })
79
+ def store(payment, options = {})
80
+ member_id = options[:member_id] || generate_unique_member_id
81
+ member_name = options[:member_name] || member_id
82
+
83
+ begin
84
+ @site_client.search_member(member_id: member_id)
85
+ rescue GMO::Payment::APIError => e
86
+ if e.error_info["ErrInfo"]&.include?(MEMBER_NOT_FOUND_ERROR)
87
+ begin
88
+ @site_client.save_member(member_id: member_id, member_name: member_name)
89
+ rescue GMO::Payment::APIError => save_error
90
+ return error_response(parse_error_message(save_error))
91
+ end
92
+ else
93
+ return error_response(parse_error_message(e))
94
+ end
95
+ end
96
+
97
+ card_params = {
98
+ member_id: member_id,
99
+ seq_mode: 1
100
+ }
101
+
102
+ if payment.is_a?(String)
103
+ card_params[:token] = payment
104
+ else
105
+ card_params[:card_no] = payment.number
106
+ card_params[:expire] = expdate(payment)
107
+ card_params[:holder_name] = payment.name if payment.name.present?
108
+ card_params[:security_code] = payment.verification_value if payment.verification_value.present?
109
+ end
110
+
111
+ card_params[:card_pass] = options[:card_pass] if options[:card_pass]
112
+
113
+ begin
114
+ result = @site_client.save_card(card_params)
115
+ card_seq = result['CardSeq']
116
+ authorization = "#{member_id}|#{card_seq}"
117
+
118
+ Response.new(
119
+ true,
120
+ 'カード登録に成功しました',
121
+ result,
122
+ authorization: authorization,
123
+ test: test?
124
+ )
125
+ rescue GMO::Payment::APIError => e
126
+ error_response(parse_error_message(e))
127
+ end
128
+ end
129
+
130
+ # 登録済みのクレジットカードを削除します
131
+ #
132
+ # @param identification [String] カード識別子("member_id|card_seq"形式)
133
+ # @param options [Hash] オプション(現在未使用)
134
+ # @return [Response] レスポンスオブジェクト
135
+ #
136
+ # @example カード削除
137
+ # gateway.unstore("user_001|0")
138
+ def unstore(identification, options = {})
139
+ member_id, card_seq = split_authorization(identification)
140
+ return error_response('無効な識別子フォーマットです') unless member_id && card_seq
141
+
142
+ begin
143
+ @site_client.delete_card(member_id: member_id, card_seq: card_seq, seq_mode: 1)
144
+ Response.new(true, 'カード削除に成功しました', {}, test: test?)
145
+ rescue GMO::Payment::APIError => e
146
+ error_response(parse_error_message(e))
147
+ end
148
+ end
149
+
150
+ # オーソリ(仮売上)を実行します
151
+ # 後でcaptureメソッドで売上計上する必要があります
152
+ #
153
+ # @param money [Integer] 金額(円単位)
154
+ # @param payment_source [String, CreditCard] トークン、"member_id|card_seq"形式の識別子、またはクレジットカードオブジェクト
155
+ # @param options [Hash] オプション
156
+ # @option options [String] :order_id 注文ID(省略時は自動生成)
157
+ # @option options [Integer] :tax 税額
158
+ # @option options [Integer] :method 支払方法(デフォルト: 1=一括払い)
159
+ # @option options [Integer] :pay_times 分割回数
160
+ # @option options [String] :ret_url 3DS認証後の戻りURL
161
+ # @option options [String] :callback_type コールバックタイプ
162
+ # @option options [String] :td_tenant_name 3DSテナント名
163
+ # @option options [String] :tds2_type 3DS2.0未対応時取り扱い(デフォルト: '3'=通常オーソリ実施)
164
+ # @option options [String] :td_required 決済時3DS必須タイプ(デフォルト: '0'=契約内容に従う、'1'=3DS認証必須、'2'=3DS認証必須ではない)
165
+ # @option options [String] :tds2_email メールアドレス(3DS2.0用)
166
+ # @option options [String] :tds2_mobile_phone_cc 携帯電話国コード(3DS2.0用)
167
+ # @option options [String] :tds2_mobile_phone_no 携帯電話番号(3DS2.0用)
168
+ # @option options [String] :tds2_ship_addr_city 配送先都市(3DS2.0用)
169
+ # @option options [String] :tds2_ship_addr_country 配送先国(3DS2.0用)
170
+ # @option options [String] :tds2_ship_addr_line1 配送先住所1(3DS2.0用)
171
+ # @option options [String] :tds2_ship_addr_post_code 配送先郵便番号(3DS2.0用)
172
+ # @option options [String] :tds2_bill_addr_city 請求先都市(3DS2.0用)
173
+ # @option options [String] :tds2_bill_addr_country 請求先国(3DS2.0用)
174
+ # @option options [String] :tds2_bill_addr_line1 請求先住所1(3DS2.0用)
175
+ # @option options [String] :tds2_bill_addr_post_code 請求先郵便番号(3DS2.0用)
176
+ # @option options [String] :tds2_home_phone_cc 自宅電話国コード(3DS2.0用)
177
+ # @option options [String] :tds2_home_phone_no 自宅電話番号(3DS2.0用)
178
+ # @option options [String] :tds2_work_phone_cc 勤務先電話国コード(3DS2.0用)
179
+ # @option options [String] :tds2_work_phone_no 勤務先電話番号(3DS2.0用)
180
+ # @option options [String] :tds2_ship_addr_line2 配送先住所2(3DS2.0用)
181
+ # @option options [String] :tds2_ship_addr_line3 配送先住所3(3DS2.0用)
182
+ # @option options [String] :tds2_ship_addr_state 配送先州/都道府県(3DS2.0用)
183
+ # @option options [String] :tds2_bill_addr_line2 請求先住所2(3DS2.0用)
184
+ # @option options [String] :tds2_bill_addr_line3 請求先住所3(3DS2.0用)
185
+ # @option options [String] :tds2_bill_addr_state 請求先州/都道府県(3DS2.0用)
186
+ # @option options [String] :tds2_ch_acc_change アカウント変更インジケータ(3DS2.0用)
187
+ # @option options [String] :tds2_ch_acc_date アカウント作成日(3DS2.0用)
188
+ # @option options [String] :tds2_ch_acc_pw_change パスワード変更インジケータ(3DS2.0用)
189
+ # @option options [String] :tds2_nb_purchase_account 購入回数(3DS2.0用)
190
+ # @option options [String] :tds2_payment_acc_age 決済アカウント年齢(3DS2.0用)
191
+ # @option options [String] :tds2_provision_attempts_day プロビジョニング試行回数(3DS2.0用)
192
+ # @option options [String] :tds2_ship_address_usage 配送先住所の使用状況(3DS2.0用)
193
+ # @option options [String] :tds2_ship_name_ind 配送先名インジケータ(3DS2.0用)
194
+ # @option options [String] :tds2_suspicious_acc_activity 不審なアクティビティインジケータ(3DS2.0用)
195
+ # @option options [String] :tds2_txn_activity_day 1日あたりの取引数(3DS2.0用)
196
+ # @option options [String] :tds2_txn_activity_year 1年あたりの取引数(3DS2.0用)
197
+ # @option options [String] :tds2_three_ds_req_auth_data 3DS要求者認証データ(3DS2.0用)
198
+ # @option options [String] :tds2_three_ds_req_auth_method 3DS要求者認証方法(3DS2.0用)
199
+ # @option options [String] :tds2_three_ds_req_auth_timestamp 3DS要求者認証タイムスタンプ(3DS2.0用)
200
+ # @option options [String] :tds2_acs_challenge_mandated ACSチャレンジ必須(3DS2.0用)
201
+ # @option options [String] :tds2_three_ds_req_prior_ref 3DS要求者事前参照(3DS2.0用)
202
+ # @return [Response] レスポンスオブジェクト。authorizationに"access_id|access_pass|order_id"形式の識別子が含まれます
203
+ #
204
+ # @example 登録済みカードでオーソリ
205
+ # gateway.authorize(1000, "user_001|0", { order_id: "order_123" })
206
+ #
207
+ # @example トークンでオーソリ
208
+ # gateway.authorize(1000, "token_string", { order_id: "order_123" })
209
+ def authorize(money, payment_source, options = {})
210
+ perform_transaction(
211
+ money,
212
+ payment_source,
213
+ 'AUTH',
214
+ 'オーソリに成功しました',
215
+ options.merge(tds2_type: options[:tds2_type] || '3', td_required: options[:td_required] || '0')
216
+ )
217
+ end
218
+
219
+ # 即時売上を実行します
220
+ # authorizeとcaptureを同時に行います
221
+ #
222
+ # @param money [Integer] 金額(円単位)
223
+ # @param payment_source [String, CreditCard] トークン、"member_id|card_seq"形式の識別子、またはクレジットカードオブジェクト
224
+ # @param options [Hash] オプション
225
+ # @option options [String] :order_id 注文ID(省略時は自動生成)
226
+ # @option options [Integer] :tax 税額
227
+ # @option options [Integer] :method 支払方法(デフォルト: 1=一括払い)
228
+ # @option options [Integer] :pay_times 分割回数
229
+ # @option options [String] :ret_url 3DS認証後の戻りURL
230
+ # @option options [String] :callback_type コールバックタイプ
231
+ # @option options [String] :td_tenant_name 3DSテナント名
232
+ # @option options [String] :tds2_type 3DS2.0未対応時取り扱い(デフォルト: '3'=通常オーソリ実施)
233
+ # @option options [String] :tds2_email メールアドレス(3DS2.0用)
234
+ # @option options [String] :tds2_mobile_phone_cc 携帯電話国コード(3DS2.0用)
235
+ # @option options [String] :tds2_mobile_phone_no 携帯電話番号(3DS2.0用)
236
+ # @option options [String] :tds2_ship_addr_city 配送先都市(3DS2.0用)
237
+ # @option options [String] :tds2_ship_addr_country 配送先国(3DS2.0用)
238
+ # @option options [String] :tds2_ship_addr_line1 配送先住所1(3DS2.0用)
239
+ # @option options [String] :tds2_ship_addr_post_code 配送先郵便番号(3DS2.0用)
240
+ # @option options [String] :tds2_bill_addr_city 請求先都市(3DS2.0用)
241
+ # @option options [String] :tds2_bill_addr_country 請求先国(3DS2.0用)
242
+ # @option options [String] :tds2_bill_addr_line1 請求先住所1(3DS2.0用)
243
+ # @option options [String] :tds2_bill_addr_post_code 請求先郵便番号(3DS2.0用)
244
+ # @option options [String] :tds2_home_phone_cc 自宅電話国コード(3DS2.0用)
245
+ # @option options [String] :tds2_home_phone_no 自宅電話番号(3DS2.0用)
246
+ # @option options [String] :tds2_work_phone_cc 勤務先電話国コード(3DS2.0用)
247
+ # @option options [String] :tds2_work_phone_no 勤務先電話番号(3DS2.0用)
248
+ # @option options [String] :tds2_ship_addr_line2 配送先住所2(3DS2.0用)
249
+ # @option options [String] :tds2_ship_addr_line3 配送先住所3(3DS2.0用)
250
+ # @option options [String] :tds2_ship_addr_state 配送先州/都道府県(3DS2.0用)
251
+ # @option options [String] :tds2_bill_addr_line2 請求先住所2(3DS2.0用)
252
+ # @option options [String] :tds2_bill_addr_line3 請求先住所3(3DS2.0用)
253
+ # @option options [String] :tds2_bill_addr_state 請求先州/都道府県(3DS2.0用)
254
+ # @option options [String] :tds2_ch_acc_change アカウント変更インジケータ(3DS2.0用)
255
+ # @option options [String] :tds2_ch_acc_date アカウント作成日(3DS2.0用)
256
+ # @option options [String] :tds2_ch_acc_pw_change パスワード変更インジケータ(3DS2.0用)
257
+ # @option options [String] :tds2_nb_purchase_account 購入回数(3DS2.0用)
258
+ # @option options [String] :tds2_payment_acc_age 決済アカウント年齢(3DS2.0用)
259
+ # @option options [String] :tds2_provision_attempts_day プロビジョニング試行回数(3DS2.0用)
260
+ # @option options [String] :tds2_ship_address_usage 配送先住所の使用状況(3DS2.0用)
261
+ # @option options [String] :tds2_ship_name_ind 配送先名インジケータ(3DS2.0用)
262
+ # @option options [String] :tds2_suspicious_acc_activity 不審なアクティビティインジケータ(3DS2.0用)
263
+ # @option options [String] :tds2_txn_activity_day 1日あたりの取引数(3DS2.0用)
264
+ # @option options [String] :tds2_txn_activity_year 1年あたりの取引数(3DS2.0用)
265
+ # @option options [String] :tds2_three_ds_req_auth_data 3DS要求者認証データ(3DS2.0用)
266
+ # @option options [String] :tds2_three_ds_req_auth_method 3DS要求者認証方法(3DS2.0用)
267
+ # @option options [String] :tds2_three_ds_req_auth_timestamp 3DS要求者認証タイムスタンプ(3DS2.0用)
268
+ # @option options [String] :tds2_acs_challenge_mandated ACSチャレンジ必須(3DS2.0用)
269
+ # @option options [String] :tds2_three_ds_req_prior_ref 3DS要求者事前参照(3DS2.0用)
270
+ # @return [Response] レスポンスオブジェクト
271
+ #
272
+ # @example 登録済みカードで即時売上
273
+ # gateway.purchase(1000, "user_001|0", { order_id: "order_123" })
274
+ def purchase(money, payment_source, options = {})
275
+ perform_transaction(
276
+ money,
277
+ payment_source,
278
+ 'CAPTURE',
279
+ '決済に成功しました',
280
+ options.merge(tds2_type: options[:tds2_type] || '3')
281
+ )
282
+ end
283
+
284
+ # オーソリ済みの取引を売上計上します
285
+ #
286
+ # @param money [Integer] 金額(円単位)
287
+ # @param authorization [String] authorizeメソッドで取得した認証情報("access_id|access_pass|order_id"形式)
288
+ # @param options [Hash] オプション
289
+ # @option options [Integer] :tax 税額
290
+ # @return [Response] レスポンスオブジェクト
291
+ #
292
+ # @example 売上計上
293
+ # response = gateway.authorize(1000, "user_001|0", { order_id: "order_123" })
294
+ # gateway.capture(1000, response.authorization)
295
+ def capture(money, authorization, options = {})
296
+ access_id, access_pass, order_id = split_authorization(authorization)
297
+ return error_response('無効な認証情報です') unless access_id && access_pass
298
+
299
+ begin
300
+ alter_params = {
301
+ access_id: access_id,
302
+ access_pass: access_pass,
303
+ job_cd: 'SALES',
304
+ amount: amount(money)
305
+ }
306
+ alter_params[:tax] = options[:tax].to_i if options[:tax]
307
+ alter_params[:shipping] = options[:shipping].to_i if options[:shipping]
308
+
309
+ result = @shop_client.alter_tran(alter_params)
310
+ Response.new(true, '売上計上に成功しました', result, test: test?)
311
+ rescue GMO::Payment::APIError => e
312
+ error_response(parse_error_message(e))
313
+ end
314
+ end
315
+
316
+ # 取引を取消します
317
+ # オーソリ済みまたは売上済みの取引を取り消すことができます
318
+ #
319
+ # @param authorization [String] authorizeまたはpurchaseメソッドで取得した認証情報
320
+ # @param options [Hash] オプション(現在未使用)
321
+ # @return [Response] レスポンスオブジェクト
322
+ #
323
+ # @example 取引取消
324
+ # response = gateway.authorize(1000, "user_001|0", { order_id: "order_123" })
325
+ # gateway.void(response.authorization)
326
+ def void(authorization, options = {})
327
+ access_id, access_pass, _order_id = split_authorization(authorization)
328
+ return error_response('無効な認証情報です') unless access_id && access_pass
329
+
330
+ begin
331
+ alter_params = {
332
+ access_id: access_id,
333
+ access_pass: access_pass,
334
+ job_cd: 'CANCEL'
335
+ }
336
+
337
+ result = @shop_client.alter_tran(alter_params)
338
+ Response.new(true, '取消に成功しました', result,
339
+ authorization: result['TranID'],
340
+ test: test?)
341
+ rescue GMO::Payment::APIError => e
342
+ error_response(parse_error_message(e))
343
+ end
344
+ end
345
+
346
+ #
347
+ # @param money [Integer] 新しい金額(セント単位)
348
+ # @param authorization [String] authorizeまたはpurchaseメソッドで取得した認証情報
349
+ # @param options [Hash] オプション
350
+ # @option options [String] :job_cd 処理区分('AUTH'または'CAPTURE'、デフォルト: 'CAPTURE')
351
+ # @option options [Integer] :tax 税額(セント単位)
352
+ # @return [Response] レスポンスオブジェクト
353
+ #
354
+ # @example オーソリ済み取引の金額変更
355
+ # response = gateway.authorize(100000, "user_001|0", { order_id: "order_123" })
356
+ # gateway.update_amount(50000, response.authorization, { job_cd: 'AUTH' })
357
+ #
358
+ # @example 売上計上済み取引の金額変更
359
+ # response = gateway.purchase(100000, "user_001|0", { order_id: "order_123" })
360
+ # gateway.update_amount(50000, response.authorization, { job_cd: 'CAPTURE' })
361
+ def update_amount(money, authorization, options = {})
362
+ access_id, access_pass, _order_id = split_authorization(authorization)
363
+ return error_response('無効な認証情報です') unless access_id && access_pass
364
+
365
+ job_cd = options[:job_cd] || 'CAPTURE'
366
+
367
+ begin
368
+ change_params = {
369
+ shop_id: @options[:shop_id],
370
+ shop_pass: @options[:shop_pass],
371
+ access_id: access_id,
372
+ access_pass: access_pass,
373
+ job_cd: job_cd,
374
+ amount: amount(money)
375
+ }
376
+ change_params[:tax] = amount(options[:tax]) if options[:tax]
377
+
378
+ # NOTE: gmo gem (v0.5.8) にchange_tranメソッドがない場合は
379
+ # post_requestを直接呼び出してChangeTran.idPassエンドポイントにアクセス
380
+ result = @shop_client.send(:post_request, 'ChangeTran.idPass', change_params)
381
+
382
+ Response.new(true, '金額変更に成功しました', result,
383
+ authorization: result['TranID'],
384
+ test: test?)
385
+ rescue GMO::Payment::APIError => e
386
+ error_response(e.message)
387
+ end
388
+ end
389
+
390
+ # 取引情報を照会します
391
+ #
392
+ # @param order_id [String] 注文ID
393
+ # @param options [Hash] オプション(現在未使用)
394
+ # @return [Response] レスポンスオブジェクト。取引の詳細情報が含まれます
395
+ #
396
+ # @example 取引照会
397
+ # gateway.search_trade("order_123")
398
+ def search_trade(order_id, options = {})
399
+ begin
400
+ result = @shop_client.search_trade(order_id: order_id)
401
+ Response.new(true, '取引照会に成功しました', result, test: test?)
402
+ rescue GMO::Payment::APIError => e
403
+ error_response(parse_error_message(e))
404
+ end
405
+ end
406
+ # 3DS2.0認証後の決済を完了します
407
+ # 3DS2.0認証チャレンジ完了後、この決済を実行して取引を確定させます
408
+ #
409
+ # @param access_id [String] EntryTranで取得したアクセスID
410
+ # @param access_pass [String] EntryTranで取得したアクセスパス
411
+ # @param options [Hash] オプション(現在未使用)
412
+ # @return [Response] レスポンスオブジェクト。決済実行結果が含まれます
413
+ # authorizationには"access_id|access_pass|order_id"形式の識別子が含まれます
414
+ #
415
+ # @example 3DS2.0認証後の決済完了
416
+ # response = gateway.authorize(1000, "user_001|0", { order_id: "order_123", ret_url: "https://example.com/callback" })
417
+ # access_id = response.params['AccessID']
418
+ # access_pass = response.params['AccessPass']
419
+ # gateway.secure_tran2(access_id, access_pass)
420
+ def secure_tran2(access_id, access_pass, options = {})
421
+ begin
422
+ result = @shop_client.secure_tran_2(access_id: access_id, access_pass: access_pass)
423
+ # レスポンスのOrderIDを使ってauthorizationを構築
424
+ # これにより、後続のcapture/void/credit操作で使用できるようになります
425
+ order_id = result['OrderID']
426
+ authorization = "#{access_id}|#{access_pass}|#{order_id}"
427
+
428
+ Response.new(true, '3DS2.0認証後の決済に成功しました', result,
429
+ authorization: authorization,
430
+ test: test?)
431
+ rescue GMO::Payment::APIError => e
432
+ error_response(parse_error_message(e))
433
+ end
434
+ end
435
+
436
+
437
+
438
+ private
439
+
440
+ def perform_transaction(money, payment_source, job_cd, success_message, options = {})
441
+ order_id = options[:order_id] || generate_order_id
442
+
443
+ begin
444
+ entry_params = {
445
+ order_id: order_id,
446
+ job_cd: job_cd,
447
+ amount: amount(money)
448
+ }
449
+ entry_params[:tax] = amount(options[:tax]) if options[:tax]
450
+ entry_params[:shipping] = amount(options[:shipping]) if options[:shipping]
451
+ entry_params[:td_flag] = '2'
452
+ entry_params[:td_tenant_name] = options[:td_tenant_name] if options[:td_tenant_name]
453
+ entry_params[:tds2_type] = options[:tds2_type]
454
+ entry_params[:td_required] = options[:td_required] if options[:td_required]
455
+
456
+ entry_result = @shop_client.entry_tran(entry_params)
457
+ access_id = entry_result['AccessID']
458
+ access_pass = entry_result['AccessPass']
459
+
460
+ exec_params = build_exec_tran_params(access_id, access_pass, order_id, payment_source, options)
461
+ exec_result = execute_transaction(exec_params, payment_source)
462
+
463
+ authorization = "#{access_id}|#{access_pass}|#{order_id}"
464
+
465
+ Response.new(
466
+ true,
467
+ success_message,
468
+ exec_result.merge('AccessID' => access_id, 'AccessPass' => access_pass, 'OrderID' => order_id),
469
+ authorization: authorization,
470
+ test: test?
471
+ )
472
+ rescue GMO::Payment::APIError => e
473
+ error_response(parse_error_message(e))
474
+ end
475
+ end
476
+
477
+ def build_exec_tran_params(access_id, access_pass, order_id, payment_source, options)
478
+ params = {
479
+ access_id: access_id,
480
+ access_pass: access_pass,
481
+ order_id: order_id
482
+ }
483
+
484
+ if payment_source.is_a?(String)
485
+ if payment_source.include?('|')
486
+ member_id, card_seq = payment_source.split('|')
487
+ params[:member_id] = member_id
488
+ params[:card_seq] = card_seq
489
+ params[:seq_mode] = 1
490
+ else
491
+ params[:token] = payment_source
492
+ end
493
+ else
494
+ params[:card_no] = payment_source.number
495
+ params[:expire] = expdate(payment_source)
496
+ params[:security_code] = payment_source.verification_value if payment_source.verification_value.present?
497
+ params[:holder_name] = payment_source.name if payment_source.name.present?
498
+ end
499
+
500
+ params[:method] = options[:method] || 1
501
+ params[:pay_times] = options[:pay_times] if options[:pay_times]
502
+ params[:ret_url] = options[:ret_url] if options[:ret_url]
503
+ params[:callback_type] = options[:callback_type] if options[:callback_type]
504
+
505
+ add_3ds2_params(params, options)
506
+
507
+ params
508
+ end
509
+
510
+ def add_3ds2_params(params, options)
511
+ tds2_params = %w[
512
+ tds2_ch_acc_change tds2_ch_acc_date tds2_ch_acc_pw_change tds2_nb_purchase_account
513
+ tds2_payment_acc_age tds2_provision_attempts_day tds2_ship_address_usage
514
+ tds2_ship_name_ind tds2_suspicious_acc_activity tds2_txn_activity_day
515
+ tds2_txn_activity_year tds2_three_ds_req_auth_data tds2_three_ds_req_auth_method
516
+ tds2_three_ds_req_auth_timestamp tds2_acs_challenge_mandated tds2_three_ds_req_prior_ref
517
+ tds2_email tds2_home_phone_cc tds2_home_phone_no tds2_mobile_phone_cc
518
+ tds2_mobile_phone_no tds2_work_phone_cc tds2_work_phone_no tds2_ship_addr_city
519
+ tds2_ship_addr_country tds2_ship_addr_line1 tds2_ship_addr_line2 tds2_ship_addr_line3
520
+ tds2_ship_addr_post_code tds2_ship_addr_state tds2_bill_addr_city tds2_bill_addr_country
521
+ tds2_bill_addr_line1 tds2_bill_addr_line2 tds2_bill_addr_line3 tds2_bill_addr_post_code
522
+ tds2_bill_addr_state
523
+ ]
524
+
525
+ tds2_params.each do |param|
526
+ key = param.to_sym
527
+ params[key] = options[key] if options[key]
528
+ end
529
+ end
530
+
531
+ def split_authorization(authorization)
532
+ return [ nil, nil, nil ] unless authorization
533
+
534
+ parts = authorization.split('|')
535
+ case parts.length
536
+ when 2
537
+ [ parts[0], parts[1], nil ]
538
+ when 3
539
+ parts
540
+ else
541
+ [ nil, nil, nil ]
542
+ end
543
+ end
544
+
545
+ def generate_unique_member_id
546
+ "member-#{Time.now.to_i}-#{SecureRandom.hex(4)}"
547
+ end
548
+
549
+ def generate_order_id
550
+ "order-#{Time.now.to_i}-#{SecureRandom.hex(4)}"
551
+ end
552
+
553
+ def expdate(payment)
554
+ "#{format('%02d', payment.month)}#{payment.year.to_s[-2..]}"
555
+ end
556
+
557
+ # 詳細エラーコード(例: 42G020000)から簡略エラーコード(例: G02)を抽出
558
+ #
559
+ # @param detailed_code [String] 詳細エラーコード
560
+ # @return [String, nil] 簡略エラーコード
561
+ def extract_error_code(detailed_code)
562
+ return nil unless detailed_code.is_a?(String)
563
+
564
+ # C系/G系エラー: 42C010000 -> C01, 42G020000 -> G02
565
+ if detailed_code.match(/^42([CG])(\d{2})/)
566
+ "#{$1}#{$2}"
567
+ # E系/M系エラー: E01010001 -> E01, M01002001 -> M01
568
+ elsif detailed_code.match(/^([EM])(\d{2})/)
569
+ "#{$1}#{$2}"
570
+ else
571
+ detailed_code
572
+ end
573
+ end
574
+
575
+ # GMO::Payment::APIErrorから日本語エラーメッセージを抽出
576
+ #
577
+ # @param error [GMO::Payment::APIError] APIエラーオブジェクト
578
+ # @return [String] 日本語エラーメッセージ
579
+ def parse_error_message(error)
580
+ return error.message unless error.respond_to?(:error_info)
581
+
582
+ error_info = error.error_info
583
+ return error.message unless error_info.is_a?(Hash)
584
+
585
+ # ErrInfoから詳細エラーコードを抽出
586
+ err_info_str = error_info['ErrInfo'].to_s
587
+ detailed_codes = err_info_str.scan(/[A-Z0-9]+/)
588
+
589
+ # 最初に見つかった日本語メッセージを返す
590
+ detailed_codes.each do |code|
591
+ # 詳細コードで検索(例: E21030007, 42G020000)
592
+ if GMO_PG_OFFICIAL_ERROR_MESSAGES.key?(code)
593
+ return GMO_PG_OFFICIAL_ERROR_MESSAGES[code]
594
+ end
595
+
596
+ # 簡略コードで検索(例: E21, G02)
597
+ simplified_code = extract_error_code(code)
598
+ if simplified_code && GMO_PG_OFFICIAL_ERROR_MESSAGES.key?(simplified_code)
599
+ return GMO_PG_OFFICIAL_ERROR_MESSAGES[simplified_code]
600
+ end
601
+ end
602
+
603
+ # メッセージが見つからない場合は汎用メッセージを返す
604
+ '決済処理に失敗しました。申し訳ございませんが、しばらく時間をあけて購入画面からやり直してください。'
605
+ end
606
+
607
+ def error_response(message)
608
+ Response.new(false, message, {}, test: test?)
609
+ end
610
+
611
+ # 金額を整数に変換
612
+ # Spree::Money#amount_in_centsが既に正しい円単位の値を返すため、
613
+ # 単純にIntegerに変換するだけで良い
614
+ # 例: 2000円 -> amount_in_cents(2000) -> amount(2000) = 2000円
615
+ def amount(money)
616
+ return money unless money.is_a?(Numeric)
617
+
618
+ money.to_i
619
+ end
620
+
621
+ def execute_transaction(params, payment_source)
622
+ if payment_source.is_a?(String) && payment_source.include?('|')
623
+ params[:site_id] = @options[:site_id]
624
+ params[:site_pass] = @options[:site_pass]
625
+ params[:client_field_flg] = if params[:client_field_1] || params[:client_field_2] || params[:client_field_3]
626
+ '1'
627
+ else
628
+ '0'
629
+ end
630
+ params[:device_category] = '0'
631
+
632
+ # NOTE: gmo gem (v0.5.8) のexec_tranはmember_id/card_seq方式に対応していないため、
633
+ # post_requestを直接呼び出してExecTran.idPassエンドポイントにアクセスしています。
634
+ # gmo gemのアップデート時には動作確認が必要です。
635
+ @shop_client.send(:post_request, 'ExecTran.idPass', params)
636
+ else
637
+ @shop_client.exec_tran(params)
638
+ end
639
+ end
640
+ end
641
+ end
642
+ end