11import { db } from '@sim/db'
2- import {
3- member ,
4- organization ,
5- subscription as subscriptionTable ,
6- user ,
7- userStats ,
8- } from '@sim/db/schema'
9- import { and , eq , sql } from 'drizzle-orm'
2+ import { member , user , userStats } from '@sim/db/schema'
3+ import { and , eq } from 'drizzle-orm'
104import { type NextRequest , NextResponse } from 'next/server'
115import { z } from 'zod'
126import { getSession } from '@/lib/auth'
137import { getUserUsageData } from '@/lib/billing/core/usage'
14- import { requireStripeClient } from '@/lib/billing/stripe-client '
8+ import { removeUserFromOrganization } from '@/lib/billing/organizations/membership '
159import { createLogger } from '@/lib/logs/console/logger'
1610
1711const logger = createLogger ( 'OrganizationMemberAPI' )
@@ -41,7 +35,6 @@ export async function GET(
4135 const url = new URL ( request . url )
4236 const includeUsage = url . searchParams . get ( 'include' ) === 'usage'
4337
44- // Verify user has access to this organization
4538 const userMember = await db
4639 . select ( )
4740 . from ( member )
@@ -58,7 +51,6 @@ export async function GET(
5851 const userRole = userMember [ 0 ] . role
5952 const hasAdminAccess = [ 'owner' , 'admin' ] . includes ( userRole )
6053
61- // Get target member details
6254 const memberQuery = db
6355 . select ( {
6456 id : member . id ,
@@ -80,7 +72,6 @@ export async function GET(
8072 return NextResponse . json ( { error : 'Member not found' } , { status : 404 } )
8173 }
8274
83- // Check if user can view this member's details
8475 const canViewDetails = hasAdminAccess || session . user . id === memberId
8576
8677 if ( ! canViewDetails ) {
@@ -89,7 +80,6 @@ export async function GET(
8980
9081 let memberData = memberEntry [ 0 ]
9182
92- // Include usage data if requested and user has permission
9383 if ( includeUsage && hasAdminAccess ) {
9484 const usageData = await db
9585 . select ( {
@@ -181,7 +171,6 @@ export async function PUT(
181171 return NextResponse . json ( { error : 'Forbidden - Admin access required' } , { status : 403 } )
182172 }
183173
184- // Check if target member exists
185174 const targetMember = await db
186175 . select ( )
187176 . from ( member )
@@ -192,25 +181,21 @@ export async function PUT(
192181 return NextResponse . json ( { error : 'Member not found' } , { status : 404 } )
193182 }
194183
195- // Prevent changing owner role
196184 if ( targetMember [ 0 ] . role === 'owner' ) {
197185 return NextResponse . json ( { error : 'Cannot change owner role' } , { status : 400 } )
198186 }
199187
200- // Prevent non-owners from promoting to admin
201188 if ( role === 'admin' && userMember [ 0 ] . role !== 'owner' ) {
202189 return NextResponse . json (
203190 { error : 'Only owners can promote members to admin' } ,
204191 { status : 403 }
205192 )
206193 }
207194
208- // Prevent admins from changing other admins' roles - only owners can modify admin roles
209195 if ( targetMember [ 0 ] . role === 'admin' && userMember [ 0 ] . role !== 'owner' ) {
210196 return NextResponse . json ( { error : 'Only owners can change admin roles' } , { status : 403 } )
211197 }
212198
213- // Update member role
214199 const updatedMember = await db
215200 . update ( member )
216201 . set ( { role } )
@@ -264,9 +249,8 @@ export async function DELETE(
264249 return NextResponse . json ( { error : 'Unauthorized' } , { status : 401 } )
265250 }
266251
267- const { id : organizationId , memberId } = await params
252+ const { id : organizationId , memberId : targetUserId } = await params
268253
269- // Verify user has admin access
270254 const userMember = await db
271255 . select ( )
272256 . from ( member )
@@ -281,209 +265,54 @@ export async function DELETE(
281265 }
282266
283267 const canRemoveMembers =
284- [ 'owner' , 'admin' ] . includes ( userMember [ 0 ] . role ) || session . user . id === memberId
268+ [ 'owner' , 'admin' ] . includes ( userMember [ 0 ] . role ) || session . user . id === targetUserId
285269
286270 if ( ! canRemoveMembers ) {
287271 return NextResponse . json ( { error : 'Forbidden - Insufficient permissions' } , { status : 403 } )
288272 }
289273
290- // Check if target member exists
291274 const targetMember = await db
292- . select ( )
275+ . select ( { id : member . id , role : member . role } )
293276 . from ( member )
294- . where ( and ( eq ( member . organizationId , organizationId ) , eq ( member . userId , memberId ) ) )
277+ . where ( and ( eq ( member . organizationId , organizationId ) , eq ( member . userId , targetUserId ) ) )
295278 . limit ( 1 )
296279
297280 if ( targetMember . length === 0 ) {
298281 return NextResponse . json ( { error : 'Member not found' } , { status : 404 } )
299282 }
300283
301- // Prevent removing the owner
302- if ( targetMember [ 0 ] . role === 'owner' ) {
303- return NextResponse . json ( { error : 'Cannot remove organization owner' } , { status : 400 } )
304- }
305-
306- // Capture departed member's usage and reset their cost to prevent double billing
307- try {
308- const departingUserStats = await db
309- . select ( { currentPeriodCost : userStats . currentPeriodCost } )
310- . from ( userStats )
311- . where ( eq ( userStats . userId , memberId ) )
312- . limit ( 1 )
284+ const result = await removeUserFromOrganization ( {
285+ userId : targetUserId ,
286+ organizationId,
287+ memberId : targetMember [ 0 ] . id ,
288+ } )
313289
314- if ( departingUserStats . length > 0 && departingUserStats [ 0 ] . currentPeriodCost ) {
315- const usage = Number . parseFloat ( departingUserStats [ 0 ] . currentPeriodCost )
316- if ( usage > 0 ) {
317- await db
318- . update ( organization )
319- . set ( {
320- departedMemberUsage : sql `${ organization . departedMemberUsage } + ${ usage } ` ,
321- } )
322- . where ( eq ( organization . id , organizationId ) )
323-
324- await db
325- . update ( userStats )
326- . set ( { currentPeriodCost : '0' } )
327- . where ( eq ( userStats . userId , memberId ) )
328-
329- logger . info ( 'Captured departed member usage and reset user cost' , {
330- organizationId,
331- memberId,
332- usage,
333- } )
334- }
290+ if ( ! result . success ) {
291+ if ( result . error === 'Cannot remove organization owner' ) {
292+ return NextResponse . json ( { error : result . error } , { status : 400 } )
335293 }
336- } catch ( usageCaptureError ) {
337- logger . error ( 'Failed to capture departed member usage' , {
338- organizationId,
339- memberId,
340- error : usageCaptureError ,
341- } )
342- }
343-
344- // Remove member
345- const removedMember = await db
346- . delete ( member )
347- . where ( and ( eq ( member . organizationId , organizationId ) , eq ( member . userId , memberId ) ) )
348- . returning ( )
349-
350- if ( removedMember . length === 0 ) {
351- return NextResponse . json ( { error : 'Failed to remove member' } , { status : 500 } )
294+ if ( result . error === 'Member not found' ) {
295+ return NextResponse . json ( { error : result . error } , { status : 404 } )
296+ }
297+ return NextResponse . json ( { error : result . error } , { status : 500 } )
352298 }
353299
354300 logger . info ( 'Organization member removed' , {
355301 organizationId,
356- removedMemberId : memberId ,
302+ removedMemberId : targetUserId ,
357303 removedBy : session . user . id ,
358- wasSelfRemoval : session . user . id === memberId ,
304+ wasSelfRemoval : session . user . id === targetUserId ,
305+ billingActions : result . billingActions ,
359306 } )
360307
361- // If the removed user left their last paid team and has a personal Pro set to cancel_at_period_end, restore it
362- try {
363- const remainingPaidTeams = await db
364- . select ( { orgId : member . organizationId } )
365- . from ( member )
366- . where ( eq ( member . userId , memberId ) )
367-
368- let hasAnyPaidTeam = false
369- if ( remainingPaidTeams . length > 0 ) {
370- const orgIds = remainingPaidTeams . map ( ( m ) => m . orgId )
371- const orgPaidSubs = await db
372- . select ( )
373- . from ( subscriptionTable )
374- . where ( and ( eq ( subscriptionTable . status , 'active' ) , eq ( subscriptionTable . plan , 'team' ) ) )
375-
376- hasAnyPaidTeam = orgPaidSubs . some ( ( s ) => orgIds . includes ( s . referenceId ) )
377- }
378-
379- if ( ! hasAnyPaidTeam ) {
380- const personalProRows = await db
381- . select ( )
382- . from ( subscriptionTable )
383- . where (
384- and (
385- eq ( subscriptionTable . referenceId , memberId ) ,
386- eq ( subscriptionTable . status , 'active' ) ,
387- eq ( subscriptionTable . plan , 'pro' )
388- )
389- )
390- . limit ( 1 )
391-
392- const personalPro = personalProRows [ 0 ]
393- if (
394- personalPro &&
395- personalPro . cancelAtPeriodEnd === true &&
396- personalPro . stripeSubscriptionId
397- ) {
398- try {
399- const stripe = requireStripeClient ( )
400- await stripe . subscriptions . update ( personalPro . stripeSubscriptionId , {
401- cancel_at_period_end : false ,
402- } )
403- } catch ( stripeError ) {
404- logger . error ( 'Stripe restore cancel_at_period_end failed for personal Pro' , {
405- userId : memberId ,
406- stripeSubscriptionId : personalPro . stripeSubscriptionId ,
407- error : stripeError ,
408- } )
409- }
410-
411- try {
412- await db
413- . update ( subscriptionTable )
414- . set ( { cancelAtPeriodEnd : false } )
415- . where ( eq ( subscriptionTable . id , personalPro . id ) )
416-
417- logger . info ( 'Restored personal Pro after leaving last paid team' , {
418- userId : memberId ,
419- personalSubscriptionId : personalPro . id ,
420- } )
421- } catch ( dbError ) {
422- logger . error ( 'DB update failed when restoring personal Pro' , {
423- userId : memberId ,
424- subscriptionId : personalPro . id ,
425- error : dbError ,
426- } )
427- }
428-
429- // Also restore the snapshotted Pro usage back to currentPeriodCost
430- try {
431- const userStatsRows = await db
432- . select ( {
433- currentPeriodCost : userStats . currentPeriodCost ,
434- proPeriodCostSnapshot : userStats . proPeriodCostSnapshot ,
435- } )
436- . from ( userStats )
437- . where ( eq ( userStats . userId , memberId ) )
438- . limit ( 1 )
439-
440- if ( userStatsRows . length > 0 ) {
441- const currentUsage = userStatsRows [ 0 ] . currentPeriodCost || '0'
442- const snapshotUsage = userStatsRows [ 0 ] . proPeriodCostSnapshot || '0'
443-
444- const currentNum = Number . parseFloat ( currentUsage )
445- const snapshotNum = Number . parseFloat ( snapshotUsage )
446- const restoredUsage = ( currentNum + snapshotNum ) . toString ( )
447-
448- await db
449- . update ( userStats )
450- . set ( {
451- currentPeriodCost : restoredUsage ,
452- proPeriodCostSnapshot : '0' , // Clear the snapshot
453- } )
454- . where ( eq ( userStats . userId , memberId ) )
455-
456- logger . info ( 'Restored Pro usage after leaving team' , {
457- userId : memberId ,
458- previousUsage : currentUsage ,
459- snapshotUsage : snapshotUsage ,
460- restoredUsage : restoredUsage ,
461- } )
462- }
463- } catch ( usageRestoreError ) {
464- logger . error ( 'Failed to restore Pro usage after leaving team' , {
465- userId : memberId ,
466- error : usageRestoreError ,
467- } )
468- }
469- }
470- }
471- } catch ( postRemoveError ) {
472- logger . error ( 'Post-removal personal Pro restore check failed' , {
473- organizationId,
474- memberId,
475- error : postRemoveError ,
476- } )
477- }
478-
479308 return NextResponse . json ( {
480309 success : true ,
481310 message :
482- session . user . id === memberId
311+ session . user . id === targetUserId
483312 ? 'You have left the organization'
484313 : 'Member removed successfully' ,
485314 data : {
486- removedMemberId : memberId ,
315+ removedMemberId : targetUserId ,
487316 removedBy : session . user . id ,
488317 removedAt : new Date ( ) . toISOString ( ) ,
489318 } ,
0 commit comments