Skip to content

Commit 6f37e96

Browse files
committed
fix(enterprise): keep owner activation recoverable
1 parent b92eb75 commit 6f37e96

3 files changed

Lines changed: 48 additions & 8 deletions

File tree

apps/sim/app/api/v1/admin/dashboard/enterprise-owner-claims/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { parseRequest } from '@/lib/api/server'
88
import { dollarsToCredits } from '@/lib/billing/credits/conversion'
99
import {
1010
createEnterpriseOwnerClaim,
11-
getPendingEnterpriseOwnerClaimsPage,
11+
getOpenEnterpriseOwnerClaimsPage,
1212
} from '@/lib/billing/enterprise-owner-claim'
1313
import { EnterpriseProvisioningError } from '@/lib/billing/enterprise-provisioning'
1414
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
@@ -36,7 +36,7 @@ export const GET = withRouteHandler(
3636
if (!parsed.success) return parsed.response
3737
try {
3838
const { limit, offset } = parsed.data.query
39-
const result = await getPendingEnterpriseOwnerClaimsPage({ limit, offset })
39+
const result = await getOpenEnterpriseOwnerClaimsPage({ limit, offset })
4040
return listResponse(result.data, {
4141
total: result.total,
4242
limit,

apps/sim/lib/billing/enterprise-owner-claim.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ import {
7777
acceptEnterpriseOwnerClaim,
7878
enterpriseOwnerClaimOutboxHandlers,
7979
getEnterpriseOwnerClaimDetails,
80+
retryEnterpriseOwnerClaim,
8081
reviewEnterpriseOwnerClaim,
8182
revokeEnterpriseOwnerClaim,
8283
} from '@/lib/billing/enterprise-owner-claim'
@@ -372,4 +373,36 @@ describe('Enterprise future-owner claims', () => {
372373
expect(mocks.createOrganization).not.toHaveBeenCalled()
373374
expect(mocks.enqueue).not.toHaveBeenCalled()
374375
})
376+
377+
it('repairs the parent claim when activation already attached provisioning', async () => {
378+
const accepted = {
379+
acceptedAt: '2026-08-20T12:00:00.000Z',
380+
ownerUserId: 'owner-1',
381+
organizationId: 'org-1',
382+
workspaceIds: ['workspace-1'],
383+
reportingPeriodAnchorDate: '2026-08-20',
384+
activationEventId: 'activation-1',
385+
createdDefaultWorkspaceId: null,
386+
}
387+
const acceptedPayload = { ...claimPayload(), acceptance: accepted }
388+
const activationRow = {
389+
...claimRow({ claimId: 'claim-1', provisioningOperationId: 'provisioning-1' }),
390+
id: 'activation-1',
391+
eventType: 'enterprise.activate-owner-claim',
392+
}
393+
queueTableRows(outboxEvent, [claimRow(acceptedPayload)])
394+
queueTableRows(outboxEvent, [activationRow])
395+
queueTableRows(outboxEvent, [
396+
claimRow({ ...acceptedPayload, provisioningOperationId: 'provisioning-1' }),
397+
])
398+
queueTableRows(outboxEvent, [activationRow])
399+
400+
const result = await retryEnterpriseOwnerClaim('claim-1')
401+
402+
expect(result).toMatchObject({ status: 'applied', provisioningOperationId: 'provisioning-1' })
403+
expect(mocks.patchPayload).toHaveBeenCalledWith(expect.anything(), 'claim-1', {
404+
provisioningOperationId: 'provisioning-1',
405+
})
406+
expect(mocks.issueProvisioning).not.toHaveBeenCalled()
407+
})
375408
})

apps/sim/lib/billing/enterprise-owner-claim.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -448,21 +448,25 @@ export async function getEnterpriseOwnerClaimView(
448448
return toClaimView(row, payload)
449449
}
450450

451-
export async function getPendingEnterpriseOwnerClaimsPage(params: {
451+
export async function getOpenEnterpriseOwnerClaimsPage(params: {
452452
limit: number
453453
offset: number
454454
}): Promise<{ data: EnterpriseOwnerClaimView[]; total: number }> {
455-
const unacceptedClaim = and(
455+
const openClaim = and(
456456
eq(outboxEvent.eventType, ENTERPRISE_OWNER_CLAIM_EVENT_TYPE),
457-
sql`${outboxEvent.payload} -> 'acceptance' is null`,
458-
sql`${outboxEvent.payload} -> 'revokedAt' is null`
457+
sql`${outboxEvent.payload} -> 'revokedAt' is null`,
458+
sql`${outboxEvent.payload} -> 'provisioningOperationId' is null`,
459+
or(
460+
sql`${outboxEvent.payload} -> 'acceptance' is not null`,
461+
sql`(${outboxEvent.payload} ->> 'expiresAt')::timestamptz > now()`
462+
)
459463
)
460464
const [countRows, rows] = await Promise.all([
461-
db.select({ value: count() }).from(outboxEvent).where(unacceptedClaim),
465+
db.select({ value: count() }).from(outboxEvent).where(openClaim),
462466
db
463467
.select()
464468
.from(outboxEvent)
465-
.where(unacceptedClaim)
469+
.where(openClaim)
466470
.orderBy(desc(outboxEvent.createdAt), desc(outboxEvent.id))
467471
.limit(params.limit)
468472
.offset(params.offset),
@@ -646,6 +650,9 @@ export async function retryEnterpriseOwnerClaim(
646650
throw new Error('Enterprise owner activation payload is invalid')
647651
provisioningOperationId =
648652
payload.provisioningOperationId ?? activationPayload.data.provisioningOperationId ?? null
653+
if (!payload.provisioningOperationId && provisioningOperationId) {
654+
await patchOutboxEventPayload(tx, claimId, { provisioningOperationId })
655+
}
649656
if (activationRow.status === 'dead_letter') {
650657
await resetOutboxEventForRetry(tx, activationRow.id)
651658
eventIdToProcess = activationRow.id

0 commit comments

Comments
 (0)