@@ -22,7 +22,7 @@ import {
2222 isOrgScopedSubscription ,
2323} from '@/lib/billing/subscriptions/utils'
2424import { Decimal , toDecimal , toNumber } from '@/lib/billing/utils/decimal'
25- import type { DbOrTx } from '@/lib/db/types'
25+ import type { DbClient } from '@/lib/db/types'
2626
2727export { getPlanPricing }
2828
@@ -32,6 +32,8 @@ const logger = createLogger('Billing')
3232
3333interface GetOrganizationSubscriptionOptions {
3434 onError ?: 'return-null' | 'throw'
35+ /** Read-routing client (primary or replica); defaults to the primary. */
36+ executor ?: DbClient
3537}
3638
3739/**
@@ -42,14 +44,18 @@ interface GetOrganizationSubscriptionOptions {
4244 * For product-access gating use `getOrganizationSubscriptionUsable`
4345 * (from `core/subscription.ts`), which excludes `past_due`.
4446 * Returns `null` when there is no entitled sub.
47+ *
48+ * `options.executor` exists for replica routing on display/summary read
49+ * paths only. Enforcement and webhook callers must read the primary —
50+ * omit the executor (or pass `db`).
4551 */
4652export async function getOrganizationSubscription (
4753 organizationId : string ,
4854 options : GetOrganizationSubscriptionOptions = { }
4955) {
50- const { onError = 'return-null' } = options
56+ const { onError = 'return-null' , executor = db } = options
5157 try {
52- const orgSubs = await db
58+ const orgSubs = await executor
5359 . select ( )
5460 . from ( subscription )
5561 . where (
@@ -111,13 +117,16 @@ export async function isSubscriptionOrgScoped(sub: { referenceId: string }): Pro
111117 * column is `NOT NULL DEFAULT '0'` and mixing scopes would break
112118 * current-period billing math.
113119 */
114- async function aggregateOrgMemberStats ( organizationId : string ) : Promise < {
120+ async function aggregateOrgMemberStats (
121+ organizationId : string ,
122+ executor : DbClient = db
123+ ) : Promise < {
115124 memberIds : string [ ]
116125 currentPeriodCost : number
117126 currentPeriodCopilotCost : number
118127 lastPeriodCopilotCost : number
119128} > {
120- const rows = await db
129+ const rows = await executor
121130 . select ( {
122131 userId : member . userId ,
123132 currentPeriodCost : userStats . currentPeriodCost ,
@@ -386,7 +395,7 @@ export async function calculateSubscriptionOverage(sub: {
386395export async function getSimplifiedBillingSummary (
387396 userId : string ,
388397 organizationId ?: string ,
389- executor : DbOrTx = db
398+ executor : DbClient = db
390399) : Promise < {
391400 type : 'individual' | 'organization'
392401 plan : string
@@ -432,8 +441,8 @@ export async function getSimplifiedBillingSummary(
432441 // Get subscription and usage data upfront
433442 const [ subscription , usageData ] = await Promise . all ( [
434443 organizationId
435- ? getOrganizationSubscription ( organizationId )
436- : getHighestPrioritySubscription ( userId ) ,
444+ ? getOrganizationSubscription ( organizationId , { executor } )
445+ : getHighestPrioritySubscription ( userId , { executor } ) ,
437446 getUserUsageData ( userId , executor ) ,
438447 ] )
439448
@@ -455,7 +464,7 @@ export async function getSimplifiedBillingSummary(
455464 // Pool usage/copilot across all members in one query. Must not use
456465 // `getUserUsageData` per-member — it now returns the pool itself
457466 // for org-scoped subs, which would N-times-count.
458- const pooled = await aggregateOrgMemberStats ( organizationId )
467+ const pooled = await aggregateOrgMemberStats ( organizationId , executor )
459468
460469 const rawCurrentUsage = pooled . currentPeriodCost
461470 const totalLastPeriodCopilotCost = pooled . lastPeriodCopilotCost
@@ -495,7 +504,8 @@ export async function getSimplifiedBillingSummary(
495504 if ( planDollars > 0 ) {
496505 const userBounds = await getOrgMemberRefreshBounds (
497506 organizationId ,
498- subscription . periodStart
507+ subscription . periodStart ,
508+ executor
499509 )
500510 refreshDeduction = await computeDailyRefreshConsumed (
501511 {
@@ -516,7 +526,8 @@ export async function getSimplifiedBillingSummary(
516526 const { limit : orgUsageLimit } = await getOrgUsageLimit (
517527 organizationId ,
518528 plan ,
519- subscription . seats ?? null
529+ subscription . seats ?? null ,
530+ executor
520531 )
521532
522533 const percentUsed =
@@ -532,7 +543,7 @@ export async function getSimplifiedBillingSummary(
532543 )
533544 : 0
534545
535- const orgCredits = await getCreditBalance ( userId )
546+ const orgCredits = await getCreditBalance ( userId , executor )
536547 const orgBillingInterval = getBillingInterval ( subscription . metadata as SubscriptionMetadata )
537548
538549 return {
@@ -576,7 +587,7 @@ export async function getSimplifiedBillingSummary(
576587 }
577588 }
578589
579- const userStatsRows = await db
590+ const userStatsRows = await executor
580591 . select ( {
581592 currentPeriodCopilotCost : userStats . currentPeriodCopilotCost ,
582593 lastPeriodCopilotCost : userStats . lastPeriodCopilotCost ,
@@ -597,7 +608,7 @@ export async function getSimplifiedBillingSummary(
597608 let totalCopilotCost = copilotCost
598609 let totalLastPeriodCopilotCost = lastPeriodCopilotCost
599610 if ( orgScoped && subscription ?. referenceId ) {
600- const pooled = await aggregateOrgMemberStats ( subscription . referenceId )
611+ const pooled = await aggregateOrgMemberStats ( subscription . referenceId , executor )
601612 totalCopilotCost = pooled . currentPeriodCopilotCost
602613 totalLastPeriodCopilotCost = pooled . lastPeriodCopilotCost
603614 }
@@ -631,7 +642,7 @@ export async function getSimplifiedBillingSummary(
631642 )
632643 : 0
633644
634- const userCredits = await getCreditBalance ( userId )
645+ const userCredits = await getCreditBalance ( userId , executor )
635646 const individualBillingInterval = getBillingInterval (
636647 subscription ?. metadata as SubscriptionMetadata
637648 )
0 commit comments