@@ -2,8 +2,8 @@ import { db } from '@sim/db'
22import { member , permissions , usageLog , workspace } from '@sim/db/schema'
33import { createLogger } from '@sim/logger'
44import { getPostgresCancellationReason } from '@sim/utils/errors'
5- import { and , countDistinct , eq , inArray , notExists , type SQL , sql } from 'drizzle-orm'
6- import { alias , unionAll } from 'drizzle-orm/pg-core'
5+ import { and , countDistinct , eq , inArray , notInArray , type SQL , sql } from 'drizzle-orm'
6+ import { unionAll } from 'drizzle-orm/pg-core'
77import { readLedgerBounded } from '@/lib/billing/core/ledger-read'
88import type { ResolvedUsagePeriod } from '@/lib/billing/core/reporting-period'
99import { buildUsageAnalyticsScope } from '@/lib/billing/core/usage-analytics'
@@ -50,6 +50,8 @@ interface DashboardWorkflowRuns {
5050 total : number
5151 /** Runs per acting user; a run with rows from two actors counts for each. */
5252 byUser : Map < string , number >
53+ /** Distinct runs across the organization's former actors together. */
54+ formerActors : number
5355}
5456
5557/**
@@ -139,25 +141,58 @@ export async function readDashboardLedgerSpendByUser(
139141}
140142
141143/**
142- * One entity's workflow runs over its period, in total and per acting user, from one
143- * scan. `null` when the count does not finish within {@link WORKFLOW_RUNS_BUDGET_MS}.
144+ * Who no longer represents an organization's usage: an actor who is neither a member nor a
145+ * collaborator on one of its workspaces any more. Uncorrelated, so each list is hashed once
146+ * rather than probed for every ledger row.
144147 */
145- export async function readDashboardWorkflowRuns (
146- window : DashboardLedgerWindow
148+ function isFormerActor ( organizationId : string ) : SQL {
149+ const members = db
150+ . select ( { userId : member . userId } )
151+ . from ( member )
152+ . where ( eq ( member . organizationId , organizationId ) )
153+ const collaborators = db
154+ . select ( { userId : permissions . userId } )
155+ . from ( permissions )
156+ . innerJoin (
157+ workspace ,
158+ and ( eq ( permissions . entityType , 'workspace' ) , eq ( permissions . entityId , workspace . id ) )
159+ )
160+ . where ( eq ( workspace . organizationId , organizationId ) )
161+ return sql `(${ notInArray ( usageLog . userId , members ) } and ${ notInArray ( usageLog . userId , collaborators ) } )`
162+ }
163+
164+ /**
165+ * An organization's workflow runs over its period, in total, per acting user and across its
166+ * former actors, from one scan. `null` when the count does not finish within
167+ * {@link WORKFLOW_RUNS_BUDGET_MS}.
168+ */
169+ export async function readDashboardOrganizationWorkflowRuns (
170+ organizationId : string ,
171+ period : ResolvedUsagePeriod
147172) : Promise < DashboardWorkflowRuns | null > {
148- let rows : { userId : string | null ; workflowRuns : number } [ ]
173+ let rows : { userId : string | null ; formerActor : boolean | null ; workflowRuns : number } [ ]
149174 try {
150175 rows = await readLedgerBounded (
151176 db ,
152- ( tx ) =>
153- tx
177+ ( tx ) => {
178+ const workflowRows = tx
154179 . select ( {
155- userId : sql < string | null > `${ usageLog . userId } ` ,
156- workflowRuns : countDistinct ( usageLog . executionId ) ,
180+ userId : usageLog . userId ,
181+ executionId : usageLog . executionId ,
182+ formerActor : sql < boolean > `${ isFormerActor ( organizationId ) } ` . as ( 'former_actor' ) ,
157183 } )
158184 . from ( usageLog )
159- . where ( and ( ...windowScope ( window ) , WORKFLOW_SOURCE ) )
160- . groupBy ( sql `grouping sets ((${ usageLog . userId } ), ())` ) ,
185+ . where ( and ( ...windowScope ( organizationWindow ( organizationId , period ) ) , WORKFLOW_SOURCE ) )
186+ . as ( 'workflow_rows' )
187+ return tx
188+ . select ( {
189+ userId : sql < string | null > `${ workflowRows . userId } ` ,
190+ formerActor : sql < boolean | null > `${ workflowRows . formerActor } ` ,
191+ workflowRuns : countDistinct ( workflowRows . executionId ) ,
192+ } )
193+ . from ( workflowRows )
194+ . groupBy ( sql `grouping sets ((${ workflowRows . userId } ), (${ workflowRows . formerActor } ), ())` )
195+ } ,
161196 WORKFLOW_RUNS_BUDGET_MS
162197 )
163198 } catch ( error ) {
@@ -166,71 +201,34 @@ export async function readDashboardWorkflowRuns(
166201 getPostgresCancellationReason ( error ) === 'statement_timeout'
167202 if ( ! overBudget ) throw error
168203 logger . warn ( 'Workflow run count exceeded its budget' , {
169- entityType : window . entity . type ,
170- entityId : window . entity . id ,
204+ organizationId,
171205 budgetMs : WORKFLOW_RUNS_BUDGET_MS ,
172206 } )
173207 return null
174208 }
175209
176- const runs : DashboardWorkflowRuns = { total : 0 , byUser : new Map ( ) }
210+ const runs : DashboardWorkflowRuns = { total : 0 , byUser : new Map ( ) , formerActors : 0 }
177211 for ( const row of rows ) {
178- if ( row . userId === null ) runs . total = row . workflowRuns
179- else runs . byUser . set ( row . userId , row . workflowRuns )
212+ if ( row . userId !== null ) runs . byUser . set ( row . userId , row . workflowRuns )
213+ else if ( row . formerActor === null ) runs . total = row . workflowRuns
214+ else if ( row . formerActor ) runs . formerActors = row . workflowRuns
180215 }
181216 return runs
182217}
183218
184- const historicalUsageMember = alias ( member , 'historical_usage_member' )
185- const historicalUsagePermission = alias ( permissions , 'historical_usage_permission' )
186- const historicalUsageWorkspace = alias ( workspace , 'historical_usage_workspace' )
187-
188- /**
189- * An organization's period spend per actor who is neither a member nor a collaborator on
190- * one of its workspaces any more.
191- */
219+ /** An organization's period spend per former actor (see {@link isFormerActor}). */
192220export async function readDashboardHistoricalActorSpend (
193221 organizationId : string ,
194222 period : ResolvedUsagePeriod
195223) : Promise < Map < string , number > > {
196- const currentMember = db
197- . select ( { value : sql `1` } )
198- . from ( historicalUsageMember )
199- . where (
200- and (
201- eq ( historicalUsageMember . organizationId , organizationId ) ,
202- eq ( historicalUsageMember . userId , usageLog . userId )
203- )
204- )
205- const currentCollaborator = db
206- . select ( { value : sql `1` } )
207- . from ( historicalUsagePermission )
208- . innerJoin (
209- historicalUsageWorkspace ,
210- and (
211- eq ( historicalUsagePermission . entityType , 'workspace' ) ,
212- eq ( historicalUsagePermission . entityId , historicalUsageWorkspace . id )
213- )
214- )
215- . where (
216- and (
217- eq ( historicalUsageWorkspace . organizationId , organizationId ) ,
218- eq ( historicalUsagePermission . userId , usageLog . userId )
219- )
220- )
221-
222224 const rows = await db
223225 . select ( {
224226 userId : usageLog . userId ,
225227 cost : sql < string > `coalesce(sum(${ usageLog . cost } ), 0)` ,
226228 } )
227229 . from ( usageLog )
228230 . where (
229- and (
230- ...windowScope ( organizationWindow ( organizationId , period ) ) ,
231- notExists ( currentMember ) ,
232- notExists ( currentCollaborator )
233- )
231+ and ( ...windowScope ( organizationWindow ( organizationId , period ) ) , isFormerActor ( organizationId ) )
234232 )
235233 . groupBy ( usageLog . userId )
236234 return new Map ( rows . map ( ( row ) => [ row . userId , Number ( row . cost ) ] ) )
0 commit comments