diff --git a/app/models/spree/payment_decorator.rb b/app/models/spree/payment_decorator.rb index 9097fb1b91..a09e65c2fd 100644 --- a/app/models/spree/payment_decorator.rb +++ b/app/models/spree/payment_decorator.rb @@ -7,12 +7,10 @@ module Spree attr_accessible :source def ensure_correct_adjustment - # Don't charge for invalid payments. - # PayPalExpress always creates a payment that is invalidated later. - # Unknown: What about failed payments? - if state == "invalid" - adjustment.andand.destroy - elsif adjustment + revoke_adjustment_eligibility if ['failed', 'invalid'].include?(state) + return if adjustment.try(:finalized?) + + if adjustment adjustment.originator = payment_method adjustment.label = adjustment_label adjustment.save @@ -101,5 +99,16 @@ module Spree rescue ActiveMerchant::ConnectionError => e gateway_error e end + + # Don't charge fees for invalid or failed payments. + # This is called twice for failed payments, because the persistence of the 'failed' + # state is acheived through some trickery using an after_rollback callback on the + # payment model. See Spree::Payment#persist_invalid + def revoke_adjustment_eligibility + return unless adjustment.reload + return if adjustment.finalized? + adjustment.update_attribute(:eligible, false) + adjustment.finalize! + end end end diff --git a/spec/models/spree/payment_spec.rb b/spec/models/spree/payment_spec.rb index 0877959cbb..36fb1ceaea 100644 --- a/spec/models/spree/payment_spec.rb +++ b/spec/models/spree/payment_spec.rb @@ -124,5 +124,89 @@ module Spree payment.refund! end end + + describe "applying transaction fees" do + let!(:order) { create(:order) } + let!(:line_item) { create(:line_item, order: order, quantity: 3, price: 5.00) } + + before do + order.reload.update! + end + + context "to Stripe payments" do + let(:shop) { create(:enterprise) } + let(:payment_method) { create(:stripe_payment_method, distributor_ids: [create(:distributor_enterprise).id], preferred_enterprise_id: shop.id) } + let(:payment) { create(:payment, order: order, payment_method: payment_method, amount: order.total) } + let(:calculator) { Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10) } + + before do + payment_method.calculator = calculator + payment_method.save! + + allow(order).to receive(:pending_payments) { [payment] } + end + + context "when the payment fails" do + let(:failed_response) { ActiveMerchant::Billing::Response.new(false, "This is an error message") } + + before do + allow(payment_method).to receive(:purchase) { failed_response } + end + + it "makes the transaction fee ineligible and finalizes it" do + # Decided to wrap the save process in order.process_payments! + # since that is the context it is usually performed in + order.process_payments! + expect(order.payments.count).to eq 1 + expect(order.payments).to include payment + expect(payment.state).to eq "failed" + expect(payment.adjustment.eligible?).to be false + expect(payment.adjustment.finalized?).to be true + expect(order.adjustments.payment_fee.count).to eq 1 + expect(order.adjustments.payment_fee.eligible).to_not include payment.adjustment + end + end + + context "when the payment information is invalid" do + before do + allow(payment_method).to receive(:supports?) { false } + end + + it "makes the transaction fee ineligible and finalizes it" do + # Decided to wrap the save process in order.process_payments! + # since that is the context it is usually performed in + order.process_payments! + expect(order.payments.count).to eq 1 + expect(order.payments).to include payment + expect(payment.state).to eq "invalid" + expect(payment.adjustment.eligible?).to be false + expect(payment.adjustment.finalized?).to be true + expect(order.adjustments.payment_fee.count).to eq 1 + expect(order.adjustments.payment_fee.eligible).to_not include payment.adjustment + end + end + + context "when the payment is processed successfully" do + let(:successful_response) { ActiveMerchant::Billing::Response.new(true, "Yay!") } + + before do + allow(payment_method).to receive(:purchase) { successful_response } + end + + it "creates an appropriate adjustment" do + # Decided to wrap the save process in order.process_payments! + # since that is the context it is usually performed in + order.process_payments! + expect(order.payments.count).to eq 1 + expect(order.payments).to include payment + expect(payment.state).to eq "completed" + expect(payment.adjustment.eligible?).to be true + expect(order.adjustments.payment_fee.count).to eq 1 + expect(order.adjustments.payment_fee.eligible).to include payment.adjustment + expect(payment.adjustment.amount).to eq 1.5 + end + end + end + end end end