Merge pull request #13961 from mkllnk/taler-checkout-stock-error

Taler checkout stock error
This commit is contained in:
Maikel
2026-03-20 11:29:58 +11:00
committed by GitHub
19 changed files with 201 additions and 448 deletions

View File

@@ -13,6 +13,9 @@ CAPYBARA_MAX_WAIT_TIME="10"
# successful fallback to `en`.
LOCALE="en_TST"
# For multilingual - ENV doesn't have array so pass it as string with commas
AVAILABLE_LOCALES="en_TST,es,pt"
OFN_REDIS_JOBS_URL="redis://localhost:6379/2"
SECRET_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

View File

@@ -2,15 +2,36 @@
module PaymentGateways
class TalerController < BaseController
include OrderStockCheck
include OrderCompletion
class StockError < StandardError
end
# The Taler merchant backend has taken the payment.
# Now we just need to confirm that and update our local database
# before finalising the order.
def confirm
payment = Spree::Payment.find(params[:payment_id])
# Process payment early because it's probably paid already.
# We want to capture that before any validations raise errors.
unless payment.process!
return redirect_to order_failed_route(step: "payment")
end
@order = payment.order
process_payment_completion!
OrderLocker.lock_order_and_variants(@order) do
raise StockError unless sufficient_stock?
process_payment_completion!
end
rescue Spree::Core::GatewayError => e
flash[:notice] = e.message
redirect_to order_failed_route(step: "payment")
rescue StockError
flash[:notice] = t("checkout.payment_cancelled_due_to_stock")
redirect_to main_app.checkout_step_path(step: "details")
end
end
end

View File

@@ -674,8 +674,6 @@ module Spree
end
def process_each_payment
raise Core::GatewayError, Spree.t(:no_pending_payments) if pending_payments.empty?
pending_payments.each do |payment|
if payment.amount.zero? && zero_priced_order?
payment.update_columns(state: "completed", captured_at: Time.zone.now)

View File

@@ -158,10 +158,10 @@ module Openfoodnetwork
# Activate observers that should always be running.
# config.active_record.observers = :cacher, :garbage_collector, :forum_observer
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
# The default locale is set in the environment.
config.i18n.default_locale = OpenFoodNetwork::I18nConfig.default_locale
config.i18n.available_locales = OpenFoodNetwork::I18nConfig.available_locales
config.i18n.fallbacks = OpenFoodNetwork::I18nConfig.fallbacks
I18n.locale = config.i18n.locale = config.i18n.default_locale
# Calculate digests for locale files so we can know when they change

View File

@@ -111,8 +111,6 @@ Rails.application.configure do
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
config.i18n.fallbacks = [:en]
# Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true

View File

@@ -91,10 +91,6 @@ Rails.application.configure do
# Use https in email links
config.action_mailer.default_url_options = { protocol: 'https' }
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = [:en]
# Send deprecation notices to registered listeners
config.active_support.deprecation = :notify

View File

@@ -60,10 +60,6 @@ Openfoodnetwork::Application.configure do
# Enable threaded mode
# config.threadsafe!
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation can not be found)
config.i18n.fallbacks = [:en]
# Send deprecation notices to registered listeners
config.active_support.deprecation = :notify
end

View File

@@ -89,12 +89,6 @@ Rails.application.configure do
# Raises error for missing translations.
config.i18n.raise_on_missing_translations = true
# Tests assume English text on the site.
config.i18n.default_locale = "en"
config.i18n.available_locales = ['en', 'es', 'pt']
config.i18n.fallbacks = [:en]
I18n.locale = config.i18n.locale = config.i18n.default_locale
# Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true

View File

@@ -20,6 +20,10 @@ module OpenFoodNetwork
(selectable_locales + [default_locale, source_locale]).uniq
end
def self.fallbacks
[default_locale, source_locale].uniq
end
# The default locale that is used when the user doesn't have a preference.
def self.default_locale
ENV["LOCALE"] || ENV["I18N_LOCALE"] || source_locale

