Skip to content

Commit 950bf50

Browse files
committed
feat(admin): make dashboard billing operations durable
1 parent 85902eb commit 950bf50

59 files changed

Lines changed: 6806 additions & 607 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { getErrorMessage } from '@sim/utils/errors'
2+
import { toDashboardProvisioning } from '@/lib/admin/dashboard'
3+
import { adminDashboardRetryEnterpriseFollowUpJobContract } from '@/lib/api/contracts/v1/admin/dashboard'
4+
import { parseRequest } from '@/lib/api/server'
5+
import {
6+
EnterpriseProvisioningError,
7+
retryEnterpriseFollowUpJob,
8+
} from '@/lib/billing/enterprise-provisioning'
9+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
10+
import { getAdminAuditActor } from '@/app/api/v1/admin/dashboard/actor'
11+
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
12+
import {
13+
adminValidationErrorResponse,
14+
badRequestResponse,
15+
singleResponse,
16+
} from '@/app/api/v1/admin/responses'
17+
18+
interface RouteParams {
19+
id: string
20+
jobId: string
21+
}
22+
23+
export const POST = withRouteHandler(
24+
withAdminAuthParams<RouteParams>(async (request, context) => {
25+
const parsed = await parseRequest(
26+
adminDashboardRetryEnterpriseFollowUpJobContract,
27+
request,
28+
context,
29+
{ validationErrorResponse: adminValidationErrorResponse }
30+
)
31+
if (!parsed.success) return parsed.response
32+
try {
33+
return singleResponse(
34+
toDashboardProvisioning(
35+
await retryEnterpriseFollowUpJob(
36+
parsed.data.params.id,
37+
parsed.data.params.jobId,
38+
await getAdminAuditActor(request)
39+
)
40+
)
41+
)
42+
} catch (error) {
43+
return badRequestResponse(
44+
error instanceof EnterpriseProvisioningError
45+
? error.message
46+
: getErrorMessage(error, 'Failed to retry Enterprise follow-up job')
47+
)
48+
}
49+
})
50+
)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { getErrorMessage } from '@sim/utils/errors'
2+
import { toDashboardProvisioning } from '@/lib/admin/dashboard'
3+
import { adminDashboardRetryEnterpriseInvitationContract } from '@/lib/api/contracts/v1/admin/dashboard'
4+
import { parseRequest } from '@/lib/api/server'
5+
import {
6+
EnterpriseProvisioningError,
7+
retryEnterpriseInvitation,
8+
} from '@/lib/billing/enterprise-provisioning'
9+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
10+
import { getAdminAuditActor } from '@/app/api/v1/admin/dashboard/actor'
11+
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
12+
import {
13+
adminValidationErrorResponse,
14+
badRequestResponse,
15+
singleResponse,
16+
} from '@/app/api/v1/admin/responses'
17+
18+
interface RouteParams {
19+
id: string
20+
inviteId: string
21+
}
22+
23+
export const POST = withRouteHandler(
24+
withAdminAuthParams<RouteParams>(async (request, context) => {
25+
const parsed = await parseRequest(
26+
adminDashboardRetryEnterpriseInvitationContract,
27+
request,
28+
context,
29+
{ validationErrorResponse: adminValidationErrorResponse }
30+
)
31+
if (!parsed.success) return parsed.response
32+
try {
33+
return singleResponse(
34+
toDashboardProvisioning(
35+
await retryEnterpriseInvitation(
36+
parsed.data.params.id,
37+
parsed.data.params.inviteId,
38+
await getAdminAuditActor(request)
39+
)
40+
)
41+
)
42+
} catch (error) {
43+
return badRequestResponse(
44+
error instanceof EnterpriseProvisioningError
45+
? error.message
46+
: getErrorMessage(error, 'Failed to retry Enterprise invitation')
47+
)
48+
}
49+
})
50+
)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { createLogger } from '@sim/logger'
2+
import { getErrorMessage } from '@sim/utils/errors'
3+
import { adminDashboardEnterpriseReviewContract } from '@/lib/api/contracts/v1/admin/dashboard'
4+
import { parseRequest } from '@/lib/api/server'
5+
import { dollarsToCredits } from '@/lib/billing/credits/conversion'
6+
import {
7+
EnterpriseProvisioningError,
8+
reviewEnterpriseProvisioning,
9+
} from '@/lib/billing/enterprise-provisioning'
10+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
11+
import { withAdminAuth } from '@/app/api/v1/admin/middleware'
12+
import {
13+
adminInvalidJsonResponse,
14+
adminValidationErrorResponse,
15+
badRequestResponse,
16+
internalErrorResponse,
17+
singleResponse,
18+
} from '@/app/api/v1/admin/responses'
19+
20+
const logger = createLogger('AdminEnterpriseProvisioningReviewAPI')
21+
22+
export const POST = withRouteHandler(
23+
withAdminAuth(async (request) => {
24+
const parsed = await parseRequest(
25+
adminDashboardEnterpriseReviewContract,
26+
request,
27+
{},
28+
{
29+
validationErrorResponse: adminValidationErrorResponse,
30+
invalidJsonResponse: adminInvalidJsonResponse,
31+
}
32+
)
33+
if (!parsed.success) return parsed.response
34+
try {
35+
const { usageLimitDollars, ...body } = parsed.data.body
36+
return singleResponse(
37+
await reviewEnterpriseProvisioning({
38+
...body,
39+
usageLimitCredits:
40+
usageLimitDollars === undefined ? undefined : dollarsToCredits(usageLimitDollars),
41+
})
42+
)
43+
} catch (error) {
44+
if (error instanceof EnterpriseProvisioningError) return badRequestResponse(error.message)
45+
logger.error('Failed to review Enterprise provisioning', { error })
46+
return internalErrorResponse(getErrorMessage(error, 'Failed to review Enterprise plan'))
47+
}
48+
})
49+
)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export const POST = withRouteHandler(
4444
usageLimitDollars === undefined ? undefined : dollarsToCredits(usageLimitDollars),
4545
requestedByEmail: actor.email ?? 'admin-api',
4646
requestedByUserId: actor.id,
47+
requestedByName: actor.name,
4748
})
4849
)
4950
)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { createLogger } from '@sim/logger'
2+
import { getErrorMessage } from '@sim/utils/errors'
3+
import { getDashboardSubscriptionBillingActions } from '@/lib/admin/subscription-lifecycle'
4+
import { adminDashboardBillingActionsContract } from '@/lib/api/contracts/v1/admin/dashboard'
5+
import { parseRequest } from '@/lib/api/server'
6+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
7+
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
8+
import {
9+
adminValidationErrorResponse,
10+
badRequestResponse,
11+
singleResponse,
12+
} from '@/app/api/v1/admin/responses'
13+
14+
const logger = createLogger('AdminDashboardBillingActionsAPI')
15+
16+
export const GET = withRouteHandler(
17+
withAdminAuthParams<{ id: string }>(async (request, context) => {
18+
const parsed = await parseRequest(adminDashboardBillingActionsContract, request, context, {
19+
validationErrorResponse: adminValidationErrorResponse,
20+
})
21+
if (!parsed.success) return parsed.response
22+
try {
23+
return singleResponse(await getDashboardSubscriptionBillingActions(parsed.data.params.id))
24+
} catch (error) {
25+
logger.warn('Could not load organization billing actions', { error })
26+
return badRequestResponse(getErrorMessage(error, 'Could not load billing actions'))
27+
}
28+
})
29+
)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { getErrorMessage } from '@sim/utils/errors'
2+
import { requestDashboardSubscriptionCancellation } from '@/lib/admin/subscription-lifecycle'
3+
import { adminDashboardCancelSubscriptionContract } from '@/lib/api/contracts/v1/admin/dashboard'
4+
import { parseRequest } from '@/lib/api/server'
5+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
6+
import { getAdminAuditActor } from '@/app/api/v1/admin/dashboard/actor'
7+
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
8+
import {
9+
adminInvalidJsonResponse,
10+
adminValidationErrorResponse,
11+
badRequestResponse,
12+
singleResponse,
13+
} from '@/app/api/v1/admin/responses'
14+
15+
export const POST = withRouteHandler(
16+
withAdminAuthParams<{ id: string }>(async (request, context) => {
17+
const parsed = await parseRequest(adminDashboardCancelSubscriptionContract, request, context, {
18+
validationErrorResponse: adminValidationErrorResponse,
19+
invalidJsonResponse: adminInvalidJsonResponse,
20+
})
21+
if (!parsed.success) return parsed.response
22+
try {
23+
return singleResponse(
24+
await requestDashboardSubscriptionCancellation({
25+
organizationId: parsed.data.params.id,
26+
...parsed.data.body,
27+
actor: await getAdminAuditActor(request),
28+
})
29+
)
30+
} catch (error) {
31+
return badRequestResponse(getErrorMessage(error, 'Could not cancel subscription'))
32+
}
33+
})
34+
)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { getErrorMessage } from '@sim/utils/errors'
2+
import { retryAdminInvitationOperationJob } from '@/lib/admin/invitation-operation'
3+
import { adminDashboardRetryInvitationOperationJobContract } from '@/lib/api/contracts/v1/admin/dashboard'
4+
import { parseRequest } from '@/lib/api/server'
5+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
6+
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
7+
import {
8+
adminValidationErrorResponse,
9+
badRequestResponse,
10+
singleResponse,
11+
} from '@/app/api/v1/admin/responses'
12+
13+
interface RouteParams {
14+
id: string
15+
operationId: string
16+
jobId: string
17+
}
18+
19+
export const POST = withRouteHandler(
20+
withAdminAuthParams<RouteParams>(async (request, context) => {
21+
const parsed = await parseRequest(
22+
adminDashboardRetryInvitationOperationJobContract,
23+
request,
24+
context,
25+
{ validationErrorResponse: adminValidationErrorResponse }
26+
)
27+
if (!parsed.success) return parsed.response
28+
try {
29+
return singleResponse(
30+
await retryAdminInvitationOperationJob(
31+
parsed.data.params.id,
32+
parsed.data.params.operationId,
33+
parsed.data.params.jobId
34+
)
35+
)
36+
} catch (error) {
37+
return badRequestResponse(getErrorMessage(error, 'Failed to retry invitation operation job'))
38+
}
39+
})
40+
)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { getErrorMessage } from '@sim/utils/errors'
2+
import { getAdminInvitationOperation } from '@/lib/admin/invitation-operation'
3+
import { adminDashboardGetInvitationOperationContract } from '@/lib/api/contracts/v1/admin/dashboard'
4+
import { parseRequest } from '@/lib/api/server'
5+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
6+
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
7+
import {
8+
adminValidationErrorResponse,
9+
badRequestResponse,
10+
singleResponse,
11+
} from '@/app/api/v1/admin/responses'
12+
13+
interface RouteParams {
14+
id: string
15+
operationId: string
16+
}
17+
18+
export const GET = withRouteHandler(
19+
withAdminAuthParams<RouteParams>(async (request, context) => {
20+
const parsed = await parseRequest(
21+
adminDashboardGetInvitationOperationContract,
22+
request,
23+
context,
24+
{ validationErrorResponse: adminValidationErrorResponse }
25+
)
26+
if (!parsed.success) return parsed.response
27+
try {
28+
return singleResponse(
29+
await getAdminInvitationOperation(parsed.data.params.id, parsed.data.params.operationId)
30+
)
31+
} catch (error) {
32+
return badRequestResponse(getErrorMessage(error, 'Failed to load invitation operation'))
33+
}
34+
})
35+
)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { getErrorMessage } from '@sim/utils/errors'
2+
import { createAdminInvitationOperation } from '@/lib/admin/invitation-operation'
3+
import { adminDashboardInvitePeopleContract } from '@/lib/api/contracts/v1/admin/dashboard'
4+
import { parseRequest } from '@/lib/api/server'
5+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
6+
import { getAdminAuditActor } from '@/app/api/v1/admin/dashboard/actor'
7+
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
8+
import {
9+
adminInvalidJsonResponse,
10+
adminValidationErrorResponse,
11+
badRequestResponse,
12+
singleResponse,
13+
} from '@/app/api/v1/admin/responses'
14+
15+
export const POST = withRouteHandler(
16+
withAdminAuthParams<{ id: string }>(async (request, context) => {
17+
const parsed = await parseRequest(adminDashboardInvitePeopleContract, request, context, {
18+
validationErrorResponse: adminValidationErrorResponse,
19+
invalidJsonResponse: adminInvalidJsonResponse,
20+
})
21+
if (!parsed.success) return parsed.response
22+
try {
23+
return singleResponse(
24+
await createAdminInvitationOperation({
25+
operationId: parsed.data.body.operationId,
26+
organizationId: parsed.data.params.id,
27+
emails: parsed.data.body.emails,
28+
workspaceIds: parsed.data.body.workspaceIds,
29+
role: parsed.data.body.role,
30+
permission: parsed.data.body.permission,
31+
actor: await getAdminAuditActor(request),
32+
})
33+
)
34+
} catch (error) {
35+
return badRequestResponse(getErrorMessage(error, 'Failed to invite people'))
36+
}
37+
})
38+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { getErrorMessage } from '@sim/utils/errors'
2+
import { retryAdminMemberFollowUpJob } from '@/lib/admin/member-operation'
3+
import { adminDashboardRetryMemberFollowUpJobContract } from '@/lib/api/contracts/v1/admin/dashboard'
4+
import { parseRequest } from '@/lib/api/server'
5+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
6+
import { getAdminAuditActor } from '@/app/api/v1/admin/dashboard/actor'
7+
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
8+
import {
9+
adminValidationErrorResponse,
10+
badRequestResponse,
11+
singleResponse,
12+
} from '@/app/api/v1/admin/responses'
13+
14+
interface RouteParams {
15+
id: string
16+
operationId: string
17+
jobId: string
18+
}
19+
20+
export const POST = withRouteHandler(
21+
withAdminAuthParams<RouteParams>(async (request, context) => {
22+
const parsed = await parseRequest(
23+
adminDashboardRetryMemberFollowUpJobContract,
24+
request,
25+
context,
26+
{ validationErrorResponse: adminValidationErrorResponse }
27+
)
28+
if (!parsed.success) return parsed.response
29+
try {
30+
return singleResponse(
31+
await retryAdminMemberFollowUpJob(
32+
parsed.data.params.id,
33+
parsed.data.params.operationId,
34+
parsed.data.params.jobId,
35+
await getAdminAuditActor(request)
36+
)
37+
)
38+
} catch (error) {
39+
return badRequestResponse(
40+
getErrorMessage(error, 'Could not retry member-operation follow-up job')
41+
)
42+
}
43+
})
44+
)

0 commit comments

Comments
 (0)