close
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getErrorMessage } from '@sim/utils/errors'
import { previewDashboardEnterpriseBillingTerms } from '@/lib/admin/dashboard'
import { adminDashboardPreviewBillingTermsContract } from '@/lib/api/contracts/v1/admin/dashboard'
import { previewDashboardEnterpriseReportingPeriod } from '@/lib/admin/dashboard'
import { adminDashboardPreviewReportingPeriodContract } from '@/lib/api/contracts/v1/admin/dashboard'
import { parseRequest } from '@/lib/api/server'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
Expand All @@ -13,18 +13,23 @@ import {

export const POST = withRouteHandler(
withAdminAuthParams<{ id: string }>(async (request, context) => {
const parsed = await parseRequest(adminDashboardPreviewBillingTermsContract, request, context, {
validationErrorResponse: adminValidationErrorResponse,
invalidJsonResponse: adminInvalidJsonResponse,
})
const parsed = await parseRequest(
adminDashboardPreviewReportingPeriodContract,
request,
context,
{
validationErrorResponse: adminValidationErrorResponse,
invalidJsonResponse: adminInvalidJsonResponse,
}
)
if (!parsed.success) return parsed.response
try {
return singleResponse(
await previewDashboardEnterpriseBillingTerms(parsed.data.params.id, parsed.data.body)
await previewDashboardEnterpriseReportingPeriod(parsed.data.params.id, parsed.data.body)
)
} catch (error) {
return badRequestResponse(
getErrorMessage(error, 'Failed to preview Enterprise billing terms')
getErrorMessage(error, 'Failed to preview Enterprise reporting period')
)
}
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getErrorMessage } from '@sim/utils/errors'
import { updateDashboardEnterpriseBillingTerms } from '@/lib/admin/dashboard'
import { adminDashboardUpdateBillingTermsContract } from '@/lib/api/contracts/v1/admin/dashboard'
import { updateDashboardEnterpriseReportingPeriod } from '@/lib/admin/dashboard'
import { adminDashboardUpdateReportingPeriodContract } from '@/lib/api/contracts/v1/admin/dashboard'
import { parseRequest } from '@/lib/api/server'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getAdminAuditActor } from '@/app/api/v1/admin/dashboard/actor'
Expand All @@ -14,20 +14,27 @@ import {

export const PATCH = withRouteHandler(
withAdminAuthParams<{ id: string }>(async (request, context) => {
const parsed = await parseRequest(adminDashboardUpdateBillingTermsContract, request, context, {
validationErrorResponse: adminValidationErrorResponse,
invalidJsonResponse: adminInvalidJsonResponse,
})
const parsed = await parseRequest(
adminDashboardUpdateReportingPeriodContract,
request,
context,
{
validationErrorResponse: adminValidationErrorResponse,
invalidJsonResponse: adminInvalidJsonResponse,
}
)
if (!parsed.success) return parsed.response
try {
await updateDashboardEnterpriseBillingTerms(
await updateDashboardEnterpriseReportingPeriod(
parsed.data.params.id,
parsed.data.body,
await getAdminAuditActor(request)
)
return singleResponse({ success: true as const })
} catch (error) {
return badRequestResponse(getErrorMessage(error, 'Failed to update Enterprise billing terms'))
return badRequestResponse(
getErrorMessage(error, 'Failed to update Enterprise reporting period')
)
}
})
)
76 changes: 58 additions & 18 deletions apps/sim/lib/admin/dashboard-organizations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ import {
getDashboardOrganization,
listDashboardOrganizations,
toDashboardConfigurationUpdate,
updateDashboardEnterpriseBillingTerms,
updateDashboardEnterpriseReportingPeriod,
updateDashboardEnterpriseSeats,
updateDashboardOrganizationLimits,
} from '@/lib/admin/dashboard'
Expand Down Expand Up @@ -189,22 +189,51 @@ describe('toDashboardConfigurationUpdate', () => {
concurrencyLimit: 50,
},
requestedTerms: null,
providerAccepted: false,
error: null,
},
})
).toEqual({
id: 'config-2',
status: 'pending',
requestedUsageLimitDollars: 50_000,
requestedInvoiceAmountUsd: null,
requestedBillingInterval: null,
requestedReportingPeriodInterval: null,
requestedReportingPeriodAnchorDate: null,
requestedSeats: 20,
requestedConcurrencyLimit: 50,
requestedWorkflowExecutionTimeoutSeconds: null,
providerAccepted: false,
retryable: true,
error: null,
})
})

