Skip to content

Commit

Permalink
feat(dunning): Create payment on payment request creation (#2485)
Browse files Browse the repository at this point in the history
## Context

We want to be able to manually request payment of the overdue balance
and send emails for reminders.

👉

https://getlago.canny.io/feature-requests/p/send-reminders-for-overdue-invoices

 ## Description

The goal of this PR is to kick off the payment creation for payment
requests when customer has a payment provider.
  • Loading branch information
ancorcruz authored Aug 27, 2024
1 parent 6b8d0a8 commit dda3814
Show file tree
Hide file tree
Showing 12 changed files with 215 additions and 7 deletions.
18 changes: 18 additions & 0 deletions app/jobs/payment_requests/payments/adyen_create_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module PaymentRequests
module Payments
class AdyenCreateJob < ApplicationJob
queue_as 'providers'

unique :until_executed

retry_on Faraday::ConnectionFailed, wait: :polynomially_longer, attempts: 6

def perform(payable)
result = PaymentRequests::Payments::AdyenService.new(payable).create
result.raise_if_error!
end
end
end
end
16 changes: 16 additions & 0 deletions app/jobs/payment_requests/payments/gocardless_create_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

module PaymentRequests
module Payments
class GocardlessCreateJob < ApplicationJob
queue_as 'providers'

unique :until_executed

def perform(payable)
result = PaymentRequests::Payments::GocardlessService.new(payable).create
result.raise_if_error!
end
end
end
end
19 changes: 19 additions & 0 deletions app/jobs/payment_requests/payments/stripe_create_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module PaymentRequests
module Payments
class StripeCreateJob < ApplicationJob
queue_as 'providers'

unique :until_executed, on_conflict: :log

retry_on Stripe::RateLimitError, wait: :polynomially_longer, attempts: 6
retry_on Stripe::APIConnectionError, wait: :polynomially_longer, attempts: 6

def perform(payable)
result = PaymentRequests::Payments::StripeService.new(payable).create
result.raise_if_error!
end
end
end
end
3 changes: 2 additions & 1 deletion app/services/payment_requests/create_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ def call
# NOTE: Send payment_request.created webhook
SendWebhookJob.perform_later("payment_request.created", payment_request)

# TODO: When payment provider is set: Create payment intent for the overdue invoices
Payments::CreateService.call(payment_request)

# TODO: When payment provider is not set: Send email to the customer

result.payment_request = payment_request
Expand Down
36 changes: 36 additions & 0 deletions app/services/payment_requests/payments/create_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

module PaymentRequests
module Payments
class CreateService < BaseService
def initialize(payable)
@payable = payable

super
end

def call
case payment_provider
when :adyen
PaymentRequests::Payments::AdyenCreateJob.perform_later(payable)
when :gocardless
PaymentRequests::Payments::GocardlessCreateJob.perform_later(payable)
when :stripe
PaymentRequests::Payments::StripeCreateJob.perform_later(payable)
end
# TODO: Do something when no payment provider is set
# or leave it to the caller
rescue ActiveJob::Uniqueness::JobNotUnique => e
Sentry.capture_exception(e)
end

private

attr_reader :payable

def payment_provider
payable.customer.payment_provider&.to_sym
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ def self.service_class(payment_provider)
case payment_provider&.to_s
when 'stripe'
PaymentRequests::Payments::StripeService
# when 'adyen'
# PaymentRequests::Payments::AdyenService
# when 'gocardless'
# PaymentRequests::Payments::GocardlessService
when 'adyen'
PaymentRequests::Payments::AdyenService
when 'gocardless'
PaymentRequests::Payments::GocardlessService
else
raise(NotImplementedError)
end
Expand Down
22 changes: 22 additions & 0 deletions spec/jobs/payment_requests/payments/adyen_create_job_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe PaymentRequests::Payments::AdyenCreateJob, type: :job do
let(:payment_request) { create(:payment_request) }

let(:adyen_service) { instance_double(PaymentRequests::Payments::AdyenService) }

it 'calls the stripe create service' do
allow(PaymentRequests::Payments::AdyenService).to receive(:new)
.with(payment_request)
.and_return(adyen_service)
allow(adyen_service).to receive(:create)
.and_return(BaseService::Result.new)

described_class.perform_now(payment_request)

expect(PaymentRequests::Payments::AdyenService).to have_received(:new)
expect(adyen_service).to have_received(:create)
end
end
22 changes: 22 additions & 0 deletions spec/jobs/payment_requests/payments/gocardless_create_job_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe PaymentRequests::Payments::GocardlessCreateJob, type: :job do
let(:payment_request) { create(:payment_request) }

let(:gocardless_service) { instance_double(PaymentRequests::Payments::GocardlessService) }

it 'calls the stripe create service' do
allow(PaymentRequests::Payments::GocardlessService).to receive(:new)
.with(payment_request)
.and_return(gocardless_service)
allow(gocardless_service).to receive(:create)
.and_return(BaseService::Result.new)

described_class.perform_now(payment_request)

expect(PaymentRequests::Payments::GocardlessService).to have_received(:new)
expect(gocardless_service).to have_received(:create)
end
end
22 changes: 22 additions & 0 deletions spec/jobs/payment_requests/payments/stripe_create_job_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe PaymentRequests::Payments::StripeCreateJob, type: :job do
let(:payment_request) { create(:payment_request) }

let(:stripe_service) { instance_double(PaymentRequests::Payments::StripeService) }

it 'calls the stripe create service' do
allow(PaymentRequests::Payments::StripeService).to receive(:new)
.with(payment_request)
.and_return(stripe_service)
allow(stripe_service).to receive(:create)
.and_return(BaseService::Result.new)

described_class.perform_now(payment_request)

expect(PaymentRequests::Payments::StripeService).to have_received(:new)
expect(stripe_service).to have_received(:create)
end
end
8 changes: 8 additions & 0 deletions spec/services/payment_requests/create_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,14 @@
expect(SendWebhookJob).to have_been_enqueued.with("payment_request.created", PaymentRequest)
end

it "creates a payment for the payment request" do
allow(PaymentRequests::Payments::CreateService).to receive(:call)

result = create_service.call

expect(PaymentRequests::Payments::CreateService).to have_received(:call).with(result.payment_request)
end

it "returns the payment request", :aggregate_failures do
result = create_service.call

Expand Down
44 changes: 44 additions & 0 deletions spec/services/payment_requests/payments/create_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe PaymentRequests::Payments::CreateService, type: :service do
subject(:create_service) { described_class.new(payment_request) }

let(:payment_request) do
create(:payment_request, customer:, organization: customer.organization)
end
let(:customer) { create(:customer, payment_provider:) }

describe "#call" do
context "with adyen payment provider" do
let(:payment_provider) { "adyen" }

it "enqueues a job to create a adyen payment" do
expect do
create_service.call
end.to have_enqueued_job(PaymentRequests::Payments::AdyenCreateJob)
end
end

context "with gocardless payment provider" do
let(:payment_provider) { "gocardless" }

it "enqueues a job to create a gocardless payment" do
expect do
create_service.call
end.to have_enqueued_job(PaymentRequests::Payments::GocardlessCreateJob)
end
end

context "with strip payment provider" do
let(:payment_provider) { "stripe" }

it "enqueues a job to create a stripe payment" do
expect do
create_service.call
end.to have_enqueued_job(PaymentRequests::Payments::StripeCreateJob)
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@
context 'when adyen' do
let(:payment_provider) { 'adyen' }

xit 'returns correct class' do
it 'returns correct class' do
expect(factory_service.class.to_s).to eq('PaymentRequests::Payments::AdyenService')
end
end

context 'when gocardless' do
let(:payment_provider) { 'gocardless' }

xit 'returns correct class' do
it 'returns correct class' do
expect(factory_service.class.to_s).to eq('PaymentRequests::Payments::GocardlessService')
end
end
Expand Down

0 comments on commit dda3814

Please sign in to comment.