# frozen_string_literal: true require 'spec_helper' RSpec.describe "checking out an order with a Stripe SCA payment method" do include ShopWorkflow include AuthenticationHelper include OpenFoodNetwork::ApiHelper include StripeHelper include StripeStubs let!(:order_cycle) { create(:simple_order_cycle) } let!(:enterprise) { create(:distributor_enterprise) } let!(:shipping_method) do create( :shipping_method, calculator: 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:) } let!(:line_item) { create(:line_item, price: 12.34) } let!(:order) { line_item.order } let(:address) { create(:address) } let(:stripe_payment_method) { "pm_123" } let(:customer_id) { "cus_A123" } let(:hubs_stripe_payment_method) { "pm_456" } let(:payment_intent_id) { "pi_123" } let(:stripe_redirect_url) { "http://stripe.com/redirect" } let(:payments_attributes) do { payment_method_id: payment_method.id, source_attributes: { gateway_payment_profile_id: stripe_payment_method, 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 let(:payment_intent_response_mock) do { status: 200, body: JSON.generate(object: "payment_intent", amount: 2000, charges: { data: [{ id: "ch_1234", amount: 2000 }] }) } end let(:payment_intent_authorize_response_mock) do { status: 200, body: JSON.generate(id: payment_intent_id, object: "payment_intent", amount: 2000, status: "requires_capture", last_payment_error: nil, charges: { data: [{ id: "ch_1234", amount: 2000 }] }) } end before do order_cycle_distributed_variants = double(:order_cycle_distributed_variants) allow(OrderCycles::DistributedVariantsService).to receive(:new) { order_cycle_distributed_variants } allow(order_cycle_distributed_variants).to receive(:distributes_order_variants?) { true } allow(Stripe).to receive(:publishable_key).and_return("some_token") allow(Spree::Config).to receive(:stripe_connect_enabled).and_return(true) Stripe.api_key = "sk_test_12345" order.update(distributor_id: enterprise.id, order_cycle_id: order_cycle.id) order.reload.update_totals pick_order order # Authorizes the payment stub_request(:post, "https://api.stripe.com/v1/payment_intents") .with(basic_auth: ["sk_test_12345", ""], body: /.*#{order.number}/) .to_return(payment_intent_authorize_response_mock) # Retrieves payment intent info stub_request(:get, "https://api.stripe.com/v1/payment_intents/#{payment_intent_id}") .with(headers: { 'Stripe-Account' => 'abc123' }) .to_return(payment_intent_authorize_response_mock) # Captures the payment stub_request(:post, "https://api.stripe.com/v1/payment_intents/#{payment_intent_id}/capture") .with(basic_auth: ["sk_test_12345", ""], body: { amount_to_capture: "1234" }) .to_return(payment_intent_response_mock) 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_add_metadata_request(payment_method: "pm_456", response: {}) end pending "when the user submits a new card and doesn't request that the card is saved for later" do let(:hubs_payment_method_response_mock) do { status: 200, body: JSON.generate(id: hubs_stripe_payment_method) } end before do # Clones the payment method to the hub's stripe account stub_request(:post, "https://api.stripe.com/v1/payment_methods") .with(body: { payment_method: stripe_payment_method }, headers: { 'Stripe-Account' => 'abc123' }) .to_return(hubs_payment_method_response_mock) end context "and the payment intent 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 order_path(order, order_token: order.token) 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 stripe_payment_method 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 payment intent request returns an error message" do let(:payment_intent_response_mock) do { status: 402, body: JSON.generate(error: { message: "payment-intent-failure" }) } end it "should not process the payment" do put(update_checkout_path, params:) expect(response).to have_http_status :bad_request expect(json_response["flash"]["error"]).to eq "payment-intent-failure" expect(order.payments.completed.count).to be 0 end end end pending "when saving a card or using a stored card is involved" do let(:hubs_payment_method_response_mock) do { status: 200, body: JSON.generate(id: hubs_stripe_payment_method, customer: customer_id) } end let(:customer_response_mock) do { status: 200, body: JSON.generate(id: customer_id, sources: { data: [{ id: "1" }] }) } end before do # Clones the payment method to the hub's stripe account stub_request(:post, "https://api.stripe.com/v1/payment_methods") .with(body: { customer: customer_id, payment_method: stripe_payment_method }, headers: { 'Stripe-Account' => 'abc123' }) .to_return(hubs_payment_method_response_mock) # Creates a customer # This stubs the customers call to both the main stripe account and the connected account stub_request(:post, "https://api.stripe.com/v1/customers") .with(body: { email: order.email }) .to_return(customer_response_mock) # Attaches the payment method to the customer in the hub's stripe account stub_request(:post, "https://api.stripe.com/v1/payment_methods/#{hubs_stripe_payment_method}/attach") .with(body: { customer: customer_id }, headers: { 'Stripe-Account' => 'abc123' }) .to_return(hubs_payment_method_response_mock) end context "when the user submits a new card and requests that the card is saved for later" do let(:payment_method_attach_response_mock) do { status: 200, body: JSON.generate(id: stripe_payment_method, customer: customer_id) } end before do source_attributes = params[:order][:payments_attributes][0][:source_attributes] source_attributes[:save_requested_by_customer] = '1' # Attaches the payment method to the customer stub_request(:post, "https://api.stripe.com/v1/payment_methods/#{stripe_payment_method}/attach") .with(body: { customer: customer_id }) .to_return(payment_method_attach_response_mock) end context "and the customer, payment_method and payment_intent requests are successful" do it "should process the payment, and store the card/customer details" do put(update_checkout_path, params:) expect(json_response["path"]).to eq order_path(order, order_token: order.token) 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 stripe_payment_method 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 customer request returns an error message" do let(:customer_response_mock) do { status: 402, body: JSON.generate(error: { message: "customer-store-failure" }) } end it "should not process the payment" do put(update_checkout_path, params:) expect(response).to have_http_status :bad_request expect(json_response["flash"]["error"]) .to eq(format("There was a problem with your payment information: %s", 'customer-store-failure')) expect(order.payments.completed.count).to be 0 end end context "when the payment intent request returns an error message" do let(:payment_intent_response_mock) do { status: 402, body: JSON.generate(error: { message: "payment-intent-failure" }) } end it "should not process the payment" do put(update_checkout_path, params:) expect(response).to have_http_status :bad_request expect(json_response["flash"]["error"]).to eq "payment-intent-failure" expect(order.payments.completed.count).to be 0 end end context "when the payment_method request returns an error message" do let(:hubs_payment_method_response_mock) do { status: 402, body: JSON.generate(error: { message: "payment-method-failure" }) } end it "should not process the payment" do put(update_checkout_path, params:) expect(response).to have_http_status :bad_request expect(json_response["flash"]["error"]).to include "payment-method-failure" expect(order.payments.completed.count).to be 0 end end end context "when the user selects an existing card" do let(:credit_card) do create( :credit_card, user_id: order.user_id, gateway_payment_profile_id: stripe_payment_method, gateway_customer_profile_id: customer_id, last_digits: "4321", cc_type: "master", first_name: "Sammy", last_name: "Signpost", month: 11, year: 2026 ) end before do params[:order][:existing_card_id] = credit_card.id login_as(order.user) end context "and the payment intent and payment method 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 order_path(order, order_token: order.token) 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 stripe_payment_method 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 payment intent request returns an error message" do let(:payment_intent_response_mock) do { status: 402, body: JSON.generate(error: { message: "payment-intent-failure" }) } end it "should not process the payment" do put(update_checkout_path, params:) expect(response).to have_http_status :bad_request expect(json_response["flash"]["error"]).to eq "payment-intent-failure" expect(order.payments.completed.count).to be 0 end end context "when the stripe API sends a url for the authorization of the transaction" do let(:payment_intent_authorize_response_mock) do { status: 200, body: JSON.generate(id: payment_intent_id, object: "payment_intent", next_source_action: { type: "authorize_with_url", authorize_with_url: { url: stripe_redirect_url } }, status: "requires_source_action") } end it "redirects the user to the authorization stripe url" do put(update_checkout_path, params:) expect(response).to have_http_status :ok expect(response.body).to include stripe_redirect_url end end end end end