Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions app/jobs/payment_providers/cancel_payment_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module PaymentProviders
class CancelPaymentJob < ApplicationJob
queue_as do
if ActiveModel::Type::Boolean.new.cast(ENV["SIDEKIQ_PAYMENTS"])
:payments
else
:providers
end
end

def perform(payment)
PaymentProviders::CancelPaymentService.call!(payment:)
end
end
end
43 changes: 43 additions & 0 deletions app/services/payment_providers/cancel_payment_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

module PaymentProviders
class CancelPaymentService < BaseService
Result = BaseResult[:payment]

def initialize(payment:)
@payment = payment
super
end

def call
result.payment = payment

return result if payment.payment_provider.blank?
return result if payment.provider_payment_id.blank?
return result if payment.succeeded?

case payment.payment_provider.type
when "PaymentProviders::StripeProvider"
PaymentProviders::Stripe::Payments::CancelService.call!(payment:)
when "PaymentProviders::AdyenProvider"
PaymentProviders::Adyen::Payments::CancelService.call!(payment:)
when "PaymentProviders::GocardlessProvider"
PaymentProviders::Gocardless::Payments::CancelService.call!(payment:)
else
# Cashfree, Flutterwave, MoneyHash, and any future provider without a
# dedicated cancel service: nothing to do here. The eventual webhook
# (or reconciliation) is the lifecycle authority for the Payment.
Rails.logger.info(
"PaymentProviders::CancelPaymentService: PSP cancel not supported for " \
"#{payment.payment_provider.type} (payment #{payment.id}); skipping"
)
end
Comment thread
ancorcruz marked this conversation as resolved.

result
end

private

attr_reader :payment
end
end
24 changes: 24 additions & 0 deletions spec/jobs/payment_providers/cancel_payment_job_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe PaymentProviders::CancelPaymentJob do
let(:organization) { create(:organization) }
let(:customer) { create(:customer, organization:) }
let(:invoice) { create(:invoice, customer:, organization:) }
let(:payment_provider) { create(:stripe_provider, organization:) }
let(:payment) do
create(:payment, payable: invoice, payment_provider:, organization:, customer:,
provider_payment_id: "pi_123", payable_payment_status: :pending)
end

before do
allow(PaymentProviders::CancelPaymentService).to receive(:call!)
end

it "forwards the payment to the dispatcher service" do
described_class.perform_now(payment)

expect(PaymentProviders::CancelPaymentService).to have_received(:call!).with(payment:)
end
end
166 changes: 166 additions & 0 deletions spec/services/payment_providers/cancel_payment_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe PaymentProviders::CancelPaymentService do
subject(:result) { described_class.call(payment:) }

let(:organization) { create(:organization) }
let(:customer) { create(:customer, organization:) }
let(:invoice) { create(:invoice, customer:, organization:) }

before do
allow(PaymentProviders::Stripe::Payments::CancelService).to receive(:call!)
allow(PaymentProviders::Adyen::Payments::CancelService).to receive(:call!)
allow(PaymentProviders::Gocardless::Payments::CancelService).to receive(:call!)
end

context "when payment has no payment provider" do
let(:payment) do
create(:payment, payable: invoice, payment_provider: nil, organization:, customer:,
provider_payment_id: "pi_123", payable_payment_status: :pending)
end

it "returns a successful result with the payment" do
expect(result).to be_success
expect(result.payment).to eq(payment)
end

it "does not call any PSP cancel service" do
result

expect(PaymentProviders::Stripe::Payments::CancelService).not_to have_received(:call!)
expect(PaymentProviders::Adyen::Payments::CancelService).not_to have_received(:call!)
expect(PaymentProviders::Gocardless::Payments::CancelService).not_to have_received(:call!)
end
end

context "when payment has no provider_payment_id" do
let(:payment_provider) { create(:stripe_provider, organization:) }
let(:payment) do
create(:payment, payable: invoice, payment_provider:, organization:, customer:,
provider_payment_id: nil, payable_payment_status: :pending)
end

