Skip to content

Commit 420e1df

Browse files
committed
simplify
1 parent d2049d2 commit 420e1df

1 file changed

Lines changed: 63 additions & 144 deletions

File tree

apps/sim/lib/billing/calculations/usage-monitor.ts

Lines changed: 63 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
import { db } from '@sim/db'
2-
import { member, organization, subscription, userStats } from '@sim/db/schema'
2+
import { member, userStats } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { and, eq, inArray } from 'drizzle-orm'
55
import {
66
getHighestPrioritySubscription,
77
type HighestPrioritySubscription,
88
} from '@/lib/billing/core/plan'
9-
import { getOrgUsageLimit, getUserUsageLimit } from '@/lib/billing/core/usage'
9+
import { getUserUsageLimit } from '@/lib/billing/core/usage'
1010
import {
1111
computeDailyRefreshConsumed,
1212
getOrgMemberRefreshBounds,
1313
} from '@/lib/billing/credits/daily-refresh'
1414
import { getPlanTierDollars, isPaid } from '@/lib/billing/plan-helpers'
15-
import {
16-
ENTITLED_SUBSCRIPTION_STATUSES,
17-
isOrgScopedSubscription,
18-
} from '@/lib/billing/subscriptions/utils'
15+
import { isOrgScopedSubscription } from '@/lib/billing/subscriptions/utils'
1916
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
2017
import { toError } from '@/lib/core/utils/helpers'
2118

@@ -39,6 +36,57 @@ interface UsageData {
3936
organizationId: string | null
4037
}
4138

