Skip to content

Commit

Permalink
fix(stripe): Handle missing payment
Browse files Browse the repository at this point in the history
  • Loading branch information
vincent-pochet committed Nov 15, 2024
1 parent 360e224 commit f7f6101
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 109 deletions.
2 changes: 2 additions & 0 deletions app/models/payment_providers/stripe_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

module PaymentProviders
class StripeProvider < BaseProvider
StripePayment = Data.define(:id, :status, :metadata)

SUCCESS_REDIRECT_URL = 'https://stripe.com/'

# NOTE: find the complete list of event types at https://stripe.com/docs/api/events/types
Expand Down
39 changes: 24 additions & 15 deletions app/services/invoices/payments/stripe_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,19 @@ def call
raise
end

def update_payment_status(organization_id:, provider_payment_id:, status:, metadata: {})
payment = if metadata[:payment_type] == 'one-time'
create_payment(provider_payment_id:, metadata:)
def update_payment_status(organization_id:, status:, stripe_payment:)
payment = if stripe_payment.metadata[:payment_type] == 'one-time'
create_payment(stripe_payment)
else
Payment.find_by(provider_payment_id:)
Payment.find_by(provider_payment_id: stripe_payment.id)
end

unless payment
handle_missing_payment(organization_id, stripe_payment)
return result unless result.payment

payment = result.payment
end
return handle_missing_payment(organization_id, metadata) unless payment

result.payment = payment
result.invoice = payment.payable
Expand Down Expand Up @@ -120,8 +126,8 @@ def generate_payment_url

delegate :organization, :customer, to: :invoice

def create_payment(provider_payment_id:, metadata:)
@invoice = Invoice.find_by(id: metadata[:lago_invoice_id])
def create_payment(stripe_payment, invoice: nil)
@invoice = invoice || Invoice.find_by(id: stripe_payment.metadata[:lago_invoice_id])
unless @invoice
result.not_found_failure!(resource: 'invoice')
return
Expand All @@ -130,12 +136,12 @@ def create_payment(provider_payment_id:, metadata:)
increment_payment_attempts

Payment.new(
payable: invoice,
payable: @invoice,
payment_provider_id: stripe_payment_provider.id,
payment_provider_customer_id: customer.stripe_customer.id,
amount_cents: invoice.total_amount_cents,
amount_currency: invoice.currency&.upcase,
provider_payment_id:
amount_cents: @invoice.total_amount_cents,
amount_currency: @invoice.currency,
provider_payment_id: stripe_payment.id
)
end

Expand Down Expand Up @@ -298,19 +304,22 @@ def deliver_error_webhook(stripe_error)
})
end

def handle_missing_payment(organization_id, metadata)
def handle_missing_payment(organization_id, stripe_payment)
# NOTE: Payment was not initiated by lago
return result unless metadata&.key?(:lago_invoice_id)
return result unless stripe_payment.metadata&.key?(:lago_invoice_id)

# NOTE: Invoice does not belong to this lago organization
# It means the same Stripe secret key is used for multiple organizations
invoice = Invoice.find_by(id: metadata[:lago_invoice_id], organization_id:)
invoice = Invoice.find_by(id: stripe_payment.metadata[:lago_invoice_id], organization_id:)
return result if invoice.nil?

# NOTE: Invoice exists but payment status is failed
return result if invoice.payment_failed?

result.not_found_failure!(resource: 'stripe_payment')
# NOTE: For some reason payment is missing in the database... (killed sidekiq job, etc.)
# We have to recreate it from the received data
result.payment = create_payment(stripe_payment, invoice:)
result
end

