Skip to content

Commit c4f66fa

Browse files
committed
address comments
1 parent b7d2777 commit c4f66fa

2 files changed

Lines changed: 72 additions & 10 deletions

File tree

apps/sim/lib/billing/cleanup-dispatcher.ts

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import { db } from '@sim/db'
2+
import type { WorkspaceMode } from '@sim/db/schema'
23
import { organization, workspace } from '@sim/db/schema'
34
import { createLogger } from '@sim/logger'
45
import { tasks } from '@trigger.dev/sdk'
56
import { eq, isNull } from 'drizzle-orm'
7+
import { getOrganizationSubscription } from '@/lib/billing/core/billing'
68
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
79
import { getPlanType, type PlanCategory } from '@/lib/billing/plan-helpers'
810
import { getJobQueue } from '@/lib/core/async-jobs'
911
import { shouldExecuteInline } from '@/lib/core/async-jobs/config'
1012
import type { EnqueueOptions } from '@/lib/core/async-jobs/types'
1113
import { isTriggerAvailable } from '@/lib/knowledge/documents/service'
14+
import { isOrganizationWorkspace } from '@/lib/workspaces/policy'
1215

1316
const logger = createLogger('RetentionDispatcher')
1417

@@ -41,11 +44,15 @@ interface CleanupJobConfig {
4144
interface WorkspaceCleanupScopeRow {
4245
id: string
4346
billedAccountUserId: string
47+
organizationId: string | null
48+
workspaceMode: WorkspaceMode
4449
organizationSettings: OrganizationRetentionSettings | null
4550
}
4651

4752
const DAY = 24
4853

54+
type PlanResolutionEntry = readonly [string, PlanCategory]
55+
4956
/**
5057
* Single source of truth for cleanup retention: which key each job type reads
5158
* from `organization.dataRetentionSettings`, and the default retention (in
@@ -72,6 +79,8 @@ async function listActiveWorkspaceCleanupScopeRows(): Promise<WorkspaceCleanupSc
7279
.select({
7380
id: workspace.id,
7481
billedAccountUserId: workspace.billedAccountUserId,
82+
organizationId: workspace.organizationId,
83+
workspaceMode: workspace.workspaceMode,
7584
organizationSettings: organization.dataRetentionSettings,
7685
})
7786
.from(workspace)
@@ -91,12 +100,56 @@ async function resolvePlanTypesByBilledUserId(
91100
const billedUserIds = Array.from(new Set(rows.map((row) => row.billedAccountUserId)))
92101
const entries = await Promise.all(
93102
billedUserIds.map(async (userId) => {
94-
const subscription = await getHighestPrioritySubscription(userId, { onError: 'throw' })
95-
return [userId, getPlanType(subscription?.plan)] as const
103+
try {
104+
const subscription = await getHighestPrioritySubscription(userId, { onError: 'throw' })
105+
return [userId, getPlanType(subscription?.plan)] as const
106+
} catch (error) {
107+
logger.error('Skipping cleanup for billed user after plan lookup failed', {
108+
userId,
109+
error,
110+
})
111+
return null
112+
}
113+
})
114+
)
115+
116+
return new Map(entries.filter((entry): entry is PlanResolutionEntry => entry !== null))
117+
}
118+
119+
async function resolvePlanTypesByWorkspaceId(
120+
rows: WorkspaceCleanupScopeRow[]
121+
): Promise<Map<string, PlanCategory>> {
122+
const userScopedRows = rows.filter((row) => !isOrganizationWorkspace(row))
123+
const userPlanByBilledUserId = await resolvePlanTypesByBilledUserId(userScopedRows)
124+
const entries = await Promise.all(
125+
rows.map(async (row) => {
126+
const organizationId = isOrganizationWorkspace(row) ? row.organizationId : null
127+
if (organizationId) {
128+
try {
129+
const subscription = await getOrganizationSubscription(organizationId, {
130+
onError: 'throw',
131+
})
132+
return [row.id, getPlanType(subscription?.plan)] as const
133+
} catch (error) {
134+
logger.error('Skipping cleanup for organization workspace after plan lookup failed', {
135+
workspaceId: row.id,
136+
organizationId,
137+
error,
138+
})
139+
return null
140+
}
141+
}
142+
143+
const plan = userPlanByBilledUserId.get(row.billedAccountUserId)
144+
if (plan === undefined) {
145+
return null
146+
}
147+
148+
return [row.id, plan] as const
96149
})
97150
)
98151

99-
return new Map(entries)
152+
return new Map(entries.filter((entry): entry is PlanResolutionEntry => entry !== null))
100153
}
101154

102155
/**
@@ -107,10 +160,8 @@ async function resolvePlanTypesByBilledUserId(
107160
*/
108161
async function resolveWorkspaceIdsForPlan(plan: NonEnterprisePlan): Promise<string[]> {
109162
const rows = await listActiveWorkspaceCleanupScopeRows()
110-
const planByBilledUserId = await resolvePlanTypesByBilledUserId(rows)
111-
return rows
112-
.filter((row) => planByBilledUserId.get(row.billedAccountUserId) === plan)
113-
.map((row) => row.id)
163+
const planByWorkspaceId = await resolvePlanTypesByWorkspaceId(rows)
164+
return rows.filter((row) => planByWorkspaceId.get(row.id) === plan).map((row) => row.id)
114165
}
115166

116167
export interface ResolvedCleanupScope {
@@ -198,10 +249,10 @@ export async function dispatchCleanupJobs(
198249
}
199250

200251
const activeWorkspaceRows = await listActiveWorkspaceCleanupScopeRows()
201-
const planByBilledUserId = await resolvePlanTypesByBilledUserId(activeWorkspaceRows)
252+
const planByWorkspaceId = await resolvePlanTypesByWorkspaceId(activeWorkspaceRows)
202253
const enterpriseRows = activeWorkspaceRows.filter(
203254
(row) =>
204-
planByBilledUserId.get(row.billedAccountUserId) === 'enterprise' &&
255+
planByWorkspaceId.get(row.id) === 'enterprise' &&
205256
row.organizationSettings?.[config.key] != null
206257
)
207258

apps/sim/lib/billing/core/billing.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ import { createLogger } from '@sim/logger'
2828

2929
const logger = createLogger('Billing')
3030

31+
interface GetOrganizationSubscriptionOptions {
32+
onError?: 'return-null' | 'throw'
33+
}
34+
3135
/**
3236
* Get the organization's subscription row when its status is one of
3337
* `ENTITLED_SUBSCRIPTION_STATUSES` (includes `past_due`). Use this
@@ -37,7 +41,11 @@ const logger = createLogger('Billing')
3741
* (from `core/subscription.ts`), which excludes `past_due`.
3842
* Returns `null` when there is no entitled sub.
3943
*/
40-
export async function getOrganizationSubscription(organizationId: string) {
44+
export async function getOrganizationSubscription(
45+
organizationId: string,
46+
options: GetOrganizationSubscriptionOptions = {}
47+
) {
48+
const { onError = 'return-null' } = options
4149
try {
4250
const orgSubs = await db
4351
.select()
@@ -54,6 +62,9 @@ export async function getOrganizationSubscription(organizationId: string) {
5462
return orgSubs.length > 0 ? orgSubs[0] : null
5563
} catch (error) {
5664
logger.error('Error getting organization subscription', { error, organizationId })
65+
if (onError === 'throw') {
66+
throw error
67+
}
5768
return null
5869
}
5970
}

0 commit comments

Comments
 (0)