diff --git a/app/controllers/checkout_controller.rb b/app/controllers/checkout_controller.rb index fdc98e55f4..3279fb00e7 100644 --- a/app/controllers/checkout_controller.rb +++ b/app/controllers/checkout_controller.rb @@ -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 diff --git a/app/services/checkout/validation.rb b/app/services/checkout/validation.rb index a2e66803f2..3e5406617f 100644 --- a/app/services/checkout/validation.rb +++ b/app/services/checkout/validation.rb @@ -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 diff --git a/app/views/checkout/_payment.html.haml b/app/views/checkout/_payment.html.haml index b4c52c0100..4b855cc50b 100644 --- a/app/views/checkout/_payment.html.haml +++ b/app/views/checkout/_payment.html.haml @@ -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| diff --git a/app/views/checkout/_summary.html.haml b/app/views/checkout/_summary.html.haml index 4a897f5651..1f46d5a61a 100644 --- a/app/views/checkout/_summary.html.haml +++ b/app/views/checkout/_summary.html.haml @@ -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 } diff --git a/app/views/spree/orders/_totals_footer.html.haml b/app/views/spree/orders/_totals_footer.html.haml index 223524e676..ecd29ccf31 100644 --- a/app/views/spree/orders/_totals_footer.html.haml +++ b/app/views/spree/orders/_totals_footer.html.haml @@ -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 diff --git a/config/locales/en.yml b/config/locales/en.yml index d234b163a8..74ca7579a6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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 diff --git a/spec/controllers/checkout_controller_spec.rb b/spec/controllers/checkout_controller_spec.rb index 345672015c..09227afc33 100644 --- a/spec/controllers/checkout_controller_spec.rb +++ b/spec/controllers/checkout_controller_spec.rb @@ -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 diff --git a/spec/system/consumer/checkout/details_spec.rb b/spec/system/consumer/checkout/details_spec.rb index 949abe9979..e585e6a460 100644 --- a/spec/system/consumer/checkout/details_spec.rb +++ b/spec/system/consumer/checkout/details_spec.rb @@ -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 diff --git a/spec/system/consumer/checkout/payment_spec.rb b/spec/system/consumer/checkout/payment_spec.rb index da6e7bc1a5..f4c90af3bf 100644 --- a/spec/system/consumer/checkout/payment_spec.rb +++ b/spec/system/consumer/checkout/payment_spec.rb @@ -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 diff --git a/spec/system/consumer/checkout/summary_spec.rb b/spec/system/consumer/checkout/summary_spec.rb index 2ff1108f9f..668dbbc135 100644 --- a/spec/system/consumer/checkout/summary_spec.rb +++ b/spec/system/consumer/checkout/summary_spec.rb @@ -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