Skip to content

Commit b1a9443

Browse files
improvement(billing): move overage calculations out of txes (#4595)
* improvement(billing): move calc subscription overage out of tx * fix double billing risk * address comments * address comments * share timeout const
1 parent b5dba82 commit b1a9443

6 files changed

Lines changed: 979 additions & 184 deletions

File tree

apps/sim/lib/billing/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ export const SEARCH_TOOL_COST = 0.01
3434
*/
3535
export const DEFAULT_OVERAGE_THRESHOLD = 100
3636

37+
/**
38+
* Maximum time to wait on billing coordination row locks before retrying later.
39+
*/
40+
export const BILLING_LOCK_TIMEOUT_MS = 5_000
41+
3742
/**
3843
* Available credit tiers. Each tier maps a credit amount to the underlying dollar cost.
3944
* 1 credit = $0.005, so credits = dollars * 200.

apps/sim/lib/billing/organizations/membership.ts

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -926,34 +926,6 @@ export async function removeUserFromOrganization(
926926
)
927927
}
928928

929-
let capturedUsage = 0
930-
if (!skipBillingLogic) {
931-
const [departingUserStats] = await tx
932-
.select({ currentPeriodCost: userStats.currentPeriodCost })
933-
.from(userStats)
934-
.where(eq(userStats.userId, userId))
935-
.limit(1)
936-
937-
if (departingUserStats?.currentPeriodCost) {
938-
const usage = toNumber(toDecimal(departingUserStats.currentPeriodCost))
939-
if (usage > 0) {
940-
await tx
941-
.update(organization)
942-
.set({
943-
departedMemberUsage: sql`${organization.departedMemberUsage} + ${usage}`,
944-
})
945-
.where(eq(organization.id, organizationId))
946-
947-
await tx
948-
.update(userStats)
949-
.set({ currentPeriodCost: '0' })
950-
.where(eq(userStats.userId, userId))
951-
952-
capturedUsage = usage
953-
}
954-
}
955-
}
956-
957929
const [targetUser] = await tx
958930
.select({ email: user.email })
959931
.from(user)
@@ -979,7 +951,44 @@ export async function removeUserFromOrganization(
979951
.from(workspace)
980952
.where(eq(workspace.organizationId, organizationId))
981953

954+
const captureDepartedUsage = async () => {
955+
if (skipBillingLogic) return 0
956+
957+
await tx
958+
.select({ id: organization.id })
959+
.from(organization)
960+
.where(eq(organization.id, organizationId))
961+
.for('update')
962+
.limit(1)
963+
964+
const [departingUserStats] = await tx
965+
.select({ currentPeriodCost: userStats.currentPeriodCost })
966+
.from(userStats)
967+
.where(eq(userStats.userId, userId))
968+
.for('update')
969+
.limit(1)
970+
971+
const usage = toNumber(toDecimal(departingUserStats?.currentPeriodCost))
972+
if (usage <= 0) return 0
973+
974+
await tx
975+
.update(organization)
976+
.set({
977+
departedMemberUsage: sql`${organization.departedMemberUsage} + ${usage}`,
978+
})
979+
.where(eq(organization.id, organizationId))
980+
981+
await tx
982+
.update(userStats)
983+
.set({ currentPeriodCost: '0' })
984+
.where(eq(userStats.userId, userId))
985+
986+
return usage
987+
}
988+
982989
if (orgWorkspaces.length === 0) {
990+
const capturedUsage = await captureDepartedUsage()
991+
983992
return {
984993
workspaceIdsToRevoke: [] as string[],
985994
usageCaptured: capturedUsage,
@@ -1022,6 +1031,7 @@ export async function removeUserFromOrganization(
10221031
workspaceIds,
10231032
userId,
10241033
})
1034+
const capturedUsage = await captureDepartedUsage()
10251035

10261036
return {
10271037
workspaceIdsToRevoke: deletedPerms.map((row) => row.entityId),

0 commit comments

Comments
 (0)