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,860 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Spree::PaymentMethod::GmoPg, type: :model do
6
+ let(:payment_method) { described_class.new }
7
+
8
+ describe "#provider_class" do
9
+ subject { payment_method.provider_class }
10
+
11
+ it { is_expected.to eq ActiveMerchant::Billing::GmoPgGateway }
12
+ end
13
+
14
+ describe "#payment_source_class" do
15
+ subject { payment_method.payment_source_class }
16
+
17
+ it { is_expected.to eq Spree::CreditCard }
18
+ end
19
+
20
+ describe "#method_type" do
21
+ subject { payment_method.method_type }
22
+
23
+ it { is_expected.to eq 'gmo_pg' }
24
+ end
25
+
26
+ describe "#payment_profiles_supported?" do
27
+ subject { payment_method.payment_profiles_supported? }
28
+
29
+ it { is_expected.to be true }
30
+ end
31
+
32
+ describe "#supports?" do
33
+ subject { payment_method.supports?(source) }
34
+
35
+ context "source is Spree::CreditCard" do
36
+ let(:source) { Spree::CreditCard.new }
37
+
38
+ it { is_expected.to be true }
39
+ end
40
+
41
+ context "source is not Spree::CreditCard" do
42
+ let(:source) { Object.new }
43
+
44
+ it { is_expected.to be false }
45
+ end
46
+ end
47
+
48
+ describe "#auto_capture?" do
49
+ subject { payment_method.auto_capture? }
50
+
51
+ context "auto_capture == true" do
52
+ before { payment_method.auto_capture = true }
53
+
54
+ it { is_expected.to be true }
55
+ end
56
+
57
+ context "auto_capture == false" do
58
+ before { payment_method.auto_capture = false }
59
+
60
+ it { is_expected.to be false }
61
+ end
62
+ end
63
+
64
+ describe "#custom_form_fields_partial_name" do
65
+ subject { payment_method.custom_form_fields_partial_name }
66
+
67
+ it { is_expected.to eq 'gmo_pg_form_fields' }
68
+ end
69
+
70
+ describe "#options" do
71
+ subject { payment_method.options }
72
+
73
+ before do
74
+ payment_method.preferred_shop_id = 'test_shop'
75
+ payment_method.preferred_shop_pass = 'test_pass'
76
+ payment_method.preferred_site_id = 'test_site'
77
+ payment_method.preferred_site_pass = 'test_site_pass'
78
+ payment_method.preferred_test_mode = true
79
+ end
80
+
81
+ it {
82
+ expect(subject).to eq(
83
+ shop_id: 'test_shop',
84
+ shop_pass: 'test_pass',
85
+ site_id: 'test_site',
86
+ site_pass: 'test_site_pass',
87
+ test: true
88
+ )
89
+ }
90
+ end
91
+
92
+ describe "preferences" do
93
+ describe "default values" do
94
+ it { expect(payment_method.preferred_test_mode).to be true }
95
+ end
96
+
97
+ describe "setting values" do
98
+ before do
99
+ payment_method.preferred_shop_id = 'shop123'
100
+ payment_method.preferred_shop_pass = 'shoppass'
101
+ payment_method.preferred_site_id = 'site123'
102
+ payment_method.preferred_site_pass = 'sitepass'
103
+ payment_method.preferred_test_mode = false
104
+ payment_method.auto_capture = true
105
+ end
106
+
107
+ it { expect(payment_method.preferred_shop_id).to eq 'shop123' }
108
+ it { expect(payment_method.preferred_shop_pass).to eq 'shoppass' }
109
+ it { expect(payment_method.preferred_site_id).to eq 'site123' }
110
+ it { expect(payment_method.preferred_site_pass).to eq 'sitepass' }
111
+ it { expect(payment_method.preferred_test_mode).to be false }
112
+ it { expect(payment_method.auto_capture).to be true }
113
+ end
114
+ end
115
+
116
+ describe "#create_profile" do
117
+ let(:payment_method) { create(:payment_method_gmo_pg) }
118
+ let(:order) { create(:order, user: user) }
119
+ let(:payment) { build(:payment, order: order, payment_method: payment_method, source: credit_card) }
120
+ let(:credit_card) { create(:credit_card, user: user, gateway_payment_profile_id: token) }
121
+ let(:token) { 'tk_test_token_12345' }
122
+ let(:gateway) { instance_double(ActiveMerchant::Billing::GmoPgGateway) }
123
+
124
+ before do
125
+ allow(payment_method).to receive(:provider).and_return(gateway)
126
+ end
127
+
128
+ context "ゲスト購入の場合" do
129
+ let(:user) { nil }
130
+
131
+ it "トークンをそのまま保持し、SaveMember/SaveCard APIを呼ばない" do
132
+ expect(gateway).not_to receive(:store)
133
+ # Spree 5.3 の order 生成は instrumentation で logger.info を発火させるため、
134
+ # 期待メッセージ以外の info も通す。これが無いと strict mock が無関係なログを弾いて落ちる。
135
+ allow(Rails.logger).to receive(:info)
136
+ expect(Rails.logger).to receive(:info).with(/Guest checkout: Token saved without card registration/).at_least(:once)
137
+
138
+ payment_method.create_profile(payment)
139
+
140
+ credit_card.reload
141
+ expect(credit_card.gateway_payment_profile_id).to eq token
142
+ expect(credit_card.gateway_customer_profile_id).to be_nil
143
+ expect(credit_card.user_id).to be_nil
144
+ expect(credit_card.last_digits).to be_present
145
+ expect(credit_card.cc_type).to be_present
146
+ expect(credit_card.name).to be_present
147
+ end
148
+ end
149
+
150
+ context "ログイン済みユーザーの場合" do
151
+ let(:user) { create(:user) }
152
+ let(:response) { double(success?: success, message: message, authorization: authorization) }
153
+ let(:member_id) { user.id.to_s }
154
+ let(:card_seq) { '1' }
155
+ let(:authorization) { "#{member_id}|#{card_seq}" }
156
+
157
+ context "登録済みカードを使う場合" do
158
+ let(:credit_card) { create(:credit_card, user: user, gateway_payment_profile_id: card_seq, gateway_customer_profile_id: member_id) }
159
+
160
+ it "SaveMember/SaveCard APIを呼ばず、何もしない" do
161
+ expect(gateway).not_to receive(:store)
162
+ # Spree 5.3 の order 生成 instrumentation の logger.info を通す(下記strict mock対策)。
163
+ allow(Rails.logger).to receive(:info)
164
+ expect(Rails.logger).to receive(:info).with(/Using existing registered card/).at_least(:once)
165
+
166
+ payment_method.create_profile(payment)
167
+
168
+ credit_card.reload
169
+ expect(credit_card.gateway_payment_profile_id).to eq card_seq
170
+ expect(credit_card.gateway_customer_profile_id).to eq member_id
171
+ end
172
+ end
173
+
174
+ context "SaveMember/SaveCard APIが成功した場合" do
175
+ let(:credit_card) { create(:credit_card, user: user, gateway_payment_profile_id: token, gateway_customer_profile_id: nil) }
176
+ let(:success) { true }
177
+ let(:message) { 'OK' }
178
+
179
+ before do
180
+ allow(gateway).to receive(:store).and_return(response)
181
+ allow(payment_method.gateway_customers).to receive(:find_or_create_by).and_return(double(profile_id: member_id))
182
+ end
183
+
184
+ it "トークンからCardSeqに置き換え、GatewayCustomerを作成する" do
185
+ expect(gateway).to receive(:store).with(
186
+ token,
187
+ {
188
+ member_id: member_id,
189
+ order_id: order.number
190
+ }
191
+ )
192
+ expect(Rails.logger).to receive(:info).with(/Logged-in user checkout: Registering card with GMO-PG/).at_least(:once)
193
+ expect(Rails.logger).to receive(:info).with(/Card registered successfully/).at_least(:once)
194
+
195
+ payment_method.create_profile(payment)
196
+
197
+ credit_card.reload
198
+ expect(credit_card.gateway_payment_profile_id).to eq card_seq
199
+ expect(credit_card.gateway_customer_profile_id).to eq member_id
200
+ expect(credit_card.user_id).to eq user.id
201
+ expect(credit_card.last_digits).to be_present
202
+ expect(credit_card.cc_type).to be_present
203
+ expect(credit_card.name).to be_present
204
+ end
205
+
206
+ context "同じカード番号で異なる有効期限を2回登録した場合" do
207
+ let!(:first_card) { create(:credit_card, user: user, payment_method: payment_method, month: 3, year: 2026, name: 'TARO YAMADA', gateway_customer_profile_id: member_id, gateway_payment_profile_id: card_seq) }
208
+
209
+ it "新規レコードを作成せず、既存レコードを更新し、paymentのsourceを付け替える" do
210
+ # 2回目のカード登録をシミュレート
211
+ second_token = 'tk_test_token_67890'
212
+ second_credit_card = create(:credit_card, user: user, payment_method: payment_method, month: 4, year: 2026, name: 'TARO YAMADA', gateway_payment_profile_id: second_token, gateway_customer_profile_id: nil)
213
+
214
+ second_order = create(:order, user: user)
215
+ second_response = double(success?: true, message: 'OK', authorization: "#{member_id}|#{card_seq}")
216
+
217
+ # モックとexpectを先に設定してから payment を作成
218
+ allow(gateway).to receive(:store).with(
219
+ second_token,
220
+ {
221
+ member_id: member_id,
222
+ order_id: second_order.number
223
+ }
224
+ ).and_return(second_response)
225
+
226
+ # Spree 5.3 の order 生成 instrumentation の logger.info を通す(下記strict mock対策)。
227
+ allow(Rails.logger).to receive(:info)
228
+ expect(Rails.logger).to receive(:info).with(/Logged-in user checkout: Registering card with GMO-PG/).at_least(:once)
229
+ expect(Rails.logger).to receive(:info).with(/GMO updated existing card/).at_least(:once)
230
+ expect(Rails.logger).to receive(:info).with(/Updated existing card record/).at_least(:once)
231
+
232
+ expect(user.credit_cards.count).to eq 2
233
+
234
+ # 削除されるカードのIDを保存
235
+ second_credit_card_id = second_credit_card.id
236
+
237
+ # payment作成時にafter_saveコールバックでcreate_profileが自動的に呼ばれる
238
+ second_payment = create(:payment, order: second_order, payment_method: payment_method, source: second_credit_card)
239
+
240
+ expect(user.credit_cards.count).to eq 1
241
+ first_card.reload
242
+ expect(first_card.month).to eq 4
243
+ expect(first_card.year).to eq 2026
244
+ expect(first_card.name).to eq 'TARO YAMADA'
245
+ expect(first_card.gateway_payment_profile_id).to eq card_seq
246
+ expect(first_card.gateway_customer_profile_id).to eq member_id
247
+
248
+ second_payment.reload
249
+ expect(second_payment.source).to eq first_card
250
+
251
+ expect { Spree::CreditCard.find(second_credit_card_id) }.to raise_error(ActiveRecord::RecordNotFound)
252
+ end
253
+
254
+ it "defaultフラグは維持される" do
255
+ first_card.update!(default: true)
256
+
257
+ second_token = 'tk_test_token_67890'
258
+ second_credit_card = create(:credit_card, user: user, payment_method: payment_method, month: 4, year: 2026, name: 'TARO YAMADA', gateway_payment_profile_id: second_token, gateway_customer_profile_id: nil)
259
+
260
+ second_order = create(:order, user: user)
261
+ second_response = double(success?: true, message: 'OK', authorization: "#{member_id}|#{card_seq}")
262
+
263
+ # モックを先に設定してから payment を作成
264
+ allow(gateway).to receive(:store).with(
265
+ second_token,
266
+ {
267
+ member_id: member_id,
268
+ order_id: second_order.number
269
+ }
270
+ ).and_return(second_response)
271
+
272
+ # payment作成時にafter_saveコールバックでcreate_profileが自動的に呼ばれる
273
+ second_payment = create(:payment, order: second_order, payment_method: payment_method, source: second_credit_card)
274
+
275
+ first_card.reload
276
+ expect(first_card.default).to be true
277
+ end
278
+ end
279
+ end
280
+
281
+ context "SaveMember/SaveCard APIが失敗した場合" do
282
+ let(:credit_card) { create(:credit_card, user: user, gateway_payment_profile_id: token, gateway_customer_profile_id: nil) }
283
+ let(:success) { false }
284
+ let(:message) { 'API Error' }
285
+
286
+ before do
287
+ allow(gateway).to receive(:store).and_return(response)
288
+ end
289
+
290
+ it "Spree::Core::GatewayErrorを発生させる" do
291
+ expect {
292
+ payment_method.create_profile(payment)
293
+ }.to raise_error(Spree::Core::GatewayError, 'API Error')
294
+ end
295
+ end
296
+ end
297
+
298
+ context "gateway_payment_profile_idが空の場合" do
299
+ let(:user) { nil }
300
+ let(:credit_card) { create(:credit_card, user: user, gateway_payment_profile_id: nil) }
301
+
302
+ it "何もしない" do
303
+ expect(gateway).not_to receive(:store)
304
+ payment_method.create_profile(payment)
305
+ end
306
+ end
307
+ end
308
+
309
+ describe "#disable_customer_profile" do
310
+ let(:payment_method) { create(:payment_method_gmo_pg) }
311
+ let(:credit_card) { create(:credit_card, gateway_payment_profile_id: 'card_seq_123', gateway_customer_profile_id: 'member_123') }
312
+ let(:gateway) { instance_double(ActiveMerchant::Billing::GmoPgGateway) }
313
+ let(:response) { double(success?: success, message: message) }
314
+
315
+ before do
316
+ allow(payment_method).to receive(:provider).and_return(gateway)
317
+ end
318
+
319
+ context "削除成功時" do
320
+ let(:success) { true }
321
+ let(:message) { 'OK' }
322
+
323
+ before do
324
+ allow(gateway).to receive(:unstore).with('member_123|card_seq_123').and_return(response)
325
+ end
326
+
327
+ it "GMO-PG側から削除し、DB側も論理削除する" do
328
+ expect(gateway).to receive(:unstore).with('member_123|card_seq_123')
329
+ expect(credit_card).to receive(:destroy)
330
+ payment_method.disable_customer_profile(credit_card)
331
+ end
332
+ end
333
+
334
+ context "GMO-PG側で既に削除済み(404エラー)の場合" do
335
+ let(:success) { false }
336
+ let(:message) { 'Card not found (E01390007)' }
337
+
338
+ before do
339
+ allow(gateway).to receive(:unstore).with('member_123|card_seq_123').and_return(response)
340
+ end
341
+
342
+ it "エラーを無視してDB側を論理削除する" do
343
+ expect(gateway).to receive(:unstore).with('member_123|card_seq_123')
344
+ expect(credit_card).to receive(:destroy)
345
+ payment_method.disable_customer_profile(credit_card)
346
+ end
347
+ end
348
+
349
+ context "GMO-PG側で削除失敗時" do
350
+ let(:success) { false }
351
+ let(:message) { 'Internal Server Error' }
352
+
353
+ before do
354
+ allow(gateway).to receive(:unstore).with('member_123|card_seq_123').and_return(response)
355
+ end
356
+
357
+ it "GatewayErrorを投げる" do
358
+ expect {
359
+ payment_method.disable_customer_profile(credit_card)
360
+ }.to raise_error(Spree::Core::GatewayError, 'Internal Server Error')
361
+ end
362
+ end
363
+
364
+ context "gateway_payment_profile_idがない場合" do
365
+ let(:credit_card_without_profile) { create(:credit_card, gateway_payment_profile_id: nil) }
366
+
367
+ it "GMO-PGへの削除はスキップし、DB側のみ論理削除する" do
368
+ expect(gateway).not_to receive(:unstore)
369
+ expect(credit_card_without_profile).to receive(:destroy)
370
+ payment_method.disable_customer_profile(credit_card_without_profile)
371
+ end
372
+ end
373
+
374
+ context "sourceがCreditCardでない場合" do
375
+ let(:invalid_source) { Object.new }
376
+
377
+ it "何もしない" do
378
+ expect(gateway).not_to receive(:unstore)
379
+ payment_method.disable_customer_profile(invalid_source)
380
+ end
381
+ end
382
+ end
383
+
384
+ # 3DS決済フローの共有例
385
+ RSpec.shared_examples "3DS payment flow" do |action|
386
+ let(:payment_method) { create(:payment_method_gmo_pg) }
387
+ let(:gateway) { instance_double(ActiveMerchant::Billing::GmoPgGateway) }
388
+ let(:order) { create(:order, number: 'R123456', user: nil) }
389
+ let(:credit_card) { create(:credit_card, gateway_payment_profile_id: 'tk_test_token_123', user: nil) }
390
+ let(:payment) { create(:payment, order: order, payment_method: payment_method, source: credit_card, state: 'processing') }
391
+ let(:money) { 1000 }
392
+ let(:gateway_options) { { order_id: "#{order.number}-PA1" } }
393
+
394
+ before do
395
+ allow(gateway).to receive(:store).and_return(
396
+ double(success?: true, authorization: "#{order.user_id}|0")
397
+ )
398
+ allow(payment_method).to receive_messages(provider: gateway, generate_3ds_callback_url: 'http://example.com/callback', find_order_and_payment: [ order, payment ])
399
+ end
400
+
401
+ context "3DS認証が不要な場合" do
402
+ let(:response) { double(success?: true, params: {}, authorization: 'auth_123') }
403
+
404
+ it "通常のgateway.#{action}を呼び出してレスポンスを返す" do
405
+ allow(gateway).to receive(action).with(
406
+ money,
407
+ 'tk_test_token_123',
408
+ hash_including(order_id: gateway_options[:order_id])
409
+ ).and_return(response)
410
+
411
+ result = payment_method.public_send(action, money, credit_card, gateway_options)
412
+ expect(result).to eq response
413
+ end
414
+ end
415
+
416
+ context "3DS認証が必要な場合(1回目の#{action})" do
417
+ let(:redirect_url) { 'https://gmo-pg.example.com/3ds' }
418
+ let(:response) do
419
+ double(
420
+ success?: true,
421
+ params: { 'ACS' => '2', 'RedirectUrl' => redirect_url, 'AccessID' => 'test_access_id', 'AccessPass' => 'test_access_pass' },
422
+ authorization: 'auth_123'
423
+ )
424
+ end
425
+
426
+ # 保存は with_lock トランザクション内ではロールバックされるため、ここでは行わず
427
+ # 再処理に必要な情報を例外に積んで返す(保存はコントローラ側で行う)
428
+ it "PaymentRedirectRequiredをraiseし、例外に再処理用データを積む" do
429
+ allow(gateway).to receive(action).with(
430
+ money,
431
+ 'tk_test_token_123',
432
+ hash_including(
433
+ order_id: gateway_options[:order_id],
434
+ ret_url: 'http://example.com/callback'
435
+ )
436
+ ).and_return(response)
437
+
438
+ expect {
439
+ payment_method.public_send(action, money, credit_card, gateway_options)
440
+ }.to raise_error(Spree::PaymentRedirectRequired) do |error|
441
+ expect(error.redirect_url).to eq redirect_url
442
+ expect(error.payment).to eq payment
443
+ expect(error.gmo_pg_data).to include(
444
+ redirect_url: redirect_url,
445
+ access_id: 'test_access_id',
446
+ access_pass: 'test_access_pass'
447
+ )
448
+ expect(error.gmo_pg_data[:redirect_at]).to be_present
449
+ end
450
+ end
451
+ end
452
+
453
+ context "3DS認証完了後(2回目の#{action})" do
454
+ let(:access_id) { 'access_id_123' }
455
+ let(:access_pass) { 'access_pass_123' }
456
+ let(:response) { double(success?: true, params: {}, authorization: 'auth_123') }
457
+
458
+ before do
459
+ payment.private_metadata[:gmo_pg] = {
460
+ redirect_url: 'https://gmo-pg.example.com/3ds',
461
+ access_id: access_id,
462
+ access_pass: access_pass
463
+ }
464
+ payment.save!
465
+ end
466
+
467
+ context "SecureTran2が成功した場合" do
468
+ it "SecureTran2を呼び出してmetadataを削除する" do
469
+ allow(gateway).to receive(:secure_tran2).with(access_id, access_pass).and_return(response)
470
+
471
+ result = payment_method.public_send(action, money, credit_card, gateway_options)
472
+
473
+ expect(result).to eq response
474
+ expect(payment.reload.private_metadata[:gmo_pg]).to be_nil
475
+ end
476
+ end
477
+
478
+ context "SecureTran2が失敗した場合" do
479
+ let(:error_response) { double(success?: false, message: '認証に失敗しました') }
480
+
481
+ it "metadataを削除してGatewayErrorをraiseする" do
482
+ allow(gateway).to receive(:secure_tran2).with(access_id, access_pass).and_return(error_response)
483
+
484
+ expect {
485
+ payment_method.public_send(action, money, credit_card, gateway_options)
486
+ }.to raise_error(Spree::Core::GatewayError, '認証に失敗しました')
487
+
488
+ expect(payment.reload.private_metadata[:gmo_pg]).to be_nil
489
+ end
490
+ end
491
+ end
492
+
493
+ context "ブラウザバックシナリオ(redirect_urlのみでaccess_id/access_passなし)" do
494
+ let(:redirect_url) { 'https://gmo-pg.example.com/3ds' }
495
+ let(:response) do
496
+ double(
497
+ success?: true,
498
+ params: { 'ACS' => '2', 'RedirectUrl' => redirect_url },
499
+ authorization: 'auth_123'
500
+ )
501
+ end
502
+
503
+ before do
504
+ payment.private_metadata[:gmo_pg] = {
505
+ redirect_url: 'https://gmo-pg.example.com/3ds'
506
+ }
507
+ payment.save!
508
+ end
509
+
510
+ it "1回目のフローに入り、再度3DSリダイレクトする" do
511
+ allow(gateway).to receive(action).with(
512
+ money,
513
+ 'tk_test_token_123',
514
+ hash_including(
515
+ order_id: gateway_options[:order_id],
516
+ ret_url: 'http://example.com/callback'
517
+ )
518
+ ).and_return(response)
519
+
520
+ expect {
521
+ payment_method.public_send(action, money, credit_card, gateway_options)
522
+ }.to raise_error(Spree::PaymentRedirectRequired) do |error|
523
+ expect(error.redirect_url).to eq redirect_url
524
+ end
525
+ end
526
+ end
527
+
528
+ context "登録済みカード(member_id + CardSeq)の場合" do
529
+ let(:credit_card_saved) { create(:credit_card, gateway_customer_profile_id: 'member_001', gateway_payment_profile_id: '0', user: nil) }
530
+ let(:payment_saved) { create(:payment, order: order, payment_method: payment_method, source: credit_card_saved, amount: money) }
531
+ let(:response) { double(success?: true, params: {}, authorization: 'auth_123') }
532
+
533
+ before do
534
+ order.update_columns(total: money, payment_total: 0, item_total: money)
535
+ allow(payment_method).to receive(:find_order_and_payment).and_return([ order, payment_saved ])
536
+ end
537
+
538
+ it "member_id|card_seq形式でgateway.#{action}を呼び出す" do
539
+ allow(gateway).to receive(action).with(
540
+ money,
541
+ 'member_001|0',
542
+ hash_including(order_id: gateway_options[:order_id])
543
+ ).and_return(response)
544
+
545
+ result = payment_method.public_send(action, money, credit_card_saved, gateway_options)
546
+ expect(result).to eq response
547
+ end
548
+ end
549
+ end
550
+
551
+ describe "#authorize" do
552
+ include_examples "3DS payment flow", :authorize
553
+ end
554
+
555
+ describe "#purchase" do
556
+ include_examples "3DS payment flow", :purchase
557
+ end
558
+
559
+ describe ".persist_3ds_redirect_data" do
560
+ let(:payment_method) { create(:payment_method_gmo_pg) }
561
+ let(:order) { create(:order, number: 'R123456', user: nil) }
562
+ let(:credit_card) { create(:credit_card, user: nil) }
563
+ let(:payment) { create(:payment, order: order, payment_method: payment_method, source: credit_card, state: 'processing') }
564
+ let(:gmo_pg_data) do
565
+ {
566
+ redirect_url: 'https://gmo-pg.example.com/3ds',
567
+ redirect_at: 1_700_000_000,
568
+ access_id: 'access_id_123',
569
+ access_pass: 'access_pass_123'
570
+ }
571
+ end
572
+
573
+ subject { described_class.persist_3ds_redirect_data(payment, gmo_pg_data) }
574
+
575
+ context "payment と gmo_pg_data がある場合" do
576
+ it "metadataを保存し checkout 状態に戻す" do
577
+ subject
578
+
579
+ payment.reload
580
+ expect(payment.private_metadata[:gmo_pg]).to include(
581
+ redirect_url: 'https://gmo-pg.example.com/3ds',
582
+ access_id: 'access_id_123',
583
+ access_pass: 'access_pass_123'
584
+ )
585
+ expect(payment.state).to eq 'checkout'
586
+ end
587
+ end
588
+
589
+ context "payment が nil の場合" do
590
+ let(:payment) { nil }
591
+
592
+ it { expect { subject }.not_to raise_error }
593
+ end
594
+
595
+ context "gmo_pg_data が空の場合" do
596
+ let(:gmo_pg_data) { {} }
597
+
598
+ it "metadataを変更しない" do
599
+ subject
600
+
601
+ expect(payment.reload.private_metadata[:gmo_pg]).to be_nil
602
+ end
603
+ end
604
+ end
605
+
606
+ describe "#cancel" do
607
+ let(:payment_method) { create(:payment_method_gmo_pg) }
608
+ let(:gateway) { instance_double(ActiveMerchant::Billing::GmoPgGateway) }
609
+ let(:order) { create(:order, number: 'R123456', user: nil) }
610
+ let(:credit_card) { create(:credit_card, user: nil) }
611
+ let(:payment) { create(:payment, order: order, payment_method: payment_method, source: credit_card, response_code: 'access_id_123|access_pass_123|R123456-PA1') }
612
+ let(:response_code) { 'access_id_123|access_pass_123|R123456-PA1' }
613
+
614
+ before do
615
+ allow(payment_method).to receive(:provider).and_return(gateway)
616
+ end
617
+
618
+ context "void成功時" do
619
+ let(:response) { double(success?: true, message: 'Void成功', authorization: 'void_auth_123') }
620
+
621
+ it "provider.voidを呼び出して成功レスポンスを返す" do
622
+ allow(gateway).to receive(:void).with(response_code, anything).and_return(response)
623
+
624
+ result = payment_method.cancel(response_code, payment)
625
+ expect(result).to eq response
626
+ expect(result.success?).to be true
627
+ end
628
+ end
629
+
630
+ context "void失敗時" do
631
+ let(:error_response) { double(success?: false, message: 'Void失敗') }
632
+
633
+ it "provider.voidを呼び出して失敗レスポンスを返す" do
634
+ allow(gateway).to receive(:void).with(response_code, anything).and_return(error_response)
635
+
636
+ result = payment_method.cancel(response_code, payment)
637
+ expect(result).to eq error_response
638
+ expect(result.success?).to be false
639
+ end
640
+ end
641
+
642
+ context "paymentがnilの場合" do
643
+ let(:response) { double(success?: true, message: 'Void成功') }
644
+
645
+ it "空のoptionsでprovider.voidを呼び出す" do
646
+ allow(gateway).to receive(:void).with(response_code, {}).and_return(response)
647
+
648
+ result = payment_method.cancel(response_code, nil)
649
+ expect(result).to eq response
650
+ end
651
+ end
652
+ end
653
+
654
+ describe "#void" do
655
+ let(:payment_method) { create(:payment_method_gmo_pg) }
656
+ let(:gateway) { instance_double(ActiveMerchant::Billing::GmoPgGateway) }
657
+ let(:order) { create(:order, number: 'R123456', user: nil) }
658
+ let(:credit_card) { create(:credit_card, user: nil) }
659
+ let(:payment) { create(:payment, order: order, payment_method: payment_method, source: credit_card, response_code: 'access_id_123|access_pass_123|R123456-PA1') }
660
+ let(:response_code) { 'access_id_123|access_pass_123|R123456-PA1' }
661
+ let(:response) { double(success?: true, message: 'Void成功') }
662
+
663
+ before do
664
+ allow(payment_method).to receive(:provider).and_return(gateway)
665
+ end
666
+
667
+ context "3つの引数で呼び出された場合(Spreeの標準動作)" do
668
+ let(:gateway_options) { { order_id: 'R123456' } }
669
+
670
+ it "provider.voidを2つの引数で呼び出す" do
671
+ allow(gateway).to receive(:void).and_return(response)
672
+ expect(gateway).to receive(:void).with(response_code, gateway_options)
673
+
674
+ result = payment_method.void(response_code, credit_card, gateway_options)
675
+ expect(result).to eq response
676
+ end
677
+ end
678
+
679
+ context "2つの引数で呼び出された場合(互換性)" do
680
+ let(:options) { { order_id: 'R123456' } }
681
+
682
+ it "provider.voidを正しく呼び出す" do
683
+ allow(gateway).to receive(:void).and_return(response)
684
+ expect(gateway).to receive(:void).with(response_code, options)
685
+
686
+ result = payment_method.void(response_code, options)
687
+ expect(result).to eq response
688
+ end
689
+ end
690
+
691
+ context "1つの引数で呼び出された場合" do
692
+ it "provider.voidを空のoptionsで呼び出す" do
693
+ allow(gateway).to receive(:void).and_return(response)
694
+ expect(gateway).to receive(:void).with(response_code, {})
695
+
696
+ result = payment_method.void(response_code)
697
+ expect(result).to eq response
698
+ end
699
+ end
700
+ end
701
+
702
+ describe "#credit" do
703
+ let(:payment_method) { create(:payment_method_gmo_pg) }
704
+ let(:gateway) { instance_double(ActiveMerchant::Billing::GmoPgGateway) }
705
+ let(:order) { create(:order, number: 'R123456', user: nil) }
706
+ let(:credit_card) { create(:credit_card, user: nil) }
707
+ let(:payment) { create(:payment, order: order, payment_method: payment_method, source: credit_card, response_code: authorization, state: payment_state) }
708
+ let(:authorization) { 'access_id_123|access_pass_123|R123456-PA1' }
709
+ let(:captured_amount) { 10000.0 }
710
+ let(:payment_state) { 'completed' }
711
+
712
+ before do
713
+ allow(payment_method).to receive(:provider).and_return(gateway)
714
+ payment.update_column(:amount, captured_amount)
715
+ allow(payment).to receive(:captured_amount).and_return(captured_amount)
716
+ end
717
+
718
+ context "originatorが無い場合" do
719
+ let(:options) { {} }
720
+
721
+ it "GatewayErrorを発生させる" do
722
+ expect {
723
+ payment_method.credit(100000, credit_card, authorization, options)
724
+ }.to raise_error(Spree::Core::GatewayError, 'Refund originator is required')
725
+ end
726
+ end
727
+
728
+ context "全額返金の場合" do
729
+ let(:refund) { double('Refund', payment: payment) }
730
+ let(:options) { { originator: refund } }
731
+ let(:money) { captured_amount.to_i }
732
+ let(:response) { double(success?: true, message: 'Void成功') }
733
+ let(:refunds_relation) { double('RefundsRelation') }
734
+
735
+ before do
736
+ allow(payment).to receive(:refunds).and_return(refunds_relation)
737
+ allow(refunds_relation).to receive_messages(where: refunds_relation, not: refunds_relation)
738
+ allow(refunds_relation).to receive(:sum).with(:amount).and_return(0.0)
739
+ end
740
+
741
+ it "provider.voidを呼び出して全額キャンセルする" do
742
+ expect(gateway).to receive(:void).with(authorization, options)
743
+ allow(gateway).to receive(:void).and_return(response)
744
+
745
+ result = payment_method.credit(money, credit_card, authorization, options)
746
+ expect(result).to eq response
747
+ end
748
+ end
749
+
750
+ context "部分返金の場合(売上計上済み)" do
751
+ let(:refund) { double('Refund', payment: payment) }
752
+ let(:options) { { originator: refund } }
753
+ let(:refund_amount) { 3000.0 }
754
+ let(:money) { refund_amount.to_i }
755
+ let(:new_amount_cents) { (captured_amount - refund_amount).to_i }
756
+ let(:response) { double(success?: true, message: '金額変更成功') }
757
+ let(:refunds_relation) { double('RefundsRelation') }
758
+
759
+ before do
760
+ allow(payment).to receive(:refunds).and_return(refunds_relation)
761
+ allow(refunds_relation).to receive_messages(where: refunds_relation, not: refunds_relation)
762
+ allow(refunds_relation).to receive(:sum).with(:amount).and_return(0.0)
763
+ end
764
+
765
+ it "provider.update_amountを呼び出して減額する(job_cd: CAPTURE)" do
766
+ expect(gateway).to receive(:update_amount).with(
767
+ new_amount_cents,
768
+ authorization,
769
+ hash_including(job_cd: 'CAPTURE')
770
+ )
771
+ allow(gateway).to receive(:update_amount).and_return(response)
772
+
773
+ result = payment_method.credit(money, credit_card, authorization, options)
774
+ expect(result).to eq response
775
+ end
776
+ end
777
+
778
+ context "部分返金の場合(オーソリのみ)" do
779
+ let(:payment_state) { 'pending' }
780
+ let(:refund) { double('Refund', payment: payment) }
781
+ let(:options) { { originator: refund } }
782
+ let(:refund_amount) { 3000.0 }
783
+ let(:money) { refund_amount.to_i }
784
+ let(:new_amount_cents) { (captured_amount - refund_amount).to_i }
785
+ let(:response) { double(success?: true, message: '金額変更成功') }
786
+ let(:refunds_relation) { double('RefundsRelation') }
787
+
788
+ before do
789
+ allow(refunds_relation).to receive_messages(where: refunds_relation, not: refunds_relation)
790
+ allow(refunds_relation).to receive(:sum).with(:amount).and_return(0.0)
791
+ allow(payment).to receive_messages(refunds: refunds_relation, completed?: false)
792
+ end
793
+
794
+ it "provider.update_amountを呼び出して減額する(job_cd: AUTH)" do
795
+ expect(gateway).to receive(:update_amount).with(
796
+ new_amount_cents,
797
+ authorization,
798
+ hash_including(job_cd: 'AUTH')
799
+ )
800
+ allow(gateway).to receive(:update_amount).and_return(response)
801
+
802
+ result = payment_method.credit(money, credit_card, authorization, options)
803
+ expect(result).to eq response
804
+ end
805
+ end
806
+
807
+ context "複数回の部分返金の場合" do
808
+ let(:refund) { double('Refund', payment: payment) }
809
+ let(:options) { { originator: refund } }
810
+ let(:first_refund_amount) { 3000.0 }
811
+ let(:second_refund_amount) { 2000.0 }
812
+ let(:money) { second_refund_amount.to_i }
813
+ let(:new_amount_cents) { (captured_amount - first_refund_amount - second_refund_amount).to_i }
814
+ let(:response) { double(success?: true, message: '金額変更成功') }
815
+ let(:refunds_relation) { double('RefundsRelation') }
816
+
817
+ before do
818
+ allow(payment).to receive(:refunds).and_return(refunds_relation)
819
+ allow(refunds_relation).to receive_messages(where: refunds_relation, not: refunds_relation)
820
+ allow(refunds_relation).to receive(:sum).with(:amount).and_return(first_refund_amount)
821
+ end
822
+
823
+ it "既存の返金額を考慮して新しい金額を計算する" do
824
+ expect(gateway).to receive(:update_amount).with(
825
+ new_amount_cents,
826
+ authorization,
827
+ hash_including(job_cd: 'CAPTURE')
828
+ )
829
+ allow(gateway).to receive(:update_amount).and_return(response)
830
+
831
+ result = payment_method.credit(money, credit_card, authorization, options)
832
+ expect(result).to eq response
833
+ end
834
+ end
835
+
836
+ context "複数回の部分返金で最後が全額返金となる場合" do
837
+ let(:refund) { double('Refund', payment: payment) }
838
+ let(:options) { { originator: refund } }
839
+ let(:first_refund_amount) { 3000.0 }
840
+ let(:second_refund_amount) { 7000.0 }
841
+ let(:money) { second_refund_amount.to_i }
842
+ let(:response) { double(success?: true, message: 'Void成功') }
843
+ let(:refunds_relation) { double('RefundsRelation') }
844
+
845
+ before do
846
+ allow(payment).to receive(:refunds).and_return(refunds_relation)
847
+ allow(refunds_relation).to receive_messages(where: refunds_relation, not: refunds_relation)
848
+ allow(refunds_relation).to receive(:sum).with(:amount).and_return(first_refund_amount)
849
+ end
850
+
851
+ it "provider.voidを呼び出して全額キャンセルする" do
852
+ expect(gateway).to receive(:void).with(authorization, options)
853
+ allow(gateway).to receive(:void).and_return(response)
854
+
855
+ result = payment_method.credit(money, credit_card, authorization, options)
856
+ expect(result).to eq response
857
+ end
858
+ end
859
+ end
860
+ end