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,154 @@
1
+ <% if @order.state == 'confirm' %>
2
+ <!-- 確認画面: カード情報の確認表示のみ -->
3
+ <% payment = @order.payments.valid.last %>
4
+ <% if payment&.source %>
5
+ <div class="payment-summary p-3 border rounded text-sm">
6
+ <%= payment.source.cc_type.upcase %> **** <%= payment.source.last_digits %>
7
+ (<%= payment.source.month %>/<%= payment.source.year %>)
8
+ </div>
9
+ <% end %>
10
+ <% else %>
11
+ <!-- 支払い方法選択画面: 既存のフォーム表示 -->
12
+ <div class="payment-gateway"
13
+ data-controller="card-validation gmo-pg-payment"
14
+ data-gmo-pg-payment-shop-id-value="<%= payment_method.preferred_shop_id %>"
15
+ data-gmo-pg-payment-test-mode-value="<%= payment_method.preferred_test_mode %>"
16
+ data-gmo-pg-payment-error-messages-value="<%= {
17
+ 'E01' => Spree.t('gmo_pg.errors.invalid_card_info'),
18
+ 'E11' => Spree.t('gmo_pg.errors.card_declined'),
19
+ 'E61' => Spree.t('gmo_pg.errors.invalid_security_code'),
20
+ 'E91' => Spree.t('gmo_pg.errors.processing_error'),
21
+ '201' => Spree.t('gmo_pg.errors.invalid_card_number'),
22
+ 'default' => Spree.t('gmo_pg.errors.token_acquisition_failed')
23
+ }.to_json %>">
24
+
25
+ <% param_prefix = "payment_source[#{payment_method.id}]" %>
26
+
27
+ <!-- エラーメッセージ表示エリア -->
28
+ <div class="alert alert-danger"
29
+ style="display: none;"
30
+ data-gmo-pg-payment-target="errorContainer"
31
+ role="alert">
32
+ </div>
33
+
34
+ <% if @order.user && @order.user.payment_sources.where(payment_method: payment_method).any? %>
35
+ <%= turbo_frame_tag "saved-cards-#{payment_method.id}" do %>
36
+ <%= render 'spree/checkout/payment/saved_cards',
37
+ payment_method: payment_method,
38
+ payment_sources: @order.user.payment_sources.where(payment_method: payment_method) %>
39
+ <% end %>
40
+
41
+ <div class="or-divider mt-3 mb-3">
42
+ <%= radio_button_tag "order[existing_card]", "", true,
43
+ data: {
44
+ gmo_pg_payment_target: "newCardRadio",
45
+ action: "change->gmo-pg-payment#handleCardSelection"
46
+ },
47
+ class: "mr-2"
48
+ %>
49
+ <label class="text-sm"><%= Spree.t('gmo_pg.or_enter_new_card') %></label>
50
+ </div>
51
+ <% end %>
52
+
53
+ <div data-gmo-pg-payment-target="newCardForm" class="payment-gateway-fields">
54
+ <!-- カード名義人 -->
55
+ <div class="form-group mb-3">
56
+ <label for="name_on_card_<%= payment_method.id %>" class="block text-xs text-neutral-600 mb-1">
57
+ <%= Spree.t(:name_on_card) %>
58
+ </label>
59
+ <%= text_field_tag "#{param_prefix}[name]", '',
60
+ id: "name_on_card_#{payment_method.id}",
61
+ data: { gmo_pg_payment_target: "nameInput" },
62
+ placeholder: Spree.t(:name_on_card),
63
+ class: 'text-input w-full',
64
+ required: true
65
+ %>
66
+ </div>
67
+
68
+ <!-- カード番号 (card-validationが自動フォーマット) -->
69
+ <div class="form-group mb-3 relative">
70
+ <label for="card_number_<%= payment_method.id %>" class="block text-xs text-neutral-600 mb-1">
71
+ <%= Spree.t(:card_number) %>
72
+ </label>
73
+ <%= text_field_tag "#{param_prefix}[number]", '',
74
+ id: "card_number_#{payment_method.id}",
75
+ data: {
76
+ card_validation_target: "number",
77
+ gmo_pg_payment_target: "numberInput"
78
+ },
79
+ maxlength: 19,
80
+ autocomplete: "off",
81
+ placeholder: Spree.t(:card_number),
82
+ class: 'text-input w-full pr-10',
83
+ required: true
84
+ %>
85
+ <div class="absolute right-2 top-7"
86
+ data-card-validation-target="typeContainer"
87
+ id="credit-card-type-container-<%= payment_method.id %>">
88
+ </div>
89
+ </div>
90
+
91
+ <div class="flex gap-3">
92
+ <!-- 有効期限 (card-validationがMM/YYYYフォーマット) -->
93
+ <div class="form-group">
94
+ <label for="card_expiry_<%= payment_method.id %>" class="block text-xs text-neutral-600 mb-1">
95
+ <%= Spree.t(:expiration_date) %>
96
+ </label>
97
+ <%= text_field_tag "#{param_prefix}[expiry]", '',
98
+ id: "card_expiry_#{payment_method.id}",
99
+ class: 'text-input',
100
+ data: {
101
+ card_validation_target: "expiry",
102
+ gmo_pg_payment_target: "expiryInput"
103
+ },
104
+ placeholder: Spree.t(:card_expiration_placeholder),
105
+ required: true
106
+ %>
107
+ </div>
108
+
109
+ <!-- CVV (card-validationが桁数調整) -->
110
+ <div class="form-group">
111
+ <label for="card_code_<%= payment_method.id %>" class="block text-xs text-neutral-600 mb-1">
112
+ <%= Spree.t(:cvv) %>
113
+ </label>
114
+ <%= text_field_tag "#{param_prefix}[verification_value]", '',
115
+ id: "card_code_#{payment_method.id}",
116
+ class: 'text-input',
117
+ data: {
118
+ card_validation_target: "cvv",
119
+ gmo_pg_payment_target: "cvvInput"
120
+ },
121
+ maxlength: 4,
122
+ pattern: '[0-9]*',
123
+ inputmode: 'numeric',
124
+ placeholder: Spree.t(:cvv),
125
+ required: true
126
+ %>
127
+ </div>
128
+ </div>
129
+
130
+ <!-- トークン格納用 (GMO-PGトークン) -->
131
+ <%= hidden_field_tag "#{param_prefix}[gateway_payment_profile_id]", '',
132
+ data: { gmo_pg_payment_target: "token" }
133
+ %>
134
+
135
+ <!-- マスク済みカード番号格納用 -->
136
+ <%= hidden_field_tag "#{param_prefix}[last_digits]", '',
137
+ data: { gmo_pg_payment_target: "maskedCard" }
138
+ %>
139
+
140
+ <!-- カードタイプ -->
141
+ <%= hidden_field_tag "#{param_prefix}[cc_type]", '',
142
+ id: "cc_type_#{payment_method.id}",
143
+ data: { card_validation_target: "ccType" }
144
+ %>
145
+
146
+ <!-- カードアイコン(card-validationが自動表示) -->
147
+ <% credit_card_icons.each do |card| %>
148
+ <div id="credit-card-icon-<%= card %>" class="hidden">
149
+ <%= payment_method_icon_tag card, height: 32 %>
150
+ </div>
151
+ <% end %>
152
+ </div>
153
+ </div>
154
+ <% end %>
@@ -0,0 +1,36 @@
1
+ <div class="saved-cards mb-4">
2
+ <% if payment_sources.any? %>
3
+ <h3 class="text-sm font-semibold mb-2"><%= Spree.t('gmo_pg.saved_cards') %></h3>
4
+
5
+ <% payment_sources.each do |card| %>
6
+ <div class="saved-card-item mb-2 p-3 border rounded flex items-center justify-between">
7
+ <label class="flex items-center flex-1 cursor-pointer">
8
+ <%= radio_button_tag "order[existing_card]", card.id, false,
9
+ data: {
10
+ gmo_pg_payment_target: "existingCardRadio",
11
+ action: "change->gmo-pg-payment#handleCardSelection"
12
+ },
13
+ class: "mr-2"
14
+ %>
15
+ <span class="text-sm">
16
+ <%= card.cc_type.upcase %> **** <%= card.last_digits %>
17
+ (<%= card.month %>/<%= card.year %>)
18
+ </span>
19
+ </label>
20
+
21
+ <%= button_tag Spree.t('gmo_pg.delete_card'),
22
+ type: 'button',
23
+ class: 'btn-sm btn-danger ml-2',
24
+ data: {
25
+ turbo_method: :delete,
26
+ turbo_frame: "saved-cards-#{payment_method.id}",
27
+ turbo_confirm: Spree.t('gmo_pg.delete_card_confirm'),
28
+ action: 'click->gmo-pg-payment#deleteCard',
29
+ url: spree.payment_source_path(card)
30
+ } %>
31
+ </div>
32
+ <% end %>
33
+ <% else %>
34
+ <p class="text-sm text-gray-500"><%= Spree.t('gmo_pg.no_saved_cards') %></p>
35
+ <% end %>
36
+ </div>
@@ -0,0 +1,51 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>決済処理中...</title>
6
+ <style>
7
+ body {
8
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
9
+ display: flex;
10
+ justify-content: center;
11
+ align-items: center;
12
+ height: 100vh;
13
+ margin: 0;
14
+ background-color: #f5f5f5;
15
+ }
16
+ .loading {
17
+ text-align: center;
18
+ }
19
+ .spinner {
20
+ border: 4px solid #f3f3f3;
21
+ border-top: 4px solid #3498db;
22
+ border-radius: 50%;
23
+ width: 40px;
24
+ height: 40px;
25
+ animation: spin 1s linear infinite;
26
+ margin: 0 auto 20px;
27
+ }
28
+ @keyframes spin {
29
+ 0% { transform: rotate(0deg); }
30
+ 100% { transform: rotate(360deg); }
31
+ }
32
+ </style>
33
+ </head>
34
+ <body>
35
+ <div class="loading">
36
+ <div class="spinner"></div>
37
+ <p>決済処理中です。しばらくお待ちください...</p>
38
+ </div>
39
+
40
+ <%= form_tag spree.update_checkout_path(state: @order.state, token: @order.token),
41
+ method: :patch,
42
+ id: 'gmo_pg_callback_form' do %>
43
+ <!-- 必要に応じてパラメータ追加 -->
44
+ <% end %>
45
+
46
+ <script>
47
+ // 自動的にフォームをサブミット
48
+ document.getElementById('gmo_pg_callback_form').submit();
49
+ </script>
50
+ </body>
51
+ </html>
@@ -0,0 +1,3 @@
1
+ <div class="alert alert-danger text-center mb-4 p-3" role="alert">
2
+ <p class="mb-0"><%= local_assigns[:message] || Spree.t('error') %></p>
3
+ </div>
@@ -0,0 +1,9 @@
1
+ # Stimulusコントローラーエントリーポイント
2
+ pin 'spree_gmo_pg', to: 'spree_gmo_pg/index.js', preload: true
3
+
4
+ # Stimulusコントローラー
5
+ # Engine.rootは初期化時に使えないため、__dir__を使用
6
+ pin_all_from File.expand_path('../app/javascript/spree_gmo_pg/controllers', __dir__),
7
+ under: 'spree_gmo_pg/controllers',
8
+ to: 'spree_gmo_pg/controllers',
9
+ preload: false
@@ -0,0 +1,29 @@
1
+ en:
2
+ spree:
3
+ gmo_pg:
4
+ api_credentials: "API Credentials"
5
+ settings: "Settings"
6
+ shop_id: "Shop ID"
7
+ shop_id_hint: "Enter the Shop ID issued by GMO-PG"
8
+ shop_pass: "Shop Password"
9
+ shop_pass_hint: "Enter the Shop Password issued by GMO-PG"
10
+ site_id: "Site ID"
11
+ site_id_hint: "Enter the Site ID issued by GMO-PG (required for member management)"
12
+ site_pass: "Site Password"
13
+ site_pass_hint: "Enter the Site Password issued by GMO-PG (required for member management)"
14
+ test_mode: "Test Mode"
15
+ test_mode_hint: "Check this to connect to the test environment (pt01.mul-pay.jp). Make sure to turn this off in production"
16
+ saved_cards: "Saved Cards"
17
+ or_enter_new_card: "Or enter a new card"
18
+ delete_card: "Delete"
19
+ delete_card_confirm: "Are you sure you want to delete this card?"
20
+ card_deleted: "Card has been deleted"
21
+ unauthorized: "You are not authorized to delete this card"
22
+ no_saved_cards: "No saved cards"
23
+ errors:
24
+ token_acquisition_failed: "An error occurred while processing card information"
25
+ invalid_card_info: "Card information is invalid"
26
+ card_declined: "This card cannot be used"
27
+ invalid_security_code: "Security code is invalid"
28
+ processing_error: "An error occurred during processing"
29
+ invalid_card_number: "Card number is invalid"
@@ -0,0 +1,29 @@
1
+ ja:
2
+ spree:
3
+ gmo_pg:
4
+ api_credentials: "API認証情報"
5
+ settings: "動作設定"
6
+ shop_id: "ショップID"
7
+ shop_id_hint: "GMO-PGから発行されたショップIDを入力してください"
8
+ shop_pass: "ショップパスワード"
9
+ shop_pass_hint: "GMO-PGから発行されたショップパスワードを入力してください"
10
+ site_id: "サイトID"
11
+ site_id_hint: "GMO-PGから発行されたサイトIDを入力してください(会員管理機能利用時)"
12
+ site_pass: "サイトパスワード"
13
+ site_pass_hint: "GMO-PGから発行されたサイトパスワードを入力してください(会員管理機能利用時)"
14
+ test_mode: "テストモード"
15
+ test_mode_hint: "チェックを入れるとテスト環境(pt01.mul-pay.jp)に接続します。本番環境では必ずオフにしてください"
16
+ saved_cards: "登録済みのカード"
17
+ or_enter_new_card: "または新規カードを入力"
18
+ delete_card: "削除"
19
+ delete_card_confirm: "このカードを削除してもよろしいですか?"
20
+ card_deleted: "カードを削除しました"
21
+ unauthorized: "このカードを削除する権限がありません"
22
+ no_saved_cards: "登録済みのカードはありません"
23
+ errors:
24
+ token_acquisition_failed: "カード情報の処理中にエラーが発生しました"
25
+ invalid_card_info: "カード情報が正しくありません"
26
+ card_declined: "このカードはご利用いただけません"
27
+ invalid_security_code: "セキュリティコードが正しくありません"
28
+ processing_error: "処理中にエラーが発生しました"
29
+ invalid_card_number: "カード番号が正しくありません"
data/config/routes.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ Spree::Core::Engine.routes.draw do
4
+ resources :payment_sources, only: [ :destroy ]
5
+
6
+ # 3DSコールバックエンドポイント
7
+ # GMO-PGのCallbackType=3(GET方式)に対応
8
+ # GET方式はSameSite=Lax cookieが送信されるため、セッション維持が可能
9
+ get '/gmo_pg/3ds_callback', to: 'gmo_pg_callback#callback', as: :gmo_pg_3ds_callback
10
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ # 3DS認証リダイレクトが必要な場合に投げる例外
5
+ # StandardErrorではなくExceptionから継承することで、
6
+ # state_machineのrescue StandardErrorをバイパスする
7
+ class PaymentRedirectRequired < Exception
8
+ attr_reader :redirect_url, :payment, :gmo_pg_data
9
+
10
+ # @param redirect_url [String] 3DS認証のリダイレクト先URL
11
+ # @param payment [Spree::Payment] 対象の決済
12
+ # @param gmo_pg_data [Hash] コールバックでの再処理に必要な access_id / access_pass 等
13
+ # この例外は process_payments! の with_lock トランザクション内で raise されるため、
14
+ # そこで metadata を save してもロールバックで消えてしまう。トランザクション外
15
+ # (CheckoutController の rescue)で永続化できるよう、必要情報を例外に積んで渡す。
16
+ def initialize(redirect_url, payment: nil, gmo_pg_data: nil)
17
+ @redirect_url = redirect_url
18
+ @payment = payment
19
+ @gmo_pg_data = gmo_pg_data
20
+ super("Payment redirect required: #{redirect_url}")
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpreeGmoPg
4
+ class Engine < Rails::Engine
5
+ require 'spree/core'
6
+ isolate_namespace Spree
7
+ engine_name 'spree_gmo_pg'
8
+
9
+ # Decoratorの読み込み
10
+ def self.activate
11
+ Dir.glob(File.join(File.dirname(__FILE__), '../../app/**/*_decorator*.rb')) do |c|
12
+ Rails.configuration.cache_classes ? require(c) : load(c)
13
+ end
14
+ end
15
+
16
+ # Zeitwerk対応(overridesの除外)
17
+ initializer 'spree_gmo_pg.autoloader' do |app|
18
+ if Rails.autoloaders.zeitwerk_enabled?
19
+ Rails.autoloaders.main.ignore("#{root}/app/overrides")
20
+ end
21
+ end
22
+
23
+ # アセットパスの追加
24
+ initializer 'spree_gmo_pg.assets' do |app|
25
+ app.config.assets.paths << root.join('app/javascript')
26
+ app.config.assets.precompile += %w[spree_gmo_pg_manifest]
27
+ end
28
+
29
+ # importmapの読み込み
30
+ initializer 'spree_gmo_pg.importmap', before: 'importmap' do |app|
31
+ if defined?(Importmap)
32
+ app.config.importmap.paths << root.join('config/importmap.rb')
33
+ app.config.importmap.cache_sweepers << root.join('app/javascript')
34
+ end
35
+ end
36
+
37
+ # 決済方法の登録
38
+ config.after_initialize do |app|
39
+ app.config.spree.payment_methods << Spree::PaymentMethod::GmoPg
40
+ end
41
+
42
+ config.to_prepare(&method(:activate).to_proc)
43
+ end
44
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpreeGmoPg
4
+ VERSION = '1.0.0'
5
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spree_core'
4
+ require 'deface'
5
+
6
+ # Spree::PaymentMethod::GmoPg はクラス本体で Spree::PaymentMethod::AutoCaptureDigitalConcern を
7
+ # include し(= クラス定義の評価時点で解決できていなければならない)、provider_class で
8
+ # ActiveMerchant::Billing::GmoPgGateway を返す。後者を定義するのは active_merchant_gmo_pg の
9
+ # lib エントリなので、gemspec の add_dependency だけでは足りずここで require する必要がある。
10
+ # @see https://github.com/be-agile/giga-repeat/issues/1307
11
+ require 'active_merchant_gmo_pg'
12
+ require 'spree_auto_capture_digital'
13
+
14
+ require 'spree_gmo_pg/version'
15
+ require 'spree_gmo_pg/engine'
16
+ require 'spree/payment_redirect_required'
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Spree::GmoPgCallbackController, type: :controller do
6
+ routes { Spree::Core::Engine.routes }
7
+
8
+ describe "GET #callback" do
9
+ let(:order) { create(:order, token: 'test_token_123') }
10
+ let(:payment_method) { create(:payment_method_gmo_pg) }
11
+ let(:credit_card) { create(:credit_card, user: nil) }
12
+ let(:payment) { create(:payment, order: order, payment_method: payment_method, source: credit_card) }
13
+
14
+ context "Order が見つかる場合" do
15
+ before do
16
+ payment.update!(private_metadata: { gmo_pg: { access_id: 'stored_access_id', access_pass: 'stored_access_pass' } })
17
+ end
18
+
19
+ it "ビューをレンダリングする" do
20
+ get :callback, params: { token: order.token, AccessID: 'stored_access_id' }
21
+
22
+ expect(response).to have_http_status(:success)
23
+ expect(response).to render_template(:callback)
24
+ expect(assigns(:order)).to eq(order)
25
+ end
26
+ end
27
+
28
+ context "Order が見つからない場合" do
29
+ it "root_path にリダイレクトする" do
30
+ get :callback, params: { token: 'invalid_token', AccessID: 'test_access_id', AccessPass: 'test_access_pass' }
31
+
32
+ expect(response).to redirect_to('/')
33
+ expect(flash[:alert]).to eq('ご注文が見つかりませんでした')
34
+ end
35
+ end
36
+
37
+ context "Payment が見つからない場合" do
38
+ it "checkout_state_path にリダイレクトする" do
39
+ get :callback, params: { token: order.token, AccessID: 'test_access_id', AccessPass: 'test_access_pass' }
40
+
41
+ # Spree 5.x のルーティング形式: /checkout/:token/:state
42
+ expect(response).to redirect_to("/checkout/#{order.token}/#{order.state}")
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Spree::PaymentSourcesController, type: :controller do
6
+ routes { Spree::Core::Engine.routes }
7
+
8
+ let(:user) { create(:user) }
9
+ let(:other_user) { create(:user) }
10
+ let(:payment_method) { create(:payment_method_gmo_pg) }
11
+ let(:credit_card) { create(:credit_card, user: user, payment_method: payment_method, gateway_payment_profile_id: 'card_seq_123') }
12
+
13
+ before do
14
+ allow(controller).to receive_messages(spree_current_user: user, authenticate_user!: true)
15
+ end
16
+
17
+ describe "DELETE #destroy" do
18
+ context "自分のカードを削除する場合" do
19
+ context "削除成功時" do
20
+ before do
21
+ # Stub GMO-PG API call
22
+ stub_request(:post, "https://pt01.mul-pay.jp/payment/DeleteCard.idPass")
23
+ .to_return(status: 200, body: "", headers: {})
24
+ end
25
+
26
+ it "カードを削除し、更新後のカード一覧をturbo_streamで返す" do
27
+ delete :destroy, params: { id: credit_card.id }, format: :turbo_stream
28
+ expect(response).to have_http_status(:success)
29
+ expect(response.body).to include("turbo-stream")
30
+ expect(response.body).to include("saved-cards-#{payment_method.id}")
31
+ end
32
+ end
33
+
34
+ context "削除失敗時" do
35
+ before do
36
+ # Stub GMO-PG API call to return error (GMO-PG returns HTTP 200 with error code in body)
37
+ stub_request(:post, "https://pt01.mul-pay.jp/payment/DeleteCard.idPass")
38
+ .to_return(
39
+ status: 200,
40
+ body: "ErrCode=E01&ErrInfo=E01390008",
41
+ headers: { 'Content-Type' => 'application/x-www-form-urlencoded' }
42
+ )
43
+ end
44
+
45
+ it "エラーメッセージを返す" do
46
+ delete :destroy, params: { id: credit_card.id }, format: :turbo_stream
47
+ expect(response).to have_http_status(:unprocessable_entity)
48
+ expect(response.body).to include("turbo-stream")
49
+ end
50
+ end
51
+ end
52
+
53
+ context "他人のカードを削除しようとした場合" do
54
+ let(:other_card) { create(:credit_card, user: other_user, payment_method: payment_method) }
55
+
56
+ it "権限エラーを返す" do
57
+ delete :destroy, params: { id: other_card.id }, format: :turbo_stream
58
+ expect(response).to have_http_status(:forbidden)
59
+ expect(response.body).to include("turbo-stream")
60
+ end
61
+ end
62
+
63
+ context "ログインしていない場合" do
64
+ before do
65
+ allow(controller).to receive(:spree_current_user).and_return(nil)
66
+ allow(controller).to receive(:authenticate_user!).and_call_original
67
+ end
68
+
69
+ it "認証エラーを返す" do
70
+ delete :destroy, params: { id: credit_card.id }, format: :turbo_stream
71
+ expect(response).to have_http_status(:redirect)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :payment_method_gmo_pg, class: 'Spree::PaymentMethod::GmoPg' do
5
+ name { 'GMO-PG Credit Card' }
6
+ active { true }
7
+ auto_capture { false }
8
+
9
+ preferred_shop_id { 'test_shop_id' }
10
+ preferred_shop_pass { 'test_shop_pass' }
11
+ preferred_site_id { 'test_site_id' }
12
+ preferred_site_pass { 'test_site_pass' }
13
+ preferred_test_mode { true }
14
+
15
+ after(:build) do |payment_method|
16
+ payment_method.stores << (Spree::Store.first || FactoryBot.create(:store))
17
+ end
18
+ end
19
+ end