Merge pull request #6713 from andrewpbrett/sca-subs-emails

Send emails when subscription payments require SCA auth
This commit is contained in:
Andy Brett
2021-02-12 08:23:32 -08:00
committed by GitHub
22 changed files with 158 additions and 38 deletions

View File

@@ -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')

View File

@@ -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

View 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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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!

View File

@@ -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

View 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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,"

View File

@@ -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

View File

@@ -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?

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)