View File

@@ -15,7 +15,7 @@ RSpec.describe I18nHelper do
it "sets the default locale" do
helper.set_locale
expect(I18n.locale).to eq :en
expect(I18n.locale).to eq :en_TST
end
it "sets the chosen locale" do
@@ -36,11 +36,11 @@ RSpec.describe I18nHelper do
it "ignores unavailable locales" do
allow(helper).to receive(:params) { { locale: "xx" } }
helper.set_locale
expect(I18n.locale).to eq :en
expect(I18n.locale).to eq :en_TST
end
it "remembers the last chosen locale" do
allow(helper).to receive(:params) { { locale: "en" } }
allow(helper).to receive(:params) { { locale: "en_TST" } }
helper.set_locale
allow(helper).to receive(:params) { { locale: "es" } }
@@ -71,7 +71,7 @@ RSpec.describe I18nHelper do
allow(helper).to receive(:params) { {} }
helper.set_locale
expect(I18n.locale).to eq :en
expect(I18n.locale).to eq :en_TST
end
end
@@ -82,7 +82,7 @@ RSpec.describe I18nHelper do
it "sets the default locale" do
helper.set_locale
expect(I18n.locale).to eq :en
expect(I18n.locale).to eq :en_TST
end
it "sets the chosen locale" do
@@ -102,7 +102,7 @@ RSpec.describe I18nHelper do
end
it "remembers the last chosen locale" do
allow(helper).to receive(:params) { { locale: "en" } }
allow(helper).to receive(:params) { { locale: "en_TST" } }
helper.set_locale
allow(helper).to receive(:params) { { locale: "es" } }

View File

@@ -282,11 +282,6 @@ RSpec.describe Spree::Order do
let(:payment) { build(:payment) }
before { allow(order).to receive_messages pending_payments: [payment], total: 10 }
it "returns false if no pending_payments available" do
allow(order).to receive_messages pending_payments: []
expect(order.process_payments!).to be_falsy
end
context "when the processing is sucessful" do
it "processes the payments" do
expect(payment).to receive(:process!)

View File