it('surfaces a legacy coupled cadence as reporting-only and disables retry', () => {
expect(
toDashboardConfigurationUpdate({
latestRevision: 3,
desiredMetadata: {},
desiredTerms: null,
hasUnappliedIntent: true,
effectiveSeatCapacity: 20,
configurationUpdate: {
id: 'legacy-config',
status: 'failed',
requestedMetadata: {
reportingPeriodAnchorDate: '2026-05-01',
seats: 20,
},
requestedTerms: { invoiceAmountCents: 50_000, billingInterval: 'year' },
providerAccepted: false,
error: 'Commercial-term updates are unsupported',
},
})
).toMatchObject({
requestedReportingPeriodAnchorDate: '2026-05-01',
requestedReportingPeriodInterval: 'year',
retryable: false,
})
})
})

describe('listDashboardOrganizations', () => {
Expand Down Expand Up @@ -458,7 +487,7 @@ describe('updateDashboardOrganizationLimits', () => {
})
})

describe('updateDashboardEnterpriseBillingTerms', () => {
describe('updateDashboardEnterpriseReportingPeriod', () => {
beforeEach(() => {
vi.clearAllMocks()
resetDbChainMock()
Expand All @@ -477,23 +506,27 @@ describe('updateDashboardEnterpriseBillingTerms', () => {
})
})

it('queues a cadence and immutable Price change through the existing Stripe intent', async () => {
it('queues only independent reporting metadata and preserves commercial metadata', async () => {
queueTableRows(subscription, [
{
id: 'sub-1',
stripeSubscriptionId: 'stripe-sub-1',
plan: 'enterprise',
status: 'active',
billingInterval: 'month',
metadata: { plan: 'enterprise', referenceId: 'org-1', monthlyPrice: 125, seats: 10 },
metadata: {
plan: 'enterprise',
referenceId: 'org-1',
monthlyPrice: 125,
seats: 10,
},
},
])

await updateDashboardEnterpriseBillingTerms(
await updateDashboardEnterpriseReportingPeriod(
'org-1',
{
invoiceAmountUsd: 1200,
billingInterval: 'year',
reportingPeriodInterval: 'year',
reportingPeriodAnchorDate: '2026-01-31',
},
{ id: 'admin-1', name: 'Admin', email: 'admin@sim.ai' }
Expand All @@ -504,19 +537,17 @@ describe('updateDashboardEnterpriseBillingTerms', () => {
'stripe.sync-enterprise-metadata',
expect.objectContaining({
revision: 4,
terms: { invoiceAmountCents: 120_000, billingInterval: 'year' },
metadata: expect.objectContaining({
invoiceAmountCents: 120_000,
monthlyPrice: 125,
reportingPeriodAnchorDate: '2026-01-31',
reportingPeriodInterval: 'year',
}),
})
)
expect(
(mocks.enqueueOutboxEvent.mock.calls[0][2] as { metadata: Record<string, unknown> }).metadata
).toMatchObject({ monthlyPrice: null })
expect(mocks.enqueueOutboxEvent.mock.calls[0][2]).not.toHaveProperty('terms')
})

it('updates only metadata when the applied Price already matches', async () => {
it('does not compare the requested reporting cadence with the Stripe cadence', async () => {
queueTableRows(subscription, [
{
id: 'sub-1',
Expand All @@ -528,16 +559,25 @@ describe('updateDashboardEnterpriseBillingTerms', () => {
},
])

await updateDashboardEnterpriseBillingTerms(
await updateDashboardEnterpriseReportingPeriod(
'org-1',
{
invoiceAmountUsd: 1200,
billingInterval: 'year',
reportingPeriodInterval: 'month',
reportingPeriodAnchorDate: '2025-01-31',
},
{ id: 'admin-1', name: 'Admin', email: 'admin@sim.ai' }
)

expect(mocks.enqueueOutboxEvent.mock.calls[0][2]).not.toHaveProperty('terms')
expect(mocks.enqueueOutboxEvent.mock.calls[0][2]).toMatchObject({
metadata: {
plan: 'enterprise',
referenceId: 'org-1',
seats: 10,
monthlyPrice: 125,
reportingPeriodAnchorDate: '2025-01-31',
reportingPeriodInterval: 'month',
},
})
})
})
Loading
Loading