39+
/**
40+
* Sum `currentPeriodCost` across all members of an org, then subtract
41+
* daily-refresh credits (with per-user window bounds for mid-cycle
42+
* joiners).
43+
*/
44+
async function computePooledOrgUsage(
45+
organizationId: string,
46+
sub: {
47+
plan: string | null
48+
seats: number | null
49+
periodStart: Date | null
50+
periodEnd: Date | null
51+
}
52+
): Promise<number> {
53+
const teamMembers = await db
54+
.select({ userId: member.userId })
55+
.from(member)
56+
.where(eq(member.organizationId, organizationId))
57+
58+
if (teamMembers.length === 0) return 0
59+
60+
const memberIds = teamMembers.map((tm) => tm.userId)
61+
const memberStatsRows = await db
62+
.select({ current: userStats.currentPeriodCost, total: userStats.totalCost })
63+
.from(userStats)
64+
.where(inArray(userStats.userId, memberIds))
65+
66+
let pooled = 0
67+
for (const stats of memberStatsRows) {
68+
pooled += Number.parseFloat(stats.current?.toString() || stats.total.toString())
69+
}
70+
71+
if (isPaid(sub.plan) && sub.periodStart) {
72+
const planDollars = getPlanTierDollars(sub.plan)
73+
if (planDollars > 0) {
74+
const userBounds = await getOrgMemberRefreshBounds(organizationId, sub.periodStart)
75+
const refresh = await computeDailyRefreshConsumed({
76+
userIds: memberIds,
77+
periodStart: sub.periodStart,
78+
periodEnd: sub.periodEnd ?? null,
79+
planDollars,
80+
seats: sub.seats || 1,
81+
userBounds: Object.keys(userBounds).length > 0 ? userBounds : undefined,
82+
})
83+
pooled = Math.max(0, pooled - refresh)
84+
}
85+
}
86+
87+
return pooled
88+
}
89+
4290
/**
4391
* Checks a user's cost usage against their subscription plan limit
4492
* and returns usage information including whether they're approaching the limit
@@ -75,47 +123,13 @@ export async function checkUsageStatus(
75123
logger.info('Using stored usage limit', { userId, limit })
76124

77125
const subIsOrgScoped = isOrgScopedSubscription(sub, userId)
126+
const scope: 'user' | 'organization' = subIsOrgScoped ? 'organization' : 'user'
127+
const organizationId: string | null = subIsOrgScoped && sub ? sub.referenceId : null
78128

79129
let currentUsage = 0
80-
let effectiveLimit = limit
81-
let scope: 'user' | 'organization' = subIsOrgScoped ? 'organization' : 'user'
82-
let blockingOrgId: string | null = subIsOrgScoped && sub ? sub.referenceId : null
83130

84131
if (subIsOrgScoped && sub) {
85-
const teamMembers = await db
86-
.select({ userId: member.userId })
87-
.from(member)
88-
.where(eq(member.organizationId, sub.referenceId))
89-
90-
let pooled = 0
91-
if (teamMembers.length > 0) {
92-
const memberIds = teamMembers.map((tm) => tm.userId)
93-
const memberStatsRows = await db
94-
.select({ current: userStats.currentPeriodCost, total: userStats.totalCost })
95-
.from(userStats)
96-
.where(inArray(userStats.userId, memberIds))
97-
98-
for (const stats of memberStatsRows) {
99-
pooled += Number.parseFloat(stats.current?.toString() || stats.total.toString())
100-
}
101-
102-
if (isPaid(sub.plan) && sub.periodStart) {
103-
const planDollars = getPlanTierDollars(sub.plan)
104-
if (planDollars > 0) {
105-
const userBounds = await getOrgMemberRefreshBounds(sub.referenceId, sub.periodStart)
106-
const refresh = await computeDailyRefreshConsumed({
107-
userIds: memberIds,
108-
periodStart: sub.periodStart,
109-
periodEnd: sub.periodEnd ?? null,
110-
planDollars,
111-
seats: sub.seats || 1,
112-
userBounds: Object.keys(userBounds).length > 0 ? userBounds : undefined,
113-
})
114-
pooled = Math.max(0, pooled - refresh)
115-
}
116-
}
117-
}
118-
currentUsage = pooled
132+
currentUsage = await computePooledOrgUsage(sub.referenceId, sub)
119133
} else {
120134
const statsRecords = await db
121135
.select()
@@ -155,124 +169,29 @@ export async function checkUsageStatus(
155169
currentUsage = Math.max(0, rawUsage - refresh)
156170
}
157171

158-
// Defense-in-depth: enforce every entitled org cap the user belongs
159-
// to, even when their priority sub is personal. If a secondary org
160-
// pool is blocking, surface its numbers so the error message does
161-
// not quote personal usage while enforcing an org cap.
162-
try {
163-
const memberships = await db
164-
.select({ organizationId: member.organizationId })
165-
.from(member)
166-
.where(eq(member.userId, userId))
167-
168-
for (const m of memberships) {
169-
// Already handled above as the primary org.
170-
if (subIsOrgScoped && sub && sub.referenceId === m.organizationId) continue
171-
172-
// Refresh math below needs THIS org's plan/period/seats, not
173-
// the caller's primary sub (which may be a personal Pro).
174-
const [orgSub] = await db
175-
.select({
176-
plan: subscription.plan,
177-
seats: subscription.seats,
178-
periodStart: subscription.periodStart,
179-
periodEnd: subscription.periodEnd,
180-
})
181-
.from(subscription)
182-
.where(
183-
and(
184-
eq(subscription.referenceId, m.organizationId),
185-
inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES)
186-
)
187-
)
188-
.limit(1)
189-
if (!orgSub) continue
190-
191-
const [org] = await db
192-
.select({ id: organization.id })
193-
.from(organization)
194-
.where(eq(organization.id, m.organizationId))
195-
.limit(1)
196-
if (!org) continue
197-
198-
// Use the same resolver as primary-path enforcement so the
199-
// `basePrice × seats` floor is applied here too. Reading
200-
// `organization.orgUsageLimit` raw would miss the floor when
201-
// the column is null or has drifted below minimum.
202-
const { limit: orgCap } = await getOrgUsageLimit(org.id, orgSub.plan, orgSub.seats)
203-
204-
const teamMembers = await db
205-
.select({ userId: member.userId })
206-
.from(member)
207-
.where(eq(member.organizationId, org.id))
208-
209-
let pooledUsage = 0
210-
if (teamMembers.length > 0) {
211-
const memberIds = teamMembers.map((tm) => tm.userId)
212-
const allMemberStats = await db
213-
.select({ current: userStats.currentPeriodCost, total: userStats.totalCost })
214-
.from(userStats)
215-
.where(inArray(userStats.userId, memberIds))
216-
217-
for (const stats of allMemberStats) {
218-
pooledUsage += Number.parseFloat(stats.current?.toString() || stats.total.toString())
219-
}
220-
}
221-
222-
if (isPaid(orgSub.plan) && orgSub.periodStart) {
223-
const planDollars = getPlanTierDollars(orgSub.plan)
224-
if (planDollars > 0) {
225-
const memberIds = teamMembers.map((tm) => tm.userId)
226-
const userBounds = await getOrgMemberRefreshBounds(org.id, orgSub.periodStart)
227-
const orgRefreshDeduction = await computeDailyRefreshConsumed({
228-
userIds: memberIds,
229-
periodStart: orgSub.periodStart,
230-
periodEnd: orgSub.periodEnd ?? null,
231-
planDollars,
232-
seats: orgSub.seats || 1,
233-
userBounds: Object.keys(userBounds).length > 0 ? userBounds : undefined,
234-
})
235-
pooledUsage = Math.max(0, pooledUsage - orgRefreshDeduction)
236-
}
237-
}
238-
239-
if (orgCap > 0 && pooledUsage >= orgCap) {
240-
currentUsage = pooledUsage
241-
effectiveLimit = orgCap
242-
scope = 'organization'
243-
blockingOrgId = org.id
244-
break
245-
}
246-
}
247-
} catch (error) {
248-
logger.warn('Error checking organization usage limits', { error, userId })
249-
}
250-
251-
const percentUsed =
252-
effectiveLimit > 0 ? Math.min((currentUsage / effectiveLimit) * 100, 100) : 100
253-
254-
const isExceeded = currentUsage >= effectiveLimit
172+
const percentUsed = limit > 0 ? Math.min((currentUsage / limit) * 100, 100) : 100
173+
const isExceeded = currentUsage >= limit
255174
const isWarning = !isExceeded && percentUsed >= WARNING_THRESHOLD
256175

257176
logger.info('Final usage statistics', {
258177
userId,
259178
currentUsage,
260-
limit: effectiveLimit,
179+
limit,
261180
percentUsed,
262181
isWarning,
263182
isExceeded,
264183
scope,
265-
organizationId: blockingOrgId,
184+
organizationId,
266185
})
267186

268187
return {
269188
percentUsed,
270189
isWarning,
271190
isExceeded,
272191
currentUsage,
273-
limit: effectiveLimit,
192+
limit,
274193
scope,
275-
organizationId: blockingOrgId,
194+
organizationId,
276195
}
277196
} catch (error) {
278197
logger.error('Error checking usage status', {

0 commit comments

Comments
 (0)