@@ -15,8 +15,8 @@ import {
1515 textKey ,
1616 timestampKey ,
1717} from '@/lib/api/list-query'
18- import { USAGE_LEDGER_STATEMENT_TIMEOUT_MS } from '@/lib/billing/constants'
1918import { defaultBillingPeriod } from '@/lib/billing/core/billing-period'
19+ import { readLedgerBounded } from '@/lib/billing/core/ledger-read'
2020import { getHighestPrioritySubscription } from '@/lib/billing/core/plan'
2121import {
2222 resolveSubscriptionUsagePeriod ,
@@ -216,10 +216,6 @@ async function resolveBillingContext(
216216/**
217217 * Returns attributed ledger usage for a billing entity/period. The ledger is
218218 * the sole source of truth for usage — there is no userStats baseline.
219- *
220- * The sum runs in a transaction of its own on the given client so that it can
221- * be bounded by {@link USAGE_LEDGER_STATEMENT_TIMEOUT_MS} for that statement
222- * alone: `SET LOCAL` ends with the transaction and never reaches the pool.
223219 */
224220export async function getBillingPeriodUsageCost (
225221 billingEntity : BillingEntity ,
@@ -243,17 +239,14 @@ export async function getBillingPeriodUsageCost(
243239 )
244240 }
245241
246- const [ row ] = await executor . transaction ( async ( tx ) => {
247- await tx . execute (
248- sql . raw ( `SET LOCAL statement_timeout = '${ USAGE_LEDGER_STATEMENT_TIMEOUT_MS } ms'` )
249- )
250- return tx
242+ const [ row ] = await readLedgerBounded ( executor , ( tx ) =>
243+ tx
251244 . select ( {
252245 cost : sql < string > `COALESCE(SUM(${ usageLog . cost } ), 0)` ,
253246 } )
254247 . from ( usageLog )
255248 . where ( and ( ...conditions ) )
256- } )
249+ )
257250
258251 return Number . parseFloat ( row ?. cost ?? '0' )
259252}
@@ -274,36 +267,38 @@ export async function getBillingPeriodWorkflowRunCount(
274267 billingPeriod : UsageQueryPeriod ,
275268 executor : DbClient = db
276269) : Promise < number > {
277- const [ row ] = await executor
278- . select ( {
279- /**
280- * The exclusion goes through `notInArray`, not `<> ALL(${array})`. Interpolating
281- * a JavaScript array into a `sql` template emits parenthesized scalar binds —
282- * `ALL(($1))` — which Postgres rejects outright with "op ANY/ALL (array)
283- * requires array on right side". Unit tests cannot catch it, because `@sim/db`
284- * is mocked and no statement is ever rendered.
285- */
286- workflowRuns :
287- sql < number > `COUNT(DISTINCT ${ usageLog . executionId } ) FILTER (WHERE ${ usageLog . source } = 'workflow' AND ${ notInArray ( usageLog . category , [ ...UNBILLED_USAGE_CATEGORIES ] ) } )` . mapWith (
288- Number
289- ) ,
290- } )
291- . from ( usageLog )
292- . where (
293- and (
294- eq ( usageLog . billingEntityType , billingEntity . type ) ,
295- eq ( usageLog . billingEntityId , billingEntity . id ) ,
296- ...( billingPeriod . source === 'reporting'
297- ? [
298- gte ( usageLog . createdAt , billingPeriod . start ) ,
299- lt ( usageLog . createdAt , billingPeriod . end ) ,
300- ]
301- : [
302- eq ( usageLog . billingPeriodStart , billingPeriod . start ) ,
303- eq ( usageLog . billingPeriodEnd , billingPeriod . end ) ,
304- ] )
270+ const [ row ] = await readLedgerBounded ( executor , ( tx ) =>
271+ tx
272+ . select ( {
273+ /**
274+ * The exclusion goes through `notInArray`, not `<> ALL(${array})`. Interpolating
275+ * a JavaScript array into a `sql` template emits parenthesized scalar binds —
276+ * `ALL(($1))` — which Postgres rejects outright with "op ANY/ALL (array)
277+ * requires array on right side". Unit tests cannot catch it, because `@sim/db`
278+ * is mocked and no statement is ever rendered.
279+ */
280+ workflowRuns :
281+ sql < number > `COUNT(DISTINCT ${ usageLog . executionId } ) FILTER (WHERE ${ usageLog . source } = 'workflow' AND ${ notInArray ( usageLog . category , [ ...UNBILLED_USAGE_CATEGORIES ] ) } )` . mapWith (
282+ Number
283+ ) ,
284+ } )
285+ . from ( usageLog )
286+ . where (
287+ and (
288+ eq ( usageLog . billingEntityType , billingEntity . type ) ,
289+ eq ( usageLog . billingEntityId , billingEntity . id ) ,
290+ ...( billingPeriod . source === 'reporting'
291+ ? [
292+ gte ( usageLog . createdAt , billingPeriod . start ) ,
293+ lt ( usageLog . createdAt , billingPeriod . end ) ,
294+ ]
295+ : [
296+ eq ( usageLog . billingPeriodStart , billingPeriod . start ) ,
297+ eq ( usageLog . billingPeriodEnd , billingPeriod . end ) ,
298+ ] )
299+ )
305300 )
306- )
301+ )
307302
308303 return row ?. workflowRuns ?? 0
309304}
@@ -321,27 +316,29 @@ export async function getBillingPeriodUsageCostWithSourceSubset(
321316 source : UsageLogSource [ ] ,
322317 executor : DbClient = db
323318) : Promise < { total : number ; subset : number } > {
324- const [ row ] = await executor
325- . select ( {
326- total : sql < string > `COALESCE(SUM(${ usageLog . cost } ), 0)` ,
327- subset : sql < string > `COALESCE(SUM(${ usageLog . cost } ) FILTER (WHERE ${ inArray ( usageLog . source , source ) } ), 0)` ,
328- } )
329- . from ( usageLog )
330- . where (
331- and (
332- eq ( usageLog . billingEntityType , billingEntity . type ) ,
333- eq ( usageLog . billingEntityId , billingEntity . id ) ,
334- ...( billingPeriod . source === 'reporting'
335- ? [
336- gte ( usageLog . createdAt , billingPeriod . start ) ,
337- lt ( usageLog . createdAt , billingPeriod . end ) ,
338- ]
339- : [
340- eq ( usageLog . billingPeriodStart , billingPeriod . start ) ,
341- eq ( usageLog . billingPeriodEnd , billingPeriod . end ) ,
342- ] )
319+ const [ row ] = await readLedgerBounded ( executor , ( tx ) =>
320+ tx
321+ . select ( {
322+ total : sql < string > `COALESCE(SUM(${ usageLog . cost } ), 0)` ,
323+ subset : sql < string > `COALESCE(SUM(${ usageLog . cost } ) FILTER (WHERE ${ inArray ( usageLog . source , source ) } ), 0)` ,
324+ } )
325+ . from ( usageLog )
326+ . where (
327+ and (
328+ eq ( usageLog . billingEntityType , billingEntity . type ) ,
329+ eq ( usageLog . billingEntityId , billingEntity . id ) ,
330+ ...( billingPeriod . source === 'reporting'
331+ ? [
332+ gte ( usageLog . createdAt , billingPeriod . start ) ,
333+ lt ( usageLog . createdAt , billingPeriod . end ) ,
334+ ]
335+ : [
336+ eq ( usageLog . billingPeriodStart , billingPeriod . start ) ,
337+ eq ( usageLog . billingPeriodEnd , billingPeriod . end ) ,
338+ ] )
339+ )
343340 )
344- )
341+ )
345342
346343 return {
347344 total : Number . parseFloat ( row ?. total ?? '0' ) ,
@@ -377,14 +374,16 @@ export async function getBillingPeriodUsageCostByUser(
377374 }
378375 if ( userIds ) conditions . push ( inArray ( usageLog . userId , [ ...userIds ] ) )
379376
380- const rows = await executor
381- . select ( {
382- userId : usageLog . userId ,
383- cost : sql < string > `COALESCE(SUM(${ usageLog . cost } ), 0)` ,
384- } )
385- . from ( usageLog )
386- . where ( and ( ...conditions ) )
387- . groupBy ( usageLog . userId )
377+ const rows = await readLedgerBounded ( executor , ( tx ) =>
378+ tx
379+ . select ( {
380+ userId : usageLog . userId ,
381+ cost : sql < string > `COALESCE(SUM(${ usageLog . cost } ), 0)` ,
382+ } )
383+ . from ( usageLog )
384+ . where ( and ( ...conditions ) )
385+ . groupBy ( usageLog . userId )
386+ )
388387
389388 return new Map ( rows . map ( ( row ) => [ row . userId , Number . parseFloat ( row . cost ?? '0' ) ] ) )
390389}
0 commit comments