MCPcopy Index your code
hub / github.com/simstudioai/sim / handleCreditPurchaseSuccess

Function handleCreditPurchaseSuccess

apps/sim/lib/billing/webhooks/invoices.ts:634–774  ·  view source on GitHub ↗

* Handle credit purchase invoice payment succeeded.

(invoice: Stripe.Invoice)

Source from the content-addressed store, hash-verified

632 * Handle credit purchase invoice payment succeeded.
633 */
634async function handleCreditPurchaseSuccess(invoice: Stripe.Invoice): Promise<void> {
635 const { entityType, entityId, amountDollars, purchasedBy } = invoice.metadata || {}
636 if (!entityType || !entityId || !amountDollars) {
637 logger.error('Missing metadata in credit purchase invoice', {
638 invoiceId: invoice.id,
639 metadata: invoice.metadata,
640 })
641 return
642 }
643
644 if (entityType !== 'user' && entityType !== 'organization') {
645 logger.error('Invalid entityType in credit purchase', { invoiceId: invoice.id, entityType })
646 return
647 }
648
649 const amount = Number.parseFloat(amountDollars)
650 if (!Number.isFinite(amount) || amount <= 0) {
651 logger.error('Invalid amount in credit purchase', { invoiceId: invoice.id, amountDollars })
652 return
653 }
654
655 if (!invoice.id) {
656 logger.error('Credit purchase invoice missing id, cannot dedupe', {
657 metadata: invoice.metadata,
658 })
659 return
660 }
661
662 // Idempotent apply: duplicate Stripe deliveries collapse to a single
663 // execution. On exception the key is released (retryFailures: true)
664 // so the next Stripe retry runs from scratch. On success, subsequent
665 // deliveries short-circuit with the cached result.
666 //
667 // CRITICAL: everything after `addCredits` must be either idempotent or
668 // wrapped in try/catch that does not rethrow. Otherwise a failure
669 // after credits commit would release the key and the retry would
670 // double-credit. `setUsageLimitForCredits` and the email are both
671 // best-effort and wrapped; the subscription lookup before them is a
672 // read, safe to rerun.
673 await stripeWebhookIdempotency.executeWithIdempotency('credit-purchase', invoice.id, async () => {
674 await addCredits(entityType, entityId, amount)
675
676 try {
677 const subscription = await db
678 .select()
679 .from(subscriptionTable)
680 .where(
681 and(
682 eq(subscriptionTable.referenceId, entityId),
683 inArray(subscriptionTable.status, ENTITLED_SUBSCRIPTION_STATUSES)
684 )
685 )
686 .limit(1)
687
688 if (subscription.length > 0) {
689 const sub = subscription[0]
690 const newCreditBalance = await getCreditBalanceForEntity(entityType, entityId)
691 await setUsageLimitForCredits(entityType, entityId, sub.plan, sub.seats, newCreditBalance)

Callers 1

Calls 11

addCreditsFunction · 0.90
setUsageLimitForCreditsFunction · 0.90
sendEmailFunction · 0.90
getEmailSubjectFunction · 0.90
isOrgAdminRoleFunction · 0.85
errorMethod · 0.80
infoMethod · 0.80
eqFunction · 0.50

Tested by

no test coverage detected