mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-03-11 03:40:20 +00:00
Add ability to "void" a customer credit payment
Voiding the payment will refund the credit used to the customer.
This commit is contained in:
@@ -185,7 +185,7 @@ module Spree
|
||||
end
|
||||
|
||||
def allowed_events
|
||||
%w{capture void_transaction credit refund resend_authorization_email
|
||||
%w{capture void_transaction credit refund internal_void resend_authorization_email
|
||||
capture_and_complete_order}
|
||||
end
|
||||
|
||||
|
||||
@@ -144,6 +144,24 @@ module Spree
|
||||
end
|
||||
end
|
||||
|
||||
def internal_void!
|
||||
return true if void?
|
||||
|
||||
options = { customer_id: order.customer_id, payment_id: id, order_number: order.number }
|
||||
response = payment_method.void(
|
||||
(amount * 100).round,
|
||||
nil,
|
||||
options
|
||||
)
|
||||
record_response(response)
|
||||
|
||||
if response.success?
|
||||
void
|
||||
else
|
||||
gateway_error(response)
|
||||
end
|
||||
end
|
||||
|
||||
def partial_credit(amount)
|
||||
return if amount > credit_allowed
|
||||
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
module Spree
|
||||
class PaymentMethod
|
||||
class CustomerCredit < Spree::PaymentMethod
|
||||
def actions
|
||||
%w{internal_void}
|
||||
end
|
||||
|
||||
# We should only void complete payment, otherwise we will be refunding credit that was
|
||||
# not used in the first place.
|
||||
def can_internal_void?(payment)
|
||||
payment.state == "completed"
|
||||
end
|
||||
|
||||
# Main method called by Spree::Payment::Processing during checkout
|
||||
# - amount is in cents
|
||||
# - options: {
|
||||
@@ -39,6 +49,38 @@ module Spree
|
||||
ActiveMerchant::Billing::Response.new(true, message)
|
||||
end
|
||||
|
||||
# Main method called by Spree::Payment::Processing for void
|
||||
# - amount is in cents
|
||||
# - options: {
|
||||
# customer_id:, payment_id:, order_number:
|
||||
# }
|
||||
def void(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?
|
||||
return error_response("credit_payment_method_missing") if payment_method.nil?
|
||||
|
||||
customer.with_lock do
|
||||
description = I18n.t(
|
||||
"order_void_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("void_success", scope: "credit_payment_method")
|
||||
ActiveMerchant::Billing::Response.new(true, message)
|
||||
end
|
||||
|
||||
def method_type
|
||||
"check" # empty view
|
||||
end
|
||||
|
||||
@@ -8,7 +8,8 @@ table tbody tr {
|
||||
}
|
||||
|
||||
&.action-remove td,
|
||||
&.action-void td {
|
||||
&.action-void td,
|
||||
&.action-internal_void td {
|
||||
text-decoration: line-through;
|
||||
|
||||
&.actions {
|
||||
|
||||
@@ -156,9 +156,9 @@ $states-bg-colors: $color-ste-completed-bg, $color-ste-complete-bg,
|
||||
$states-text-colors: $color-ste-completed-text, $color-ste-complete-text, $color-ste-sold-text, $color-ste-pending-text, $color-ste-awaiting_return-text, $color-ste-returned-text, $color-ste-credit_owed-text, $color-ste-paid-text, $color-ste-shipped-text, $color-ste-balance_due-text, $color-ste-backorder-text, $color-ste-checkout-text, $color-ste-cart-text, $color-ste-address-text, $color-ste-delivery-text, $color-ste-payment-text, $color-ste-confirmation-text, $color-ste-canceled-text, $color-ste-ready-text, $color-ste-void-text, $color-ste-requires_authorization-text, $color-ste-active-text, $color-ste-inactive-text !default;
|
||||
|
||||
// Available actions
|
||||
$actions: edit, clone, remove, void, capture, save, cancel, mail !default;
|
||||
$actions-bg-colors: $color-action-edit-bg, $color-action-clone-bg, $color-action-remove-bg, $color-action-void-bg, $color-action-capture-bg, $color-action-save-bg, $color-action-cancel-bg, $color-action-mail-bg !default;
|
||||
$actions-brd-colors: $color-action-edit-brd, $color-action-clone-brd, $color-action-remove-brd, $color-action-void-brd, $color-action-capture-brd, $color-action-save-brd, $color-action-cancel-brd, $color-action-mail-brd !default;
|
||||
$actions: edit, clone, remove, void, internal_void, capture, save, cancel, mail !default;
|
||||
$actions-bg-colors: $color-action-edit-bg, $color-action-clone-bg, $color-action-remove-bg, $color-action-void-bg, $color-action-void-bg, $color-action-capture-bg, $color-action-save-bg, $color-action-cancel-bg, $color-action-mail-bg !default;
|
||||
$actions-brd-colors: $color-action-edit-brd, $color-action-clone-brd, $color-action-remove-brd, $color-action-void-brd, $color-action-void-brd, $color-action-capture-brd, $color-action-save-brd, $color-action-cancel-brd, $color-action-mail-brd !default;
|
||||
|
||||
// Sizes
|
||||
//--------------------------------------------------------------
|
||||
|
||||
@@ -30,7 +30,8 @@ button[class*="icon-"] {
|
||||
}
|
||||
|
||||
.icon-cancel:before,
|
||||
.icon-void:before {
|
||||
.icon-void:before,
|
||||
.icon-internal_void:before {
|
||||
@extend .icon-remove, :before;
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,8 @@ table {
|
||||
}
|
||||
}
|
||||
.icon-trash:hover,
|
||||
.icon-void:hover {
|
||||
.icon-void:hover,
|
||||
.icon-internal_void:hover {
|
||||
background-color: $color-error;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
@@ -5198,7 +5198,9 @@ en:
|
||||
name: Customer credit
|
||||
description: Allow customer to pay with credit
|
||||
success: Payment with credit was sucessful
|
||||
void_success: Credit void was sucessful
|
||||
order_payment_description: "Payment for order: %{order_number}"
|
||||
order_void_description: "Refund for order: %{order_number}"
|
||||
errors:
|
||||
customer_not_found: Customer not found
|
||||
missing_payment: Missing payment
|
||||
|
||||
@@ -8,9 +8,7 @@ RSpec.describe Spree::PaymentMethod::CustomerCredit do
|
||||
describe "#purchase" do
|
||||
let(:response) { subject.purchase(amount, nil, options) }
|
||||
|
||||
let!(:credit_payment_method) {
|
||||
create(:customer_credit_payment_method)
|
||||
}
|
||||
let!(:credit_payment_method) { create(:customer_credit_payment_method) }
|
||||
let(:amount) { 1000 } # in cents
|
||||
let(:options) {
|
||||
{
|
||||
@@ -27,6 +25,7 @@ RSpec.describe Spree::PaymentMethod::CustomerCredit do
|
||||
|
||||
expect(response).to be_a(ActiveMerchant::Billing::Response)
|
||||
expect(response.success?).to be(true)
|
||||
expect(response.message).to eq("Payment with credit was sucessful")
|
||||
end
|
||||
|
||||
it "debits the payment from customer the account transaction" do
|
||||
@@ -93,7 +92,84 @@ RSpec.describe Spree::PaymentMethod::CustomerCredit do
|
||||
let!(:credit_payment_method) { nil }
|
||||
|
||||
around do |example|
|
||||
# Customer is needed to create a purchase and a customer which is linked to an enterprise.
|
||||
# Customer is needed to create a purchase and a customer is linked to an enterprise.
|
||||
# That means FactoryBot will create an enterprise, so we disable the after create callback
|
||||
# so that credit payment methods are not created.
|
||||
Enterprise.skip_callback(:create, :after, :add_credit_payment_method)
|
||||
example.run
|
||||
Enterprise.set_callback(:create, :after, :add_credit_payment_method)
|
||||
end
|
||||
|
||||
it "returns an error" do
|
||||
expect(response.success?).to be(false)
|
||||
expect(response.message).to eq("Credit payment method is missing")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#void" do
|
||||
let(:response) { subject.void(amount, nil, options) }
|
||||
let(:amount) { 1500 } # 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) }
|
||||
let!(:credit_payment_method) { create(:customer_credit_payment_method) }
|
||||
|
||||
it "returns a success response" do
|
||||
expect(response).to be_a(ActiveMerchant::Billing::Response)
|
||||
expect(response.success?).to be(true)
|
||||
expect(response.message).to eq("Credit void was sucessful")
|
||||
end
|
||||
|
||||
it "credits the payment to customer the account transaction" do
|
||||
expect(response.success?).to be(true)
|
||||
|
||||
transaction = customer.customer_account_transactions.last
|
||||
expect(transaction.amount).to eq(15.00)
|
||||
expect(transaction.payment_method).to be_a(Spree::PaymentMethod::CustomerCredit)
|
||||
expect(transaction.payment).to eq(payment)
|
||||
expect(transaction.description).to eq("Refund for order: R023075164")
|
||||
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 }
|
||||
|
||||
around do |example|
|
||||
# Customer is needed to create a purchase and a customer is linked to an enterprise.
|
||||
# That means FactoryBot will create an enterprise, so we disable the after create callback
|
||||
# so that credit payment methods are not created.
|
||||
Enterprise.skip_callback(:create, :after, :add_credit_payment_method)
|
||||
|
||||
@@ -943,6 +943,83 @@ RSpec.describe Spree::Payment do
|
||||
end
|
||||
end
|
||||
|
||||
describe "internal_void!" 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(:void).and_return(success_response)
|
||||
end
|
||||
|
||||
it "calls void on the internal payment method" do
|
||||
expect(payment_method).to receive(:void).with(
|
||||
amount_in_cents, nil, options
|
||||
).and_return(success_response)
|
||||
|
||||
payment.internal_void!
|
||||
end
|
||||
|
||||
it "logs the response" do
|
||||
expect(payment).to receive(:record_response)
|
||||
|
||||
payment.internal_void!
|
||||
end
|
||||
|
||||
context "when successful" do
|
||||
before do
|
||||
allow(payment.payment_method).to receive(:void).with(
|
||||
amount_in_cents, nil, options
|
||||
).and_return(success_response)
|
||||
end
|
||||
|
||||
it "voids the payment" do
|
||||
allow(payment).to receive(:record_response)
|
||||
|
||||
expect { payment.internal_void! }.to change { payment.state }.to("void")
|
||||
end
|
||||
end
|
||||
|
||||
context "when unsuccessful" do
|
||||
before do
|
||||
allow(payment_method).to receive(:void).and_return(failed_response)
|
||||
end
|
||||
|
||||
it "does not create void payment" do
|
||||
# Instanciate payment so our count expectation works as expected
|
||||
payment
|
||||
expect { payment.internal_void! }
|
||||
.to raise_error(Spree::Core::GatewayError)
|
||||
.and change { Spree::Payment.count }.by(0)
|
||||
end
|
||||
|
||||
it "raises an error" do
|
||||
expect { payment.internal_void! }.to raise_error(Spree::Core::GatewayError)
|
||||
end
|
||||
end
|
||||
|
||||
context "when payment already voided" do
|
||||
it "does nothing" do
|
||||
payment.void!
|
||||
expect(payment_method).not_to receive(:void)
|
||||
|
||||
payment.internal_void!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "applying transaction fees" do
|
||||
let!(:order) { create(:order) }
|
||||
let!(:line_item) { create(:line_item, order:, quantity: 3, price: 5.00) }
|
||||
|
||||
Reference in New Issue
Block a user