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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +174 -0
- data/Rakefile +8 -0
- data/lib/active_merchant/billing/gateways/gmo_pg.rb +642 -0
- data/lib/active_merchant/billing/gateways/gmo_pg_error_messages.rb +483 -0
- data/lib/active_merchant_gmo_pg/engine.rb +13 -0
- data/lib/active_merchant_gmo_pg/version.rb +5 -0
- data/lib/active_merchant_gmo_pg.rb +10 -0
- data/spec/active_merchant/billing/gmo_pg_gateway_spec.rb +386 -0
- data/spec/spec_helper.rb +24 -0
- metadata +124 -0
|
@@ -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
|