Automatically use credit at checkout when available

This only cover the ideal scenario, error handling will be added later
This commit is contained in:
Gaetan Craig-Riou
2026-02-16 14:05:03 +11:00
parent 5a376c9106
commit f5a3093e41
10 changed files with 332 additions and 18 deletions

View File

@@ -31,6 +31,11 @@ class CheckoutController < BaseController
check_step
end
if payment_step? || summary_step?
credit_payment_method = @order.distributor.payment_methods.customer_credit
@paid_with_credit = @order.payments.find_by(payment_method: credit_payment_method)&.amount
end
return if available_shipping_methods.any?
flash[:error] = I18n.t('checkout.errors.no_shipping_methods_available')
@@ -113,7 +118,7 @@ class CheckoutController < BaseController
@selected_payment_method ||= Checkout::PaymentMethodFetcher.new(@order).call
end
def update_order
def update_order # rubocop:disable Metrics/CyclomaticComplexity
return if params[:confirm_order] || @order.errors.any?
# Checking if shipping method updated before @order get updated. We can't use this guard
@@ -121,6 +126,9 @@ class CheckoutController < BaseController
shipping_method_updated = @order.shipping_method&.id != params[:shipping_method_id].to_i
@order.select_shipping_method(params[:shipping_method_id])
add_payment_with_credit if credit_available? && details_step?
@order.update(order_params)
# We need to update voucher to take into account:
# * when moving away from "details" step : potential change in shipping method fees
@@ -139,6 +147,29 @@ class CheckoutController < BaseController
VoucherAdjustmentsService.new(@order).update
end
def credit_available?
return false if @order.customer.nil?
available_credit > 0
end
def add_payment_with_credit
credit_payment_method = @order.distributor.payment_methods.customer_credit
return if @order.payments.where(payment_method: credit_payment_method).exists?
amount = [available_credit, @order.total].min
ActiveRecord::Base.transaction do
payment = @order.payments.create!(payment_method: credit_payment_method, amount:)
payment.internal_purchase!
end
end
def available_credit
@available_credit ||= @order.customer.customer_account_transactions.last&.balance || 0.00
end
def validate_current_step
Checkout::Validation.new(@order, params).call && @order.errors.empty?
end

View File

@@ -28,6 +28,8 @@ module Checkout
def validate_payment
return true if params.dig(:order, :payments_attributes, 0, :payment_method_id).present?
return true if order.zero_priced_order?
# No payment required, it's usually due to the order being paid by customer credit
return true if order.outstanding_balance.zero?
order.errors.add :payment_method, I18n.t('checkout.errors.select_a_payment_method')
end

View File

@@ -8,10 +8,17 @@
.checkout-title
= t("checkout.step2.payment_method.title")
- if @order.zero_priced_order?
- if @order.zero_priced_order? || @order.outstanding_balance.zero?
%h3= t(:no_payment_required)
= hidden_field_tag "order[payments_attributes][][amount]", 0
- if @order.zero_priced_order?
= hidden_field_tag "order[payments_attributes][][amount]", 0
- if @paid_with_credit
= t(:credit_used, amount: Spree::Money.new(@paid_with_credit))
- else
- if @paid_with_credit
= t(:credit_used, amount: Spree::Money.new(@paid_with_credit))
- selected_payment_method = @order.payments&.with_state(:checkout)&.first&.payment_method_id
- selected_payment_method ||= available_payment_methods[0].id if available_payment_methods.length == 1
- available_payment_methods.each do |payment_method|

View File

@@ -56,7 +56,8 @@
.summary-right{ "data-controller": "sticky", "data-sticky-target": "container" }
.summary-right-line.total
.summary-right-line-label= t :order_total_price
.summary-right-line-value#order_total= @order.display_total.to_html
.summary-right-line-value#order_total= @order.display_outstanding_balance.to_html
.summary-right-line
.summary-right-line-label= t :order_produce
@@ -78,6 +79,11 @@
.summary-right-line-label= t :order_includes_tax
.summary-right-line-value#tax-row= display_checkout_tax_total(@order)
- if @paid_with_credit.present?
.summary-right-line
.summary-right-line-label= t :customer_credit
.summary-right-line-value#customer-credit= Spree::Money.new(-1 * @paid_with_credit).to_html
.checkout-submit
- if any_terms_required?(@order.distributor)
= render partial: "terms_and_conditions", locals: { f: f }

View File

@@ -38,7 +38,7 @@
%h5.not-paid
= t :order_balance_due
%td.text-right.total.not-paid
%h5.not-paid
%h5.not-paid#balance-due
= order.display_outstanding_balance.to_html
- if order.outstanding_balance.negative?
%tr.total

View File

@@ -2568,6 +2568,8 @@ en:
order_total: Total order
order_payment: "Paying via:"
no_payment_required: "No payment required"
credit_used: "Credit used: %{amount}"
customer_credit: Credit
order_billing_address: Billing address
order_delivery_on: Delivery on
order_delivery_address: Delivery address