it "returns a successful result with the payment" do
expect(result).to be_success
expect(result.payment).to eq(payment)
end

it "does not call any PSP cancel service" do
result

expect(PaymentProviders::Stripe::Payments::CancelService).not_to have_received(:call!)
end
end

context "when payment is already succeeded" do
let(:payment_provider) { create(:stripe_provider, organization:) }
let(:payment) do
create(:payment, payable: invoice, payment_provider:, organization:, customer:,
provider_payment_id: "pi_123", payable_payment_status: :succeeded)
end

it "returns a successful result with the payment" do
expect(result).to be_success
expect(result.payment).to eq(payment)
end

it "does not call the PSP cancel service (canceling a succeeded payment would be destructive)" do
result

expect(PaymentProviders::Stripe::Payments::CancelService).not_to have_received(:call!)
end
end

context "when the provider is Stripe" do
let(:payment_provider) { create(:stripe_provider, organization:) }
let(:payment) do
create(:payment, payable: invoice, payment_provider:, organization:, customer:,
provider_payment_id: "pi_123", payable_payment_status: :pending)
end

it "routes to the Stripe cancel service with the payment" do
result

expect(PaymentProviders::Stripe::Payments::CancelService).to have_received(:call!).with(payment:)
end

it "does not call the other PSP cancel services" do
result

expect(PaymentProviders::Adyen::Payments::CancelService).not_to have_received(:call!)
expect(PaymentProviders::Gocardless::Payments::CancelService).not_to have_received(:call!)
end
end

context "when the provider is Adyen" do
let(:payment_provider) { create(:adyen_provider, organization:) }
let(:provider_customer) { create(:adyen_customer, customer:, payment_provider:) }
let(:payment) do
create(:payment, payable: invoice, payment_provider:, payment_provider_customer: provider_customer,
organization:, customer:, provider_payment_id: "PSPREF123", payable_payment_status: :pending)
end

it "routes to the Adyen cancel service with the payment" do
result

expect(PaymentProviders::Adyen::Payments::CancelService).to have_received(:call!).with(payment:)
end

it "does not call the other PSP cancel services" do
result

expect(PaymentProviders::Stripe::Payments::CancelService).not_to have_received(:call!)
expect(PaymentProviders::Gocardless::Payments::CancelService).not_to have_received(:call!)
end
end

context "when the provider is GoCardless" do
let(:payment_provider) { create(:gocardless_provider, organization:) }
let(:provider_customer) { create(:gocardless_customer, customer:, payment_provider:) }
let(:payment) do
create(:payment, payable: invoice, payment_provider:, payment_provider_customer: provider_customer,
organization:, customer:, provider_payment_id: "PM123", payable_payment_status: :pending)
end

it "routes to the GoCardless cancel service with the payment" do
result

expect(PaymentProviders::Gocardless::Payments::CancelService).to have_received(:call!).with(payment:)
end

it "does not call the other PSP cancel services" do
result

expect(PaymentProviders::Stripe::Payments::CancelService).not_to have_received(:call!)
expect(PaymentProviders::Adyen::Payments::CancelService).not_to have_received(:call!)
end
end

context "when the provider has no dedicated cancel service (Cashfree, Flutterwave, MoneyHash)" do
let(:payment_provider) { create(:cashfree_provider, organization:) }
let(:payment) do
create(:payment, payable: invoice, payment_provider:, organization:, customer:,
provider_payment_id: "cf_123", payable_payment_status: :pending)
end

it "returns a successful result without calling any PSP cancel service" do
result

expect(result).to be_success
expect(PaymentProviders::Stripe::Payments::CancelService).not_to have_received(:call!)
expect(PaymentProviders::Adyen::Payments::CancelService).not_to have_received(:call!)
expect(PaymentProviders::Gocardless::Payments::CancelService).not_to have_received(:call!)
end

it "logs that the provider is unsupported" do
allow(Rails.logger).to receive(:info)

result

expect(Rails.logger).to have_received(:info)
.with(a_string_matching(/PSP cancel not supported.*CashfreeProvider/))
end
end
end
Loading