diff --git a/app/models/spree/order.rb b/app/models/spree/order.rb index d44455d53b..00dfb64d4f 100644 --- a/app/models/spree/order.rb +++ b/app/models/spree/order.rb @@ -553,10 +553,6 @@ module Spree line_item_adjustments.destroy_all end - def has_step?(step) - checkout_steps.include?(step) - end - def state_changed(name) state = "#{name}_state" return unless persisted? @@ -803,7 +799,6 @@ module Spree end def has_available_shipment - return unless has_step?("delivery") return unless address? return unless ship_address&.valid? # errors.add(:base, :no_shipping_methods_available) if available_shipping_methods.empty? diff --git a/app/models/spree/order/checkout.rb b/app/models/spree/order/checkout.rb index ffecf63f23..82efef81e7 100644 --- a/app/models/spree/order/checkout.rb +++ b/app/models/spree/order/checkout.rb @@ -9,7 +9,6 @@ module Spree class_attribute :previous_states class_attribute :checkout_flow class_attribute :checkout_steps - class_attribute :removed_transitions def self.checkout_flow(&block) if block_given? @@ -24,7 +23,6 @@ module Spree self.checkout_steps = {} self.next_event_transitions = [] self.previous_states = [:cart] - self.removed_transitions = [] # Build the checkout flow using the checkout_flow defined either # within the Order class, or a decorator for that class. @@ -95,43 +93,6 @@ module Spree end end - def self.insert_checkout_step(name, options = {}) - before = options.delete(:before) - after = options.delete(:after) unless before - after = checkout_steps.keys.last unless before || after - - cloned_steps = checkout_steps.clone - cloned_removed_transitions = removed_transitions.clone - checkout_flow do - cloned_steps.each_pair do |key, value| - go_to_state(name, options) if key == before - go_to_state(key, value) - go_to_state(name, options) if key == after - end - cloned_removed_transitions.each do |transition| - remove_transition(transition) - end - end - end - - def self.remove_checkout_step(name) - cloned_steps = checkout_steps.clone - cloned_removed_transitions = removed_transitions.clone - checkout_flow do - cloned_steps.each_pair do |key, value| - go_to_state(key, value) unless key == name - end - cloned_removed_transitions.each do |transition| - remove_transition(transition) - end - end - end - - def self.remove_transition(options = {}) - removed_transitions << options - next_event_transitions.delete(find_transition(options)) - end - def self.find_transition(options = {}) return nil if options.nil? || !options.include?(:from) || !options.include?(:to) @@ -173,10 +134,6 @@ module Spree checkout_steps.index(step) end - def self.removed_transitions - @removed_transitions ||= [] - end - def can_go_to_state?(state) return false unless self.state.present? && checkout_step?(state) && diff --git a/spec/features/admin/payments_spec.rb b/spec/features/admin/payments_spec.rb index 851843dc30..677b047045 100644 --- a/spec/features/admin/payments_spec.rb +++ b/spec/features/admin/payments_spec.rb @@ -30,31 +30,4 @@ feature ' expect(page).to have_content "New Payment" end end - - context "with a StripeSCA payment method" do - before do - stripe_payment_method = create(:stripe_sca_payment_method, distributors: [order.distributor]) - order.payments << create(:payment, payment_method: stripe_payment_method, order: order) - end - - it "renders the payment details" do - login_as_admin_and_visit spree.admin_order_payments_path order - - page.click_link("StripeSCA") - expect(page).to have_content order.payments.last.source.last_digits - end - - context "with a deleted credit card" do - before do - order.payments.last.update_attribute(:source, nil) - end - - it "renders the payment details" do - login_as_admin_and_visit spree.admin_order_payments_path order - - page.click_link("StripeSCA") - expect(page).to have_content order.payments.last.amount - end - end - end end diff --git a/spec/features/admin/payments_stripe_spec.rb b/spec/features/admin/payments_stripe_spec.rb new file mode 100644 index 0000000000..b296d0fc32 --- /dev/null +++ b/spec/features/admin/payments_stripe_spec.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +require 'spec_helper' + +feature ' + As an hub manager + I want to make Stripe payments +' do + include AuthenticationHelper + include StripeHelper + + let!(:order) { create(:completed_order_with_fees) } + let!(:stripe_payment_method) do + create(:stripe_sca_payment_method, distributors: [order.distributor]) + end + let!(:stripe_account) do + create(:stripe_account, enterprise: order.distributor, stripe_user_id: "abc123") + end + + before { setup_stripe } + + context "making a new Stripe payment", js: true do + before do + stub_payment_methods_post_request + stub_payment_intent_get_request + end + + context "for a complete order" do + context "with a card that succeeds on card registration" do + before { stub_payment_intents_post_request order: order, stripe_account_header: true } + + context "and succeeds on payment capture" do + before { stub_successful_capture_request order: order } + + it "adds a payment with state complete" do + login_as_admin_and_visit spree.new_admin_order_payment_path order + + fill_in "payment_amount", with: order.total.to_s + fill_in_card_details_in_backoffice + click_button "Update" + + expect(page).to have_link "StripeSCA" + expect(OrderPaymentFinder.new(order.reload).last_payment.state).to eq "completed" + end + end + + context "but fails on payment capture" do + let(:error_message) { "Card was declined: insufficient funds." } + + before { stub_failed_capture_request order: order, response: { message: error_message } } + + it "fails to add a payment due to card error" do + login_as_admin_and_visit spree.new_admin_order_payment_path order + + fill_in "payment_amount", with: order.total.to_s + fill_in_card_details_in_backoffice + click_button "Update" + + expect(page).to have_link "StripeSCA" + expect(page).to have_content "FAILED" + expect(OrderPaymentFinder.new(order.reload).last_payment.state).to eq "failed" + end + end + end + + context "with a card that fails on registration because it requires(redirects) extra auth" do + before do + stub_payment_intents_post_request_with_redirect order: order, + redirect_url: "www.dummy.org" + end + + it "fails to add a payment due to card error" do + login_as_admin_and_visit spree.new_admin_order_payment_path order + + fill_in "payment_amount", with: order.total.to_s + fill_in_card_details_in_backoffice + click_button "Update" + + expect(page).to have_link "StripeSCA" + expect(page).to have_content "PROCESSING" + expect(OrderPaymentFinder.new(order.reload).last_payment.state).to eq "processing" + end + end + end + + context "for an order in payment state" do + let!(:order) { create(:order_with_line_items, distributor: create(:enterprise)) } + + before do + stub_payment_intents_post_request order: order, stripe_account_header: true + stub_successful_capture_request order: order + + while !order.payment? do break unless order.next! end + end + + it "adds a payment with state complete" do + login_as_admin_and_visit spree.new_admin_order_payment_path order + + fill_in "payment_amount", with: order.total.to_s + fill_in_card_details_in_backoffice + click_button "Update" + + expect(page).to have_link "StripeSCA" + expect(OrderPaymentFinder.new(order.reload).last_payment.state).to eq "completed" + end + end + end + + context "with a payment using a StripeSCA payment method" do + before do + order.update payments: [] + order.payments << create(:payment, payment_method: stripe_payment_method, order: order) + end + + it "renders the payment details" do + login_as_admin_and_visit spree.admin_order_payments_path order + + page.click_link("StripeSCA") + expect(page).to have_content order.payments.last.source.last_digits + end + + context "with a deleted credit card" do + before do + order.payments.last.update source: nil + end + + it "renders the payment details" do + login_as_admin_and_visit spree.admin_order_payments_path order + + page.click_link("StripeSCA") + expect(page).to have_content order.payments.last.amount + end + end + + context "that is completed", js: true do + let(:payment) { OrderPaymentFinder.new(order.reload).last_payment } + + before do + stub_payment_intent_get_request stripe_account_header: false + stub_successful_capture_request order: order + + payment.update response_code: "pi_123", amount: order.total + payment.purchase! + + stub_refund_request + end + + it "allows to refund the payment" do + login_as_admin_and_visit spree.admin_order_payments_path order + + expect(page).to have_link "StripeSCA" + expect(page).to have_content "COMPLETED" + + page.find('a.icon-void').click + + expect(page).to have_content "VOID" + expect(payment.reload.state).to eq "void" + end + end + end +end diff --git a/spec/features/consumer/shopping/checkout_stripe_spec.rb b/spec/features/consumer/shopping/checkout_stripe_spec.rb index 349bb6838b..6c99489013 100644 --- a/spec/features/consumer/shopping/checkout_stripe_spec.rb +++ b/spec/features/consumer/shopping/checkout_stripe_spec.rb @@ -93,7 +93,7 @@ feature "Check out with Stripe", js: true do context "with guest checkout" do before do stub_payment_intent_get_request - stub_hub_payment_methods_request + stub_payment_methods_post_request end context "when the card is accepted" do @@ -127,21 +127,10 @@ feature "Check out with Stripe", js: true do end context "when the card needs extra SCA authorization", js: true do - let(:stripe_redirect_url) { checkout_path(payment_intent: "pi_123") } - let(:payment_intent_authorize_response) do - { status: 200, body: JSON.generate(id: "pi_123", - object: "payment_intent", - next_source_action: { - type: "authorize_with_url", - authorize_with_url: { url: stripe_redirect_url } - }, - status: "requires_source_action") } - end - before do - 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) + stripe_redirect_url = checkout_path(payment_intent: "pi_123") + stub_payment_intents_post_request_with_redirect order: order, + redirect_url: stripe_redirect_url end describe "and the authorization succeeds" do diff --git a/spec/lib/open_food_network/group_buy_report_spec.rb b/spec/lib/open_food_network/group_buy_report_spec.rb index 4cadaf4688..8231b2da95 100644 --- a/spec/lib/open_food_network/group_buy_report_spec.rb +++ b/spec/lib/open_food_network/group_buy_report_spec.rb @@ -13,24 +13,23 @@ module OpenFoodNetwork @variant1 = create(:variant) @variant1.product.supplier = @supplier1 @variant1.product.save! + @variant1.reload + shipping_instructions = "pick up on thursday please!" order1 = create(:order, distributor: distributor, bill_address: bill_address, special_instructions: shipping_instructions) line_item11 = create(:line_item, variant: @variant1, order: order1) - order1.line_items << line_item11 - @orders << order1 + @orders << order1.reload order2 = create(:order, distributor: distributor, bill_address: bill_address, special_instructions: shipping_instructions) line_item21 = create(:line_item, variant: @variant1, order: order2) - order2.line_items << line_item21 @variant2 = create(:variant) @variant2.product.supplier = @supplier1 @variant2.product.save! line_item22 = create(:line_item, variant: @variant2, order: order2) - order2.line_items << line_item22 - @orders << order2 + @orders << order2.reload @supplier2 = create(:supplier_enterprise) @variant3 = create(:variant, weight: nil) @@ -39,8 +38,7 @@ module OpenFoodNetwork order3 = create(:order, distributor: distributor, bill_address: bill_address, special_instructions: shipping_instructions) line_item31 = create(:line_item, variant: @variant3, order: order3) - order3.line_items << line_item31 - @orders << order3 + @orders << order3.reload end it "should return a header row describing the report" do diff --git a/spec/models/spree/calculator_spec.rb b/spec/models/spree/calculator_spec.rb index 2aa8c5d0a8..12d0853da8 100644 --- a/spec/models/spree/calculator_spec.rb +++ b/spec/models/spree/calculator_spec.rb @@ -10,9 +10,7 @@ module Spree let!(:line_item2) { create(:line_item, order: order) } before do - order.line_items << line_item - order.line_items << line_item2 - order.shipments = [shipment] + order.reload.shipments = [shipment] end describe "#line_items_for" do diff --git a/spec/models/spree/order/checkout_spec.rb b/spec/models/spree/order/checkout_spec.rb index e5bcea4a2e..6d24512509 100644 --- a/spec/models/spree/order/checkout_spec.rb +++ b/spec/models/spree/order/checkout_spec.rb @@ -30,16 +30,6 @@ describe Spree::Order::Checkout do expect(Spree::Order.find_transition({ foo: :bar, baz: :dog })).to be_falsy end - it '.remove_transition' do - options = { from: transitions.first.keys.first, to: transitions.first.values.first } - allow(Spree::Order).to receive(:next_event_transition).and_return([options]) - expect(Spree::Order.remove_transition(options)).to be_truthy - end - - it '.remove_transition when contract was broken' do - expect(Spree::Order.remove_transition(nil)).to be_falsy - end - context "#checkout_steps" do context "when payment not required" do before { allow(order).to receive_messages payment_required?: false } @@ -160,145 +150,6 @@ describe Spree::Order::Checkout do end end - context "subclassed order" do - # This causes another test above to fail, but fixing this test should make - # the other test pass - class SubclassedOrder < Spree::Order - checkout_flow do - go_to_state :payment - go_to_state :complete - end - end - - it "should only call default transitions once when checkout_flow is redefined" do - order = SubclassedOrder.new - allow(order).to receive_messages payment_required?: true - expect(order).to receive(:process_payments!).once - order.state = "payment" - order.next! - expect(order.state).to eq "complete" - end - end - - context "re-define checkout flow" do - before do - @old_checkout_flow = Spree::Order.checkout_flow - Spree::Order.class_eval do - checkout_flow do - go_to_state :payment - go_to_state :complete - end - end - end - - after do - Spree::Order.checkout_flow(&@old_checkout_flow) - end - - it "should not keep old event transitions when checkout_flow is redefined" do - expect(Spree::Order.next_event_transitions).to eq [{ cart: :payment }, { payment: :complete }] - end - - it "should not keep old events when checkout_flow is redefined" do - state_machine = Spree::Order.state_machine - expect(state_machine.states.any? { |s| s.name == :address }).to be_falsy - known_states = state_machine.events[:next].branches.map(&:known_states).flatten - expect(known_states).to_not include(:address) - expect(known_states).to_not include(:delivery) - expect(known_states).to_not include(:confirm) - end - end - - # Regression test for Spree #3665 - context "with only a complete step" do - before do - @old_checkout_flow = Spree::Order.checkout_flow - Spree::Order.class_eval do - checkout_flow do - go_to_state :complete - end - end - end - - after do - Spree::Order.checkout_flow(&@old_checkout_flow) - end - - it "does not attempt to process payments" do - allow(order).to receive_message_chain(:line_items, :present?).and_return(true) - expect(order).to_not receive(:payment_required?) - expect(order).to_not receive(:process_payments!) - order.next! - end - end - - context "insert checkout step" do - before do - @old_checkout_flow = Spree::Order.checkout_flow - Spree::Order.class_eval do - insert_checkout_step :new_step, before: :address - end - end - - after do - Spree::Order.checkout_flow(&@old_checkout_flow) - end - - it "should maintain removed transitions" do - transition = Spree::Order.find_transition(from: :delivery, to: :confirm) - expect(transition).to be_nil - end - - context "before" do - before do - Spree::Order.class_eval do - insert_checkout_step :before_address, before: :address - end - end - - specify do - order = Spree::Order.new - expect(order.checkout_steps).to eq %w(new_step before_address address delivery complete) - end - end - - context "after" do - before do - Spree::Order.class_eval do - insert_checkout_step :after_address, after: :address - end - end - - specify do - order = Spree::Order.new - expect(order.checkout_steps).to eq %w(new_step address after_address delivery complete) - end - end - end - - context "remove checkout step" do - before do - @old_checkout_flow = Spree::Order.checkout_flow - Spree::Order.class_eval do - remove_checkout_step :address - end - end - - after do - Spree::Order.checkout_flow(&@old_checkout_flow) - end - - it "should maintain removed transitions" do - transition = Spree::Order.find_transition(from: :delivery, to: :confirm) - expect(transition).to be_nil - end - - specify do - order = Spree::Order.new - expect(order.checkout_steps).to eq %w(delivery complete) - end - end - describe 'event :restart_checkout' do let(:order) { build_stubbed(:order) } diff --git a/spec/services/order_checkout_restart_spec.rb b/spec/services/order_checkout_restart_spec.rb index c2dc9b43f1..f74a427c02 100644 --- a/spec/services/order_checkout_restart_spec.rb +++ b/spec/services/order_checkout_restart_spec.rb @@ -23,7 +23,7 @@ describe OrderCheckoutRestart do order.update_attribute(:state, "payment") end - xcontext "when order ship address is nil" do + context "when order ship address is nil" do before { order.ship_address = nil } it "resets the order state, and clears incomplete shipments and payments" do @@ -33,7 +33,7 @@ describe OrderCheckoutRestart do end end - xcontext "when order ship address is not empty" do + context "when order ship address is not empty" do before { order.ship_address = order.address_from_distributor } it "resets the order state, and clears incomplete shipments and payments" do diff --git a/spec/support/request/stripe_helper.rb b/spec/support/request/stripe_helper.rb index 608b1cadd0..ef71652b1a 100644 --- a/spec/support/request/stripe_helper.rb +++ b/spec/support/request/stripe_helper.rb @@ -21,16 +21,31 @@ module StripeHelper fill_in 'CVC', with: '123' end + def fill_in_card_details_in_backoffice + choose "StripeSCA" + fill_in "cardholder_name", with: "David Gilmour" + fill_in "stripe-cardnumber", with: "4242424242424242" + fill_in "exp-date", with: "01-01-2050" + fill_in "cvc", with: "678" + end + def setup_stripe allow(Stripe).to receive(:api_key) { "sk_test_12345" } allow(Stripe).to receive(:publishable_key) { "pk_test_12345" } Spree::Config.set(stripe_connect_enabled: true) end - def stub_payment_intents_post_request(order:, response: {}) + def stub_payment_intents_post_request(order:, response: {}, stripe_account_header: true) + stub = stub_request(:post, "https://api.stripe.com/v1/payment_intents") + .with(basic_auth: ["sk_test_12345", ""], body: /.*#{order.number}/) + stub = stub.with(headers: { 'Stripe-Account' => 'abc123' }) if stripe_account_header + stub.to_return(payment_intent_authorize_response_mock(response)) + end + + def stub_payment_intents_post_request_with_redirect(order:, redirect_url:) 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(response)) + .to_return(payment_intent_redirect_response_mock(redirect_url)) end def stub_payment_intent_get_request(response: {}, stripe_account_header: true) @@ -39,7 +54,7 @@ module StripeHelper stub.to_return(payment_intent_authorize_response_mock(response)) end - def stub_hub_payment_methods_request(response: {}) + def stub_payment_methods_post_request(response: {}) stub_request(:post, "https://api.stripe.com/v1/payment_methods") .with(body: { payment_method: "pm_123" }, headers: { 'Stripe-Account' => 'abc123' }) @@ -61,6 +76,13 @@ module StripeHelper .to_return(response_mock) end + def stub_refund_request + stub_request(:post, "https://api.stripe.com/v1/charges/ch_1234/refunds") + .with(body: { amount: 2000, expand: ["charge"] }, + headers: { 'Stripe-Account' => 'abc123' }) + .to_return(payment_successful_refund_mock) + end + private def payment_intent_authorize_response_mock(options) @@ -74,6 +96,16 @@ module StripeHelper charges: { data: [{ id: "ch_1234", amount: 2000 }] }) } end + def payment_intent_redirect_response_mock(redirect_url) + { status: 200, body: JSON.generate(id: "pi_123", + object: "payment_intent", + next_source_action: { + type: "authorize_with_url", + authorize_with_url: { url: redirect_url } + }, + status: "requires_source_action") } + end + def payment_successful_capture_mock(options) { status: options[:code] || 200, body: JSON.generate(object: "payment_intent", @@ -91,4 +123,11 @@ module StripeHelper { status: options[:code] || 200, body: JSON.generate(id: "pm_456", customer: "cus_A123") } end + + def payment_successful_refund_mock + { status: 200, + body: JSON.generate(object: "refund", + amount: 2000, + charge: "ch_1234") } + end end