Skip to content

Commit

Permalink
misc(payment): Refactor payment creation on provider
Browse files Browse the repository at this point in the history
  • Loading branch information
vincent-pochet committed Dec 13, 2024
1 parent 43b9389 commit b9ee6ce
Show file tree
Hide file tree
Showing 18 changed files with 1,516 additions and 1,315 deletions.
4 changes: 4 additions & 0 deletions app/services/base_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ def call(**args, &block)
raise NotImplementedError
end

def call!(*, &)
call(*, &).raise_if_error!
end

def call_async(**args, &block)
raise NotImplementedError
end
Expand Down
54 changes: 10 additions & 44 deletions app/services/invoices/payments/adyen_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,47 +16,13 @@ def initialize(invoice = nil)
super
end

def call
result.invoice = invoice
return result unless should_process_payment?

increment_payment_attempts

res = create_adyen_payment
return result unless res

adyen_success, _adyen_error = handle_adyen_response(res)
return result unless adyen_success

payment = Payment.new(
payable: invoice,
payment_provider_id: adyen_payment_provider.id,
payment_provider_customer_id: customer.adyen_customer.id,
amount_cents: invoice.total_amount_cents,
amount_currency: invoice.currency.upcase,
provider_payment_id: res.response['pspReference'],
status: res.response['resultCode']
)
payment.save!

invoice_payment_status = invoice_payment_status(payment.status)
update_invoice_payment_status(payment_status: invoice_payment_status)

Integrations::Aggregator::Payments::CreateJob.perform_later(payment:) if payment.should_sync_payment?

result.payment = payment
result
rescue Faraday::ConnectionFailed => e
raise Invoices::Payments::ConnectionError, e
end

def update_payment_status(provider_payment_id:, status:, metadata: {})
payment = if metadata[:payment_type] == 'one-time'
payment = if metadata[:payment_type] == "one-time"
create_payment(provider_payment_id:, metadata:)
else
Payment.find_by(provider_payment_id:)
end
return result.not_found_failure!(resource: 'adyen_payment') unless payment
return result.not_found_failure!(resource: "adyen_payment") unless payment

result.payment = payment
result.invoice = payment.payable
Expand All @@ -81,7 +47,7 @@ def generate_payment_url

return result unless result.success?

result.payment_url = res.response['url']
result.payment_url = res.response["url"]

result
rescue Adyen::AdyenError => e
Expand Down Expand Up @@ -139,7 +105,7 @@ def update_payment_method_id
Lago::Adyen::Params.new(payment_method_params).to_h
).response

payment_method_id = result['storedPaymentMethods']&.first&.dig('id')
payment_method_id = result["storedPaymentMethods"]&.first&.dig("id")
customer.adyen_customer.update!(payment_method_id:) if payment_method_id
end

Expand Down Expand Up @@ -172,13 +138,13 @@ def payment_params
},
reference: invoice.number,
paymentMethod: {
type: 'scheme',
type: "scheme",
storedPaymentMethodId: customer.adyen_customer.payment_method_id
},
shopperReference: customer.adyen_customer.provider_customer_id,
merchantAccount: adyen_payment_provider.merchant_account,
shopperInteraction: 'ContAuth',
recurringProcessingModel: 'UnscheduledCardOnFile'
shopperInteraction: "ContAuth",
recurringProcessingModel: "UnscheduledCardOnFile"
}
prms[:shopperEmail] = customer.email if customer.email
prms
Expand All @@ -194,15 +160,15 @@ def payment_url_params
merchantAccount: adyen_payment_provider.merchant_account,
returnUrl: success_redirect_url,
shopperReference: customer.external_id,
storePaymentMethodMode: 'enabled',
recurringProcessingModel: 'UnscheduledCardOnFile',
storePaymentMethodMode: "enabled",
recurringProcessingModel: "UnscheduledCardOnFile",
expiresAt: Time.current + 1.day,
metadata: {
lago_customer_id: customer.id,
lago_invoice_id: invoice.id,
invoice_issuing_date: invoice.issuing_date.iso8601,
invoice_type: invoice.invoice_type,
payment_type: 'one-time'
payment_type: "one-time"
}
}
prms[:shopperEmail] = customer.email if customer.email
Expand Down
72 changes: 63 additions & 9 deletions app/services/invoices/payments/create_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,39 @@ def call
return result unless should_process_payment?

unless invoice.total_amount_cents.positive?
Invoices::UpdateService.call!(
invoice:,
params: {payment_status: :succeeded, ready_for_payment_processing: false},
webhook_notification: true
)
update_invoice_payment_status(payment_status: :succeeded)
return result
end

# TODO(payments): Create a pending paymnent record with a DB uniqueness constraint on invoice_id
# and inject it to the payment services to avoid duplicated payments
::PaymentProviders::CreatePaymentFactory.new_instance(provider:, invoice:).call
invoice.update!(payment_attempts: invoice.payment_attempts + 1)

