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

misc(PaymentRequest): Apply payment idempotency refactor #2986

Merged
merged 4 commits into from
Jan 13, 2025
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
2 changes: 1 addition & 1 deletion app/jobs/invoices/payments/adyen_create_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class AdyenCreateJob < ApplicationJob
retry_on Faraday::ConnectionFailed, wait: :polynomially_longer, attempts: 6

def perform(invoice)
# NOTE: Legacy job, kept only to avoid existing jobs
# NOTE: Legacy job, kept only to avoid failure with existing jobs

Invoices::Payments::CreateService.call!(invoice:, payment_provider: :adyen)
end
Expand Down
2 changes: 1 addition & 1 deletion app/jobs/invoices/payments/gocardless_create_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class GocardlessCreateJob < ApplicationJob
unique :until_executed, on_conflict: :log

def perform(invoice)
# NOTE: Legacy job, kept only to avoid existing jobs
# NOTE: Legacy job, kept only to avoid faileure with existing jobs

Invoices::Payments::CreateService.call!(invoice:, payment_provider: :gocardless)
end
Expand Down
2 changes: 1 addition & 1 deletion app/jobs/invoices/payments/stripe_create_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class StripeCreateJob < ApplicationJob
retry_on Invoices::Payments::RateLimitError, wait: :polynomially_longer, attempts: 6

def perform(invoice)
# NOTE: Legacy job, kept only to avoid existing jobs
# NOTE: Legacy job, kept only to avoid faileure with existing jobs

Invoices::Payments::CreateService.call!(invoice:, payment_provider: :stripe)
end
Expand Down
6 changes: 2 additions & 4 deletions app/jobs/payment_requests/payments/adyen_create_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ class AdyenCreateJob < ApplicationJob
retry_on Faraday::ConnectionFailed, wait: :polynomially_longer, attempts: 6

def perform(payable)
result = PaymentRequests::Payments::AdyenService.new(payable).create
# NOTE: Legacy job, kept only to avoid faileure with existing jobs

PaymentRequestMailer.with(payment_request: payable).requested.deliver_later if result.payable&.payment_failed?

result.raise_if_error!
PaymentRequests::Payments::CreateService.call!(payable:, payment_provider: 'adyen')
end
end
end
Expand Down
19 changes: 19 additions & 0 deletions app/jobs/payment_requests/payments/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 CreateJob < ApplicationJob
queue_as 'providers'

unique :until_executed, on_conflict: :log

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

def perform(payable:, payment_provider:)
PaymentRequests::Payments::CreateService.call!(payable:, payment_provider:)
end
end
end
end
6 changes: 2 additions & 4 deletions app/jobs/payment_requests/payments/gocardless_create_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ class GocardlessCreateJob < ApplicationJob
unique :until_executed, on_conflict: :log

def perform(payable)
result = PaymentRequests::Payments::GocardlessService.new(payable).create
# NOTE: Legacy job, kept only to avoid faileure with existing jobs

PaymentRequestMailer.with(payment_request: payable).requested.deliver_later if result.payable&.payment_failed?

result.raise_if_error!
PaymentRequests::Payments::CreateService.call!(payable:, payment_provider: 'gocardless')
end
end
end
Expand Down
6 changes: 2 additions & 4 deletions app/jobs/payment_requests/payments/stripe_create_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@ class StripeCreateJob < ApplicationJob
retry_on ::Stripe::APIConnectionError, wait: :polynomially_longer, attempts: 6

def perform(payable)
result = PaymentRequests::Payments::StripeService.new(payable).create
# NOTE: Legacy job, kept only to avoid faileure with existing jobs

PaymentRequestMailer.with(payment_request: payable).requested.deliver_later if result.payable&.payment_failed?

result.raise_if_error!
PaymentRequests::Payments::CreateService.call!(payable:, payment_provider: 'stripe')
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/models/payment_providers/adyen_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module PaymentProviders
class AdyenProvider < BaseProvider
SUCCESS_REDIRECT_URL = 'https://www.adyen.com/'

PENDING_STATUSES = %w[AuthorisedPending Received].freeze
PROCESSING_STATUSES = %w[AuthorisedPending Received].freeze
SUCCESS_STATUSES = %w[Authorised SentForSettle SettleScheduled Settled Refunded].freeze
FAILED_STATUSES = %w[Cancelled CaptureFailed Error Expired Refused].freeze

