diff --git a/app/services/orders/customer_credit_service.rb b/app/services/orders/customer_credit_service.rb index 3e6494cec6..13d6602816 100644 --- a/app/services/orders/customer_credit_service.rb +++ b/app/services/orders/customer_credit_service.rb @@ -10,15 +10,47 @@ module Orders add_payment_with_credit if credit_available? end + def refund # rubocop:disable Metrics/AbcSize + if order.payment_state != "credit_owed" + return Response.new( + success: false, message: I18n.t(:no_credit_owed, scope: translation_scope) + ) + end + + if credit_payment_method.nil? + error_message = I18n.t(:credit_payment_method_missing, scope: translation_scope) + log_error(error_message) + return Response.new(success: false, message: error_message) + end + + amount = order.new_outstanding_balance + order.customer.with_lock do + payment = order.payments.create!( payment_method: credit_payment_method, amount: amount, + state: "completed", skip_source_validation: true) + + options = { customer_id: order.customer_id, payment_id: payment.id, + order_number: order.number } + response = credit_payment_method.void((-1 * amount * 100).round, nil, options) + + raise response.message if response.failure? + + Response.new(success: true, message: I18n.t(:refund_sucessful, scope: translation_scope)) + end + rescue StandardError => e + # Even though the transaction rolled back, the order still have a payment in memory, + # so we reload the payments so the payment doesn't get saved later on + order.payments.reload + log_error(e) + Response.new(success: false, message: e.to_s) + end + private attr_reader :order def add_payment_with_credit - credit_payment_method = order.distributor.payment_methods.customer_credit - if credit_payment_method.nil? - error_message = "Customer credit payment method is missing, please check configuration" + error_message = I18n.t(:credit_payment_method_missing, scope: translation_scope) log_error(error_message) return end @@ -50,9 +82,34 @@ module Orders @available_credit ||= order.customer.customer_account_transactions.last&.balance || 0.00 end + def credit_payment_method + order.distributor.payment_methods.customer_credit + end + def log_error(error) Rails.logger.error("Orders::CustomerCreditService: #{error}") Alert.raise(error) end + + def translation_scope + "orders.customer_credit_service" + end + + class Response + attr_reader :message + + def initialize(success:, message:) + @success = success + @message = message + end + + def success? + @success + end + + def failure? + !success? + end + end end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 12e4bf20ad..4271e34a22 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -5209,3 +5209,8 @@ en: not_enough_credit_available: Not enough credit available customer_account_transaction: account_creation: Account creation + orders: + customer_credit_service: + no_credit_owed: No credit owed + credit_payment_method_missing: Customer credit payment method is missing, please check configuration + refund_sucessful: Refund successful! diff --git a/spec/services/orders/customer_credit_service_spec.rb b/spec/services/orders/customer_credit_service_spec.rb index b8cc027193..87c64741a4 100644 --- a/spec/services/orders/customer_credit_service_spec.rb +++ b/spec/services/orders/customer_credit_service_spec.rb @@ -5,15 +5,15 @@ require 'spec_helper' RSpec.describe Orders::CustomerCreditService do subject { described_class.new(order) } - let(:order) { - create(:order_with_line_items, line_items_count: 1, distributor:, order_cycle:, - customer: create(:customer, enterprise: distributor)) - } let(:distributor) { create(:distributor_enterprise) } let(:order_cycle) { create(:order_cycle, distributors: [distributor]) } let(:credit_payment_method) { order.distributor.payment_methods.customer_credit } describe "#apply" do + let(:order) { + create(:order_with_line_items, line_items_count: 1, distributor:, order_cycle:, + customer: create(:customer, enterprise: distributor)) + } it "adds a customer credit payment to the order" do # Add credit create( @@ -126,4 +126,110 @@ RSpec.describe Orders::CustomerCreditService do end end end + + describe "#refund" do + let(:order) { create(:completed_order_with_fees) } + + before do + # Overpay to put the order payment state in "credit_owed" + payment = order.payments.first + payment.complete! + payment.update(amount: 48.00) + order.update_order! + end + + it "adds a customer credit payment to the order" do + expect { subject.refund }.to change { order.payments.count }.by(1) + + last_payment = order.payments.reload.order(:id).last + expect(last_payment.payment_method).to eq(credit_payment_method) + expect(last_payment.amount).to eq(-12.00) + expect(last_payment.state).to eq("completed") + + expect(order.payment_state).to eq("paid") + end + + it "adds an entry in customer account transaction" do + subject.refund + + last_transaction = order.customer.customer_account_transactions.last + expect(last_transaction.payment_method).to eq(credit_payment_method) + expect(last_transaction.amount).to eq(12.00) + end + + it "returns sucessful reponse" do + response = subject.refund + + expect(response.success?).to eq(true) + expect(response.message).to eq("Refund successful!") + end + + context "when order payment state is not 'credit_owed'" do + before do + order.update(payment_state: "paid") + end + + it "does nothing" do + expect { subject.refund }.not_to change { order.payments.count } + end + + it "returns a failed respond" do + response = subject.refund + + expect(response.failure?).to eq(true) + expect(response.message).to eq("No credit owed") + end + end + + context "when credit payment method is missing" do + before do + credit_payment_method.destroy! + end + + it "logs the error" do + expect(Alert).to receive(:raise).with( + "Customer credit payment method is missing, please check configuration" + ) + subject.refund + end + + it "doesn't create a credit payment" do + expect { subject.refund }.not_to change { order.payments.count } + end + + it "returns a failed response" do + response = subject.refund + + expect(response.failure?).to be(true) + expect(response.message).to eq( + "Customer credit payment method is missing, please check configuration" + ) + end + end + + context "when payment creation fails" do + before do + failed_response = ActiveMerchant::Billing::Response.new(false, "Void error") + allow_any_instance_of(Spree::PaymentMethod::CustomerCredit).to receive(:void) + .and_return(failed_response) + end + + it "logs the error" do + expect(Alert).to receive(:raise).with(RuntimeError) + subject.refund + end + + it "doesn't create a credit payment" do + # We use `length` to check the payments in memory + expect { subject.refund }.not_to change { order.payments.length } + end + + it "returns a failed response" do + response = subject.refund + + expect(response.failure?).to eq(true) + expect(response.message).to eq(RuntimeError.new("Void error").to_s) + end + end + end end