payment_result = ::PaymentProviders::CreatePaymentFactory.new_instance(
provider:, invoice:, provider_customer: current_payment_provider_customer
).call!

deliver_error_webhook(payment_result) if payment_result.error_message.present?

if payment_result.payment.present?
result.payment = payment_result.payment

update_invoice_payment_status(
payment_status: payment_result.payment_status,
processing: result.payment.status == "processing"
)

if result.payment.should_sync_payment?
Integrations::Aggregator::Payments::CreateJob.perform_later(payment: result.payment)
end
elsif payment_result.payment_status.present?
update_invoice_payment_status(payment_status: payment_result.payment_status)
end

result
rescue BaseService::ServiceFailure => e
deliver_error_webhook(e.result)
update_invoice_payment_status(payment_status: e.result.payment_status) if e.result.payment_status.present?

raise
end

def call_async
Expand All @@ -51,8 +73,40 @@ def provider

def should_process_payment?
return false if invoice.payment_succeeded? || invoice.voided?
return false if current_payment_provider.blank?

current_payment_provider_customer&.provider_customer_id
end

def current_payment_provider
@current_payment_provider ||= payment_provider(customer)
end

def current_payment_provider_customer
@current_payment_provider_customer ||= customer.payment_provider_customers
.find_by(payment_provider_id: current_payment_provider.id)
end

def update_invoice_payment_status(payment_status:, processing: false)
Invoices::UpdateService.call!(
invoice: invoice,
params: {
payment_status:,
# NOTE: A proper `processing` payment status should be introduced for invoices
ready_for_payment_processing: !processing && payment_status.to_sym != :succeeded
},
webhook_notification: payment_status.to_sym == :succeeded
)
end

payment_provider(customer).present?
def deliver_error_webhook(payment_result)
DeliverErrorWebhookService.call_async(invoice, {
provider_customer_id: current_payment_provider_customer.provider_customer_id,
provider_error: {
message: payment_result.error_message,
error_code: payment_result.error_code
}
})
end
end
end
Expand Down
129 changes: 1 addition & 128 deletions app/services/invoices/payments/gocardless_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,6 @@ 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
Expand All @@ -29,52 +16,9 @@ def initialize(invoice = nil)
super
end

def call
result.invoice = invoice
return result unless should_process_payment?

increment_payment_attempts

gocardless_result = create_gocardless_payment

payment = Payment.new(
payable: invoice,
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
)
payment.save!

invoice_payment_status = invoice_payment_status(payment.status)
update_invoice_payment_status(payment_status: invoice_payment_status)

Integrations::Aggregator::Payments::CreateJob.perform_later(payment:) if payment.should_sync_payment?

result.payment = payment
result
rescue MandateNotFoundError => e
deliver_error_webhook(e)
update_invoice_payment_status(payment_status: :failed, deliver_webhook: false)

result.service_failure!(code: e.code, message: e.message)
result
rescue GoCardlessPro::Error, GoCardlessPro::ValidationError => e
deliver_error_webhook(e)
update_invoice_payment_status(payment_status: :failed, deliver_webhook: false)

if e.is_a?(GoCardlessPro::ValidationError)
result
else
raise
end
end

def update_payment_status(provider_payment_id:, status:)
payment = Payment.find_by(provider_payment_id:)
return result.not_found_failure!(resource: 'gocardless_payment') unless payment
return result.not_found_failure!(resource: "gocardless_payment") unless payment

result.payment = payment
result.invoice = payment.payable
Expand All @@ -96,63 +40,6 @@ def update_payment_status(provider_payment_id:, status:)

delegate :organization, :customer, to: :invoice

def should_process_payment?
return false if invoice.payment_succeeded? || invoice.voided?
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: invoice.total_amount_cents,
currency: invoice.currency.upcase,
retry_if_possible: false,
metadata: {
lago_customer_id: customer.id,
lago_invoice_id: invoice.id,
invoice_issuing_date: invoice.issuing_date.iso8601
},
links: {
mandate: mandate_id
}
},
headers: {
'Idempotency-Key' => "#{invoice.id}/#{invoice.payment_attempts}"
}
)
end

def invoice_payment_status(payment_status)
return :pending if PENDING_STATUSES.include?(payment_status)
return :succeeded if SUCCESS_STATUSES.include?(payment_status)
Expand All @@ -172,20 +59,6 @@ def update_invoice_payment_status(payment_status:, deliver_webhook: true)
)
update_invoice_result.raise_if_error!
end

def increment_payment_attempts
invoice.update!(payment_attempts: invoice.payment_attempts + 1)
end

def deliver_error_webhook(gocardless_error)
DeliverErrorWebhookService.call_async(invoice, {
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

0 comments on commit b9ee6ce

Please sign in to comment.