diff --git a/app/controllers/spree/admin/payments_controller.rb b/app/controllers/spree/admin/payments_controller.rb index b93034750a..bdc610e8a7 100644 --- a/app/controllers/spree/admin/payments_controller.rb +++ b/app/controllers/spree/admin/payments_controller.rb @@ -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') diff --git a/app/controllers/spree/orders_controller.rb b/app/controllers/spree/orders_controller.rb index bf4bd814e9..40c315400c 100644 --- a/app/controllers/spree/orders_controller.rb +++ b/app/controllers/spree/orders_controller.rb @@ -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 diff --git a/app/helpers/full_url_helper.rb b/app/helpers/full_url_helper.rb new file mode 100644 index 0000000000..e3bd833346 --- /dev/null +++ b/app/helpers/full_url_helper.rb @@ -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 diff --git a/app/jobs/subscription_confirm_job.rb b/app/jobs/subscription_confirm_job.rb index 29ecd6b6d0..53adc4725b 100644 --- a/app/jobs/subscription_confirm_job.rb +++ b/app/jobs/subscription_confirm_job.rb @@ -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) diff --git a/app/mailers/payment_mailer.rb b/app/mailers/payment_mailer.rb index b0c4a4bbff..253d7b794a 100644 --- a/app/mailers/payment_mailer.rb +++ b/app/mailers/payment_mailer.rb @@ -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 diff --git a/app/models/spree/credit_card.rb b/app/models/spree/credit_card.rb index 2a10e78794..5a4efea485 100644 --- a/app/models/spree/credit_card.rb +++ b/app/models/spree/credit_card.rb @@ -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 diff --git a/app/models/spree/gateway/stripe_sca.rb b/app/models/spree/gateway/stripe_sca.rb index 351adf019d..a2fcc94717 100644 --- a/app/models/spree/gateway/stripe_sca.rb +++ b/app/models/spree/gateway/stripe_sca.rb @@ -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 diff --git a/app/models/spree/payment.rb b/app/models/spree/payment.rb index 0d014462b9..f2ac9ee02e 100644 --- a/app/models/spree/payment.rb +++ b/app/models/spree/payment.rb @@ -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 diff --git a/app/models/spree/payment/processing.rb b/app/models/spree/payment/processing.rb index aabafeac37..80d9d2dc0f 100644 --- a/app/models/spree/payment/processing.rb +++ b/app/models/spree/payment/processing.rb @@ -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! diff --git a/app/services/checkout/stripe_redirect.rb b/app/services/checkout/stripe_redirect.rb index e7c40a4b9e..401f03b23c 100644 --- a/app/services/checkout/stripe_redirect.rb +++ b/app/services/checkout/stripe_redirect.rb @@ -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 diff --git a/app/services/process_payment_intent.rb b/app/services/process_payment_intent.rb new file mode 100644 index 0000000000..4f2aeec7df --- /dev/null +++ b/app/services/process_payment_intent.rb @@ -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 diff --git a/app/views/payment_mailer/authorization_required.html.haml b/app/views/payment_mailer/authorization_required.html.haml new file mode 100644 index 0000000000..080a88de06 --- /dev/null +++ b/app/views/payment_mailer/authorization_required.html.haml @@ -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) diff --git a/app/views/payment_mailer/authorization_required.text.haml b/app/views/payment_mailer/authorization_required.text.haml new file mode 100644 index 0000000000..b084ca2755 --- /dev/null +++ b/app/views/payment_mailer/authorization_required.text.haml @@ -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) diff --git a/app/views/payment_mailer/authorize_payment.html.haml b/app/views/payment_mailer/authorize_payment.html.haml index 438a51c416..ea25e0e7b4 100644 --- a/app/views/payment_mailer/authorize_payment.html.haml +++ b/app/views/payment_mailer/authorize_payment.html.haml @@ -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) diff --git a/app/views/payment_mailer/authorize_payment.text.haml b/app/views/payment_mailer/authorize_payment.text.haml index 956e63ec9f..39ca5e3dbc 100644 --- a/app/views/payment_mailer/authorize_payment.text.haml +++ b/app/views/payment_mailer/authorize_payment.text.haml @@ -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) diff --git a/config/locales/en.yml b/config/locales/en.yml index 1368104cfc..4237d739f1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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," diff --git a/engines/order_management/app/services/order_management/order/send_authorization_emails.rb b/engines/order_management/app/services/order_management/order/send_authorization_emails.rb new file mode 100644 index 0000000000..8e4d78fe49 --- /dev/null +++ b/engines/order_management/app/services/order_management/order/send_authorization_emails.rb @@ -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 diff --git a/engines/order_management/app/services/order_management/subscriptions/stripe_sca_payment_authorize.rb b/engines/order_management/app/services/order_management/order/stripe_sca_payment_authorize.rb similarity index 73% rename from engines/order_management/app/services/order_management/subscriptions/stripe_sca_payment_authorize.rb rename to engines/order_management/app/services/order_management/order/stripe_sca_payment_authorize.rb index ea4fbe5ce7..4739c1a357 100644 --- a/engines/order_management/app/services/order_management/subscriptions/stripe_sca_payment_authorize.rb +++ b/engines/order_management/app/services/order_management/order/stripe_sca_payment_authorize.rb @@ -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? diff --git a/engines/order_management/app/services/order_management/subscriptions/summarizer.rb b/engines/order_management/app/services/order_management/subscriptions/summarizer.rb index fe02a9ad9c..86c7deadea 100644 --- a/engines/order_management/app/services/order_management/subscriptions/summarizer.rb +++ b/engines/order_management/app/services/order_management/subscriptions/summarizer.rb @@ -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 diff --git a/engines/order_management/spec/services/order_management/subscriptions/stripe_sca_payment_authorize_spec.rb b/engines/order_management/spec/services/order_management/order/stripe_sca_payment_authorize_spec.rb similarity index 54% rename from engines/order_management/spec/services/order_management/subscriptions/stripe_sca_payment_authorize_spec.rb rename to engines/order_management/spec/services/order_management/order/stripe_sca_payment_authorize_spec.rb index 17ece20be4..74a03dfcf4 100644 --- a/engines/order_management/spec/services/order_management/subscriptions/stripe_sca_payment_authorize_spec.rb +++ b/engines/order_management/spec/services/order_management/order/stripe_sca_payment_authorize_spec.rb @@ -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 diff --git a/spec/jobs/subscription_confirm_job_spec.rb b/spec/jobs/subscription_confirm_job_spec.rb index c06e65cf39..39bec020b1 100644 --- a/spec/jobs/subscription_confirm_job_spec.rb +++ b/spec/jobs/subscription_confirm_job_spec.rb @@ -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 diff --git a/spec/services/checkout/stripe_redirect_spec.rb b/spec/services/checkout/stripe_redirect_spec.rb index d85152ecbf..05b46dfe25 100644 --- a/spec/services/checkout/stripe_redirect_spec.rb +++ b/spec/services/checkout/stripe_redirect_spec.rb @@ -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)