(params: {
tx: DbTransaction
userId: string
subscriptionId: string
expiresAt: Date
logger: Logger
})
| 715 | * balance zeroed. |
| 716 | */ |
| 717 | export async function migrateUnusedCredits(params: { |
| 718 | tx: DbTransaction |
| 719 | userId: string |
| 720 | subscriptionId: string |
| 721 | expiresAt: Date |
| 722 | logger: Logger |
| 723 | }): Promise<void> { |
| 724 | const { tx, userId, subscriptionId, expiresAt, logger } = params |
| 725 | const now = new Date() |
| 726 | |
| 727 | const unusedGrants = await tx |
| 728 | .select() |
| 729 | .from(schema.creditLedger) |
| 730 | .where( |
| 731 | and( |
| 732 | eq(schema.creditLedger.user_id, userId), |
| 733 | gt(schema.creditLedger.balance, 0), |
| 734 | ne(schema.creditLedger.type, 'subscription'), |
| 735 | isNull(schema.creditLedger.org_id), |
| 736 | gt(schema.creditLedger.expires_at, now), |
| 737 | lte(schema.creditLedger.expires_at, expiresAt), |
| 738 | ), |
| 739 | ) |
| 740 | |
| 741 | const totalUnused = unusedGrants.reduce( |
| 742 | (sum, grant) => sum + grant.balance, |
| 743 | 0, |
| 744 | ) |
| 745 | |
| 746 | // Deterministic ID ensures idempotency — duplicate webhook deliveries |
| 747 | // will hit onConflictDoNothing and the handleSubscribe caller checks |
| 748 | // for this operation_id before running. |
| 749 | const operationId = `subscribe-migrate-${subscriptionId}` |
| 750 | |
| 751 | if (totalUnused === 0) { |
| 752 | // Still insert the marker for idempotency so handleSubscribe's check |
| 753 | // short-circuits on duplicate webhook deliveries. |
| 754 | await tx |
| 755 | .insert(schema.creditLedger) |
| 756 | .values({ |
| 757 | operation_id: operationId, |
| 758 | user_id: userId, |
| 759 | type: 'free', |
| 760 | principal: 0, |
| 761 | balance: 0, |
| 762 | priority: GRANT_PRIORITIES.free, |
| 763 | expires_at: expiresAt, |
| 764 | description: 'Migrated credits from subscription transition', |
| 765 | }) |
| 766 | .onConflictDoNothing({ target: schema.creditLedger.operation_id }) |
| 767 | logger.debug({ userId }, 'No unused credits to migrate') |
| 768 | return |
| 769 | } |
| 770 | |
| 771 | // Zero out old grants |
| 772 | for (const grant of unusedGrants) { |
| 773 | await tx |
| 774 | .update(schema.creditLedger) |
no test coverage detected