-
Notifications
You must be signed in to change notification settings - Fork 100
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(dunning): Create GoCardLess payment (#2481)
## 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 create a GoCardLess payment mandate for customers with this payment provider.
- Loading branch information
Showing
2 changed files
with
417 additions
and
0 deletions.
There are no files selected for viewing
182 changes: 182 additions & 0 deletions
182
app/services/payment_requests/payments/gocardless_service.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
# frozen_string_literal: true | ||
|
||
module PaymentRequests | ||
module Payments | ||
class GocardlessService < BaseService | ||
include Customers::PaymentProviderFinder | ||
|
||
class MandateNotFoundError < StandardError | ||
DEFAULT_MESSAGE = "No mandate available for payment" | ||
ERROR_CODE = "no_mandate_error" | ||
|
||
def initialize(msg = DEFAULT_MESSAGE) | ||
super | ||
end | ||
|
||
def code | ||
ERROR_CODE | ||
end | ||
end | ||
|
||
PENDING_STATUSES = %w[pending_customer_approval pending_submission submitted confirmed] | ||
.freeze | ||
SUCCESS_STATUSES = %w[paid_out].freeze | ||
FAILED_STATUSES = %w[cancelled customer_approval_denied failed charged_back].freeze | ||
|
||
def initialize(payable = nil) | ||
@payable = payable | ||
|
||
super(nil) | ||
end | ||
|
||
def create | ||
result.payable = payable | ||
return result unless should_process_payment? | ||
|
||
unless payable.total_amount_cents.positive? | ||
update_payable_payment_status(payment_status: :succeeded) | ||
return result | ||
end | ||
|
||
payable.increment_payment_attempts! | ||
|
||
gocardless_result = create_gocardless_payment | ||
|
||
payment = Payment.new( | ||
payable: payable, | ||
payment_provider_id: gocardless_payment_provider.id, | ||
payment_provider_customer_id: customer.gocardless_customer.id, | ||
amount_cents: gocardless_result.amount, | ||
amount_currency: gocardless_result.currency&.upcase, | ||
provider_payment_id: gocardless_result.id, | ||
status: gocardless_result.status | ||
) | ||
|
||
ActiveRecord::Base.transaction do | ||
payment.save! | ||
|
||
payable_payment_status = payable_payment_status(payment.status) | ||
update_payable_payment_status(payment_status: payable_payment_status) | ||
update_invoices_payment_status(payment_status: payable_payment_status) | ||
end | ||
|
||
Integrations::Aggregator::Payments::CreateJob.perform_later(payment:) if payment.should_sync_payment? | ||
|
||
result.payment = payment | ||
result | ||
rescue MandateNotFoundError => e | ||
deliver_error_webhook(e) | ||
update_payable_payment_status(payment_status: :failed, deliver_webhook: false) | ||
|
||
result.service_failure!(code: e.code, message: e.message) | ||
result | ||
end | ||
|
||
private | ||
|
||
attr_accessor :payable | ||
|
||
delegate :organization, :customer, to: :payable | ||
|
||
def should_process_payment? | ||
return false if payable.payment_succeeded? | ||
return false if gocardless_payment_provider.blank? | ||
|
||
!!customer&.gocardless_customer&.provider_customer_id | ||
end | ||
|
||
def client | ||
@client ||= GoCardlessPro::Client.new( | ||
access_token: gocardless_payment_provider.access_token, | ||
environment: gocardless_payment_provider.environment | ||
) | ||
end | ||
|
||
def gocardless_payment_provider | ||
@gocardless_payment_provider ||= payment_provider(customer) | ||
end | ||
|
||
def mandate_id | ||
result = client.mandates.list( | ||
params: { | ||
customer: customer.gocardless_customer.provider_customer_id, | ||
status: %w[pending_customer_approval pending_submission submitted active] | ||
} | ||
) | ||
|
||
mandate = result&.records&.first | ||
|
||
raise MandateNotFoundError unless mandate | ||
|
||
customer.gocardless_customer.provider_mandate_id = mandate.id | ||
customer.gocardless_customer.save! | ||
|
||
mandate.id | ||
end | ||
|
||
def create_gocardless_payment | ||
client.payments.create( | ||
params: { | ||
amount: payable.total_amount_cents, | ||
currency: payable.currency.upcase, | ||
retry_if_possible: false, | ||
metadata: { | ||
lago_customer_id: customer.id, | ||
lago_payment_request_id: payable.id, | ||
lago_invoice_ids: payable.invoice_ids | ||
}, | ||
links: { | ||
mandate: mandate_id | ||
} | ||
}, | ||
headers: { | ||
'Idempotency-Key' => "#{payable.id}/#{payable.payment_attempts}" | ||
} | ||
) | ||
rescue GoCardlessPro::Error => e | ||
deliver_error_webhook(e) | ||
update_payable_payment_status(payment_status: :failed, deliver_webhook: false) | ||
|
||
raise | ||
end | ||
|
||
def payable_payment_status(payment_status) | ||
return :pending if PENDING_STATUSES.include?(payment_status) | ||
return :succeeded if SUCCESS_STATUSES.include?(payment_status) | ||
return :failed if FAILED_STATUSES.include?(payment_status) | ||
|
||
payment_status | ||
end | ||
|
||
def update_payable_payment_status(payment_status:, deliver_webhook: true) | ||
payable.update!( | ||
payment_status:, | ||
ready_for_payment_processing: payment_status.to_sym != :succeeded | ||
) | ||
end | ||
|
||
def update_invoices_payment_status(payment_status:, deliver_webhook: true) | ||
payable.invoices.each do |invoice| | ||
Invoices::UpdateService.call( | ||
invoice:, | ||
params: { | ||
payment_status:, | ||
ready_for_payment_processing: payment_status.to_sym != :succeeded | ||
}, | ||
webhook_notification: deliver_webhook | ||
).raise_if_error! | ||
end | ||
end | ||
|
||
def deliver_error_webhook(gocardless_error) | ||
DeliverErrorWebhookService.call_async(payable, { | ||
provider_customer_id: customer.gocardless_customer.provider_customer_id, | ||
provider_error: { | ||
message: gocardless_error.message, | ||
error_code: gocardless_error.code | ||
} | ||
}) | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.