* Handle credit purchase invoice payment succeeded.
(invoice: Stripe.Invoice)
| 632 | * Handle credit purchase invoice payment succeeded. |
| 633 | */ |
| 634 | async 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) |
no test coverage detected