252 stripe hardening#253
Conversation
- Add webhook event router for routing Stripe events to handlers - Add checkout handlers (extracted from webhooks.js) - Add subscription handlers for lifecycle events - Add invoice handlers for payment tracking and dunning - Add payment intent handlers for async payment tracking - Add customer handlers for data sync - Add subscription status mapping utility - Integrate router into main webhook endpoint - Add event ID deduplication for authoritative idempotency All 76 billing tests pass. Phase 1-2 of stripe hardening plan complete.
- Create dunning.js with queueDunningEmail function - Integrate dunning into handleInvoicePaymentFailed handler - Escalating email templates based on attempt count (1-3) - HTML and plain text email templates with urgency indicators - Update plan to document EmailQueue DO usage - Export dunning from handlers barrel Uses existing EmailQueue Durable Object for reliable delivery with: - Automatic retry with exponential backoff - Dead letter queue for failed emails - Postmark integration All 76 billing tests pass.
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the 📝 WalkthroughWalkthroughIntroduces modular Stripe webhook handlers for checkout, subscriptions, invoices, payment intents, and customer events with deduplication and ledger tracking. Enhances admin database viewer with schema-aware filtering, foreign key navigation, and expanded API responses. Adds comprehensive test coverage and supporting utilities including dunning email infrastructure and subscription status mapping. Changes
Sequence DiagramssequenceDiagram
participant Client as Stripe
participant Router as WebhookRouter
participant Handler as Event Handler
participant DB as Database
participant Logger as Logger
Client->>Router: POST webhook event
Router->>Router: Verify signature
Router->>DB: Check dedupe by eventId
alt Event already processed
DB-->>Router: Ledger exists
Router-->>Client: 200 SKIPPED_DUPLICATE
else New event
Router->>Handler: routeStripeEvent(event, ctx)
Handler->>DB: Query/Update subscription/invoice
Handler->>Logger: Log event action
DB-->>Handler: Operation result
Logger-->>Handler: Acknowledged
Handler-->>Router: {handled, result, ledgerContext}
Router->>DB: Insert/update stripe_event_ledger
Router-->>Client: 200 webhook_processed
end
sequenceDiagram
participant Invoice as Invoice Handler
participant DB as Database
participant User as User Lookup
participant Queue as EmailQueue DO
participant Email as Email Service
Invoice->>DB: Find subscription by ID
alt Payment failed
DB-->>Invoice: subscription found, past_due
Invoice->>User: Resolve user by customerId
User-->>Invoice: user found
Invoice->>Queue: queueDunningEmail(params, ctx)
Queue->>Queue: Select template by attemptCount
Queue->>Queue: Build HTML + text bodies
Queue->>Email: POST enqueue payload
Email-->>Queue: success response
Queue-->>Invoice: result.success
Invoice->>Logger: Log dunning_email_queued
end
sequenceDiagram
participant UI as DatabaseViewer UI
participant Query as useAdminTableRows Hook
participant API as Admin Database Route
participant Schema as Schema Cache
participant DB as Database
UI->>UI: User clicks FK cell
UI->>UI: navigateToForeignKey(table, column)
UI->>Query: Call hook with filterBy/filterValue
Query->>API: GET /admin/tables/{table}/rows?filterBy=column&filterValue=x
API->>Schema: useAdminTableSchema(table)
Schema->>DB: Fetch table schema with FK metadata
DB-->>Schema: Schema + foreignKey references
Schema-->>API: columnSchemaMap
API->>DB: Query rows filtered by column=value
DB-->>API: Filtered result set
API-->>Query: Rows + schema metadata
Query-->>UI: Update state + re-render
UI->>UI: Display filtered rows with FK indicators
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
corates | 480b66c | Commit Preview URL | Jan 08 2026, 08:58 PM |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In @packages/docs/audits/flowglad-polar-extraction-analysis-2026-01.md:
- Around line 446-451: Remove the duplicate markdown heading by deleting the
second occurrence of "## 2. Stripe Implementation Hardening" (the duplicate
heading text) so only a single "## 2. Stripe Implementation Hardening" remains
in the document; ensure surrounding content and section numbering remain intact
after removal.
In @packages/workers/src/routes/billing/__tests__/invoiceHandlers.test.js:
- Around line 30-45: Replace the local seedSubscription helper in
invoiceHandlers.test.js with the shared seedSubscription helper from the test
helpers module; remove the ad-hoc function and import seedSubscription from the
shared helpers used across tests, and update all test calls to pass the helper's
expected schema (use Unix-second timestamps for createdAt/updatedAt, include
stripeSubscriptionId, stripeCustomerId, referenceId, status, etc.) so the Zod
validation in the shared helper accepts the fixtures (e.g., compute nowSec =
Math.floor(Date.now()/1000) and pass createdAt: nowSec, updatedAt: nowSec).
In @packages/workers/src/routes/billing/handlers/dunning.js:
- Around line 126-179: The HTML email in buildDunningEmailHtml interpolates
userName, amount, and invoiceUrl directly which can lead to XSS; add an
escapeHtml helper that replaces &, <, >, and " (and returns empty string for
null/undefined) and use it when inserting userName and amount into the template
(e.g., ${escapeHtml(userName || 'there')} and ${escapeHtml(String(amount))});
also sanitize invoiceUrl for the href by escaping it and optionally
validating/normalizing it to an allowed protocol (e.g., only http/https) before
embedding to avoid javascript: URLs.
In @packages/workers/src/routes/billing/handlers/invoiceHandlers.js:
- Around line 95-96: The function handleInvoicePaymentFailed destructures _env
from ctx but later uses ctx.env, creating an inconsistent unused variable;
change the destructuring to use env (const { db, logger, env } = ctx) or stop
destructuring and consistently use ctx.env, and update any references inside
handleInvoicePaymentFailed to use the chosen form so _env is not left unused and
env access is consistent.
🧹 Nitpick comments (14)
packages/workers/src/routes/admin/database.js (1)
143-173: LGTM! Safe FK detection with appropriate fallback.The foreign key detection logic correctly:
- Extracts FK references from Drizzle column config
- Validates referenced tables against
ALLOWED_TABLESto prevent exposure- Uses try-catch to gracefully skip FK info on resolution errors
The implementation safely handles edge cases without breaking the endpoint response.
💡 Optional: Add debug logging for FK resolution failures
Consider logging FK resolution errors in development to aid schema debugging:
try { const refColumn = refFn(); if (refColumn?.table) { const refTableName = Object.entries(dbSchema).find(([, t]) => t === refColumn.table)?.[0]; if (refTableName && ALLOWED_TABLES.includes(refTableName)) { columnInfo.foreignKey = { table: refTableName, column: refColumn.name, }; } } - } catch { + } catch (err) { + // FK resolution failed, skip FK info + console.debug(`Failed to resolve FK for ${name}:`, err.message); - // Reference function failed, skip FK info }packages/workers/src/routes/billing/__tests__/subscriptionHandlers.test.js (1)
31-46: Consider extractingseedSubscriptionto shared test helpers.The local
seedSubscriptionhelper works correctly, but based on the coding guidelines, test data seeding should use providedseed*helpers frompackages/workers/src/__tests__/helpers.jsfor consistency across test files. IfseedSubscriptiondoesn't exist in helpers.js, consider adding it there to enable reuse in other billing-related tests.packages/workers/src/routes/billing/handlers/checkoutHandlers.js (2)
116-118:setMonth()edge case may cause unexpected date calculations.JavaScript's
setMonth()can produce unexpected results at month boundaries. For example, adding 6 months to August 31 results in February 31, which rolls over to March 2 or 3.Consider using a library like
date-fnsor calculating with milliseconds for predictable results:♻️ Safer date calculation
- const newExpiresAt = new Date(baseExpiresAt * 1000); - newExpiresAt.setMonth(newExpiresAt.getMonth() + 6); + // Add 6 months worth of days (approximately 182 days) for predictable behavior + const SIX_MONTHS_MS = 182 * 24 * 60 * 60 * 1000; + const newExpiresAt = new Date(baseExpiresAt * 1000 + SIX_MONTHS_MS);
154-158: Consider handlingpayment_intentas potentially an object.Similar to how
session.customeris handled (lines 63-64),session.payment_intentmay be either a string ID or an expanded object depending on Stripe API settings.♻️ Consistent ID extraction
metadata: { purchaserUserId, stripeCheckoutSessionId: session.id, - stripePaymentIntentId: session.payment_intent, + stripePaymentIntentId: + typeof session.payment_intent === 'string' + ? session.payment_intent + : session.payment_intent?.id, },packages/workers/src/routes/billing/handlers/customerHandlers.js (1)
68-82: Consider wrapping database operations in try-catch.Per coding guidelines, database operations should be wrapped in try-catch blocks and return domain errors using
createDomainErrorwithSYSTEM_ERRORS.DB_ERROR. If error handling is centralized in the webhook processor, this may be acceptable, but explicit error handling here would make the handlers more robust.♻️ Add error handling example
+import { createDomainError, SYSTEM_ERRORS } from '@corates/shared'; if (hasChanges) { + try { await db .update(user) .set({ ...updates, updatedAt: new Date(), }) .where(eq(user.id, existingUser.id)); + } catch (error) { + logger.error('customer_update_db_error', { userId: existingUser.id, error: error.message }); + return { + handled: false, + result: 'db_error', + error: createDomainError(SYSTEM_ERRORS.DB_ERROR, { cause: error }), + }; + }packages/workers/src/routes/billing/webhooks.js (2)
257-264: Verify ledger status mapping for unhandled events.When
handlerResult.handledis false, the status is set toPROCESSED(Line 258). This seems intentional for "event_type_not_handled" cases, but consider whetherIGNORED_UNHANDLEDor similar would better distinguish truly processed events from those the system doesn't handle.
288-297: Handler errors mapped to VALIDATION_ERRORS.INVALID_INPUT may be misleading.When a handler returns an error (e.g., database failure), mapping it to
VALIDATION_ERRORS.INVALID_INPUTdoesn't accurately represent the error type. Consider usingSYSTEM_ERRORS.INTERNAL_ERRORor introducing handler-specific error mapping.Suggested approach
// Return error response if handler indicated failure if (handlerResult.error) { - // Map handler errors to domain errors for consistent API responses - const errorDetails = { - field: handlerResult.result, - value: handlerResult.error, - }; - const domainError = createDomainError(VALIDATION_ERRORS.INVALID_INPUT, errorDetails); + // Handler failures are typically internal/system errors, not validation errors + const domainError = createDomainError(SYSTEM_ERRORS.INTERNAL_ERROR, { + operation: handlerResult.result, + originalError: handlerResult.error, + }); return c.json(domainError, domainError.statusCode); }packages/workers/src/routes/billing/handlers/dunning.js (2)
23-43: Validate additional required parameters.The function only validates
userEmail, butinvoiceUrlis critical for the email to be useful. IfinvoiceUrlis missing, the email will have an empty/broken link.Suggested validation
if (!userEmail) { logger.stripe('dunning_email_skipped_no_email', { subscriptionId, orgId, attemptCount, }); return false; } + + if (!invoiceUrl) { + logger.stripe('dunning_email_skipped_no_invoice_url', { + subscriptionId, + orgId, + attemptCount, + }); + return false; + }
88-120: Consider adding a timeout to the DO fetch call.The EmailQueue DO fetch has no timeout, which could cause the webhook handler to hang if the DO is unresponsive. Cloudflare Workers have execution time limits, so a long-running fetch could cause the entire webhook to fail.
Suggested approach with AbortController
try { // Queue via EmailQueue DO const queueId = env.EMAIL_QUEUE.idFromName('default'); const queue = env.EMAIL_QUEUE.get(queueId); + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + const response = await queue.fetch( new Request('https://internal/enqueue', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(emailPayload), + signal: controller.signal, }), ); + + clearTimeout(timeoutId);packages/workers/src/routes/billing/handlers/subscriptionStatus.js (1)
107-109: Consider whetherpast_dueshould be inisActiveSubscription.Including
past_dueinisActiveSubscriptionis a business decision - technically the subscription is still billable but payment has failed. The name might imply only "healthy" states. If intentional, a brief comment would clarify.Optional clarifying comment
/** * Check if a subscription status indicates active billing + * Note: Includes past_due since subscription is still active (just with failed payment) * @param {string} status - Stripe subscription status * @returns {boolean} */ export function isActiveSubscription(status) { return ['active', 'trialing', 'past_due'].includes(status); }packages/workers/src/routes/billing/handlers/subscriptionHandlers.js (4)
234-240: Missing logging when subscription not found in pause handler.Unlike
handleSubscriptionDeleted(Line 181-183) which logs when subscription isn't found,handleSubscriptionPausedsilently returns. For consistency and debugging, add logging.Suggested fix
if (!existing) { + logger.stripe('subscription_not_found_for_pause', { + stripeSubscriptionId: sub.id, + }); return { handled: true, result: 'subscription_not_found', ledgerContext: { stripeSubscriptionId: sub.id }, }; }
281-287: Missing logging when subscription not found in resume handler.Same issue as the pause handler - add logging for consistency.
Suggested fix
if (!existing) { + logger.stripe('subscription_not_found_for_resume', { + stripeSubscriptionId: sub.id, + }); return { handled: true, result: 'subscription_not_found', ledgerContext: { stripeSubscriptionId: sub.id }, }; }
256-263: Inconsistent ledgerContext: missing stripeCustomerId in pause handler.Other handlers include
stripeCustomerIdin the ledgerContext (see Lines 86, 159, 213), buthandleSubscriptionPausedomits it. For consistency and traceability, include it.Suggested fix
return { handled: true, result: 'paused', ledgerContext: { stripeSubscriptionId: sub.id, + stripeCustomerId: typeof sub.customer === 'string' ? sub.customer : sub.customer?.id, orgId: existing.referenceId, }, };
306-313: Inconsistent ledgerContext: missing stripeCustomerId in resume handler.Same issue as the pause handler - add
stripeCustomerIdfor consistency.Suggested fix
return { handled: true, result: 'resumed', ledgerContext: { stripeSubscriptionId: sub.id, + stripeCustomerId: typeof sub.customer === 'string' ? sub.customer : sub.customer?.id, orgId: existing.referenceId, }, };
| ## 2. Stripe Implementation Hardening | ||
|
|
||
| --- | ||
|
|
||
| ## 2. Stripe Implementation Hardening | ||
|
|
There was a problem hiding this comment.
Duplicate heading detected.
Lines 446-451 contain a duplicate "## 2. Stripe Implementation Hardening" heading. Remove the duplicate at line 450 to fix the markdown structure.
📝 Suggested fix
---
## 2. Stripe Implementation Hardening
----
-
-## 2. Stripe Implementation Hardening
### 2.1 Current CoRATES Implementation Analysis📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ## 2. Stripe Implementation Hardening | |
| --- | |
| ## 2. Stripe Implementation Hardening | |
| ## 2. Stripe Implementation Hardening | |
| --- | |
| ### 2.1 Current CoRATES Implementation Analysis |
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
450-450: Multiple headings with the same content
(MD024, no-duplicate-heading)
🤖 Prompt for AI Agents
In @packages/docs/audits/flowglad-polar-extraction-analysis-2026-01.md around
lines 446 - 451, Remove the duplicate markdown heading by deleting the second
occurrence of "## 2. Stripe Implementation Hardening" (the duplicate heading
text) so only a single "## 2. Stripe Implementation Hardening" remains in the
document; ensure surrounding content and section numbering remain intact after
removal.
| // Helper to seed a subscription | ||
| async function seedSubscription(db, data) { | ||
| await db.insert(subscription).values({ | ||
| id: data.id, | ||
| plan: data.plan || 'team', | ||
| referenceId: data.referenceId || 'org-1', | ||
| stripeCustomerId: data.stripeCustomerId || 'cus_123', | ||
| stripeSubscriptionId: data.stripeSubscriptionId, | ||
| status: data.status || 'active', | ||
| periodStart: data.periodStart || new Date(), | ||
| periodEnd: data.periodEnd || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), | ||
| cancelAtPeriodEnd: data.cancelAtPeriodEnd || false, | ||
| createdAt: new Date(), | ||
| updatedAt: new Date(), | ||
| }); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Use shared seedSubscription helper instead of local implementation.
Per coding guidelines, tests should "Reuse provided helpers from packages/workers/src/__tests__/helpers.js... instead of implementing ad-hoc solutions." The shared seedSubscription helper (shown in relevant_code_snippets) includes Zod schema validation and consistent date handling.
♻️ Use shared helper
-import { resetTestDatabase, seedUser } from '@/__tests__/helpers.js';
+import { resetTestDatabase, seedUser, seedSubscription } from '@/__tests__/helpers.js';
-// Helper to seed a subscription
-async function seedSubscription(db, data) {
- await db.insert(subscription).values({
- id: data.id,
- plan: data.plan || 'team',
- referenceId: data.referenceId || 'org-1',
- stripeCustomerId: data.stripeCustomerId || 'cus_123',
- stripeSubscriptionId: data.stripeSubscriptionId,
- status: data.status || 'active',
- periodStart: data.periodStart || new Date(),
- periodEnd: data.periodEnd || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
- cancelAtPeriodEnd: data.cancelAtPeriodEnd || false,
- createdAt: new Date(),
- updatedAt: new Date(),
- });
-}Then update calls to use the helper's expected schema (with Unix timestamps):
const nowSec = Math.floor(Date.now() / 1000);
await seedSubscription({
id: 'sub-local-1',
stripeSubscriptionId: 'sub_123',
status: 'past_due',
referenceId: 'org-1',
stripeCustomerId: 'cus_123',
createdAt: nowSec,
updatedAt: nowSec,
});Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In @packages/workers/src/routes/billing/__tests__/invoiceHandlers.test.js around
lines 30 - 45, Replace the local seedSubscription helper in
invoiceHandlers.test.js with the shared seedSubscription helper from the test
helpers module; remove the ad-hoc function and import seedSubscription from the
shared helpers used across tests, and update all test calls to pass the helper's
expected schema (use Unix-second timestamps for createdAt/updatedAt, include
stripeSubscriptionId, stripeCustomerId, referenceId, status, etc.) so the Zod
validation in the shared helper accepts the fixtures (e.g., compute nowSec =
Math.floor(Date.now()/1000) and pass createdAt: nowSec, updatedAt: nowSec).
| function buildDunningEmailHtml({ userName, amount, invoiceUrl, attemptCount, urgency }) { | ||
| const urgencyColors = { | ||
| low: '#f59e0b', // amber | ||
| medium: '#f97316', // orange | ||
| high: '#ef4444', // red | ||
| }; | ||
|
|
||
| const color = urgencyColors[urgency] || urgencyColors.medium; | ||
|
|
||
| const finalNotice = | ||
| attemptCount >= 3 ? | ||
| ` | ||
| <div style="background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px; padding: 16px; margin: 20px 0;"> | ||
| <p style="margin: 0; color: #991b1b;"><strong>Final Notice:</strong> Your subscription will be canceled if payment is not received.</p> | ||
| </div> | ||
| ` | ||
| : ''; | ||
|
|
||
| return ` | ||
| <!DOCTYPE html> | ||
| <html> | ||
| <head> | ||
| <meta charset="utf-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
| </head> | ||
| <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #1f2937; max-width: 600px; margin: 0 auto; padding: 20px;"> | ||
| <div style="border-left: 4px solid ${color}; padding-left: 16px; margin-bottom: 24px;"> | ||
| <h1 style="color: ${color}; margin: 0 0 8px 0; font-size: 20px;">Payment Failed</h1> | ||
| <p style="margin: 0; color: #6b7280;">Attempt ${attemptCount} of 3</p> | ||
| </div> | ||
|
|
||
| <p>Hi ${userName || 'there'},</p> | ||
|
|
||
| <p>We were unable to process your payment of <strong>${amount}</strong> for your CoRATES subscription.</p> | ||
|
|
||
| ${finalNotice} | ||
|
|
||
| <p>Please update your payment method to continue your subscription:</p> | ||
|
|
||
| <a href="${invoiceUrl}" style="display: inline-block; background: #2563eb; color: white; padding: 12px 24px; border-radius: 6px; text-decoration: none; font-weight: 500; margin: 16px 0;">Update Payment Method</a> | ||
|
|
||
| <p style="color: #6b7280; font-size: 14px; margin-top: 32px;"> | ||
| If you have any questions, please contact our support team. | ||
| </p> | ||
|
|
||
| <hr style="border: none; border-top: 1px solid #e5e7eb; margin: 32px 0;"> | ||
|
|
||
| <p style="color: #9ca3af; font-size: 12px;"> | ||
| CoRATES - Collaborative Research Appraisal Tool for Evidence Synthesis | ||
| </p> | ||
| </body> | ||
| </html> | ||
| `.trim(); | ||
| } |
There was a problem hiding this comment.
Potential XSS in HTML email template.
The userName, amount, and invoiceUrl values are interpolated directly into HTML without escaping. While email clients typically sandbox content, some may render JavaScript in certain contexts. Consider escaping HTML entities.
Suggested helper
function escapeHtml(str) {
if (!str) return '';
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
// Then use: ${escapeHtml(userName || 'there')}🤖 Prompt for AI Agents
In @packages/workers/src/routes/billing/handlers/dunning.js around lines 126 -
179, The HTML email in buildDunningEmailHtml interpolates userName, amount, and
invoiceUrl directly which can lead to XSS; add an escapeHtml helper that
replaces &, <, >, and " (and returns empty string for null/undefined) and use it
when inserting userName and amount into the template (e.g.,
${escapeHtml(userName || 'there')} and ${escapeHtml(String(amount))}); also
sanitize invoiceUrl for the href by escaping it and optionally
validating/normalizing it to an allowed protocol (e.g., only http/https) before
embedding to avoid javascript: URLs.
| export async function handleInvoicePaymentFailed(invoice, ctx) { | ||
| const { db, logger, _env } = ctx; |
There was a problem hiding this comment.
Typo: _env destructured but ctx.env used.
Line 96 destructures _env (unused variable pattern), but Line 167 accesses ctx.env. This works but is inconsistent. Either use env consistently or access via ctx.env throughout.
Suggested fix
export async function handleInvoicePaymentFailed(invoice, ctx) {
- const { db, logger, _env } = ctx;
+ const { db, logger, env } = ctx;Then update Line 167:
- if (stripeCustomerId && ctx.env?.EMAIL_QUEUE) {
+ if (stripeCustomerId && env?.EMAIL_QUEUE) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function handleInvoicePaymentFailed(invoice, ctx) { | |
| const { db, logger, _env } = ctx; | |
| export async function handleInvoicePaymentFailed(invoice, ctx) { | |
| const { db, logger, env } = ctx; | |
| // ... rest of function body ... | |
| // At line 167, the following change would also be applied: | |
| if (stripeCustomerId && env?.EMAIL_QUEUE) { | |
| // ... email queue logic ... | |
| } |
🤖 Prompt for AI Agents
In @packages/workers/src/routes/billing/handlers/invoiceHandlers.js around lines
95 - 96, The function handleInvoicePaymentFailed destructures _env from ctx but
later uses ctx.env, creating an inconsistent unused variable; change the
destructuring to use env (const { db, logger, env } = ctx) or stop destructuring
and consistently use ctx.env, and update any references inside
handleInvoicePaymentFailed to use the chosen form so _env is not left unused and
env access is consistent.
| // Extract failure details | ||
| const failureReason = | ||
| invoice.last_finalization_error?.message || | ||
| invoice.status_transitions?.finalized_at || | ||
| 'unknown'; |
There was a problem hiding this comment.
Unclear failure reason extraction logic.
The fallback chain uses status_transitions?.finalized_at as a failure reason, which is a timestamp, not a reason string. This could produce confusing log entries.
Suggested fix
// Extract failure details
const failureReason =
invoice.last_finalization_error?.message ||
- invoice.status_transitions?.finalized_at ||
+ invoice.last_payment_error?.message ||
'unknown';
Summary by CodeRabbit
New Features
Tests
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.