mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-27 01:43:22 +00:00
Merge pull request #6713 from andrewpbrett/sca-subs-emails
Send emails when subscription payments require SCA auth
This commit is contained in:
@@ -157,7 +157,7 @@ module Spree
|
||||
|
||||
raise Spree::Core::GatewayError, I18n.t('authorization_failure') unless @payment.pending?
|
||||
|
||||
return unless @payment.cvv_response_message.present?
|
||||
return unless @payment.authorization_action_required?
|
||||
|
||||
PaymentMailer.authorize_payment(@payment).deliver_later
|
||||
raise Spree::Core::GatewayError, I18n.t('action_required')
|
||||
|
||||
@@ -26,6 +26,7 @@ module Spree
|
||||
|
||||
def show
|
||||
@order = Spree::Order.find_by!(number: params[:id])
|
||||
ProcessPaymentIntent.new(params["payment_intent"], @order).call!
|
||||
end
|
||||
|
||||
def empty
|
||||
|
||||
17
app/helpers/full_url_helper.rb
Normal file
17
app/helpers/full_url_helper.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module FullUrlHelper
|
||||
def url_helpers
|
||||
# This is how we can get the helpers with a usable root_url outside the controllers
|
||||
Rails.application.routes.default_url_options = ActionMailer::Base.default_url_options
|
||||
Rails.application.routes.url_helpers
|
||||
end
|
||||
|
||||
def full_checkout_path
|
||||
URI.join(url_helpers.root_url, url_helpers.checkout_path).to_s
|
||||
end
|
||||
|
||||
def full_order_path(order)
|
||||
URI.join(url_helpers.root_url, url_helpers.order_path(order)).to_s
|
||||
end
|
||||
end
|
||||
@@ -84,7 +84,9 @@ class SubscriptionConfirmJob < ActiveJob::Base
|
||||
def authorize_payment!(order)
|
||||
return if order.subscription.payment_method.class != Spree::Gateway::StripeSCA
|
||||
|
||||
OrderManagement::Subscriptions::StripeScaPaymentAuthorize.new(order).call!
|
||||
OrderManagement::Order::StripeScaPaymentAuthorize.new(order).
|
||||
extend(OrderManagement::Order::SendAuthorizationEmails).
|
||||
call!
|
||||
end
|
||||
|
||||
def send_confirmation_email(order)
|
||||
|
||||
@@ -7,8 +7,22 @@ class PaymentMailer < Spree::BaseMailer
|
||||
@payment = payment
|
||||
subject = I18n.t('spree.payment_mailer.authorize_payment.subject',
|
||||
distributor: @payment.order.distributor.name)
|
||||
mail(to: payment.order.user.email,
|
||||
from: from_address,
|
||||
subject: subject)
|
||||
I18n.with_locale valid_locale(@payment.order.user) do
|
||||
mail(to: payment.order.user.email,
|
||||
from: from_address,
|
||||
subject: subject)
|
||||
end
|
||||
end
|
||||
|
||||
def authorization_required(payment)
|
||||
@payment = payment
|
||||
shop_owner = @payment.order.distributor.owner
|
||||
subject = I18n.t('spree.payment_mailer.authorization_required.subject',
|
||||
order: @payment.order)
|
||||
I18n.with_locale valid_locale(shop_owner) do
|
||||
mail(to: shop_owner.email,
|
||||
from: from_address,
|
||||
subject: subject)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -83,12 +83,12 @@ module Spree
|
||||
end
|
||||
|
||||
def can_resend_authorization_email?(payment)
|
||||
payment.pending? && payment.cvv_response_message.present?
|
||||
payment.pending? && payment.authorization_action_required?
|
||||
end
|
||||
|
||||
# Indicates whether its possible to capture the payment
|
||||
def can_capture?(payment)
|
||||
return false if payment.cvv_response_message.present?
|
||||
return false if payment.authorization_action_required?
|
||||
|
||||
payment.pending? || payment.checkout?
|
||||
end
|
||||
|
||||
@@ -10,6 +10,8 @@ require 'active_merchant/billing/gateways/stripe_decorator'
|
||||
module Spree
|
||||
class Gateway
|
||||
class StripeSCA < Gateway
|
||||
include FullUrlHelper
|
||||
|
||||
preference :enterprise_id, :integer
|
||||
|
||||
validate :ensure_enterprise_selected
|
||||
@@ -145,16 +147,6 @@ module Spree
|
||||
|
||||
errors.add(:stripe_account_owner, I18n.t(:error_required))
|
||||
end
|
||||
|
||||
def full_checkout_path
|
||||
URI.join(url_helpers.root_url, url_helpers.checkout_path).to_s
|
||||
end
|
||||
|
||||
def url_helpers
|
||||
# This is how we can get the helpers with a usable root_url outside the controllers
|
||||
Rails.application.routes.default_url_options = ActionMailer::Base.default_url_options
|
||||
Rails.application.routes.url_helpers
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -115,7 +115,7 @@ module Spree
|
||||
end
|
||||
|
||||
def resend_authorization_email!
|
||||
return unless cvv_response_message.present?
|
||||
return unless authorization_action_required?
|
||||
|
||||
PaymentMailer.authorize_payment(self).deliver_later
|
||||
end
|
||||
|
||||
@@ -15,6 +15,7 @@ module Spree
|
||||
|
||||
def process_offline!
|
||||
return unless validate!
|
||||
return if authorization_action_required?
|
||||
|
||||
if payment_method.auto_capture?
|
||||
charge_offline!
|
||||
@@ -185,6 +186,10 @@ module Spree
|
||||
options
|
||||
end
|
||||
|
||||
def authorization_action_required?
|
||||
cvv_response_message.present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate!
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
# Provides the redirect path if a redirect to the payment gateway is needed
|
||||
module Checkout
|
||||
class StripeRedirect
|
||||
include FullUrlHelper
|
||||
|
||||
def initialize(params, order)
|
||||
@params = params
|
||||
@order = order
|
||||
@@ -12,10 +14,11 @@ module Checkout
|
||||
def path
|
||||
return unless stripe_payment_method?
|
||||
|
||||
payment = OrderManagement::Subscriptions::StripeScaPaymentAuthorize.new(@order).call!
|
||||
payment = OrderManagement::Order::StripeScaPaymentAuthorize.new(@order).
|
||||
call!(full_checkout_path)
|
||||
raise if @order.errors.any?
|
||||
|
||||
field_with_url(payment) if url?(field_with_url(payment))
|
||||
field_with_url(payment)
|
||||
end
|
||||
|
||||
private
|
||||
@@ -28,14 +31,8 @@ module Checkout
|
||||
payment_method.is_a?(Spree::Gateway::StripeSCA)
|
||||
end
|
||||
|
||||
def url?(string)
|
||||
return false if string.blank?
|
||||
|
||||
string.starts_with?("http")
|
||||
end
|
||||
|
||||
# Stripe::AuthorizeResponsePatcher patches the Stripe authorization response
|
||||
# so that this field stores the redirect URL
|
||||
# so that this field stores the redirect URL. It also verifies that it is a Stripe URL.
|
||||
def field_with_url(payment)
|
||||
payment.cvv_response_message
|
||||
end
|
||||
|
||||
32
app/services/process_payment_intent.rb
Normal file
32
app/services/process_payment_intent.rb
Normal file
@@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ProcessPaymentIntent
|
||||
def initialize(payment_intent, order)
|
||||
@payment_intent = payment_intent
|
||||
@order = order
|
||||
@last_payment = OrderPaymentFinder.new(order).last_payment
|
||||
end
|
||||
|
||||
def call!
|
||||
return unless valid?
|
||||
|
||||
last_payment.update_attribute(:cvv_response_message, nil)
|
||||
last_payment.complete!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :order, :payment_intent, :last_payment
|
||||
|
||||
def valid?
|
||||
order.present? && valid_intent_string? && matches_last_payment?
|
||||
end
|
||||
|
||||
def valid_intent_string?
|
||||
payment_intent&.starts_with?("pi_")
|
||||
end
|
||||
|
||||
def matches_last_payment?
|
||||
last_payment&.state == "pending" && last_payment&.response_code == payment_intent
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
= t('spree.payment_mailer.authorization_required.message', order_number: @payment.order.number)
|
||||
= link_to spree.edit_admin_order_url(@payment.order), spree.edit_admin_order_url(@payment.order)
|
||||
@@ -0,0 +1,3 @@
|
||||
= t('spree.payment_mailer.authorization_required.message', order_number: @payment.order.number)
|
||||
|
||||
= link_to spree.edit_admin_order_url(@payment.order), spree.edit_admin_order_url(@payment.order)
|
||||
@@ -1,2 +1,2 @@
|
||||
= t('.instructions', distributor: @payment.order.distributor.name, amount: @payment.display_amount)
|
||||
= main_app.authorize_payment_url(@payment)
|
||||
= t('spree.payment_mailer.authorize_payment.instructions', distributor: @payment.order.distributor.name, amount: @payment.display_amount)
|
||||
= link_to main_app.authorize_payment_url(@payment), main_app.authorize_payment_url(@payment)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
= t('.instructions', distributor: @payment.order.distributor.name, amount: @payment.display_amount)
|
||||
= t('spree.payment_mailer.authorize_payment.instructions', distributor: @payment.order.distributor.name, amount: @payment.display_amount)
|
||||
|
||||
= main_app.authorize_payment_url(@payment)
|
||||
= link_to main_app.authorize_payment_url(@payment), main_app.authorize_payment_url(@payment)
|
||||
|
||||
@@ -3658,6 +3658,9 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
authorize_payment:
|
||||
subject: "Please authorize your payment to %{distributor} on OFN"
|
||||
instructions: "Your payment of %{amount} to %{distributor} requires additional authentication. Please visit the following URL to authorize your payment:"
|
||||
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."
|
||||
shipment_mailer:
|
||||
shipped_email:
|
||||
dear_customer: "Dear Customer,"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrderManagement
|
||||
module Order
|
||||
module SendAuthorizationEmails
|
||||
def call!(redirect_url = full_order_path(@order))
|
||||
super(redirect_url)
|
||||
|
||||
return unless @payment.authorization_action_required?
|
||||
|
||||
PaymentMailer.authorize_payment(@payment).deliver_now
|
||||
PaymentMailer.authorization_required(@payment).deliver_now
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,17 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrderManagement
|
||||
module Subscriptions
|
||||
module Order
|
||||
class StripeScaPaymentAuthorize
|
||||
include FullUrlHelper
|
||||
|
||||
def initialize(order)
|
||||
@order = order
|
||||
@payment = OrderPaymentFinder.new(@order).last_pending_payment
|
||||
end
|
||||
|
||||
def call!
|
||||
def call!(redirect_url = full_order_path(@order))
|
||||
return unless @payment&.checkout?
|
||||
|
||||
@payment.authorize!
|
||||
@payment.authorize!(redirect_url)
|
||||
|
||||
@order.errors.add(:base, I18n.t('authorization_failure')) unless @payment.pending?
|
||||
|
||||
@@ -35,12 +35,14 @@ module OrderManagement
|
||||
record_issue(type, order, line2)
|
||||
end
|
||||
|
||||
# This uses `deliver_now` since it's called from inside a job
|
||||
def send_placement_summary_emails
|
||||
@summaries.values.each do |summary|
|
||||
SubscriptionMailer.placement_summary_email(summary).deliver_now
|
||||
end
|
||||
end
|
||||
|
||||
# This uses `deliver_now` since it's called from inside a job
|
||||
def send_confirmation_summary_emails
|
||||
@summaries.values.each do |summary|
|
||||
SubscriptionMailer.confirmation_summary_email(summary).deliver_now
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
require 'spec_helper'
|
||||
|
||||
module OrderManagement
|
||||
module Subscriptions
|
||||
module Order
|
||||
describe StripeScaPaymentAuthorize do
|
||||
let(:order) { create(:order) }
|
||||
let(:payment_authorize) {
|
||||
OrderManagement::Subscriptions::StripeScaPaymentAuthorize.new(order)
|
||||
OrderManagement::Order::StripeScaPaymentAuthorize.new(order)
|
||||
}
|
||||
|
||||
describe "#call!" do
|
||||
@@ -57,6 +57,37 @@ module OrderManagement
|
||||
expect(order.errors[:base].first).to eq "Authorization Failure"
|
||||
end
|
||||
end
|
||||
|
||||
context "and payment authorize requires additional authorization" do
|
||||
let(:mail_mock) { double(:mailer_mock, deliver_now: true) }
|
||||
|
||||
before do
|
||||
allow(PaymentMailer).to receive(:authorize_payment) { mail_mock }
|
||||
allow(PaymentMailer).to receive(:authorization_required) { mail_mock }
|
||||
allow(payment).to receive(:authorize!) {
|
||||
payment.state = "pending"
|
||||
payment.cvv_response_message = "https://stripe.com/redirect"
|
||||
}
|
||||
end
|
||||
|
||||
it "sends an email requesting authorization and an email notifying the shop owner when requested" do
|
||||
payment_authorize.extend(OrderManagement::Order::SendAuthorizationEmails).call!
|
||||
|
||||
expect(order.errors.size).to eq 0
|
||||
expect(PaymentMailer).to have_received(:authorize_payment)
|
||||
expect(PaymentMailer).to have_received(:authorization_required)
|
||||
expect(mail_mock).to have_received(:deliver_now).twice
|
||||
end
|
||||
|
||||
it "doesn't send emails by default" do
|
||||
payment_authorize.call!
|
||||
|
||||
expect(order.errors.size).to eq 0
|
||||
expect(PaymentMailer).to_not have_received(:authorize_payment)
|
||||
expect(PaymentMailer).to_not have_received(:authorization_required)
|
||||
expect(mail_mock).to_not have_received(:deliver_now)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -130,6 +130,7 @@ describe SubscriptionConfirmJob do
|
||||
before do
|
||||
OrderWorkflow.new(order).complete!
|
||||
allow(job).to receive(:send_confirmation_email).and_call_original
|
||||
allow(job).to receive(:send_payment_authorization_emails).and_call_original
|
||||
setup_email
|
||||
expect(job).to receive(:record_order)
|
||||
end
|
||||
@@ -213,6 +214,7 @@ describe SubscriptionConfirmJob do
|
||||
it "sends a failed payment email" do
|
||||
expect(job).to receive(:send_failed_payment_email)
|
||||
expect(job).to_not receive(:send_confirmation_email)
|
||||
expect(job).to_not receive(:send_payment_authorization_emails)
|
||||
job.send(:confirm_order!, order)
|
||||
end
|
||||
end
|
||||
@@ -228,10 +230,8 @@ describe SubscriptionConfirmJob do
|
||||
end
|
||||
|
||||
it "sends only a subscription confirm email, no regular confirmation emails" do
|
||||
ActionMailer::Base.deliveries.clear
|
||||
expect{ job.send(:confirm_order!, order) }.to_not enqueue_job ConfirmOrderJob
|
||||
expect(job).to have_received(:send_confirmation_email).once
|
||||
expect(ActionMailer::Base.deliveries.count).to be 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -43,6 +43,7 @@ describe Checkout::StripeRedirect do
|
||||
stripe_payment.state = 'pending'
|
||||
true
|
||||
end
|
||||
allow(stripe_payment.order).to receive(:distributor) { distributor }
|
||||
test_redirect_url = "http://stripe_auth_url/"
|
||||
allow(stripe_payment).to receive(:cvv_response_message).and_return(test_redirect_url)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user