Skip to content

Commit

Permalink
feat(dunning): Add update_payment_status for stripe payments (#2521)
Browse files Browse the repository at this point in the history
## Roadmap Task

👉

https://getlago.canny.io/feature-requests/p/send-reminders-for-overdue-invoices

 ## Context

We want to be able to manually request payment of the overdue balance
and send emails for reminders.

 ## Description

The goal of this change is to update the status of stripe payments for
payment requests.
  • Loading branch information
ancorcruz authored Sep 2, 2024
1 parent d15d56e commit 86b00a6
Show file tree
Hide file tree
Showing 3 changed files with 293 additions and 21 deletions.
91 changes: 78 additions & 13 deletions app/services/payment_requests/payments/stripe_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ def create
payable_payment_status = payable_payment_status(payment.status)
update_payable_payment_status(
payment_status: payable_payment_status,
processing: payment.status == 'processing'
processing: payment.status == "processing"
)
update_invoices_payment_status(
payment_status: payable_payment_status,
processing: payment.status == 'processing'
processing: payment.status == "processing"
)
end

Expand All @@ -60,7 +60,7 @@ def create
rescue Stripe::AuthenticationError, Stripe::CardError, Stripe::InvalidRequestError, Stripe::PermissionError => e
# NOTE: Do not mark the payable as failed if the amount is too small for Stripe
# For now we keep it as pending.
return result if e.code == 'amount_too_small'
return result if e.code == "amount_too_small"

deliver_error_webhook(e)
update_payable_payment_status(payment_status: :failed, deliver_webhook: false)
Expand All @@ -82,13 +82,39 @@ def generate_payment_url
}
)

result.payment_url = result_url['url']
result.payment_url = result_url["url"]

result
rescue Stripe::CardError, Stripe::InvalidRequestError, Stripe::AuthenticationError, Stripe::PermissionError => e
deliver_error_webhook(e)

result.single_validation_failure!(error_code: 'payment_provider_error')
result.single_validation_failure!(error_code: "payment_provider_error")
end

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

return handle_missing_payment(organization_id, metadata) unless payment

result.payment = payment
result.payable = payment.payable
return result if payment.payable.payment_succeeded?

payment.update!(status:)

processing = status == "processing"
payment_status = payable_payment_status(status)
update_payable_payment_status(payment_status:, processing:)
update_invoices_payment_status(payment_status:, processing:)

result
rescue BaseService::FailedResult => e
result.fail_with_error!(e)
end

private
Expand Down Expand Up @@ -197,15 +223,19 @@ def payable_payment_status(payment_status)
end

def update_payable_payment_status(payment_status:, deliver_webhook: true, processing: false)
payable.update!(
payment_status:,
# NOTE: A proper `processing` payment status should be introduced for payment_requests
ready_for_payment_processing: !processing && payment_status.to_sym != :succeeded
)
UpdateService.call(
payable: result.payable,
params: {
payment_status:,
# NOTE: A proper `processing` payment status should be introduced for payment_requests
ready_for_payment_processing: !processing && payment_status.to_sym != :succeeded
},
webhook_notification: deliver_webhook
).raise_if_error!
end

def update_invoices_payment_status(payment_status:, deliver_webhook: true, processing: false)
payable.invoices.each do |invoice|
result.payable.invoices.each do |invoice|
Invoices::UpdateService.call(
invoice: invoice,
params: {
Expand All @@ -232,7 +262,7 @@ def payment_url_payload
}
}
],
mode: 'payment',
mode: "payment",
success_url: success_redirect_url,
customer: customer.stripe_customer.provider_customer_id,
payment_method_types: customer.stripe_customer.provider_payment_methods,
Expand All @@ -242,12 +272,47 @@ def payment_url_payload
lago_customer_id: customer.id,
lago_payment_request_id: payable.id,
lago_invoice_ids: payable.invoice_ids,
payment_type: 'one-time'
payment_type: "one-time"
}
}
}
end

def handle_missing_payment(organization_id, metadata)
# NOTE: Payment was not initiated by lago
return result unless metadata&.key?(:lago_payment_request_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_payment_request_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")
end

def create_payment(provider_payment_id:, metadata:)
@payable = PaymentRequest.find_by(id: metadata[:lago_payment_request_id])

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

payable.increment_payment_attempts!

Payment.new(
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:
)
end

def deliver_error_webhook(stripe_error)
DeliverErrorWebhookService.call_async(payable, {
provider_customer_id: customer.stripe_customer.provider_customer_id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@
let(:provider_payment_id) { "ch_123456" }

before do
allow(SegmentTrackJob).to receive(:perform_later)
allow(SendWebhookJob).to receive(:perform_later)
payment
end
Expand Down Expand Up @@ -329,10 +330,10 @@
end
end

context "with invalid status", :aggregate_failures do
context "with invalid status" do
let(:status) { "invalid-status" }

it "does not update the payment_status of payment_request, invoice and payment" do
it "does not update the payment_status of payment_request, invoice and payment", :aggregate_failures do
expect {
gocardless_service.update_payment_status(provider_payment_id:, status:)
}.to not_change { payment_request.reload.payment_status }
Expand Down
Loading

0 comments on commit 86b00a6

Please sign in to comment.