Skip to content

Commit f492805

Browse files
authored
fix(billing): keep archived selected workspaces in the invitation applied-state check (#8159)
1 parent 5242e46 commit f492805

2 files changed

Lines changed: 57 additions & 5 deletions

File tree

‎apps/sim/lib/billing/enterprise-provisioning.test.ts‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,56 @@ describe('Enterprise creation invitations', () => {
929929
}
930930
)
931931

932+
it('reads applied access on an archived selected workspace, which the selection keeps', async () => {
933+
const payload = operationPayload({
934+
request: { ...operationPayload().request, workspaceIds: ['workspace-1'] },
935+
applicationResult: { appliedAt: '2026-08-13T00:00:00.000Z', subscriptionId: 'sub-1' },
936+
})
937+
queueTableRows(schemaMock.outboxEvent, [{ eventType: 'stripe.provision-enterprise', payload }])
938+
queueTableRows(schemaMock.outboxEvent, [])
939+
queueTableRows(schemaMock.outboxEvent, [{ status: 'completed' }])
940+
queueTableRows(schemaMock.user, [
941+
{ userId: 'invitee-1', workspaceId: 'workspace-1', role: 'admin', permission: 'admin' },
942+
])
943+
queueTableRows(schemaMock.invitation, [])
944+
945+
await expect(
946+
inviteEnterprisePeople(
947+
{
948+
provisioningOperationId: 'operation-1',
949+
organizationId: 'org-1',
950+
ownerUserId: 'owner-1',
951+
email: 'new@example.com',
952+
role: 'member',
953+
permission: 'write',
954+
sequence: 0,
955+
},
956+
{
957+
eventId: 'invite-1',
958+
eventType: 'enterprise.invite-people',
959+
attempts: 0,
960+
checkpointPayload: vi.fn(),
961+
}
962+
)
963+
).resolves.toBeUndefined()
964+
965+
/**
966+
* The applied-state join scopes workspaces to the organization and the selection only.
967+
* An archived filter here would turn already-applied access on an archived selected
968+
* workspace into a missing one, scheduling an invitation the archived workspace refuses.
969+
*/
970+
const joinConditions = dbChainMockFns.innerJoin.mock.calls.map(([, condition]) =>
971+
JSON.stringify(condition)
972+
)
973+
const workspaceJoin = joinConditions.find((condition) =>
974+
condition.includes('workspace.organizationId')
975+
)
976+
expect(workspaceJoin).toBeDefined()
977+
expect(workspaceJoin).toContain('"workspace-1"')
978+
expect(workspaceJoin).not.toContain('workspace.archivedAt')
979+
expect(mocks.createWorkspaceInvitation).not.toHaveBeenCalled()
980+
})
981+
932982
it('waits without consuming attempts until every selected workspace move completes', async () => {
933983
const payload = operationPayload({
934984
request: {

‎apps/sim/lib/billing/enterprise-provisioning.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2758,13 +2758,15 @@ async function resolveEnterpriseInvitationApplicationState(
27582758
member,
27592759
and(eq(member.userId, user.id), eq(member.organizationId, payload.organizationId))
27602760
)
2761+
/**
2762+
* Archived workspaces stay in the join: the selection keeps them on purpose (the move
2763+
* carries them into the organization so they can be unarchived later), so a recipient
2764+
* whose access on one is already in place must read as applied, or the sweep would
2765+
* schedule an invitation the archived workspace cannot accept and never converge.
2766+
*/
27612767
.innerJoin(
27622768
workspace,
2763-
and(
2764-
eq(workspace.organizationId, member.organizationId),
2765-
inArray(workspace.id, workspaceIds),
2766-
isNull(workspace.archivedAt)
2767-
)
2769+
and(eq(workspace.organizationId, member.organizationId), inArray(workspace.id, workspaceIds))
27682770
)
27692771
.leftJoin(
27702772
permissions,

0 commit comments

Comments
 (0)