mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-03-26 05:55:15 +00:00
At closer inspection, almost all logic around which payment actions to display involves only the state of the payment. So I moved the logic there. We now have one list of all possible actions supported by the UX. Then payment methods can declare a list of supported actions. If that's conditional, they can implement the conditions themselves. The payment model itself then still filters the actions based on its state.
1247 lines
43 KiB
Ruby
1247 lines
43 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
RSpec.describe Spree::Payment do
|
|
before do
|
|
# mock the call with "ofn.payment_transition" so we don't call the related listener and services
|
|
allow(ActiveSupport::Notifications).to receive(:instrument).and_call_original
|
|
allow(ActiveSupport::Notifications).to receive(:instrument)
|
|
.with("ofn.payment_transition", any_args).and_return(nil)
|
|
end
|
|
|
|
context 'original specs from Spree' do
|
|
before { Stripe.api_key = "sk_test_12345" }
|
|
let(:order) { create(:order) }
|
|
let(:shop) { create(:enterprise) }
|
|
let(:payment_method) {
|
|
payment_method = create(:stripe_sca_payment_method,
|
|
distributor_ids: [create(:distributor_enterprise).id],
|
|
preferred_enterprise_id: shop.id)
|
|
allow(payment_method).to receive(:source_required) { true }
|
|
allow(payment_method).to receive(:payment) { true }
|
|
payment_method
|
|
}
|
|
|
|
let(:card) {
|
|
create(:credit_card)
|
|
}
|
|
|
|
let(:payment) do
|
|
payment = create(:payment)
|
|
payment.source = card
|
|
payment.order = order
|
|
payment.payment_method = payment_method
|
|
payment.response_code = "12345"
|
|
payment
|
|
end
|
|
|
|
let(:amount_in_cents) { payment.amount.to_f * 100 }
|
|
|
|
let(:success_response) do
|
|
instance_double(
|
|
ActiveMerchant::Billing::Response,
|
|
authorization: "123",
|
|
success?: true,
|
|
avs_result: { 'code' => 'avs-code' },
|
|
cvv_result: { code: nil, message: nil }
|
|
)
|
|
end
|
|
let(:failed_response) { instance_double(ActiveMerchant::Billing::Response, success?: false) }
|
|
|
|
context "extends LocalizedNumber" do
|
|
subject { build_stubbed(:payment) }
|
|
it_behaves_like "a model using the LocalizedNumber module", [:amount]
|
|
end
|
|
|
|
context 'validations' do
|
|
it "returns useful error messages when source is invalid" do
|
|
payment = build_stubbed(:payment, source: Spree::CreditCard.new)
|
|
expect(payment).not_to be_valid
|
|
cc_errors = payment.errors['Credit Card']
|
|
expect(cc_errors).to include("Number can't be blank")
|
|
expect(cc_errors).to include("Month is not a number")
|
|
expect(cc_errors).to include("Year is not a number")
|
|
expect(cc_errors).to include("Verification Value can't be blank")
|
|
end
|
|
end
|
|
|
|
context "creating a new payment alongside other incomplete payments" do
|
|
let(:order) { create(:order_with_totals) }
|
|
let!(:incomplete_payment) { create(:payment, order:, state: "pending") }
|
|
let(:new_payment) { create(:payment, order:, state: "checkout") }
|
|
|
|
it "invalidates other incomplete payments on the order" do
|
|
new_payment
|
|
|
|
expect(incomplete_payment.reload.state).to eq "invalid"
|
|
end
|
|
end
|
|
|
|
# Regression test for https://github.com/spree/spree/pull/2224
|
|
context 'failure' do
|
|
it 'should transition to failed from pending state' do
|
|
payment.state = 'pending'
|
|
payment.failure
|
|
expect(payment.state).to eql('failed')
|
|
end
|
|
|
|
it 'should transition to failed from processing state' do
|
|
payment.state = 'processing'
|
|
payment.failure
|
|
expect(payment.state).to eql('failed')
|
|
end
|
|
end
|
|
|
|
context 'invalidate' do
|
|
it 'should transition from checkout to invalid' do
|
|
payment.state = 'checkout'
|
|
payment.invalidate
|
|
expect(payment.state).to eq('invalid')
|
|
end
|
|
end
|
|
|
|
context "processing" do
|
|
before do
|
|
allow(payment).to receive(:record_response)
|
|
allow(card).to receive(:has_payment_profile?).and_return(true)
|
|
allow(payment).to receive(:update_order).and_return(true)
|
|
allow(payment).to receive(:create_payment_profile).and_return(true)
|
|
end
|
|
|
|
describe "#process!" do
|
|
it "should call purchase!" do
|
|
payment = build_stubbed(:payment, payment_method:)
|
|
expect(payment).to receive(:purchase!)
|
|
payment.process!
|
|
end
|
|
|
|
it "should make the state 'processing'" do
|
|
allow(payment_method).to receive(:capture).and_return(success_response)
|
|
allow(payment_method).to receive(:purchase).and_return(success_response)
|
|
|
|
expect(payment).to receive(:started_processing!)
|
|
|
|
payment.save!
|
|
payment.process!
|
|
end
|
|
|
|
it "should invalidate if payment method doesnt support source" do
|
|
expect(payment.payment_method).to receive(:supports?)
|
|
.with(payment.source).and_return(false)
|
|
expect { payment.process! }.to raise_error(Spree::Core::GatewayError)
|
|
expect(payment.state).to eq('invalid')
|
|
end
|
|
|
|
context "the payment is already authorized" do
|
|
before do
|
|
allow(payment).to receive(:response_code) { "pi_123" }
|
|
end
|
|
|
|
it "should call purchase" do
|
|
expect(payment).to receive(:purchase!)
|
|
payment.process!
|
|
end
|
|
end
|
|
|
|
context "with an internal payment method" do
|
|
let(:payment_method) { create(:customer_credit_payment_method) }
|
|
|
|
it "calls internal_purchase!" do
|
|
payment = build_stubbed(:payment, payment_method:)
|
|
expect(payment).to receive(:internal_purchase!)
|
|
payment.process!
|
|
end
|
|
end
|
|
end
|
|
|
|
context "#process_offline when payment is already authorized" do
|
|
before do
|
|
allow(payment).to receive(:response_code) { "pi_123" }
|
|
end
|
|
|
|
it "should call capture if the payment is already authorized" do
|
|
expect(payment).to receive(:capture!)
|
|
expect(payment).not_to receive(:purchase!)
|
|
payment.process_offline!
|
|
end
|
|
end
|
|
|
|
context "#authorize" do
|
|
before do
|
|
allow(payment_method).to receive(:authorize) { success_response }
|
|
end
|
|
|
|
it "should call authorize on the gateway with the payment amount" do
|
|
expect(payment.payment_method).to receive(:authorize).with(
|
|
amount_in_cents, card, anything
|
|
).and_return(success_response)
|
|
payment.authorize!
|
|
end
|
|
|
|
it "should call authorize on the gateway with the currency code" do
|
|
allow(payment).to receive(:currency) { "GBP" }
|
|
expect(payment.payment_method).to receive(:authorize).with(
|
|
amount_in_cents, card, hash_including({ currency: "GBP" })
|
|
).and_return(success_response)
|
|
payment.authorize!
|
|
end
|
|
|
|
it "should log the response" do
|
|
payment.authorize!
|
|
expect(payment).to have_received(:record_response)
|
|
end
|
|
|
|
context "when gateway does not match the environment" do
|
|
it "should raise an exception" do
|
|
allow(payment_method).to receive(:environment) { "foo" }
|
|
expect { payment.authorize! }.to raise_error(Spree::Core::GatewayError)
|
|
end
|
|
end
|
|
|
|
context "if successful" do
|
|
before do
|
|
expect(payment.payment_method).to receive(:authorize).with(
|
|
amount_in_cents, card, anything
|
|
).and_return(success_response)
|
|
end
|
|
|
|
it "should store the response_code, avs_response and cvv_response fields" do
|
|
payment.authorize!
|
|
expect(payment.response_code).to eq('123')
|
|
expect(payment.avs_response).to eq('avs-code')
|
|
end
|
|
|
|
it "should make payment pending" do
|
|
expect(payment).to receive(:pend!)
|
|
payment.authorize!
|
|
end
|
|
end
|
|
|
|
context "authorization is required" do
|
|
before do
|
|
allow(success_response).to receive(:cvv_result) {
|
|
{ 'code' => nil,
|
|
'message' => nil,
|
|
'redirect_auth_url' => "https://stripe.com/redirect" }
|
|
}
|
|
expect(payment.payment_method).to receive(:authorize).with(
|
|
amount_in_cents, card, anything
|
|
).and_return(success_response)
|
|
end
|
|
|
|
it "should move the payment to requires_authorization" do
|
|
expect(payment).to receive(:require_authorization!)
|
|
payment.authorize!
|
|
end
|
|
end
|
|
|
|
context "if unsuccessful" do
|
|
it "should mark payment as failed" do
|
|
allow(payment_method).to receive(:authorize).and_return(failed_response)
|
|
expect(payment).to receive(:failure)
|
|
expect(payment).not_to receive(:pend)
|
|
expect {
|
|
payment.authorize!
|
|
}.to raise_error(Spree::Core::GatewayError)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#purchase!" do
|
|
before do
|
|
allow(payment_method).to receive(:purchase).and_return(success_response)
|
|
end
|
|
|
|
it "should call purchase on the gateway with the payment amount" do
|
|
expect(payment_method).to receive(:purchase).with(amount_in_cents, card,
|
|
anything).and_return(success_response)
|
|
payment.purchase!
|
|
end
|
|
|
|
it "should log the response" do
|
|
payment.purchase!
|
|
expect(payment).to have_received(:record_response)
|
|
end
|
|
|
|
context "when gateway does not match the environment" do
|
|
it "should raise an exception" do
|
|
allow(payment_method).to receive(:environment) { "foo" }
|
|
expect { payment.purchase! }.to raise_error(Spree::Core::GatewayError)
|
|
end
|
|
end
|
|
|
|
context "if successful" do
|
|
before do
|
|
expect(payment.payment_method).to receive(:purchase).with(
|
|
amount_in_cents, card, anything
|
|
).and_return(success_response)
|
|
end
|
|
|
|
it "should store the response_code and avs_response" do
|
|
payment.purchase!
|
|
expect(payment.response_code).to eq('123')
|
|
expect(payment.avs_response).to eq('avs-code')
|
|
end
|
|
|
|
it "should make payment complete" do
|
|
expect(payment).to receive(:complete!)
|
|
payment.purchase!
|
|
end
|
|
end
|
|
|
|
context "if unsuccessful" do
|
|
it "should make payment failed" do
|
|
allow(payment_method).to receive(:purchase).and_return(failed_response)
|
|
expect(payment).to receive(:failure)
|
|
expect(payment).not_to receive(:pend)
|
|
expect { payment.purchase! }.to raise_error(Spree::Core::GatewayError)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "internal_purchase!" do
|
|
let(:order) { create(:order, customer:) }
|
|
let(:customer) { create(:customer) }
|
|
let(:payment_method) { create(:customer_credit_payment_method) }
|
|
let(:success_response) do
|
|
instance_double(
|
|
ActiveMerchant::Billing::Response,
|
|
success?: true,
|
|
authorization: nil,
|
|
)
|
|
end
|
|
let(:options) {
|
|
{ customer_id: customer.id, payment_id: payment.id, order_number: payment.order.number }
|
|
}
|
|
|
|
before do
|
|
allow(payment_method).to receive(:purchase).and_return(success_response)
|
|
end
|
|
|
|
it "calls purchase on the internal payment method" do
|
|
expect(payment_method).to receive(:purchase).with(
|
|
amount_in_cents, nil, options
|
|
).and_return(success_response)
|
|
|
|
payment.internal_purchase!
|
|
end
|
|
|
|
it "logs the response" do
|
|
expect(payment).to receive(:record_response)
|
|
|
|
payment.internal_purchase!
|
|
end
|
|
|
|
context "when successful" do
|
|
before do
|
|
expect(payment.payment_method).to receive(:purchase).with(
|
|
amount_in_cents, nil, options
|
|
).and_return(success_response)
|
|
end
|
|
|
|
it "makes payment complete" do
|
|
expect(payment).to receive(:complete!)
|
|
payment.internal_purchase!
|
|
end
|
|
end
|
|
|
|
context "when unsuccessful" do
|
|
it "makes payment failed" do
|
|
allow(payment_method).to receive(:purchase).and_return(failed_response)
|
|
|
|
expect(payment).to receive(:failure)
|
|
expect(payment).not_to receive(:pend)
|
|
expect { payment.internal_purchase! }.to raise_error(Spree::Core::GatewayError)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "#capture" do
|
|
before do
|
|
allow(payment).to receive(:complete).and_return(true)
|
|
end
|
|
|
|
context "when payment is pending" do
|
|
before do
|
|
payment.state = 'pending'
|
|
end
|
|
|
|
context "if successful" do
|
|
before do
|
|
expect(payment.payment_method).to receive(:capture).and_return(success_response)
|
|
end
|
|
|
|
it "should make payment complete" do
|
|
expect(payment).to receive(:complete)
|
|
payment.capture!
|
|
end
|
|
|
|
it "should store the response_code" do
|
|
allow(payment_method).to receive(:capture).and_return(success_response)
|
|
payment.capture!
|
|
expect(payment.response_code).to eq('123')
|
|
end
|
|
end
|
|
|
|
context "if unsuccessful" do
|
|
it "should not make payment complete" do
|
|
allow(payment_method).to receive(:capture).and_return(failed_response)
|
|
expect(payment).to receive(:failure)
|
|
expect(payment).not_to receive(:complete)
|
|
expect { payment.capture! }.to raise_error(Spree::Core::GatewayError)
|
|
end
|
|
end
|
|
end
|
|
|
|
# Regression test for #2119
|
|
context "when payment is completed" do
|
|
it "should do nothing" do
|
|
payment = build_stubbed(:payment, :completed)
|
|
expect(payment).not_to receive(:complete)
|
|
expect(payment.payment_method).not_to receive(:capture)
|
|
expect(payment.log_entries).not_to receive(:create)
|
|
payment.capture!
|
|
end
|
|
end
|
|
end
|
|
|
|
context "#void" do
|
|
before do
|
|
payment.response_code = '123'
|
|
payment.state = 'pending'
|
|
|
|
allow(payment_method).to receive(:void).and_return(success_response)
|
|
end
|
|
|
|
it "should log the response" do
|
|
payment.void_transaction!
|
|
expect(payment).to have_received(:record_response)
|
|
end
|
|
|
|
context "when payment_method does not match the environment" do
|
|
it "should raise an exception" do
|
|
payment = build_stubbed(:payment, payment_method:)
|
|
allow(payment_method).to receive(:environment) { "foo" }
|
|
expect { payment.void_transaction! }.to raise_error(Spree::Core::GatewayError)
|
|
end
|
|
end
|
|
|
|
context "if successful" do
|
|
it "should update the response_code with the authorization from the gateway" do
|
|
payment.response_code = 'abc'
|
|
payment.void_transaction!
|
|
|
|
expect(payment.response_code).to eq('123')
|
|
end
|
|
end
|
|
|
|
context "if unsuccessful" do
|
|
it "should not void the payment" do
|
|
allow(payment_method).to receive(:void).and_return(failed_response)
|
|
expect(payment).not_to receive(:void)
|
|
expect { payment.void_transaction! }.to raise_error(Spree::Core::GatewayError)
|
|
end
|
|
end
|
|
|
|
# Regression test for #2119
|
|
context "if payment is already voided" do
|
|
it "should not void the payment" do
|
|
payment = build_stubbed(:payment, payment_method:, state: 'void')
|
|
expect(payment.payment_method).not_to receive(:void)
|
|
payment.void_transaction!
|
|
end
|
|
end
|
|
|
|
context "if payment has any adjustment" do
|
|
let!(:order) { create(:order) }
|
|
let!(:payment_method) {
|
|
create(:payment_method, calculator: Calculator::FlatRate.new(preferred_amount: 10))
|
|
}
|
|
|
|
it "should create another adjustment and revoke the previous one" do
|
|
payment = create(:payment, order:, payment_method:)
|
|
expect(order.all_adjustments.payment_fee.eligible.length).to eq(1)
|
|
|
|
payment.void_transaction!
|
|
expect(order.all_adjustments.payment_fee.eligible.length).to eq(0)
|
|
|
|
payment = create(:payment, order:, payment_method:)
|
|
expect(order.all_adjustments.payment_fee.eligible.length).to eq(1)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#credit" do
|
|
before do
|
|
payment.state = 'completed'
|
|
payment.response_code = '123'
|
|
|
|
# Stub payment.method#credit
|
|
allow(payment_method).to receive(:credit).and_return(success_response)
|
|
end
|
|
|
|
context "when outstanding_balance is less than payment amount" do
|
|
before do
|
|
allow(payment.order).to receive(:outstanding_balance) { 10 }
|
|
allow(payment).to receive(:credit_allowed) { 1000 }
|
|
end
|
|
|
|
it "should call credit on the gateway with the credit amount and response_code" do
|
|
expect(payment_method).to receive(:credit).with(1000, '123',
|
|
anything).and_return(success_response)
|
|
payment.credit!
|
|
end
|
|
end
|
|
|
|
context "if payment method has any payment fees" do
|
|
before do
|
|
expect(payment.order).to receive(:outstanding_balance).at_least(:once) { 10 }
|
|
expect(payment).to receive(:credit_allowed) { 200 }
|
|
end
|
|
|
|
it "should not applied any transaction fees" do
|
|
payment.credit!
|
|
expect(payment.adjustment.finalized?).to eq(false)
|
|
expect(order.all_adjustments.payment_fee.length).to eq(0)
|
|
end
|
|
end
|
|
|
|
context "when outstanding_balance is equal to payment amount" do
|
|
before do
|
|
allow(payment.order).to receive(:outstanding_balance) { payment.amount }
|
|
end
|
|
|
|
it "should call credit on the gateway with the credit amount and response_code" do
|
|
expect(payment_method).to receive(:credit).with(
|
|
amount_in_cents, '123', anything
|
|
).and_return(success_response)
|
|
payment.credit!
|
|
end
|
|
end
|
|
|
|
context "when outstanding_balance is greater than payment amount" do
|
|
before do
|
|
allow(payment.order).to receive(:outstanding_balance) { 101 }
|
|
end
|
|
|
|
it "should call credit on the gateway with original payment amount and response_code" do
|
|
expect(payment_method).to receive(:credit).with(
|
|
amount_in_cents.to_f, '123', anything
|
|
).and_return(success_response)
|
|
payment.credit!
|
|
end
|
|
end
|
|
|
|
context "when amount <= credit_allowed" do
|
|
it "makes the state processing" do
|
|
payment.payment_method.name = 'Gateway'
|
|
payment.payment_method.distributors << create(:distributor_enterprise)
|
|
payment.payment_method.save!
|
|
|
|
payment.order = create(:order)
|
|
|
|
payment.state = 'completed'
|
|
allow(payment).to receive(:credit_allowed) { 10 }
|
|
payment.partial_credit(10)
|
|
expect(payment).to be_processing
|
|
end
|
|
|
|
it "calls credit on the source with the payment and amount" do
|
|
payment.state = 'completed'
|
|
allow(payment).to receive(:credit_allowed) { 10 }
|
|
expect(payment).to receive(:credit!).with(10)
|
|
payment.partial_credit(10)
|
|
end
|
|
end
|
|
|
|
context "when amount > credit_allowed" do
|
|
it "should not call credit on the source" do
|
|
payment = build_stubbed(:payment)
|
|
payment.state = 'completed'
|
|
allow(payment).to receive(:credit_allowed) { 10 }
|
|
payment.partial_credit(20)
|
|
expect(payment).to be_completed
|
|
end
|
|
end
|
|
|
|
it "should log the response" do
|
|
payment.credit!
|
|
expect(payment).to have_received(:record_response)
|
|
end
|
|
|
|
context "when gateway does not match the environment" do
|
|
it "should raise an exception" do
|
|
payment = build_stubbed(:payment, payment_method:)
|
|
allow(payment_method).to receive(:environment) { "foo" }
|
|
expect { payment.credit! }.to raise_error(Spree::Core::GatewayError)
|
|
end
|
|
end
|
|
|
|
context "when response is successful" do
|
|
it "should create an offsetting payment" do
|
|
expect(Spree::Payment).to receive(:create!)
|
|
payment.credit!
|
|
end
|
|
|
|
it "resulting payment should have correct values" do
|
|
allow(payment.order).to receive(:new_outstanding_balance) { 100 }
|
|
allow(payment).to receive(:credit_allowed) { 10 }
|
|
|
|
offsetting_payment = payment.credit!
|
|
expect(offsetting_payment.amount.to_f).to eq(-10)
|
|
expect(offsetting_payment).to be_completed
|
|
expect(offsetting_payment.response_code).to eq('123')
|
|
expect(offsetting_payment.source).to eq(payment)
|
|
end
|
|
|
|
context 'and the source payment card is expired' do
|
|
let(:card) do
|
|
Spree::CreditCard.new(month: 12, year: 1995, number: '4111111111111111',
|
|
verification_value: '123')
|
|
end
|
|
|
|
it 'lets the new payment to be saved' do
|
|
allow(payment.order).to receive(:new_outstanding_balance) { 100 }
|
|
allow(payment).to receive(:credit_allowed) { 10 }
|
|
|
|
offsetting_payment = payment.credit!
|
|
|
|
expect(offsetting_payment).to be_valid
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when response is unsuccessful" do
|
|
it "should not create a payment" do
|
|
allow(payment_method).to receive(:credit).and_return(failed_response)
|
|
expect(Spree::Payment).not_to receive(:create)
|
|
expect { payment.credit! }.to raise_error(Spree::Core::GatewayError)
|
|
end
|
|
end
|
|
|
|
context "when already processing" do
|
|
it "should return nil without trying to process the source" do
|
|
payment = build_stubbed(:payment)
|
|
payment.state = 'processing'
|
|
|
|
expect(payment).not_to receive(:authorize!)
|
|
expect(payment).not_to receive(:purchase!)
|
|
expect(payment.process!).to be_nil
|
|
end
|
|
end
|
|
|
|
context "with source required" do
|
|
context "raises an error if no source is specified" do
|
|
specify do
|
|
payment = build_stubbed(:payment, source: nil, payment_method:)
|
|
expect {
|
|
payment.process!
|
|
}.to raise_error(Spree::Core::GatewayError, Spree.t(:payment_processing_failed))
|
|
end
|
|
end
|
|
end
|
|
|
|
context "with source optional" do
|
|
context "raises no error if source is not specified" do
|
|
specify do
|
|
payment = build_stubbed(:payment, source: nil, payment_method:)
|
|
allow(payment.payment_method).to receive(:source_required?) { false }
|
|
expect { payment.process! }.not_to raise_error
|
|
end
|
|
end
|
|
end
|
|
|
|
context "#credit_allowed" do
|
|
it "is the difference between offsets total and payment amount" do
|
|
payment = build_stubbed(:payment, amount: 100)
|
|
allow(payment).to receive(:offsets_total) { 0 }
|
|
expect(payment.credit_allowed).to eq(100)
|
|
allow(payment).to receive(:offsets_total) { 80 }
|
|
expect(payment.credit_allowed).to eq(20)
|
|
end
|
|
end
|
|
|
|
context "#save" do
|
|
context "completed payments" do
|
|
it "updates order payment total" do
|
|
payment = create(:payment, :completed, amount: 100, order:)
|
|
expect(order.payment_total).to eq payment.amount
|
|
end
|
|
end
|
|
|
|
context "non-completed payments" do
|
|
it "doesn't update order payment total" do
|
|
expect {
|
|
create(:payment, amount: 100, order:)
|
|
}.not_to change { order.payment_total }
|
|
end
|
|
end
|
|
|
|
context 'when the payment was completed but now void' do
|
|
let(:payment) { create(:payment, :completed, amount: 100, order:) }
|
|
|
|
it 'updates order payment total' do
|
|
payment.void
|
|
expect(order.payment_total).to eq 0
|
|
end
|
|
end
|
|
|
|
context "completed orders" do
|
|
let(:order_updater) { OrderManagement::Order::Updater.new(order) }
|
|
|
|
before { allow(order).to receive(:completed?) { true } }
|
|
|
|
it "updates payment_state and shipments" do
|
|
expect(OrderManagement::Order::Updater).to receive(:new).with(order).
|
|
and_return(order_updater)
|
|
|
|
expect(order_updater).to receive(:after_payment_update).with(kind_of(Spree::Payment)).
|
|
and_call_original
|
|
|
|
expect(order_updater).to receive(:update_payment_state)
|
|
expect(order_updater).to receive(:update_shipment_state)
|
|
create(:payment, amount: 100, order:)
|
|
end
|
|
end
|
|
|
|
context "when profiles are supported" do
|
|
before do
|
|
allow(payment.source).to receive(:has_payment_profile?) { false }
|
|
end
|
|
|
|
context "when there is an error connecting to the gateway" do
|
|
it "should call gateway_error " do
|
|
pending '[Spree build] Failing spec'
|
|
message = double("gateway_error")
|
|
connection_error = ActiveMerchant::ConnectionError.new(message, nil)
|
|
expect(payment_method).to receive(:create_profile).and_raise(connection_error)
|
|
expect do
|
|
Spree::Payment.create(
|
|
amount: 100,
|
|
order:,
|
|
source: card,
|
|
payment_method:
|
|
)
|
|
end.to raise_error(Spree::Core::GatewayError)
|
|
end
|
|
end
|
|
|
|
context "when successfully connecting to the gateway" do
|
|
it "should create a payment profile" do
|
|
payment_method.name = 'Gateway'
|
|
payment_method.distributors << create(:distributor_enterprise)
|
|
payment_method.save!
|
|
|
|
payment.payment_method = payment_method
|
|
payment.source.save_requested_by_customer = true
|
|
|
|
expect(payment_method).to receive(:create_profile)
|
|
|
|
Spree::Payment.create(
|
|
amount: 100,
|
|
order: create(:order),
|
|
source: card,
|
|
payment_method:
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when the payment was completed but now void' do
|
|
let(:payment) { create(:payment, :completed, amount: 100, order:) }
|
|
|
|
it 'updates order payment total' do
|
|
payment.void
|
|
expect(order.payment_total).to eq 0
|
|
end
|
|
end
|
|
end
|
|
|
|
context "#build_source" do
|
|
it "should build the payment's source" do
|
|
params = { amount: 100, payment_method:,
|
|
source_attributes: {
|
|
expiry: "1 / 99",
|
|
number: '1234567890123',
|
|
verification_value: '123'
|
|
} }
|
|
|
|
payment = Spree::Payment.new(params)
|
|
expect(payment).to be_valid
|
|
expect(payment.source).not_to be_nil
|
|
end
|
|
|
|
it "errors when payment source not valid" do
|
|
params = { amount: 100, payment_method:,
|
|
source_attributes: { expiry: "1 / 12" } }
|
|
|
|
payment = Spree::Payment.new(params)
|
|
expect(payment).not_to be_valid
|
|
expect(payment.source).not_to be_nil
|
|
expect(payment.source.errors[:number]).not_to be_empty
|
|
expect(payment.source.errors[:verification_value]).not_to be_empty
|
|
end
|
|
end
|
|
|
|
context "#currency" do
|
|
it "returns the order currency" do
|
|
payment = build_stubbed(:payment, order: build_stubbed(:order, currency: "ABC"))
|
|
expect(payment.currency).to eq("ABC")
|
|
end
|
|
end
|
|
|
|
context "#display_amount" do
|
|
it "returns a Spree::Money for this amount" do
|
|
payment = build_stubbed(:payment)
|
|
expect(payment.display_amount).to eq(Spree::Money.new(payment.amount))
|
|
end
|
|
end
|
|
|
|
# Regression test for #2216
|
|
context "#gateway_options" do
|
|
before do
|
|
allow(order).to receive(:last_ip_address) { "192.168.1.1" }
|
|
end
|
|
|
|
it "contains an IP" do
|
|
expect(payment.gateway_options[:ip]).to eq(order.last_ip_address)
|
|
end
|
|
end
|
|
|
|
context "#set_unique_identifier" do
|
|
# Regression test for Spree #1998
|
|
it "sets a unique identifier on create" do
|
|
payment.run_callbacks(:create)
|
|
expect(payment.identifier).not_to be_blank
|
|
expect(payment.identifier.size).to eq(8)
|
|
expect(payment.identifier).to be_a(String)
|
|
end
|
|
|
|
# Regression test for Spree #3733
|
|
it "does not regenerate the identifier on re-save" do
|
|
payment.save
|
|
old_identifier = payment.identifier
|
|
payment.save
|
|
expect(payment.identifier).to eq old_identifier
|
|
end
|
|
|
|
context "other payment exists" do
|
|
let(:other_payment) {
|
|
payment_method.name = 'Gateway'
|
|
payment_method.distributors << create(:distributor_enterprise)
|
|
payment_method.save!
|
|
|
|
payment = Spree::Payment.new
|
|
payment.source = card
|
|
payment.order = create(:order)
|
|
payment.payment_method = payment_method
|
|
payment
|
|
}
|
|
|
|
before { other_payment.save! }
|
|
|
|
it "doesn't set duplicate identifier" do
|
|
allow(payment).to receive(:generate_identifier).and_return(other_payment.identifier)
|
|
allow(payment).to receive(:generate_identifier).and_call_original
|
|
|
|
payment.run_callbacks(:create)
|
|
|
|
expect(payment.identifier).not_to be_blank
|
|
expect(payment.identifier).not_to eq(other_payment.identifier)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "available actions" do
|
|
context "for most gateways" do
|
|
let(:payment) { build_stubbed(:payment, payment_method:) }
|
|
let(:payment_method) { Spree::Gateway::StripeSCA.new }
|
|
|
|
it "can capture and void" do
|
|
expect(payment.actions).to match_array %w(capture_and_complete_order void)
|
|
end
|
|
|
|
describe "when a payment has been taken" do
|
|
before do
|
|
allow(payment).to receive(:state) { 'completed' }
|
|
allow(payment).to receive(:order) { double(:order, payment_state: 'credit_owed') }
|
|
end
|
|
|
|
it "can void and credit" do
|
|
expect(payment.actions).to match_array %w(void credit)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#can_capture_and_complete_order?" do
|
|
it "should be true if payment is pending" do
|
|
payment = build_stubbed(:payment, created_at: Time.zone.now)
|
|
allow(payment).to receive(:pending?) { true }
|
|
expect(payment.can_capture_and_complete_order?).to be_truthy
|
|
end
|
|
|
|
it "should be true if payment is checkout" do
|
|
payment = build_stubbed(:payment, created_at: Time.zone.now)
|
|
allow(payment).to receive_messages pending?: false,
|
|
checkout?: true
|
|
expect(payment.can_capture_and_complete_order?).to be_truthy
|
|
end
|
|
end
|
|
|
|
describe "#can_void?" do
|
|
it "should be true if payment is not void" do
|
|
payment = build_stubbed(:payment)
|
|
allow(payment).to receive(:void?) { false }
|
|
expect(payment.can_void?).to be_truthy
|
|
end
|
|
end
|
|
|
|
describe "#can_credit?" do
|
|
it "should be false if payment is not completed" do
|
|
payment = build_stubbed(:payment)
|
|
allow(payment).to receive(:completed?) { false }
|
|
expect(payment.can_credit?).to be_falsy
|
|
end
|
|
|
|
it "should be false when order payment_state is not 'credit_owed'" do
|
|
payment = build_stubbed(:payment,
|
|
order: create(:order, payment_state: 'paid'))
|
|
allow(payment).to receive(:completed?) { true }
|
|
expect(payment.can_credit?).to be_falsy
|
|
end
|
|
|
|
it "should be false when credit_allowed is zero" do
|
|
payment = build_stubbed(:payment,
|
|
order: create(:order, payment_state: 'credit_owed'))
|
|
allow(payment).to receive_messages completed?: true,
|
|
credit_allowed: 0
|
|
|
|
expect(payment.can_credit?).to be_falsy
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "refund!" do
|
|
let(:payment) { create(:payment) }
|
|
let(:success) { double(success?: true, authorization: 'abc123') }
|
|
let(:failure) { double(success?: false) }
|
|
|
|
it "always checks the environment" do
|
|
allow(payment.payment_method).to receive(:refund) { success }
|
|
expect(payment).to receive(:check_environment)
|
|
payment.refund!
|
|
end
|
|
|
|
describe "calculating refund amount" do
|
|
let(:payment) { build_stubbed(:payment) }
|
|
|
|
it "returns the parameter amount when given" do
|
|
expect(payment.__send__(:calculate_refund_amount, 123)).to eq(123)
|
|
end
|
|
|
|
it "refunds up to the value of the payment when the outstanding balance is larger" do
|
|
allow(payment).to receive(:credit_allowed) { 123 }
|
|
allow(payment).to receive(:order) { double(:order, outstanding_balance: 1000) }
|
|
expect(payment.__send__(:calculate_refund_amount)).to eq(123)
|
|
end
|
|
|
|
it "refunds up to the outstanding balance of the order when the payment is larger" do
|
|
allow(payment).to receive(:credit_allowed) { 1000 }
|
|
allow(payment).to receive(:order) { double(:order, outstanding_balance: 123) }
|
|
expect(payment.__send__(:calculate_refund_amount)).to eq(123)
|
|
end
|
|
end
|
|
|
|
it "records the response" do
|
|
allow(payment).to receive(:calculate_refund_amount) { 123 }
|
|
allow(payment.payment_method).to receive(:refund).and_return(success)
|
|
expect(payment).to receive(:record_response).with(success)
|
|
payment.refund!
|
|
end
|
|
|
|
it "records a payment on success" do
|
|
allow(payment).to receive(:calculate_refund_amount) { 123 }
|
|
allow(payment.payment_method).to receive(:refund).and_return(success)
|
|
allow(payment).to receive(:record_response)
|
|
|
|
expect do
|
|
payment.refund!
|
|
end.to change { Spree::Payment.count }.by(1)
|
|
|
|
p = Spree::Payment.last
|
|
expect(p.order).to eq(payment.order)
|
|
expect(p.source).to eq(payment)
|
|
expect(p.payment_method).to eq(payment.payment_method)
|
|
expect(p.amount).to eq(-123)
|
|
expect(p.response_code).to eq(success.authorization)
|
|
expect(p.state).to eq('completed')
|
|
end
|
|
|
|
it "logs the error on failure" do
|
|
allow(payment).to receive(:calculate_refund_amount) { 123 }
|
|
allow(payment.payment_method).to receive(:refund).and_return(failure)
|
|
allow(payment).to receive(:record_response)
|
|
expect(payment).to receive(:gateway_error).with(failure)
|
|
payment.refund!
|
|
end
|
|
end
|
|
|
|
describe "internal_void!" do
|
|
let(:payment) do
|
|
payment = create(:payment, :completed)
|
|
payment.order = order
|
|
payment.payment_method = payment_method
|
|
payment
|
|
end
|
|
|
|
let(:order) { create(:order, customer:) }
|
|
let(:customer) { create(:customer) }
|
|
let(:payment_method) { create(:customer_credit_payment_method) }
|
|
let(:success_response) do
|
|
instance_double(
|
|
ActiveMerchant::Billing::Response,
|
|
success?: true,
|
|
authorization: nil,
|
|
)
|
|
end
|
|
let(:options) {
|
|
{ customer_id: customer.id, payment_id: payment.id, order_number: payment.order.number }
|
|
}
|
|
|
|
before do
|
|
allow(payment_method).to receive(:void).and_return(success_response)
|
|
end
|
|
|
|
it "calls void on the internal payment method" do
|
|
expect(payment_method).to receive(:void).with(
|
|
amount_in_cents, nil, options
|
|
).and_return(success_response)
|
|
|
|
payment.internal_void!
|
|
end
|
|
|
|
it "logs the response" do
|
|
expect(payment).to receive(:record_response)
|
|
|
|
payment.internal_void!
|
|
end
|
|
|
|
context "when successful" do
|
|
before do
|
|
allow(payment.payment_method).to receive(:void).with(
|
|
amount_in_cents, nil, options
|
|
).and_return(success_response)
|
|
end
|
|
|
|
it "voids the payment" do
|
|
allow(payment).to receive(:record_response)
|
|
|
|
expect { payment.internal_void! }.to change { payment.state }.to("void")
|
|
end
|
|
end
|
|
|
|
context "when unsuccessful" do
|
|
before do
|
|
allow(payment_method).to receive(:void).and_return(failed_response)
|
|
end
|
|
|
|
it "does not create void payment" do
|
|
# Instanciate payment so our count expectation works as expected
|
|
payment
|
|
expect { payment.internal_void! }
|
|
.to raise_error(Spree::Core::GatewayError)
|
|
.and change { Spree::Payment.count }.by(0)
|
|
end
|
|
|
|
it "raises an error" do
|
|
expect { payment.internal_void! }.to raise_error(Spree::Core::GatewayError)
|
|
end
|
|
end
|
|
|
|
context "when payment already voided" do
|
|
it "does nothing" do
|
|
payment.void!
|
|
expect(payment_method).not_to receive(:void)
|
|
|
|
payment.internal_void!
|
|
end
|
|
end
|
|
|
|
context "when payment not voidable" do
|
|
it "raises an error" do
|
|
payment.update(state: "pending")
|
|
expect(payment_method).not_to receive(:void)
|
|
|
|
expect {
|
|
payment.internal_void!
|
|
}.to raise_error(Spree::Core::GatewayError, "Payment not voidable")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "applying transaction fees" do
|
|
let!(:order) { create(:order) }
|
|
let!(:line_item) { create(:line_item, order:, quantity: 3, price: 5.00) }
|
|
|
|
before do
|
|
order.reload.update_order!
|
|
end
|
|
|
|
context "when order-based calculator" do
|
|
let!(:shop) { create(:enterprise) }
|
|
let!(:payment_method) { create(:payment_method, calculator:) }
|
|
let!(:calculator) do
|
|
Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10)
|
|
end
|
|
|
|
context "when order complete" do
|
|
let!(:order) { create(:completed_order_with_totals, distributor: shop) }
|
|
let!(:variant) { order.line_items.first.variant }
|
|
let!(:inventory_item) { create(:inventory_item, enterprise: shop, variant:) }
|
|
|
|
it "creates adjustment" do
|
|
payment = create(:payment, order:, payment_method:,
|
|
amount: order.total)
|
|
expect(payment.adjustment).to be_present
|
|
expect(payment.adjustment.amount).not_to eq(0)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'OFN specs from previously decorated model' do
|
|
describe "applying transaction fees" do
|
|
let!(:order) { create(:order) }
|
|
let!(:line_item) { create(:line_item, order:, quantity: 3, price: 5.00) }
|
|
|
|
before do
|
|
order.reload.update_order!
|
|
end
|
|
|
|
context "to Stripe payments" do
|
|
let(:shop) { create(:enterprise) }
|
|
let(:payment_method) {
|
|
create(:stripe_sca_payment_method, distributor_ids: [create(:distributor_enterprise).id],
|
|
preferred_enterprise_id: shop.id)
|
|
}
|
|
let(:payment) {
|
|
create(:payment, order:, payment_method:, amount: order.total)
|
|
}
|
|
let(:calculator) { 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.all_adjustments.payment_fee.count).to eq 1
|
|
expect(order.all_adjustments.payment_fee.eligible).not_to 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.all_adjustments.payment_fee.count).to eq 1
|
|
expect(order.all_adjustments.payment_fee.eligible).not_to 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.all_adjustments.payment_fee.count).to eq 1
|
|
expect(order.all_adjustments.payment_fee.eligible).to include payment.adjustment
|
|
expect(payment.adjustment.amount).to eq 1.5
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#payment_method" do
|
|
it "returns internal payment method properly" do
|
|
payment_method = create(:customer_credit_payment_method)
|
|
payment = create(:payment, payment_method: )
|
|
|
|
expect(payment.reload.payment_method).to eq(payment_method)
|
|
end
|
|
end
|
|
|
|
describe "#clear_authorization_url" do
|
|
let(:payment) { create(:payment, redirect_auth_url: "auth_url") }
|
|
|
|
it "removes the redirect_auth_url" do
|
|
payment.clear_authorization_url
|
|
expect(payment.redirect_auth_url).to eq(nil)
|
|
end
|
|
end
|
|
|
|
describe "#complete" do
|
|
let(:payment) { build(:payment, state: "processing") }
|
|
|
|
it "sets :captured_at to the current time" do
|
|
payment.complete
|
|
expect(payment.captured_at).to be_present
|
|
end
|
|
end
|
|
|
|
describe "payment transition" do
|
|
it "notifies of payment status change" do
|
|
payment = create(:payment)
|
|
|
|
allow(ActiveSupport::Notifications).to receive(:instrument).and_call_original
|
|
expect(ActiveSupport::Notifications).to receive(:instrument).with(
|
|
"ofn.payment_transition", payment: payment, event: "processing"
|
|
)
|
|
|
|
payment.started_processing!
|
|
end
|
|
end
|
|
end
|