Skip to content

Commit

Permalink
feat(dunning): Create Adyen payment (#2474)
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 create a Adyen payment for customers this
payment provider.
  • Loading branch information
ancorcruz authored Aug 26, 2024
1 parent 1ee53cb commit 6ea0009
Show file tree
Hide file tree
Showing 5 changed files with 501 additions and 15 deletions.
5 changes: 5 additions & 0 deletions app/models/payment_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ class PaymentRequest < ApplicationRecord
def invoice_ids
applied_invoices.pluck(:invoice_id)
end

def increment_payment_attempts!
increment(:payment_attempts)
save!
end
end

# == Schema Information
Expand Down
174 changes: 174 additions & 0 deletions app/services/payment_requests/payments/adyen_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# frozen_string_literal: true

module PaymentRequests
module Payments
class AdyenService < BaseService
include Lago::Adyen::ErrorHandlable
include Customers::PaymentProviderFinder

PENDING_STATUSES = %w[AuthorisedPending Received].freeze
SUCCESS_STATUSES = %w[Authorised SentForSettle SettleScheduled Settled Refunded].freeze
FAILED_STATUSES = %w[Cancelled CaptureFailed Error Expired Refused].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!

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: payable,
payment_provider_id: adyen_payment_provider.id,
payment_provider_customer_id: customer.adyen_customer.id,
amount_cents: payable.total_amount_cents,
amount_currency: payable.currency.upcase,
provider_payment_id: res.response['pspReference'],
status: res.response['resultCode']
)

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
end

private

attr_accessor :payable

delegate :organization, :customer, to: :payable

def should_process_payment?
return false if payable.payment_succeeded?
return false if adyen_payment_provider.blank?

!!customer&.adyen_customer&.provider_customer_id
end

def client
@client ||= Adyen::Client.new(
api_key: adyen_payment_provider.api_key,
env: adyen_payment_provider.environment,
live_url_prefix: adyen_payment_provider.live_prefix
)
end

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

def update_payment_method_id
result = client.checkout.payments_api.payment_methods(
Lago::Adyen::Params.new(payment_method_params).to_h
).response

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

def create_adyen_payment
update_payment_method_id

client.checkout.payments_api.payments(Lago::Adyen::Params.new(payment_params).to_h)
rescue Adyen::AuthenticationError, Adyen::ValidationError => e
deliver_error_webhook(e)
update_payable_payment_status(payment_status: :failed, deliver_webhook: false)
nil
rescue Adyen::AdyenError => e
deliver_error_webhook(e)
update_payable_payment_status(payment_status: :failed, deliver_webhook: false)
raise e
end

def payment_method_params
{
merchantAccount: adyen_payment_provider.merchant_account,
shopperReference: customer.adyen_customer.provider_customer_id
}
end

def payment_params
prms = {
amount: {
currency: payable.currency.upcase,
value: payable.total_amount_cents
},
reference: payable.id,
paymentMethod: {
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'
}
prms[:shopperEmail] = customer.email if customer.email
prms
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(adyen_error)
DeliverErrorWebhookService.call_async(payable, {
provider_customer_id: customer.adyen_customer.provider_customer_id,
provider_error: {
message: adyen_error.msg,
error_code: adyen_error.code
}
})
end
end
end
end
29 changes: 14 additions & 15 deletions app/services/payment_requests/payments/stripe_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def create
return result
end

increment_payment_attempts
payable.increment_payment_attempts!

stripe_result = create_stripe_payment
# NOTE: return if payment was not processed
Expand All @@ -40,16 +40,20 @@ def create
provider_payment_id: stripe_result.id,
status: stripe_result.status
)
payment.save!

update_payable_payment_status(
payment_status: payable_payment_status(payment.status),
processing: payment.status == 'processing'
)
update_invoices_payment_status(
payment_status: payable_payment_status(payment.status),
processing: payment.status == 'processing'
)
ActiveRecord::Base.transaction do
payment.save!

payable_payment_status = payable_payment_status(payment.status)
update_payable_payment_status(
payment_status: payable_payment_status,
processing: payment.status == 'processing'
)
update_invoices_payment_status(
payment_status: payable_payment_status,
processing: payment.status == 'processing'
)
end

result.payment = payment
result
Expand Down Expand Up @@ -201,11 +205,6 @@ def update_invoices_payment_status(payment_status:, deliver_webhook: true, proce
end
end

def increment_payment_attempts
payable.increment(:payment_attempts)
payable.save!
end

def deliver_error_webhook(stripe_error)
DeliverErrorWebhookService.call_async(payable, {
provider_customer_id: customer.stripe_customer.provider_customer_id,
Expand Down
10 changes: 10 additions & 0 deletions spec/models/payment_request_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,14 @@
expect(payment_request.invoice_ids).to eq(invoices.map(&:id))
end
end

describe "#increment_payment_attempts!" do
let(:payment_request) { create :payment_request }

it "updates payment_attempts attribute +1" do
expect { payment_request.increment_payment_attempts! }
.to change { payment_request.reload.payment_attempts }
.by(1)
end
end
end
Loading

0 comments on commit 6ea0009

Please sign in to comment.