Skip to content

Commit 40dba91

Browse files
committed
fix(billing): never let payment_failed instrumentation skip user blocking
Address Greptile P1: the payment_failed audit hoisted an unguarded isSubscriptionOrgScoped DB read (for entity_type) directly before the attempt-count user-blocking block. A transient failure of that read would throw out of the handler and skip blocking. Wrap the whole audit/analytics block in try/catch (best-effort), and let the blocking compute its own isSubscriptionOrgScoped as it did originally — instrumentation can no longer abort payment processing.
1 parent 59f1f44 commit 40dba91

1 file changed

Lines changed: 34 additions & 28 deletions

File tree

apps/sim/lib/billing/webhooks/invoices.ts

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -993,35 +993,41 @@ export async function handleInvoicePaymentFailed(event: Stripe.Event) {
993993
resolutionSource,
994994
})
995995

996-
const failureOrgScoped = await isSubscriptionOrgScoped(sub)
997-
const failureEntityType = failureOrgScoped ? 'organization' : 'user'
998-
const failureActorId = await resolveBillingActorId(failureOrgScoped, sub.referenceId)
999-
1000-
recordAudit({
1001-
actorId: failureActorId,
1002-
action: AuditAction.INVOICE_PAYMENT_FAILED,
1003-
resourceType: AuditResourceType.BILLING,
1004-
resourceId: invoice.id,
1005-
description: `Invoice payment of $${failedAmount.toFixed(2)} failed for ${failureEntityType} ${sub.referenceId} (attempt ${attemptCount})`,
1006-
metadata: {
1007-
entityType: failureEntityType,
1008-
referenceId: sub.referenceId,
1009-
plan: sub.plan,
996+
// Best-effort instrumentation; its DB reads must never abort the
997+
// user-blocking that follows, so the whole block is guarded.
998+
try {
999+
const failureOrgScoped = await isSubscriptionOrgScoped(sub)
1000+
const failureEntityType = failureOrgScoped ? 'organization' : 'user'
1001+
const failureActorId = await resolveBillingActorId(failureOrgScoped, sub.referenceId)
1002+
1003+
recordAudit({
1004+
actorId: failureActorId,
1005+
action: AuditAction.INVOICE_PAYMENT_FAILED,
1006+
resourceType: AuditResourceType.BILLING,
1007+
resourceId: invoice.id,
1008+
description: `Invoice payment of $${failedAmount.toFixed(2)} failed for ${failureEntityType} ${sub.referenceId} (attempt ${attemptCount})`,
1009+
metadata: {
1010+
entityType: failureEntityType,
1011+
referenceId: sub.referenceId,
1012+
plan: sub.plan,
1013+
amount: failedAmount,
1014+
currency: invoice.currency ?? 'usd',
1015+
attemptCount,
1016+
invoiceType: invoiceType ?? 'subscription',
1017+
invoiceId: invoice.id,
1018+
},
1019+
})
1020+
captureServerEvent(failureActorId, 'payment_failed', {
1021+
plan: sub.plan ?? 'unknown',
10101022
amount: failedAmount,
10111023
currency: invoice.currency ?? 'usd',
1012-
attemptCount,
1013-
invoiceType: invoiceType ?? 'subscription',
1014-
invoiceId: invoice.id,
1015-
},
1016-
})
1017-
captureServerEvent(failureActorId, 'payment_failed', {
1018-
plan: sub.plan ?? 'unknown',
1019-
amount: failedAmount,
1020-
currency: invoice.currency ?? 'usd',
1021-
entity_type: failureEntityType,
1022-
reference_id: sub.referenceId,
1023-
attempt_count: attemptCount,
1024-
})
1024+
entity_type: failureEntityType,
1025+
reference_id: sub.referenceId,
1026+
attempt_count: attemptCount,
1027+
})
1028+
} catch (auditError) {
1029+
logger.warn('Failed to record payment_failed instrumentation', { auditError })
1030+
}
10251031

10261032
if (attemptCount >= 1) {
10271033
logger.error('Payment failure - blocking users', {
@@ -1033,7 +1039,7 @@ export async function handleInvoicePaymentFailed(event: Stripe.Event) {
10331039
stripeSubscriptionId,
10341040
})
10351041

1036-
if (failureOrgScoped) {
1042+
if (await isSubscriptionOrgScoped(sub)) {
10371043
const memberCount = await blockOrgMembers(sub.referenceId, 'payment_failed')
10381044
logger.info('Blocked org members due to payment failure', {
10391045
invoiceType: invoiceType ?? 'subscription',

0 commit comments

Comments
 (0)