Expand Down
2 changes: 1 addition & 1 deletion app/models/payment_providers/base_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class BaseProvider < ApplicationRecord
settings_accessors :webhook_secret, :success_redirect_url

def determine_payment_status(payment_status)
return :pending if self.class::PENDING_STATUSES.include?(payment_status)
return :processing if self.class::PROCESSING_STATUSES.include?(payment_status)
return :succeeded if self.class::SUCCESS_STATUSES.include?(payment_status)
return :failed if self.class::FAILED_STATUSES.include?(payment_status)

Expand Down
2 changes: 1 addition & 1 deletion app/models/payment_providers/gocardless_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module PaymentProviders
class GocardlessProvider < BaseProvider
SUCCESS_REDIRECT_URL = 'https://gocardless.com/'

PENDING_STATUSES = %w[pending_customer_approval pending_submission submitted confirmed].freeze
PROCESSING_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

Expand Down
2 changes: 1 addition & 1 deletion app/models/payment_providers/stripe_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class StripeProvider < BaseProvider
charge.dispute.closed
].freeze

PENDING_STATUSES = %w[
PROCESSING_STATUSES = %w[
processing
requires_capture
requires_action
Expand Down
14 changes: 1 addition & 13 deletions app/services/invoices/payments/adyen_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ 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(invoice = nil)
@invoice = invoice

Expand All @@ -30,7 +26,7 @@ def update_payment_status(provider_payment_id:, status:, metadata: {})

payment.update!(status:)

invoice_payment_status = invoice_payment_status(status)
invoice_payment_status = payment.payment_provider&.determine_payment_status(status)
update_invoice_payment_status(payment_status: invoice_payment_status)

result
Expand Down Expand Up @@ -175,14 +171,6 @@ def payment_url_params
prms
end

def invoice_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_invoice_payment_status(payment_status:, deliver_webhook: true)
result = Invoices::UpdateService.call(
invoice:,
Expand Down
27 changes: 18 additions & 9 deletions app/services/invoices/payments/create_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,20 @@ def call

result.payment = payment

payment_result = ::PaymentProviders::CreatePaymentFactory.new_instance(provider:, payment:).call!
payment_result = ::PaymentProviders::CreatePaymentFactory.new_instance(
provider:,
payment:,
reference: "#{invoice.organization.name} - Invoice #{invoice.number}",
metadata: {
lago_invoice_id: invoice.id,
lago_customer_id: invoice.customer_id,
invoice_issuing_date: invoice.issuing_date.iso8601,
invoice_type: invoice.invoice_type
}
).call!

payment_status = payment_result.payment.payable_payment_status
update_invoice_payment_status(
payment_status: (payment_status == "processing") ? :pending : payment_status,
processing: payment_status == "processing"
)
update_invoice_payment_status(payment_status:)

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

Expand All @@ -58,10 +65,12 @@ def call
result.payment = e.result.payment

if e.result.payment.payable_payment_status&.to_sym != :pending
# Avoid notification for amount_too_small errors
deliver_error_webhook(e.result)
update_invoice_payment_status(payment_status: e.result.payment.payable_payment_status)
end

update_invoice_payment_status(payment_status: e.result.payment.payable_payment_status)

# Some errors should be investigated and need to be raised
raise if e.result.reraise

Expand Down Expand Up @@ -103,13 +112,13 @@ def current_payment_provider_customer
.find_by(payment_provider_id: current_payment_provider.id)
end

def update_invoice_payment_status(payment_status:, processing: false)
def update_invoice_payment_status(payment_status:)
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
payment_status: (payment_status.to_s == "processing") ? :pending : payment_status,
ready_for_payment_processing: %w[pending failed].include?(payment_status.to_s)
},
webhook_notification: payment_status.to_sym == :succeeded
)
Expand Down
15 changes: 1 addition & 14 deletions app/services/invoices/payments/gocardless_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@ module Payments
class GocardlessService < BaseService
include Customers::PaymentProviderFinder

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(invoice = nil)
@invoice = invoice

Expand All @@ -26,7 +21,7 @@ def update_payment_status(provider_payment_id:, status:)

payment.update!(status:)

invoice_payment_status = invoice_payment_status(status)
invoice_payment_status = payment.payment_provider&.determine_payment_status(status)
update_invoice_payment_status(payment_status: invoice_payment_status)

result
Expand All @@ -40,14 +35,6 @@ def update_payment_status(provider_payment_id:, status:)

delegate :organization, :customer, to: :invoice

def invoice_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_invoice_payment_status(payment_status:, deliver_webhook: true)
update_invoice_result = Invoices::UpdateService.call(
invoice: result.invoice,
Expand Down
17 changes: 2 additions & 15 deletions app/services/invoices/payments/stripe_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@ module Payments
class StripeService < BaseService
include Customers::PaymentProviderFinder

PENDING_STATUSES = %w[processing requires_capture requires_action requires_confirmation requires_payment_method]
.freeze
SUCCESS_STATUSES = %w[succeeded].freeze
FAILED_STATUSES = %w[canceled].freeze

def initialize(invoice = nil)
@invoice = invoice

Expand Down Expand Up @@ -37,7 +32,7 @@ def update_payment_status(organization_id:, status:, stripe_payment:)
payment.update!(status:)

update_invoice_payment_status(
payment_status: invoice_payment_status(status),
payment_status: payment.payment_provider&.determine_payment_status(status),
processing: status == "processing"
)

Expand Down Expand Up @@ -89,7 +84,7 @@ def create_payment(stripe_payment, invoice: nil)
status: "pending"
)

status = invoice_payment_status(stripe_payment.status)
status = payment.payment_provider&.determine_payment_status(stripe_payment.status)
status = (status.to_sym == :pending) ? :processing : status

payment.provider_payment_id = stripe_payment.id
Expand Down Expand Up @@ -150,14 +145,6 @@ def description
"#{organization.name} - Invoice #{invoice.number}"
end

def invoice_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&.to_sym
end

def update_invoice_payment_status(payment_status:, deliver_webhook: true, processing: false)
result = Invoices::UpdateService.call(
invoice: invoice.presence || @result.invoice,
Expand Down
22 changes: 6 additions & 16 deletions app/services/payment_providers/adyen/payments/create_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@ module PaymentProviders
module Adyen
module Payments
class CreateService < BaseService
PROCESSING_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(payment:)
def initialize(payment:, reference:, metadata:)
@payment = payment
@reference = reference
@metadata = metadata
@invoice = payment.payable
@provider_customer = payment.payment_provider_customer

Expand All @@ -29,7 +27,7 @@ def call

payment.provider_payment_id = adyen_result.response["pspReference"]
payment.status = adyen_result.response["resultCode"]
payment.payable_payment_status = payment_status_mapping(payment.status)
payment.payable_payment_status = payment.payment_provider&.determine_payment_status(payment.status)
payment.save!

result.payment = payment
Expand All @@ -45,7 +43,7 @@ def call

private

attr_reader :payment, :invoice, :provider_customer
attr_reader :payment, :reference, :metadata, :invoice, :provider_customer

delegate :payment_provider, :customer, to: :provider_customer

Expand Down Expand Up @@ -92,7 +90,7 @@ def payment_params
currency: payment.amount_currency.upcase,
value: payment.amount_cents
},
reference: invoice.number,
reference: reference,
paymentMethod: {
type: "scheme",
storedPaymentMethodId: provider_customer.payment_method_id
Expand All @@ -106,14 +104,6 @@ def payment_params
prms
end

def payment_status_mapping(payment_status)
return :processing if PROCESSING_STATUSES.include?(payment_status)
return :succeeded if SUCCESS_STATUSES.include?(payment_status)
return :failed if FAILED_STATUSES.include?(payment_status)

payment_status
end

def prepare_failed_result(error, reraise: false)
result.error_message = error.msg
result.error_code = error.code
Expand Down
4 changes: 2 additions & 2 deletions app/services/payment_providers/create_payment_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

module PaymentProviders
class CreatePaymentFactory
def self.new_instance(provider:, payment:)
service_class(provider:).new(payment:)
def self.new_instance(provider:, payment:, reference:, metadata:)
service_class(provider:).new(payment:, reference:, metadata:)
end

def self.service_class(provider:)
Expand Down
Loading
Loading