spree_gmo_pg 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/Gemfile +5 -0
- data/LICENSE +673 -0
- data/README.md +58 -0
- data/app/controllers/spree/gmo_pg_callback_controller.rb +37 -0
- data/app/controllers/spree/payment_sources_controller.rb +47 -0
- data/app/controllers/spree_gmo_pg/checkout_controller_decorator.rb +40 -0
- data/app/javascript/spree_gmo_pg/controllers/gmo_pg_payment_controller.js +295 -0
- data/app/javascript/spree_gmo_pg/index.js +11 -0
- data/app/models/spree/payment_method/gmo_pg.rb +418 -0
- data/app/models/spree_gmo_pg/order_updater_decorator.rb +78 -0
- data/app/overrides/hide_preference_fields_for_payment_method_gmo_pg.rb +12 -0
- data/app/overrides/spree/gmo_pg_javascript_import.rb +28 -0
- data/app/views/spree/admin/payment_methods/custom_form_fields/_gmo_pg_form_fields.html.erb +63 -0
- data/app/views/spree/checkout/payment/_gmo_pg.html.erb +154 -0
- data/app/views/spree/checkout/payment/_saved_cards.html.erb +36 -0
- data/app/views/spree/gmo_pg_callback/callback.html.erb +51 -0
- data/app/views/spree/shared/_error.html.erb +3 -0
- data/config/importmap.rb +9 -0
- data/config/locales/en.yml +29 -0
- data/config/locales/ja.yml +29 -0
- data/config/routes.rb +10 -0
- data/lib/spree/payment_redirect_required.rb +23 -0
- data/lib/spree_gmo_pg/engine.rb +44 -0
- data/lib/spree_gmo_pg/version.rb +5 -0
- data/lib/spree_gmo_pg.rb +16 -0
- data/spec/controllers/spree/gmo_pg_callback_controller_spec.rb +46 -0
- data/spec/controllers/spree/payment_sources_controller_spec.rb +75 -0
- data/spec/factories/payment_method_factory.rb +19 -0
- data/spec/models/spree/order_updater_decorator_spec.rb +221 -0
- data/spec/models/spree/payment_method/gmo_pg_spec.rb +860 -0
- data/spec/spec_helper.rb +25 -0
- data/spree_gmo_pg.gemspec +32 -0
- metadata +152 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
class PaymentMethod::GmoPg < Gateway
|
|
5
|
+
include Spree::PaymentMethod::AutoCaptureDigitalConcern
|
|
6
|
+
|
|
7
|
+
# GMO-PGでカードが既に削除済みの場合のエラーパターン
|
|
8
|
+
GMO_PG_CARD_NOT_FOUND_ERRORS = /not found|404|E01390007/i
|
|
9
|
+
|
|
10
|
+
preference :shop_id, :string
|
|
11
|
+
preference :shop_pass, :password
|
|
12
|
+
preference :site_id, :string
|
|
13
|
+
preference :site_pass, :password
|
|
14
|
+
preference :test_mode, :boolean, default: true
|
|
15
|
+
|
|
16
|
+
# 3DS認証情報を payment に保存し、コールバック後に unprocessed_payments で
|
|
17
|
+
# 再処理できるよう checkout 状態に戻す。
|
|
18
|
+
# process_payment_with_3ds は process_payments! の with_lock トランザクション内で実行され、
|
|
19
|
+
# そこで保存しても PaymentRedirectRequired の伝播でロールバックされる。そのため保存は
|
|
20
|
+
# トランザクション外(CheckoutController の rescue)からこのメソッドを呼んで行う。
|
|
21
|
+
def self.persist_3ds_redirect_data(payment, gmo_pg_data)
|
|
22
|
+
return if payment.blank? || gmo_pg_data.blank?
|
|
23
|
+
|
|
24
|
+
# ロールバック後の DB の値に合わせてから保存する
|
|
25
|
+
payment.reload
|
|
26
|
+
payment.private_metadata[:gmo_pg] = gmo_pg_data
|
|
27
|
+
payment.save!
|
|
28
|
+
# update_column で state_machine の検証をバイパスして checkout に戻す
|
|
29
|
+
payment.update_column(:state, 'checkout') unless payment.checkout?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def provider_class
|
|
33
|
+
ActiveMerchant::Billing::GmoPgGateway
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def payment_source_class
|
|
37
|
+
Spree::CreditCard
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def method_type
|
|
41
|
+
'gmo_pg'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def payment_profiles_supported?
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def supports?(source)
|
|
49
|
+
source.is_a?(Spree::CreditCard)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def can_capture?(payment)
|
|
53
|
+
payment.pending? || payment.checkout?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def custom_form_fields_partial_name
|
|
57
|
+
'gmo_pg_form_fields'
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def options
|
|
61
|
+
{
|
|
62
|
+
shop_id: preferred_shop_id,
|
|
63
|
+
shop_pass: preferred_shop_pass,
|
|
64
|
+
site_id: preferred_site_id,
|
|
65
|
+
site_pass: preferred_site_pass,
|
|
66
|
+
test: preferred_test_mode
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# オーソリ(仮売上)を実行
|
|
71
|
+
# 登録済みカードの場合は "member_id|card_seq" 形式に変換してから実行
|
|
72
|
+
# 3DS2.0対応: 1回目はEntryTran+ExecTran、2回目はSecureTran2を実行
|
|
73
|
+
def authorize(money, source, gateway_options)
|
|
74
|
+
process_payment_with_3ds(:authorize, money, source, gateway_options)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# 即時売上を実行
|
|
78
|
+
# 登録済みカードの場合は "member_id|card_seq" 形式に変換してから実行
|
|
79
|
+
# 3DS2.0対応: 1回目はEntryTran+ExecTran、2回目はSecureTran2を実行
|
|
80
|
+
def purchase(money, source, gateway_options)
|
|
81
|
+
process_payment_with_3ds(:purchase, money, source, gateway_options)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def create_profile(payment)
|
|
85
|
+
return unless payment.source.gateway_payment_profile_id.present?
|
|
86
|
+
|
|
87
|
+
source = payment.source
|
|
88
|
+
token = source.gateway_payment_profile_id
|
|
89
|
+
|
|
90
|
+
# すでに登録済みカード(member_id + CardSeq両方ある)の場合はスキップ
|
|
91
|
+
if source.gateway_customer_profile_id.present? && source.gateway_payment_profile_id.present?
|
|
92
|
+
Rails.logger.info "Using existing registered card (member_id: #{source.gateway_customer_profile_id}, CardSeq: #{source.gateway_payment_profile_id})"
|
|
93
|
+
return
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# ゲスト購入の場合は、トークンをそのまま保存して終了
|
|
97
|
+
# SaveMember/SaveCard APIは呼ばない
|
|
98
|
+
if payment.order.user.blank?
|
|
99
|
+
Rails.logger.info "Guest checkout: Token saved without card registration (token: #{token[0..10]}...)"
|
|
100
|
+
return # トークンは既にgateway_payment_profile_idに入っているので何もしない
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# ログイン済みユーザーの場合は既存の処理(#692の実装)
|
|
104
|
+
# トークンから会員登録とカード登録を実行
|
|
105
|
+
member_id = payment.order.user_id.to_s
|
|
106
|
+
Rails.logger.info "Logged-in user checkout: Registering card with GMO-PG (member_id: #{member_id})"
|
|
107
|
+
|
|
108
|
+
# ActiveMerchantのstore メソッドを呼び出し
|
|
109
|
+
# store(payment, options = {})
|
|
110
|
+
# paymentにはトークンを渡す
|
|
111
|
+
response = provider.store(
|
|
112
|
+
token,
|
|
113
|
+
{
|
|
114
|
+
member_id: member_id,
|
|
115
|
+
order_id: payment.order.number
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if response.success?
|
|
120
|
+
# response.authorizationから member_id と CardSeq を取得
|
|
121
|
+
# authorization は "member_id|card_seq" の形式
|
|
122
|
+
member_id_from_response, card_seq = response.authorization.split('|')
|
|
123
|
+
|
|
124
|
+
# GatewayCustomerを作成または取得
|
|
125
|
+
self.gateway_customers.find_or_create_by(
|
|
126
|
+
user: payment.order.user
|
|
127
|
+
) do |gc|
|
|
128
|
+
gc.profile_id = member_id
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# GMOから返却されたcard_seqが既に他のレコードに存在するかチェック
|
|
132
|
+
existing_card = payment.order.user.credit_cards.where(
|
|
133
|
+
payment_method_id: self.id,
|
|
134
|
+
gateway_customer_profile_id: member_id,
|
|
135
|
+
gateway_payment_profile_id: card_seq
|
|
136
|
+
).where.not(id: source.id).first
|
|
137
|
+
|
|
138
|
+
if existing_card
|
|
139
|
+
# 既存カードがある場合 = GMOで更新が行われた = 情報を統合
|
|
140
|
+
Rails.logger.info "GMO updated existing card (CardSeq: #{card_seq}), merging records..."
|
|
141
|
+
|
|
142
|
+
# 既存カードの情報を更新(有効期限、名義など)
|
|
143
|
+
# defaultフラグは変更しない(ユーザーが既に選択している可能性があるため)
|
|
144
|
+
existing_card.update!(
|
|
145
|
+
month: source.month,
|
|
146
|
+
year: source.year,
|
|
147
|
+
name: source.name
|
|
148
|
+
# last_digits, cc_type は変わらないはず(同じカード番号なので)
|
|
149
|
+
# default は変更しない
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# 新規作成したsourceレコードは不要なので削除
|
|
153
|
+
source.destroy
|
|
154
|
+
|
|
155
|
+
# paymentのsource_idを既存カードに変更
|
|
156
|
+
# update_columnsを使ってafter_saveコールバックをスキップ(無限ループ防止)
|
|
157
|
+
payment.update_columns(source_id: existing_card.id, source_type: existing_card.class.name)
|
|
158
|
+
|
|
159
|
+
Rails.logger.info "Updated existing card record (id: #{existing_card.id})"
|
|
160
|
+
else
|
|
161
|
+
# 既存カードがない場合 = 新規登録
|
|
162
|
+
source.update!(
|
|
163
|
+
gateway_payment_profile_id: card_seq,
|
|
164
|
+
gateway_customer_profile_id: member_id
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
Rails.logger.info "Card registered successfully (CardSeq: #{card_seq})"
|
|
168
|
+
end
|
|
169
|
+
else
|
|
170
|
+
# エラーの場合は例外を発生させる
|
|
171
|
+
raise Spree::Core::GatewayError.new(response.message)
|
|
172
|
+
end
|
|
173
|
+
rescue ActiveMerchant::ConnectionError => e
|
|
174
|
+
raise Spree::Core::GatewayError.new(e.message)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def disable_customer_profile(source)
|
|
178
|
+
return unless source.is_a?(Spree::CreditCard)
|
|
179
|
+
|
|
180
|
+
# gateway_payment_profile_idが無い場合(登録未完了)はDB側のみ削除
|
|
181
|
+
if source.gateway_payment_profile_id.blank?
|
|
182
|
+
source.destroy
|
|
183
|
+
Rails.logger.info "Local credit card deleted (not registered with GMO-PG): #{source.id}"
|
|
184
|
+
return
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# GMO-PG側から削除
|
|
188
|
+
# unstore の第1引数は "member_id|card_seq" の形式
|
|
189
|
+
identification = "#{source.gateway_customer_profile_id}|#{source.gateway_payment_profile_id}"
|
|
190
|
+
result = provider.unstore(identification)
|
|
191
|
+
|
|
192
|
+
# GMO-PG側で既に削除済み(404)や成功の場合、DB側を削除
|
|
193
|
+
if result.success? || result.message.to_s.match?(GMO_PG_CARD_NOT_FOUND_ERRORS)
|
|
194
|
+
source.destroy
|
|
195
|
+
Rails.logger.info "GMO-PG card deleted: #{source.gateway_payment_profile_id}"
|
|
196
|
+
else
|
|
197
|
+
# 削除失敗時はエラーを投げる
|
|
198
|
+
Rails.logger.error "GMO-PG card deletion failed: #{result.message}"
|
|
199
|
+
raise Spree::Core::GatewayError, result.message
|
|
200
|
+
end
|
|
201
|
+
rescue ActiveMerchant::ConnectionError => e
|
|
202
|
+
raise Spree::Core::GatewayError.new(e.message)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# 支払いをキャンセルします
|
|
206
|
+
# 注文キャンセル時に Order#after_cancel から自動的に呼び出されます
|
|
207
|
+
#
|
|
208
|
+
# @param response_code [String] 認証コード ("access_id|access_pass|order_id"形式)
|
|
209
|
+
# @param payment [Spree::Payment] 支払いオブジェクト (gateway_optionsの取得に使用)
|
|
210
|
+
# @return [ActiveMerchant::Billing::Response] void実行結果
|
|
211
|
+
def cancel(response_code, payment = nil)
|
|
212
|
+
# ActiveMerchantのvoidメソッドを呼び出してGMO-PGのオーソリをキャンセル
|
|
213
|
+
provider.void(response_code, payment&.gateway_options || {})
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# 支払いを無効化します(手動void用)
|
|
217
|
+
# 管理画面から手動でvoidする際に Payment#void_transaction! から呼び出されます
|
|
218
|
+
# payment_profiles_supported? が true のため、Spreeは3つの引数で呼び出します
|
|
219
|
+
#
|
|
220
|
+
# @param response_code [String] 認証コード ("access_id|access_pass|order_id"形式)
|
|
221
|
+
# @param source [Spree::CreditCard, Hash] カード情報、または2引数呼び出し時のoptions
|
|
222
|
+
# @param options [Hash] ゲートウェイオプション(3引数呼び出し時のみ)
|
|
223
|
+
# @return [ActiveMerchant::Billing::Response] void実行結果
|
|
224
|
+
def void(response_code, source = nil, options = {})
|
|
225
|
+
# 2引数呼び出し(古いSpreeバージョンや直接呼び出し)の互換性対応
|
|
226
|
+
# source が Hash の場合、それが実際の options
|
|
227
|
+
if source.is_a?(Hash)
|
|
228
|
+
options = source
|
|
229
|
+
source = nil
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# GMO-PG の void は authorization と options のみを使用
|
|
233
|
+
# source (CreditCard) は不要なので無視
|
|
234
|
+
provider.void(response_code, options)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# 返金を実行します
|
|
238
|
+
# payment_profiles_supported? が true のため、Spreeは4つの引数で呼び出します
|
|
239
|
+
#
|
|
240
|
+
# @param money [Integer] 返金額(セント単位)
|
|
241
|
+
# @param source [Spree::CreditCard] カード情報(使用しない)
|
|
242
|
+
# @param authorization [String] 認証コード ("access_id|access_pass|order_id"形式)
|
|
243
|
+
# @param options [Hash] ゲートウェイオプション(originator: Spree::Refundオブジェクトを含む)
|
|
244
|
+
# @return [ActiveMerchant::Billing::Response] 返金実行結果
|
|
245
|
+
def credit(money, source, authorization, options = {})
|
|
246
|
+
# 返金元の情報を取得
|
|
247
|
+
originator = options[:originator]
|
|
248
|
+
|
|
249
|
+
unless originator&.respond_to?(:payment)
|
|
250
|
+
raise Spree::Core::GatewayError, 'Refund originator is required'
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
payment = originator.payment
|
|
254
|
+
|
|
255
|
+
# 現在の残額を計算
|
|
256
|
+
# money, payment.captured_amount, refunds.sum(:amount)はすべてamount_in_centsの結果で同じ単位
|
|
257
|
+
# JPYの場合でも、修正後のamount_in_centsは正しい値を返すので×100は不要
|
|
258
|
+
captured_cents = payment.captured_amount.to_i
|
|
259
|
+
# transaction_idが存在する返金のみをカウント(完了済みの返金)
|
|
260
|
+
refunded_cents = payment.refunds.where.not(transaction_id: nil).sum(:amount).to_i
|
|
261
|
+
remaining_cents = captured_cents - refunded_cents
|
|
262
|
+
|
|
263
|
+
# 今回の返金後の残額
|
|
264
|
+
new_remaining_cents = remaining_cents - money
|
|
265
|
+
|
|
266
|
+
# 全額返金かどうか
|
|
267
|
+
if new_remaining_cents <= 0
|
|
268
|
+
# 全額返金 → void(キャンセル)
|
|
269
|
+
Rails.logger.info "[GMO-PG Refund] Full refund: money=#{money}, remaining=#{remaining_cents}, authorization=#{authorization}"
|
|
270
|
+
provider.void(authorization, options)
|
|
271
|
+
else
|
|
272
|
+
# 部分返金 → update_amount(減額)
|
|
273
|
+
job_cd = payment.completed? ? 'CAPTURE' : 'AUTH'
|
|
274
|
+
Rails.logger.info "[GMO-PG Refund] Partial refund: money=#{money}, new_remaining=#{new_remaining_cents}, job_cd=#{job_cd}, authorization=#{authorization}"
|
|
275
|
+
provider.update_amount(new_remaining_cents, authorization, options.merge(job_cd: job_cd))
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
private
|
|
280
|
+
|
|
281
|
+
# 3DS2.0対応の決済処理(authorize/purchase共通ロジック)
|
|
282
|
+
# @param action [Symbol] :authorize または :purchase
|
|
283
|
+
# @param money [Integer] 金額
|
|
284
|
+
# @param source [Spree::CreditCard] カード情報
|
|
285
|
+
# @param gateway_options [Hash] ゲートウェイオプション
|
|
286
|
+
# @return [ActiveMerchant::Billing::Response] 決済レスポンス
|
|
287
|
+
def process_payment_with_3ds(action, money, source, gateway_options)
|
|
288
|
+
# Order と Payment を取得
|
|
289
|
+
_order, payment = find_order_and_payment(gateway_options)
|
|
290
|
+
|
|
291
|
+
gmo_pg_data = payment.private_metadata[:gmo_pg] || {}
|
|
292
|
+
|
|
293
|
+
# 1回目: 通常の決済 (EntryTran + ExecTran)
|
|
294
|
+
if gmo_pg_data[:access_id].blank? || gmo_pg_data[:access_pass].blank?
|
|
295
|
+
enhanced_options = gateway_options.merge(
|
|
296
|
+
ret_url: generate_3ds_callback_url(gateway_options),
|
|
297
|
+
callback_type: '3' # GET方式でコールバック(SameSite Cookie対応)
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
response = provider.public_send(action, money, format_source(source), enhanced_options)
|
|
301
|
+
|
|
302
|
+
# エラー時の処理を追加
|
|
303
|
+
unless response.success?
|
|
304
|
+
payment.send(:record_response, response) # log_entriesにエラー詳細を記録
|
|
305
|
+
payment.failure! # state machineで processing → failed に遷移
|
|
306
|
+
raise Spree::Core::GatewayError, response.message
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# 3DS認証が必要か判定 (ACS=2 の場合)
|
|
310
|
+
if response.success? && response.params['ACS'] == '2'
|
|
311
|
+
# コールバックでの再処理に必要な Access 情報(AccessID のみ返ってくるため保持する)
|
|
312
|
+
gmo_pg_data = {
|
|
313
|
+
redirect_url: response.params['RedirectUrl'],
|
|
314
|
+
redirect_at: Time.current.to_i,
|
|
315
|
+
access_id: response.params['AccessID'],
|
|
316
|
+
access_pass: response.params['AccessPass']
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
Rails.logger.info "[GMO-PG 3DS] Redirect required: #{response.params['RedirectUrl']}"
|
|
320
|
+
|
|
321
|
+
# このメソッドは process_payments! の with_lock トランザクション内で実行されており、
|
|
322
|
+
# ここで PaymentRedirectRequired を raise するとトランザクションがロールバックされる。
|
|
323
|
+
# そのため metadata の保存・state 変更はここでは行わず(行っても巻き戻る)、
|
|
324
|
+
# 例外に必要情報を積んでトランザクション外(CheckoutController の rescue)で永続化する。
|
|
325
|
+
raise Spree::PaymentRedirectRequired.new(
|
|
326
|
+
response.params['RedirectUrl'],
|
|
327
|
+
payment: payment,
|
|
328
|
+
gmo_pg_data: gmo_pg_data
|
|
329
|
+
)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# 3DS不要の場合はそのままレスポンスを返す
|
|
333
|
+
response
|
|
334
|
+
else
|
|
335
|
+
# 2回目: コールバック後 (SecureTran2)
|
|
336
|
+
Rails.logger.info "[GMO-PG 3DS] Completing 3DS authentication with SecureTran2"
|
|
337
|
+
|
|
338
|
+
response = provider.secure_tran2(
|
|
339
|
+
gmo_pg_data[:access_id],
|
|
340
|
+
gmo_pg_data[:access_pass]
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
if response.success?
|
|
344
|
+
payment.private_metadata.delete(:gmo_pg)
|
|
345
|
+
payment.save!
|
|
346
|
+
Rails.logger.info "[GMO-PG 3DS] Authentication completed successfully"
|
|
347
|
+
else
|
|
348
|
+
payment.send(:record_response, response) # log_entriesにエラー詳細を記録
|
|
349
|
+
payment.private_metadata.delete(:gmo_pg)
|
|
350
|
+
payment.save!
|
|
351
|
+
payment.failure! # state machineで processing → failed に遷移
|
|
352
|
+
Rails.logger.error "[GMO-PG 3DS] Authentication failed: #{response.message}"
|
|
353
|
+
raise Spree::Core::GatewayError, response.message
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
response
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Order と Payment を取得する共通メソッド
|
|
361
|
+
# @param gateway_options [Hash] ゲートウェイオプション (order_id を含む)
|
|
362
|
+
# @return [Array<Spree::Order, Spree::Payment>] Order と Payment のペア
|
|
363
|
+
# @raise [Spree::Core::GatewayError] Order または Payment が見つからない場合
|
|
364
|
+
def find_order_and_payment(gateway_options)
|
|
365
|
+
order_number = gateway_options[:order_id].split('-').first
|
|
366
|
+
order = Spree::Order.find_by(number: order_number)
|
|
367
|
+
raise Spree::Core::GatewayError, "Order not found: #{order_number}" unless order
|
|
368
|
+
|
|
369
|
+
payment = order.payments.valid.last
|
|
370
|
+
raise Spree::Core::GatewayError, "Payment not found for order: #{order_number}" unless payment
|
|
371
|
+
|
|
372
|
+
[ order, payment ]
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# sourceを適切な形式に変換
|
|
376
|
+
# - 登録済みカード(member_id + CardSeq)の場合: "member_id|card_seq" 形式の文字列
|
|
377
|
+
# - 新規カード(トークン)の場合: トークン文字列
|
|
378
|
+
# - それ以外の場合: sourceをそのまま返す
|
|
379
|
+
def format_source(source)
|
|
380
|
+
return source unless source.is_a?(Spree::CreditCard)
|
|
381
|
+
|
|
382
|
+
# 登録済みカード: gateway_customer_profile_id (member_id) と gateway_payment_profile_id (card_seq) の両方がある
|
|
383
|
+
if source.gateway_customer_profile_id.present? && source.gateway_payment_profile_id.present?
|
|
384
|
+
"#{source.gateway_customer_profile_id}|#{source.gateway_payment_profile_id}"
|
|
385
|
+
# 新規カード(トークン): gateway_payment_profile_id のみがある
|
|
386
|
+
elsif source.gateway_payment_profile_id.present?
|
|
387
|
+
source.gateway_payment_profile_id
|
|
388
|
+
# 生のカード情報
|
|
389
|
+
else
|
|
390
|
+
source
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# 3DSコールバックURLを生成
|
|
395
|
+
def generate_3ds_callback_url(gateway_options)
|
|
396
|
+
order, _payment = find_order_and_payment(gateway_options)
|
|
397
|
+
|
|
398
|
+
protocol = Rails.env.production? ? 'https' : 'http'
|
|
399
|
+
store_url = Spree::Store.default.url
|
|
400
|
+
|
|
401
|
+
# スキームがない場合は補完してパース
|
|
402
|
+
uri = URI.parse(store_url.start_with?('http') ? store_url : "#{protocol}://#{store_url}")
|
|
403
|
+
|
|
404
|
+
url_options = {
|
|
405
|
+
token: order.token,
|
|
406
|
+
host: uri.host,
|
|
407
|
+
protocol: protocol
|
|
408
|
+
}
|
|
409
|
+
# ポートが標準ポート(80/443)以外の場合のみ指定
|
|
410
|
+
url_options[:port] = uri.port if uri.port && ![ 80, 443 ].include?(uri.port)
|
|
411
|
+
|
|
412
|
+
Spree::Core::Engine.routes.url_helpers.gmo_pg_3ds_callback_url(url_options)
|
|
413
|
+
rescue Spree::Core::GatewayError
|
|
414
|
+
# Order/Payment が見つからない場合は nil を返す
|
|
415
|
+
nil
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module SpreeGmoPg
|
|
2
|
+
module OrderUpdaterDecorator
|
|
3
|
+
# @gem-override spree_core-5.3.6/app/models/spree/order_updater.rb#persist_totals
|
|
4
|
+
# @see https://github.com/be-agile/giga-repeat/issues/1002
|
|
5
|
+
# super 後に、完了済み注文で合計金額が変わった場合の GMO-PG オーソリ/売上金額の自動調整を追加する。
|
|
6
|
+
def persist_totals
|
|
7
|
+
super
|
|
8
|
+
|
|
9
|
+
# 完了済み注文で金額が変わった場合、GMO-PG決済を自動調整
|
|
10
|
+
adjust_gmo_pg_payments_if_needed
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def adjust_gmo_pg_payments_if_needed
|
|
16
|
+
return unless order.completed?
|
|
17
|
+
return if order.canceled? # キャンセル時はvoidが実行されるため金額調整不要
|
|
18
|
+
|
|
19
|
+
order.payments.each do |payment|
|
|
20
|
+
next unless payment.payment_method.is_a?(Spree::PaymentMethod::GmoPg)
|
|
21
|
+
next unless payment.pending? || payment.completed?
|
|
22
|
+
next if payment.amount == order.total # 既に一致している場合はスキップ
|
|
23
|
+
|
|
24
|
+
adjust_gmo_pg_payment(payment)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def adjust_gmo_pg_payment(payment)
|
|
29
|
+
gateway = payment.payment_method.provider
|
|
30
|
+
old_captured_amount = payment.captured_amount
|
|
31
|
+
new_amount = order.total
|
|
32
|
+
|
|
33
|
+
# 既存の返金額を取得(transaction_idが存在する=完了済みの返金のみ)
|
|
34
|
+
refunded_amount = payment.refunds.where.not(transaction_id: nil).sum(:amount)
|
|
35
|
+
|
|
36
|
+
# GMOに送信する金額は、注文合計から返金額を引いたもの
|
|
37
|
+
new_gmo_amount = new_amount - refunded_amount
|
|
38
|
+
|
|
39
|
+
# pending(オーソリ済み)ならAUTH、completed(売上計上済み)ならCAPTURE
|
|
40
|
+
job_cd = payment.pending? ? 'AUTH' : 'CAPTURE'
|
|
41
|
+
|
|
42
|
+
response = gateway.update_amount(
|
|
43
|
+
Spree::Money.new(new_gmo_amount, currency: order.currency).amount_in_cents,
|
|
44
|
+
payment.response_code,
|
|
45
|
+
job_cd: job_cd
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if response.success?
|
|
49
|
+
payment.update_column(:amount, new_amount)
|
|
50
|
+
|
|
51
|
+
# 売上計上済み(completed)の場合、captured_amountとの差分をPaymentCaptureEventとして記録
|
|
52
|
+
# これによりpayment.captured_amountが正しい値を返すようになる
|
|
53
|
+
if payment.completed?
|
|
54
|
+
# 新しいcaptured_amountは、GMOに送った金額 + 返金額
|
|
55
|
+
# つまり、order.totalと同じになるべき
|
|
56
|
+
new_captured_amount = new_gmo_amount + refunded_amount
|
|
57
|
+
amount_diff = new_captured_amount - old_captured_amount
|
|
58
|
+
|
|
59
|
+
if amount_diff != 0
|
|
60
|
+
payment.capture_events.create!(amount: amount_diff)
|
|
61
|
+
Rails.logger.info("GMO-PG payment capture event created: #{payment.number} diff=#{amount_diff}, gmo_amount=#{new_gmo_amount}, refunded=#{refunded_amount}")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
Rails.logger.info("GMO-PG payment amount adjusted: #{payment.number} payment.amount=#{new_amount}, gmo_amount=#{new_gmo_amount}, refunded=#{refunded_amount}")
|
|
66
|
+
else
|
|
67
|
+
Rails.logger.error("GMO-PG amount adjustment failed for payment #{payment.number}: #{response.message}")
|
|
68
|
+
# エラー時も注文の更新は継続する
|
|
69
|
+
end
|
|
70
|
+
rescue => e
|
|
71
|
+
Rails.logger.error("GMO-PG amount adjustment error for payment #{payment.number}: #{e.message}")
|
|
72
|
+
Rails.logger.error(e.backtrace.join("\n"))
|
|
73
|
+
# エラー時も注文の更新は継続する
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
Spree::OrderUpdater.prepend(SpreeGmoPg::OrderUpdaterDecorator)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Deface::Override.new(
|
|
4
|
+
virtual_path: 'spree/admin/payment_methods/_form',
|
|
5
|
+
name: 'hide_preference_fields_for_payment_method_gmo_pg',
|
|
6
|
+
replace: "erb[loud]:contains('preference_fields(@object, f)')",
|
|
7
|
+
text: <<-HTML
|
|
8
|
+
<% unless @object.is_a?(Spree::PaymentMethod::GmoPg) %>
|
|
9
|
+
<%= preference_fields(@object, f) unless preference_fields(@object, f).empty? %>
|
|
10
|
+
<% end %>
|
|
11
|
+
HTML
|
|
12
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# GMO-PG Token SDKの読み込み(直接script srcで読み込む)
|
|
2
|
+
Deface::Override.new(
|
|
3
|
+
virtual_path: 'spree/shared/_head',
|
|
4
|
+
name: 'add_gmo_pg_token_sdk',
|
|
5
|
+
insert_after: "erb[loud]:contains('javascript_importmap_tags')",
|
|
6
|
+
text: <<-ERB
|
|
7
|
+
<%
|
|
8
|
+
# GMO-PG決済方法を取得してtest_modeを確認
|
|
9
|
+
gmo_pg_payment_method = Spree::PaymentMethod.active.find_by(type: 'Spree::PaymentMethod::GmoPg')
|
|
10
|
+
use_test_sdk = gmo_pg_payment_method&.preferred_test_mode || !Rails.env.production?
|
|
11
|
+
%>
|
|
12
|
+
<% if use_test_sdk %>
|
|
13
|
+
<script src="https://stg.static.mul-pay.jp/payment/js/mp-token.js"></script>
|
|
14
|
+
<% else %>
|
|
15
|
+
<script src="https://static.mul-pay.jp/payment/js/mp-token.js"></script>
|
|
16
|
+
<% end %>
|
|
17
|
+
ERB
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# import_module_tagの埋め込み
|
|
21
|
+
Deface::Override.new(
|
|
22
|
+
virtual_path: 'spree/shared/_head',
|
|
23
|
+
name: 'add_gmo_pg_javascript_importmap_tags',
|
|
24
|
+
insert_after: "erb[loud]:contains('javascript_importmap_tags')",
|
|
25
|
+
text: <<-ERB
|
|
26
|
+
<%= javascript_import_module_tag "spree_gmo_pg" %>
|
|
27
|
+
ERB
|
|
28
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<div class="card mb-4">
|
|
2
|
+
<div class="card-header">
|
|
3
|
+
<h5 class="card-title">
|
|
4
|
+
<%= Spree.t('gmo_pg.api_credentials') %>
|
|
5
|
+
</h5>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="card-body">
|
|
9
|
+
<div class="form-group">
|
|
10
|
+
<%= f.label :preferred_shop_id, Spree.t('gmo_pg.shop_id') %>
|
|
11
|
+
<%= f.text_field :preferred_shop_id, class: 'form-control' %>
|
|
12
|
+
<small class="form-text text-muted">
|
|
13
|
+
<%= Spree.t('gmo_pg.shop_id_hint') %>
|
|
14
|
+
</small>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="form-group">
|
|
18
|
+
<%= f.label :preferred_shop_pass, Spree.t('gmo_pg.shop_pass') %>
|
|
19
|
+
<%= f.password_field :preferred_shop_pass, class: 'form-control', value: @object.preferred_shop_pass %>
|
|
20
|
+
<small class="form-text text-muted">
|
|
21
|
+
<%= Spree.t('gmo_pg.shop_pass_hint') %>
|
|
22
|
+
</small>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="form-group">
|
|
26
|
+
<%= f.label :preferred_site_id, Spree.t('gmo_pg.site_id') %>
|
|
27
|
+
<%= f.text_field :preferred_site_id, class: 'form-control' %>
|
|
28
|
+
<small class="form-text text-muted">
|
|
29
|
+
<%= Spree.t('gmo_pg.site_id_hint') %>
|
|
30
|
+
</small>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="form-group">
|
|
34
|
+
<%= f.label :preferred_site_pass, Spree.t('gmo_pg.site_pass') %>
|
|
35
|
+
<%= f.password_field :preferred_site_pass, class: 'form-control', value: @object.preferred_site_pass %>
|
|
36
|
+
<small class="form-text text-muted">
|
|
37
|
+
<%= Spree.t('gmo_pg.site_pass_hint') %>
|
|
38
|
+
</small>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="card mb-4">
|
|
44
|
+
<div class="card-header">
|
|
45
|
+
<h5 class="card-title">
|
|
46
|
+
<%= Spree.t('gmo_pg.settings') %>
|
|
47
|
+
</h5>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="card-body">
|
|
51
|
+
<div class="form-group">
|
|
52
|
+
<div class="custom-control custom-checkbox">
|
|
53
|
+
<%= f.check_box :preferred_test_mode, class: 'custom-control-input', id: 'test_mode' %>
|
|
54
|
+
<%= f.label :preferred_test_mode, Spree.t('gmo_pg.test_mode'), class: 'custom-control-label', for: 'test_mode' %>
|
|
55
|
+
</div>
|
|
56
|
+
<small class="form-text text-muted">
|
|
57
|
+
<%= Spree.t('gmo_pg.test_mode_hint') %>
|
|
58
|
+
</small>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<%= render 'spree/admin/payment_methods/auto_capture_digital_fields', f: f %>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|