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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/Gemfile +5 -0
  4. data/LICENSE +673 -0
  5. data/README.md +58 -0
  6. data/app/controllers/spree/gmo_pg_callback_controller.rb +37 -0
  7. data/app/controllers/spree/payment_sources_controller.rb +47 -0
  8. data/app/controllers/spree_gmo_pg/checkout_controller_decorator.rb +40 -0
  9. data/app/javascript/spree_gmo_pg/controllers/gmo_pg_payment_controller.js +295 -0
  10. data/app/javascript/spree_gmo_pg/index.js +11 -0
  11. data/app/models/spree/payment_method/gmo_pg.rb +418 -0
  12. data/app/models/spree_gmo_pg/order_updater_decorator.rb +78 -0
  13. data/app/overrides/hide_preference_fields_for_payment_method_gmo_pg.rb +12 -0
  14. data/app/overrides/spree/gmo_pg_javascript_import.rb +28 -0
  15. data/app/views/spree/admin/payment_methods/custom_form_fields/_gmo_pg_form_fields.html.erb +63 -0
  16. data/app/views/spree/checkout/payment/_gmo_pg.html.erb +154 -0
  17. data/app/views/spree/checkout/payment/_saved_cards.html.erb +36 -0
  18. data/app/views/spree/gmo_pg_callback/callback.html.erb +51 -0
  19. data/app/views/spree/shared/_error.html.erb +3 -0
  20. data/config/importmap.rb +9 -0
  21. data/config/locales/en.yml +29 -0
  22. data/config/locales/ja.yml +29 -0
  23. data/config/routes.rb +10 -0
  24. data/lib/spree/payment_redirect_required.rb +23 -0
  25. data/lib/spree_gmo_pg/engine.rb +44 -0
  26. data/lib/spree_gmo_pg/version.rb +5 -0
  27. data/lib/spree_gmo_pg.rb +16 -0
  28. data/spec/controllers/spree/gmo_pg_callback_controller_spec.rb +46 -0
  29. data/spec/controllers/spree/payment_sources_controller_spec.rb +75 -0
  30. data/spec/factories/payment_method_factory.rb +19 -0
  31. data/spec/models/spree/order_updater_decorator_spec.rb +221 -0
  32. data/spec/models/spree/payment_method/gmo_pg_spec.rb +860 -0
  33. data/spec/spec_helper.rb +25 -0
  34. data/spree_gmo_pg.gemspec +32 -0
  35. 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>