# NOTE: Due to RBI limitation, all indians payment should be off_session
Expand Down
14 changes: 10 additions & 4 deletions app/services/payment_providers/stripe_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,12 @@ def handle_event(organization:, event_json:)
payment_service_klass(event)
.new.update_payment_status(
organization_id: organization.id,
provider_payment_id: event.data.object.payment_intent,
status: 'succeeded',
metadata: event.data.object.metadata.to_h.symbolize_keys
stripe_payment: PaymentProviders::StripeProvider::StripePayment.new(
id: event.data.object.payment_intent,
status: event.data.object.status,
metadata: event.data.object.metadata.to_h.symbolize_keys
)
).raise_if_error!
when 'charge.dispute.closed'
PaymentProviders::Webhooks::Stripe::ChargeDisputeClosedService.call(
Expand All @@ -120,9 +123,12 @@ def handle_event(organization:, event_json:)
payment_service_klass(event)
.new.update_payment_status(
organization_id: organization.id,
provider_payment_id: event.data.object.id,
status:,
metadata: event.data.object.metadata.to_h.symbolize_keys
stripe_payment: PaymentProviders::StripeProvider::StripePayment.new(
id: event.data.object.id,
status: event.data.object.status,
metadata: event.data.object.metadata.to_h.symbolize_keys
)
).raise_if_error!
when 'payment_method.detached'
PaymentProviderCustomers::StripeService
Expand Down
42 changes: 25 additions & 17 deletions app/services/payment_requests/payments/stripe_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,20 @@ def generate_payment_url
result.single_validation_failure!(error_code: "payment_provider_error")
end

def update_payment_status(organization_id:, provider_payment_id:, status:, metadata: {})
def update_payment_status(organization_id:, status:, stripe_payment:)
# TODO: do we have one-time payments for payment requests?
payment = if metadata[:payment_type] == "one-time"
create_payment(provider_payment_id:, metadata:)
payment = if stripe_payment.metadata[:payment_type] == "one-time"
create_payment(stripe_payment)
else
Payment.find_by(provider_payment_id:)
Payment.find_by(provider_payment_id: stripe_payment.id)
end

return handle_missing_payment(organization_id, metadata) unless payment
unless payment
handle_missing_payment(organization_id, stripe_payment)
return result unless result.payment

payment = result.payment
end

result.payment = payment
result.payable = payment.payable
Expand Down Expand Up @@ -278,38 +283,41 @@ def payment_url_payload
}
end

def handle_missing_payment(organization_id, metadata)
def handle_missing_payment(organization_id, stripe_payment)
# NOTE: Payment was not initiated by lago
return result unless metadata&.key?(:lago_payable_id)
return result unless stripe_payment.metadata&.key?(:lago_payable_id)

# NOTE: Payment Request does not belong to this lago organization
# It means the same Stripe secret key is used for multiple organizations
payment_request = PaymentRequest.find_by(id: metadata[:lago_payable_id], organization_id:)
payment_request = PaymentRequest.find_by(id: stripe_payment.metadata[:lago_payable_id], organization_id:)
return result unless payment_request

# NOTE: Payment Request exists but payment status is failed
return result if payment_request.payment_failed?

result.not_found_failure!(resource: "stripe_payment")
# NOTE: For some reason payment is missing in the database... (killed sidekiq job, etc.)
# We have to recreate it from the received data
result.payment = create_payment(stripe_payment, payable: payment_request)
result
end

def create_payment(provider_payment_id:, metadata:)
@payable = PaymentRequest.find_by(id: metadata[:lago_payable_id])
def create_payment(stripe_payment, payable: nil)
@payable = payable || PaymentRequest.find_by(id: stripe_payment.metadata[:lago_payable_id])

unless payable
unless @payable
result.not_found_failure!(resource: "payment_request")
return
end

payable.increment_payment_attempts!
@payable.increment_payment_attempts!

Payment.new(
payable:,
payable: @payable,
payment_provider_id: stripe_payment_provider.id,
payment_provider_customer_id: customer.stripe_customer.id,
amount_cents: payable.total_amount_cents,
amount_currency: payable.currency&.upcase,
provider_payment_id:
amount_cents: @payable.total_amount_cents,
amount_currency: @payable.currency,
provider_payment_id: stripe_payment.id
)
end

Expand Down
Loading

0 comments on commit f7f6101

Please sign in to comment.