Skip to content

Commit d5d08af

Browse files
improvement(billing): accurate per-member usage via shared ledger helper
Per-member/per-user usage in the org-member routes now adds the usage_log ledger to the currentPeriodCost baseline (which is no longer incremented), via a shared getOrgMemberLedgerByUser helper to avoid repeating the subscription→period→ledger lookup across the admin and member-facing routes. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d957a8e commit d5d08af

5 files changed

Lines changed: 46 additions & 48 deletions

File tree

apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import {
1111
import { parseRequest } from '@/lib/api/server'
1212
import { getSession } from '@/lib/auth'
1313
import { setActiveOrganizationForCurrentSession } from '@/lib/auth/active-organization'
14+
import { getOrgMemberLedgerByUser } from '@/lib/billing/core/organization'
1415
import { getUserUsageData } from '@/lib/billing/core/usage'
15-
import { getBillingPeriodUsageCostByUser } from '@/lib/billing/core/usage-log'
1616
import {
1717
removeExternalUserFromOrganizationWorkspaces,
1818
removeUserFromOrganization,
@@ -106,14 +106,14 @@ export const GET = withRouteHandler(
106106
// usage_log for the period. (getUserUsageData returns the org POOL for
107107
// org-scoped members, so it can't supply the per-member figure.)
108108
const memberLedger =
109-
computed.billingPeriodStart && computed.billingPeriodEnd
110-
? ((
111-
await getBillingPeriodUsageCostByUser(
112-
{ type: 'organization', id: organizationId },
113-
{ start: computed.billingPeriodStart, end: computed.billingPeriodEnd }
114-
)
115-
).get(memberId) ?? 0)
116-
: 0
109+
(
110+
await getOrgMemberLedgerByUser(
111+
organizationId,
112+
computed.billingPeriodStart && computed.billingPeriodEnd
113+
? { start: computed.billingPeriodStart, end: computed.billingPeriodEnd }
114+
: null
115+
)
116+
).get(memberId) ?? 0
117117
memberData = {
118118
...memberData,
119119
usage: {

apps/sim/app/api/organizations/[id]/members/route.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
} from '@/lib/api/contracts/organization'
1818
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
1919
import { getSession } from '@/lib/auth'
20-
import { getBillingPeriodUsageCostByUser } from '@/lib/billing/core/usage-log'
20+
import { getOrgMemberLedgerByUser } from '@/lib/billing/core/organization'
2121
import { ENTITLED_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils'
2222
import { validateSeatAvailability } from '@/lib/billing/validation/seat-management'
2323
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
@@ -143,13 +143,12 @@ export const GET = withRouteHandler(
143143
// currentPeriodCost is only a baseline; add each member's attributed
144144
// usage_log for the period (batched, one query) so the roster shows real
145145
// usage rather than the frozen baseline.
146-
const usageByUser =
146+
const usageByUser = await getOrgMemberLedgerByUser(
147+
organizationId,
147148
billingPeriodStart && billingPeriodEnd
148-
? await getBillingPeriodUsageCostByUser(
149-
{ type: 'organization', id: organizationId },
150-
{ start: billingPeriodStart, end: billingPeriodEnd }
151-
)
152-
: new Map<string, number>()
149+
? { start: billingPeriodStart, end: billingPeriodEnd }
150+
: null
151+
)
153152

154153
const membersWithUsage = base.map((row) => ({
155154
...row,

apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ import {
3535
adminV1UpdateOrganizationMemberContract,
3636
} from '@/lib/api/contracts/v1/admin'
3737
import { parseRequest } from '@/lib/api/server'
38-
import { getOrganizationSubscription } from '@/lib/billing/core/billing'
39-
import { getBillingPeriodUsageCostByUser } from '@/lib/billing/core/usage-log'
38+
import { getOrgMemberLedgerByUser } from '@/lib/billing/core/organization'
4039
import { removeUserFromOrganization } from '@/lib/billing/organizations/membership'
4140
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
4241
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
@@ -102,17 +101,7 @@ export const GET = withRouteHandler(
102101

103102
// currentPeriodCost is only a baseline; add this member's attributed
104103
// usage_log for the org's period so admin shows real current usage.
105-
const subscription = await getOrganizationSubscription(organizationId)
106-
const billingPeriod =
107-
subscription?.periodStart && subscription?.periodEnd
108-
? { start: subscription.periodStart, end: subscription.periodEnd }
109-
: null
110-
const ledgerByUser = billingPeriod
111-
? await getBillingPeriodUsageCostByUser(
112-
{ type: 'organization', id: organizationId },
113-
billingPeriod
114-
)
115-
: new Map<string, number>()
104+
const ledgerByUser = await getOrgMemberLedgerByUser(organizationId)
116105

117106
const data: AdminMemberDetail = {
118107
id: memberData.id,

apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ import {
3737
adminV1ListOrganizationMembersContract,
3838
} from '@/lib/api/contracts/v1/admin'
3939
import { parseRequest } from '@/lib/api/server'
40-
import { getOrganizationSubscription } from '@/lib/billing/core/billing'
41-
import { getBillingPeriodUsageCostByUser } from '@/lib/billing/core/usage-log'
40+
import { getOrgMemberLedgerByUser } from '@/lib/billing/core/organization'
4241
import { addUserToOrganization } from '@/lib/billing/organizations/membership'
4342
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
4443
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
@@ -113,17 +112,7 @@ export const GET = withRouteHandler(
113112

114113
// currentPeriodCost is only a baseline; add each member's attributed
115114
// usage_log for the org's period so admin shows real current usage.
116-
const subscription = await getOrganizationSubscription(organizationId)
117-
const billingPeriod =
118-
subscription?.periodStart && subscription?.periodEnd
119-
? { start: subscription.periodStart, end: subscription.periodEnd }
120-
: null
121-
const usageByUser = billingPeriod
122-
? await getBillingPeriodUsageCostByUser(
123-
{ type: 'organization', id: organizationId },
124-
billingPeriod
125-
)
126-
: new Map<string, number>()
115+
const usageByUser = await getOrgMemberLedgerByUser(organizationId)
127116

128117
const data: AdminMemberDetail[] = membersData.map((m) => ({
129118
id: m.id,

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

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,32 @@ interface MemberUsageData {
5555
joinedAt: Date
5656
}
5757

58+
/**
59+
* Per-member usage_log cost for an org's current billing period, keyed by userId.
60+
* `currentPeriodCost` is only a baseline (no longer incremented on the hot path),
61+
* so callers add this ledger component to it for each member's real current-period
62+
* usage. Pass `period` to reuse an already-fetched subscription window; omit it to
63+
* look up the org's subscription here. Returns an empty map when there's no period.
64+
*/
65+
export async function getOrgMemberLedgerByUser(
66+
organizationId: string,
67+
period?: { start: Date; end: Date } | null
68+
): Promise<Map<string, number>> {
69+
let billingPeriod = period ?? null
70+
if (period === undefined) {
71+
const subscription = await getOrganizationSubscription(organizationId)
72+
billingPeriod =
73+
subscription?.periodStart && subscription?.periodEnd
74+
? { start: subscription.periodStart, end: subscription.periodEnd }
75+
: null
76+
}
77+
if (!billingPeriod) return new Map<string, number>()
78+
return getBillingPeriodUsageCostByUser(
79+
{ type: 'organization', id: organizationId },
80+
billingPeriod
81+
)
82+
}
83+
5884
/**
5985
* Get comprehensive organization billing and usage data
6086
*/
@@ -108,12 +134,7 @@ export async function getOrganizationBillingData(
108134
subscription.periodStart && subscription.periodEnd
109135
? { start: subscription.periodStart, end: subscription.periodEnd }
110136
: null
111-
const usageByUser = billingPeriod
112-
? await getBillingPeriodUsageCostByUser(
113-
{ type: 'organization', id: subscription.referenceId },
114-
billingPeriod
115-
)
116-
: new Map<string, number>()
137+
const usageByUser = await getOrgMemberLedgerByUser(organizationId, billingPeriod)
117138

118139
// Process member data
119140
const members: MemberUsageData[] = membersWithUsage.map((memberRecord) => {

0 commit comments

Comments
 (0)