mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-03-26 05:55:15 +00:00
Merge pull request #13962 from mkllnk/taler-credit
Credit customers via Taler
This commit is contained in:
@@ -52,7 +52,7 @@ module Spree
|
||||
# (we can't use respond_override because Spree no longer uses respond_with)
|
||||
def fire
|
||||
event = params[:e]
|
||||
return unless event && @payment.payment_source
|
||||
return unless event
|
||||
|
||||
# capture_and_complete_order will complete the order, so we want to try to redeem VINE
|
||||
# voucher first and exit if it fails
|
||||
|
||||
@@ -22,10 +22,10 @@ class PaymentMailer < ApplicationMailer
|
||||
end
|
||||
end
|
||||
|
||||
def refund_available(payment, taler_order_status_url)
|
||||
def refund_available(amount, payment, taler_order_status_url)
|
||||
@order = payment.order
|
||||
@shop = @order.distributor.name
|
||||
@amount = payment.display_amount
|
||||
@amount = amount
|
||||
@taler_order_status_url = taler_order_status_url
|
||||
|
||||
I18n.with_locale valid_locale(@order.user) do
|
||||
|
||||
@@ -63,35 +63,6 @@ module Spree
|
||||
"XXXX-XXXX-XXXX-#{last_digits}"
|
||||
end
|
||||
|
||||
def actions
|
||||
%w{capture_and_complete_order void credit resend_authorization_email}
|
||||
end
|
||||
|
||||
def can_resend_authorization_email?(payment)
|
||||
payment.requires_authorization?
|
||||
end
|
||||
|
||||
# Indicates whether its possible to capture the payment
|
||||
def can_capture_and_complete_order?(payment)
|
||||
return false if payment.requires_authorization?
|
||||
|
||||
payment.pending? || payment.checkout?
|
||||
end
|
||||
|
||||
# Indicates whether its possible to void the payment.
|
||||
def can_void?(payment)
|
||||
!payment.void?
|
||||
end
|
||||
|
||||
# Indicates whether its possible to credit the payment. Note that most gateways require that the
|
||||
# payment be settled first which generally happens within 12-24 hours of the transaction.
|
||||
def can_credit?(payment)
|
||||
return false unless payment.completed?
|
||||
return false unless payment.order.payment_state == 'credit_owed'
|
||||
|
||||
payment.credit_allowed.positive?
|
||||
end
|
||||
|
||||
# Allows us to use a gateway_payment_profile_id to store Stripe Tokens
|
||||
def has_payment_profile?
|
||||
gateway_customer_profile_id.present? || gateway_payment_profile_id.present?
|
||||
|
||||
@@ -13,6 +13,35 @@ module Spree
|
||||
preference :server, :string, default: 'live'
|
||||
preference :test_mode, :boolean, default: false
|
||||
|
||||
def actions
|
||||
%w{capture_and_complete_order void credit resend_authorization_email}
|
||||
end
|
||||
|
||||
# Indicates whether its possible to capture the payment
|
||||
def can_capture_and_complete_order?(payment)
|
||||
return false if payment.requires_authorization?
|
||||
|
||||
payment.pending? || payment.checkout?
|
||||
end
|
||||
|
||||
# Indicates whether its possible to void the payment.
|
||||
def can_void?(payment)
|
||||
!payment.void?
|
||||
end
|
||||
|
||||
# Indicates whether its possible to credit the payment. Note that most gateways require that the
|
||||
# payment be settled first which generally happens within 12-24 hours of the transaction.
|
||||
def can_credit?(payment)
|
||||
return false unless payment.completed?
|
||||
return false unless payment.order.payment_state == 'credit_owed'
|
||||
|
||||
payment.credit_allowed.positive?
|
||||
end
|
||||
|
||||
def can_resend_authorization_email?(payment)
|
||||
payment.requires_authorization?
|
||||
end
|
||||
|
||||
def payment_source_class
|
||||
CreditCard
|
||||
end
|
||||
|
||||
@@ -152,11 +152,10 @@ module Spree
|
||||
end
|
||||
|
||||
def actions
|
||||
return [] unless payment_source.respond_to?(:actions)
|
||||
return [] unless payment_method.respond_to?(:actions)
|
||||
|
||||
payment_source.actions.select do |action|
|
||||
!payment_source.respond_to?("can_#{action}?") ||
|
||||
payment_source.__send__("can_#{action}?", self)
|
||||
payment_method.actions.select do |action|
|
||||
payment_method.__send__("can_#{action}?", self)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -166,11 +165,6 @@ module Spree
|
||||
PaymentMailer.authorize_payment(self).deliver_later
|
||||
end
|
||||
|
||||
def payment_source
|
||||
res = source.is_a?(Payment) ? source.source : source
|
||||
res || payment_method
|
||||
end
|
||||
|
||||
def ensure_correct_adjustment
|
||||
revoke_adjustment_eligibility if ['failed', 'invalid', 'void'].include?(state)
|
||||
return if adjustment.try(:finalized?)
|
||||
|
||||
@@ -22,11 +22,20 @@ module Spree
|
||||
preference :api_key, :password
|
||||
|
||||
def actions
|
||||
%w{void}
|
||||
%w[credit void]
|
||||
end
|
||||
|
||||
def can_void?(payment)
|
||||
payment.state == "completed"
|
||||
# The source can be another payment. Then this is an offset payment
|
||||
# like a credit record. We can't void a refund.
|
||||
payment.source == self && payment.state == "completed"
|
||||
end
|
||||
|
||||
def can_credit?(payment)
|
||||
return false unless payment.completed?
|
||||
return false unless payment.order.payment_state == 'credit_owed'
|
||||
|
||||
payment.credit_allowed.positive?
|
||||
end
|
||||
|
||||
# Name of the view to display during checkout
|
||||
@@ -68,6 +77,23 @@ module Spree
|
||||
ActiveMerchant::Billing::Response.new(success, message)
|
||||
end
|
||||
|
||||
def credit(money, response_code, gateway_options)
|
||||
amount = money / 100 # called with cents
|
||||
payment = gateway_options[:payment]
|
||||
taler_order = taler_order(id: response_code)
|
||||
status = taler_order.fetch("order_status")
|
||||
|
||||
raise "Unsupported action" if status != "paid"
|
||||
|
||||
taler_amount = "KUDOS:#{amount}"
|
||||
taler_order.refund(refund: taler_amount, reason: "credit")
|
||||
|
||||
spree_money = Spree::Money.new(amount, currency: payment.currency).to_s
|
||||
PaymentMailer.refund_available(spree_money, payment, taler_order.status_url).deliver_later
|
||||
|
||||
ActiveMerchant::Billing::Response.new(true, "Refund initiated")
|
||||
end
|
||||
|
||||
def void(response_code, gateway_options)
|
||||
payment = gateway_options[:payment]
|
||||
taler_order = taler_order(id: response_code)
|
||||
@@ -82,7 +108,8 @@ module Spree
|
||||
amount = taler_order.fetch("contract_terms")["amount"]
|
||||
taler_order.refund(refund: amount, reason: "void")
|
||||
|
||||
PaymentMailer.refund_available(payment, taler_order.status_url).deliver_later
|
||||
spree_money = payment.money.to_s
|
||||
PaymentMailer.refund_available(spree_money, payment, taler_order.status_url).deliver_later
|
||||
|
||||
ActiveMerchant::Billing::Response.new(true, "Refund initiated")
|
||||
end
|
||||
|
||||
@@ -14,7 +14,6 @@ module Checkout
|
||||
apply_strong_parameters
|
||||
set_pickup_address
|
||||
set_address_details
|
||||
set_payment_amount
|
||||
set_existing_card
|
||||
|
||||
@order_params
|
||||
@@ -58,12 +57,6 @@ module Checkout
|
||||
end
|
||||
end
|
||||
|
||||
def set_payment_amount
|
||||
return unless @order_params[:payments_attributes]
|
||||
|
||||
@order_params[:payments_attributes].first[:amount] = order.outstanding_balance.amount
|
||||
end
|
||||
|
||||
def set_existing_card
|
||||
return unless existing_card_selected?
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ RSpec.describe PaymentMailer do
|
||||
payment = build(:payment)
|
||||
payment.order.distributor = build(:enterprise, name: "Carrot Castle")
|
||||
link = "https://taler.example.com/order/1"
|
||||
mail = PaymentMailer.refund_available(payment, link)
|
||||
mail = PaymentMailer.refund_available(payment.money.to_s, payment, link)
|
||||
|
||||
expect(mail.subject).to eq "Refund from Carrot Castle"
|
||||
expect(mail.body).to include "Your payment of $45.75 to Carrot Castle is being refunded."
|
||||
|
||||
@@ -21,53 +21,6 @@ RSpec.describe Spree::CreditCard do
|
||||
|
||||
let(:credit_card) { described_class.new }
|
||||
|
||||
context "#can_capture?" do
|
||||
it "should be true if payment is pending" do
|
||||
payment = build_stubbed(:payment, created_at: Time.zone.now)
|
||||
allow(payment).to receive(:pending?) { true }
|
||||
expect(credit_card.can_capture_and_complete_order?(payment)).to be_truthy
|
||||
end
|
||||
|
||||
it "should be true if payment is checkout" do
|
||||
payment = build_stubbed(:payment, created_at: Time.zone.now)
|
||||
allow(payment).to receive_messages pending?: false,
|
||||
checkout?: true
|
||||
expect(credit_card.can_capture_and_complete_order?(payment)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context "#can_void?" do
|
||||
it "should be true if payment is not void" do
|
||||
payment = build_stubbed(:payment)
|
||||
allow(payment).to receive(:void?) { false }
|
||||
expect(credit_card.can_void?(payment)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context "#can_credit?" do
|
||||
it "should be false if payment is not completed" do
|
||||
payment = build_stubbed(:payment)
|
||||
allow(payment).to receive(:completed?) { false }
|
||||
expect(credit_card.can_credit?(payment)).to be_falsy
|
||||
end
|
||||
|
||||
it "should be false when order payment_state is not 'credit_owed'" do
|
||||
payment = build_stubbed(:payment,
|
||||
order: create(:order, payment_state: 'paid'))
|
||||
allow(payment).to receive(:completed?) { true }
|
||||
expect(credit_card.can_credit?(payment)).to be_falsy
|
||||
end
|
||||
|
||||
it "should be false when credit_allowed is zero" do
|
||||
payment = build_stubbed(:payment,
|
||||
order: create(:order, payment_state: 'credit_owed'))
|
||||
allow(payment).to receive_messages completed?: true,
|
||||
credit_allowed: 0
|
||||
|
||||
expect(credit_card.can_credit?(payment)).to be_falsy
|
||||
end
|
||||
end
|
||||
|
||||
context "#valid?" do
|
||||
it "should validate presence of number" do
|
||||
credit_card.attributes = valid_credit_card_attributes.except(:number)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Spree::Gateway do
|
||||
subject(:gateway) { test_gateway.new }
|
||||
let(:test_gateway) do
|
||||
Class.new(Spree::Gateway) do
|
||||
def provider_class
|
||||
@@ -15,13 +16,58 @@ RSpec.describe Spree::Gateway do
|
||||
|
||||
it "passes through all arguments on a method_missing call" do
|
||||
expect(Rails.env).to receive(:local?).and_return(false)
|
||||
gateway = test_gateway.new
|
||||
expect(gateway.provider).to receive(:imaginary_method).with('foo')
|
||||
gateway.imaginary_method('foo')
|
||||
end
|
||||
|
||||
it "raises an error in test env" do
|
||||
gateway = test_gateway.new
|
||||
expect { gateway.imaginary_method('foo') }.to raise_error StandardError
|
||||
end
|
||||
|
||||
describe "#can_capture?" do
|
||||
it "should be true if payment is pending" do
|
||||
payment = build_stubbed(:payment, created_at: Time.zone.now)
|
||||
allow(payment).to receive(:pending?) { true }
|
||||
expect(gateway.can_capture_and_complete_order?(payment)).to be_truthy
|
||||
end
|
||||
|
||||
it "should be true if payment is checkout" do
|
||||
payment = build_stubbed(:payment, created_at: Time.zone.now)
|
||||
allow(payment).to receive_messages pending?: false,
|
||||
checkout?: true
|
||||
expect(gateway.can_capture_and_complete_order?(payment)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe "#can_void?" do
|
||||
it "should be true if payment is not void" do
|
||||
payment = build_stubbed(:payment)
|
||||
allow(payment).to receive(:void?) { false }
|
||||
expect(gateway.can_void?(payment)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe "#can_credit?" do
|
||||
it "should be false if payment is not completed" do
|
||||
payment = build_stubbed(:payment)
|
||||
allow(payment).to receive(:completed?) { false }
|
||||
expect(gateway.can_credit?(payment)).to be_falsy
|
||||
end
|
||||
|
||||
it "should be false when order payment_state is not 'credit_owed'" do
|
||||
payment = build_stubbed(:payment,
|
||||
order: create(:order, payment_state: 'paid'))
|
||||
allow(payment).to receive(:completed?) { true }
|
||||
expect(gateway.can_credit?(payment)).to be_falsy
|
||||
end
|
||||
|
||||
it "should be false when credit_allowed is zero" do
|
||||
payment = build_stubbed(:payment,
|
||||
order: create(:order, payment_state: 'credit_owed'))
|
||||
allow(payment).to receive_messages completed?: true,
|
||||
credit_allowed: 0
|
||||
|
||||
expect(gateway.can_credit?(payment)).to be_falsy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -56,6 +56,46 @@ RSpec.describe Spree::PaymentMethod::Taler do
|
||||
end
|
||||
end
|
||||
|
||||
describe "#credit" do
|
||||
let(:order_endpoint) { "#{backend_url}/private/orders/taler-order-8" }
|
||||
let(:refund_endpoint) { "#{order_endpoint}/refund" }
|
||||
let(:taler_refund_uri) {
|
||||
"taler://refund/backend.demo.taler.net/instances/sandbox/taler-order-8/"
|
||||
}
|
||||
|
||||
it "starts the refund process" do
|
||||
order_status = { order_status: "paid" }
|
||||
stub_request(:get, order_endpoint).to_return(body: order_status.to_json)
|
||||
stub_request(:post, refund_endpoint).to_return(body: { taler_refund_uri: }.to_json)
|
||||
|
||||
order = create(:completed_order_with_totals)
|
||||
order.payments.create(
|
||||
amount: order.total, state: :completed,
|
||||
payment_method: taler,
|
||||
response_code: "taler-order-8",
|
||||
)
|
||||
expect {
|
||||
response = taler.credit(100, "taler-order-8", { payment: order.payments[0] })
|
||||
expect(response.success?).to eq true
|
||||
}.to enqueue_mail(PaymentMailer, :refund_available)
|
||||
end
|
||||
|
||||
it "raises an error if payment hasn't been taken yet" do
|
||||
order_status = { order_status: "claimed" }
|
||||
stub_request(:get, order_endpoint).to_return(body: order_status.to_json)
|
||||
|
||||
order = create(:completed_order_with_totals)
|
||||
order.payments.create(
|
||||
amount: order.total, state: :completed,
|
||||
payment_method: taler,
|
||||
response_code: "taler-order-8",
|
||||
)
|
||||
expect {
|
||||
taler.credit(100, "taler-order-8", { payment: order.payments[0] })
|
||||
}.to raise_error StandardError, "Unsupported action"
|
||||
end
|
||||
end
|
||||
|
||||
describe "#void" do
|
||||
let(:order_endpoint) { "#{backend_url}/private/orders/taler-order-8" }
|
||||
let(:refund_endpoint) { "#{order_endpoint}/refund" }
|
||||
|
||||
@@ -855,7 +855,8 @@ RSpec.describe Spree::Payment do
|
||||
|
||||
describe "available actions" do
|
||||
context "for most gateways" do
|
||||
let(:payment) { build_stubbed(:payment, source: build_stubbed(:credit_card)) }
|
||||
let(:payment) { build_stubbed(:payment, payment_method:) }
|
||||
let(:payment_method) { Spree::Gateway::StripeSCA.new }
|
||||
|
||||
it "can capture and void" do
|
||||
expect(payment.actions).to match_array %w(capture_and_complete_order void)
|
||||
|
||||
@@ -157,8 +157,6 @@ RSpec.describe Spree::Admin::PaymentsController do
|
||||
|
||||
context "with no payment source" do
|
||||
it "redirect to payments page" do
|
||||
allow(payment).to receive(:payment_source).and_return(nil)
|
||||
|
||||
put(
|
||||
"/admin/orders/#{order.number}/payments/#{order.payments.first.id}/fire?e=void",
|
||||
params: {},
|
||||
|
||||
@@ -25,7 +25,7 @@ RSpec.describe "Admin -> Order -> Payments" do
|
||||
login_as distributor.owner
|
||||
end
|
||||
|
||||
it "allows to refund a Taler payment" do
|
||||
it "allows to void a Taler payment" do
|
||||
order_status = {
|
||||
order_status: "paid",
|
||||
contract_terms: {
|
||||
@@ -51,4 +51,34 @@ RSpec.describe "Admin -> Order -> Payments" do
|
||||
expect(page).not_to have_link "Void"
|
||||
end
|
||||
end
|
||||
|
||||
it "allows to credit a Taler payment" do
|
||||
order_status = {
|
||||
order_status: "paid",
|
||||
contract_terms: {
|
||||
amount: "KUDOS:2",
|
||||
}
|
||||
}
|
||||
order_endpoint = "https://taler.example.com/private/orders/taler-id-1"
|
||||
refund_endpoint = "https://taler.example.com/private/orders/taler-id-1/refund"
|
||||
stub_request(:get, order_endpoint).to_return(body: order_status.to_json)
|
||||
stub_request(:post, refund_endpoint).to_return(body: "{}")
|
||||
|
||||
visit spree.admin_order_payments_path(order.number)
|
||||
|
||||
within row_containing("Taler") do
|
||||
expect(page).to have_text "COMPLETED"
|
||||
expect(page).to have_link "Credit"
|
||||
|
||||
click_link class: "icon-credit"
|
||||
|
||||
expect(page).to have_text "COMPLETED"
|
||||
expect(page).not_to have_link "Credit"
|
||||
end
|
||||
|
||||
# Our payment system creates a new payment to show the credit.
|
||||
within row_containing("$-9.75") do
|
||||
expect(page).not_to have_link "Void"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user