From de873ae42c72c8b92e3af13b4a378bd1447d6665 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 5 Feb 2026 13:58:45 +1100 Subject: [PATCH] Add void action to Taler for refunds --- app/mailers/payment_mailer.rb | 13 ++++++ app/models/spree/payment_method/taler.rb | 27 +++++++++++ .../payment_mailer/refund_available.html.haml | 2 + config/locales/en.yml | 3 ++ spec/mailers/payment_mailer_spec.rb | 12 +++++ .../models/spree/payment_method/taler_spec.rb | 46 +++++++++++++++++++ 6 files changed, 103 insertions(+) create mode 100644 app/views/payment_mailer/refund_available.html.haml diff --git a/app/mailers/payment_mailer.rb b/app/mailers/payment_mailer.rb index f9819d252a..b73756d9ec 100644 --- a/app/mailers/payment_mailer.rb +++ b/app/mailers/payment_mailer.rb @@ -26,4 +26,17 @@ class PaymentMailer < ApplicationMailer reply_to: @order.email) end end + + def refund_available(payment, taler_order_status_url) + @order = payment.order + @taler_order_status_url = taler_order_status_url + + subject = I18n.t("spree.payment_mailer.refund_available.subject", + order: @order) + I18n.with_locale valid_locale(@order.user) do + mail(to: @order.email, + subject:, + reply_to: @order.email) + end + end end diff --git a/app/models/spree/payment_method/taler.rb b/app/models/spree/payment_method/taler.rb index 747015f048..479f06811e 100644 --- a/app/models/spree/payment_method/taler.rb +++ b/app/models/spree/payment_method/taler.rb @@ -21,6 +21,14 @@ module Spree preference :backend_url, :string preference :api_key, :password + def actions + %w{void} + end + + def can_void?(payment) + payment.state == "completed" + end + # Name of the view to display during checkout def method_type "check" # empty view @@ -60,6 +68,25 @@ module Spree ActiveMerchant::Billing::Response.new(success, message) end + def void(response_code, gateway_options) + payment = gateway_options[:payment] + taler_order = taler_order(id: response_code) + status = taler_order.fetch("order_status") + + if status == "claimed" + return ActiveMerchant::Billing::Response.new(true, "Already expired") + end + + raise "Unsupported action" if status != "paid" + + amount = taler_order.fetch("contract_terms")["amount"] + taler_order.refund(refund: amount, reason: "void") + + PaymentMailer.refund_available(payment, taler_order.status_url).deliver_later + + ActiveMerchant::Billing::Response.new(true, "Refund initiated") + end + private def load_payment(order) diff --git a/app/views/payment_mailer/refund_available.html.haml b/app/views/payment_mailer/refund_available.html.haml new file mode 100644 index 0000000000..a980981f63 --- /dev/null +++ b/app/views/payment_mailer/refund_available.html.haml @@ -0,0 +1,2 @@ +%p= t("spree.payment_mailer.refund_available.message", order_number: @order.number) +%p= link_to @taler_order_status_url, @taler_order_status_url diff --git a/config/locales/en.yml b/config/locales/en.yml index e34ed5c0d2..5a0659d29c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -4930,6 +4930,9 @@ en: authorization_required: subject: "A payment requires authorization from the customer" message: "A payment for order %{order_number} requires additional authorization from the customer. The customer has been notified via email and the payment will appear as pending until it is authorized." + refund_available: + subject: "Refund available" + message: "Your payment for order %{order_number} is being refunded. Claim your refund following the link below." shipment_mailer: shipped_email: dear_customer: "Dear Customer," diff --git a/spec/mailers/payment_mailer_spec.rb b/spec/mailers/payment_mailer_spec.rb index 8aed58f59a..ae8ea86772 100644 --- a/spec/mailers/payment_mailer_spec.rb +++ b/spec/mailers/payment_mailer_spec.rb @@ -40,4 +40,16 @@ RSpec.describe PaymentMailer do end end end + + describe "#refund_available" do + it "tells the user to accept a refund" do + payment = create(:payment) + link = "https://taler.example.com/order/1" + mail = PaymentMailer.refund_available(payment, link) + + expect(mail.subject).to eq "Refund available" + expect(mail.body).to match "Claim your refund following the link below." + expect(mail.body).to match link + end + end end diff --git a/spec/models/spree/payment_method/taler_spec.rb b/spec/models/spree/payment_method/taler_spec.rb index 176c8aecc0..fc52541693 100644 --- a/spec/models/spree/payment_method/taler_spec.rb +++ b/spec/models/spree/payment_method/taler_spec.rb @@ -49,4 +49,50 @@ RSpec.describe Spree::PaymentMethod::Taler do expect(response.message).to eq "The payment request expired. Please try again." end end + + describe "#void" 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", + contract_terms: { + amount: "KUDOS:2", + } + } + 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.void("taler-order-8", { payment: order.payments[0] }) + expect(response.success?).to eq true + }.to enqueue_mail(PaymentMailer, :refund_available) + end + + it "returns early if payment already void" 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 { + response = taler.void("taler-order-8", { payment: order.payments[0] }) + expect(response.success?).to eq true + }.not_to enqueue_mail + end + end end