View File

@@ -307,6 +307,91 @@ RSpec.describe CheckoutController do
end
end
end
context "with credit availablle" do
let(:checkout_params) do
{
order: {
email: user.email,
bill_address_attributes: address.to_param,
ship_address_attributes: address.to_param
},
shipping_method_id: order.shipment.shipping_method.id.to_s
}
end
let(:credit_payment_method) {
order.distributor.payment_methods.customer_credit
}
before do
order.customer = create(:customer, enterprise: distributor)
order.save!
end
it "adds credit payment" do
# Add credit
create(
:customer_account_transaction,
amount: 100.00,
customer: order.customer,
payment_method: credit_payment_method
)
put(:update, params:)
credit_payment = order.payments.find_by(payment_method: credit_payment_method)
expect(credit_payment).to be_present
expect(credit_payment.amount).to eq(10.00) # order.total is 10.00
end
context "when credit payment already added" do
it "doesn't had more credit payment" do
create(
:customer_account_transaction,
amount: 100.00,
customer: order.customer,
payment_method: credit_payment_method
)
put(:update, params:)
credit_payment = order.payments.find_by(payment_method: credit_payment_method)
expect(credit_payment).to be_present
put(:update, params:)
credit_payments = order.payments.where(payment_method: credit_payment_method)
p credit_payments
expect(order.payments.where(payment_method: credit_payment_method).count).to eq(1)
end
end
context "when no credit available" do
it "doesn't add credit payment" do
put(:update, params:)
credit_payment = order.payments.where(payment_method: credit_payment_method)
expect(order.payments.where(payment_method: credit_payment_method)).to be_empty
end
end
context "when no enough credit available" do
it "adds credit payment using all credit" do
# Add credit
create(
:customer_account_transaction,
amount: 5.00,
customer: order.customer,
payment_method: credit_payment_method
)
put(:update, params:)
credit_payment = order.payments.find_by(payment_method: credit_payment_method)
expect(credit_payment.amount).to eq(5.00)
end
end
# TODO cover error scenarios here
end
end
end
@@ -526,6 +611,31 @@ RSpec.describe CheckoutController do
end
end
context "with an order paid with customer credit" do
let(:params) do
{ step: "payment" }
end
let(:credit_payment_method) {
order.distributor.payment_methods.customer_credit
}
before do
# Add payment with credit
payment = order.payments.create!(
amount: order.total, payment_method: credit_payment_method
)
payment.complete!
order.update_totals_and_states
end
it "allows proceeding to confirmation" do
put(:update, params:)
expect(response).to redirect_to checkout_step_path(:summary)
expect(order.reload.state).to eq "confirmation"
end
end
context "with a saved credit card" do
let!(:saved_card) { create(:stored_credit_card, user:) }
let(:checkout_params) do

View File

@@ -350,6 +350,45 @@ RSpec.describe "As a consumer, I want to checkout my order" do
expect(page).to have_field "shipping_method_#{shipping_with_fee.id}", checked: false
end
end
context "wiht customer credit" do
let(:credit_payment_method) { Spree::PaymentMethod.customer_credit }
let(:credit_amount) { 100.00 }
let(:customer) { create(:customer, user:, enterprise: distributor) }
before do
order.update(customer_id: customer.id)
order.update_totals_and_states
create(
:customer_account_transaction,
amount: credit_amount,
customer: order.customer,
payment_method: credit_payment_method
)
visit checkout_step_path(:details)
fill_out_details
fill_out_billing_address
choose free_shipping.name
proceed_to_payment
end
it "adds a customer credit payment" do
credit_payment = order.payments.find_by(payment_method: credit_payment_method)
expect(credit_payment).not_to be_nil
expect(credit_payment.amount).to eq(10.00) # order.total is 10.00
end
context "when credit doesn't cover the whole order" do
let(:credit_amount) { 5.00 }
it "adds a customer credit payment using the remaining credit" do
credit_payment = order.payments.find_by(payment_method: credit_payment_method)
expect(credit_payment).not_to be_nil
expect(credit_payment.amount).to eq(5.00) # order.total is 10.00
end
end
end
end
describe "not filling out delivery details" do

View File

