Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dunning): Create GoCardLess payment #2481

Merged
merged 1 commit into from
Aug 26, 2024
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
182 changes: 182 additions & 0 deletions app/services/payment_requests/payments/gocardless_service.rb
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
Loading