-
Notifications
You must be signed in to change notification settings - Fork 190
feat(stripe): handle payment failures with email, tracking and backoff #1407
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
Changes from all commits
6296da0
41a1146
60e001b
8de2602
9ae245d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -248,6 +248,146 @@ export function generateTrialStartedEmailHtml( | |||||
| `.trim(); | ||||||
| } | ||||||
|
|
||||||
| export interface PaymentFailureDetails { | ||||||
| errorMessage: string; | ||||||
| errorCode?: string; | ||||||
| declineCode?: string; | ||||||
| amount?: number; | ||||||
| currency?: string; | ||||||
| } | ||||||
|
|
||||||
| export function generatePaymentFailureEmailHtml( | ||||||
| organizationName: string, | ||||||
| details: PaymentFailureDetails, | ||||||
| ): string { | ||||||
| const escapedOrgName = escapeHtml(organizationName); | ||||||
| const escapedErrorMessage = escapeHtml(details.errorMessage); | ||||||
|
|
||||||
| // Escape currency and handle zero amount case properly | ||||||
| const escapedCurrency = details.currency | ||||||
| ? escapeHtml(details.currency) | ||||||
| : null; | ||||||
| const formattedAmount = | ||||||
| details.amount !== undefined && details.amount !== null && escapedCurrency | ||||||
| ? `${escapedCurrency} ${details.amount.toFixed(2)}` | ||||||
| : null; | ||||||
|
Comment on lines
+270
to
+273
|
||||||
|
|
||||||
| let actionMessage = "Please update your payment method and try again."; | ||||||
| if (details.declineCode === "insufficient_funds") { | ||||||
| actionMessage = | ||||||
| "Please ensure your card has sufficient funds or use a different payment method."; | ||||||
| } else if ( | ||||||
| details.declineCode === "expired_card" || | ||||||
| details.errorCode === "expired_card" | ||||||
| ) { | ||||||
| actionMessage = "Your card has expired. Please update your payment method."; | ||||||
| } else if ( | ||||||
| details.declineCode === "lost_card" || | ||||||
| details.declineCode === "stolen_card" | ||||||
| ) { | ||||||
| actionMessage = | ||||||
| "This card cannot be used. Please add a different payment method."; | ||||||
| } | ||||||
|
|
||||||
| return ` | ||||||
| <!DOCTYPE html> | ||||||
| <html lang="en"> | ||||||
| <head> | ||||||
| <meta charset="UTF-8"> | ||||||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||||
| <title>Payment Failed - LLMGateway</title> | ||||||
| </head> | ||||||
| <body | ||||||
| style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #ffffff;" | ||||||
| > | ||||||
| <table role="presentation" style="width: 100%; border-collapse: collapse;"> | ||||||
| <tr> | ||||||
| <td align="center" style="padding: 40px 20px;"> | ||||||
| <table role="presentation" style="max-width: 600px; width: 100%; border-collapse: collapse;"> | ||||||
| <!-- Header --> | ||||||
| <tr> | ||||||
| <td | ||||||
| style="background-color: #dc2626; padding: 40px 30px; text-align: center; border-radius: 8px 8px 0 0;" | ||||||
| > | ||||||
| <h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: 600;">Payment Failed</h1> | ||||||
| </td> | ||||||
| </tr> | ||||||
|
|
||||||
| <!-- Main Content --> | ||||||
| <tr> | ||||||
| <td style="background-color: #f8f9fa; padding: 40px 30px; border-radius: 0 0 8px 8px;"> | ||||||
| <p style="margin: 0 0 20px 0; font-size: 16px; line-height: 1.6; color: #333333;"> | ||||||
| Hi there, | ||||||
| </p> | ||||||
|
|
||||||
| <p style="margin: 0 0 20px 0; font-size: 16px; line-height: 1.6; color: #333333;"> | ||||||
| We were unable to process a payment for <strong>${escapedOrgName}</strong>. | ||||||
| </p> | ||||||
|
|
||||||
| <!-- Error Details Box --> | ||||||
| <div | ||||||
| style="background-color: #fef2f2; border: 1px solid #fecaca; border-radius: 6px; padding: 20px; margin-bottom: 20px;" | ||||||
| > | ||||||
| <p style="margin: 0 0 10px 0; font-size: 14px; font-weight: 600; color: #991b1b;"> | ||||||
| Error Details: | ||||||
| </p> | ||||||
| <p style="margin: 0; font-size: 14px; color: #7f1d1d;"> | ||||||
| ${escapedErrorMessage} | ||||||
| </p> | ||||||
| ${formattedAmount ? `<p style="margin: 10px 0 0 0; font-size: 14px; color: #7f1d1d;">Amount: ${formattedAmount}</p>` : ""} | ||||||
|
||||||
| ${formattedAmount ? `<p style="margin: 10px 0 0 0; font-size: 14px; color: #7f1d1d;">Amount: ${formattedAmount}</p>` : ""} | |
| ${formattedAmount ? `<p style="margin: 10px 0 0 0; font-size: 14px; color: #7f1d1d;">Amount: ${escapeHtml(formattedAmount)}</p>` : ""} |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hardcoded year "2025" in the footer copyright notice will become outdated. Consider using dynamic year generation based on the current date to avoid manual updates each year.
| © 2025 LLM Gateway. All rights reserved. This is a transactional email and it can't be unsubscribed from. | |
| © ${new Date().getFullYear()} LLM Gateway. All rights reserved. This is a transactional email and it can't be unsubscribed from. |
Copilot
AI
Jan 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new generatePaymentFailureEmailHtml function lacks test coverage. Since the repository includes comprehensive automated testing for utility functions (as seen in email-validation.spec.ts and invoice.spec.ts), this new email generation function should have corresponding tests to cover various error scenarios including different decline codes, error codes, and edge cases like missing amount/currency values.
Uh oh!
There was an error while loading. Please reload this page.