@@ -3,16 +3,19 @@
require 'spec_helper'
RSpec.describe "/payment_gateways/taler/:id" do
it "completes the order", :vcr do
shop = create(:distributor_enterprise)
taler = Spree::PaymentMethod::Taler.create!(
let(:shop) { create(:distributor_enterprise) }
let(:taler) {
Spree::PaymentMethod::Taler.create!(
name: "Taler",
environment: "test",
distributors: [shop],
preferred_backend_url: "https://backend.demo.taler.net/instances/sandbox",
preferred_api_key: "sandbox",
)
order = create(:order_ready_for_confirmation, payment_method: taler)
}
let!(:order) { create(:order_ready_for_confirmation, payment_method: taler) }
it "completes the order", :vcr do
payment = Spree::Payment.last
payment.update!(
source: taler,
@@ -30,4 +33,65 @@ RSpec.describe "/payment_gateways/taler/:id" do
payment.reload
expect(payment.state).to eq "completed"
end
it "redirects when payment invalid" do
payment = Spree::Payment.last
payment.update!(
source: taler,
payment_method: taler,
state: "processing", # invalid state to start processing again
)
get payment_gateways_confirm_taler_path(payment_id: payment.id)
expect(response).to redirect_to "/checkout/payment"
payment.reload
expect(payment.state).to eq "processing"
order.reload
expect(order.state).to eq "confirmation"
end
it "redirects when payment failed" do
payment = Spree::Payment.last
payment.update!(
source: taler,
payment_method: taler,
response_code: "2026.020-03R3ETNZZ0DVA",
)
allow_any_instance_of(Taler::Order)
.to receive(:fetch).with("order_status").and_return("claimed")
get payment_gateways_confirm_taler_path(payment_id: payment.id)
expect(response).to redirect_to "/checkout/payment"
payment.reload
expect(payment.state).to eq "failed"
order.reload
expect(order.state).to eq "confirmation"
end
it "handles all variants going out of stock" do
payment = Spree::Payment.last
payment.update!(
source: taler,
payment_method: taler,
response_code: "2026.020-03R3ETNZZ0DVA",
)
allow_any_instance_of(Taler::Order)
.to receive(:fetch).with("order_status").and_return("paid")
order.line_items[0].variant.on_hand = 0
get payment_gateways_confirm_taler_path(payment_id: payment.id)
expect(response).to redirect_to "/checkout/details"
payment.reload
expect(payment.state).to eq "completed"
order.reload
expect(order.state).to eq "confirmation"
end
end

View File

@@ -152,7 +152,7 @@ RSpec.configure do |config|
# Reset locale for all specs.
config.around(:each) do |example|
locale = ENV.fetch('LOCALE', 'en_TST')
locale = OpenFoodNetwork::I18nConfig.default_locale
I18n.with_locale(locale) { example.run }
end

View File

@@ -27,10 +27,6 @@ module WebHelper
yield
end
def set_i18n_locale(locale = 'en')
page.execute_script("I18n.locale = '#{locale}'")
end
def pick_i18n_locale
page.evaluate_script("I18n.locale;")
end

View File

@@ -10,17 +10,12 @@ RSpec.describe 'Multilingual' do
before do
login_as admin_user
visit spree.admin_dashboard_path
end
it 'has three locales available' do
expect(Rails.application.config.i18n[:default_locale]).to eq 'en'
expect(Rails.application.config.i18n[:locale]).to eq 'en'
expect(Rails.application.config.i18n[:available_locales]).to eq ['en', 'es', 'pt']
end
it 'can switch language by params' do
expect(pick_i18n_locale).to eq 'en'
visit spree.admin_dashboard_path
expect(pick_i18n_locale).to eq 'en_TST'
expect(get_i18n_translation('spree_admin_overview_enterprises_header')).to eq 'My Enterprises'
expect(page).to have_content 'My Enterprises'
expect(admin_user.locale).to be_nil
@@ -36,9 +31,9 @@ RSpec.describe 'Multilingual' do
it 'fallbacks to default_locale' do
visit spree.admin_dashboard_path(locale: 'it')
expect(pick_i18n_locale).to eq 'en'
expect(pick_i18n_locale).to eq 'en_TST'
expect(get_i18n_translation('spree_admin_overview_enterprises_header')).to eq 'My Enterprises'
expect(page).to have_content 'My Enterprises'
expect(admin_user.locale).to be_nil
expect(admin_user.reload.locale).to be_nil
end
end

View File

@@ -273,7 +273,7 @@ RSpec.describe "Authentication" do
expect_logged_in
expect(page).to have_content 'SHOP NOW'
expect(user.reload.locale).to eq "en"
expect(user.reload.locale).to eq "en_TST"
end
end

View File

@@ -49,7 +49,6 @@ RSpec.describe "As a consumer, I want to checkout my order" do
before do
login_as(user)
visit checkout_path
end
context "payment step" do
@@ -67,6 +66,7 @@ RSpec.describe "As a consumer, I want to checkout my order" do
context "with a transaction fee" do
before do
visit checkout_path
click_button "Next - Order summary"
end
@@ -84,6 +84,7 @@ RSpec.describe "As a consumer, I want to checkout my order" do
context "after completing the order" do
before do
visit checkout_path
click_on "Complete order"
end
it_behaves_like "displays the transaction fee", "order confirmation"
@@ -277,7 +278,7 @@ RSpec.describe "As a consumer, I want to checkout my order" do
describe "choosing" do
shared_examples "different payment methods" do |pay_method|
context "checking out with #{pay_method}", if: pay_method.eql?("Stripe SCA") == false do
context "checking out with #{pay_method}" do
before do
visit checkout_step_path(:payment)
end
@@ -291,46 +292,9 @@ RSpec.describe "As a consumer, I want to checkout my order" do
expect(order.reload.state).to eq "complete"
end
end
context "for Stripe SCA", if: pay_method.eql?("Stripe SCA") do
around do |example|
with_stripe_setup { example.run }
end
before do
stripe_enable
visit checkout_step_path(:payment)
end
it "selects Stripe SCA and proceeds to the summary step" do
choose pay_method.to_s
fill_out_card_details
click_on "Next - Order summary"
proceed_to_summary
end
context "when saving card" do
it "selects Stripe SCA and proceeds to the summary step" do
stub_customers_post_request(email: order.user.email)
stub_payment_method_attach_request
choose pay_method.to_s
fill_out_card_details
check "Save card for future use"
click_on "Next - Order summary"
proceed_to_summary
# Verify card has been saved with correct stripe IDs
user_credit_card = order.reload.user.credit_cards.first
expect(user_credit_card.gateway_payment_profile_id).to eq "pm_123"
expect(user_credit_card.gateway_customer_profile_id).to eq "cus_A123"
end
end
end
end
describe "shared examples" do
describe "payment method" do
let!(:cash) { create(:payment_method, distributors: [distributor], name: "Cash") }
context "Cash" do
@@ -365,7 +329,40 @@ RSpec.describe "As a consumer, I want to checkout my order" do
create(:stripe_sca_payment_method, distributors: [distributor], name: "Stripe SCA")
}
it_behaves_like "different payment methods", "Stripe SCA"
around do |example|
with_stripe_setup { example.run }
end
before do
stripe_enable
visit checkout_step_path(:payment)
end
it "selects Stripe SCA and proceeds to the summary step" do
choose "Stripe SCA"
fill_out_card_details
click_on "Next - Order summary"
proceed_to_summary
end
context "when saving card" do
it "selects Stripe SCA and proceeds to the summary step" do
stub_customers_post_request(email: order.user.email)
stub_payment_method_attach_request
choose "Stripe SCA"
fill_out_card_details
check "Save card for future use"
click_on "Next - Order summary"
proceed_to_summary
# Verify card has been saved with correct stripe IDs
user_credit_card = order.reload.user.credit_cards.first
expect(user_credit_card.gateway_payment_profile_id).to eq "pm_123"
expect(user_credit_card.gateway_customer_profile_id).to eq "cus_A123"
end
end
end
context "Taler" do

View File

@@ -5,189 +5,72 @@ require 'system_helper'
RSpec.describe 'Multilingual' do
include AuthenticationHelper
include WebHelper
include ShopWorkflow
include UIComponentHelper
include CookieHelper
let(:user) { create(:user) }
it 'has three locales available' do
expect(Rails.application.config.i18n[:default_locale]).to eq 'en'
expect(Rails.application.config.i18n[:locale]).to eq 'en'
expect(Rails.application.config.i18n[:available_locales]).to eq ['en', 'es', 'pt']
expect(Rails.application.config.i18n[:default_locale]).to eq 'en_TST'
expect(Rails.application.config.i18n[:locale]).to eq 'en_TST'
expect(Rails.application.config.i18n[:available_locales]).to eq ['en_TST', 'es', 'pt', 'en']
end
it '18n-js fallsback to default language' do
# in backend it doesn't until we change enforce_available_locales to `true`
it 'can switch language by params' do
visit root_path
set_i18n_locale('it')
expect(pick_i18n_locale).to eq 'en_TST'
expect(get_i18n_translation('label_shops')).to eq 'Shops'
expect(cookies_name).not_to include('locale')
expect(page).to have_content 'SHOPS'
visit root_path(locale: 'es')
expect(pick_i18n_locale).to eq 'es'
expect(get_i18n_translation('label_shops')).to eq 'Tiendas'
expect_menu_and_cookie_in_es
# it is not in the list of available of available_locales
visit root_path(locale: 'it')
expect(pick_i18n_locale).to eq 'es'
expect(get_i18n_translation('label_shops')).to eq 'Tiendas'
expect_menu_and_cookie_in_es
end
context 'can switch language by params' do
it 'in root path' do
visit root_path
expect(pick_i18n_locale).to eq 'en'
expect(get_i18n_translation('label_shops')).to eq 'Shops'
expect(cookies_name).not_to include('locale')
expect(page).to have_content 'SHOPS'
it 'updates user locale from cookie if it is empty' do
visit root_path(locale: 'es')
visit root_path(locale: 'es')
expect(pick_i18n_locale).to eq 'es'
expect(get_i18n_translation('label_shops')).to eq 'Tiendas'
expect_menu_and_cookie_in_es
expect_menu_and_cookie_in_es
expect(user.locale).to be_nil
login_as user
visit root_path
# it is not in the list of available of available_locales
visit root_path(locale: 'it')
expect(pick_i18n_locale).to eq 'es'
expect(get_i18n_translation('label_shops')).to eq 'Tiendas'
expect_menu_and_cookie_in_es
end
expect_menu_and_cookie_in_es
context 'with a product in the cart' do
let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) }
let!(:order_cycle) {
create(:simple_order_cycle, distributors: [distributor], variants: [product.variants.first])
}
let(:product) { create(:simple_product) }
let(:order) { create(:order, order_cycle:, distributor:) }
# The user's locale is not changed if the language was chosen before
# login. Is it a bug or a feature? Probably not important...
expect(user.reload.locale).to eq nil
before do
pick_order order
add_product_to_cart order, product, quantity: 1
end
visit root_path(locale: 'es')
expect(user.reload.locale).to eq 'es'
it "in the cart page" do
visit main_app.cart_path(locale: 'es')
logout
expect_menu_and_cookie_in_es
expect(page).to have_content 'Precio'
end
it "visiting checkout as a guest user" do
visit checkout_path(locale: 'es')
expect_menu_and_cookie_in_es
expect(page).to have_content 'Iniciar sesión'
end
end
expect_menu_and_cookie_in_es
expect(page).to have_content '¿Estás interesada en entrar en Open Food Network?'
end
context 'with user' do
let(:user) { create(:user) }
it "allows switching language via the main navigation" do
visit root_path
it 'updates user locale from cookie if it is empty' do
visit root_path(locale: 'es')
expect(page).to have_content 'SHOPS'
expect_menu_and_cookie_in_es
expect(user.locale).to be_nil
login_as user
visit root_path
find('.language-switcher').click
within '.language-switcher .dropdown' do
expect(page).not_to have_link 'English'
expect(page).to have_link 'Español'
expect_menu_and_cookie_in_es
click_link 'Español'
end
it 'updates user locale and stays in cookie after logout' do
login_as user
visit root_path(locale: 'es')
user.reload
expect(user.locale).to eq 'es'
logout
expect_menu_and_cookie_in_es
expect(page).to have_content '¿Estás interesada en entrar en Open Food Network?'
end
context "visiting checkout as logged user" do
let!(:zone) { create(:zone_with_member) }
let(:supplier) { create(:supplier_enterprise) }
let(:distributor) { create(:distributor_enterprise, charges_sales_tax: true) }
let(:product) {
create(:taxed_product, supplier_id: supplier.id, price: 10, zone:)
}
let(:variant) { product.variants.first }
let!(:order_cycle) {
create(:simple_order_cycle, suppliers: [supplier], distributors: [distributor],
coordinator: create(:distributor_enterprise),
variants: [variant])
}
let(:free_shipping) {
create(:shipping_method, require_ship_address: false)
}
let!(:payment) {
create(:payment_method, distributors: [distributor],
name: "Payment")
}
let(:order) {
create(:order_ready_for_confirmation, distributor:)
}
before do
pick_order order
login_as user
end
it "on the details step" do
visit checkout_step_path(:details, locale: 'es')
expect_menu_and_cookie_in_es
expect(page).to have_content "Sus detalles"
end
it "on the payment step" do
visit checkout_step_path(:payment, locale: 'es')
expect_menu_and_cookie_in_es
expect(page).to have_content "Puede revisar y confirmar su pedido"
end
it "on the summary step" do
visit checkout_step_path(:summary, locale: 'es')
expect_menu_and_cookie_in_es
expect(page).to have_content "Detalles de entrega"
end
end
end
describe "using the language switcher UI" do
context "when there is only one language available" do
before do
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:[]).with("LOCALE").and_return("en")
allow(ENV).to receive(:[]).with("AVAILABLE_LOCALES").and_return("en")
end
it "hides the dropdown language menu" do
visit root_path
expect(page).not_to have_css 'ul.right li.language-switcher.has-dropdown'
end
end
context "when there are multiple languages available" do
before do
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:[]).with("LOCALE").and_return("en")
allow(ENV).to receive(:[]).with("AVAILABLE_LOCALES").and_return("en,es")
end
it "allows switching language via the main navigation" do
visit root_path
expect(page).to have_content 'SHOPS'
find('.language-switcher').click
within '.language-switcher .dropdown' do
expect(page).not_to have_link 'English', href: '/locales/en'
expect(page).to have_link 'Español', href: '/locales/es'
find('li a[href="/locales/es"]').click
end
expect_menu_and_cookie_in_es
end
end
expect_menu_and_cookie_in_es
end
end

