({
organizationId,
reason,
}: ReconcileOrganizationSeatsParams)
| 39 | * always correct regardless of interleaving. |
| 40 | */ |
| 41 | export async function reconcileOrganizationSeats({ |
| 42 | organizationId, |
| 43 | reason, |
| 44 | }: ReconcileOrganizationSeatsParams): Promise<ReconcileOrganizationSeatsResult> { |
| 45 | if (!isBillingEnabled) { |
| 46 | return { changed: false, reason: 'Billing is not enabled' } |
| 47 | } |
| 48 | |
| 49 | type ReconcileOutcome = |
| 50 | | { kind: 'skip'; reason: string } |
| 51 | | { kind: 'noop'; seats: number } |
| 52 | | { |
| 53 | kind: 'changed' |
| 54 | previousSeats: number |
| 55 | seats: number |
| 56 | outboxEventId: string |
| 57 | sync: { id: string; plan: string; status: string | null } |
| 58 | } |
| 59 | |
| 60 | const outcome = await db.transaction<ReconcileOutcome>(async (tx) => { |
| 61 | const [orgSubscription] = await tx |
| 62 | .select() |
| 63 | .from(subscription) |
| 64 | .where( |
| 65 | and( |
| 66 | eq(subscription.referenceId, organizationId), |
| 67 | inArray(subscription.status, USABLE_SUBSCRIPTION_STATUSES) |
| 68 | ) |
| 69 | ) |
| 70 | .for('update') |
| 71 | .limit(1) |
| 72 | |
| 73 | if (!orgSubscription) { |
| 74 | return { kind: 'skip', reason: 'No active subscription found' } |
| 75 | } |
| 76 | if (!isTeam(orgSubscription.plan)) { |
| 77 | return { kind: 'skip', reason: 'Seat changes are only available for Team plans' } |
| 78 | } |
| 79 | if (!orgSubscription.stripeSubscriptionId) { |
| 80 | return { kind: 'skip', reason: 'No Stripe subscription found for this organization' } |
| 81 | } |
| 82 | |
| 83 | const [memberCountRow] = await tx |
| 84 | .select({ value: count() }) |
| 85 | .from(member) |
| 86 | .where(eq(member.organizationId, organizationId)) |
| 87 | |
| 88 | const targetSeats = Math.max(1, memberCountRow?.value ?? 1) |
| 89 | const currentSeats = orgSubscription.seats ?? 1 |
| 90 | |
| 91 | if (targetSeats === currentSeats) { |
| 92 | return { kind: 'noop', seats: currentSeats } |
| 93 | } |
| 94 | |
| 95 | await tx |
| 96 | .update(subscription) |
| 97 | .set({ seats: targetSeats }) |
| 98 | .where(eq(subscription.id, orgSubscription.id)) |
no test coverage detected