mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-03-11 03:40:20 +00:00
Add ability to use payment with credit
Currently it works like any other payment method you can select on checkout. It will eventually be added automatically to the order, when a customer has credit available.
This commit is contained in:
@@ -4,6 +4,8 @@ module Spree
|
||||
class Payment < ApplicationRecord
|
||||
module Processing
|
||||
def process!
|
||||
return internal_purchase! if payment_method.internal?
|
||||
|
||||
return unless validate!
|
||||
|
||||
purchase!
|
||||
@@ -20,6 +22,17 @@ module Spree
|
||||
end
|
||||
end
|
||||
|
||||
def internal_purchase!
|
||||
started_processing!
|
||||
options = { customer_id: order.customer_id, payment_id: id, order_number: order.number }
|
||||
response = payment_method.purchase(
|
||||
(amount * 100).round,
|
||||
nil,
|
||||
options
|
||||
)
|
||||
handle_response(response, :complete, :failure)
|
||||
end
|
||||
|
||||
def authorize!(return_url = nil)
|
||||
started_processing!
|
||||
gateway_action(source, :authorize, :pend, return_url:)
|
||||
@@ -248,6 +261,7 @@ module Spree
|
||||
end
|
||||
logger.error(Spree.t(:gateway_error))
|
||||
logger.error(" #{error.to_yaml}")
|
||||
# TODO why is this not captured ?
|
||||
raise Core::GatewayError, text
|
||||
end
|
||||
|
||||
|
||||
@@ -3,9 +3,82 @@
|
||||
module Spree
|
||||
class PaymentMethod
|
||||
class CustomerCredit < Spree::PaymentMethod
|
||||
def name
|
||||
try_translating(super)
|
||||
end
|
||||
|
||||
def description
|
||||
try_translating(super)
|
||||
end
|
||||
|
||||
# Main method called by Spree::Payment::Processing during checkout
|
||||
# - amount is in cents
|
||||
# - options: {
|
||||
# customer_id:, payment_id:, order_number:
|
||||
# }
|
||||
def purchase(amount, _source, options)
|
||||
calculated_amount = amount / 100.00
|
||||
|
||||
customer = Customer.find_by(id: options[:customer_id])
|
||||
return error_response("customer_not_found") if customer.nil?
|
||||
|
||||
return error_response("missing_payment") if options[:payment_id].nil?
|
||||
|
||||
payment_method = Spree::PaymentMethod.find_by(name: "credit_payment_method.name")
|
||||
return error_response("credit_payment_method_missing") if payment_method.nil?
|
||||
|
||||
available_credit = customer.customer_account_transactions.last&.balance
|
||||
|
||||
return error_response("no_credit_available") if available_credit.nil?
|
||||
|
||||
return error_response("not_enough_credit_available") if calculated_amount > available_credit
|
||||
|
||||
customer.with_lock do
|
||||
description = I18n.t(
|
||||
"order_payment_description",
|
||||
scope: "credit_payment_method",
|
||||
order_number: options[:order_number]
|
||||
)
|
||||
customer.customer_account_transactions.create(
|
||||
amount: -calculated_amount,
|
||||
currency:,
|
||||
payment_method:,
|
||||
payment_id: options[:payment_id],
|
||||
description:
|
||||
)
|
||||
end
|
||||
message = I18n.t("success", scope: "credit_payment_method")
|
||||
ActiveMerchant::Billing::Response.new(true, message)
|
||||
end
|
||||
|
||||
def method_type
|
||||
"check" # empty view
|
||||
end
|
||||
|
||||
def source_required?
|
||||
false
|
||||
end
|
||||
|
||||
def internal?
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def error_response(translation_key)
|
||||
message = I18n.t(translation_key, scope: "credit_payment_method.errors")
|
||||
ActiveMerchant::Billing::Response.new(false, message)
|
||||
end
|
||||
|
||||
def currency
|
||||
CurrentConfig.get(:currency)
|
||||
end
|
||||
|
||||
def try_translating(value)
|
||||
I18n.t!(value)
|
||||
rescue I18n::MissingTranslationData
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5192,5 +5192,16 @@ en:
|
||||
api_payment_method:
|
||||
name: API customer credit
|
||||
description: Used to credit customer via customer account transactions endpoint
|
||||
credit_payment_method:
|
||||
name: Customer credit
|
||||
description: Allow customer to pay with credit
|
||||
success: Payment with credit was sucessful
|
||||
order_payment_description: "Payment for order: %{order_number}"
|
||||
errors:
|
||||
customer_not_found: Customer not found
|
||||
missing_payment: Missing payment
|
||||
credit_payment_method_missing: Credit payment method is missing
|
||||
no_credit_available: No credit available
|
||||
not_enough_credit_available: Not enough credit available
|
||||
customer_account_transaction:
|
||||
account_creation: Account creation
|
||||
|
||||
@@ -29,4 +29,12 @@ FactoryBot.define do
|
||||
distributor { FactoryBot.create(:distributor_enterprise) }
|
||||
payment_method { FactoryBot.create(:payment_method) }
|
||||
end
|
||||
|
||||
factory :customer_credit_payment_method, class: Spree::PaymentMethod::CustomerCredit do
|
||||
name { "credit_payment_method.name" }
|
||||
description { "credit_payment_method.description" }
|
||||
environment { 'test' }
|
||||
|
||||
distributors { [Enterprise.is_distributor.first || FactoryBot.create(:distributor_enterprise)] }
|
||||
end
|
||||
end
|
||||
|
||||
140
spec/models/spree/payment_method/customer_credit_spec.rb
Normal file
140
spec/models/spree/payment_method/customer_credit_spec.rb
Normal file
@@ -0,0 +1,140 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe Spree::PaymentMethod::CustomerCredit do
|
||||
subject { build(:customer_credit_payment_method) }
|
||||
|
||||
describe "#name" do
|
||||
subject { build(:customer_credit_payment_method, name:) }
|
||||
|
||||
let(:name) { "credit_payment_method.name" }
|
||||
|
||||
it "translate the name" do
|
||||
expect(subject.name).to eq("Customer credit")
|
||||
end
|
||||
|
||||
context "when not a tranlatable string" do
|
||||
let(:name) { "customer credit payment" }
|
||||
|
||||
it "falls back to no translation" do
|
||||
expect(subject.name).to eq("customer credit payment")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#description" do
|
||||
subject { build(:customer_credit_payment_method, description:) }
|
||||
|
||||
let(:description) { "credit_payment_method.description" }
|
||||
|
||||
it "translate the name" do
|
||||
expect(subject.description).to eq("Allow customer to pay with credit")
|
||||
end
|
||||
|
||||
context "when not a tranlatable string" do
|
||||
let(:description) { "Payment method to allow customer to pay with credit" }
|
||||
|
||||
it "falls back to no translation" do
|
||||
expect(subject.description).to eq("Payment method to allow customer to pay with credit")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#purchase" do
|
||||
let(:response) { subject.purchase(amount, nil, options) }
|
||||
|
||||
let!(:payment_method) {
|
||||
create(:payment_method, name: CustomerAccountTransaction::DEFAULT_PAYMENT_METHOD_NAME)
|
||||
}
|
||||
let!(:credit_payment_method) {
|
||||
create(:customer_credit_payment_method)
|
||||
}
|
||||
let(:amount) { 1000 } # in cents
|
||||
let(:options) {
|
||||
{
|
||||
customer_id: customer.id,
|
||||
payment_id: payment.id,
|
||||
order_number: "R023075164"
|
||||
}
|
||||
}
|
||||
let(:customer) { create(:customer) }
|
||||
let!(:payment) { create(:payment, payment_method: credit_payment_method) }
|
||||
|
||||
it "returns a success response" do
|
||||
create(:customer_account_transaction, amount: 25.00, customer:)
|
||||
|
||||
expect(response).to be_a(ActiveMerchant::Billing::Response)
|
||||
expect(response.success?).to be(true)
|
||||
end
|
||||
|
||||
it "debits the payment from customer the account transaction" do
|
||||
create(:customer_account_transaction, amount: 25.00, customer:)
|
||||
|
||||
expect(response.success?).to be(true)
|
||||
|
||||
transaction = customer.customer_account_transactions.last
|
||||
expect(transaction.amount).to eq(-10.00)
|
||||
expect(transaction.payment_method).to be_a(Spree::PaymentMethod::CustomerCredit)
|
||||
expect(transaction.payment).to eq(payment)
|
||||
expect(transaction.description).to eq("Payment for order: R023075164")
|
||||
end
|
||||
|
||||
context "when not enough credit is available" do
|
||||
let!(:customer_credit) { create(:customer_account_transaction, amount: 5.00, customer:) }
|
||||
|
||||
it "returns an error" do
|
||||
expect(response.success?).to be(false)
|
||||
expect(response.message).to eq("Not enough credit available")
|
||||
end
|
||||
|
||||
it "doesn't debit the customer account transaction" do
|
||||
expect(CustomerAccountTransaction.where(customer: customer).last).to eq(customer_credit)
|
||||
end
|
||||
end
|
||||
|
||||
context "when no credit available" do
|
||||
it "returns an error" do
|
||||
expect(response.success?).to be(false)
|
||||
expect(response.message).to eq("No credit available")
|
||||
end
|
||||
end
|
||||
|
||||
context "when customer doesn't exist" do
|
||||
let(:customer) { nil }
|
||||
let(:options) {
|
||||
{
|
||||
customer_id: -1,
|
||||
payment_id: payment.id
|
||||
}
|
||||
}
|
||||
|
||||
it "returns an error" do
|
||||
expect(response.success?).to be(false)
|
||||
expect(response.message).to eq("Customer not found")
|
||||
end
|
||||
end
|
||||
|
||||
context "when payment is missing" do
|
||||
let(:options) {
|
||||
{
|
||||
customer_id: customer.id,
|
||||
}
|
||||
}
|
||||
|
||||
it "returns an error" do
|
||||
expect(response.success?).to be(false)
|
||||
expect(response.message).to eq("Missing payment")
|
||||
end
|
||||
end
|
||||
|
||||
context "when credit payment method is not configured" do
|
||||
let!(:credit_payment_method) { nil }
|
||||
|
||||
it "returns an error" do
|
||||
expect(response.success?).to be(false)
|
||||
expect(response.message).to eq("Credit payment method is missing")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -107,7 +107,7 @@ RSpec.describe Spree::Payment do
|
||||
allow(payment).to receive(:create_payment_profile).and_return(true)
|
||||
end
|
||||
|
||||
context "#process!" do
|
||||
describe "#process!" do
|
||||
it "should call purchase!" do
|
||||
payment = build_stubbed(:payment, payment_method:)
|
||||
expect(payment).to receive(:purchase!)
|
||||
@@ -141,6 +141,18 @@ RSpec.describe Spree::Payment do
|
||||
payment.process!
|
||||
end
|
||||
end
|
||||
|
||||
context "with an internal payment method" do
|
||||
let(:payment_method) {
|
||||
create(:customer_credit_payment_method, distributors: [create(:distributor_enterprise)])
|
||||
}
|
||||
|
||||
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
|
||||
@@ -236,7 +248,7 @@ RSpec.describe Spree::Payment do
|
||||
end
|
||||
end
|
||||
|
||||
context "purchase" do
|
||||
describe "#purchase!" do
|
||||
before do
|
||||
allow(payment_method).to receive(:purchase).and_return(success_response)
|
||||
end
|
||||
@@ -288,6 +300,65 @@ RSpec.describe Spree::Payment do
|
||||
end
|
||||
end
|
||||
|
||||
describe "internal_purchase!" do
|
||||
let(:order) { create(:order, customer:) }
|
||||
let(:customer) { create(:customer) }
|
||||
let(:payment_method) {
|
||||
create(:customer_credit_payment_method, distributors: [create(:distributor_enterprise)])
|
||||
}
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user