@@ -72,6 +72,7 @@ import {
7272 isOrgScopedSubscription ,
7373} from '@/lib/billing/subscriptions/utils'
7474import { toDecimal } from '@/lib/billing/utils/decimal'
75+ import { countPendingSeatInvitations } from '@/lib/billing/validation/seat-management'
7576import { env } from '@/lib/core/config/env'
7677import { executeTransactionallyIdempotent } from '@/lib/core/idempotency/transaction'
7778import { enqueueOutboxEvent } from '@/lib/core/outbox/service'
@@ -84,6 +85,8 @@ interface PaginationInput {
8485 offset : number
8586}
8687
88+ const MAX_ADMIN_MEMBER_WORKSPACE_SELECTION = 1_000
89+
8790export interface AdminMutationActor {
8891 id : string | null
8992 name : string
@@ -1119,8 +1122,12 @@ export async function updateDashboardEnterpriseSeats(
11191122 . select ( { value : count ( ) } )
11201123 . from ( member )
11211124 . where ( eq ( member . organizationId , organizationId ) )
1122- if ( seats < ( memberCountRow ?. value ?? 0 ) ) {
1123- throw new Error ( 'Seat capacity cannot be below current internal membership' )
1125+ const pendingSeats = await countPendingSeatInvitations ( organizationId , tx )
1126+ const requiredSeats = ( memberCountRow ?. value ?? 0 ) + pendingSeats
1127+ if ( seats < requiredSeats ) {
1128+ throw new Error (
1129+ `Seat capacity cannot be below ${ requiredSeats } occupied or reserved seats (${ memberCountRow ?. value ?? 0 } members and ${ pendingSeats } pending invitations)`
1130+ )
11241131 }
11251132 await enqueueEnterpriseMetadataIntent ( tx , {
11261133 subscriptionId : subscriptionRow . id ,
@@ -1135,7 +1142,7 @@ export async function updateDashboardEnterpriseSeats(
11351142 action : AuditAction . ORG_SEAT_PROVISIONED ,
11361143 resourceType : AuditResourceType . ORGANIZATION ,
11371144 resourceId : organizationId ,
1138- description : `Admin set Enterprise seat capacity to ${ seats } ` ,
1145+ description : `Admin requested Enterprise seat capacity ${ seats } ` ,
11391146 metadata : { seats } ,
11401147 } )
11411148}
@@ -1346,7 +1353,7 @@ export async function updateDashboardOrganizationLimits(
13461353 } ,
13471354 actor : AdminMutationActor
13481355) {
1349- await db . transaction ( async ( tx ) => {
1356+ const providerBacked = await db . transaction ( async ( tx ) => {
13501357 await acquireOrganizationMutationLock ( tx , organizationId )
13511358 const [ org ] = await tx
13521359 . select ( )
@@ -1413,7 +1420,7 @@ export async function updateDashboardOrganizationLimits(
14131420 }
14141421 } ,
14151422 } )
1416- return
1423+ return true
14171424 }
14181425
14191426 const [ memberCountRow ] = await tx
@@ -1447,6 +1454,7 @@ export async function updateDashboardOrganizationLimits(
14471454 } )
14481455 . where ( eq ( subscription . id , subscriptionRow . id ) )
14491456 }
1457+ return false
14501458 } )
14511459 recordAudit ( {
14521460 actorId : actor . id ,
@@ -1455,7 +1463,9 @@ export async function updateDashboardOrganizationLimits(
14551463 action : AuditAction . ORGANIZATION_UPDATED ,
14561464 resourceType : AuditResourceType . ORGANIZATION ,
14571465 resourceId : organizationId ,
1458- description : 'Admin updated organization limits' ,
1466+ description : providerBacked
1467+ ? 'Admin requested Enterprise organization-limit update'
1468+ : 'Admin updated organization limits' ,
14591469 metadata : values ,
14601470 } )
14611471}
@@ -1681,35 +1691,62 @@ export async function grantDashboardUserBalance(
16811691
16821692export async function getDashboardMemberTransferPreflight (
16831693 destinationOrganizationId : string ,
1684- userId : string
1694+ userId : string ,
1695+ workspacePage : PaginationInput = { search : '' , limit : 50 , offset : 0 }
16851696) {
1686- const [ [ destination ] , [ target ] , personalWorkspaces ] = await Promise . all ( [
1687- db
1688- . select ( { id : organization . id } )
1689- . from ( organization )
1690- . where ( eq ( organization . id , destinationOrganizationId ) )
1691- . limit ( 1 ) ,
1692- db
1693- . select ( {
1694- id : user . id ,
1695- name : user . name ,
1696- email : user . email ,
1697- memberId : member . id ,
1698- role : member . role ,
1699- organizationId : member . organizationId ,
1700- organizationName : organization . name ,
1701- } )
1702- . from ( user )
1703- . leftJoin ( member , eq ( member . userId , user . id ) )
1704- . leftJoin ( organization , eq ( organization . id , member . organizationId ) )
1705- . where ( eq ( user . id , userId ) )
1706- . limit ( 1 ) ,
1707- db
1708- . select ( { id : workspace . id , name : workspace . name , archivedAt : workspace . archivedAt } )
1709- . from ( workspace )
1710- . where ( ownedAttachableWorkspacesWhere ( { userId, includeArchived : true } ) )
1711- . orderBy ( workspace . name , workspace . id ) ,
1712- ] )
1697+ const search = workspacePage . search . trim ( )
1698+ const limit = Math . min ( Math . max ( workspacePage . limit , 1 ) , 250 )
1699+ const offset = Math . max ( workspacePage . offset , 0 )
1700+ const allPersonalWorkspacesWhere = ownedAttachableWorkspacesWhere ( {
1701+ userId,
1702+ includeArchived : true ,
1703+ } )
1704+ const matchingPersonalWorkspacesWhere = and (
1705+ allPersonalWorkspacesWhere ,
1706+ search ? or ( eq ( workspace . id , search ) , ilike ( workspace . name , `%${ search } %` ) ) : undefined
1707+ )
1708+ const [ [ destination ] , [ target ] , personalWorkspaceCount , personalWorkspaces , selectionRows ] =
1709+ await Promise . all ( [
1710+ db
1711+ . select ( { id : organization . id } )
1712+ . from ( organization )
1713+ . where ( eq ( organization . id , destinationOrganizationId ) )
1714+ . limit ( 1 ) ,
1715+ db
1716+ . select ( {
1717+ id : user . id ,
1718+ name : user . name ,
1719+ email : user . email ,
1720+ memberId : member . id ,
1721+ role : member . role ,
1722+ organizationId : member . organizationId ,
1723+ organizationName : organization . name ,
1724+ } )
1725+ . from ( user )
1726+ . leftJoin ( member , eq ( member . userId , user . id ) )
1727+ . leftJoin ( organization , eq ( organization . id , member . organizationId ) )
1728+ . where ( eq ( user . id , userId ) )
1729+ . limit ( 1 ) ,
1730+ db . select ( { value : count ( ) } ) . from ( workspace ) . where ( matchingPersonalWorkspacesWhere ) ,
1731+ db
1732+ . select ( { id : workspace . id , name : workspace . name , archivedAt : workspace . archivedAt } )
1733+ . from ( workspace )
1734+ . where ( matchingPersonalWorkspacesWhere )
1735+ . orderBy ( workspace . name , workspace . id )
1736+ . limit ( limit )
1737+ . offset ( offset ) ,
1738+ db
1739+ . select ( {
1740+ id : workspace . id ,
1741+ name : workspace . name ,
1742+ archivedAt : workspace . archivedAt ,
1743+ total : sql < number > `count(*) over()` . mapWith ( Number ) ,
1744+ } )
1745+ . from ( workspace )
1746+ . where ( allPersonalWorkspacesWhere )
1747+ . orderBy ( workspace . id )
1748+ . limit ( MAX_ADMIN_MEMBER_WORKSPACE_SELECTION + 1 ) ,
1749+ ] )
17131750 if ( ! destination ) throw new Error ( 'Destination organization not found' )
17141751 if ( ! target ) throw new Error ( 'User not found' )
17151752
@@ -1724,6 +1761,9 @@ export async function getDashboardMemberTransferPreflight(
17241761 : credentialDependencies . length > 0
17251762 ? 'Reconnect or remove source-organization credentials owned by this user before transfer'
17261763 : null
1764+ const matchingWorkspaceTotal = personalWorkspaceCount [ 0 ] ?. value ?? 0
1765+ const totalEligibleWorkspaces = selectionRows [ 0 ] ?. total ?? 0
1766+ const includesAllEligible = totalEligibleWorkspaces <= MAX_ADMIN_MEMBER_WORKSPACE_SELECTION
17271767
17281768 return {
17291769 user : { id : target . id , name : target . name , email : target . email } ,
@@ -1736,6 +1776,24 @@ export async function getDashboardMemberTransferPreflight(
17361776 name : row . name ,
17371777 archived : row . archivedAt !== null ,
17381778 } ) ) ,
1779+ workspacePagination : {
1780+ total : matchingWorkspaceTotal ,
1781+ limit,
1782+ offset,
1783+ hasMore : offset + personalWorkspaces . length < matchingWorkspaceTotal ,
1784+ } ,
1785+ workspaceSelection : {
1786+ totalEligible : totalEligibleWorkspaces ,
1787+ defaultSelectedIds : includesAllEligible ? selectionRows . map ( ( row ) => row . id ) : [ ] ,
1788+ defaultSelectedWorkspaces : includesAllEligible
1789+ ? selectionRows . map ( ( { total : _total , archivedAt, ...row } ) => ( {
1790+ ...row ,
1791+ archived : archivedAt !== null ,
1792+ } ) )
1793+ : [ ] ,
1794+ includesAllEligible,
1795+ limit : MAX_ADMIN_MEMBER_WORKSPACE_SELECTION ,
1796+ } ,
17391797 credentialDependencies,
17401798 canAdd : reason === null ,
17411799 reason,
0 commit comments