View File

@@ -3,221 +3,34 @@
require 'system_helper'
RSpec.describe "Check out with Stripe" do
include AuthenticationHelper
include ShopWorkflow
include CheckoutRequestsHelper
include StripeHelper
include StripeStubs
let(:distributor) { create(:distributor_enterprise) }
let!(:order_cycle) {
create(:simple_order_cycle, distributors: [distributor], variants: [variant])
}
let(:product) { create(:product, price: 10) }
let(:variant) { product.variants.first }
let(:order) {
create(:order, order_cycle:, distributor:, bill_address_id: nil,
ship_address_id: nil)
}
let(:shipping_with_fee) {
create(:shipping_method, require_ship_address: false, name: "Donkeys",
calculator: Calculator::FlatRate.new(preferred_amount: 4.56))
}
let(:free_shipping) { create(:shipping_method) }
let!(:check_with_fee) {
create(:payment_method, distributors: [distributor],
calculator: Calculator::FlatRate.new(preferred_amount: 5.67))
}
around do |example|
with_stripe_setup { example.run }
end
before do
stripe_enable
pick_order order
add_product_to_cart order, product
distributor.shipping_methods << [shipping_with_fee, free_shipping]
end
pending "using Stripe SCA" do
let!(:stripe_account) { create(:stripe_account, enterprise: distributor) }
let!(:stripe_sca_payment_method) {
create(:stripe_sca_payment_method, distributors: [distributor])
}
let!(:shipping_method) { create(:shipping_method) }
let(:error_message) { "Card was declined: insufficient funds." }
before do
stub_payment_intent_get_request
stub_payment_methods_post_request
end
describe "using Stripe SCA" do
context "with guest checkout" do
before do
stub_retrieve_payment_method_request("pm_123")
stub_list_customers_request(email: order.user.email, response: {})
stub_get_customer_payment_methods_request(customer: "cus_A456", response: {})
end
context "when the card is accepted" do
before do
stub_payment_intents_post_request(order:)
stub_successful_capture_request order:
end
it "completes checkout successfully" do
checkout_with_stripe
expect(page).to have_content "Confirmed"
expect(page.find("#amount-paid").text).to have_content "$19.99"
expect(order.reload.completed?).to eq true
expect(order.payments.first.state).to eq "completed"
end
it "completes checkout successfully"
end
context "when the card is rejected" do
before do
stub_payment_intents_post_request(order:)
stub_failed_capture_request order:, response: { message: error_message }
end
it "shows an error message from the Stripe response" do
checkout_with_stripe
expect(page).to have_content error_message
expect(order.reload.state).to eq "cart"
expect(order.payments.first.state).to eq "failed"
end
it "shows an error message from the Stripe response"
end
context "when the card needs extra SCA authorization" do
before do
stripe_redirect_url = checkout_path(payment_intent: "pi_123")
stub_payment_intents_post_request_with_redirect order:,
redirect_url: stripe_redirect_url
end
describe "and the authorization succeeds" do
before do
stub_successful_capture_request order:
end
it "completes checkout successfully" do
checkout_with_stripe
# We make stripe return stripe_redirect_url (which is already sending the user back
# to the checkout) as if the authorization was done. We can then control the actual
# authorization or failure of the payment through the mock
# stub_successful_capture_request
expect(page).to have_content "Confirmed"
expect(order.reload.completed?).to eq true
expect(order.payments.first.state).to eq "completed"
end
it "completes checkout successfully"
end
describe "and the authorization fails" do
before do
stub_failed_capture_request order:, response: { message: error_message }
end
it "shows an error message from the Stripe response" do
checkout_with_stripe
# We make stripe return stripe_redirect_url (which is already sending the user back to
# the checkout) as if the authorization was done. We can then control the actual
# authorization or failure of the payment through the mock stub_failed_capture_request
expect(page).to have_content error_message
expect(order.reload.state).to eq "cart"
expect(order.payments.first.state).to eq "failed"
end
it "shows an error message from the Stripe response"
end
end
context "with multiple payment attempts; one failed and one succeeded" do
before do
stub_payment_intents_post_request order:
end
it "records failed payment attempt and allows order completion" do
# First payment attempt is rejected
stub_failed_capture_request(order:, response: { message: error_message })
checkout_with_stripe
expect(page).to have_content error_message
expect(order.reload.payments.count).to eq 1
expect(order.state).to eq "cart"
expect(order.payments.first.state).to eq "failed"
# Second payment attempt is accepted
stub_successful_capture_request(order:)
place_order
expect(page).to have_content "Confirmed"
expect(order.reload.payments.count).to eq 2
expect(order.state).to eq "complete"
expect(order.payments.last.state).to eq "completed"
end
it "records failed payment attempt and allows order completion"
end
end
context "with a logged in user" do
let(:user) { order.user }
before do
login_as user
end
context "saving a card and re-using it" do
before do
stub_retrieve_payment_method_request("pm_123")
stub_list_customers_request(email: order.user.email, response: {})
stub_get_customer_payment_methods_request(customer: "cus_A456", response: {})
stub_get_customer_payment_methods_request(customer: "cus_A123", response: {})
stub_payment_methods_post_request(
request: { payment_method: "pm_123", customer: "cus_A123" },
response: { pm_id: "pm_123" }
)
stub_add_metadata_request(payment_method: "pm_123", response: {})
stub_payment_intents_post_request(order:)
stub_successful_capture_request(order:)
stub_customers_post_request email: "test@test.com" # First checkout with default details
stub_customers_post_request email: user.email # Second checkout with saved user details
stub_payment_method_attach_request
end
it "allows saving a card and re-using it" do
checkout_with_stripe guest_checkout: false, remember_card: true
expect(page).to have_content "Confirmed"
expect(order.reload.completed?).to eq true
expect(order.payments.first.state).to eq "completed"
# Verify card has been saved with correct stripe IDs
user_credit_card = order.reload.user.credit_cards.first
expect(user_credit_card.gateway_payment_profile_id).to eq "pm_123"
expect(user_credit_card.gateway_customer_profile_id).to eq "cus_A123"
# Prepare a second order
new_order = create(:order, user:, order_cycle:,
distributor:, bill_address_id: nil,
ship_address_id: nil)
pick_order(new_order)
add_product_to_cart(new_order, product, quantity: 10)
stub_payment_intents_post_request order: new_order
stub_successful_capture_request order: new_order
# Checkout with saved card
visit checkout_path
choose free_shipping.name
choose stripe_sca_payment_method.name
expect(page).to have_content "Use a saved card"
expect(page).to have_select 'selected_card', selected: "Visa x-4242 Exp:10/2050"
place_order
end
it "allows saving a card and re-using it"
end
end
end