diff --git a/spec/requests/checkout/stripe_sca_spec.rb b/spec/requests/checkout/stripe_sca_spec.rb new file mode 100644 index 0000000000..f86568b5b4 --- /dev/null +++ b/spec/requests/checkout/stripe_sca_spec.rb @@ -0,0 +1,318 @@ +require 'spec_helper' + +describe "checking out an order with a Stripe SCA payment method", type: :request do + include ShopWorkflow + include AuthenticationWorkflow + include OpenFoodNetwork::ApiHelper + + let!(:order_cycle) { create(:simple_order_cycle) } + let!(:enterprise) { create(:distributor_enterprise) } + let!(:exchange) do + create( + :exchange, + order_cycle: order_cycle, + sender: order_cycle.coordinator, + receiver: enterprise, + incoming: false, + pickup_time: "Monday" + ) + end + let!(:shipping_method) do + create( + :shipping_method, + calculator: Spree::Calculator::FlatRate.new(preferred_amount: 0), + distributors: [enterprise] + ) + end + let!(:payment_method) { create(:stripe_sca_payment_method, distributors: [enterprise]) } + let!(:stripe_account) { create(:stripe_account, enterprise: enterprise) } + let!(:line_item) { create(:line_item, price: 12.34) } + let!(:order) { line_item.order } + let(:address) { create(:address) } + let(:token) { "token123" } + let(:new_token) { "newtoken123" } + let(:card_id) { "card_XyZ456" } + let(:customer_id) { "cus_A123" } + let(:payments_attributes) do + { + payment_method_id: payment_method.id, + source_attributes: { + gateway_payment_profile_id: token, + cc_type: "visa", + last_digits: "4242", + month: 10, + year: 2025, + first_name: 'Jill', + last_name: 'Jeffreys' + } + } + end + let(:allowed_address_attributes) do + [ + "firstname", + "lastname", + "address1", + "address2", + "phone", + "city", + "zipcode", + "state_id", + "country_id" + ] + end + let(:params) do + { + format: :json, order: { + shipping_method_id: shipping_method.id, + payments_attributes: [payments_attributes], + bill_address_attributes: address.attributes.slice(*allowed_address_attributes), + ship_address_attributes: address.attributes.slice(*allowed_address_attributes) + } + } + end + + before do + order_cycle_distributed_variants = double(:order_cycle_distributed_variants) + allow(OrderCycleDistributedVariants).to receive(:new) { order_cycle_distributed_variants } + allow(order_cycle_distributed_variants).to receive(:distributes_order_variants?) { true } + + allow(Stripe).to receive(:api_key) { "sk_test_12345" } + order.update_attributes(distributor_id: enterprise.id, order_cycle_id: order_cycle.id) + order.reload.update_totals + set_order order + end + + context "when a new card is submitted" do + let(:store_response_mock) do + { + status: 200, + body: JSON.generate( + id: customer_id, + default_card: card_id, + sources: { data: [{ id: "1" }] } + ) + } + end + let(:token_response_mock) do + { status: 200, body: JSON.generate(id: new_token) } + end + let(:charge_response_mock) do + { status: 200, body: JSON.generate(id: "ch_1234", object: "charge", amount: 2000) } + end + + context "and the user doesn't request that the card is saved for later" do + before do + # Charges the card + stub_request(:post, "https://api.stripe.com/v1/payment_intents") + .with(basic_auth: ["sk_test_12345", ""], body: /#{token}.*#{order.number}/) + .to_return(charge_response_mock) + end + + context "and the charge request is successful" do + it "should process the payment without storing card details" do + put update_checkout_path, params + + expect(json_response["path"]).to eq spree.order_path(order) + expect(order.payments.completed.count).to be 1 + + card = order.payments.completed.first.source + + expect(card.gateway_customer_profile_id).to eq nil + expect(card.gateway_payment_profile_id).to eq token + expect(card.cc_type).to eq "visa" + expect(card.last_digits).to eq "4242" + expect(card.first_name).to eq "Jill" + expect(card.last_name).to eq "Jeffreys" + end + end + + context "when the charge request returns an error message" do + let(:charge_response_mock) do + { status: 402, body: JSON.generate(error: { message: "charge-failure" }) } + end + + it "should not process the payment" do + put update_checkout_path, params + + expect(response.status).to be 400 + + expect(json_response["flash"]["error"]).to eq "charge-failure" + expect(order.payments.completed.count).to be 0 + end + end + end + + context "and the customer requests that the card is saved for later" do + before do + source_attributes = params[:order][:payments_attributes][0][:source_attributes] + source_attributes[:save_requested_by_customer] = '1' + + # Saves the card against the user + stub_request(:post, "https://api.stripe.com/v1/customers") + .with(basic_auth: ["sk_test_12345", ""], body: { card: token, email: order.email }) + .to_return(store_response_mock) + + # Requests a token from the newly saved card + stub_request(:post, "https://api.stripe.com/v1/tokens") + .with(body: { card: card_id, customer: customer_id }) + .to_return(token_response_mock) + + # Charges the card + stub_request(:post, "https://api.stripe.com/v1/payment_intents") + .with( + basic_auth: ["sk_test_12345", ""], + body: /#{token}.*#{order.number}/ + ).to_return(charge_response_mock) + end + + context "and the store, token and charge requests are successful" do + it "should process the payment, and stores the card/customer details" do + put update_checkout_path, params + + expect(json_response["path"]).to eq spree.order_path(order) + expect(order.payments.completed.count).to be 1 + + card = order.payments.completed.first.source + + expect(card.gateway_customer_profile_id).to eq customer_id + expect(card.gateway_payment_profile_id).to eq card_id + expect(card.cc_type).to eq "visa" + expect(card.last_digits).to eq "4242" + expect(card.first_name).to eq "Jill" + expect(card.last_name).to eq "Jeffreys" + end + end + + context "when the store request returns an error message" do + let(:store_response_mock) do + { status: 402, body: JSON.generate(error: { message: "store-failure" }) } + end + + it "should not process the payment" do + put update_checkout_path, params + + expect(response.status).to be 400 + + expect(json_response["flash"]["error"]) + .to eq(I18n.t(:spree_gateway_error_flash_for_checkout, error: 'store-failure')) + expect(order.payments.completed.count).to be 0 + end + end + + context "when the charge request returns an error message" do + let(:charge_response_mock) do + { status: 402, body: JSON.generate(error: { message: "charge-failure" }) } + end + + it "should not process the payment" do + put update_checkout_path, params + + expect(response.status).to be 400 + + expect(json_response["flash"]["error"]).to eq "charge-failure" + expect(order.payments.completed.count).to be 0 + end + end + + context "when the token request returns an error message" do + let(:token_response_mock) do + { status: 402, body: JSON.generate(error: { message: "token-failure" }) } + end + + # Note, no requests have been stubbed + it "should not process the payment" do + put update_checkout_path, params + + expect(response.status).to be 400 + + expect(json_response["flash"]["error"]).to eq "token-failure" + expect(order.payments.completed.count).to be 0 + end + end + end + end + + context "when an existing card is submitted" do + let(:credit_card) do + create( + :credit_card, + user_id: order.user_id, + gateway_payment_profile_id: card_id, + gateway_customer_profile_id: customer_id, + last_digits: "4321", + cc_type: "master", + first_name: "Sammy", + last_name: "Signpost", + month: 11, year: 2026 + ) + end + + let(:token_response_mock) { { status: 200, body: JSON.generate(id: new_token) } } + let(:charge_response_mock) do + { status: 200, body: JSON.generate(id: "ch_1234", object: "charge", amount: 2000) } + end + + before do + params[:order][:existing_card_id] = credit_card.id + quick_login_as(order.user) + + # Requests a token + #stub_request(:post, "https://api.stripe.com/v1/tokens") + # .with(body: { "card" => card_id, "customer" => customer_id }) + # .to_return(token_response_mock) + + # Charges the card + stub_request(:post, "https://api.stripe.com/v1/payment_intents") + .with(basic_auth: ["sk_test_12345", ""], body: %r{.*#{customer_id}.*#{order.number}.*#{card_id}.*}) + .to_return(charge_response_mock) + end + + context "and the charge and token requests are accepted" do + it "should process the payment, and keep the profile ids and other card details" do + put update_checkout_path, params + + expect(json_response["path"]).to eq spree.order_path(order) + expect(order.payments.completed.count).to be 1 + + card = order.payments.completed.first.source + + expect(card.gateway_customer_profile_id).to eq customer_id + expect(card.gateway_payment_profile_id).to eq card_id + expect(card.cc_type).to eq "master" + expect(card.last_digits).to eq "4321" + expect(card.first_name).to eq "Sammy" + expect(card.last_name).to eq "Signpost" + end + end + + context "when the charge request returns an error message" do + let(:charge_response_mock) do + { status: 402, body: JSON.generate(error: { message: "charge-failure" }) } + end + + it "should not process the payment" do + put update_checkout_path, params + + expect(response.status).to be 400 + + expect(json_response["flash"]["error"]).to eq "charge-failure" + expect(order.payments.completed.count).to be 0 + end + end + + context "when the token request returns an error message" do + let(:token_response_mock) do + { status: 402, body: JSON.generate(error: { message: "token-error" }) } + end + + it "should not process the payment" do + put update_checkout_path, params + + expect(response.status).to be 400 + + expect(json_response["flash"]["error"]).to eq "token-error" + expect(order.payments.completed.count).to be 0 + end + end + end +end