Add Orders::CustomerCreditService.refund

It will be used to credit the customer any fund from an order in
credit_owed state
This commit is contained in:
Gaetan Craig-Riou
2026-02-23 13:48:45 +11:00
parent d5bd8fa086
commit ee13a3abaf
3 changed files with 175 additions and 7 deletions

View File

@@ -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

View File

@@ -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!

View File

@@ -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