11import { db } from '@sim/db'
2- import { member , subscription as subscriptionTable , user , userStats } from '@sim/db/schema'
2+ import { member , user , userStats } from '@sim/db/schema'
33import { createLogger } from '@sim/logger'
44import { isOrgAdminRole } from '@sim/platform-authz/workspace'
5- import { and , eq , inArray } from 'drizzle-orm'
5+ import { and , count , eq } from 'drizzle-orm'
66import { type NextRequest , NextResponse } from 'next/server'
77import {
88 organizationMemberQuerySchema ,
99 organizationParamsSchema ,
1010} from '@/lib/api/contracts/organization'
1111import { getValidationErrorMessage } from '@/lib/api/server'
1212import { getSession } from '@/lib/auth'
13- import { getOrgMemberLedgerByUser } from '@/lib/billing/core/organization'
14- import { ENTITLED_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils'
13+ import { getOrganizationMemberUsageSnapshot } from '@/lib/billing/core/organization'
1514import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1615
1716const logger = createLogger ( 'OrganizationMembersAPI' )
@@ -47,6 +46,7 @@ export const GET = withRouteHandler(
4746 { status : 400 }
4847 )
4948 }
49+ const { limit, offset } = queryResult . data
5050 const includeUsage = queryResult . data . include === 'usage'
5151
5252 // Verify user has access to this organization
@@ -67,7 +67,7 @@ export const GET = withRouteHandler(
6767 const hasAdminAccess = isOrgAdminRole ( userRole )
6868
6969 // Get organization members
70- const query = db
70+ const memberPageQuery = db
7171 . select ( {
7272 id : member . id ,
7373 userId : member . userId ,
@@ -80,82 +80,87 @@ export const GET = withRouteHandler(
8080 . from ( member )
8181 . innerJoin ( user , eq ( member . userId , user . id ) )
8282 . where ( eq ( member . organizationId , organizationId ) )
83+ . orderBy ( user . name , user . id )
84+ . limit ( limit )
85+ . offset ( offset )
86+
87+ const totalQuery = db
88+ . select ( { value : count ( ) } )
89+ . from ( member )
90+ . where ( eq ( member . organizationId , organizationId ) )
8391
8492 // Include usage data if requested and user has admin access
8593 if ( includeUsage && hasAdminAccess ) {
86- const base = await db
87- . select ( {
88- id : member . id ,
89- userId : member . userId ,
90- organizationId : member . organizationId ,
91- role : member . role ,
92- createdAt : member . createdAt ,
93- userName : user . name ,
94- userEmail : user . email ,
95- currentPeriodCost : userStats . currentPeriodCost ,
96- currentUsageLimit : userStats . currentUsageLimit ,
97- usageLimitUpdatedAt : userStats . usageLimitUpdatedAt ,
94+ const [ base , totalRows ] = await Promise . all ( [
95+ db
96+ . select ( {
97+ id : member . id ,
98+ userId : member . userId ,
99+ organizationId : member . organizationId ,
100+ role : member . role ,
101+ createdAt : member . createdAt ,
102+ userName : user . name ,
103+ userEmail : user . email ,
104+ currentPeriodCost : userStats . currentPeriodCost ,
105+ currentUsageLimit : userStats . currentUsageLimit ,
106+ usageLimitUpdatedAt : userStats . usageLimitUpdatedAt ,
107+ } )
108+ . from ( member )
109+ . innerJoin ( user , eq ( member . userId , user . id ) )
110+ . leftJoin ( userStats , eq ( user . id , userStats . userId ) )
111+ . where ( eq ( member . organizationId , organizationId ) )
112+ . orderBy ( user . name , user . id )
113+ . limit ( limit )
114+ . offset ( offset ) ,
115+ totalQuery ,
116+ ] )
117+
118+ const { billingPeriod, includeLegacyBaseline, usageByUser } =
119+ await getOrganizationMemberUsageSnapshot ( organizationId , {
120+ userIds : base . map ( ( row ) => row . userId ) ,
98121 } )
99- . from ( member )
100- . innerJoin ( user , eq ( member . userId , user . id ) )
101- . leftJoin ( userStats , eq ( user . id , userStats . userId ) )
102- . where ( eq ( member . organizationId , organizationId ) )
103-
104- // The billing period is the same for every member — it comes from
105- // whichever subscription covers them. Fetch once and attach to
106- // every row instead of calling `getUserUsageData` per-member,
107- // which would run an O(N) pooled query for each of N rows.
108- const [ orgSub ] = await db
109- . select ( {
110- periodStart : subscriptionTable . periodStart ,
111- periodEnd : subscriptionTable . periodEnd ,
112- } )
113- . from ( subscriptionTable )
114- . where (
115- and (
116- eq ( subscriptionTable . referenceId , organizationId ) ,
117- inArray ( subscriptionTable . status , ENTITLED_SUBSCRIPTION_STATUSES )
118- )
119- )
120- . limit ( 1 )
121-
122- const billingPeriodStart = orgSub ?. periodStart ?? null
123- const billingPeriodEnd = orgSub ?. periodEnd ?? null
124-
125- // currentPeriodCost is only a baseline; add each member's attributed
126- // usage_log for the period (batched, one query) so the roster shows real
127- // usage rather than the frozen baseline.
128- const usageByUser = await getOrgMemberLedgerByUser (
129- organizationId ,
130- billingPeriodStart && billingPeriodEnd
131- ? { start : billingPeriodStart , end : billingPeriodEnd }
132- : null
133- )
122+ const billingPeriodStart = billingPeriod ?. start ?? null
123+ const billingPeriodEnd = billingPeriod ?. end ?? null
134124
135125 const membersWithUsage = base . map ( ( row ) => ( {
136126 ...row ,
137127 currentPeriodCost : (
138- Number ( row . currentPeriodCost ?? 0 ) + ( usageByUser . get ( row . userId ) ?? 0 )
128+ ( includeLegacyBaseline ? Number ( row . currentPeriodCost ?? 0 ) : 0 ) +
129+ ( usageByUser . get ( row . userId ) ?? 0 )
139130 ) . toString ( ) ,
140131 billingPeriodStart,
141132 billingPeriodEnd,
142133 } ) )
143134
135+ const total = totalRows [ 0 ] ?. value ?? 0
144136 return NextResponse . json ( {
145137 success : true ,
146138 data : membersWithUsage ,
147- total : membersWithUsage . length ,
139+ total,
140+ pagination : {
141+ total,
142+ limit,
143+ offset,
144+ hasMore : offset + membersWithUsage . length < total ,
145+ } ,
148146 userRole,
149147 hasAdminAccess,
150148 } )
151149 }
152150
153- const members = await query
151+ const [ members , totalRows ] = await Promise . all ( [ memberPageQuery , totalQuery ] )
152+ const total = totalRows [ 0 ] ?. value ?? 0
154153
155154 return NextResponse . json ( {
156155 success : true ,
157156 data : members ,
158- total : members . length ,
157+ total,
158+ pagination : {
159+ total,
160+ limit,
161+ offset,
162+ hasMore : offset + members . length < total ,
163+ } ,
159164 userRole,
160165 hasAdminAccess,
161166 } )
0 commit comments