1313 *
1414 * Add a user to an organization with full billing logic.
1515 * Handles Pro usage snapshot and subscription cancellation like the invitation flow.
16+ * If user is already a member, updates their role if different.
1617 *
1718 * Body:
1819 * - userId: string - User ID to add
1920 * - role: string - Role ('admin' | 'member')
20- * - skipBillingLogic?: boolean - Skip Pro cancellation (default: false)
2121 *
22- * Response: AdminSingleResponse<AdminMember>
22+ * Response: AdminSingleResponse<AdminMember & {
23+ * action: 'created' | 'updated' | 'already_member',
24+ * billingActions: { proUsageSnapshotted, proCancelledAtPeriodEnd }
25+ * }>
2326 */
2427
2528import { db } from '@sim/db'
@@ -129,8 +132,6 @@ export const POST = withAdminAuthParams<RouteParams>(async (request, context) =>
129132 return badRequestResponse ( 'role must be "admin" or "member"' )
130133 }
131134
132- const skipBillingLogic = body . skipBillingLogic === true
133-
134135 const [ orgData ] = await db
135136 . select ( { id : organization . id , name : organization . name } )
136137 . from ( organization )
@@ -151,11 +152,71 @@ export const POST = withAdminAuthParams<RouteParams>(async (request, context) =>
151152 return notFoundResponse ( 'User' )
152153 }
153154
155+ const [ existingMember ] = await db
156+ . select ( {
157+ id : member . id ,
158+ role : member . role ,
159+ createdAt : member . createdAt ,
160+ organizationId : member . organizationId ,
161+ } )
162+ . from ( member )
163+ . where ( eq ( member . userId , body . userId ) )
164+ . limit ( 1 )
165+
166+ if ( existingMember ) {
167+ if ( existingMember . organizationId === organizationId ) {
168+ if ( existingMember . role !== body . role ) {
169+ await db . update ( member ) . set ( { role : body . role } ) . where ( eq ( member . id , existingMember . id ) )
170+
171+ logger . info (
172+ `Admin API: Updated user ${ body . userId } role in organization ${ organizationId } ` ,
173+ {
174+ previousRole : existingMember . role ,
175+ newRole : body . role ,
176+ }
177+ )
178+
179+ return singleResponse ( {
180+ id : existingMember . id ,
181+ userId : body . userId ,
182+ organizationId,
183+ role : body . role ,
184+ createdAt : existingMember . createdAt . toISOString ( ) ,
185+ userName : userData . name ,
186+ userEmail : userData . email ,
187+ action : 'updated' as const ,
188+ billingActions : {
189+ proUsageSnapshotted : false ,
190+ proCancelledAtPeriodEnd : false ,
191+ } ,
192+ } )
193+ }
194+
195+ return singleResponse ( {
196+ id : existingMember . id ,
197+ userId : body . userId ,
198+ organizationId,
199+ role : existingMember . role ,
200+ createdAt : existingMember . createdAt . toISOString ( ) ,
201+ userName : userData . name ,
202+ userEmail : userData . email ,
203+ action : 'already_member' as const ,
204+ billingActions : {
205+ proUsageSnapshotted : false ,
206+ proCancelledAtPeriodEnd : false ,
207+ } ,
208+ } )
209+ }
210+
211+ return badRequestResponse (
212+ `User is already a member of another organization. Users can only belong to one organization at a time.`
213+ )
214+ }
215+
154216 const result = await addUserToOrganization ( {
155217 userId : body . userId ,
156218 organizationId,
157219 role : body . role ,
158- skipBillingLogic,
159220 } )
160221
161222 if ( ! result . success ) {
@@ -176,11 +237,11 @@ export const POST = withAdminAuthParams<RouteParams>(async (request, context) =>
176237 role : body . role ,
177238 memberId : result . memberId ,
178239 billingActions : result . billingActions ,
179- skipBillingLogic,
180240 } )
181241
182242 return singleResponse ( {
183243 ...data ,
244+ action : 'created' as const ,
184245 billingActions : {
185246 proUsageSnapshotted : result . billingActions . proUsageSnapshotted ,
186247 proCancelledAtPeriodEnd : result . billingActions . proCancelledAtPeriodEnd ,
0 commit comments