@@ -22,12 +22,6 @@ RSpec.describe "As a consumer, I want to checkout my order" do
create(:simple_order_cycle, suppliers: [supplier], distributors: [distributor],
coordinator: create(:distributor_enterprise), variants: [variant])
}
let(:order) {
create(:order, order_cycle:, distributor:, bill_address_id: nil,
ship_address_id: nil, state: "cart",
line_items: [create(:line_item, variant:)])
}
let(:fee_tax_rate) { create(:tax_rate, amount: 0.10, zone:, included_in_price: true) }
let(:fee_tax_category) { create(:tax_category, tax_rates: [fee_tax_rate]) }
let(:enterprise_fee) { create(:enterprise_fee, amount: 1.23, tax_category: fee_tax_category) }
@@ -114,6 +108,40 @@ RSpec.describe "As a consumer, I want to checkout my order" do
end
end
context "with credit available" do
let(:credit_payment_method) { Spree::PaymentMethod.customer_credit }
let(:payment_amount) { 10.00 }
before do
create(
:customer_account_transaction,
amount: 100, customer: order.customer,
payment_method: credit_payment_method
)
# Add credit payment
payment = order.payments.create!(payment_method: credit_payment_method,
amount: payment_amount)
payment.internal_purchase!
visit checkout_step_path(:payment)
end
it "displays no payment required" do
expect(page).to have_content "No payment required"
expect(page).to have_content "Credit used: $10.00"
end
context "when credit does not cover the whole order" do
let(:credit_amount) { 5.00 }
let(:payment_amount) { 5.00 }
it "shows credit used and available payment method" do
expect(page).to have_content "Credit used: $5.00"
expect(page).to have_content "Payment with Fee $1.23"
end
end
end
describe "vouchers" do
context "with no voucher available" do
before do

View File

@@ -24,10 +24,9 @@ RSpec.describe "As a consumer, I want to checkout my order" do
coordinator: create(:distributor_enterprise), variants: [variant])
}
let(:order) {
create(:order, order_cycle:, distributor:, bill_address_id: nil,
ship_address_id: nil, state: "cart",
line_items: [create(:line_item, variant:)])
create(:order_ready_for_confirmation, distributor:)
}
let(:fee_tax_rate) { create(:tax_rate, amount: 0.10, zone:, included_in_price: true) }
let(:fee_tax_category) { create(:tax_category, tax_rates: [fee_tax_rate]) }
let(:enterprise_fee) { create(:enterprise_fee, amount: 1.23, tax_category: fee_tax_category) }
@@ -52,10 +51,6 @@ RSpec.describe "As a consumer, I want to checkout my order" do
end
context "summary step" do
let(:order) {
create(:order_ready_for_confirmation, distributor:)
}
describe "display the delivery address and not the ship address" do
let(:ship_address) { create(:address, :randomized) }
let(:bill_address) { create(:address, :randomized) }
@@ -386,6 +381,100 @@ RSpec.describe "As a consumer, I want to checkout my order" do
end
end
end
context "with customer credit" do
let(:credit_payment_method) { Spree::PaymentMethod.customer_credit }
let(:order) { create(:order_ready_for_payment, distributor:) }
let(:payment_amount) { 10.00 }
before do
create(
:customer_account_transaction,
amount: 100,
customer: order.customer,
payment_method: credit_payment_method
)
# Add credit payment
payment = order.payments.create!(payment_method: credit_payment_method,
amount: payment_amount)
payment.internal_purchase!
end
it "displays the customer credit used" do
# Move to ready for confirmation
order.next!
visit checkout_step_path(:summary)
expect(page).to have_content "Customer credit"
within ".summary-right" do
expect(page).to have_content "Credit"
expect(page).to have_selector("#customer-credit", text: with_currency(-10.00))
expect(page).to have_selector("#order_total", text: with_currency(0.00))
end
end
context "when completing order" do
it "displays the order as paid" do
# Move to ready for confirmation
order.next!
visit checkout_step_path(:summary)
place_order
# TODO it should be displaying some kind indication it was paid with credit
expect(page).to have_content "PAID"
expect(page).to have_content "Paying via: Customer credit"
expect(page).to have_selector("#amount-paid", text: with_currency(10.00))
end
end
context "when credit doesn't cover the whole order" do
let(:payment_amount) { 2.00 }
let(:payment_method) { create(:payment_method, distributors: [distributor]) }
before do
# Add another payment to cover the rest of the order
order.payments.create!(payment_method:, amount: 8.00)
end
it "displays the customer credit used" do
# Move to ready for confirmation
order.next!
visit checkout_step_path(:summary)
expect(page).to have_content payment_method.display_name
within ".summary-right" do
expect(page).to have_content "Credit"
expect(page).to have_selector("#customer-credit", text: with_currency(-2.00))
# actual order total is 10.00
expect(page).to have_selector("#order_total", text: with_currency(8.00))
end
end
context "when completing order" do
it "displays part of the order whas paid with credit" do
# Move to ready for confirmation
order.next!
visit checkout_step_path(:summary)
place_order
expect(page).to have_content "NOT PAID"
expect(page).to have_content "Paying via: #{payment_method.display_name}"
within "#line-items" do
expect(page).to have_selector("#amount-paid", text: with_currency(2.00))
# actual order total is 10.00
expect(page).to have_content("Balance Due")
expect(page).to have_selector("#balance-due", text: with_currency(8.00))
end
end
end
end
end
end
context "with previous open orders" do