From ecf64b3bade7698fe23d4db19f259894e44f5175 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 18 Aug 2026 16:34:14 -0700 Subject: [PATCH 1/4] feat(billing): align enterprise reporting periods --- .../app/api/billing/update-cost/route.test.ts | 4 + apps/sim/app/api/billing/update-cost/route.ts | 3 + .../copilot/api-keys/validate/route.test.ts | 16 +- .../api/copilot/api-keys/validate/route.ts | 5 +- .../[id]/members/[memberId]/route.ts | 31 +- .../api/organizations/[id]/members/route.ts | 44 +- .../workspace-moves/[moveId]/retry/route.ts | 50 + .../preflight/route.ts | 35 + .../[id]/billing-terms/preview/route.ts | 31 + .../organizations/[id]/billing-terms/route.ts | 33 + .../[id]/configuration-update/retry/route.ts | 38 + .../dashboard/organizations/[id]/route.ts | 2 +- .../[id]/members/[memberId]/route.ts | 12 +- .../admin/organizations/[id]/members/route.ts | 14 +- .../lib/admin/dashboard-organizations.test.ts | 227 +- apps/sim/lib/admin/dashboard.ts | 834 +- .../api/contracts/v1/admin/dashboard.test.ts | 126 +- .../lib/api/contracts/v1/admin/dashboard.ts | 269 +- .../lib/billing/calculations/usage-monitor.ts | 55 +- .../billing/core/billing-attribution.test.ts | 18 + .../lib/billing/core/billing-attribution.ts | 93 +- apps/sim/lib/billing/core/organization.ts | 67 +- .../lib/billing/core/reporting-period.test.ts | 62 + apps/sim/lib/billing/core/reporting-period.ts | 106 + apps/sim/lib/billing/core/usage-log.ts | 61 +- apps/sim/lib/billing/core/usage.ts | 52 +- .../billing/enterprise-credit-limits.test.ts | 10 +- .../lib/billing/enterprise-credit-limits.ts | 6 +- .../sim/lib/billing/enterprise-outbox.test.ts | 82 + apps/sim/lib/billing/enterprise-outbox.ts | 100 +- .../billing/enterprise-provisioning.test.ts | 626 +- .../lib/billing/enterprise-provisioning.ts | 854 +- .../organizations/member-limits.test.ts | 21 + .../billing/organizations/member-limits.ts | 59 +- apps/sim/lib/billing/subscriptions/utils.ts | 2 +- .../sim/lib/billing/threshold-billing.test.ts | 40 + apps/sim/lib/billing/threshold-billing.ts | 24 +- apps/sim/lib/billing/types/index.test.ts | 18 + apps/sim/lib/billing/types/index.ts | 69 +- .../lib/billing/webhooks/enterprise.test.ts | 132 +- apps/sim/lib/billing/webhooks/enterprise.ts | 197 +- apps/sim/lib/core/outbox/service.test.ts | 40 + apps/sim/lib/core/outbox/service.ts | 83 +- apps/sim/lib/logs/execution/logger.ts | 18 +- apps/sim/lib/posthog/events.ts | 3 +- .../db/migrations/0290_workable_jigsaw.sql | 11 + .../db/migrations/meta/0290_snapshot.json | 19139 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 10 + scripts/check-api-validation-contracts.ts | 4 +- 50 files changed, 23226 insertions(+), 617 deletions(-) create mode 100644 apps/sim/app/api/v1/admin/dashboard/enterprise-provisioning/[id]/workspace-moves/[moveId]/retry/route.ts create mode 100644 apps/sim/app/api/v1/admin/dashboard/enterprise-provisioning/preflight/route.ts create mode 100644 apps/sim/app/api/v1/admin/dashboard/organizations/[id]/billing-terms/preview/route.ts create mode 100644 apps/sim/app/api/v1/admin/dashboard/organizations/[id]/billing-terms/route.ts create mode 100644 apps/sim/app/api/v1/admin/dashboard/organizations/[id]/configuration-update/retry/route.ts create mode 100644 apps/sim/lib/billing/core/reporting-period.test.ts create mode 100644 apps/sim/lib/billing/core/reporting-period.ts create mode 100644 packages/db/migrations/0290_workable_jigsaw.sql create mode 100644 packages/db/migrations/meta/0290_snapshot.json diff --git a/apps/sim/app/api/billing/update-cost/route.test.ts b/apps/sim/app/api/billing/update-cost/route.test.ts index 2edb84d2a31..cb1abf8f194 100644 --- a/apps/sim/app/api/billing/update-cost/route.test.ts +++ b/apps/sim/app/api/billing/update-cost/route.test.ts @@ -88,6 +88,7 @@ const ACCOUNT_BILLING_DECISION = { billingPeriod: { start: '2026-07-01T00:00:00.000Z', end: '2026-08-01T00:00:00.000Z', + source: 'reporting' as const, }, } @@ -418,6 +419,7 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => { billingPeriod: { start: new Date('2026-07-01T00:00:00.000Z'), end: new Date('2026-08-01T00:00:00.000Z'), + source: 'reporting', }, }) ) @@ -431,6 +433,7 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => { expectedBillingPeriod: { start: new Date('2026-07-01T00:00:00.000Z'), end: new Date('2026-08-01T00:00:00.000Z'), + source: 'reporting', }, } ) @@ -660,6 +663,7 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => { expectedBillingPeriod: { start: new Date('2026-07-01T00:00:00.000Z'), end: new Date('2026-08-01T00:00:00.000Z'), + source: 'reporting', }, } ) diff --git a/apps/sim/app/api/billing/update-cost/route.ts b/apps/sim/app/api/billing/update-cost/route.ts index 308d7c31958..854daaa725e 100644 --- a/apps/sim/app/api/billing/update-cost/route.ts +++ b/apps/sim/app/api/billing/update-cost/route.ts @@ -280,6 +280,9 @@ async function updateCostInner(req: NextRequest, span: Span): Promise { billingPeriod: { start: new Date(ACCOUNT_BILLING_DECISION.billingPeriod.start), end: new Date(ACCOUNT_BILLING_DECISION.billingPeriod.end), + source: ACCOUNT_BILLING_DECISION.billingPeriod.source, }, }) mockGetUserEntityPermissions.mockResolvedValue('read') @@ -387,11 +390,16 @@ describe('POST /api/copilot/api-keys/validate billing protocols', () => { ) expect(res.status).toBe(200) - expect(mockCheckServerSideUsageLimits).toHaveBeenCalledWith('user-1', ACCOUNT_SUBSCRIPTION) + expect(mockCheckServerSideUsageLimits).toHaveBeenCalledWith( + 'user-1', + ACCOUNT_SUBSCRIPTION, + expect.objectContaining({ billingEntity: ACCOUNT_BILLING_DECISION.billingEntity }) + ) expect(mockCheckAttributedUsageLimits).not.toHaveBeenCalled() expect(mockResolveBillingAttribution).not.toHaveBeenCalled() expect(mockGetUserEntityPermissions).not.toHaveBeenCalled() expect(mockGetWorkspaceBillingSettings).not.toHaveBeenCalled() + expect(mockSerializeAccountBillingDecisionHeader).toHaveBeenCalledWith(ACCOUNT_BILLING_DECISION) expect(res.headers.get('x-sim-billing-account-decision')).toBe('serialized-account-decision') }) @@ -407,7 +415,11 @@ describe('POST /api/copilot/api-keys/validate billing protocols', () => { ) expect(res.status).toBe(200) - expect(mockCheckServerSideUsageLimits).toHaveBeenCalledWith('user-1', ACCOUNT_SUBSCRIPTION) + expect(mockCheckServerSideUsageLimits).toHaveBeenCalledWith( + 'user-1', + ACCOUNT_SUBSCRIPTION, + expect.objectContaining({ billingEntity: ACCOUNT_BILLING_DECISION.billingEntity }) + ) }) it('fails direct-v1 admission closed when its payer cannot be resolved', async () => { diff --git a/apps/sim/app/api/copilot/api-keys/validate/route.ts b/apps/sim/app/api/copilot/api-keys/validate/route.ts index eaed8ca72fe..0e5c3fb153d 100644 --- a/apps/sim/app/api/copilot/api-keys/validate/route.ts +++ b/apps/sim/app/api/copilot/api-keys/validate/route.ts @@ -169,7 +169,7 @@ async function checkAdmissionUsage(admission: AdmissionBillingDecision): Promise onError: 'throw', }) const billingContext = deriveBillingContext(admission.userId, subscription) - const usage = await checkServerSideUsageLimits(admission.userId, subscription) + const usage = await checkServerSideUsageLimits(admission.userId, subscription, billingContext) return { isExceeded: usage.isExceeded, currentUsage: usage.currentUsage, @@ -181,6 +181,9 @@ async function checkAdmissionUsage(admission: AdmissionBillingDecision): Promise billingPeriod: { start: billingContext.billingPeriod.start.toISOString(), end: billingContext.billingPeriod.end.toISOString(), + ...(billingContext.billingPeriod.source + ? { source: billingContext.billingPeriod.source } + : {}), }, }, } diff --git a/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts b/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts index 38e2e831c8e..96942b68a89 100644 --- a/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts +++ b/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts @@ -9,8 +9,7 @@ import { updateOrganizationMemberRoleContract } from '@/lib/api/contracts/organi import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { setActiveOrganizationForCurrentSession } from '@/lib/auth/active-organization' -import { getOrgMemberLedgerByUser } from '@/lib/billing/core/organization' -import { getUserUsageData } from '@/lib/billing/core/usage' +import { getOrganizationMemberUsageSnapshot } from '@/lib/billing/core/organization' import { removeExternalUserFromOrganizationWorkspaces, removeUserFromOrganization, @@ -99,31 +98,23 @@ export const GET = withRouteHandler( .where(eq(userStats.userId, memberId)) .limit(1) - const computed = await getUserUsageData(memberId, dbReplica) - if (usageData.length > 0) { - // currentPeriodCost is only a baseline; add this member's attributed - // usage_log for the period. (getUserUsageData returns the org POOL for - // org-scoped members, so it can't supply the per-member figure.) - const memberLedger = - ( - await getOrgMemberLedgerByUser( - organizationId, - computed.billingPeriodStart && computed.billingPeriodEnd - ? { start: computed.billingPeriodStart, end: computed.billingPeriodEnd } - : null, - dbReplica - ) - ).get(memberId) ?? 0 + const { billingPeriod, includeLegacyBaseline, usageByUser } = + await getOrganizationMemberUsageSnapshot(organizationId, { + executor: dbReplica, + userIds: [memberId], + }) + const memberLedger = usageByUser.get(memberId) ?? 0 memberData = { ...memberData, usage: { ...usageData[0], currentPeriodCost: ( - Number(usageData[0].currentPeriodCost ?? 0) + memberLedger + (includeLegacyBaseline ? Number(usageData[0].currentPeriodCost ?? 0) : 0) + + memberLedger ).toString(), - billingPeriodStart: computed.billingPeriodStart, - billingPeriodEnd: computed.billingPeriodEnd, + billingPeriodStart: billingPeriod?.start ?? null, + billingPeriodEnd: billingPeriod?.end ?? null, }, } as typeof memberData & { usage: (typeof usageData)[0] & { diff --git a/apps/sim/app/api/organizations/[id]/members/route.ts b/apps/sim/app/api/organizations/[id]/members/route.ts index 416d5c3ba03..c03740f35c4 100644 --- a/apps/sim/app/api/organizations/[id]/members/route.ts +++ b/apps/sim/app/api/organizations/[id]/members/route.ts @@ -1,8 +1,8 @@ import { db } from '@sim/db' -import { member, subscription as subscriptionTable, user, userStats } from '@sim/db/schema' +import { member, user, userStats } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { isOrgAdminRole } from '@sim/platform-authz/workspace' -import { and, eq, inArray } from 'drizzle-orm' +import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { organizationMemberQuerySchema, @@ -10,8 +10,7 @@ import { } from '@/lib/api/contracts/organization' import { getValidationErrorMessage } from '@/lib/api/server' import { getSession } from '@/lib/auth' -import { getOrgMemberLedgerByUser } from '@/lib/billing/core/organization' -import { ENTITLED_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils' +import { getOrganizationMemberUsageSnapshot } from '@/lib/billing/core/organization' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' const logger = createLogger('OrganizationMembersAPI') @@ -101,41 +100,18 @@ export const GET = withRouteHandler( .leftJoin(userStats, eq(user.id, userStats.userId)) .where(eq(member.organizationId, organizationId)) - // The billing period is the same for every member — it comes from - // whichever subscription covers them. Fetch once and attach to - // every row instead of calling `getUserUsageData` per-member, - // which would run an O(N) pooled query for each of N rows. - const [orgSub] = await db - .select({ - periodStart: subscriptionTable.periodStart, - periodEnd: subscriptionTable.periodEnd, + const { billingPeriod, includeLegacyBaseline, usageByUser } = + await getOrganizationMemberUsageSnapshot(organizationId, { + userIds: base.length <= 1_000 ? base.map((row) => row.userId) : undefined, }) - .from(subscriptionTable) - .where( - and( - eq(subscriptionTable.referenceId, organizationId), - inArray(subscriptionTable.status, ENTITLED_SUBSCRIPTION_STATUSES) - ) - ) - .limit(1) - - const billingPeriodStart = orgSub?.periodStart ?? null - const billingPeriodEnd = orgSub?.periodEnd ?? null - - // currentPeriodCost is only a baseline; add each member's attributed - // usage_log for the period (batched, one query) so the roster shows real - // usage rather than the frozen baseline. - const usageByUser = await getOrgMemberLedgerByUser( - organizationId, - billingPeriodStart && billingPeriodEnd - ? { start: billingPeriodStart, end: billingPeriodEnd } - : null - ) + const billingPeriodStart = billingPeriod?.start ?? null + const billingPeriodEnd = billingPeriod?.end ?? null const membersWithUsage = base.map((row) => ({ ...row, currentPeriodCost: ( - Number(row.currentPeriodCost ?? 0) + (usageByUser.get(row.userId) ?? 0) + (includeLegacyBaseline ? Number(row.currentPeriodCost ?? 0) : 0) + + (usageByUser.get(row.userId) ?? 0) ).toString(), billingPeriodStart, billingPeriodEnd, diff --git a/apps/sim/app/api/v1/admin/dashboard/enterprise-provisioning/[id]/workspace-moves/[moveId]/retry/route.ts b/apps/sim/app/api/v1/admin/dashboard/enterprise-provisioning/[id]/workspace-moves/[moveId]/retry/route.ts new file mode 100644 index 00000000000..7962b6504d5 --- /dev/null +++ b/apps/sim/app/api/v1/admin/dashboard/enterprise-provisioning/[id]/workspace-moves/[moveId]/retry/route.ts @@ -0,0 +1,50 @@ +import { getErrorMessage } from '@sim/utils/errors' +import { toDashboardProvisioning } from '@/lib/admin/dashboard' +import { adminDashboardRetryEnterpriseWorkspaceMoveContract } from '@/lib/api/contracts/v1/admin/dashboard' +import { parseRequest } from '@/lib/api/server' +import { + EnterpriseProvisioningError, + retryEnterpriseWorkspaceMove, +} from '@/lib/billing/enterprise-provisioning' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { getAdminAuditActor } from '@/app/api/v1/admin/dashboard/actor' +import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' +import { + adminValidationErrorResponse, + badRequestResponse, + singleResponse, +} from '@/app/api/v1/admin/responses' + +interface RouteParams { + id: string + moveId: string +} + +export const POST = withRouteHandler( + withAdminAuthParams(async (request, context) => { + const parsed = await parseRequest( + adminDashboardRetryEnterpriseWorkspaceMoveContract, + request, + context, + { validationErrorResponse: adminValidationErrorResponse } + ) + if (!parsed.success) return parsed.response + try { + return singleResponse( + toDashboardProvisioning( + await retryEnterpriseWorkspaceMove( + parsed.data.params.id, + parsed.data.params.moveId, + await getAdminAuditActor(request) + ) + ) + ) + } catch (error) { + return badRequestResponse( + error instanceof EnterpriseProvisioningError + ? error.message + : getErrorMessage(error, 'Failed to retry Enterprise workspace move') + ) + } + }) +) diff --git a/apps/sim/app/api/v1/admin/dashboard/enterprise-provisioning/preflight/route.ts b/apps/sim/app/api/v1/admin/dashboard/enterprise-provisioning/preflight/route.ts new file mode 100644 index 00000000000..148247ca897 --- /dev/null +++ b/apps/sim/app/api/v1/admin/dashboard/enterprise-provisioning/preflight/route.ts @@ -0,0 +1,35 @@ +import { getErrorMessage } from '@sim/utils/errors' +import { adminDashboardEnterprisePreflightContract } from '@/lib/api/contracts/v1/admin/dashboard' +import { parseRequest } from '@/lib/api/server' +import { + EnterpriseProvisioningError, + getEnterpriseIssuancePreflight, +} from '@/lib/billing/enterprise-provisioning' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { withAdminAuth } from '@/app/api/v1/admin/middleware' +import { + adminValidationErrorResponse, + badRequestResponse, + singleResponse, +} from '@/app/api/v1/admin/responses' + +export const GET = withRouteHandler( + withAdminAuth(async (request) => { + const parsed = await parseRequest( + adminDashboardEnterprisePreflightContract, + request, + {}, + { validationErrorResponse: adminValidationErrorResponse } + ) + if (!parsed.success) return parsed.response + try { + return singleResponse(await getEnterpriseIssuancePreflight(parsed.data.query)) + } catch (error) { + return badRequestResponse( + error instanceof EnterpriseProvisioningError + ? error.message + : getErrorMessage(error, 'Failed to prepare Enterprise issuance') + ) + } + }) +) diff --git a/apps/sim/app/api/v1/admin/dashboard/organizations/[id]/billing-terms/preview/route.ts b/apps/sim/app/api/v1/admin/dashboard/organizations/[id]/billing-terms/preview/route.ts new file mode 100644 index 00000000000..fd5c3d494c4 --- /dev/null +++ b/apps/sim/app/api/v1/admin/dashboard/organizations/[id]/billing-terms/preview/route.ts @@ -0,0 +1,31 @@ +import { getErrorMessage } from '@sim/utils/errors' +import { previewDashboardEnterpriseBillingTerms } from '@/lib/admin/dashboard' +import { adminDashboardPreviewBillingTermsContract } 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' +import { + adminInvalidJsonResponse, + adminValidationErrorResponse, + badRequestResponse, + singleResponse, +} from '@/app/api/v1/admin/responses' + +export const POST = withRouteHandler( + withAdminAuthParams<{ id: string }>(async (request, context) => { + const parsed = await parseRequest(adminDashboardPreviewBillingTermsContract, request, context, { + validationErrorResponse: adminValidationErrorResponse, + invalidJsonResponse: adminInvalidJsonResponse, + }) + if (!parsed.success) return parsed.response + try { + return singleResponse( + await previewDashboardEnterpriseBillingTerms(parsed.data.params.id, parsed.data.body) + ) + } catch (error) { + return badRequestResponse( + getErrorMessage(error, 'Failed to preview Enterprise billing terms') + ) + } + }) +) diff --git a/apps/sim/app/api/v1/admin/dashboard/organizations/[id]/billing-terms/route.ts b/apps/sim/app/api/v1/admin/dashboard/organizations/[id]/billing-terms/route.ts new file mode 100644 index 00000000000..e9afd5d98ea --- /dev/null +++ b/apps/sim/app/api/v1/admin/dashboard/organizations/[id]/billing-terms/route.ts @@ -0,0 +1,33 @@ +import { getErrorMessage } from '@sim/utils/errors' +import { updateDashboardEnterpriseBillingTerms } from '@/lib/admin/dashboard' +import { adminDashboardUpdateBillingTermsContract } 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' +import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' +import { + adminInvalidJsonResponse, + adminValidationErrorResponse, + badRequestResponse, + singleResponse, +} from '@/app/api/v1/admin/responses' + +export const PATCH = withRouteHandler( + withAdminAuthParams<{ id: string }>(async (request, context) => { + const parsed = await parseRequest(adminDashboardUpdateBillingTermsContract, request, context, { + validationErrorResponse: adminValidationErrorResponse, + invalidJsonResponse: adminInvalidJsonResponse, + }) + if (!parsed.success) return parsed.response + try { + await updateDashboardEnterpriseBillingTerms( + 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')) + } + }) +) diff --git a/apps/sim/app/api/v1/admin/dashboard/organizations/[id]/configuration-update/retry/route.ts b/apps/sim/app/api/v1/admin/dashboard/organizations/[id]/configuration-update/retry/route.ts new file mode 100644 index 00000000000..fefd5ecc691 --- /dev/null +++ b/apps/sim/app/api/v1/admin/dashboard/organizations/[id]/configuration-update/retry/route.ts @@ -0,0 +1,38 @@ +import { getErrorMessage } from '@sim/utils/errors' +import { retryDashboardEnterpriseConfigurationUpdate } from '@/lib/admin/dashboard' +import { adminDashboardRetryConfigurationUpdateContract } 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' +import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' +import { + adminInvalidJsonResponse, + adminValidationErrorResponse, + badRequestResponse, + singleResponse, +} from '@/app/api/v1/admin/responses' + +export const POST = withRouteHandler( + withAdminAuthParams<{ id: string }>(async (request, context) => { + const parsed = await parseRequest( + adminDashboardRetryConfigurationUpdateContract, + request, + context, + { + validationErrorResponse: adminValidationErrorResponse, + invalidJsonResponse: adminInvalidJsonResponse, + } + ) + if (!parsed.success) return parsed.response + try { + await retryDashboardEnterpriseConfigurationUpdate( + parsed.data.params.id, + parsed.data.body.operationId, + await getAdminAuditActor(request) + ) + return singleResponse({ success: true as const }) + } catch (error) { + return badRequestResponse(getErrorMessage(error, 'Failed to retry configuration update')) + } + }) +) diff --git a/apps/sim/app/api/v1/admin/dashboard/organizations/[id]/route.ts b/apps/sim/app/api/v1/admin/dashboard/organizations/[id]/route.ts index 727cb6a0fbc..bba17a7b086 100644 --- a/apps/sim/app/api/v1/admin/dashboard/organizations/[id]/route.ts +++ b/apps/sim/app/api/v1/admin/dashboard/organizations/[id]/route.ts @@ -20,7 +20,7 @@ export const GET = withRouteHandler( }) if (!parsed.success) return parsed.response try { - const organization = await getDashboardOrganization(parsed.data.params.id) + const organization = await getDashboardOrganization(parsed.data.params.id, parsed.data.query) return organization ? singleResponse(organization) : notFoundResponse('Organization') } catch (error) { logger.error('Failed to get dashboard organization', { error }) diff --git a/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts b/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts index 83234df0a7b..05bb34a6a59 100644 --- a/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts +++ b/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts @@ -36,7 +36,7 @@ import { adminV1UpdateOrganizationMemberContract, } from '@/lib/api/contracts/v1/admin' import { parseRequest } from '@/lib/api/server' -import { getOrgMemberLedgerByUser } from '@/lib/billing/core/organization' +import { getOrganizationMemberUsageSnapshot } from '@/lib/billing/core/organization' import { removeUserFromOrganization, WORKSPACE_BILLING_ACCOUNT_REMOVAL_ERROR, @@ -104,9 +104,10 @@ export const GET = withRouteHandler( return notFoundResponse('Member') } - // currentPeriodCost is only a baseline; add this member's attributed - // usage_log for the org's period so admin shows real current usage. - const ledgerByUser = await getOrgMemberLedgerByUser(organizationId) + const { includeLegacyBaseline, usageByUser } = await getOrganizationMemberUsageSnapshot( + organizationId, + { userIds: [memberData.userId] } + ) const data: AdminMemberDetail = { id: memberData.id, @@ -117,7 +118,8 @@ export const GET = withRouteHandler( userName: memberData.userName, userEmail: memberData.userEmail, currentPeriodCost: ( - Number(memberData.currentPeriodCost ?? 0) + (ledgerByUser.get(memberData.userId) ?? 0) + (includeLegacyBaseline ? Number(memberData.currentPeriodCost ?? 0) : 0) + + (usageByUser.get(memberData.userId) ?? 0) ).toString(), currentUsageLimit: memberData.currentUsageLimit, billingBlocked: memberData.billingBlocked ?? false, diff --git a/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts b/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts index fef6b78a576..6a7c1c17302 100644 --- a/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts +++ b/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts @@ -40,7 +40,7 @@ import { } from '@/lib/api/contracts/v1/admin' import { parseRequest } from '@/lib/api/server' import { getOrganizationSubscription } from '@/lib/billing/core/billing' -import { getOrgMemberLedgerByUser } from '@/lib/billing/core/organization' +import { getOrganizationMemberUsageSnapshot } from '@/lib/billing/core/organization' import { syncUsageLimitsFromSubscription } from '@/lib/billing/core/usage' import { ensureUserInOrganizationTx } from '@/lib/billing/organizations/membership' import { reconcileOrganizationSeats } from '@/lib/billing/organizations/seats' @@ -133,9 +133,12 @@ export const GET = withRouteHandler( const total = countResult[0].count - // currentPeriodCost is only a baseline; add each member's attributed - // usage_log for the org's period so admin shows real current usage. - const usageByUser = await getOrgMemberLedgerByUser(organizationId) + const { includeLegacyBaseline, usageByUser } = await getOrganizationMemberUsageSnapshot( + organizationId, + { + userIds: membersData.map((row) => row.userId), + } + ) const data: AdminMemberDetail[] = membersData.map((m) => ({ id: m.id, @@ -146,7 +149,8 @@ export const GET = withRouteHandler( userName: m.userName, userEmail: m.userEmail, currentPeriodCost: ( - Number(m.currentPeriodCost ?? 0) + (usageByUser.get(m.userId) ?? 0) + (includeLegacyBaseline ? Number(m.currentPeriodCost ?? 0) : 0) + + (usageByUser.get(m.userId) ?? 0) ).toString(), currentUsageLimit: m.currentUsageLimit, billingBlocked: m.billingBlocked ?? false, diff --git a/apps/sim/lib/admin/dashboard-organizations.test.ts b/apps/sim/lib/admin/dashboard-organizations.test.ts index f240d05383f..2c0414d0644 100644 --- a/apps/sim/lib/admin/dashboard-organizations.test.ts +++ b/apps/sim/lib/admin/dashboard-organizations.test.ts @@ -1,6 +1,13 @@ /** @vitest-environment node */ -import { member, organization, permissions, subscription } from '@sim/db/schema' +import { + member, + organization, + permissions, + subscription, + usageLog, + workspace, +} from '@sim/db/schema' import { dbChainMockFns, queueTableRows, resetDbChainMock } from '@sim/testing' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' @@ -8,6 +15,8 @@ vi.unmock('drizzle-orm') const mocks = vi.hoisted(() => ({ provisionings: new Map(), + resolveMetadataIntent: vi.fn(), + enqueueOutboxEvent: vi.fn(), })) vi.mock('@sim/audit', () => ({ @@ -41,7 +50,8 @@ vi.mock('@/lib/billing/enterprise-provisioning', () => ({ })) vi.mock('@/lib/billing/enterprise-outbox', () => ({ ENTERPRISE_METADATA_SYNC_EVENT_TYPE: 'stripe.sync-enterprise-metadata', - resolveEnterpriseMetadataIntent: vi.fn(), + enterpriseMetadataSyncPayloadSchema: { safeParse: vi.fn() }, + resolveEnterpriseMetadataIntent: mocks.resolveMetadataIntent, })) vi.mock('@/lib/billing/organizations/member-limits', () => ({ setOrgMemberUsageLimit: vi.fn() })) vi.mock('@/lib/billing/organizations/billing-identity-lock', () => ({ @@ -57,9 +67,15 @@ vi.mock('@/lib/billing/organizations/seats', () => ({ reconcileOrganizationSeats vi.mock('@/lib/core/idempotency/transaction', () => ({ executeTransactionallyIdempotent: vi.fn(), })) -vi.mock('@/lib/core/outbox/service', () => ({ enqueueOutboxEvent: vi.fn() })) +vi.mock('@/lib/core/outbox/service', () => ({ enqueueOutboxEvent: mocks.enqueueOutboxEvent })) -import { listDashboardOrganizations, toDashboardConfigurationUpdate } from '@/lib/admin/dashboard' +import { + getDashboardOrganization, + listDashboardOrganizations, + toDashboardConfigurationUpdate, + updateDashboardEnterpriseBillingTerms, + updateDashboardOrganizationLimits, +} from '@/lib/admin/dashboard' afterAll(() => { resetDbChainMock() @@ -71,6 +87,7 @@ describe('toDashboardConfigurationUpdate', () => { toDashboardConfigurationUpdate({ latestRevision: 2, desiredMetadata: {}, + desiredTerms: null, hasUnappliedIntent: true, effectiveSeatCapacity: 20, configurationUpdate: { @@ -81,6 +98,7 @@ describe('toDashboardConfigurationUpdate', () => { seats: 20, concurrencyLimit: 50, }, + requestedTerms: null, error: null, }, }) @@ -88,6 +106,9 @@ describe('toDashboardConfigurationUpdate', () => { id: 'config-2', status: 'pending', requestedUsageLimitDollars: 50_000, + requestedInvoiceAmountUsd: null, + requestedBillingInterval: null, + requestedReportingPeriodAnchorDate: null, requestedSeats: 20, requestedConcurrencyLimit: 50, requestedWorkflowExecutionTimeoutSeconds: null, @@ -151,7 +172,203 @@ describe('listDashboardOrganizations', () => { externalCollaboratorCount: 0, planLabel: 'No plan', }) - expect(dbChainMockFns.select).toHaveBeenCalledTimes(4) + // Pagination, membership/collaborators, and two batched usage aggregates. + // This count remains constant regardless of the number of organizations. + expect(dbChainMockFns.select).toHaveBeenCalledTimes(6) expect(dbChainMockFns.selectDistinctOn).toHaveBeenCalledTimes(1) }) }) + +describe('getDashboardOrganization', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + mocks.provisionings = new Map() + }) + + it('returns explicit counts and independent page metadata for bounded detail collections', async () => { + queueTableRows(organization, [ + { id: 'org-1', name: 'One', orgUsageLimit: '100', creditBalance: '10' }, + ]) + queueTableRows(member, [{ value: 0 }]) + queueTableRows(permissions, [{ value: 0 }]) + queueTableRows(subscription, []) + queueTableRows(member, []) + queueTableRows(usageLog, []) + queueTableRows(usageLog, [{ usedDollars: '12.5', actorCount: 2 }]) + queueTableRows(member, []) + queueTableRows(member, []) + queueTableRows(permissions, []) + queueTableRows(workspace, [ + { id: 'workspace-1', name: 'One' }, + { id: 'workspace-2', name: 'Two' }, + ]) + queueTableRows(workspace, [{ value: 3 }]) + + const result = await getDashboardOrganization('org-1', { + paginated: true, + limit: 2, + memberOffset: 0, + externalCollaboratorOffset: 0, + workspaceOffset: 0, + }) + + expect(result).toMatchObject({ + memberPagination: { total: 0, limit: 2, offset: 0, hasMore: false }, + externalCollaboratorPagination: { total: 0, limit: 2, offset: 0, hasMore: false }, + workspacePagination: { total: 3, limit: 2, offset: 0, hasMore: true }, + historicalActorUsage: { usedDollars: 12.5, actorCount: 2 }, + workspaces: [ + { id: 'workspace-1', name: 'One' }, + { id: 'workspace-2', name: 'Two' }, + ], + }) + }) +}) + +describe('updateDashboardOrganizationLimits', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + mocks.resolveMetadataIntent.mockResolvedValue({ + latestRevision: 2, + desiredMetadata: { + plan: 'enterprise', + referenceId: 'org-1', + seats: 10, + usageLimitCredits: 18_000, + }, + desiredTerms: null, + hasUnappliedIntent: false, + effectiveSeatCapacity: 10, + configurationUpdate: null, + }) + }) + + it('writes a configured Stripe base that materializes to the requested total after prepaid', async () => { + queueTableRows(organization, [{ id: 'org-1', creditBalance: '10', orgUsageLimit: '100' }]) + queueTableRows(subscription, [ + { + id: 'sub-1', + plan: 'enterprise', + status: 'active', + metadata: { usageLimitCredits: 18_000, seats: 10 }, + }, + ]) + + await updateDashboardOrganizationLimits( + 'org-1', + { usageLimitDollars: 50 }, + { id: 'admin-1', name: 'Admin', email: 'admin@sim.ai' } + ) + + expect(mocks.enqueueOutboxEvent).toHaveBeenCalledWith( + expect.anything(), + 'stripe.sync-enterprise-metadata', + expect.objectContaining({ + subscriptionId: 'sub-1', + metadata: expect.objectContaining({ usageLimitCredits: 8_000 }), + }) + ) + }) + + it('rejects a total limit below the prepaid balance', async () => { + queueTableRows(organization, [{ id: 'org-1', creditBalance: '10', orgUsageLimit: '100' }]) + queueTableRows(subscription, [ + { id: 'sub-1', plan: 'enterprise', status: 'active', metadata: {} }, + ]) + + await expect( + updateDashboardOrganizationLimits( + 'org-1', + { usageLimitDollars: 5 }, + { id: 'admin-1', name: 'Admin', email: 'admin@sim.ai' } + ) + ).rejects.toThrow('cannot be below its prepaid balance') + expect(mocks.enqueueOutboxEvent).not.toHaveBeenCalled() + }) +}) + +describe('updateDashboardEnterpriseBillingTerms', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + mocks.resolveMetadataIntent.mockResolvedValue({ + latestRevision: 3, + desiredMetadata: { + plan: 'enterprise', + referenceId: 'org-1', + seats: 10, + monthlyPrice: 125, + }, + desiredTerms: null, + hasUnappliedIntent: false, + effectiveSeatCapacity: 10, + configurationUpdate: null, + }) + }) + + it('queues a cadence and immutable Price change through the existing Stripe intent', 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 }, + }, + ]) + + await updateDashboardEnterpriseBillingTerms( + 'org-1', + { + invoiceAmountUsd: 1200, + billingInterval: 'year', + reportingPeriodAnchorDate: '2026-01-31', + }, + { id: 'admin-1', name: 'Admin', email: 'admin@sim.ai' } + ) + + expect(mocks.enqueueOutboxEvent).toHaveBeenCalledWith( + expect.anything(), + 'stripe.sync-enterprise-metadata', + expect.objectContaining({ + revision: 4, + terms: { invoiceAmountCents: 120_000, billingInterval: 'year' }, + metadata: expect.objectContaining({ + invoiceAmountCents: 120_000, + reportingPeriodAnchorDate: '2026-01-31', + }), + }) + ) + expect( + (mocks.enqueueOutboxEvent.mock.calls[0][2] as { metadata: Record }).metadata + ).toMatchObject({ monthlyPrice: null }) + }) + + it('updates only metadata when the applied Price already matches', async () => { + queueTableRows(subscription, [ + { + id: 'sub-1', + stripeSubscriptionId: 'stripe-sub-1', + plan: 'enterprise', + status: 'active', + billingInterval: 'year', + metadata: { invoiceAmountCents: 120_000, seats: 10 }, + }, + ]) + + await updateDashboardEnterpriseBillingTerms( + 'org-1', + { + invoiceAmountUsd: 1200, + billingInterval: 'year', + reportingPeriodAnchorDate: '2025-01-31', + }, + { id: 'admin-1', name: 'Admin', email: 'admin@sim.ai' } + ) + + expect(mocks.enqueueOutboxEvent.mock.calls[0][2]).not.toHaveProperty('terms') + }) +}) diff --git a/apps/sim/lib/admin/dashboard.ts b/apps/sim/lib/admin/dashboard.ts index 6f0ee065cc1..9a1a62cf2ff 100644 --- a/apps/sim/lib/admin/dashboard.ts +++ b/apps/sim/lib/admin/dashboard.ts @@ -4,26 +4,50 @@ import { member, organization, organizationMemberUsageLimit, + outboxEvent, permissions, subscription, + usageLog, user, userStats, workspace, } from '@sim/db/schema' import { getErrorMessage } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { and, count, countDistinct, desc, eq, ilike, inArray, isNull, or, sql } from 'drizzle-orm' +import { + and, + count, + countDistinct, + desc, + eq, + gte, + ilike, + inArray, + isNull, + lt, + notExists, + or, + sql, +} from 'drizzle-orm' +import { alias } from 'drizzle-orm/pg-core' import { getOrganizationUsageLimitFallbackDollars, getTeamOrganizationEconomics, } from '@/lib/admin/organization-economics' import { parseBillingConcurrencyLimit } from '@/lib/billing/concurrency-defaults' import { getBillingConcurrencyLimit } from '@/lib/billing/concurrency-limits' +import { defaultBillingPeriod } from '@/lib/billing/core/billing-period' import { getHighestPrioritySubscription } from '@/lib/billing/core/plan' +import { + type ResolvedUsagePeriod, + resolveEnterpriseReportingPeriod, + resolveSubscriptionUsagePeriod, +} from '@/lib/billing/core/reporting-period' import { syncUsageLimitsFromSubscription } from '@/lib/billing/core/usage' import { creditsToDollars, dollarsToCredits } from '@/lib/billing/credits/conversion' import { ENTERPRISE_METADATA_SYNC_EVENT_TYPE, + enterpriseMetadataSyncPayloadSchema, resolveEnterpriseMetadataIntent, } from '@/lib/billing/enterprise-outbox' import { @@ -90,6 +114,7 @@ async function enqueueEnterpriseMetadataIntent( subscriptionId: string appliedMetadata: unknown buildDesiredMetadata: (current: Record) => Record + terms?: { invoiceAmountCents: number; billingInterval: 'month' | 'year' } | null } ): Promise<{ version: number; desiredMetadata: Record }> { const intent = await resolveEnterpriseMetadataIntent( @@ -97,14 +122,24 @@ async function enqueueEnterpriseMetadataIntent( params.subscriptionId, params.appliedMetadata ) + if (intent.hasUnappliedIntent) { + throw new Error( + intent.configurationUpdate?.providerAccepted + ? 'Stripe accepted the previous Enterprise update, but Sim has not reconciled it. Retry that update before making another change.' + : 'An Enterprise configuration update is already in progress. Wait for it to apply before making another change.' + ) + } const { simConfigRevision: _appliedRevision, simConfigOperationId: _appliedOperationId, + simConfigDeliveryRevision: _appliedDeliveryRevision, + simConfigDeliveryAttempt: _appliedDeliveryAttempt, ...current } = { ...intent.desiredMetadata, } const desiredMetadata = params.buildDesiredMetadata(current) + const desiredTerms = params.terms === undefined ? intent.desiredTerms : params.terms const version = intent.latestRevision + 1 await enqueueOutboxEvent(tx, ENTERPRISE_METADATA_SYNC_EVENT_TYPE, { @@ -112,6 +147,7 @@ async function enqueueEnterpriseMetadataIntent( revision: version, deliveryRevision: 0, metadata: desiredMetadata, + ...(desiredTerms ? { terms: desiredTerms } : {}), }) return { version, desiredMetadata } } @@ -145,12 +181,217 @@ interface DashboardOrganizationSummaryInput { latestSubscription: typeof subscription.$inferSelect | null provisioning: EnterpriseProvisioningView | null owner: { id: string; name: string; email: string } | null + usageDollars: number + usagePeriod: ResolvedUsagePeriod +} + +interface DashboardOrganizationUsageContext { + organizationId: string + period: ResolvedUsagePeriod +} + +interface DashboardOrganizationUsage { + total: number + byUser: Map +} + +function resolveDashboardUsagePeriod( + latestSubscription: typeof subscription.$inferSelect | null +): ResolvedUsagePeriod { + return ( + resolveSubscriptionUsagePeriod(latestSubscription) ?? { + ...defaultBillingPeriod(), + source: 'default' as const, + anchorDate: null, + interval: null, + } + ) +} + +async function getDashboardOrganizationUsage( + contexts: DashboardOrganizationUsageContext[], + options: { includeUserBreakdown?: boolean; userIds?: string[] } = {} +): Promise> { + const result = new Map() + for (const context of contexts) { + result.set(context.organizationId, { total: 0, byUser: new Map() }) + } + if (contexts.length === 0) return result + + const includeUserBreakdown = options.includeUserBreakdown ?? true + if (options.userIds?.length === 0) return result + const ledgerPeriodWhere = or( + ...contexts.map((context) => + and( + eq(usageLog.billingEntityType, 'organization'), + eq(usageLog.billingEntityId, context.organizationId), + ...(context.period.source === 'reporting' + ? [ + gte(usageLog.createdAt, context.period.start), + lt(usageLog.createdAt, context.period.end), + ] + : [ + eq(usageLog.billingPeriodStart, context.period.start), + eq(usageLog.billingPeriodEnd, context.period.end), + ]) + ) + ) + ) + + if (!includeUserBreakdown) { + const ledgerTotals = await db + .select({ + organizationId: usageLog.billingEntityId, + cost: sql`coalesce(sum(${usageLog.cost}), 0)`, + }) + .from(usageLog) + .where(ledgerPeriodWhere) + .groupBy(usageLog.billingEntityId) + for (const row of ledgerTotals) { + if (!row.organizationId) continue + const usage = result.get(row.organizationId) + if (usage) usage.total += Number(row.cost) + } + + const legacyOrganizationIds = contexts + .filter((context) => context.period.source !== 'reporting') + .map((context) => context.organizationId) + if (legacyOrganizationIds.length > 0) { + const baselineTotals = await db + .select({ + organizationId: member.organizationId, + cost: sql`coalesce(sum(${userStats.currentPeriodCost}), 0)`, + }) + .from(member) + .leftJoin(userStats, eq(userStats.userId, member.userId)) + .where(inArray(member.organizationId, legacyOrganizationIds)) + .groupBy(member.organizationId) + for (const row of baselineTotals) { + const usage = result.get(row.organizationId) + if (usage) usage.total += Number(row.cost) + } + } + return result + } + + const ledgerRows = await db + .select({ + organizationId: usageLog.billingEntityId, + userId: usageLog.userId, + cost: sql`coalesce(sum(${usageLog.cost}), 0)`, + }) + .from(usageLog) + .where( + options.userIds + ? and(ledgerPeriodWhere, inArray(usageLog.userId, options.userIds)) + : ledgerPeriodWhere + ) + .groupBy(usageLog.billingEntityId, usageLog.userId) + + for (const row of ledgerRows) { + if (!row.organizationId) continue + const usage = result.get(row.organizationId) + if (!usage) continue + const amount = Number(row.cost) + usage.total += amount + usage.byUser.set(row.userId, (usage.byUser.get(row.userId) ?? 0) + amount) + } + + const legacyOrganizationIds = contexts + .filter((context) => context.period.source !== 'reporting') + .map((context) => context.organizationId) + if (legacyOrganizationIds.length > 0) { + const baselineRows = await db + .select({ + organizationId: member.organizationId, + userId: member.userId, + cost: userStats.currentPeriodCost, + }) + .from(member) + .leftJoin(userStats, eq(userStats.userId, member.userId)) + .where( + options.userIds + ? and( + inArray(member.organizationId, legacyOrganizationIds), + inArray(member.userId, options.userIds) + ) + : inArray(member.organizationId, legacyOrganizationIds) + ) + for (const row of baselineRows) { + const usage = result.get(row.organizationId) + if (!usage) continue + const amount = Number(row.cost ?? 0) + usage.total += amount + usage.byUser.set(row.userId, (usage.byUser.get(row.userId) ?? 0) + amount) + } + } + return result +} + +const historicalUsageMember = alias(member, 'historical_usage_member') +const historicalUsagePermission = alias(permissions, 'historical_usage_permission') +const historicalUsageWorkspace = alias(workspace, 'historical_usage_workspace') + +async function getHistoricalActorUsage( + organizationId: string, + period: ResolvedUsagePeriod +): Promise<{ usedDollars: number; actorCount: number }> { + const currentMember = db + .select({ value: sql`1` }) + .from(historicalUsageMember) + .where( + and( + eq(historicalUsageMember.organizationId, organizationId), + eq(historicalUsageMember.userId, usageLog.userId) + ) + ) + const currentCollaborator = db + .select({ value: sql`1` }) + .from(historicalUsagePermission) + .innerJoin( + historicalUsageWorkspace, + and( + eq(historicalUsagePermission.entityType, 'workspace'), + eq(historicalUsagePermission.entityId, historicalUsageWorkspace.id) + ) + ) + .where( + and( + eq(historicalUsageWorkspace.organizationId, organizationId), + eq(historicalUsagePermission.userId, usageLog.userId) + ) + ) + const [row] = await db + .select({ + usedDollars: sql`coalesce(sum(${usageLog.cost}), 0)`, + actorCount: countDistinct(usageLog.userId), + }) + .from(usageLog) + .where( + and( + eq(usageLog.billingEntityType, 'organization'), + eq(usageLog.billingEntityId, organizationId), + ...(period.source === 'reporting' + ? [gte(usageLog.createdAt, period.start), lt(usageLog.createdAt, period.end)] + : [ + eq(usageLog.billingPeriodStart, period.start), + eq(usageLog.billingPeriodEnd, period.end), + ]), + notExists(currentMember), + notExists(currentCollaborator) + ) + ) + return { + usedDollars: Number(row?.usedDollars ?? 0), + actorCount: row?.actorCount ?? 0, + } } export function toDashboardProvisioning(view: EnterpriseProvisioningView) { const { usageLimitCredits, ...rest } = view return { ...rest, + monthlyInvoiceAmountUsd: view.billingInterval === 'month' ? view.invoiceAmountUsd : null, usageLimitDollars: creditsToDollars(usageLimitCredits), } } @@ -162,21 +403,16 @@ function buildDashboardOrganizationSummary({ latestSubscription, provisioning, owner, + usageDollars, + usagePeriod, }: DashboardOrganizationSummaryInput) { const metadata = metadataRecord(latestSubscription?.metadata) const teamEconomics = getTeamOrganizationEconomics(latestSubscription?.plan, memberCount) - const planAllowanceDollars = teamEconomics?.planAllowanceDollars ?? null const invoiceAmountCents = metadataNumber(metadata, 'invoiceAmountCents') const monthlyPrice = metadataNumber(metadata, 'monthlyPrice') - const effectiveUsageLimitDollars = Number(org.orgUsageLimit ?? 0) - const metadataUsageLimitDollars = - metadataNumber(metadata, 'usageLimitCredits') === null - ? null - : creditsToDollars(metadataNumber(metadata, 'usageLimitCredits') ?? 0) - const usageLimitDollars = Math.max( - 0, - metadataUsageLimitDollars === null ? effectiveUsageLimitDollars : metadataUsageLimitDollars - ) + const usageLimitDollars = Math.max(0, Number(org.orgUsageLimit ?? 0)) + const planAllowanceDollars = teamEconomics?.planAllowanceDollars ?? null + const reportingPeriod = usagePeriod const seats = latestSubscription?.plan === 'enterprise' ? Math.max(0, Math.round(metadataNumber(metadata, 'seats') ?? 0)) @@ -211,25 +447,50 @@ function buildDashboardOrganizationSummary({ workflowExecutionTimeoutSeconds, planAllowanceDollars, usageLimitDollars, - effectiveUsageLimitDollars, + effectiveUsageLimitDollars: usageLimitDollars, prepaidBalanceDollars: Number(org.creditBalance ?? 0), - monthlyInvoiceAmountUsd: + invoiceAmountUsd: latestSubscription?.plan === 'enterprise' ? invoiceAmountCents !== null ? invoiceAmountCents / 100 : (monthlyPrice ?? null) : (teamEconomics?.monthlyInvoiceAmountUsd ?? null), + monthlyInvoiceAmountUsd: + latestSubscription?.plan === 'enterprise' + ? latestSubscription.billingInterval === 'year' + ? null + : invoiceAmountCents !== null + ? invoiceAmountCents / 100 + : (monthlyPrice ?? null) + : (teamEconomics?.monthlyInvoiceAmountUsd ?? null), + billingInterval: + latestSubscription?.billingInterval === 'year' || + latestSubscription?.billingInterval === 'month' + ? latestSubscription.billingInterval + : teamEconomics + ? 'month' + : null, + reportingPeriod: { + anchorDate: reportingPeriod.anchorDate, + interval: reportingPeriod.interval, + currentStart: reportingPeriod.start.toISOString(), + currentEnd: reportingPeriod.end.toISOString(), + source: reportingPeriod.source, + }, + usage: { usedDollars: Math.max(0, usageDollars), limitDollars: usageLimitDollars }, provisioning: provisioning ? toDashboardProvisioning(provisioning) : null, subscription: latestSubscription, } } export function toDashboardConfigurationUpdate( - intent: Awaited> | null + intent: Awaited> | null, + prepaidBalanceDollars = 0 ) { const update = intent?.configurationUpdate if (!update) return null const metadata = update.requestedMetadata + const terms = update.requestedTerms const usageLimitCredits = metadataNumber(metadata, 'usageLimitCredits') const seats = metadataNumber(metadata, 'seats') const concurrencyLimit = metadataNumber(metadata, 'concurrencyLimit') @@ -242,11 +503,20 @@ export function toDashboardConfigurationUpdate( id: update.id, status: update.status, requestedUsageLimitDollars: - usageLimitCredits === null ? null : creditsToDollars(usageLimitCredits), + usageLimitCredits === null + ? null + : creditsToDollars(usageLimitCredits) + prepaidBalanceDollars, + requestedInvoiceAmountUsd: terms ? terms.invoiceAmountCents / 100 : null, + requestedBillingInterval: terms?.billingInterval ?? null, + requestedReportingPeriodAnchorDate: + typeof metadata.reportingPeriodAnchorDate === 'string' + ? metadata.reportingPeriodAnchorDate + : null, requestedSeats: seats === null ? null : Math.round(seats), requestedConcurrencyLimit: concurrencyLimit === null ? null : Math.round(concurrencyLimit), requestedWorkflowExecutionTimeoutSeconds: workflowExecutionTimeoutSeconds === null ? null : Math.round(workflowExecutionTimeoutSeconds), + providerAccepted: update.providerAccepted, error: update.error, } } @@ -286,6 +556,98 @@ export async function listDashboardUsers({ search, limit, offset }: PaginationIn .limit(limit) .offset(offset), ]) + const organizationIds = [...new Set(rows.flatMap((row) => row.organizationId ?? []))] + const personalUserIds = rows.filter((row) => !row.organizationId).map((row) => row.id) + const [organizationSubscriptions, personalSubscriptions] = await Promise.all([ + organizationIds.length === 0 + ? Promise.resolve([]) + : db + .selectDistinctOn([subscription.referenceId]) + .from(subscription) + .where(inArray(subscription.referenceId, organizationIds)) + .orderBy( + subscription.referenceId, + sql`case when ${subscription.status} in ('active', 'past_due') then 0 else 1 end`, + sql`coalesce(${subscription.endedAt}, ${subscription.canceledAt}, ${subscription.periodEnd}, ${subscription.periodStart}) desc nulls last`, + desc(subscription.id) + ), + personalUserIds.length === 0 + ? Promise.resolve([]) + : db + .selectDistinctOn([subscription.referenceId]) + .from(subscription) + .where(inArray(subscription.referenceId, personalUserIds)) + .orderBy( + subscription.referenceId, + sql`case when ${subscription.status} in ('active', 'past_due') then 0 else 1 end`, + sql`coalesce(${subscription.endedAt}, ${subscription.canceledAt}, ${subscription.periodEnd}, ${subscription.periodStart}) desc nulls last`, + desc(subscription.id) + ), + ]) + const organizationSubscriptionMap = new Map( + organizationSubscriptions.map((row) => [row.referenceId, row]) + ) + const organizationUsage = await getDashboardOrganizationUsage( + organizationIds.map((organizationId) => ({ + organizationId, + period: resolveDashboardUsagePeriod(organizationSubscriptionMap.get(organizationId) ?? null), + })), + { userIds: rows.map((row) => row.id) } + ) + const personalSubscriptionMap = new Map( + personalSubscriptions.map((row) => [row.referenceId, row]) + ) + const personalPeriods = new Map( + personalUserIds.map((userId) => [ + userId, + resolveDashboardUsagePeriod(personalSubscriptionMap.get(userId) ?? null), + ]) + ) + const personalLedgerRows = + personalUserIds.length === 0 + ? [] + : await db + .select({ + userId: usageLog.billingEntityId, + cost: sql`coalesce(sum(${usageLog.cost}), 0)`, + }) + .from(usageLog) + .where( + or( + ...personalUserIds.map((userId) => { + const period = personalPeriods.get(userId) as ResolvedUsagePeriod + return and( + eq(usageLog.billingEntityType, 'user'), + eq(usageLog.billingEntityId, userId), + ...(period.source === 'reporting' + ? [gte(usageLog.createdAt, period.start), lt(usageLog.createdAt, period.end)] + : [ + eq(usageLog.billingPeriodStart, period.start), + eq(usageLog.billingPeriodEnd, period.end), + ]) + ) + }) + ) + ) + .groupBy(usageLog.billingEntityId) + const personalUsage = new Map( + personalLedgerRows.flatMap((row) => + row.userId ? ([[row.userId, Number(row.cost)]] as const) : [] + ) + ) + const legacyPersonalIds = personalUserIds.filter( + (userId) => personalPeriods.get(userId)?.source !== 'reporting' + ) + if (legacyPersonalIds.length > 0) { + const baselineRows = await db + .select({ userId: userStats.userId, cost: userStats.currentPeriodCost }) + .from(userStats) + .where(inArray(userStats.userId, legacyPersonalIds)) + for (const row of baselineRows) { + personalUsage.set(row.userId, (personalUsage.get(row.userId) ?? 0) + Number(row.cost ?? 0)) + } + } + return { data: rows.map((row) => ({ id: row.id, @@ -295,6 +657,9 @@ export async function listDashboardUsers({ search, limit, offset }: PaginationIn row.organizationId && row.organizationName ? { id: row.organizationId, name: row.organizationName } : null, + usageDollars: row.organizationId + ? (organizationUsage.get(row.organizationId)?.byUser.get(row.id) ?? 0) + : (personalUsage.get(row.id) ?? 0), })), pagination: { total: totalRow[0]?.total ?? 0, @@ -327,7 +692,9 @@ async function getDashboardOrganizationSummary(organizationId: string) { ) .where(isNull(member.id)), getLatestSubscription(organizationId), - getLatestEnterpriseProvisionings([organizationId]), + getLatestEnterpriseProvisionings([organizationId], { + includeWorkspaceMoveFailures: true, + }), ]) if (!org) return null @@ -338,14 +705,23 @@ async function getDashboardOrganizationSummary(organizationId: string) { .where(and(eq(member.organizationId, organizationId), eq(member.role, 'owner'))) .limit(1) const memberCount = memberCountRow?.value ?? 0 - return buildDashboardOrganizationSummary({ - org, - memberCount, - externalCollaboratorCount: externalCountRow?.value ?? 0, - latestSubscription, - provisioning: provisionings.get(organizationId) ?? null, - owner: owner ?? null, + const period = resolveDashboardUsagePeriod(latestSubscription) + const usage = await getDashboardOrganizationUsage([{ organizationId, period }], { + includeUserBreakdown: false, }) + return { + ...buildDashboardOrganizationSummary({ + org, + memberCount, + externalCollaboratorCount: externalCountRow?.value ?? 0, + latestSubscription, + provisioning: provisionings.get(organizationId) ?? null, + owner: owner ?? null, + usageDollars: usage.get(organizationId)?.total ?? 0, + usagePeriod: period, + }), + usagePeriod: period, + } } export async function listDashboardOrganizations({ search, limit, offset }: PaginationInput) { @@ -428,6 +804,22 @@ export async function listDashboardOrganizations({ search, limit, offset }: Pagi ) ) const subscriptionByOrganization = new Map(subscriptionRows.map((row) => [row.referenceId, row])) + const usagePeriodsByOrganization = new Map( + organizationIds.map( + (organizationId) => + [ + organizationId, + resolveDashboardUsagePeriod(subscriptionByOrganization.get(organizationId) ?? null), + ] as const + ) + ) + const usageByOrganization = await getDashboardOrganizationUsage( + organizationIds.map((organizationId) => ({ + organizationId, + period: usagePeriodsByOrganization.get(organizationId)!, + })), + { includeUserBreakdown: false } + ) const data = orgRows.map((org) => { const membership = membershipsByOrganization.get(org.id) const owner = @@ -445,6 +837,8 @@ export async function listDashboardOrganizations({ search, limit, offset }: Pagi latestSubscription: subscriptionByOrganization.get(org.id) ?? null, provisioning: provisionings.get(org.id) ?? null, owner, + usageDollars: usageByOrganization.get(org.id)?.total ?? 0, + usagePeriod: usagePeriodsByOrganization.get(org.id)!, }) return summary }) @@ -459,78 +853,163 @@ export async function listDashboardOrganizations({ search, limit, offset }: Pagi } } -export async function getDashboardOrganization(organizationId: string) { +export async function getDashboardOrganization( + organizationId: string, + pagination: { + paginated: boolean + limit: number + memberOffset: number + externalCollaboratorOffset: number + workspaceOffset: number + } = { + paginated: false, + limit: 50, + memberOffset: 0, + externalCollaboratorOffset: 0, + workspaceOffset: 0, + } +) { const summary = await getDashboardOrganizationSummary(organizationId) if (!summary) return null - const { subscription: subscriptionRow, ...base } = summary - const [memberRows, externalRows, workspaceRows, limitRows, configurationIntent] = - await Promise.all([ - db - .select({ - id: member.id, - userId: user.id, - name: user.name, - email: user.email, - role: member.role, - }) - .from(member) - .innerJoin(user, eq(user.id, member.userId)) - .where(eq(member.organizationId, organizationId)) - .orderBy(user.name), - db - .select({ - userId: user.id, - name: user.name, - email: user.email, - workspaceCount: countDistinct(workspace.id), - }) - .from(permissions) - .innerJoin(user, eq(user.id, permissions.userId)) - .innerJoin( - workspace, - and( - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, workspace.id), - eq(workspace.organizationId, organizationId) - ) - ) - .leftJoin( - member, - and(eq(member.userId, permissions.userId), eq(member.organizationId, organizationId)) + const { subscription: subscriptionRow, usagePeriod, ...base } = summary + const memberQuery = () => + db + .select({ + id: member.id, + userId: user.id, + name: user.name, + email: user.email, + role: member.role, + }) + .from(member) + .innerJoin(user, eq(user.id, member.userId)) + .where(eq(member.organizationId, organizationId)) + .orderBy(user.name, user.id) + const externalCollaboratorQuery = () => + db + .select({ + userId: user.id, + name: user.name, + email: user.email, + workspaceCount: countDistinct(workspace.id), + }) + .from(permissions) + .innerJoin(user, eq(user.id, permissions.userId)) + .innerJoin( + workspace, + and( + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, workspace.id), + eq(workspace.organizationId, organizationId) ) - .where(isNull(member.id)) - .groupBy(user.id, user.name, user.email) - .orderBy(user.name), - db - .select({ id: workspace.id, name: workspace.name }) - .from(workspace) - .where(eq(workspace.organizationId, organizationId)) - .orderBy(workspace.name), - db - .select({ - userId: organizationMemberUsageLimit.userId, - limit: organizationMemberUsageLimit.usageLimit, - }) - .from(organizationMemberUsageLimit) - .where(eq(organizationMemberUsageLimit.organizationId, organizationId)), + ) + .leftJoin( + member, + and(eq(member.userId, permissions.userId), eq(member.organizationId, organizationId)) + ) + .where(isNull(member.id)) + .groupBy(user.id, user.name, user.email) + .orderBy(user.name, user.id) + const workspaceQuery = () => + db + .select({ id: workspace.id, name: workspace.name }) + .from(workspace) + .where(eq(workspace.organizationId, organizationId)) + .orderBy(workspace.name, workspace.id) + + const [memberRows, externalRows, workspaceRows, workspaceCountRows, configurationIntent] = + await Promise.all([ + pagination.paginated + ? memberQuery().limit(pagination.limit).offset(pagination.memberOffset) + : memberQuery(), + pagination.paginated + ? externalCollaboratorQuery() + .limit(pagination.limit) + .offset(pagination.externalCollaboratorOffset) + : externalCollaboratorQuery(), + pagination.paginated + ? workspaceQuery().limit(pagination.limit).offset(pagination.workspaceOffset) + : workspaceQuery(), + pagination.paginated + ? db + .select({ value: count() }) + .from(workspace) + .where(eq(workspace.organizationId, organizationId)) + : Promise.resolve([]), subscriptionRow?.plan === 'enterprise' ? resolveEnterpriseMetadataIntent(db, subscriptionRow.id, subscriptionRow.metadata) : Promise.resolve(null), ]) + const visibleUserIds = [ + ...new Set([...memberRows.map((row) => row.userId), ...externalRows.map((row) => row.userId)]), + ] + const [limitRows, usageByOrganization, historicalActorUsage] = await Promise.all([ + visibleUserIds.length === 0 + ? Promise.resolve([]) + : db + .select({ + userId: organizationMemberUsageLimit.userId, + limit: organizationMemberUsageLimit.usageLimit, + }) + .from(organizationMemberUsageLimit) + .where( + and( + eq(organizationMemberUsageLimit.organizationId, organizationId), + inArray(organizationMemberUsageLimit.userId, visibleUserIds) + ) + ), + getDashboardOrganizationUsage([{ organizationId, period: usagePeriod }], { + userIds: visibleUserIds, + }), + getHistoricalActorUsage(organizationId, usagePeriod), + ]) const limits = new Map(limitRows.map((row) => [row.userId, Number(row.limit)])) + const usageByUser = usageByOrganization.get(organizationId)?.byUser ?? new Map() + const workspaceTotal = pagination.paginated + ? (workspaceCountRows[0]?.value ?? 0) + : workspaceRows.length return { ...base, - configurationUpdate: toDashboardConfigurationUpdate(configurationIntent), + configurationUpdate: toDashboardConfigurationUpdate( + configurationIntent, + base.prepaidBalanceDollars + ), + historicalActorUsage, members: memberRows.map((row) => ({ ...row, usageLimitDollars: limits.get(row.userId) ?? null, + usageDollars: usageByUser.get(row.userId) ?? 0, })), externalCollaborators: externalRows.map((row) => ({ ...row, workspaceCount: row.workspaceCount, usageLimitDollars: limits.get(row.userId) ?? null, + usageDollars: usageByUser.get(row.userId) ?? 0, })), workspaces: workspaceRows, + memberPagination: { + total: base.memberCount, + limit: pagination.paginated ? pagination.limit : memberRows.length, + offset: pagination.paginated ? pagination.memberOffset : 0, + hasMore: + pagination.paginated && pagination.memberOffset + memberRows.length < base.memberCount, + }, + externalCollaboratorPagination: { + total: base.externalCollaboratorCount, + limit: pagination.paginated ? pagination.limit : externalRows.length, + offset: pagination.paginated ? pagination.externalCollaboratorOffset : 0, + hasMore: + pagination.paginated && + pagination.externalCollaboratorOffset + externalRows.length < + base.externalCollaboratorCount, + }, + workspacePagination: { + total: workspaceTotal, + limit: pagination.paginated ? pagination.limit : workspaceRows.length, + offset: pagination.paginated ? pagination.workspaceOffset : 0, + hasMore: + pagination.paginated && pagination.workspaceOffset + workspaceRows.length < workspaceTotal, + }, subscription: subscriptionRow ? { id: subscriptionRow.id, @@ -539,7 +1018,7 @@ export async function getDashboardOrganization(organizationId: string) { periodStart: subscriptionRow.periodStart?.toISOString() ?? null, periodEnd: subscriptionRow.periodEnd?.toISOString() ?? null, stripeSubscriptionId: subscriptionRow.stripeSubscriptionId, - invoiceAmountUsd: base.monthlyInvoiceAmountUsd, + invoiceAmountUsd: base.invoiceAmountUsd, } : null, } @@ -590,6 +1069,196 @@ export async function updateDashboardEnterpriseSeats( }) } +interface DashboardEnterpriseBillingTerms { + invoiceAmountUsd: number + billingInterval: 'month' | 'year' + reportingPeriodAnchorDate: string +} + +function validateDashboardEnterpriseBillingTerms(values: DashboardEnterpriseBillingTerms) { + const invoiceAmountCents = Math.round(values.invoiceAmountUsd * 100) + if ( + invoiceAmountCents <= 0 || + !Number.isSafeInteger(invoiceAmountCents) || + Math.abs(values.invoiceAmountUsd * 100 - invoiceAmountCents) > 1e-8 + ) { + throw new Error('Invoice amount must be at least $0.01 and use whole cents') + } + const reportingPeriod = resolveEnterpriseReportingPeriod( + values.reportingPeriodAnchorDate, + values.billingInterval + ) + if (!reportingPeriod) { + throw new Error('Contract start must be a valid UTC date that is not in the future') + } + return { invoiceAmountCents, reportingPeriod } +} + +export async function previewDashboardEnterpriseBillingTerms( + organizationId: string, + values: DashboardEnterpriseBillingTerms +) { + const { reportingPeriod } = validateDashboardEnterpriseBillingTerms(values) + const [[org], [subscriptionRow]] = await Promise.all([ + db + .select({ orgUsageLimit: organization.orgUsageLimit }) + .from(organization) + .where(eq(organization.id, organizationId)) + .limit(1), + db + .select({ id: subscription.id }) + .from(subscription) + .where( + and( + eq(subscription.referenceId, organizationId), + eq(subscription.plan, 'enterprise'), + inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES) + ) + ) + .limit(1), + ]) + if (!org || !subscriptionRow) throw new Error('Active Enterprise subscription not found') + const usage = await getDashboardOrganizationUsage([{ organizationId, period: reportingPeriod }], { + includeUserBreakdown: false, + }) + const usedDollars = usage.get(organizationId)?.total ?? 0 + const limitDollars = Number(org.orgUsageLimit ?? 0) + return { + reportingPeriod: { + anchorDate: reportingPeriod.anchorDate, + interval: reportingPeriod.interval, + currentStart: reportingPeriod.start.toISOString(), + currentEnd: reportingPeriod.end.toISOString(), + source: reportingPeriod.source, + }, + usage: { usedDollars, limitDollars }, + exceedsLimit: usedDollars > limitDollars, + } +} + +export async function updateDashboardEnterpriseBillingTerms( + organizationId: string, + values: DashboardEnterpriseBillingTerms, + actor: AdminMutationActor +) { + const { invoiceAmountCents } = validateDashboardEnterpriseBillingTerms(values) + await db.transaction(async (tx) => { + await acquireOrganizationMutationLock(tx, organizationId) + const [subscriptionRow] = await tx + .select() + .from(subscription) + .where( + and( + eq(subscription.referenceId, organizationId), + eq(subscription.plan, 'enterprise'), + inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES) + ) + ) + .for('update') + .limit(1) + if (!subscriptionRow?.stripeSubscriptionId) { + throw new Error('Active Stripe-backed Enterprise subscription not found') + } + const appliedMetadata = metadataRecord(subscriptionRow.metadata) + const appliedInvoiceAmountCents = + metadataNumber(appliedMetadata, 'invoiceAmountCents') ?? + Math.round((metadataNumber(appliedMetadata, 'monthlyPrice') ?? 0) * 100) + const appliedBillingInterval = subscriptionRow.billingInterval === 'year' ? 'year' : 'month' + const termsChanged = + appliedInvoiceAmountCents !== invoiceAmountCents || + appliedBillingInterval !== values.billingInterval + await enqueueEnterpriseMetadataIntent(tx, { + subscriptionId: subscriptionRow.id, + appliedMetadata: subscriptionRow.metadata, + terms: termsChanged ? { invoiceAmountCents, billingInterval: values.billingInterval } : null, + buildDesiredMetadata: (current) => { + const { monthlyPrice: _legacyMonthlyPrice, ...rest } = current + return { + ...rest, + // Stripe metadata updates merge by default. An empty value removes + // the legacy key after the neutral amount has been written. + monthlyPrice: null, + invoiceAmountCents, + reportingPeriodAnchorDate: values.reportingPeriodAnchorDate, + } + }, + }) + }) + recordAudit({ + actorId: actor.id, + actorName: actor.name, + actorEmail: actor.email, + action: AuditAction.ORGANIZATION_UPDATED, + resourceType: AuditResourceType.ORGANIZATION, + resourceId: organizationId, + description: 'Admin requested Enterprise billing-term update', + metadata: { ...values }, + }) +} + +export async function retryDashboardEnterpriseConfigurationUpdate( + organizationId: string, + operationId: string, + actor: AdminMutationActor +) { + await db.transaction(async (tx) => { + await acquireOrganizationMutationLock(tx, organizationId) + const [subscriptionRow] = await tx + .select({ id: subscription.id }) + .from(subscription) + .where( + and( + eq(subscription.referenceId, organizationId), + eq(subscription.plan, 'enterprise'), + inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES) + ) + ) + .for('update') + .limit(1) + if (!subscriptionRow) throw new Error('Active Enterprise subscription not found') + const [event] = await tx + .select({ status: outboxEvent.status, payload: outboxEvent.payload }) + .from(outboxEvent) + .where( + and( + eq(outboxEvent.id, operationId), + eq(outboxEvent.eventType, ENTERPRISE_METADATA_SYNC_EVENT_TYPE) + ) + ) + .for('update') + .limit(1) + const payload = enterpriseMetadataSyncPayloadSchema.safeParse(event?.payload) + if (!event || !payload.success || payload.data.subscriptionId !== subscriptionRow.id) { + throw new Error('Enterprise configuration update not found') + } + if (event.status !== 'dead_letter') { + throw new Error('Only a failed Enterprise configuration update can be retried') + } + await tx + .update(outboxEvent) + .set({ + status: 'pending', + attempts: 0, + lastError: null, + availableAt: new Date(), + lockedAt: null, + processedAt: null, + payload: sql`((${outboxEvent.payload}::jsonb - 'acknowledgement') || ${JSON.stringify({ deliveryRevision: payload.data.deliveryRevision + 1 })}::jsonb)::json`, + }) + .where(eq(outboxEvent.id, operationId)) + }) + recordAudit({ + actorId: actor.id, + actorName: actor.name, + actorEmail: actor.email, + action: AuditAction.ORGANIZATION_UPDATED, + resourceType: AuditResourceType.ORGANIZATION, + resourceId: organizationId, + description: 'Admin retried Enterprise configuration update', + metadata: { operationId }, + }) +} + export async function updateDashboardOrganizationLimits( organizationId: string, values: { @@ -634,6 +1303,13 @@ export async function updateDashboardOrganizationLimits( if (!hasPaidSubscriptionStatus(subscriptionRow.status)) { throw new Error('Enterprise limits can be changed only for an active subscription') } + const prepaidBalanceDollars = Number(org.creditBalance ?? 0) + if ( + values.usageLimitDollars !== undefined && + values.usageLimitDollars < prepaidBalanceDollars + ) { + throw new Error('Enterprise usage limit cannot be below its prepaid balance') + } await enqueueEnterpriseMetadataIntent(tx, { subscriptionId: subscriptionRow.id, appliedMetadata: subscriptionRow.metadata, @@ -642,9 +1318,11 @@ export async function updateDashboardOrganizationLimits( values.usageLimitDollars === undefined ? Math.round( metadataNumber(current, 'usageLimitCredits') ?? - dollarsToCredits(Number(org.orgUsageLimit ?? 0)) + dollarsToCredits( + Math.max(0, Number(org.orgUsageLimit ?? 0) - prepaidBalanceDollars) + ) ) - : dollarsToCredits(values.usageLimitDollars) + : dollarsToCredits(values.usageLimitDollars - prepaidBalanceDollars) return { ...current, usageLimitCredits: configuredUsageLimit, diff --git a/apps/sim/lib/api/contracts/v1/admin/dashboard.test.ts b/apps/sim/lib/api/contracts/v1/admin/dashboard.test.ts index 870487d3651..80042a87304 100644 --- a/apps/sim/lib/api/contracts/v1/admin/dashboard.test.ts +++ b/apps/sim/lib/api/contracts/v1/admin/dashboard.test.ts @@ -3,8 +3,11 @@ import { describe, expect, it } from 'vitest' import { adminDashboardBalanceGrantBodySchema, + adminDashboardEnterprisePreflightQuerySchema, + adminDashboardEnterprisePreflightSchema, adminDashboardIssueEnterpriseBodySchema, adminDashboardLimitsBodySchema, + adminDashboardOrganizationDetailQuerySchema, adminDashboardOrganizationSummarySchema, adminDashboardUpdateMemberBodySchema, } from '@/lib/api/contracts/v1/admin/dashboard' @@ -63,7 +66,17 @@ describe('admin dashboard credit grant contract', () => { usageLimitDollars: 0.001, effectiveUsageLimitDollars: 0.001, prepaidBalanceDollars: 0.001, + invoiceAmountUsd: null, monthlyInvoiceAmountUsd: null, + billingInterval: null, + reportingPeriod: { + anchorDate: null, + interval: null, + currentStart: '2026-08-01T00:00:00.000Z', + currentEnd: '2026-09-01T00:00:00.000Z', + source: 'default', + }, + usage: { usedDollars: 0.001, limitDollars: 0.001 }, provisioning: null, }).success ).toBe(true) @@ -74,7 +87,7 @@ describe('admin dashboard credit grant contract', () => { expect( adminDashboardIssueEnterpriseBodySchema.safeParse({ ownerUserId: 'owner-1', - monthlyInvoiceAmountUsd: 500, + invoiceAmountUsd: 500, seats: 10, concurrencyLimit: 1250, pausePaymentCollection: true, @@ -84,6 +97,115 @@ describe('admin dashboard credit grant contract', () => { expect(adminDashboardLimitsBodySchema.safeParse({ concurrencyLimit: 1.5 }).success).toBe(false) }) + it('defaults Enterprise issuance to annual while accepting an explicit monthly cadence', () => { + const annual = adminDashboardIssueEnterpriseBodySchema.parse({ + ownerUserId: 'owner-1', + invoiceAmountUsd: 1_200, + seats: 10, + }) + const monthly = adminDashboardIssueEnterpriseBodySchema.parse({ + ownerUserId: 'owner-1', + invoiceAmountUsd: 100, + billingInterval: 'month', + seats: 10, + }) + + expect(annual.billingInterval).toBe('year') + expect(monthly.billingInterval).toBe('month') + }) + + it('keeps the legacy monthly issuance request monthly during a rolling deployment', () => { + const legacy = adminDashboardIssueEnterpriseBodySchema.parse({ + ownerUserId: 'owner-1', + monthlyInvoiceAmountUsd: 100, + seats: 10, + }) + + expect(legacy).toMatchObject({ + billingInterval: 'month', + invoiceAmountUsd: 100, + }) + expect(legacy).not.toHaveProperty('monthlyInvoiceAmountUsd') + }) + + it('rejects conflicting legacy and interval invoice amounts', () => { + expect( + adminDashboardIssueEnterpriseBodySchema.safeParse({ + ownerUserId: 'owner-1', + invoiceAmountUsd: 1_200, + monthlyInvoiceAmountUsd: 100, + seats: 10, + }).success + ).toBe(false) + }) + + it('rejects a monthly-named legacy amount with an explicit annual cadence', () => { + expect( + adminDashboardIssueEnterpriseBodySchema.safeParse({ + ownerUserId: 'owner-1', + monthlyInvoiceAmountUsd: 100, + billingInterval: 'year', + seats: 10, + }).success + ).toBe(false) + }) + + it('paginates Enterprise workspace preflight without hiding the total inventory', () => { + expect(adminDashboardEnterprisePreflightQuerySchema.parse({ ownerUserId: 'owner-1' })).toEqual({ + ownerUserId: 'owner-1', + search: '', + limit: 50, + offset: 0, + }) + expect( + adminDashboardEnterprisePreflightSchema.safeParse({ + owner: { id: 'owner-1', name: 'Owner', email: 'owner@example.com' }, + organization: null, + personalWorkspaces: [{ id: 'workspace-1', name: 'One', archived: false }], + workspacePagination: { total: 51, limit: 50, offset: 0, hasMore: true }, + workspaceSelection: { + totalEligible: 51, + defaultSelectedIds: Array.from({ length: 51 }, (_, index) => `workspace-${index + 1}`), + defaultSelectedWorkspaces: Array.from({ length: 51 }, (_, index) => ({ + id: `workspace-${index + 1}`, + name: `Workspace ${index + 1}`, + archived: false, + })), + includesAllEligible: true, + limit: 1_000, + }, + billingPreview: null, + canIssue: true, + reason: null, + }).success + ).toBe(true) + }) + + it('keeps organization detail unbounded for legacy callers and supports bounded collection pages', () => { + expect(adminDashboardOrganizationDetailQuerySchema.parse({})).toEqual({ + paginated: false, + limit: 50, + memberOffset: 0, + externalCollaboratorOffset: 0, + workspaceOffset: 0, + }) + expect( + adminDashboardOrganizationDetailQuerySchema.parse({ + paginated: 'true', + limit: '25', + memberOffset: '50', + externalCollaboratorOffset: '75', + workspaceOffset: '100', + }) + ).toEqual({ + paginated: true, + limit: 25, + memberOffset: 50, + externalCollaboratorOffset: 75, + workspaceOffset: 100, + }) + }) + it('does not expose included allowance as an editable organization control', () => { expect(adminDashboardLimitsBodySchema.safeParse({ includedMonthlyDollars: 100 }).success).toBe( false @@ -95,7 +217,7 @@ describe('admin dashboard credit grant contract', () => { expect( adminDashboardIssueEnterpriseBodySchema.safeParse({ ownerUserId: 'owner-1', - monthlyInvoiceAmountUsd: 500, + invoiceAmountUsd: 500, seats: 10, concurrencyLimit: null, }).success diff --git a/apps/sim/lib/api/contracts/v1/admin/dashboard.ts b/apps/sim/lib/api/contracts/v1/admin/dashboard.ts index 4ca6412acab..33aa3b9a9d1 100644 --- a/apps/sim/lib/api/contracts/v1/admin/dashboard.ts +++ b/apps/sim/lib/api/contracts/v1/admin/dashboard.ts @@ -1,8 +1,10 @@ import { z } from 'zod' import { type ContractJsonResponse, defineRouteContract } from '@/lib/api/contracts/types' import { + adminV1BooleanQuerySchema, adminV1IdParamsSchema, adminV1ListResponseSchema, + adminV1PaginationMetaSchema, adminV1PaginationQuerySchema, adminV1QueryStringSchema, adminV1SingleResponseSchema, @@ -31,14 +33,43 @@ export const adminDashboardUserSchema = z.object({ name: z.string(), email: z.string(), activeOrganization: z.object({ id: z.string(), name: z.string() }).nullable(), + usageDollars: dollarAmountSchema, }) +const adminDashboardBillingIntervalSchema = z.enum(['month', 'year']) +const adminDashboardReportingPeriodSchema = z.object({ + anchorDate: z.string().nullable(), + interval: adminDashboardBillingIntervalSchema.nullable(), + currentStart: z.string(), + currentEnd: z.string(), + source: z.enum(['reporting', 'stripe', 'default']), +}) +const adminDashboardUsageSchema = z.object({ + usedDollars: dollarAmountSchema, + limitDollars: dollarAmountSchema, +}) +const adminDashboardWorkspaceMoveProgressSchema = z.object({ + selected: z.number().int().min(0), + moved: z.number().int().min(0), + pending: z.number().int().min(0), + failedCount: z.number().int().min(0), + failed: z.array( + z.object({ eventId: z.string(), workspaceId: z.string(), error: z.string().nullable() }) + ), +}) + +const adminDashboardDateOnlySchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/) +const adminDashboardInvoiceAmountSchema = z.number().min(0.01).max(10_000_000).multipleOf(0.01) + export const adminDashboardProvisioningSchema = z.object({ id: z.string(), ownerUserId: z.string(), organizationId: z.string(), status: z.enum(['pending', 'processing', 'dead_letter', 'awaiting_webhook', 'applied']), - monthlyInvoiceAmountUsd: z.number(), + invoiceAmountUsd: z.number(), + monthlyInvoiceAmountUsd: z.number().nullable(), + billingInterval: adminDashboardBillingIntervalSchema, + reportingPeriodAnchorDate: z.string().nullable(), usageLimitDollars: creditAlignedDollarAmountSchema, seats: z.number().int().positive(), concurrencyLimit: z.number().int().positive().max(MAX_BILLING_CONCURRENCY_LIMIT), @@ -52,6 +83,7 @@ export const adminDashboardProvisioningSchema = z.object({ error: z.string().nullable(), createdAt: z.string(), updatedAt: z.string(), + workspaceMoves: adminDashboardWorkspaceMoveProgressSchema, }) export const adminDashboardOrganizationSummarySchema = z.object({ @@ -76,7 +108,11 @@ export const adminDashboardOrganizationSummarySchema = z.object({ usageLimitDollars: dollarAmountSchema, effectiveUsageLimitDollars: dollarAmountSchema, prepaidBalanceDollars: dollarAmountSchema, + invoiceAmountUsd: z.number().nullable(), monthlyInvoiceAmountUsd: z.number().nullable(), + billingInterval: adminDashboardBillingIntervalSchema.nullable(), + reportingPeriod: adminDashboardReportingPeriodSchema, + usage: adminDashboardUsageSchema, provisioning: adminDashboardProvisioningSchema.nullable(), }) @@ -87,6 +123,9 @@ export const adminDashboardOrganizationDetailSchema = id: z.string(), status: z.enum(['pending', 'processing', 'failed']), requestedUsageLimitDollars: dollarAmountSchema.nullable(), + requestedInvoiceAmountUsd: z.number().positive().nullable(), + requestedBillingInterval: adminDashboardBillingIntervalSchema.nullable(), + requestedReportingPeriodAnchorDate: adminDashboardDateOnlySchema.nullable(), requestedSeats: z.number().int().positive().nullable(), requestedConcurrencyLimit: z .number() @@ -100,9 +139,14 @@ export const adminDashboardOrganizationDetailSchema = .positive() .max(MAX_WORKFLOW_EXECUTION_TIMEOUT_SECONDS) .nullable(), + providerAccepted: z.boolean(), error: z.string().nullable(), }) .nullable(), + historicalActorUsage: z.object({ + usedDollars: dollarAmountSchema, + actorCount: z.number().int().min(0), + }), members: z.array( z.object({ id: z.string(), @@ -111,6 +155,7 @@ export const adminDashboardOrganizationDetailSchema = email: z.string(), role: z.string(), usageLimitDollars: dollarAmountSchema.nullable(), + usageDollars: dollarAmountSchema, }) ), externalCollaborators: z.array( @@ -120,9 +165,13 @@ export const adminDashboardOrganizationDetailSchema = email: z.string(), workspaceCount: z.number().int().min(1), usageLimitDollars: dollarAmountSchema.nullable(), + usageDollars: dollarAmountSchema, }) ), workspaces: z.array(z.object({ id: z.string(), name: z.string() })), + memberPagination: adminV1PaginationMetaSchema, + externalCollaboratorPagination: adminV1PaginationMetaSchema, + workspacePagination: adminV1PaginationMetaSchema, subscription: z .object({ id: z.string(), @@ -140,22 +189,72 @@ export const adminDashboardSearchQuerySchema = adminV1PaginationQuerySchema.exte search: adminV1QueryStringSchema.default(''), }) -export const adminDashboardIssueEnterpriseBodySchema = z.object({ - ownerUserId: z.string().min(1), - organizationName: z.string().trim().min(1).max(120).optional(), - monthlyInvoiceAmountUsd: z.number().min(0.01).max(10_000_000).multipleOf(0.01), - usageLimitDollars: creditAlignedDollarAmountSchema.optional(), - seats: z.number().int().positive().max(100_000), - concurrencyLimit: z.number().int().positive().max(MAX_BILLING_CONCURRENCY_LIMIT).optional(), - workflowExecutionTimeoutSeconds: z - .number() - .int() - .positive() - .max(MAX_WORKFLOW_EXECUTION_TIMEOUT_SECONDS) - .optional(), - pausePaymentCollection: z.boolean().optional(), +export const adminDashboardOrganizationDetailQuerySchema = z.object({ + paginated: adminV1BooleanQuerySchema, + limit: adminV1PaginationQuerySchema.shape.limit.default(50), + memberOffset: adminV1PaginationQuerySchema.shape.offset.default(0), + externalCollaboratorOffset: adminV1PaginationQuerySchema.shape.offset.default(0), + workspaceOffset: adminV1PaginationQuerySchema.shape.offset.default(0), }) +export const adminDashboardIssueEnterpriseBodySchema = z + .object({ + ownerUserId: z.string().min(1), + organizationName: z.string().trim().min(1).max(120).optional(), + invoiceAmountUsd: adminDashboardInvoiceAmountSchema.optional(), + monthlyInvoiceAmountUsd: adminDashboardInvoiceAmountSchema.optional(), + billingInterval: adminDashboardBillingIntervalSchema.optional(), + reportingPeriodAnchorDate: adminDashboardDateOnlySchema.optional(), + workspaceIds: z.array(z.string().min(1)).max(1_000).default([]), + usageLimitDollars: creditAlignedDollarAmountSchema.optional(), + seats: z.number().int().positive().max(100_000), + concurrencyLimit: z.number().int().positive().max(MAX_BILLING_CONCURRENCY_LIMIT).optional(), + workflowExecutionTimeoutSeconds: z + .number() + .int() + .positive() + .max(MAX_WORKFLOW_EXECUTION_TIMEOUT_SECONDS) + .optional(), + pausePaymentCollection: z.boolean().optional(), + }) + .superRefine((body, context) => { + if (body.invoiceAmountUsd === undefined && body.monthlyInvoiceAmountUsd === undefined) { + context.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Invoice amount is required', + path: ['invoiceAmountUsd'], + }) + } + if ( + body.invoiceAmountUsd !== undefined && + body.monthlyInvoiceAmountUsd !== undefined && + body.invoiceAmountUsd !== body.monthlyInvoiceAmountUsd + ) { + context.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Legacy and interval invoice amounts must match when both are provided', + path: ['monthlyInvoiceAmountUsd'], + }) + } + if ( + body.invoiceAmountUsd === undefined && + body.monthlyInvoiceAmountUsd !== undefined && + body.billingInterval === 'year' + ) { + context.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Annual cadence requires the interval-neutral invoiceAmountUsd field', + path: ['invoiceAmountUsd'], + }) + } + }) + .transform(({ monthlyInvoiceAmountUsd, ...body }) => ({ + ...body, + invoiceAmountUsd: body.invoiceAmountUsd ?? (monthlyInvoiceAmountUsd as number), + billingInterval: + body.billingInterval ?? (body.invoiceAmountUsd === undefined ? ('month' as const) : 'year'), + })) + export const adminDashboardSeatsBodySchema = z.object({ seats: z.number().int().positive().max(100_000), }) @@ -221,6 +320,88 @@ export const adminDashboardMemberPreflightSchema = z.object({ reason: z.string().nullable(), }) +export const adminDashboardEnterprisePreflightQuerySchema = adminDashboardSearchQuerySchema.extend({ + ownerUserId: z.string().min(1), + limit: adminV1PaginationQuerySchema.shape.limit.default(50), + offset: adminV1PaginationQuerySchema.shape.offset.default(0), + invoiceAmountUsd: z.coerce.number().min(0.01).max(10_000_000).multipleOf(0.01).optional(), + billingInterval: adminDashboardBillingIntervalSchema.optional(), + reportingPeriodAnchorDate: adminDashboardDateOnlySchema.optional(), + usageLimitDollars: z.coerce + .number() + .finite() + .min(0) + .max(Number.MAX_SAFE_INTEGER / 200) + .refine((value) => Math.abs(value * 200 - Math.round(value * 200)) < 1e-8, { + error: 'Dollar amounts must use $0.005 increments', + }) + .optional(), +}) + +export const adminDashboardEnterprisePreflightSchema = z.object({ + owner: z.object({ id: z.string(), name: z.string(), email: z.string() }), + organization: z.object({ id: z.string(), name: z.string(), role: z.string() }).nullable(), + personalWorkspaces: z.array( + z.object({ id: z.string(), name: z.string(), archived: z.boolean() }) + ), + workspacePagination: adminV1PaginationMetaSchema, + workspaceSelection: z + .object({ + totalEligible: z.number().int().min(0), + defaultSelectedIds: z.array(z.string().min(1)).max(1_000), + defaultSelectedWorkspaces: z + .array(z.object({ id: z.string(), name: z.string(), archived: z.boolean() })) + .max(1_000), + includesAllEligible: z.boolean(), + limit: z.literal(1_000), + }) + .superRefine((selection, context) => { + const validCompleteSelection = + selection.includesAllEligible && + selection.totalEligible <= selection.limit && + selection.defaultSelectedIds.length === selection.totalEligible && + selection.defaultSelectedWorkspaces.length === selection.totalEligible + const validOverLimitSelection = + !selection.includesAllEligible && + selection.totalEligible > selection.limit && + selection.defaultSelectedIds.length === 0 && + selection.defaultSelectedWorkspaces.length === 0 + if (!validCompleteSelection && !validOverLimitSelection) { + context.addIssue({ + code: z.ZodIssueCode.custom, + message: + 'Default workspace selection must be complete or explicitly empty when over limit', + path: ['defaultSelectedIds'], + }) + } + }), + billingPreview: z + .object({ + reportingPeriod: adminDashboardReportingPeriodSchema, + usage: adminDashboardUsageSchema, + invoiceAmountUsd: adminDashboardInvoiceAmountSchema, + configuredUsageLimitDollars: dollarAmountSchema, + prepaidBalanceDollars: dollarAmountSchema, + effectiveUsageLimitDollars: dollarAmountSchema, + exceedsLimit: z.boolean(), + }) + .nullable(), + canIssue: z.boolean(), + reason: z.string().nullable(), +}) + +export const adminDashboardBillingTermsBodySchema = z.object({ + invoiceAmountUsd: z.number().min(0.01).max(10_000_000).multipleOf(0.01), + billingInterval: adminDashboardBillingIntervalSchema, + reportingPeriodAnchorDate: adminDashboardDateOnlySchema, +}) + +export const adminDashboardBillingTermsPreviewSchema = z.object({ + reportingPeriod: adminDashboardReportingPeriodSchema, + usage: adminDashboardUsageSchema, + exceedsLimit: z.boolean(), +}) + export const adminDashboardMemberParamsSchema = adminV1IdParamsSchema.extend({ memberId: z.string().min(1), }) @@ -246,6 +427,10 @@ export const adminDashboardTransferOwnershipBodySchema = z.object({ newOwnerUserId: z.string().min(1), }) +export const adminDashboardRetryConfigurationUpdateBodySchema = z.object({ + operationId: z.string().min(1), +}) + const adminDashboardMutationResultSchema = z.object({ success: z.literal(true) }) const adminDashboardBalanceGrantResultSchema = adminDashboardMutationResultSchema.extend({ prepaidBalanceDollars: dollarAmountSchema, @@ -280,6 +465,7 @@ export const adminDashboardGetOrganizationContract = defineRouteContract({ method: 'GET', path: '/api/v1/admin/dashboard/organizations/[id]', params: adminV1IdParamsSchema, + query: adminDashboardOrganizationDetailQuerySchema, response: { mode: 'json', schema: adminV1SingleResponseSchema(adminDashboardOrganizationDetailSchema), @@ -296,6 +482,16 @@ export const adminDashboardIssueEnterpriseContract = defineRouteContract({ }, }) +export const adminDashboardEnterprisePreflightContract = defineRouteContract({ + method: 'GET', + path: '/api/v1/admin/dashboard/enterprise-provisioning/preflight', + query: adminDashboardEnterprisePreflightQuerySchema, + response: { + mode: 'json', + schema: adminV1SingleResponseSchema(adminDashboardEnterprisePreflightSchema), + }, +}) + export const adminDashboardRetryEnterpriseContract = defineRouteContract({ method: 'POST', path: '/api/v1/admin/dashboard/enterprise-provisioning/[id]/retry', @@ -306,6 +502,16 @@ export const adminDashboardRetryEnterpriseContract = defineRouteContract({ }, }) +export const adminDashboardRetryEnterpriseWorkspaceMoveContract = defineRouteContract({ + method: 'POST', + path: '/api/v1/admin/dashboard/enterprise-provisioning/[id]/workspace-moves/[moveId]/retry', + params: adminV1IdParamsSchema.extend({ moveId: z.string().min(1) }), + response: { + mode: 'json', + schema: adminV1SingleResponseSchema(adminDashboardProvisioningSchema), + }, +}) + export const adminDashboardUpdateSeatsContract = defineRouteContract({ method: 'PATCH', path: '/api/v1/admin/dashboard/organizations/[id]/seats', @@ -328,6 +534,39 @@ export const adminDashboardUpdateLimitsContract = defineRouteContract({ }, }) +export const adminDashboardPreviewBillingTermsContract = defineRouteContract({ + method: 'POST', + path: '/api/v1/admin/dashboard/organizations/[id]/billing-terms/preview', + params: adminV1IdParamsSchema, + body: adminDashboardBillingTermsBodySchema, + response: { + mode: 'json', + schema: adminV1SingleResponseSchema(adminDashboardBillingTermsPreviewSchema), + }, +}) + +export const adminDashboardUpdateBillingTermsContract = defineRouteContract({ + method: 'PATCH', + path: '/api/v1/admin/dashboard/organizations/[id]/billing-terms', + params: adminV1IdParamsSchema, + body: adminDashboardBillingTermsBodySchema, + response: { + mode: 'json', + schema: adminV1SingleResponseSchema(adminDashboardMutationResultSchema), + }, +}) + +export const adminDashboardRetryConfigurationUpdateContract = defineRouteContract({ + method: 'POST', + path: '/api/v1/admin/dashboard/organizations/[id]/configuration-update/retry', + params: adminV1IdParamsSchema, + body: adminDashboardRetryConfigurationUpdateBodySchema, + response: { + mode: 'json', + schema: adminV1SingleResponseSchema(adminDashboardMutationResultSchema), + }, +}) + export const adminDashboardGrantBalanceContract = defineRouteContract({ method: 'POST', path: '/api/v1/admin/dashboard/organizations/[id]/credits', diff --git a/apps/sim/lib/billing/calculations/usage-monitor.ts b/apps/sim/lib/billing/calculations/usage-monitor.ts index 14a2cd47db5..0cfe7981ec8 100644 --- a/apps/sim/lib/billing/calculations/usage-monitor.ts +++ b/apps/sim/lib/billing/calculations/usage-monitor.ts @@ -6,12 +6,18 @@ import { eq } from 'drizzle-orm' import { isOrganizationBillingBlocked } from '@/lib/billing/core/access' import { defaultBillingPeriod } from '@/lib/billing/core/billing-period' import { getHighestPrioritySubscription } from '@/lib/billing/core/plan' +import { resolveSubscriptionUsagePeriod } from '@/lib/billing/core/reporting-period' import { getPooledOrgCurrentPeriodCost, getUserUsageLimit, type UsageLimitSubscription, } from '@/lib/billing/core/usage' -import { type BillingEntity, getBillingPeriodUsageCost } from '@/lib/billing/core/usage-log' +import { + type BillingContext, + type BillingEntity, + getBillingPeriodUsageCost, + type UsageQueryPeriod, +} from '@/lib/billing/core/usage-log' import { dollarsToCredits } from '@/lib/billing/credits/conversion' import { computeDailyRefreshConsumed, @@ -48,26 +54,30 @@ interface UsageData { async function computePooledOrgUsage( organizationId: string, - sub: { - plan: string | null - seats: number | null - periodStart: Date | null - periodEnd: Date | null - } + sub: UsageLimitSubscription, + preloadedBillingPeriod?: UsageQueryPeriod ): Promise { const { memberIds, currentPeriodCost } = await getPooledOrgCurrentPeriodCost(organizationId) if (memberIds.length === 0) return 0 - const billingPeriod = - sub.periodStart && sub.periodEnd - ? { start: sub.periodStart, end: sub.periodEnd } - : defaultBillingPeriod() + const billingPeriod = preloadedBillingPeriod ?? + resolveSubscriptionUsagePeriod(sub) ?? { + ...defaultBillingPeriod(), + source: 'default' as const, + anchorDate: null, + interval: null, + } const ledgerUsage = await getBillingPeriodUsageCost( { type: 'organization', id: organizationId }, billingPeriod ) - return applyOrgRefresh(organizationId, sub, currentPeriodCost + ledgerUsage, memberIds) + return applyOrgRefresh( + organizationId, + sub, + (billingPeriod.source === 'reporting' ? 0 : currentPeriodCost) + ledgerUsage, + memberIds + ) } /** @@ -76,7 +86,8 @@ async function computePooledOrgUsage( */ export async function checkUsageStatus( userId: string, - preloadedSubscription?: UsageLimitSubscription | null + preloadedSubscription?: UsageLimitSubscription | null, + preloadedBillingContext?: BillingContext ): Promise { try { if (!isBillingEnabled) { @@ -108,7 +119,11 @@ export async function checkUsageStatus( const organizationId: string | null = subIsOrgScoped && sub ? sub.referenceId : null if (subIsOrgScoped && sub) { - const currentUsage = await computePooledOrgUsage(sub.referenceId, sub) + const currentUsage = await computePooledOrgUsage( + sub.referenceId, + sub, + preloadedBillingContext?.billingPeriod + ) return buildUsageData({ currentUsage, limit, scope, organizationId }) } @@ -132,9 +147,10 @@ export async function checkUsageStatus( } const billingPeriod = - sub?.periodStart && sub.periodEnd + preloadedBillingContext?.billingPeriod ?? + (sub?.periodStart && sub.periodEnd ? { start: sub.periodStart, end: sub.periodEnd } - : defaultBillingPeriod() + : defaultBillingPeriod()) const ledgerUsage = await getBillingPeriodUsageCost({ type: 'user', id: userId }, billingPeriod) let currentUsage = toNumber(toDecimal(statsRecords[0].currentPeriodCost)) + ledgerUsage if (sub && isPaid(sub.plan) && sub.periodStart) { @@ -372,7 +388,8 @@ export async function checkBillingEntityBlocked( */ export async function checkServerSideUsageLimits( userId: string, - preloadedSubscription?: UsageLimitSubscription | null + preloadedSubscription?: UsageLimitSubscription | null, + preloadedBillingContext?: BillingContext ): Promise<{ isExceeded: boolean currentUsage: number @@ -403,7 +420,7 @@ export async function checkServerSideUsageLimits( return { isExceeded: true, currentUsage, limit: 0, message: blocked.message } } - const usageData = await checkUsageStatus(userId, preloadedSubscription) + const usageData = await checkUsageStatus(userId, preloadedSubscription, preloadedBillingContext) const formattedUsage = (usageData.currentUsage ?? 0).toFixed(2) const formattedLimit = (usageData.limit ?? 0).toFixed(2) @@ -455,7 +472,7 @@ export async function checkServerSideUsageLimits( export async function checkOrganizationMemberUsageLimit( userId: string, organizationId: string, - billingPeriod: { start: Date; end: Date } + billingPeriod: UsageQueryPeriod ): Promise { try { if (!isHosted || !isBillingEnabled || !organizationId) { diff --git a/apps/sim/lib/billing/core/billing-attribution.test.ts b/apps/sim/lib/billing/core/billing-attribution.test.ts index 8f813492072..41068d71088 100644 --- a/apps/sim/lib/billing/core/billing-attribution.test.ts +++ b/apps/sim/lib/billing/core/billing-attribution.test.ts @@ -110,6 +110,7 @@ describe('resolveBillingAttribution', () => { billingEntity: { id: 'org-b', type: 'organization' }, billingPeriod: { end: '2026-08-01T00:00:00.000Z', + source: 'stripe', start: '2026-07-01T00:00:00.000Z', }, organizationId: 'org-b', @@ -371,6 +372,7 @@ describe('resolveBillingAttribution', () => { billingEntity: { type: 'organization', id: 'org-b' }, billingPeriod: { end: new Date('2026-08-01T00:00:00.000Z'), + source: 'stripe', start: new Date('2026-07-01T00:00:00.000Z'), }, }) @@ -385,6 +387,7 @@ describe('serialized attribution boundaries', () => { billingPeriod: { start: '2026-07-01T00:00:00.000Z', end: '2026-08-01T00:00:00.000Z', + source: 'stripe', }, organizationId: 'org-b', payerSubscription: { @@ -464,6 +467,7 @@ describe('serialized attribution boundaries', () => { billingPeriod: { start: '2026-06-30T20:00:00.000-04:00', end: '2026-07-31T20:00:00.000-04:00', + source: 'stripe', }, }) ).toBe(true) @@ -674,6 +678,19 @@ describe('checkAttributedUsageLimits', () => { start: new Date('2026-07-01T00:00:00.000Z'), }) }) + + it('preserves a custom reporting-period source for the per-member cap', async () => { + await checkAttributedUsageLimits({ + ...attribution, + billingPeriod: { ...attribution.billingPeriod, source: 'reporting' }, + }) + + expect(mockCheckOrganizationMemberUsageLimit).toHaveBeenCalledWith('external-a', 'org-b', { + end: new Date('2026-08-01T00:00:00.000Z'), + source: 'reporting', + start: new Date('2026-07-01T00:00:00.000Z'), + }) + }) }) describe('modern billing envelopes', () => { @@ -711,6 +728,7 @@ describe('modern billing envelopes', () => { billingPeriod: { start: '2026-07-01T00:00:00.000Z', end: '2026-08-01T00:00:00.000Z', + source: 'reporting' as const, }, } const serialized = serializeAccountBillingDecisionHeader(decision) diff --git a/apps/sim/lib/billing/core/billing-attribution.ts b/apps/sim/lib/billing/core/billing-attribution.ts index 5c99fa846ba..a0c3d27521c 100644 --- a/apps/sim/lib/billing/core/billing-attribution.ts +++ b/apps/sim/lib/billing/core/billing-attribution.ts @@ -13,6 +13,11 @@ import { parseBillingConcurrencyLimit } from '@/lib/billing/concurrency-defaults import { getOrganizationSubscription } from '@/lib/billing/core/billing' import { defaultBillingPeriod } from '@/lib/billing/core/billing-period' import { getHighestPriorityPersonalSubscription } from '@/lib/billing/core/plan' +import { + ENTERPRISE_REPORTING_PERIOD_ANCHOR_METADATA_KEY, + resolveSubscriptionUsagePeriod, + type UsagePeriodSource, +} from '@/lib/billing/core/reporting-period' import type { BillingContext, BillingEntity } from '@/lib/billing/core/usage-log' import { parseWorkflowExecutionTimeoutSeconds } from '@/lib/billing/execution-timeout-defaults' import { isEnterprise } from '@/lib/billing/plan-helpers' @@ -46,6 +51,8 @@ export interface PayerSubscriptionSnapshot { readonly seats: number | null readonly periodStart: string | null readonly periodEnd: string | null + readonly billingInterval?: 'month' | 'year' + readonly enterpriseReportingPeriodAnchorDate?: string readonly enterpriseConcurrencyLimit?: number readonly enterpriseWorkflowExecutionTimeoutSeconds?: number } @@ -53,6 +60,7 @@ export interface PayerSubscriptionSnapshot { export interface BillingPeriodSnapshot { readonly start: string readonly end: string + readonly source?: UsagePeriodSource } /** @@ -85,6 +93,7 @@ export interface AccountBillingDecision { readonly billingPeriod: { readonly start: string readonly end: string + readonly source?: UsagePeriodSource } } @@ -134,6 +143,14 @@ function serializeSubscription( isEnterprise(subscription.plan) && isRecordLike(subscription.metadata) ? parseWorkflowExecutionTimeoutSeconds(subscription.metadata.workflowExecutionTimeoutSeconds) : null + const billingInterval = + subscription.billingInterval === 'month' || subscription.billingInterval === 'year' + ? subscription.billingInterval + : null + const enterpriseReportingPeriodAnchorDate = + isEnterprise(subscription.plan) && isRecordLike(subscription.metadata) + ? subscription.metadata[ENTERPRISE_REPORTING_PERIOD_ANCHOR_METADATA_KEY] + : null return Object.freeze({ id: subscription.id, referenceId: subscription.referenceId, @@ -142,6 +159,10 @@ function serializeSubscription( seats: subscription.seats ?? null, periodStart: subscription.periodStart?.toISOString() ?? null, periodEnd: subscription.periodEnd?.toISOString() ?? null, + ...(billingInterval ? { billingInterval } : {}), + ...(typeof enterpriseReportingPeriodAnchorDate === 'string' + ? { enterpriseReportingPeriodAnchorDate } + : {}), ...(enterpriseConcurrencyLimit !== null ? { enterpriseConcurrencyLimit } : {}), ...(enterpriseWorkflowExecutionTimeoutSeconds !== null ? { enterpriseWorkflowExecutionTimeoutSeconds } @@ -224,6 +245,15 @@ export function assertBillingAttributionSnapshot(value: unknown): BillingAttribu if (periodEnd <= periodStart) { throw new Error('Billing attribution billing period must end after it starts') } + const periodSource = rawPeriod.source + if ( + periodSource !== undefined && + periodSource !== 'reporting' && + periodSource !== 'stripe' && + periodSource !== 'default' + ) { + throw new Error('Billing attribution billing period source is invalid') + } let payerSubscription: PayerSubscriptionSnapshot | null = null if (raw.payerSubscription !== null) { @@ -279,6 +309,23 @@ export function assertBillingAttributionSnapshot(value: unknown): BillingAttribu ) { throw new Error('Billing attribution Enterprise workflow execution timeout is invalid') } + const billingInterval = subscription.billingInterval + if ( + billingInterval !== undefined && + billingInterval !== 'month' && + billingInterval !== 'year' + ) { + throw new Error('Billing attribution subscription interval is invalid') + } + const enterpriseReportingPeriodAnchorDate = subscription.enterpriseReportingPeriodAnchorDate + if ( + enterpriseReportingPeriodAnchorDate !== undefined && + (!isEnterprise(subscription.plan) || + typeof enterpriseReportingPeriodAnchorDate !== 'string' || + !/^\d{4}-\d{2}-\d{2}$/.test(enterpriseReportingPeriodAnchorDate)) + ) { + throw new Error('Billing attribution Enterprise reporting-period anchor is invalid') + } payerSubscription = { id: subscription.id, @@ -288,6 +335,10 @@ export function assertBillingAttributionSnapshot(value: unknown): BillingAttribu seats: subscription.seats as number | null, periodStart: subscriptionStart?.toISOString() ?? null, periodEnd: subscriptionEnd?.toISOString() ?? null, + ...(billingInterval !== undefined ? { billingInterval } : {}), + ...(enterpriseReportingPeriodAnchorDate !== undefined + ? { enterpriseReportingPeriodAnchorDate } + : {}), ...(enterpriseConcurrencyLimit !== undefined ? { enterpriseConcurrencyLimit } : {}), ...(enterpriseWorkflowExecutionTimeoutSeconds !== undefined ? { enterpriseWorkflowExecutionTimeoutSeconds } @@ -304,6 +355,7 @@ export function assertBillingAttributionSnapshot(value: unknown): BillingAttribu billingPeriod: { start: periodStart.toISOString(), end: periodEnd.toISOString(), + ...(periodSource !== undefined ? { source: periodSource } : {}), }, payerSubscription, }) @@ -431,6 +483,15 @@ function assertAccountBillingDecision(value: unknown): AccountBillingDecision { if (!Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime()) || end <= start) { throw new Error('Account billing decision must contain a valid billing period') } + const source = value.billingPeriod.source + if ( + source !== undefined && + source !== 'reporting' && + source !== 'stripe' && + source !== 'default' + ) { + throw new Error('Account billing decision must contain a valid billing period source') + } return Object.freeze({ userId: value.userId, @@ -441,6 +502,7 @@ function assertAccountBillingDecision(value: unknown): AccountBillingDecision { billingPeriod: Object.freeze({ start: start.toISOString(), end: end.toISOString(), + ...(source !== undefined ? { source } : {}), }), }) } @@ -494,6 +556,22 @@ export function toUsageLimitSubscription(attribution: BillingAttributionSnapshot seats: snapshot.seats, periodStart: snapshot.periodStart ? new Date(snapshot.periodStart) : null, periodEnd: snapshot.periodEnd ? new Date(snapshot.periodEnd) : null, + billingInterval: snapshot.billingInterval ?? null, + metadata: snapshot.enterpriseReportingPeriodAnchorDate + ? { + [ENTERPRISE_REPORTING_PERIOD_ANCHOR_METADATA_KEY]: + snapshot.enterpriseReportingPeriodAnchorDate, + } + : null, + usagePeriod: { + start: new Date(attribution.billingPeriod.start), + end: new Date(attribution.billingPeriod.end), + source: + attribution.billingPeriod.source ?? + (snapshot.enterpriseReportingPeriodAnchorDate ? 'reporting' : 'stripe'), + anchorDate: snapshot.enterpriseReportingPeriodAnchorDate ?? null, + interval: snapshot.billingInterval ?? null, + }, } } @@ -547,10 +625,10 @@ function buildBillingAttributionSnapshot(params: { }): BillingAttributionSnapshot { const { actorUserId, workspaceId, billedAccountUserId, organizationId, payerSubscription } = params - const period = - payerSubscription?.periodStart && payerSubscription.periodEnd - ? { start: payerSubscription.periodStart, end: payerSubscription.periodEnd } - : defaultBillingPeriod() + const period = resolveSubscriptionUsagePeriod(payerSubscription) ?? { + ...defaultBillingPeriod(), + source: 'default' as const, + } const billingEntity: BillingEntity = organizationId ? { type: 'organization', id: organizationId } : { type: 'user', id: billedAccountUserId } @@ -564,6 +642,7 @@ function buildBillingAttributionSnapshot(params: { billingPeriod: { start: period.start.toISOString(), end: period.end.toISOString(), + source: period.source, }, payerSubscription: serializeSubscription(payerSubscription), }) @@ -649,6 +728,9 @@ export function toBillingContext(attribution: BillingAttributionSnapshot): Billi billingPeriod: { start: new Date(validatedAttribution.billingPeriod.start), end: new Date(validatedAttribution.billingPeriod.end), + ...(validatedAttribution.billingPeriod.source + ? { source: validatedAttribution.billingPeriod.source } + : {}), }, } } @@ -744,6 +826,9 @@ export async function checkAttributedUsageLimits( { start: new Date(validatedAttribution.billingPeriod.start), end: new Date(validatedAttribution.billingPeriod.end), + ...(validatedAttribution.billingPeriod.source + ? { source: validatedAttribution.billingPeriod.source } + : {}), } ) if (memberUsage.isExceeded) { diff --git a/apps/sim/lib/billing/core/organization.ts b/apps/sim/lib/billing/core/organization.ts index 7290bf1d9dc..f2a168d5184 100644 --- a/apps/sim/lib/billing/core/organization.ts +++ b/apps/sim/lib/billing/core/organization.ts @@ -4,9 +4,11 @@ import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' import { isOrganizationBillingBlocked } from '@/lib/billing/core/access' import { getOrganizationSubscription, getPlanPricing } from '@/lib/billing/core/billing' +import { resolveSubscriptionUsagePeriod } from '@/lib/billing/core/reporting-period' import { getBillingPeriodUsageCost, getBillingPeriodUsageCostByUser, + type UsageQueryPeriod, } from '@/lib/billing/core/usage-log' import { computeDailyRefreshConsumed, @@ -67,26 +69,55 @@ interface MemberUsageData { */ export async function getOrgMemberLedgerByUser( organizationId: string, - period?: { start: Date; end: Date } | null, - executor: DbClient = db + period?: UsageQueryPeriod | null, + executor: DbClient = db, + userIds?: readonly string[] ): Promise> { let billingPeriod = period ?? null if (period === undefined) { const subscription = await getOrganizationSubscription(organizationId, { executor }) - billingPeriod = - subscription?.periodStart && subscription?.periodEnd - ? { start: subscription.periodStart, end: subscription.periodEnd } - : null + billingPeriod = resolveSubscriptionUsagePeriod(subscription) } if (!billingPeriod) return new Map() return getBillingPeriodUsageCostByUser( { type: 'organization', id: organizationId }, billingPeriod, undefined, - executor + executor, + userIds ) } +export interface OrganizationMemberUsageSnapshot { + billingPeriod: UsageQueryPeriod | null + includeLegacyBaseline: boolean + usageByUser: Map +} + +/** + * Resolves the organization's usage period once and returns the ledger usage + * for only the requested actors. Reporting periods never include the legacy + * userStats baseline; Stripe/default periods retain it for compatibility. + */ +export async function getOrganizationMemberUsageSnapshot( + organizationId: string, + options: { + executor?: DbClient + userIds?: readonly string[] + } = {} +): Promise { + const executor = options.executor ?? db + const subscription = await getOrganizationSubscription(organizationId, { executor }) + const billingPeriod = resolveSubscriptionUsagePeriod(subscription) + return { + billingPeriod, + includeLegacyBaseline: billingPeriod?.source !== 'reporting', + usageByUser: billingPeriod + ? await getOrgMemberLedgerByUser(organizationId, billingPeriod, executor, options.userIds) + : new Map(), + } +} + /** * Get comprehensive organization billing and usage data */ @@ -137,16 +168,14 @@ export async function getOrganizationBillingData( // Per-member current-period usage = userStats baseline + attributed usage_log // rows. currentPeriodCost is no longer incremented on the hot path, so the // baseline alone under-reports; add each member's ledger sum for the period. - const billingPeriod = - subscription.periodStart && subscription.periodEnd - ? { start: subscription.periodStart, end: subscription.periodEnd } - : null - const usageByUser = await getOrgMemberLedgerByUser(organizationId, billingPeriod, executor) + const { billingPeriod, includeLegacyBaseline, usageByUser } = + await getOrganizationMemberUsageSnapshot(organizationId, { executor }) // Process member data const members: MemberUsageData[] = membersWithUsage.map((memberRecord) => { const currentUsage = - Number(memberRecord.currentPeriodCost || 0) + (usageByUser.get(memberRecord.userId) ?? 0) + (includeLegacyBaseline ? Number(memberRecord.currentPeriodCost || 0) : 0) + + (usageByUser.get(memberRecord.userId) ?? 0) const usageLimit = Number(memberRecord.currentUsageLimit || getFreeTierLimit()) const percentUsed = usageLimit > 0 ? (currentUsage / usageLimit) * 100 : 0 @@ -168,10 +197,10 @@ export async function getOrganizationBillingData( // from raw baselines, NOT members[].currentUsage — the latter already folds // in per-member usage_log for display, so summing it AND adding the org // ledger would double-count. - let totalCurrentUsage = membersWithUsage.reduce( - (sum, m) => sum + Number(m.currentPeriodCost || 0), - 0 - ) + let totalCurrentUsage = + billingPeriod?.source === 'reporting' + ? 0 + : membersWithUsage.reduce((sum, m) => sum + Number(m.currentPeriodCost || 0), 0) if (billingPeriod) { totalCurrentUsage += await getBillingPeriodUsageCost( { type: 'organization', id: subscription.referenceId }, @@ -239,8 +268,8 @@ export async function getOrganizationBillingData( const pendingSeats = await countPendingSeatInvitations(organizationId, executor) const usedSeats = members.length + pendingSeats - const billingPeriodStart = subscription.periodStart || null - const billingPeriodEnd = subscription.periodEnd || null + const billingPeriodStart = billingPeriod?.start ?? null + const billingPeriodEnd = billingPeriod?.end ?? null return { organizationId, diff --git a/apps/sim/lib/billing/core/reporting-period.test.ts b/apps/sim/lib/billing/core/reporting-period.test.ts new file mode 100644 index 00000000000..863c477ff1d --- /dev/null +++ b/apps/sim/lib/billing/core/reporting-period.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from 'vitest' +import { + resolveEnterpriseReportingPeriod, + resolveSubscriptionUsagePeriod, +} from '@/lib/billing/core/reporting-period' + +describe('Enterprise reporting periods', () => { + it('resolves a backdated annual contract anniversary', () => { + expect( + resolveEnterpriseReportingPeriod('2025-06-15', 'year', new Date('2026-08-13T12:00:00.000Z')) + ).toMatchObject({ + start: new Date('2026-06-15T00:00:00.000Z'), + end: new Date('2027-06-15T00:00:00.000Z'), + source: 'reporting', + anchorDate: '2025-06-15', + interval: 'year', + }) + }) + + it('clamps monthly anniversaries to the last calendar day', () => { + expect( + resolveEnterpriseReportingPeriod('2026-01-31', 'month', new Date('2026-02-28T12:00:00.000Z')) + ).toMatchObject({ + start: new Date('2026-02-28T00:00:00.000Z'), + end: new Date('2026-03-31T00:00:00.000Z'), + }) + }) + + it('clamps leap-day annual anniversaries without drifting', () => { + expect( + resolveEnterpriseReportingPeriod('2024-02-29', 'year', new Date('2027-03-01T00:00:00.000Z')) + ).toMatchObject({ + start: new Date('2027-02-28T00:00:00.000Z'), + end: new Date('2028-02-29T00:00:00.000Z'), + }) + }) + + it('rolls to the next period at the exact end-exclusive anniversary', () => { + expect( + resolveEnterpriseReportingPeriod('2026-01-31', 'month', new Date('2026-03-31T00:00:00.000Z')) + ).toMatchObject({ + start: new Date('2026-03-31T00:00:00.000Z'), + end: new Date('2026-04-30T00:00:00.000Z'), + }) + }) + + it('falls back to the Stripe period when custom metadata is absent or future-dated', () => { + const stripe = { + plan: 'enterprise', + billingInterval: 'year', + periodStart: new Date('2026-08-01T00:00:00.000Z'), + periodEnd: new Date('2027-08-01T00:00:00.000Z'), + } + expect(resolveSubscriptionUsagePeriod(stripe)).toMatchObject({ source: 'stripe' }) + expect( + resolveSubscriptionUsagePeriod( + { ...stripe, metadata: { reportingPeriodAnchorDate: '2099-01-01' } }, + new Date('2026-08-13T00:00:00.000Z') + ) + ).toMatchObject({ source: 'stripe' }) + }) +}) diff --git a/apps/sim/lib/billing/core/reporting-period.ts b/apps/sim/lib/billing/core/reporting-period.ts new file mode 100644 index 00000000000..bfaf004912c --- /dev/null +++ b/apps/sim/lib/billing/core/reporting-period.ts @@ -0,0 +1,106 @@ +import { isRecordLike } from '@sim/utils/object' +import { isEnterprise } from '@/lib/billing/plan-helpers' + +export const ENTERPRISE_REPORTING_PERIOD_ANCHOR_METADATA_KEY = 'reportingPeriodAnchorDate' + +export type BillingInterval = 'month' | 'year' +export type UsagePeriodSource = 'reporting' | 'stripe' | 'default' + +export interface ResolvedUsagePeriod { + start: Date + end: Date + source: UsagePeriodSource + anchorDate: string | null + interval: BillingInterval | null +} + +interface SubscriptionPeriodInput { + plan?: string | null + billingInterval?: string | null + metadata?: unknown + periodStart?: Date | null + periodEnd?: Date | null + usagePeriod?: ResolvedUsagePeriod | null +} + +function parseDateOnly(value: unknown): { value: string; date: Date } | null { + if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return null + const date = new Date(`${value}T00:00:00.000Z`) + if (!Number.isFinite(date.getTime()) || date.toISOString().slice(0, 10) !== value) return null + return { value, date } +} + +function parseBillingInterval(value: unknown): BillingInterval | null { + return value === 'month' || value === 'year' ? value : null +} + +function calendarDateAtOffset(anchor: Date, monthOffset: number): Date { + const targetMonth = anchor.getUTCMonth() + monthOffset + const targetYear = anchor.getUTCFullYear() + Math.floor(targetMonth / 12) + const normalizedMonth = ((targetMonth % 12) + 12) % 12 + const lastDay = new Date(Date.UTC(targetYear, normalizedMonth + 1, 0)).getUTCDate() + return new Date(Date.UTC(targetYear, normalizedMonth, Math.min(anchor.getUTCDate(), lastDay))) +} + +export function resolveEnterpriseReportingPeriod( + anchorDate: string, + interval: BillingInterval, + now: Date = new Date() +): ResolvedUsagePeriod | null { + const parsed = parseDateOnly(anchorDate) + if (!parsed || parsed.date.getTime() > now.getTime()) return null + + const intervalMonths = interval === 'year' ? 12 : 1 + const elapsedMonths = + (now.getUTCFullYear() - parsed.date.getUTCFullYear()) * 12 + + now.getUTCMonth() - + parsed.date.getUTCMonth() + let intervalOffset = Math.max(0, Math.floor(elapsedMonths / intervalMonths)) + let start = calendarDateAtOffset(parsed.date, intervalOffset * intervalMonths) + if (start.getTime() > now.getTime()) { + intervalOffset = Math.max(0, intervalOffset - 1) + start = calendarDateAtOffset(parsed.date, intervalOffset * intervalMonths) + } + const end = calendarDateAtOffset(parsed.date, (intervalOffset + 1) * intervalMonths) + + return { + start, + end, + source: 'reporting', + anchorDate: parsed.value, + interval, + } +} + +export function resolveSubscriptionUsagePeriod( + subscription: SubscriptionPeriodInput | null | undefined, + now: Date = new Date() +): ResolvedUsagePeriod | null { + if ( + subscription?.usagePeriod && + subscription.usagePeriod.end.getTime() > subscription.usagePeriod.start.getTime() + ) { + return subscription.usagePeriod + } + if (subscription && isEnterprise(subscription.plan)) { + const metadata = isRecordLike(subscription.metadata) ? subscription.metadata : {} + const anchor = metadata[ENTERPRISE_REPORTING_PERIOD_ANCHOR_METADATA_KEY] + const interval = parseBillingInterval(subscription.billingInterval) + if (typeof anchor === 'string' && interval) { + const reportingPeriod = resolveEnterpriseReportingPeriod(anchor, interval, now) + if (reportingPeriod) return reportingPeriod + } + } + + if (subscription?.periodStart && subscription.periodEnd) { + return { + start: subscription.periodStart, + end: subscription.periodEnd, + source: 'stripe', + anchorDate: null, + interval: parseBillingInterval(subscription.billingInterval), + } + } + + return null +} diff --git a/apps/sim/lib/billing/core/usage-log.ts b/apps/sim/lib/billing/core/usage-log.ts index 50c7a8a345a..83213b145fe 100644 --- a/apps/sim/lib/billing/core/usage-log.ts +++ b/apps/sim/lib/billing/core/usage-log.ts @@ -7,6 +7,10 @@ import { generateId } from '@sim/utils/id' import { and, desc, eq, gte, inArray, lt, lte, or, sql } from 'drizzle-orm' import { defaultBillingPeriod } from '@/lib/billing/core/billing-period' import { getHighestPrioritySubscription } from '@/lib/billing/core/plan' +import { + resolveSubscriptionUsagePeriod, + type UsagePeriodSource, +} from '@/lib/billing/core/reporting-period' import { apportionCredits } from '@/lib/billing/credits/conversion' import { isOrgScopedSubscription } from '@/lib/billing/subscriptions/utils' import type { InternalUsageLogSource } from '@/lib/billing/usage-sources' @@ -126,7 +130,13 @@ type ResolvedSubscription = Awaited { const conditions = [ eq(usageLog.billingEntityType, billingEntity.type), eq(usageLog.billingEntityId, billingEntity.id), - eq(usageLog.billingPeriodStart, billingPeriod.start), - eq(usageLog.billingPeriodEnd, billingPeriod.end), + ...(billingPeriod.source === 'reporting' + ? [gte(usageLog.createdAt, billingPeriod.start), lt(usageLog.createdAt, billingPeriod.end)] + : [ + eq(usageLog.billingPeriodStart, billingPeriod.start), + eq(usageLog.billingPeriodEnd, billingPeriod.end), + ]), ] if (source) { conditions.push( @@ -210,7 +224,7 @@ export async function getBillingPeriodUsageCost( */ export async function getBillingPeriodUsageCostWithSourceSubset( billingEntity: BillingEntity, - billingPeriod: { start: Date; end: Date }, + billingPeriod: UsageQueryPeriod, source: UsageLogSource[], executor: DbClient = db ): Promise<{ total: number; subset: number }> { @@ -224,8 +238,15 @@ export async function getBillingPeriodUsageCostWithSourceSubset( and( eq(usageLog.billingEntityType, billingEntity.type), eq(usageLog.billingEntityId, billingEntity.id), - eq(usageLog.billingPeriodStart, billingPeriod.start), - eq(usageLog.billingPeriodEnd, billingPeriod.end) + ...(billingPeriod.source === 'reporting' + ? [ + gte(usageLog.createdAt, billingPeriod.start), + lt(usageLog.createdAt, billingPeriod.end), + ] + : [ + eq(usageLog.billingPeriodStart, billingPeriod.start), + eq(usageLog.billingPeriodEnd, billingPeriod.end), + ]) ) ) @@ -237,21 +258,31 @@ export async function getBillingPeriodUsageCostWithSourceSubset( export async function getBillingPeriodUsageCostByUser( billingEntity: BillingEntity, - billingPeriod: { start: Date; end: Date }, + billingPeriod: UsageQueryPeriod, source?: UsageLogSource | UsageLogSource[], - executor: DbClient = db + executor: DbClient = db, + userIds?: readonly string[] ): Promise> { + if (userIds?.length === 0) return new Map() + if (userIds && userIds.length > 1_000) { + throw new Error('Billing usage user filter cannot exceed 1,000 users') + } const conditions = [ eq(usageLog.billingEntityType, billingEntity.type), eq(usageLog.billingEntityId, billingEntity.id), - eq(usageLog.billingPeriodStart, billingPeriod.start), - eq(usageLog.billingPeriodEnd, billingPeriod.end), + ...(billingPeriod.source === 'reporting' + ? [gte(usageLog.createdAt, billingPeriod.start), lt(usageLog.createdAt, billingPeriod.end)] + : [ + eq(usageLog.billingPeriodStart, billingPeriod.start), + eq(usageLog.billingPeriodEnd, billingPeriod.end), + ]), ] if (source) { conditions.push( Array.isArray(source) ? inArray(usageLog.source, source) : eq(usageLog.source, source) ) } + if (userIds) conditions.push(inArray(usageLog.userId, [...userIds])) const rows = await executor .select({ diff --git a/apps/sim/lib/billing/core/usage.ts b/apps/sim/lib/billing/core/usage.ts index b68d1982623..14a3b65ec01 100644 --- a/apps/sim/lib/billing/core/usage.ts +++ b/apps/sim/lib/billing/core/usage.ts @@ -15,6 +15,10 @@ import { import { getEffectiveBillingStatus } from '@/lib/billing/core/access' import { defaultBillingPeriod } from '@/lib/billing/core/billing-period' import { getHighestPrioritySubscription } from '@/lib/billing/core/plan' +import { + type ResolvedUsagePeriod, + resolveSubscriptionUsagePeriod, +} from '@/lib/billing/core/reporting-period' import { getBillingPeriodUsageCost } from '@/lib/billing/core/usage-log' import { computeDailyRefreshConsumed, @@ -53,6 +57,9 @@ export interface UsageLimitSubscription { seats: number | null periodStart: Date | null periodEnd: Date | null + billingInterval?: string | null + metadata?: unknown + usagePeriod?: ResolvedUsagePeriod | null } /** @@ -220,12 +227,16 @@ export async function getUserUsageData( const stats = userStatsData[0] const orgScoped = isOrgScopedSubscription(subscription, userId) - const billingPeriod = - subscription?.periodStart && subscription.periodEnd - ? { start: subscription.periodStart, end: subscription.periodEnd } - : defaultBillingPeriod() + const billingPeriod = resolveSubscriptionUsagePeriod(subscription) ?? { + ...defaultBillingPeriod(), + source: 'default' as const, + anchorDate: null, + interval: null, + } - let currentUsageDecimal = toDecimal(stats.currentPeriodCost) + let currentUsageDecimal = toDecimal( + billingPeriod.source === 'reporting' ? 0 : stats.currentPeriodCost + ) if (!orgScoped) { currentUsageDecimal = currentUsageDecimal.plus( await getBillingPeriodUsageCost( @@ -277,15 +288,16 @@ export async function getUserUsageData( undefined, executor ) - currentUsage = pooled.currentPeriodCost + ledgerUsage + currentUsage = + (billingPeriod.source === 'reporting' ? 0 : pooled.currentPeriodCost) + ledgerUsage } else { limit = stats.currentUsageLimit ? toNumber(toDecimal(stats.currentUsageLimit)) : getFreeTierLimit() } - const billingPeriodStart = subscription?.periodStart ?? null - const billingPeriodEnd = subscription?.periodEnd ?? null + const billingPeriodStart = billingPeriod.source === 'default' ? null : billingPeriod.start + const billingPeriodEnd = billingPeriod.source === 'default' ? null : billingPeriod.end let dailyRefreshConsumed = 0 if (subscription && isPaid(subscription.plan) && billingPeriodStart) { @@ -713,12 +725,14 @@ export async function getEffectiveCurrentPeriodCost( const pooled = await getPooledOrgCurrentPeriodCost(subscription.referenceId, executor) if (pooled.memberIds.length === 0) return 0 refreshUserIds = pooled.memberIds - const billingPeriod = - subscription.periodStart && subscription.periodEnd - ? { start: subscription.periodStart, end: subscription.periodEnd } - : defaultBillingPeriod() + const billingPeriod = resolveSubscriptionUsagePeriod(subscription) ?? { + ...defaultBillingPeriod(), + source: 'default' as const, + anchorDate: null, + interval: null, + } rawCost = - pooled.currentPeriodCost + + (billingPeriod.source === 'reporting' ? 0 : pooled.currentPeriodCost) + (await getBillingPeriodUsageCost( { type: 'organization', id: subscription.referenceId }, billingPeriod, @@ -733,12 +747,14 @@ export async function getEffectiveCurrentPeriodCost( .limit(1) if (rows.length === 0) return 0 - const billingPeriod = - subscription?.periodStart && subscription.periodEnd - ? { start: subscription.periodStart, end: subscription.periodEnd } - : defaultBillingPeriod() + const billingPeriod = resolveSubscriptionUsagePeriod(subscription) ?? { + ...defaultBillingPeriod(), + source: 'default' as const, + anchorDate: null, + interval: null, + } rawCost = - toNumber(toDecimal(rows[0].current)) + + (billingPeriod.source === 'reporting' ? 0 : toNumber(toDecimal(rows[0].current))) + (await getBillingPeriodUsageCost( { type: 'user', id: userId }, billingPeriod, diff --git a/apps/sim/lib/billing/enterprise-credit-limits.test.ts b/apps/sim/lib/billing/enterprise-credit-limits.test.ts index 0fd263b3e18..5676f1ba713 100644 --- a/apps/sim/lib/billing/enterprise-credit-limits.test.ts +++ b/apps/sim/lib/billing/enterprise-credit-limits.test.ts @@ -12,7 +12,7 @@ describe('deriveEnterpriseCreditLimits', () => { invoiceAmountCents: '99900', usageLimitCredits: '20000', }, - monthlyPriceUsd: 999, + invoiceAmountUsd: 999, prepaidBalanceDollars: 10, }) ).toEqual({ @@ -29,17 +29,17 @@ describe('deriveEnterpriseCreditLimits', () => { metadata: { usageLimitCredits: '8000', }, - monthlyPriceUsd: 999, + invoiceAmountUsd: 999, prepaidBalanceDollars: 10, }).effectiveUsageLimitCredits ).toBe(10000) }) - it('defaults the usage limit to the monthly price when metadata is absent', () => { + it('defaults the usage limit to the invoice amount when metadata is absent', () => { expect( deriveEnterpriseCreditLimits({ metadata: {}, - monthlyPriceUsd: 50, + invoiceAmountUsd: 50, prepaidBalanceDollars: 0, }) ).toEqual({ @@ -56,7 +56,7 @@ describe('deriveEnterpriseCreditLimits', () => { metadata: { usageLimitCredits: '20000', }, - monthlyPriceUsd: 100, + invoiceAmountUsd: 100, prepaidBalanceDollars: '0.001', }) ).toMatchObject({ diff --git a/apps/sim/lib/billing/enterprise-credit-limits.ts b/apps/sim/lib/billing/enterprise-credit-limits.ts index ef7ab5e615a..3f18dd715f9 100644 --- a/apps/sim/lib/billing/enterprise-credit-limits.ts +++ b/apps/sim/lib/billing/enterprise-credit-limits.ts @@ -3,19 +3,19 @@ import { toDecimal } from '@/lib/billing/utils/decimal' interface DeriveEnterpriseCreditLimitsInput { metadata: Record - monthlyPriceUsd: number + invoiceAmountUsd: number prepaidBalanceDollars: string | number } export function deriveEnterpriseCreditLimits({ metadata, - monthlyPriceUsd, + invoiceAmountUsd, prepaidBalanceDollars, }: DeriveEnterpriseCreditLimitsInput) { const parsedUsageLimitCredits = Number(metadata.usageLimitCredits) const configuredUsageLimitCredits = Number.isFinite(parsedUsageLimitCredits) ? Math.max(0, Math.round(parsedUsageLimitCredits)) - : dollarsToCredits(monthlyPriceUsd) + : dollarsToCredits(invoiceAmountUsd) const prepaidBalance = toDecimal(prepaidBalanceDollars) const prepaidCredits = dollarsToCredits(prepaidBalance.toNumber()) const effectiveUsageLimitDollars = toDecimal(configuredUsageLimitCredits) diff --git a/apps/sim/lib/billing/enterprise-outbox.test.ts b/apps/sim/lib/billing/enterprise-outbox.test.ts index 5f63af9fde1..2bc57064cfd 100644 --- a/apps/sim/lib/billing/enterprise-outbox.test.ts +++ b/apps/sim/lib/billing/enterprise-outbox.test.ts @@ -26,6 +26,7 @@ import { assertNoUnresolvedEnterpriseIssuance, deriveEnterpriseOperationStatus, EnterpriseIssuanceInProgressError, + enterpriseMetadataIntentMatchesStripeSubscription, enterpriseOperationMatchesStripeSubscription, enterpriseProvisionPayloadSchema, isEnterpriseOperationUnresolved, @@ -41,6 +42,7 @@ const payload = { requestedByEmail: 'admin@sim.ai', requestedByUserId: 'admin-1', invoiceAmountCents: 10000, + billingInterval: 'month' as const, usageLimitCredits: 20000, seats: 5, concurrencyLimit: 1250, @@ -188,6 +190,49 @@ describe('Enterprise issuance Stripe-term correlation', () => { }) }) +describe('Enterprise configuration Stripe-term correlation', () => { + const configurationPayload = { + subscriptionId: 'sub-local', + revision: 2, + deliveryRevision: 1, + metadata: { seats: 7, reportingPeriodAnchorDate: '2026-08-01', monthlyPrice: null }, + terms: { invoiceAmountCents: 12000, billingInterval: 'year' as const }, + stripeProgress: {}, + } + + function configuredSubscription(metadata: Record = {}): Stripe.Subscription { + return stripeSubscription({ + amount: 12000, + interval: 'year', + metadata: { + seats: '7', + reportingPeriodAnchorDate: '2026-08-01', + simConfigOperationId: 'config-2', + simConfigRevision: '2', + simConfigDeliveryRevision: '1', + ...metadata, + }, + }) + } + + it('requires the exact desired metadata, delivery marker, and billing terms', () => { + expect( + enterpriseMetadataIntentMatchesStripeSubscription( + configurationPayload, + 'config-2', + configuredSubscription() + ) + ).toBe(true) + expect( + enterpriseMetadataIntentMatchesStripeSubscription( + configurationPayload, + 'config-2', + configuredSubscription({ seats: '8' }) + ) + ).toBe(false) + }) +}) + function executorReturning(rows: unknown[]) { const chain = { from: () => chain, @@ -222,6 +267,8 @@ describe('Enterprise metadata intent admission state', () => { id: 'config-2', status: 'pending', requestedMetadata: { seats: 7 }, + requestedTerms: null, + providerAccepted: false, error: null, }) }) @@ -249,6 +296,41 @@ describe('Enterprise metadata intent admission state', () => { id: 'config-2', status: 'failed', requestedMetadata: { seats: 7 }, + requestedTerms: null, + providerAccepted: false, + error: null, + }) + }) + + it('keeps a Stripe-accepted dead letter fail-closed until reconciliation', async () => { + const state = await resolveEnterpriseMetadataIntent( + executorReturning([ + { + id: 'config-2', + status: 'dead_letter', + payload: { + subscriptionId: 'sub-local', + revision: 2, + metadata: { seats: 7 }, + acknowledgement: { + startedAt: '2026-08-01T00:00:00.000Z', + deadlineAt: '2026-08-01T00:30:00.000Z', + }, + }, + }, + ]), + 'sub-local', + { seats: '10', simConfigRevision: '1', simConfigOperationId: 'config-1' } + ) + + expect(state.hasUnappliedIntent).toBe(true) + expect(state.effectiveSeatCapacity).toBe(7) + expect(state.configurationUpdate).toEqual({ + id: 'config-2', + status: 'failed', + requestedMetadata: { seats: 7 }, + requestedTerms: null, + providerAccepted: true, error: null, }) }) diff --git a/apps/sim/lib/billing/enterprise-outbox.ts b/apps/sim/lib/billing/enterprise-outbox.ts index 283f4330a38..8458d7703f2 100644 --- a/apps/sim/lib/billing/enterprise-outbox.ts +++ b/apps/sim/lib/billing/enterprise-outbox.ts @@ -8,6 +8,7 @@ import type { DbOrTx } from '@/lib/db/types' export const ENTERPRISE_PROVISION_EVENT_TYPE = 'stripe.provision-enterprise' export const ENTERPRISE_METADATA_SYNC_EVENT_TYPE = 'stripe.sync-enterprise-metadata' +export const ENTERPRISE_WORKSPACE_MOVE_EVENT_TYPE = 'enterprise.move-workspace' const nonnegativeInteger = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER) @@ -18,7 +19,14 @@ export const enterpriseProvisionRequestSchema = z.object({ requestedByEmail: z.string().min(1), requestedByUserId: z.string().nullable(), invoiceAmountCents: z.number().int().positive(), + billingInterval: z.enum(['month', 'year']).default('month'), + reportingPeriodAnchorDate: z + .string() + .regex(/^\d{4}-\d{2}-\d{2}$/) + .optional(), + workspaceIds: z.array(z.string().min(1)).max(1_000).default([]), usageLimitCredits: nonnegativeInteger, + prepaidBalanceCreditsAtIssuance: nonnegativeInteger.default(0), seats: z.number().int().positive(), concurrencyLimit: z.number().int().positive().max(MAX_BILLING_CONCURRENCY_LIMIT).optional(), workflowExecutionTimeoutSeconds: z @@ -31,7 +39,7 @@ export const enterpriseProvisionRequestSchema = z.object({ }) export const enterpriseProvisionPayloadSchema = z.object({ - version: z.literal(1), + version: z.union([z.literal(1), z.literal(2)]), request: enterpriseProvisionRequestSchema, retryRevision: nonnegativeInteger, stripeProgress: z @@ -57,11 +65,79 @@ export const enterpriseMetadataSyncPayloadSchema = z.object({ subscriptionId: z.string().min(1), revision: z.number().int().positive(), deliveryRevision: nonnegativeInteger.default(0), + acknowledgement: z + .object({ + startedAt: z.string().datetime(), + deadlineAt: z.string().datetime(), + }) + .optional(), metadata: z.record(z.string(), z.unknown()), + terms: z + .object({ + invoiceAmountCents: z.number().int().positive(), + billingInterval: z.enum(['month', 'year']), + }) + .optional(), + stripeProgress: z.object({ priceId: z.string().min(1).optional() }).default({}), }) export type EnterpriseMetadataSyncPayload = z.infer +function stripeMetadataValueMatches( + metadata: Stripe.Metadata, + key: string, + expected: unknown +): boolean { + if (expected === null) return metadata[key] === undefined || metadata[key] === '' + if (expected === undefined) return true + return metadata[key] === String(expected) +} + +/** Exact guard before a configuration marker can acknowledge an admin intent. */ +export function enterpriseMetadataIntentMatchesStripeSubscription( + payload: EnterpriseMetadataSyncPayload, + operationId: string, + stripeSubscription: Stripe.Subscription +): boolean { + const metadata = stripeSubscription.metadata ?? {} + if ( + metadata.simConfigOperationId !== operationId || + metadata.simConfigRevision !== String(payload.revision) || + metadata.simConfigDeliveryRevision !== String(payload.deliveryRevision) || + !Object.entries(payload.metadata).every(([key, value]) => + stripeMetadataValueMatches(metadata, key, value) + ) + ) { + return false + } + + if (!payload.terms) return true + const items = stripeSubscription.items?.data ?? [] + const price = items[0]?.price + return ( + !stripeSubscription.schedule && + stripeSubscription.collection_method === 'send_invoice' && + stripeSubscription.days_until_due === 30 && + items.length === 1 && + (items[0]?.quantity ?? 1) === 1 && + price?.currency === 'usd' && + price.unit_amount === payload.terms.invoiceAmountCents && + price.recurring?.interval === payload.terms.billingInterval && + (price.recurring.interval_count ?? 1) === 1 + ) +} + +export const enterpriseWorkspaceMovePayloadSchema = z.object({ + provisioningOperationId: z.string().min(1), + workspaceId: z.string().min(1), + destinationOrganizationId: z.string().min(1), + expectedOwnerId: z.string().min(1), + adminEmail: z.string().email(), + sequence: z.number().int().min(0), +}) + +export type EnterpriseWorkspaceMovePayload = z.infer + export type EnterpriseOperationStatus = | 'pending' | 'processing' @@ -129,10 +205,12 @@ export function enterpriseOperationMatchesStripeSubscription( stripeSubscription.days_until_due === 30 && price?.currency === 'usd' && price.unit_amount === request.invoiceAmountCents && - price.recurring?.interval === 'month' && + price.recurring?.interval === request.billingInterval && (price.recurring.interval_count ?? 1) === 1 && stripeMetadataInteger(metadata, 'invoiceAmountCents') === request.invoiceAmountCents && stripeMetadataInteger(metadata, 'usageLimitCredits') === request.usageLimitCredits && + (request.reportingPeriodAnchorDate === undefined || + metadata.reportingPeriodAnchorDate === request.reportingPeriodAnchorDate) && stripeMetadataInteger(metadata, 'seats') === request.seats && (request.concurrencyLimit === undefined || stripeMetadataInteger(metadata, 'concurrencyLimit') === request.concurrencyLimit) && @@ -215,21 +293,25 @@ function positiveInteger(value: unknown): number | null { export interface EnterpriseMetadataIntentState { latestRevision: number desiredMetadata: Record + desiredTerms: EnterpriseMetadataSyncPayload['terms'] | null hasUnappliedIntent: boolean effectiveSeatCapacity: number | null configurationUpdate: { id: string status: 'pending' | 'processing' | 'failed' requestedMetadata: Record + requestedTerms: EnterpriseMetadataSyncPayload['terms'] | null + providerAccepted: boolean error: string | null } | null } /** * Resolve the latest admin-authored Enterprise configuration entirely from the - * generic outbox. A dead-lettered intent is not effective until explicitly - * retried. An increase cannot grant seats before Stripe's webhook applies it; - * a decrease constrains admission immediately, so both directions are safe. + * generic outbox. A dead letter before the provider accepts the mutation is + * ineffective. Once the durable acknowledgement window starts, Stripe already + * contains the desired state, so a later dead letter remains effective and + * fail-closed until reconciliation or an explicit retry completes. */ export async function resolveEnterpriseMetadataIntent( executor: DbOrTx, @@ -264,6 +346,7 @@ export async function resolveEnterpriseMetadataIntent( return { latestRevision: appliedRevision, desiredMetadata: appliedMetadata, + desiredTerms: null, hasUnappliedIntent: false, effectiveSeatCapacity: appliedSeats, configurationUpdate: null, @@ -277,7 +360,9 @@ export async function resolveEnterpriseMetadataIntent( const appliedOperationId = appliedMetadata.simConfigOperationId const operationApplied = appliedOperationId === latest.id - const hasUnappliedIntent = latest.status !== 'dead_letter' && !operationApplied + const providerAccepted = parsed.data.acknowledgement !== undefined + const hasUnappliedIntent = + !operationApplied && (latest.status !== 'dead_letter' || providerAccepted) const desiredMetadata = hasUnappliedIntent ? parsed.data.metadata : appliedMetadata const desiredSeats = positiveInteger(parsed.data.metadata.seats) const effectiveSeatCapacity = hasUnappliedIntent @@ -291,6 +376,7 @@ export async function resolveEnterpriseMetadataIntent( return { latestRevision: Math.max(appliedRevision, parsed.data.revision), desiredMetadata, + desiredTerms: hasUnappliedIntent ? (parsed.data.terms ?? null) : null, hasUnappliedIntent, effectiveSeatCapacity, configurationUpdate: operationApplied @@ -304,6 +390,8 @@ export async function resolveEnterpriseMetadataIntent( ? 'processing' : 'pending', requestedMetadata: parsed.data.metadata, + requestedTerms: parsed.data.terms ?? null, + providerAccepted, error: latest.status === 'dead_letter' ? (latest.lastError ?? null) : null, }, } diff --git a/apps/sim/lib/billing/enterprise-provisioning.test.ts b/apps/sim/lib/billing/enterprise-provisioning.test.ts index a091247a3e9..cc1e64c0418 100644 --- a/apps/sim/lib/billing/enterprise-provisioning.test.ts +++ b/apps/sim/lib/billing/enterprise-provisioning.test.ts @@ -7,6 +7,7 @@ import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ subscriptionsCreate: vi.fn(), subscriptionsList: vi.fn(), + subscriptionsRetrieve: vi.fn(), subscriptionsUpdate: vi.fn(), invoicesRetrieve: vi.fn(), invoicesUpdate: vi.fn(), @@ -15,6 +16,7 @@ const mocks = vi.hoisted(() => ({ productsCreate: vi.fn(), productsRetrieve: vi.fn(), pricesList: vi.fn(), + pricesCreate: vi.fn(), pricesRetrieve: vi.fn(), enqueue: vi.fn(), patchPayload: vi.fn(), @@ -37,10 +39,11 @@ vi.mock('@/lib/billing/stripe-client', () => ({ requireStripeClient: () => ({ customers: { create: mocks.customersCreate, list: mocks.customersList }, products: { create: mocks.productsCreate, retrieve: mocks.productsRetrieve }, - prices: { list: mocks.pricesList, retrieve: mocks.pricesRetrieve }, + prices: { list: mocks.pricesList, create: mocks.pricesCreate, retrieve: mocks.pricesRetrieve }, subscriptions: { create: mocks.subscriptionsCreate, list: mocks.subscriptionsList, + retrieve: mocks.subscriptionsRetrieve, update: mocks.subscriptionsUpdate, }, invoices: { retrieve: mocks.invoicesRetrieve, update: mocks.invoicesUpdate }, @@ -52,6 +55,12 @@ vi.mock('@/lib/billing/webhooks/enterprise-reconciliation-lease', () => ({ ), })) vi.mock('@/lib/core/outbox/service', () => ({ + deferOutboxHandler: (reason: string, minimumBackoffMs?: number, consumeAttempt = true) => ({ + outcome: 'deferred', + reason, + ...(minimumBackoffMs === undefined ? {} : { minimumBackoffMs }), + ...(consumeAttempt ? {} : { consumeAttempt: false }), + }), enqueueOutboxEvent: mocks.enqueue, patchOutboxEventPayload: mocks.patchPayload, })) @@ -60,6 +69,8 @@ import { buildEnterpriseProvisioningRequestKey, decideEnterpriseProvisioningIssue, decideEnterpriseProvisioningRetry, + getEnterpriseIssuancePreflight, + getLatestEnterpriseProvisionings, provisionEnterpriseInStripe, syncEnterpriseMetadataInStripe, } from '@/lib/billing/enterprise-provisioning' @@ -68,6 +79,132 @@ afterAll(() => { resetDbChainMock() }) +describe('Enterprise issuance preflight', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + }) + + it('returns a bounded workspace page with an authoritative matching total', async () => { + queueTableRows(schemaMock.user, [{ id: 'owner-1', name: 'Owner', email: 'owner@example.com' }]) + queueTableRows(schemaMock.member, []) + queueTableRows(schemaMock.workspace, [{ value: 3 }]) + queueTableRows(schemaMock.workspace, [ + { id: 'workspace-1', name: 'One', archivedAt: null }, + { id: 'workspace-2', name: 'Two', archivedAt: new Date('2026-01-01T00:00:00.000Z') }, + ]) + queueTableRows(schemaMock.workspace, [ + { id: 'workspace-1', name: 'One', archivedAt: null, total: 3 }, + { + id: 'workspace-2', + name: 'Two', + archivedAt: new Date('2026-01-01T00:00:00.000Z'), + total: 3, + }, + { id: 'workspace-3', name: 'Three', archivedAt: null, total: 3 }, + ]) + + await expect( + getEnterpriseIssuancePreflight({ + ownerUserId: 'owner-1', + search: '', + limit: 2, + offset: 0, + }) + ).resolves.toMatchObject({ + personalWorkspaces: [ + { id: 'workspace-1', name: 'One', archived: false }, + { id: 'workspace-2', name: 'Two', archived: true }, + ], + workspacePagination: { total: 3, limit: 2, offset: 0, hasMore: true }, + workspaceSelection: { + totalEligible: 3, + defaultSelectedIds: ['workspace-1', 'workspace-2', 'workspace-3'], + defaultSelectedWorkspaces: [ + { id: 'workspace-1', name: 'One', archived: false }, + { id: 'workspace-2', name: 'Two', archived: true }, + { id: 'workspace-3', name: 'Three', archived: false }, + ], + includesAllEligible: true, + limit: 1_000, + }, + }) + }) + + it('does not silently choose an arbitrary subset when eligibility exceeds the issuance cap', async () => { + queueTableRows(schemaMock.user, [{ id: 'owner-1', name: 'Owner', email: 'owner@example.com' }]) + queueTableRows(schemaMock.member, []) + queueTableRows(schemaMock.workspace, [{ value: 1_001 }]) + queueTableRows(schemaMock.workspace, [{ id: 'workspace-1', name: 'One', archivedAt: null }]) + queueTableRows( + schemaMock.workspace, + Array.from({ length: 1_001 }, (_, index) => ({ + id: `workspace-${index + 1}`, + name: `Workspace ${index + 1}`, + archivedAt: null, + total: 1_001, + })) + ) + + const result = await getEnterpriseIssuancePreflight({ + ownerUserId: 'owner-1', + search: '', + limit: 1, + offset: 0, + }) + + expect(result.workspaceSelection).toEqual({ + totalEligible: 1_001, + defaultSelectedIds: [], + defaultSelectedWorkspaces: [], + includesAllEligible: false, + limit: 1_000, + }) + }) + + it('previews backdated ledger usage, prepaid balance, and the effective default limit', async () => { + queueTableRows(schemaMock.user, [{ id: 'owner-1', name: 'Owner', email: 'owner@example.com' }]) + queueTableRows(schemaMock.member, [ + { + role: 'owner', + organizationId: 'org-1', + organizationName: 'Acme', + organizationCreditBalance: '25', + }, + ]) + queueTableRows(schemaMock.workspace, [{ value: 0 }]) + queueTableRows(schemaMock.workspace, []) + queueTableRows(schemaMock.workspace, []) + queueTableRows(schemaMock.subscription, []) + queueTableRows(schemaMock.usageLog, [{ cost: '150' }]) + + const result = await getEnterpriseIssuancePreflight({ + ownerUserId: 'owner-1', + search: '', + limit: 1, + offset: 0, + invoiceAmountUsd: 1_200, + billingInterval: 'year', + reportingPeriodAnchorDate: '2026-08-01', + }) + + expect(result.billingPreview).toMatchObject({ + reportingPeriod: { + anchorDate: '2026-08-01', + interval: 'year', + currentStart: '2026-08-01T00:00:00.000Z', + currentEnd: '2027-08-01T00:00:00.000Z', + source: 'reporting', + }, + usage: { usedDollars: 150, limitDollars: 1_225 }, + configuredUsageLimitDollars: 1_200, + prepaidBalanceDollars: 25, + effectiveUsageLimitDollars: 1_225, + exceedsLimit: false, + }) + }) +}) + function operationPayload(overrides: Record = {}) { return { version: 1 as const, @@ -102,37 +239,55 @@ describe('Enterprise issuance serialization decisions', () => { it('includes the configured or invoice-defaulted usage limit in the request key', () => { const input = { ownerUserId: 'owner-1', - monthlyInvoiceAmountUsd: 125, + invoiceAmountUsd: 125, + reportingPeriodAnchorDate: '2026-08-01', usageLimitCredits: 24000, seats: 12, requestedByEmail: 'admin@sim.ai', requestedByUserId: 'admin-1', } + const normalizedTerms = { + billingInterval: 'year' as const, + reportingPeriodAnchorDate: '2026-08-01', + } - expect(buildEnterpriseProvisioningRequestKey(input, 'org-1')).toBe( - 'enterprise-v4:owner-1:org-1:12500:24000:12:concurrency=default:workflow-timeout=default:collection=active' + expect(buildEnterpriseProvisioningRequestKey(input, 'org-1', normalizedTerms)).toBe( + 'enterprise-v5:owner-1:org-1:12500:year:2026-08-01::24000:12:concurrency=default:workflow-timeout=default:collection=active' ) expect( - buildEnterpriseProvisioningRequestKey({ ...input, concurrencyLimit: 1250 }, 'org-1') + buildEnterpriseProvisioningRequestKey( + { ...input, concurrencyLimit: 1250 }, + 'org-1', + normalizedTerms + ) ).toBe( - 'enterprise-v4:owner-1:org-1:12500:24000:12:concurrency=1250:workflow-timeout=default:collection=active' + 'enterprise-v5:owner-1:org-1:12500:year:2026-08-01::24000:12:concurrency=1250:workflow-timeout=default:collection=active' ) expect( - buildEnterpriseProvisioningRequestKey({ ...input, pausePaymentCollection: true }, 'org-1') + buildEnterpriseProvisioningRequestKey( + { ...input, pausePaymentCollection: true }, + 'org-1', + normalizedTerms + ) ).toBe( - 'enterprise-v4:owner-1:org-1:12500:24000:12:concurrency=default:workflow-timeout=default:collection=paused' + 'enterprise-v5:owner-1:org-1:12500:year:2026-08-01::24000:12:concurrency=default:workflow-timeout=default:collection=paused' ) expect( - buildEnterpriseProvisioningRequestKey({ ...input, usageLimitCredits: undefined }, 'org-1') + buildEnterpriseProvisioningRequestKey( + { ...input, usageLimitCredits: undefined }, + 'org-1', + normalizedTerms + ) ).toBe( - 'enterprise-v4:owner-1:org-1:12500:25000:12:concurrency=default:workflow-timeout=default:collection=active' + 'enterprise-v5:owner-1:org-1:12500:year:2026-08-01::25000:12:concurrency=default:workflow-timeout=default:collection=active' ) }) it('keeps concurrency and workflow timeout in distinct request-key slots', () => { const input = { ownerUserId: 'owner-1', - monthlyInvoiceAmountUsd: 125, + invoiceAmountUsd: 125, + reportingPeriodAnchorDate: '2026-08-01', usageLimitCredits: 24000, seats: 12, requestedByEmail: 'admin@sim.ai', @@ -141,11 +296,13 @@ describe('Enterprise issuance serialization decisions', () => { const concurrencyKey = buildEnterpriseProvisioningRequestKey( { ...input, concurrencyLimit: 100 }, - 'org-1' + 'org-1', + { billingInterval: 'year', reportingPeriodAnchorDate: '2026-08-01' } ) const workflowTimeoutKey = buildEnterpriseProvisioningRequestKey( { ...input, workflowExecutionTimeoutSeconds: 100 }, - 'org-1' + 'org-1', + { billingInterval: 'year', reportingPeriodAnchorDate: '2026-08-01' } ) expect(concurrencyKey).not.toBe(workflowTimeoutKey) @@ -242,10 +399,59 @@ function arrangeWorkerReads( queueTableRows(schemaMock.member, [{ value: finalMemberCount }]) } +describe('Enterprise workspace-move progress', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + }) + + it('keeps the total failed count visible when list views omit bounded failure details', async () => { + const payload = operationPayload({ + request: { + ...operationPayload().request, + workspaceIds: ['workspace-1', 'workspace-2', 'workspace-3'], + }, + }) + const now = new Date('2026-08-13T00:00:00.000Z') + queueTableRows(schemaMock.outboxEvent, [ + { + id: 'operation-1', + eventType: 'stripe.provision-enterprise', + status: 'pending', + payload, + attempts: 0, + maxAttempts: 5, + availableAt: now, + lockedAt: null, + processedAt: null, + lastError: null, + createdAt: now, + }, + ]) + queueTableRows(schemaMock.outboxEvent, [{ operationId: 'operation-1', moved: 1, failed: 1 }]) + + const provisionings = await getLatestEnterpriseProvisionings(['org-1']) + + expect(provisionings.get('org-1')?.workspaceMoves).toEqual({ + selected: 3, + moved: 1, + pending: 1, + failedCount: 1, + failed: [], + }) + }) +}) + describe('Enterprise issuance outbox handler', () => { beforeEach(() => { vi.clearAllMocks() resetDbChainMock() + mocks.subscriptionsRetrieve.mockResolvedValue({ + id: 'sub_1', + metadata: {}, + items: { data: [] }, + schedule: null, + }) mocks.subscriptionsList.mockResolvedValue({ data: [], has_more: false }) mocks.customersList.mockResolvedValue({ data: [], has_more: false }) mocks.pricesList.mockResolvedValue({ data: [], has_more: false }) @@ -307,6 +513,48 @@ describe('Enterprise issuance outbox handler', () => { }) }) + it('creates an annual Price when the issuance cadence is yearly', async () => { + arrangeWorkerReads() + mocks.pricesRetrieve.mockResolvedValue({ + id: 'price_1', + currency: 'usd', + unit_amount: 120000, + recurring: { interval: 'year' }, + product: 'prod_1', + metadata: { enterpriseOperationId: 'operation-1' }, + }) + const annual = operationPayload({ + request: { + ...operationPayload().request, + requestKey: 'enterprise-v5:annual', + invoiceAmountCents: 120000, + billingInterval: 'year', + reportingPeriodAnchorDate: '2026-08-01', + }, + }) + + await provisionEnterpriseInStripe(annual, context()) + + expect(mocks.productsCreate).toHaveBeenCalledWith( + expect.objectContaining({ + default_price_data: expect.objectContaining({ + unit_amount: 120000, + recurring: { interval: 'year' }, + }), + }), + expect.any(Object) + ) + expect(mocks.subscriptionsCreate).toHaveBeenCalledWith( + expect.objectContaining({ + metadata: expect.objectContaining({ + invoiceAmountCents: '120000', + reportingPeriodAnchorDate: '2026-08-01', + }), + }), + expect.any(Object) + ) + }) + it('recovers an existing subscription and nudges a genuine webhook with retry revision', async () => { arrangeWorkerReads() mocks.subscriptionsList.mockResolvedValue({ @@ -472,15 +720,28 @@ describe('Enterprise metadata outbox handler', () => { queueTableRows(schemaMock.outboxEvent, [{ id: 'metadata-event-1', payload }]) queueTableRows(schemaMock.member, [{ value: 10 }]) mocks.subscriptionsUpdate.mockResolvedValue({ id: 'sub_1' }) + const checkpointPayload = vi.fn() await expect( syncEnterpriseMetadataInStripe(payload, { eventId: 'metadata-event-1', eventType: 'stripe.sync-enterprise-metadata', attempts: 0, - checkpointPayload: vi.fn(), + checkpointPayload, }) - ).rejects.toThrow('Awaiting verified Stripe webhook application') + ).resolves.toEqual({ + outcome: 'deferred', + reason: 'Waiting for the verified Stripe webhook acknowledgement', + minimumBackoffMs: 30_000, + consumeAttempt: false, + }) + + expect(checkpointPayload).toHaveBeenCalledWith({ + acknowledgement: { + startedAt: expect.any(String), + deadlineAt: expect.any(String), + }, + }) expect(mocks.subscriptionsUpdate).toHaveBeenCalledWith( 'sub_1', @@ -493,6 +754,7 @@ describe('Enterprise metadata outbox handler', () => { simConfigDeliveryRevision: '0', simConfigDeliveryAttempt: '0', }), + expand: ['latest_invoice'], }, { idempotencyKey: 'enterprise-config:local-sub-1:metadata-event-1:delivery:0:attempt:0', @@ -527,18 +789,348 @@ describe('Enterprise metadata outbox handler', () => { attempts: 0, checkpointPayload: vi.fn(), }) - ).rejects.toThrow('Awaiting verified Stripe webhook application') + ).resolves.toEqual({ + outcome: 'deferred', + reason: 'Waiting for the verified Stripe webhook acknowledgement', + minimumBackoffMs: 30_000, + consumeAttempt: false, + }) expect(mocks.subscriptionsUpdate).toHaveBeenCalledWith( 'sub_1', - { + expect.objectContaining({ metadata: expect.objectContaining({ concurrencyLimit: '', simConfigOperationId: 'metadata-event-2', }), + }), + expect.any(Object) + ) + }) + + it('does not consume attempts while a written Stripe delivery is inside its webhook grace period', async () => { + const payload = { + subscriptionId: 'local-sub-1', + revision: 6, + deliveryRevision: 2, + acknowledgement: { + startedAt: '2026-08-13T00:00:00.000Z', + deadlineAt: '2099-08-13T00:30:00.000Z', }, + metadata: { plan: 'enterprise', referenceId: 'org-1', seats: 15 }, + } + queueTableRows(schemaMock.subscription, [ + { stripeSubscriptionId: 'sub_1', referenceId: 'org-1', metadata: {} }, + ]) + queueTableRows(schemaMock.subscription, [{ metadata: {} }]) + queueTableRows(schemaMock.outboxEvent, [{ id: 'metadata-event-grace', payload }]) + queueTableRows(schemaMock.member, [{ value: 10 }]) + mocks.subscriptionsRetrieve.mockResolvedValue({ + id: 'sub_1', + metadata: { + plan: 'enterprise', + referenceId: 'org-1', + seats: '15', + simConfigOperationId: 'metadata-event-grace', + simConfigRevision: '6', + simConfigDeliveryRevision: '2', + }, + }) + + await expect( + syncEnterpriseMetadataInStripe(payload, { + eventId: 'metadata-event-grace', + eventType: 'stripe.sync-enterprise-metadata', + attempts: 4, + checkpointPayload: vi.fn(), + }) + ).resolves.toMatchObject({ + outcome: 'deferred', + consumeAttempt: false, + reason: 'Waiting for the verified Stripe webhook acknowledgement', + }) + + expect(mocks.subscriptionsUpdate).not.toHaveBeenCalled() + }) + + it('consumes the finite missing-ack budget only after the durable grace deadline', async () => { + const payload = { + subscriptionId: 'local-sub-1', + revision: 6, + deliveryRevision: 2, + acknowledgement: { + startedAt: '2000-08-13T00:00:00.000Z', + deadlineAt: '2000-08-13T00:30:00.000Z', + }, + metadata: { plan: 'enterprise', referenceId: 'org-1', seats: 15 }, + } + queueTableRows(schemaMock.subscription, [ + { stripeSubscriptionId: 'sub_1', referenceId: 'org-1', metadata: {} }, + ]) + queueTableRows(schemaMock.subscription, [{ metadata: {} }]) + queueTableRows(schemaMock.outboxEvent, [{ id: 'metadata-event-missing', payload }]) + queueTableRows(schemaMock.member, [{ value: 10 }]) + mocks.subscriptionsRetrieve.mockResolvedValue({ + id: 'sub_1', + metadata: { + simConfigOperationId: 'metadata-event-missing', + simConfigDeliveryRevision: '2', + }, + }) + + await expect( + syncEnterpriseMetadataInStripe(payload, { + eventId: 'metadata-event-missing', + eventType: 'stripe.sync-enterprise-metadata', + attempts: 4, + checkpointPayload: vi.fn(), + }) + ).resolves.toEqual({ + outcome: 'deferred', + reason: + 'Verified Stripe webhook acknowledgement was not received before the acknowledgement deadline', + }) + }) + + it('replaces the single Enterprise Price in place without proration', async () => { + const payload = { + subscriptionId: 'local-sub-1', + revision: 6, + deliveryRevision: 0, + metadata: { + plan: 'enterprise', + referenceId: 'org-1', + seats: 15, + invoiceAmountCents: 120000, + reportingPeriodAnchorDate: '2026-01-31', + }, + terms: { invoiceAmountCents: 120000, billingInterval: 'year' as const }, + stripeProgress: {}, + } + queueTableRows(schemaMock.subscription, [ + { stripeSubscriptionId: 'sub_1', referenceId: 'org-1', metadata: {} }, + ]) + queueTableRows(schemaMock.subscription, [{ metadata: {} }]) + queueTableRows(schemaMock.outboxEvent, [{ id: 'metadata-event-3', payload }]) + queueTableRows(schemaMock.member, [{ value: 10 }]) + mocks.subscriptionsRetrieve.mockResolvedValue({ + id: 'sub_1', + metadata: {}, + schedule: null, + collection_method: 'send_invoice', + days_until_due: 30, + items: { + data: [{ id: 'si_1', price: { product: 'prod_1' } }], + }, + }) + mocks.pricesList.mockResolvedValue({ data: [], has_more: false }) + mocks.pricesCreate.mockResolvedValue({ + id: 'price_year', + currency: 'usd', + unit_amount: 120000, + recurring: { interval: 'year', interval_count: 1 }, + product: 'prod_1', + metadata: { enterpriseConfigOperationId: 'metadata-event-3' }, + }) + mocks.subscriptionsUpdate.mockResolvedValue({ + id: 'sub_1', + metadata: {}, + items: { data: [] }, + pause_collection: null, + }) + const checkpointPayload = vi.fn() + + await expect( + syncEnterpriseMetadataInStripe(payload, { + eventId: 'metadata-event-3', + eventType: 'stripe.sync-enterprise-metadata', + attempts: 0, + checkpointPayload, + }) + ).resolves.toMatchObject({ outcome: 'deferred' }) + + expect(mocks.subscriptionsUpdate).toHaveBeenCalledWith( + 'sub_1', + expect.objectContaining({ + items: [{ id: 'si_1', price: 'price_year', quantity: 1 }], + proration_behavior: 'none', + billing_cycle_anchor: 'now', + metadata: expect.objectContaining({ + invoiceAmountCents: '120000', + reportingPeriodAnchorDate: '2026-01-31', + }), + }), expect.any(Object) ) + expect(checkpointPayload).toHaveBeenCalledWith({ + stripeProgress: { priceId: 'price_year' }, + }) + }) + + it('preserves paused collection and freezes the cadence-change invoice as a draft', async () => { + const payload = { + subscriptionId: 'local-sub-1', + revision: 7, + deliveryRevision: 0, + metadata: { + plan: 'enterprise', + referenceId: 'org-1', + seats: 15, + invoiceAmountCents: 120000, + }, + terms: { invoiceAmountCents: 120000, billingInterval: 'year' as const }, + stripeProgress: {}, + } + queueTableRows(schemaMock.subscription, [ + { stripeSubscriptionId: 'sub_1', referenceId: 'org-1', metadata: {} }, + ]) + queueTableRows(schemaMock.subscription, [{ metadata: {} }]) + queueTableRows(schemaMock.outboxEvent, [{ id: 'metadata-event-paused', payload }]) + queueTableRows(schemaMock.member, [{ value: 10 }]) + mocks.subscriptionsRetrieve.mockResolvedValue({ + id: 'sub_1', + metadata: {}, + schedule: null, + collection_method: 'send_invoice', + days_until_due: 30, + pause_collection: { behavior: 'keep_as_draft', resumes_at: null }, + items: { + data: [ + { + id: 'si_1', + price: { product: 'prod_1', recurring: { interval: 'month', interval_count: 1 } }, + }, + ], + }, + }) + mocks.pricesList.mockResolvedValue({ data: [], has_more: false }) + mocks.pricesCreate.mockResolvedValue({ + id: 'price_year', + currency: 'usd', + unit_amount: 120000, + recurring: { interval: 'year', interval_count: 1 }, + product: 'prod_1', + metadata: { enterpriseConfigOperationId: 'metadata-event-paused' }, + }) + mocks.subscriptionsUpdate.mockResolvedValue({ + id: 'sub_1', + pause_collection: { behavior: 'keep_as_draft', resumes_at: null }, + latest_invoice: { id: 'in_change', status: 'draft', auto_advance: true }, + }) + + await syncEnterpriseMetadataInStripe(payload, { + eventId: 'metadata-event-paused', + eventType: 'stripe.sync-enterprise-metadata', + attempts: 0, + checkpointPayload: vi.fn(), + }) + + expect(mocks.invoicesUpdate).toHaveBeenCalledWith( + 'in_change', + { auto_advance: false }, + { idempotencyKey: 'enterprise:metadata-event-paused:initial-invoice-draft' } + ) + expect(mocks.subscriptionsUpdate.mock.calls[0][1]).not.toHaveProperty('pause_collection') + }) + + it('does not treat an old paid invoice as a failed paused amount-only update', async () => { + const payload = { + subscriptionId: 'local-sub-1', + revision: 8, + deliveryRevision: 0, + metadata: { + plan: 'enterprise', + referenceId: 'org-1', + seats: 15, + invoiceAmountCents: 150000, + }, + terms: { invoiceAmountCents: 150000, billingInterval: 'year' as const }, + stripeProgress: {}, + } + queueTableRows(schemaMock.subscription, [ + { stripeSubscriptionId: 'sub_1', referenceId: 'org-1', metadata: {} }, + ]) + queueTableRows(schemaMock.subscription, [{ metadata: {} }]) + queueTableRows(schemaMock.outboxEvent, [{ id: 'metadata-event-paused-amount', payload }]) + queueTableRows(schemaMock.member, [{ value: 10 }]) + mocks.subscriptionsRetrieve.mockResolvedValue({ + id: 'sub_1', + metadata: {}, + schedule: null, + collection_method: 'send_invoice', + days_until_due: 30, + pause_collection: { behavior: 'keep_as_draft', resumes_at: null }, + latest_invoice: { id: 'in_old', status: 'paid', auto_advance: false }, + items: { + data: [ + { + id: 'si_1', + price: { product: 'prod_1', recurring: { interval: 'year', interval_count: 1 } }, + }, + ], + }, + }) + mocks.pricesList.mockResolvedValue({ data: [], has_more: false }) + mocks.pricesCreate.mockResolvedValue({ + id: 'price_year_new_amount', + currency: 'usd', + unit_amount: 150000, + recurring: { interval: 'year', interval_count: 1 }, + product: 'prod_1', + metadata: { enterpriseConfigOperationId: 'metadata-event-paused-amount' }, + }) + mocks.subscriptionsUpdate.mockResolvedValue({ + id: 'sub_1', + pause_collection: { behavior: 'keep_as_draft', resumes_at: null }, + latest_invoice: { id: 'in_old', status: 'paid', auto_advance: false }, + }) + + await expect( + syncEnterpriseMetadataInStripe(payload, { + eventId: 'metadata-event-paused-amount', + eventType: 'stripe.sync-enterprise-metadata', + attempts: 0, + checkpointPayload: vi.fn(), + }) + ).resolves.toMatchObject({ outcome: 'deferred' }) + + expect(mocks.invoicesUpdate).not.toHaveBeenCalled() + expect(mocks.subscriptionsUpdate.mock.calls[0][1]).not.toHaveProperty('billing_cycle_anchor') + }) + + it('rejects billing-term changes controlled by a Stripe Schedule', async () => { + const payload = { + subscriptionId: 'local-sub-1', + revision: 6, + deliveryRevision: 0, + metadata: { plan: 'enterprise', referenceId: 'org-1', seats: 15 }, + terms: { invoiceAmountCents: 120000, billingInterval: 'year' as const }, + stripeProgress: {}, + } + queueTableRows(schemaMock.subscription, [ + { stripeSubscriptionId: 'sub_1', referenceId: 'org-1', metadata: {} }, + ]) + queueTableRows(schemaMock.subscription, [{ metadata: {} }]) + queueTableRows(schemaMock.outboxEvent, [{ id: 'metadata-event-4', payload }]) + queueTableRows(schemaMock.member, [{ value: 10 }]) + mocks.subscriptionsRetrieve.mockResolvedValue({ + id: 'sub_1', + metadata: {}, + schedule: 'sub_sched_1', + collection_method: 'send_invoice', + days_until_due: 30, + items: { data: [{ id: 'si_1', price: { product: 'prod_1' } }] }, + }) + + await expect( + syncEnterpriseMetadataInStripe(payload, { + eventId: 'metadata-event-4', + eventType: 'stripe.sync-enterprise-metadata', + attempts: 0, + checkpointPayload: vi.fn(), + }) + ).rejects.toThrow('Stripe Schedule') + expect(mocks.subscriptionsUpdate).not.toHaveBeenCalled() }) it('suppresses an older metadata event after acquiring the subscription lease', async () => { diff --git a/apps/sim/lib/billing/enterprise-provisioning.ts b/apps/sim/lib/billing/enterprise-provisioning.ts index 613b206070a..cd5346ea5f1 100644 --- a/apps/sim/lib/billing/enterprise-provisioning.ts +++ b/apps/sim/lib/billing/enterprise-provisioning.ts @@ -1,21 +1,26 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' -import { member, organization, outboxEvent, subscription, user } from '@sim/db/schema' +import { member, organization, outboxEvent, subscription, user, workspace } from '@sim/db/schema' import { generateId } from '@sim/utils/id' -import { and, count, desc, eq, inArray, isNull, sql } from 'drizzle-orm' +import { and, count, desc, eq, ilike, inArray, isNull, notInArray, or, sql } from 'drizzle-orm' import type Stripe from 'stripe' import { parseBillingConcurrencyLimit } from '@/lib/billing/concurrency-defaults' import { getBillingConcurrencyLimit } from '@/lib/billing/concurrency-limits' +import { resolveEnterpriseReportingPeriod } from '@/lib/billing/core/reporting-period' +import { getBillingPeriodUsageCost } from '@/lib/billing/core/usage-log' import { dollarsToCredits } from '@/lib/billing/credits/conversion' import { deriveEnterpriseOperationStatus, ENTERPRISE_METADATA_SYNC_EVENT_TYPE, ENTERPRISE_PROVISION_EVENT_TYPE, + ENTERPRISE_WORKSPACE_MOVE_EVENT_TYPE, type EnterpriseOperationStatus, type EnterpriseProvisionPayload, type EnterpriseProvisionRequest, + enterpriseMetadataIntentMatchesStripeSubscription, enterpriseMetadataSyncPayloadSchema, enterpriseProvisionPayloadSchema, + enterpriseWorkspaceMovePayloadSchema, parseEnterpriseProvisionPayload, } from '@/lib/billing/enterprise-outbox' import { @@ -28,9 +33,49 @@ import { requireStripeClient } from '@/lib/billing/stripe-client' import { TERMINAL_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils' import { withEnterpriseReconciliationLease } from '@/lib/billing/webhooks/enterprise-reconciliation-lease' import { env } from '@/lib/core/config/env' -import { enqueueOutboxEvent, type OutboxHandler } from '@/lib/core/outbox/service' +import { + deferOutboxHandler, + enqueueOutboxEvent, + type OutboxEventContext, + type OutboxHandler, +} from '@/lib/core/outbox/service' +import { moveWorkspaceToOrganization } from '@/lib/workspaces/admin-move' +import { ownedAttachableWorkspacesWhere } from '@/lib/workspaces/organization-workspaces' const TERMINAL_STATUSES = new Set(TERMINAL_SUBSCRIPTION_STATUSES) +const ENTERPRISE_WEBHOOK_ACKNOWLEDGEMENT_GRACE_MS = 30 * 60 * 1000 +const ENTERPRISE_WEBHOOK_ACKNOWLEDGEMENT_POLL_MS = 30 * 1000 +const MAX_ENTERPRISE_WORKSPACE_SELECTION = 1_000 + +async function waitForEnterpriseWebhookAcknowledgement( + acknowledgement: { startedAt: string; deadlineAt: string } | undefined, + context: OutboxEventContext +) { + let durableAcknowledgement = acknowledgement + if (!durableAcknowledgement) { + const startedAt = new Date() + durableAcknowledgement = { + startedAt: startedAt.toISOString(), + deadlineAt: new Date( + startedAt.getTime() + ENTERPRISE_WEBHOOK_ACKNOWLEDGEMENT_GRACE_MS + ).toISOString(), + } + await context.checkpointPayload({ acknowledgement: durableAcknowledgement }) + } + + const remainingGraceMs = new Date(durableAcknowledgement.deadlineAt).getTime() - Date.now() + if (remainingGraceMs > 0) { + return deferOutboxHandler( + 'Waiting for the verified Stripe webhook acknowledgement', + Math.min(ENTERPRISE_WEBHOOK_ACKNOWLEDGEMENT_POLL_MS, remainingGraceMs), + false + ) + } + + return deferOutboxHandler( + 'Verified Stripe webhook acknowledgement was not received before the acknowledgement deadline' + ) +} function metadataRecord(value: unknown): Record { return value && typeof value === 'object' && !Array.isArray(value) @@ -192,6 +237,33 @@ async function findOperationPrice( return match } +async function findConfigurationPrice( + stripe: Stripe, + productId: string, + operationId: string +): Promise { + let match: Stripe.Price | null = null + let startingAfter: string | undefined + for (;;) { + const page = await stripe.prices.list({ + product: productId, + limit: 100, + ...(startingAfter ? { starting_after: startingAfter } : {}), + }) + for (const candidate of page.data) { + if (candidate.metadata?.enterpriseConfigOperationId !== operationId) continue + if (match && match.id !== candidate.id) { + throw new Error('Multiple Stripe prices exist for this Enterprise configuration update') + } + match = candidate + } + if (!page.has_more) break + startingAfter = page.data.at(-1)?.id + if (!startingAfter) break + } + return match +} + function isStripeMissingResource(error: unknown): boolean { return Boolean( error && @@ -228,7 +300,7 @@ function assertEnterprisePrice( if ( price.currency !== 'usd' || price.unit_amount !== request.invoiceAmountCents || - price.recurring?.interval !== 'month' || + price.recurring?.interval !== request.billingInterval || (price.recurring.interval_count ?? 1) !== 1 || price.metadata?.enterpriseOperationId !== operationId || productId !== expectedProductId @@ -237,10 +309,32 @@ function assertEnterprisePrice( } } +function assertEnterpriseConfigurationPrice( + price: Stripe.Price, + terms: { invoiceAmountCents: number; billingInterval: 'month' | 'year' }, + operationId: string, + expectedProductId: string +): void { + const productId = typeof price.product === 'string' ? price.product : price.product?.id + if ( + price.currency !== 'usd' || + price.unit_amount !== terms.invoiceAmountCents || + price.recurring?.interval !== terms.billingInterval || + (price.recurring.interval_count ?? 1) !== 1 || + price.metadata?.enterpriseConfigOperationId !== operationId || + productId !== expectedProductId + ) { + throw new Error('Recovered Stripe price does not match the Enterprise billing-term update') + } +} + export interface IssueEnterpriseProvisioningInput { ownerUserId: string organizationName?: string - monthlyInvoiceAmountUsd: number + invoiceAmountUsd: number + billingInterval?: 'month' | 'year' + reportingPeriodAnchorDate?: string + workspaceIds?: string[] usageLimitCredits?: number seats: number concurrencyLimit?: number @@ -255,7 +349,9 @@ export interface EnterpriseProvisioningView { ownerUserId: string organizationId: string status: EnterpriseOperationStatus - monthlyInvoiceAmountUsd: number + invoiceAmountUsd: number + billingInterval: 'month' | 'year' + reportingPeriodAnchorDate: string | null usageLimitCredits: number seats: number concurrencyLimit: number @@ -265,6 +361,15 @@ export interface EnterpriseProvisioningView { error: string | null createdAt: string updatedAt: string + workspaceMoves: EnterpriseWorkspaceMoveProgress +} + +export interface EnterpriseWorkspaceMoveProgress { + selected: number + moved: number + pending: number + failedCount: number + failed: Array<{ eventId: string; workspaceId: string; error: string | null }> } export class EnterpriseProvisioningError extends Error { @@ -274,6 +379,215 @@ export class EnterpriseProvisioningError extends Error { } } +export interface EnterpriseIssuancePreflight { + owner: { id: string; name: string; email: string } + organization: { id: string; name: string; role: string } | null + personalWorkspaces: Array<{ id: string; name: string; archived: boolean }> + workspacePagination: { total: number; limit: number; offset: number; hasMore: boolean } + workspaceSelection: { + totalEligible: number + defaultSelectedIds: string[] + defaultSelectedWorkspaces: Array<{ id: string; name: string; archived: boolean }> + includesAllEligible: boolean + limit: number + } + billingPreview: { + reportingPeriod: { + anchorDate: string + interval: 'month' | 'year' + currentStart: string + currentEnd: string + source: 'reporting' + } + usage: { usedDollars: number; limitDollars: number } + invoiceAmountUsd: number + configuredUsageLimitDollars: number + prepaidBalanceDollars: number + effectiveUsageLimitDollars: number + exceedsLimit: boolean + } | null + canIssue: boolean + reason: string | null +} + +export async function getEnterpriseIssuancePreflight({ + ownerUserId, + search, + limit, + offset, + invoiceAmountUsd, + billingInterval, + reportingPeriodAnchorDate, + usageLimitDollars, +}: { + ownerUserId: string + search: string + limit: number + offset: number + invoiceAmountUsd?: number + billingInterval?: 'month' | 'year' + reportingPeriodAnchorDate?: string + usageLimitDollars?: number +}): Promise { + const [owner] = await db + .select({ id: user.id, name: user.name, email: user.email }) + .from(user) + .where(eq(user.id, ownerUserId)) + .limit(1) + if (!owner) throw new EnterpriseProvisioningError('Owner user not found') + + const [membership] = await db + .select({ + role: member.role, + organizationId: organization.id, + organizationName: organization.name, + organizationCreditBalance: organization.creditBalance, + }) + .from(member) + .innerJoin(organization, eq(organization.id, member.organizationId)) + .where(eq(member.userId, ownerUserId)) + .limit(1) + const trimmedSearch = search.trim() + const allPersonalWorkspacesWhere = ownedAttachableWorkspacesWhere({ + userId: ownerUserId, + includeArchived: true, + }) + const personalWorkspaceWhere = and( + allPersonalWorkspacesWhere, + trimmedSearch + ? or(ilike(workspace.name, `%${trimmedSearch}%`), eq(workspace.id, trimmedSearch)) + : undefined + ) + const [personalWorkspaceCount, personalWorkspaces, selectionRows] = await Promise.all([ + db.select({ value: count() }).from(workspace).where(personalWorkspaceWhere), + db + .select({ id: workspace.id, name: workspace.name, archivedAt: workspace.archivedAt }) + .from(workspace) + .where(personalWorkspaceWhere) + .orderBy(workspace.name, workspace.id) + .limit(limit) + .offset(offset), + db + .select({ + id: workspace.id, + name: workspace.name, + archivedAt: workspace.archivedAt, + total: sql`count(*) over()`.mapWith(Number), + }) + .from(workspace) + .where(allPersonalWorkspacesWhere) + .orderBy(workspace.id) + .limit(MAX_ENTERPRISE_WORKSPACE_SELECTION + 1), + ]) + + let reason: string | null = null + if (membership?.role && membership.role !== 'owner') { + reason = 'The selected user is already a non-owner member of an organization' + } else if (membership?.organizationId) { + const [nonterminalSubscription] = await db + .select({ id: subscription.id }) + .from(subscription) + .where( + and( + eq(subscription.referenceId, membership.organizationId), + or( + isNull(subscription.status), + notInArray(subscription.status, [...TERMINAL_SUBSCRIPTION_STATUSES]) + ) + ) + ) + .limit(1) + if (nonterminalSubscription) { + reason = 'The selected owner organization already has a nonterminal subscription' + } + } + + const totalPersonalWorkspaces = personalWorkspaceCount[0]?.value ?? 0 + const totalEligibleWorkspaces = selectionRows[0]?.total ?? 0 + const includesAllEligible = totalEligibleWorkspaces <= MAX_ENTERPRISE_WORKSPACE_SELECTION + const previewTermsComplete = + invoiceAmountUsd !== undefined && + billingInterval !== undefined && + reportingPeriodAnchorDate !== undefined + const prepaidBalanceDollars = Number(membership?.organizationCreditBalance ?? 0) + if ( + reason === null && + usageLimitDollars !== undefined && + usageLimitDollars < prepaidBalanceDollars + ) { + reason = 'Enterprise usage limit cannot be below the organization prepaid balance' + } + let billingPreview: EnterpriseIssuancePreflight['billingPreview'] = null + if (previewTermsComplete) { + const reportingPeriod = resolveEnterpriseReportingPeriod( + reportingPeriodAnchorDate, + billingInterval + ) + const configuredUsageLimitDollars = + usageLimitDollars === undefined + ? invoiceAmountUsd + : Math.max(0, usageLimitDollars - prepaidBalanceDollars) + const effectiveUsageLimitDollars = configuredUsageLimitDollars + prepaidBalanceDollars + const usedDollars = membership?.organizationId + ? await getBillingPeriodUsageCost( + { type: 'organization', id: membership.organizationId }, + reportingPeriod + ) + : 0 + billingPreview = { + reportingPeriod: { + anchorDate: reportingPeriod.anchorDate, + interval: reportingPeriod.interval, + currentStart: reportingPeriod.start.toISOString(), + currentEnd: reportingPeriod.end.toISOString(), + source: reportingPeriod.source, + }, + usage: { usedDollars, limitDollars: effectiveUsageLimitDollars }, + invoiceAmountUsd, + configuredUsageLimitDollars, + prepaidBalanceDollars, + effectiveUsageLimitDollars, + exceedsLimit: usedDollars > effectiveUsageLimitDollars, + } + } + + return { + owner, + organization: membership + ? { + id: membership.organizationId, + name: membership.organizationName, + role: membership.role, + } + : null, + personalWorkspaces: personalWorkspaces.map(({ archivedAt, ...row }) => ({ + ...row, + archived: archivedAt !== null, + })), + workspacePagination: { + total: totalPersonalWorkspaces, + limit, + offset, + hasMore: offset + personalWorkspaces.length < totalPersonalWorkspaces, + }, + workspaceSelection: { + totalEligible: totalEligibleWorkspaces, + defaultSelectedIds: includesAllEligible ? selectionRows.map((row) => row.id) : [], + defaultSelectedWorkspaces: includesAllEligible + ? selectionRows.map(({ total: _total, archivedAt, ...row }) => ({ + ...row, + archived: archivedAt !== null, + })) + : [], + includesAllEligible, + limit: MAX_ENTERPRISE_WORKSPACE_SELECTION, + }, + billingPreview, + canIssue: reason === null, + reason, + } +} + function slugifyOrganizationName(name: string, organizationId: string): string { const base = name .toLowerCase() @@ -286,15 +600,21 @@ function slugifyOrganizationName(name: string, organizationId: string): string { /** Builds a deterministic key from every Enterprise commercial term. */ export function buildEnterpriseProvisioningRequestKey( input: IssueEnterpriseProvisioningInput, - organizationId: string + organizationId: string, + normalizedTerms: { + billingInterval: 'month' | 'year' + reportingPeriodAnchorDate: string + } ): string { - const usageLimitCredits = - input.usageLimitCredits ?? dollarsToCredits(input.monthlyInvoiceAmountUsd) + const usageLimitCredits = input.usageLimitCredits ?? dollarsToCredits(input.invoiceAmountUsd) const requestTerms: Array = [ - 'enterprise-v4', + 'enterprise-v5', input.ownerUserId, organizationId, - Math.round(input.monthlyInvoiceAmountUsd * 100), + Math.round(input.invoiceAmountUsd * 100), + normalizedTerms.billingInterval, + normalizedTerms.reportingPeriodAnchorDate, + [...(input.workspaceIds ?? [])].sort().join(','), usageLimitCredits, input.seats, `concurrency=${input.concurrencyLimit ?? 'default'}`, @@ -306,7 +626,8 @@ export function buildEnterpriseProvisioningRequestKey( function toEnterpriseProvisioningView( row: typeof outboxEvent.$inferSelect, - payload: EnterpriseProvisionPayload + payload: EnterpriseProvisionPayload, + workspaceMoves: EnterpriseWorkspaceMoveProgress ): EnterpriseProvisioningView { const request = payload.request const updatedAt = row.processedAt ?? row.lockedAt ?? row.availableAt ?? row.createdAt @@ -315,8 +636,10 @@ function toEnterpriseProvisioningView( ownerUserId: request.ownerUserId, organizationId: request.organizationId, status: deriveEnterpriseOperationStatus(row.status, payload), - monthlyInvoiceAmountUsd: request.invoiceAmountCents / 100, - usageLimitCredits: request.usageLimitCredits, + invoiceAmountUsd: request.invoiceAmountCents / 100, + billingInterval: request.billingInterval, + reportingPeriodAnchorDate: request.reportingPeriodAnchorDate ?? null, + usageLimitCredits: request.usageLimitCredits + request.prepaidBalanceCreditsAtIssuance, seats: request.seats, concurrencyLimit: getBillingConcurrencyLimit('enterprise', request.concurrencyLimit), workflowExecutionTimeoutSeconds: @@ -330,9 +653,92 @@ function toEnterpriseProvisioningView( error: row.lastError, createdAt: row.createdAt.toISOString(), updatedAt: updatedAt.toISOString(), + workspaceMoves, } } +async function getEnterpriseWorkspaceMoveProgress( + operations: Array<{ id: string; payload: EnterpriseProvisionPayload }>, + options: { includeFailures?: boolean } = {} +): Promise> { + const operationIds = operations.map((operation) => operation.id) + const operationIdExpression = sql`${outboxEvent.payload} ->> 'provisioningOperationId'` + const progressRows = + operationIds.length === 0 + ? [] + : await db + .select({ + operationId: operationIdExpression, + moved: sql`count(*) filter (where ${outboxEvent.status} = 'completed')`.mapWith( + Number + ), + failed: + sql`count(*) filter (where ${outboxEvent.status} = 'dead_letter')`.mapWith( + Number + ), + }) + .from(outboxEvent) + .where( + and( + eq(outboxEvent.eventType, ENTERPRISE_WORKSPACE_MOVE_EVENT_TYPE), + inArray(operationIdExpression, operationIds) + ) + ) + .groupBy(operationIdExpression) + + const failedRows = + options.includeFailures && operationIds.length > 0 + ? await db + .select({ + id: outboxEvent.id, + payload: outboxEvent.payload, + lastError: outboxEvent.lastError, + }) + .from(outboxEvent) + .where( + and( + eq(outboxEvent.eventType, ENTERPRISE_WORKSPACE_MOVE_EVENT_TYPE), + eq(outboxEvent.status, 'dead_letter'), + inArray(operationIdExpression, operationIds) + ) + ) + .orderBy(outboxEvent.createdAt, outboxEvent.id) + .limit(operationIds.length * MAX_ENTERPRISE_WORKSPACE_SELECTION) + : [] + + const result = new Map() + for (const operation of operations) { + result.set(operation.id, { + selected: operation.payload.request.workspaceIds.length, + moved: 0, + pending: operation.payload.request.workspaceIds.length, + failedCount: 0, + failed: [], + }) + } + for (const row of progressRows) { + const progress = result.get(row.operationId) + if (!progress) continue + progress.moved = row.moved + progress.failedCount = row.failed + } + for (const row of failedRows) { + const parsed = enterpriseWorkspaceMovePayloadSchema.safeParse(row.payload) + if (!parsed.success) continue + const progress = result.get(parsed.data.provisioningOperationId) + if (!progress) continue + progress.failed.push({ + eventId: row.id, + workspaceId: parsed.data.workspaceId, + error: row.lastError, + }) + } + for (const progress of result.values()) { + progress.pending = Math.max(0, progress.selected - progress.moved - progress.failedCount) + } + return result +} + async function getEnterpriseProvisioningById( operationId: string ): Promise { @@ -348,7 +754,21 @@ async function getEnterpriseProvisioningById( .limit(1) if (!row) return null const payload = parseEnterpriseProvisionPayload(row.payload) - return payload ? toEnterpriseProvisioningView(row, payload) : null + if (!payload) return null + const progress = await getEnterpriseWorkspaceMoveProgress([{ id: row.id, payload }], { + includeFailures: true, + }) + return toEnterpriseProvisioningView( + row, + payload, + progress.get(row.id) ?? { + selected: payload.request.workspaceIds.length, + moved: 0, + pending: payload.request.workspaceIds.length, + failedCount: 0, + failed: [], + } + ) } type EnterpriseSubscriptionState = Pick< @@ -425,17 +845,33 @@ export function decideEnterpriseProvisioningRetry( export async function issueEnterpriseProvisioning( input: IssueEnterpriseProvisioningInput ): Promise { - const invoiceAmountCents = Math.round(input.monthlyInvoiceAmountUsd * 100) + const invoiceAmountCents = Math.round(input.invoiceAmountUsd * 100) if ( invoiceAmountCents <= 0 || !Number.isSafeInteger(invoiceAmountCents) || - Math.abs(input.monthlyInvoiceAmountUsd * 100 - invoiceAmountCents) > 1e-8 + Math.abs(input.invoiceAmountUsd * 100 - invoiceAmountCents) > 1e-8 ) { throw new EnterpriseProvisioningError( - 'Monthly invoice amount must be at least $0.01 and use whole cents' + 'Invoice amount must be at least $0.01 and use whole cents' ) } - const defaultUsageLimitCredits = dollarsToCredits(input.monthlyInvoiceAmountUsd) + const defaultUsageLimitCredits = dollarsToCredits(input.invoiceAmountUsd) + const billingInterval = input.billingInterval ?? 'year' + const reportingPeriodAnchorDate = + input.reportingPeriodAnchorDate ?? new Date().toISOString().slice(0, 10) + const parsedReportingAnchor = new Date(`${reportingPeriodAnchorDate}T00:00:00.000Z`) + if ( + !/^\d{4}-\d{2}-\d{2}$/.test(reportingPeriodAnchorDate) || + !Number.isFinite(parsedReportingAnchor.getTime()) || + parsedReportingAnchor.toISOString().slice(0, 10) !== reportingPeriodAnchorDate || + parsedReportingAnchor.getTime() > Date.now() + ) { + throw new EnterpriseProvisioningError('Reporting-period start must be today or a past date') + } + const workspaceIds = [...new Set(input.workspaceIds ?? [])].sort() + if (workspaceIds.length > MAX_ENTERPRISE_WORKSPACE_SELECTION) { + throw new EnterpriseProvisioningError('At most 1,000 workspaces can be selected') + } if ( input.concurrencyLimit !== undefined && parseBillingConcurrencyLimit(input.concurrencyLimit) !== input.concurrencyLimit @@ -449,29 +885,26 @@ export async function issueEnterpriseProvisioning( ) { throw new EnterpriseProvisioningError('Workflow execution timeout is invalid') } - const result = await db.transaction(async (tx) => { - const [owner] = await tx - .select({ id: user.id, name: user.name, email: user.email }) - .from(user) - .where(eq(user.id, input.ownerUserId)) - .for('update') - .limit(1) - if (!owner) throw new EnterpriseProvisioningError('Owner user not found') - const [membership] = await tx - .select({ role: member.role, organizationId: member.organizationId }) - .from(member) - .where(eq(member.userId, input.ownerUserId)) - .limit(1) + // Discover the lock scope without holding a transaction connection or row + // lock. The transaction below re-reads all membership state after taking + // the canonical organization → user-billing-identity lock order. + const [membershipSnapshot] = await db + .select({ role: member.role, organizationId: member.organizationId }) + .from(member) + .where(eq(member.userId, input.ownerUserId)) + .limit(1) + const result = await db.transaction(async (tx) => { let organizationId: string - if (membership) { - if (membership.role !== 'owner') { + let organizationToCreate: { id: string; name: string } | null = null + if (membershipSnapshot) { + if (membershipSnapshot.role !== 'owner') { throw new EnterpriseProvisioningError( 'The selected user is a member, but not the owner, of an organization' ) } - organizationId = membership.organizationId + organizationId = membershipSnapshot.organizationId await acquireOrganizationMutationLock(tx, organizationId) await acquireUserBillingIdentityLock(tx, input.ownerUserId) const [currentMembership] = await tx @@ -503,11 +936,22 @@ export async function issueEnterpriseProvisioning( ) } organizationId = `org_${generateId()}` + organizationToCreate = { id: organizationId, name: input.organizationName } + } + + const [owner] = await tx + .select({ id: user.id }) + .from(user) + .where(eq(user.id, input.ownerUserId)) + .limit(1) + if (!owner) throw new EnterpriseProvisioningError('Owner user not found') + + if (organizationToCreate) { const now = new Date() await tx.insert(organization).values({ - id: organizationId, - name: input.organizationName, - slug: slugifyOrganizationName(input.organizationName, organizationId), + id: organizationToCreate.id, + name: organizationToCreate.name, + slug: slugifyOrganizationName(organizationToCreate.name, organizationToCreate.id), createdAt: now, updatedAt: now, }) @@ -520,8 +964,31 @@ export async function issueEnterpriseProvisioning( }) } - await acquireOrganizationMutationLock(tx, organizationId) - const requestKey = buildEnterpriseProvisioningRequestKey(input, organizationId) + // Existing organizations were locked before the billing-identity lock. + // Newly created rows are transaction-private, so no second advisory lock + // is necessary and avoiding one preserves the canonical lock order. + const [organizationRow] = await tx + .select({ creditBalance: organization.creditBalance }) + .from(organization) + .where(eq(organization.id, organizationId)) + .for('update') + .limit(1) + if (!organizationRow) throw new EnterpriseProvisioningError('Organization not found') + const prepaidCredits = dollarsToCredits(Number(organizationRow.creditBalance ?? 0)) + if (input.usageLimitCredits !== undefined && input.usageLimitCredits < prepaidCredits) { + throw new EnterpriseProvisioningError( + 'Enterprise usage limit cannot be below the organization prepaid balance' + ) + } + const configuredUsageLimitCredits = + input.usageLimitCredits === undefined + ? defaultUsageLimitCredits + : input.usageLimitCredits - prepaidCredits + const normalizedInput = { ...input, usageLimitCredits: configuredUsageLimitCredits } + const requestKey = buildEnterpriseProvisioningRequestKey(normalizedInput, organizationId, { + billingInterval, + reportingPeriodAnchorDate, + }) const [lockedOwner] = await tx .select({ role: member.role }) .from(member) @@ -541,6 +1008,30 @@ export async function issueEnterpriseProvisioning( ) } + if (workspaceIds.length > 0) { + const selectedWorkspaces = await tx + .select({ id: workspace.id }) + .from(workspace) + .where( + and( + ownedAttachableWorkspacesWhere({ + userId: input.ownerUserId, + includeArchived: true, + }), + inArray(workspace.id, workspaceIds) + ) + ) + .orderBy(workspace.id) + if ( + selectedWorkspaces.length !== workspaceIds.length || + selectedWorkspaces.some((row, index) => row.id !== workspaceIds[index]) + ) { + throw new EnterpriseProvisioningError( + 'One or more selected workspaces are no longer owned personal workspaces' + ) + } + } + const operationRows = await tx .select() .from(outboxEvent) @@ -569,7 +1060,11 @@ export async function issueEnterpriseProvisioning( requestedByEmail: input.requestedByEmail, requestedByUserId: input.requestedByUserId, invoiceAmountCents, - usageLimitCredits: input.usageLimitCredits ?? defaultUsageLimitCredits, + billingInterval, + reportingPeriodAnchorDate, + workspaceIds, + usageLimitCredits: configuredUsageLimitCredits, + prepaidBalanceCreditsAtIssuance: prepaidCredits, seats: input.seats, ...(input.concurrencyLimit !== undefined ? { concurrencyLimit: input.concurrencyLimit } : {}), ...(input.workflowExecutionTimeoutSeconds !== undefined @@ -578,7 +1073,7 @@ export async function issueEnterpriseProvisioning( pausePaymentCollection: input.pausePaymentCollection ?? false, } const payload: EnterpriseProvisionPayload = { - version: 1, + version: 2, request, retryRevision: 0, stripeProgress: {}, @@ -600,7 +1095,10 @@ export async function issueEnterpriseProvisioning( description: `Admin requested Enterprise issuance for organization ${view.organizationId}`, metadata: { organizationId: view.organizationId, - invoiceAmountCents: Math.round(view.monthlyInvoiceAmountUsd * 100), + invoiceAmountCents: Math.round(view.invoiceAmountUsd * 100), + billingInterval: view.billingInterval, + reportingPeriodAnchorDate: view.reportingPeriodAnchorDate, + workspaceCount: workspaceIds.length, usageLimitCredits: view.usageLimitCredits, seats: view.seats, concurrencyLimit: view.concurrencyLimit, @@ -696,6 +1194,74 @@ export async function retryEnterpriseProvisioning( return view } +export async function retryEnterpriseWorkspaceMove( + operationId: string, + moveEventId: string, + actor: { id: string | null; name: string; email: string | null } +): Promise { + const [snapshot] = await db + .select({ payload: outboxEvent.payload }) + .from(outboxEvent) + .where( + and( + eq(outboxEvent.id, moveEventId), + eq(outboxEvent.eventType, ENTERPRISE_WORKSPACE_MOVE_EVENT_TYPE) + ) + ) + .limit(1) + const parsedSnapshot = enterpriseWorkspaceMovePayloadSchema.safeParse(snapshot?.payload) + if (!parsedSnapshot.success || parsedSnapshot.data.provisioningOperationId !== operationId) { + throw new EnterpriseProvisioningError('Enterprise workspace move not found') + } + + const retried = await db.transaction(async (tx) => { + await acquireOrganizationMutationLock(tx, parsedSnapshot.data.destinationOrganizationId) + const [row] = await tx + .select({ status: outboxEvent.status, payload: outboxEvent.payload }) + .from(outboxEvent) + .where(eq(outboxEvent.id, moveEventId)) + .for('update') + .limit(1) + const payload = enterpriseWorkspaceMovePayloadSchema.safeParse(row?.payload) + if (!row || !payload.success || payload.data.provisioningOperationId !== operationId) { + throw new EnterpriseProvisioningError('Enterprise workspace move not found') + } + if (row.status !== 'dead_letter') return false + await tx + .update(outboxEvent) + .set({ + status: 'pending', + attempts: 0, + lastError: null, + availableAt: new Date(), + lockedAt: null, + processedAt: null, + }) + .where(eq(outboxEvent.id, moveEventId)) + return true + }) + + const view = await getEnterpriseProvisioningById(operationId) + if (!view) throw new EnterpriseProvisioningError('Enterprise operation not found') + if (retried) { + recordAudit({ + actorId: actor.id, + actorName: actor.name, + actorEmail: actor.email, + action: AuditAction.ORGANIZATION_UPDATED, + resourceType: AuditResourceType.WORKSPACE, + resourceId: parsedSnapshot.data.workspaceId, + description: 'Admin retried Enterprise issuance workspace move', + metadata: { + organizationId: parsedSnapshot.data.destinationOrganizationId, + provisioningOperationId: operationId, + moveEventId, + }, + }) + } + return view +} + async function resolveCanonicalCustomer(params: { stripe: Stripe operationId: string @@ -846,7 +1412,9 @@ export const provisionEnterpriseInStripe: OutboxHandler = async (rawPay organizationId: request.organizationId, enterpriseOperationId: context.eventId, invoiceAmountCents: request.invoiceAmountCents.toString(), - monthlyPrice: (request.invoiceAmountCents / 100).toFixed(2), + ...(request.reportingPeriodAnchorDate + ? { reportingPeriodAnchorDate: request.reportingPeriodAnchorDate } + : {}), usageLimitCredits: request.usageLimitCredits.toString(), seats: request.seats.toString(), ...(request.concurrencyLimit !== undefined @@ -902,7 +1470,7 @@ export const provisionEnterpriseInStripe: OutboxHandler = async (rawPay default_price_data: { currency: 'usd', unit_amount: request.invoiceAmountCents, - recurring: { interval: 'month' }, + recurring: { interval: request.billingInterval }, metadata: { enterpriseOperationId: context.eventId }, }, expand: ['default_price'], @@ -1047,7 +1615,7 @@ export const syncEnterpriseMetadataInStripe: OutboxHandler = async ( const stripeSubscriptionId = subscriptionRow.stripeSubscriptionId if (metadataRecord(subscriptionRow.metadata).simConfigOperationId === context.eventId) return - await withEnterpriseReconciliationLease(stripeSubscriptionId, async () => { + return withEnterpriseReconciliationLease(stripeSubscriptionId, async () => { const [currentSubscription] = await db .select({ metadata: subscription.metadata }) .from(subscription) @@ -1095,44 +1663,216 @@ export const syncEnterpriseMetadataInStripe: OutboxHandler = async ( metadata.simConfigDeliveryRevision = String(latestPayload.data.deliveryRevision) metadata.simConfigDeliveryAttempt = String(context.attempts) - await requireStripeClient().subscriptions.update( + const stripe = requireStripeClient() + const terms = latestPayload.data.terms + const stripeSubscription = await stripe.subscriptions.retrieve(stripeSubscriptionId, { + expand: ['latest_invoice'], + }) + const deliveryAlreadyWritten = enterpriseMetadataIntentMatchesStripeSubscription( + latestPayload.data, + context.eventId, + stripeSubscription + ) + if (deliveryAlreadyWritten) { + return waitForEnterpriseWebhookAcknowledgement(latestPayload.data.acknowledgement, context) + } + let priceId = latestPayload.data.stripeProgress.priceId ?? null + let updateItems: Stripe.SubscriptionUpdateParams.Item[] | undefined + let billingIntervalChanged = false + + if (terms) { + if (stripeSubscription.schedule) { + throw new Error( + 'Enterprise billing terms cannot be changed while a Stripe Schedule controls the subscription' + ) + } + if ( + stripeSubscription.collection_method !== 'send_invoice' || + stripeSubscription.days_until_due !== 30 + ) { + throw new Error( + 'Enterprise billing-term updates require send-invoice collection with 30-day terms' + ) + } + const items = stripeSubscription.items.data + if (items.length !== 1) { + throw new Error( + 'Enterprise billing-term updates require exactly one Stripe subscription item' + ) + } + const currentItem = items[0] + billingIntervalChanged = currentItem.price.recurring?.interval !== terms.billingInterval + const productId = + typeof currentItem.price.product === 'string' + ? currentItem.price.product + : currentItem.price.product?.id + if (!productId) throw new Error('Enterprise subscription price has no reusable product') + + let price: Stripe.Price | null = null + if (priceId) { + price = await stripe.prices.retrieve(priceId) + } else { + price = await findConfigurationPrice(stripe, productId, context.eventId) + } + if (!price) { + price = await stripe.prices.create( + { + currency: 'usd', + unit_amount: terms.invoiceAmountCents, + recurring: { interval: terms.billingInterval }, + product: productId, + metadata: { enterpriseConfigOperationId: context.eventId }, + }, + { + idempotencyKey: `enterprise-config:${payload.subscriptionId}:${context.eventId}:price`, + } + ) + } + assertEnterpriseConfigurationPrice(price, terms, context.eventId, productId) + priceId = price.id + if (latestPayload.data.stripeProgress.priceId !== priceId) { + await context.checkpointPayload({ stripeProgress: { priceId } }) + } + updateItems = [{ id: currentItem.id, price: priceId, quantity: 1 }] + } + + const updatedSubscription = await stripe.subscriptions.update( stripeSubscriptionId, - { metadata }, + { + metadata, + ...(updateItems + ? { + items: updateItems, + proration_behavior: 'none' as const, + ...(billingIntervalChanged ? { billing_cycle_anchor: 'now' as const } : {}), + } + : {}), + expand: ['latest_invoice'], + }, { idempotencyKey: `enterprise-config:${payload.subscriptionId}:${context.eventId}:delivery:${latestPayload.data.deliveryRevision}:attempt:${context.attempts}`, } ) + const priorPause = stripeSubscription.pause_collection + const updatedPause = updatedSubscription.pause_collection + const pausePreserved = + priorPause === null + ? updatedPause === null + : priorPause?.behavior === updatedPause?.behavior && + (priorPause?.resumes_at ?? null) === (updatedPause?.resumes_at ?? null) + if (!pausePreserved) { + throw new Error('Stripe did not preserve Enterprise payment-collection pause settings') + } + + // Stripe's keep_as_draft contract handles future invoices itself. Only an + // interval switch creates an immediate full-period invoice; inspect that + // invoice rather than mistaking an older paid invoice for a failed update + // during an amount-only Price replacement. + if ( + terms && + billingIntervalChanged && + updatedSubscription.pause_collection?.behavior === 'keep_as_draft' + ) { + await keepInitialEnterpriseInvoiceAsDraft({ + stripe, + subscription: updatedSubscription, + operationId: context.eventId, + }) + } + // Stripe's verified webhook is the only path that applies metadata to the - // canonical subscription row. Keep this same outbox operation retryable - // until a later attempt observes that acknowledgement. - throw new Error('Awaiting verified Stripe webhook application') + // canonical subscription row. Normal delivery latency has a durable grace + // window that does not consume handler attempts. A genuinely missing + // acknowledgement begins consuming the finite outbox budget after it. + return waitForEnterpriseWebhookAcknowledgement(latestPayload.data.acknowledgement, context) + }) +} + +export const moveEnterpriseWorkspace: OutboxHandler = async (rawPayload, context) => { + const parsed = enterpriseWorkspaceMovePayloadSchema.safeParse(rawPayload) + if (!parsed.success) throw new Error('Invalid Enterprise workspace-move outbox payload') + const payload = parsed.data + + const [earlierActive] = await db + .select({ id: outboxEvent.id }) + .from(outboxEvent) + .where( + and( + eq(outboxEvent.eventType, ENTERPRISE_WORKSPACE_MOVE_EVENT_TYPE), + inArray(outboxEvent.status, ['pending', 'processing']), + sql`${outboxEvent.payload} ->> 'provisioningOperationId' = ${payload.provisioningOperationId}`, + sql`coalesce((${outboxEvent.payload} ->> 'sequence')::integer, 0) < ${payload.sequence}`, + sql`${outboxEvent.id} <> ${context.eventId}` + ) + ) + .limit(1) + if (earlierActive) { + // This is dependency ordering, not a failed delivery. The earlier row has + // its own finite attempt budget and will become completed or dead-letter, + // so waiting here must not consume this workspace's retry budget. + return deferOutboxHandler('Waiting for an earlier Enterprise workspace move', undefined, false) + } + + await moveWorkspaceToOrganization({ + workspaceId: payload.workspaceId, + destinationOrganizationId: payload.destinationOrganizationId, + expectedOwnerId: payload.expectedOwnerId, + adminEmail: payload.adminEmail, }) } export const enterpriseIssuanceOutboxHandlers = { [ENTERPRISE_PROVISION_EVENT_TYPE]: provisionEnterpriseInStripe, [ENTERPRISE_METADATA_SYNC_EVENT_TYPE]: syncEnterpriseMetadataInStripe, + [ENTERPRISE_WORKSPACE_MOVE_EVENT_TYPE]: moveEnterpriseWorkspace, } as const -export async function getLatestEnterpriseProvisionings(organizationIds: string[]) { +export async function getLatestEnterpriseProvisionings( + organizationIds: string[], + options: { includeWorkspaceMoveFailures?: boolean } = {} +) { const result = new Map() if (organizationIds.length === 0) return result + const organizationIdExpression = sql`${outboxEvent.payload} #>> '{request,organizationId}'` const rows = await db - .select() + .selectDistinctOn([organizationIdExpression]) .from(outboxEvent) .where( and( eq(outboxEvent.eventType, ENTERPRISE_PROVISION_EVENT_TYPE), - inArray(sql`${outboxEvent.payload} #>> '{request,organizationId}'`, organizationIds) + inArray(organizationIdExpression, organizationIds) ) ) - .orderBy(desc(outboxEvent.createdAt), desc(outboxEvent.id)) + .orderBy(organizationIdExpression, desc(outboxEvent.createdAt), desc(outboxEvent.id)) + const latestRows: Array<{ + row: typeof outboxEvent.$inferSelect + payload: EnterpriseProvisionPayload + }> = [] for (const row of rows) { const payload = parseEnterpriseProvisionPayload(row.payload) if (!payload) throw new Error(`Enterprise issuance outbox payload ${row.id} is invalid`) - if (result.has(payload.request.organizationId)) continue - result.set(payload.request.organizationId, toEnterpriseProvisioningView(row, payload)) + latestRows.push({ row, payload }) + } + const progress = await getEnterpriseWorkspaceMoveProgress( + latestRows.map(({ row, payload }) => ({ id: row.id, payload })), + { includeFailures: options.includeWorkspaceMoveFailures } + ) + for (const { row, payload } of latestRows) { + result.set( + payload.request.organizationId, + toEnterpriseProvisioningView( + row, + payload, + progress.get(row.id) ?? { + selected: payload.request.workspaceIds.length, + moved: 0, + pending: payload.request.workspaceIds.length, + failedCount: 0, + failed: [], + } + ) + ) } return result } diff --git a/apps/sim/lib/billing/organizations/member-limits.test.ts b/apps/sim/lib/billing/organizations/member-limits.test.ts index 0367b73c80f..8c1c9e60782 100644 --- a/apps/sim/lib/billing/organizations/member-limits.test.ts +++ b/apps/sim/lib/billing/organizations/member-limits.test.ts @@ -140,6 +140,27 @@ describe('getOrgMemberUsageForBillingPeriod', () => { expect(mixedHistoryCall).toBeDefined() expect(mockGetOrganizationSubscription).not.toHaveBeenCalled() }) + + it('uses only immutable same-organization ledger rows for a custom reporting window', async () => { + const billingPeriod = { + start: new Date('2025-08-13T00:00:00.000Z'), + end: new Date('2026-08-13T00:00:00.000Z'), + source: 'reporting' as const, + } + queueTableRows(schemaTables.usageLog, [{ cost: '18.25' }]) + + await expect( + getOrgMemberUsageForBillingPeriod('contract-org', 'actor-2', billingPeriod) + ).resolves.toBe(18.25) + + expect(mockEq).toHaveBeenCalledWith('usageLog.userId', 'actor-2') + expect(mockEq).toHaveBeenCalledWith('usageLog.billingEntityType', 'organization') + expect(mockEq).toHaveBeenCalledWith('usageLog.billingEntityId', 'contract-org') + expect(mockGte).toHaveBeenCalledWith('usageLog.createdAt', billingPeriod.start) + expect(mockLt).toHaveBeenCalledWith('usageLog.createdAt', billingPeriod.end) + expect(mockEq).not.toHaveBeenCalledWith('workspace.organizationId', 'contract-org') + expect(mockIsNull).not.toHaveBeenCalledWith('usageLog.billingEntityType') + }) }) describe('setOrgMemberUsageLimit', () => { diff --git a/apps/sim/lib/billing/organizations/member-limits.ts b/apps/sim/lib/billing/organizations/member-limits.ts index b259412720e..16caadf6313 100644 --- a/apps/sim/lib/billing/organizations/member-limits.ts +++ b/apps/sim/lib/billing/organizations/member-limits.ts @@ -5,6 +5,8 @@ import { generateId } from '@sim/utils/id' import { and, eq, gte, isNull, lt, or, sql } from 'drizzle-orm' import { getOrganizationSubscription } from '@/lib/billing/core/billing' import { defaultBillingPeriod } from '@/lib/billing/core/billing-period' +import { resolveSubscriptionUsagePeriod } from '@/lib/billing/core/reporting-period' +import type { UsageQueryPeriod } from '@/lib/billing/core/usage-log' import { toDecimal, toNumber } from '@/lib/billing/utils/decimal' import type { DbOrTx } from '@/lib/db/types' @@ -95,7 +97,7 @@ export async function setOrgMemberUsageLimit( export async function getOrgMemberUsageForBillingPeriod( organizationId: string, userId: string, - billingPeriod: { start: Date; end: Date } + billingPeriod: UsageQueryPeriod ): Promise { const [row] = await db .select({ cost: sql`COALESCE(SUM(${usageLog.cost}), 0)` }) @@ -104,25 +106,34 @@ export async function getOrgMemberUsageForBillingPeriod( .where( and( eq(usageLog.userId, userId), - or( - and( - eq(usageLog.billingEntityType, 'organization'), - eq(usageLog.billingEntityId, organizationId), - eq(usageLog.billingPeriodStart, billingPeriod.start), - eq(usageLog.billingPeriodEnd, billingPeriod.end) - ), - and( - isNull(usageLog.billingEntityType), - isNull(usageLog.billingEntityId), - eq(workspace.organizationId, organizationId), - or( - isNull(workspace.organizationAssignedAt), - gte(usageLog.createdAt, workspace.organizationAssignedAt) - ), - gte(usageLog.createdAt, billingPeriod.start), - lt(usageLog.createdAt, billingPeriod.end) - ) - ) + ...(billingPeriod.source === 'reporting' + ? [ + eq(usageLog.billingEntityType, 'organization'), + eq(usageLog.billingEntityId, organizationId), + gte(usageLog.createdAt, billingPeriod.start), + lt(usageLog.createdAt, billingPeriod.end), + ] + : [ + or( + and( + eq(usageLog.billingEntityType, 'organization'), + eq(usageLog.billingEntityId, organizationId), + eq(usageLog.billingPeriodStart, billingPeriod.start), + eq(usageLog.billingPeriodEnd, billingPeriod.end) + ), + and( + isNull(usageLog.billingEntityType), + isNull(usageLog.billingEntityId), + eq(workspace.organizationId, organizationId), + or( + isNull(workspace.organizationAssignedAt), + gte(usageLog.createdAt, workspace.organizationAssignedAt) + ), + gte(usageLog.createdAt, billingPeriod.start), + lt(usageLog.createdAt, billingPeriod.end) + ) + ), + ]) ) ) @@ -150,10 +161,10 @@ export async function getOrgMemberUsageForCurrentPeriod( prefetchedSubscription === undefined ? await getOrganizationSubscription(organizationId) : prefetchedSubscription - const billingPeriod = - subscription?.periodStart && subscription.periodEnd - ? { start: subscription.periodStart, end: subscription.periodEnd } - : defaultBillingPeriod() + const billingPeriod = resolveSubscriptionUsagePeriod(subscription) ?? { + ...defaultBillingPeriod(), + source: 'default' as const, + } return getOrgMemberUsageForBillingPeriod(organizationId, userId, billingPeriod) } diff --git a/apps/sim/lib/billing/subscriptions/utils.ts b/apps/sim/lib/billing/subscriptions/utils.ts index 3d7fc1f3a09..9a9d450cee4 100644 --- a/apps/sim/lib/billing/subscriptions/utils.ts +++ b/apps/sim/lib/billing/subscriptions/utils.ts @@ -188,7 +188,7 @@ export function canEditUsageLimit(subscription: any): boolean { } // Only Pro and Team plans can edit limits - // Enterprise has fixed limits that match their monthly cost + // Enterprise has a fixed, administrator-controlled contract-period limit. return isPro(subscription.plan) || isTeam(subscription.plan) } diff --git a/apps/sim/lib/billing/threshold-billing.test.ts b/apps/sim/lib/billing/threshold-billing.test.ts index 9e1276b278f..c984f0c8038 100644 --- a/apps/sim/lib/billing/threshold-billing.test.ts +++ b/apps/sim/lib/billing/threshold-billing.test.ts @@ -392,6 +392,46 @@ describe('checkAndBillOverageThreshold', () => { expect(mockCalculateSubscriptionOverage).not.toHaveBeenCalled() }) + it('does not compare an Enterprise reporting window to Stripe before the ineligible no-op', async () => { + mockIsEnterprise.mockReturnValue(true) + + await expect( + checkAndBillOverageThreshold('user-1', undefined, { + onError: 'throw', + expectedBillingPeriod: { + start: new Date('2025-08-13T00:00:00.000Z'), + end: new Date('2026-08-13T00:00:00.000Z'), + }, + }) + ).resolves.toEqual({ status: 'no-op', reason: 'plan-ineligible' }) + + expect(mockCalculateSubscriptionOverage).not.toHaveBeenCalled() + }) + + it('does not compare an organization Enterprise reporting window to Stripe', async () => { + mockGetOrganizationSubscriptionUsable.mockResolvedValue({ + ...usableOrgSubscription, + plan: 'enterprise', + }) + mockIsEnterprise.mockReturnValue(true) + + await expect( + checkAndBillPayerOverageThreshold( + { type: 'organization', id: 'org-1' }, + { + onError: 'throw', + expectedBillingPeriod: { + start: new Date('2025-08-13T00:00:00.000Z'), + end: new Date('2026-08-13T00:00:00.000Z'), + }, + } + ) + ).resolves.toEqual({ status: 'no-op', reason: 'plan-ineligible' }) + + expect(mockIsOrganizationBillingBlocked).not.toHaveBeenCalled() + expect(mockComputeOrgOverageAmount).not.toHaveBeenCalled() + }) + it('wraps organization provider failures through the strict payer helper', async () => { mockGetOrganizationSubscriptionUsable.mockResolvedValue(usableOrgSubscription) mockIsOrganizationBillingBlocked.mockRejectedValue(new Error('Organization lookup unavailable')) diff --git a/apps/sim/lib/billing/threshold-billing.ts b/apps/sim/lib/billing/threshold-billing.ts index 48947a484c2..f99f821a110 100644 --- a/apps/sim/lib/billing/threshold-billing.ts +++ b/apps/sim/lib/billing/threshold-billing.ts @@ -262,6 +262,10 @@ export async function checkAndBillOverageThreshold( return noOp(options, 'no-subscription') } + if (isFree(userSubscription.plan) || isEnterprise(userSubscription.plan)) { + return noOp(options, 'plan-ineligible') + } + assertExpectedBillingPeriod( { type: 'user', id: userId }, userSubscription.periodStart, @@ -274,10 +278,6 @@ export async function checkAndBillOverageThreshold( return noOp(options, 'billing-ineligible') } - if (isFree(userSubscription.plan) || isEnterprise(userSubscription.plan)) { - return noOp(options, 'plan-ineligible') - } - // Org-scoped subs are billed at the org level regardless of plan name. if (isOrgScopedSubscription(userSubscription, userId)) { logger.debug('Org-scoped subscription detected - triggering org-level threshold billing', { @@ -538,6 +538,14 @@ async function checkAndBillOrganizationOverageThreshold( return noOp(options, 'no-subscription') } + if (isEnterprise(orgSubscription.plan) || isFree(orgSubscription.plan)) { + logger.debug('Organization plan not eligible for overage billing, skipping', { + organizationId, + plan: orgSubscription.plan, + }) + return noOp(options, 'plan-ineligible') + } + assertExpectedBillingPeriod( { type: 'organization', id: organizationId }, orgSubscription.periodStart, @@ -557,14 +565,6 @@ async function checkAndBillOrganizationOverageThreshold( stripeSubscriptionId: orgSubscription.stripeSubscriptionId, }) - if (isEnterprise(orgSubscription.plan) || isFree(orgSubscription.plan)) { - logger.debug('Organization plan not eligible for overage billing, skipping', { - organizationId, - plan: orgSubscription.plan, - }) - return noOp(options, 'plan-ineligible') - } - const memberUsageRows = await db .select({ userId: member.userId, diff --git a/apps/sim/lib/billing/types/index.test.ts b/apps/sim/lib/billing/types/index.test.ts index 3a762d65c39..3a79403c247 100644 --- a/apps/sim/lib/billing/types/index.test.ts +++ b/apps/sim/lib/billing/types/index.test.ts @@ -53,6 +53,7 @@ describe('Enterprise subscription metadata', () => { plan: 'enterprise', referenceId: 'org-1', monthlyPrice: 500, + invoiceAmountUsd: 500, seats: 25, }) }) @@ -67,7 +68,24 @@ describe('Enterprise subscription metadata', () => { plan: 'enterprise', referenceId: 'org-1', monthlyPrice: 500, + invoiceAmountUsd: 500, seats: 25, }) }) + + it('prefers the neutral invoice amount for annual Enterprise metadata', () => { + expect( + parseEnterpriseSubscriptionMetadata({ + plan: 'enterprise', + referenceId: 'org-1', + invoiceAmountCents: '120000', + seats: '25', + reportingPeriodAnchorDate: '2026-01-31', + }) + ).toMatchObject({ + invoiceAmountCents: 120000, + invoiceAmountUsd: 1200, + reportingPeriodAnchorDate: '2026-01-31', + }) + }) }) diff --git a/apps/sim/lib/billing/types/index.ts b/apps/sim/lib/billing/types/index.ts index 16952adcf29..b521eeb8003 100644 --- a/apps/sim/lib/billing/types/index.ts +++ b/apps/sim/lib/billing/types/index.ts @@ -9,30 +9,51 @@ import { parseWorkflowExecutionTimeoutSeconds, } from '@/lib/billing/execution-timeout-defaults' -export const enterpriseSubscriptionMetadataSchema = z.object({ - plan: z - .string() - .transform((v) => v.toLowerCase()) - .pipe(z.literal('enterprise')), - // The referenceId must be provided in Stripe metadata to link to the organization - // This gets stored in the subscription.referenceId column - referenceId: z.string().min(1), - // The fixed monthly price for this enterprise customer (as string from Stripe metadata) - // This will be used to set the organization's usage limit - monthlyPrice: z.coerce.number().positive(), - // Number of seats for invitation limits (not for billing) - seats: z.coerce.number().int().positive(), - concurrencyLimit: z.coerce - .number() - .int() - .positive() - .max(MAX_BILLING_CONCURRENCY_LIMIT) - .optional(), - workflowExecutionTimeoutSeconds: z.preprocess( - (value) => parseWorkflowExecutionTimeoutSeconds(value) ?? undefined, - z.number().int().positive().max(MAX_WORKFLOW_EXECUTION_TIMEOUT_SECONDS).optional() - ), -}) +export const enterpriseSubscriptionMetadataSchema = z + .object({ + plan: z + .string() + .transform((v) => v.toLowerCase()) + .pipe(z.literal('enterprise')), + referenceId: z.string().min(1), + invoiceAmountCents: z.coerce.number().int().positive().optional(), + /** Legacy monthly Enterprise metadata retained for existing subscriptions. */ + monthlyPrice: z.coerce.number().positive().optional(), + seats: z.coerce.number().int().positive(), + reportingPeriodAnchorDate: z + .string() + .regex(/^\d{4}-\d{2}-\d{2}$/) + .refine((value) => { + const parsed = new Date(`${value}T00:00:00.000Z`) + return ( + Number.isFinite(parsed.getTime()) && + parsed.toISOString().slice(0, 10) === value && + parsed.getTime() <= Date.now() + ) + }, 'Reporting-period anchor must be a valid UTC date that is not in the future') + .optional(), + concurrencyLimit: z.coerce + .number() + .int() + .positive() + .max(MAX_BILLING_CONCURRENCY_LIMIT) + .optional(), + workflowExecutionTimeoutSeconds: z.preprocess( + (value) => parseWorkflowExecutionTimeoutSeconds(value) ?? undefined, + z.number().int().positive().max(MAX_WORKFLOW_EXECUTION_TIMEOUT_SECONDS).optional() + ), + }) + .refine( + (metadata) => metadata.invoiceAmountCents !== undefined || metadata.monthlyPrice !== undefined, + { error: 'Enterprise invoice amount metadata is required' } + ) + .transform((metadata) => ({ + ...metadata, + invoiceAmountUsd: + metadata.invoiceAmountCents !== undefined + ? metadata.invoiceAmountCents / 100 + : (metadata.monthlyPrice as number), + })) export type EnterpriseSubscriptionMetadata = z.infer diff --git a/apps/sim/lib/billing/webhooks/enterprise.test.ts b/apps/sim/lib/billing/webhooks/enterprise.test.ts index 018edda9918..501db061365 100644 --- a/apps/sim/lib/billing/webhooks/enterprise.test.ts +++ b/apps/sim/lib/billing/webhooks/enterprise.test.ts @@ -14,11 +14,8 @@ import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ subscriptionsRetrieve: vi.fn(), patchOutboxEventPayload: vi.fn(), + enqueueOutboxEvent: vi.fn(), reapplyPaidOrgJoinBillingForExistingMemberTx: vi.fn(), - acquireUserBillingIdentityLock: vi.fn(), - acquireInvitationMutationLocks: vi.fn(), - attachOwnedWorkspacesToOrganizationTx: vi.fn(), - invalidateWorkspaceTableLimitsCache: vi.fn(), })) vi.mock('@sim/audit', () => ({ @@ -39,10 +36,6 @@ vi.mock('@/lib/billing/organizations/membership', () => ({ reapplyPaidOrgJoinBillingForExistingMemberTx: mocks.reapplyPaidOrgJoinBillingForExistingMemberTx, })) -vi.mock('@/lib/billing/organizations/billing-identity-lock', () => ({ - acquireUserBillingIdentityLock: mocks.acquireUserBillingIdentityLock, -})) - vi.mock('@/lib/billing/stripe-client', () => ({ requireStripeClient: () => ({ subscriptions: { retrieve: mocks.subscriptionsRetrieve }, @@ -59,23 +52,11 @@ vi.mock('@/lib/billing/webhooks/enterprise-reconciliation-lease', () => ({ ), })) -vi.mock('@/lib/billing/webhooks/idempotency', () => ({ - stripeWebhookIdempotency: { - executeWithIdempotency: vi.fn( - async (_provider: string, _identifier: string, operation: () => Promise) => - operation() - ), - }, -})) - vi.mock('@/lib/core/outbox/service', () => ({ + enqueueOutboxEvent: mocks.enqueueOutboxEvent, patchOutboxEventPayload: mocks.patchOutboxEventPayload, })) -vi.mock('@/lib/invitations/locks', () => ({ - acquireInvitationMutationLocks: mocks.acquireInvitationMutationLocks, -})) - vi.mock('@/lib/messaging/email/mailer', () => ({ sendEmail: vi.fn(), })) @@ -88,20 +69,13 @@ vi.mock('@/lib/posthog/server', () => ({ captureServerEvent: vi.fn(), })) -vi.mock('@/lib/table/billing', () => ({ - invalidateWorkspaceTableLimitsCache: mocks.invalidateWorkspaceTableLimitsCache, -})) - -vi.mock('@/lib/workspaces/organization-workspaces', () => ({ - attachOwnedWorkspacesToOrganizationTx: mocks.attachOwnedWorkspacesToOrganizationTx, - ownedAttachableWorkspacesWhere: vi.fn(), -})) - import { handleManualEnterpriseSubscription } from '@/lib/billing/webhooks/enterprise' const ENTERPRISE_PROVISION_EVENT_TYPE = 'stripe.provision-enterprise' -function operationPayload(options: { applied?: boolean; pausePaymentCollection?: boolean } = {}) { +function operationPayload( + options: { applied?: boolean; pausePaymentCollection?: boolean; workspaceIds?: string[] } = {} +) { return { version: 1 as const, request: { @@ -114,6 +88,7 @@ function operationPayload(options: { applied?: boolean; pausePaymentCollection?: usageLimitCredits: 24000, seats: 12, concurrencyLimit: 1250, + workspaceIds: options.workspaceIds ?? [], pausePaymentCollection: options.pausePaymentCollection ?? false, }, retryRevision: 0, @@ -180,18 +155,12 @@ function eventFor(subscription: Stripe.Subscription): Stripe.Event { function queueSuccessfulExistingSubscriptionReconciliation(options: { operation?: ReturnType - workspaceIds?: string[] }) { queueTableRows(schemaMock.organization, [{ creditBalance: '0' }]) if (options.operation) { queueTableRows(schemaMock.outboxEvent, [ { eventType: ENTERPRISE_PROVISION_EVENT_TYPE, payload: options.operation }, ]) - if (!('applicationResult' in options.operation)) { - const workspaceRows = (options.workspaceIds ?? []).map((id) => ({ id })) - queueTableRows(schemaMock.workspace, workspaceRows) - queueTableRows(schemaMock.workspace, workspaceRows) - } queueTableRows(schemaMock.outboxEvent, [ { eventType: ENTERPRISE_PROVISION_EVENT_TYPE, payload: options.operation }, ]) @@ -210,12 +179,7 @@ describe('Enterprise webhook issuance correlation', () => { resetDbChainMock() mocks.patchOutboxEventPayload.mockResolvedValue(true) mocks.reapplyPaidOrgJoinBillingForExistingMemberTx.mockResolvedValue(undefined) - mocks.attachOwnedWorkspacesToOrganizationTx.mockResolvedValue({ - attachedWorkspaceIds: [], - addedMemberIds: [], - skippedMembers: [], - usageLimitUserIds: [], - }) + mocks.enqueueOutboxEvent.mockResolvedValue('move-event') }) afterAll(() => { @@ -250,64 +214,49 @@ describe('Enterprise webhook issuance correlation', () => { expect(mocks.reapplyPaidOrgJoinBillingForExistingMemberTx).not.toHaveBeenCalled() }) - it('sweeps the Enterprise owner personal workspaces when issuance is applied', async () => { + it('queues the exact selected Enterprise owner workspaces after issuance is applied', async () => { const subscription = stripeSubscription({ operationId: 'operation-1', paused: false }) mocks.subscriptionsRetrieve.mockResolvedValue(subscription) queueSuccessfulExistingSubscriptionReconciliation({ - operation: operationPayload(), - workspaceIds: ['workspace-1', 'workspace-archived'], - }) - mocks.attachOwnedWorkspacesToOrganizationTx.mockResolvedValueOnce({ - attachedWorkspaceIds: ['workspace-1', 'workspace-archived'], - addedMemberIds: [], - skippedMembers: [], - usageLimitUserIds: [], + operation: operationPayload({ workspaceIds: ['workspace-1', 'workspace-archived'] }), }) await expect( handleManualEnterpriseSubscription(eventFor(subscription)) ).resolves.toBeUndefined() - expect(mocks.acquireInvitationMutationLocks).toHaveBeenCalledWith(expect.anything(), { - invitationIds: [], - workspaceIds: ['workspace-1', 'workspace-archived'], - }) - expect(mocks.acquireUserBillingIdentityLock).toHaveBeenCalledWith(expect.anything(), 'owner-1') - expect(mocks.attachOwnedWorkspacesToOrganizationTx).toHaveBeenCalledWith(expect.anything(), { - ownerUserId: 'owner-1', - organizationId: 'org-1', - workspaceIds: ['workspace-1', 'workspace-archived'], - externalMemberPolicy: 'external-all', - ownerMatch: 'owner', - includeArchived: true, - }) - expect(mocks.invalidateWorkspaceTableLimitsCache).toHaveBeenCalledTimes(2) + expect(mocks.enqueueOutboxEvent).toHaveBeenNthCalledWith( + 1, + expect.anything(), + 'enterprise.move-workspace', + expect.objectContaining({ workspaceId: 'workspace-1', sequence: 0 }) + ) + expect(mocks.enqueueOutboxEvent).toHaveBeenNthCalledWith( + 2, + expect.anything(), + 'enterprise.move-workspace', + expect.objectContaining({ workspaceId: 'workspace-archived', sequence: 1 }) + ) expect(mocks.patchOutboxEventPayload).toHaveBeenCalled() }) - it('retries without applying when the Enterprise owner workspace set changes', async () => { + it('does not discover owner workspaces that were not selected at confirmation', async () => { const subscription = stripeSubscription({ operationId: 'operation-1', paused: false }) mocks.subscriptionsRetrieve.mockResolvedValue(subscription) - queueTableRows(schemaMock.outboxEvent, [ - { eventType: ENTERPRISE_PROVISION_EVENT_TYPE, payload: operationPayload() }, - ]) - queueTableRows(schemaMock.workspace, [{ id: 'workspace-1' }]) - queueTableRows(schemaMock.organization, [{ creditBalance: '0' }]) - queueTableRows(schemaMock.outboxEvent, [ - { eventType: ENTERPRISE_PROVISION_EVENT_TYPE, payload: operationPayload() }, - ]) - queueTableRows(schemaMock.user, [{ stripeCustomerId: 'cus_1' }]) - queueTableRows(schemaMock.member, [{ value: 1 }]) - queueTableRows(schemaMock.subscription, []) - queueTableRows(schemaMock.subscription, [{ id: 'local-sub-1', referenceId: 'org-1' }]) - queueTableRows(schemaMock.workspace, [{ id: 'workspace-1' }, { id: 'workspace-2' }]) + queueSuccessfulExistingSubscriptionReconciliation({ + operation: operationPayload({ workspaceIds: ['workspace-1'] }), + }) - await expect(handleManualEnterpriseSubscription(eventFor(subscription))).rejects.toThrow( - 'personal workspaces changed during reconciliation' - ) + await expect( + handleManualEnterpriseSubscription(eventFor(subscription)) + ).resolves.toBeUndefined() - expect(mocks.attachOwnedWorkspacesToOrganizationTx).not.toHaveBeenCalled() - expect(mocks.patchOutboxEventPayload).not.toHaveBeenCalled() + expect(mocks.enqueueOutboxEvent).toHaveBeenCalledTimes(1) + expect(mocks.enqueueOutboxEvent).toHaveBeenCalledWith( + expect.anything(), + 'enterprise.move-workspace', + expect.objectContaining({ workspaceId: 'workspace-1' }) + ) }) it('allows later Stripe metadata edits after the issuance was already applied', async () => { @@ -333,4 +282,17 @@ describe('Enterprise webhook issuance correlation', () => { handleManualEnterpriseSubscription(eventFor(subscription)) ).resolves.toBeUndefined() }) + + it('reconciles a duplicate event again so a stale generic webhook write is corrected', async () => { + const subscription = stripeSubscription({}) + mocks.subscriptionsRetrieve.mockResolvedValue(subscription) + queueSuccessfulExistingSubscriptionReconciliation({}) + queueSuccessfulExistingSubscriptionReconciliation({}) + const event = eventFor(subscription) + + await expect(handleManualEnterpriseSubscription(event)).resolves.toBeUndefined() + await expect(handleManualEnterpriseSubscription(event)).resolves.toBeUndefined() + + expect(mocks.subscriptionsRetrieve).toHaveBeenCalledTimes(2) + }) }) diff --git a/apps/sim/lib/billing/webhooks/enterprise.ts b/apps/sim/lib/billing/webhooks/enterprise.ts index aa7bc71681d..19c15bd15a1 100644 --- a/apps/sim/lib/billing/webhooks/enterprise.ts +++ b/apps/sim/lib/billing/webhooks/enterprise.ts @@ -1,6 +1,6 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' -import { member, organization, outboxEvent, subscription, user, workspace } from '@sim/db/schema' +import { member, organization, outboxEvent, subscription, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' import { and, asc, count, eq, inArray, sql } from 'drizzle-orm' @@ -8,12 +8,15 @@ import type Stripe from 'stripe' import { getEmailSubject, renderEnterpriseSubscriptionEmail } from '@/components/emails' import { deriveEnterpriseCreditLimits } from '@/lib/billing/enterprise-credit-limits' import { + ENTERPRISE_METADATA_SYNC_EVENT_TYPE, ENTERPRISE_PROVISION_EVENT_TYPE, + ENTERPRISE_WORKSPACE_MOVE_EVENT_TYPE, type EnterpriseProvisionPayload, + enterpriseMetadataIntentMatchesStripeSubscription, + enterpriseMetadataSyncPayloadSchema, enterpriseOperationMatchesStripeSubscription, parseEnterpriseProvisionPayload, } from '@/lib/billing/enterprise-outbox' -import { acquireUserBillingIdentityLock } from '@/lib/billing/organizations/billing-identity-lock' import { acquireOrganizationMutationLock, reapplyPaidOrgJoinBillingForExistingMemberTx, @@ -25,88 +28,20 @@ import { type EnterpriseReconciliationLease, withEnterpriseReconciliationLease, } from '@/lib/billing/webhooks/enterprise-reconciliation-lease' -import { stripeWebhookIdempotency } from '@/lib/billing/webhooks/idempotency' -import { patchOutboxEventPayload } from '@/lib/core/outbox/service' -import { acquireInvitationMutationLocks } from '@/lib/invitations/locks' +import { enqueueOutboxEvent, patchOutboxEventPayload } from '@/lib/core/outbox/service' import { sendEmail } from '@/lib/messaging/email/mailer' import { getFromEmailAddress } from '@/lib/messaging/email/utils' import { captureServerEvent } from '@/lib/posthog/server' -import { invalidateWorkspaceTableLimitsCache } from '@/lib/table/billing' -import { - attachOwnedWorkspacesToOrganizationTx, - ownedAttachableWorkspacesWhere, -} from '@/lib/workspaces/organization-workspaces' import { parseEnterpriseSubscriptionMetadata } from '../types' const logger = createLogger('BillingEnterprise') -interface EnterpriseWorkspaceSweepPlan { - operationId: string - ownerUserId: string - workspaceIds: string[] -} - -function sameOrderedIds(left: string[], right: string[]): boolean { - return left.length === right.length && left.every((id, index) => id === right[index]) -} - -async function planEnterpriseOwnerWorkspaceSweep( - metadata: Stripe.Metadata, - organizationId: string -): Promise { - const operationId = metadata.enterpriseOperationId - if (!operationId) return null - - const [operationRow] = await db - .select({ eventType: outboxEvent.eventType, payload: outboxEvent.payload }) - .from(outboxEvent) - .where(eq(outboxEvent.id, operationId)) - .limit(1) - const payload = operationRow ? parseEnterpriseProvisionPayload(operationRow.payload) : null - if ( - operationRow?.eventType !== ENTERPRISE_PROVISION_EVENT_TYPE || - !payload || - payload.applicationResult || - payload.request.organizationId !== organizationId - ) { - return null - } - - const workspaceIds = ( - await db - .select({ id: workspace.id }) - .from(workspace) - .where( - ownedAttachableWorkspacesWhere({ - userId: payload.request.ownerUserId, - includeArchived: true, - }) - ) - .orderBy(workspace.id) - ).map((row) => row.id) - - return { operationId, ownerUserId: payload.request.ownerUserId, workspaceIds } -} - export async function handleManualEnterpriseSubscription(event: Stripe.Event) { - return stripeWebhookIdempotency.executeWithIdempotency( - 'manual-enterprise-subscription', - event.id, - () => processManualEnterpriseSubscription(event) - ) + return processManualEnterpriseSubscription(event) } async function processManualEnterpriseSubscription(event: Stripe.Event) { const eventSubscription = event.data.object as Stripe.Subscription - const eventPlan = eventSubscription.metadata?.plan?.toLowerCase() ?? '' - if (eventPlan !== 'enterprise') { - logger.info('[subscription] Skipping non-enterprise subscription', { - subscriptionId: eventSubscription.id, - plan: eventPlan || 'unknown', - }) - return - } - return withEnterpriseReconciliationLease(eventSubscription.id, (lease) => reconcileManualEnterpriseSubscription(eventSubscription, lease) ) @@ -169,11 +104,22 @@ async function reconcileManualEnterpriseSubscription( throw new Error('Invalid enterprise metadata for subscription') } - const { seats, monthlyPrice } = enterpriseMetadata - const workspaceSweepPlan = await planEnterpriseOwnerWorkspaceSweep(metadata, referenceId) - + const { seats, invoiceAmountUsd } = enterpriseMetadata // Get the first subscription item which contains the period information const referenceItem = stripeSubscription.items?.data?.[0] + if ( + stripeSubscription.items?.data?.length !== 1 || + referenceItem?.price.currency !== 'usd' || + referenceItem.price.unit_amount === null || + Math.abs(referenceItem.price.unit_amount / 100 - invoiceAmountUsd) > 1e-8 || + (referenceItem.price.recurring?.interval !== 'month' && + referenceItem.price.recurring?.interval !== 'year') || + (referenceItem.price.recurring.interval_count ?? 1) !== 1 + ) { + throw new Error( + 'Enterprise subscription must have one USD monthly or annual Price matching its metadata' + ) + } const subscriptionRow = { id: generateId(), @@ -204,12 +150,6 @@ async function reconcileManualEnterpriseSubscription( } const coreResult = await db.transaction(async (tx) => { - if (workspaceSweepPlan && workspaceSweepPlan.workspaceIds.length > 0) { - await acquireInvitationMutationLocks(tx, { - invitationIds: [], - workspaceIds: workspaceSweepPlan.workspaceIds, - }) - } await acquireOrganizationMutationLock(tx, referenceId) await tx.execute( sql`select pg_advisory_xact_lock(hashtextextended(${`stripe-subscription:${stripeSubscription.id}`}, 0))` @@ -339,6 +279,35 @@ async function reconcileManualEnterpriseSubscription( ) } + const configOperationId = metadata.simConfigOperationId + if (typeof configOperationId === 'string' && configOperationId.length > 0) { + const [configurationRow] = await tx + .select({ eventType: outboxEvent.eventType, payload: outboxEvent.payload }) + .from(outboxEvent) + .where(eq(outboxEvent.id, configOperationId)) + .for('update') + .limit(1) + const configurationPayload = enterpriseMetadataSyncPayloadSchema.safeParse( + configurationRow?.payload + ) + const validConfiguration = Boolean( + existing && + configurationRow?.eventType === ENTERPRISE_METADATA_SYNC_EVENT_TYPE && + configurationPayload.success && + configurationPayload.data.subscriptionId === existing.id && + enterpriseMetadataIntentMatchesStripeSubscription( + configurationPayload.data, + configOperationId, + stripeSubscription + ) + ) + if (!validConfiguration) { + throw new Error( + `Enterprise configuration operation ${configOperationId} does not exactly match the Stripe subscription` + ) + } + } + if (existing) { await tx .update(subscription) @@ -366,7 +335,7 @@ async function reconcileManualEnterpriseSubscription( const creditLimits = deriveEnterpriseCreditLimits({ metadata, - monthlyPriceUsd: monthlyPrice, + invoiceAmountUsd, prepaidBalanceDollars: organizationRow.creditBalance, }) await tx @@ -377,44 +346,17 @@ async function reconcileManualEnterpriseSubscription( }) .where(eq(organization.id, referenceId)) - let attachedWorkspaceIds: string[] = [] if (operationNewlyApplied && correlatedOperation) { - if ( - !workspaceSweepPlan || - workspaceSweepPlan.operationId !== operationId || - workspaceSweepPlan.ownerUserId !== correlatedOperation.request.ownerUserId - ) { - throw new Error('Unable to establish the Enterprise owner workspace sweep') - } - - await acquireUserBillingIdentityLock(tx, workspaceSweepPlan.ownerUserId) - const currentWorkspaceIds = ( - await tx - .select({ id: workspace.id }) - .from(workspace) - .where( - ownedAttachableWorkspacesWhere({ - userId: workspaceSweepPlan.ownerUserId, - includeArchived: true, - }) - ) - .orderBy(workspace.id) - ).map((row) => row.id) - if (!sameOrderedIds(workspaceSweepPlan.workspaceIds, currentWorkspaceIds)) { - throw new Error( - 'Enterprise owner personal workspaces changed during reconciliation; retry the webhook' - ) + for (const [sequence, workspaceId] of correlatedOperation.request.workspaceIds.entries()) { + await enqueueOutboxEvent(tx, ENTERPRISE_WORKSPACE_MOVE_EVENT_TYPE, { + provisioningOperationId: operationId, + workspaceId, + destinationOrganizationId: referenceId, + expectedOwnerId: correlatedOperation.request.ownerUserId, + adminEmail: correlatedOperation.request.requestedByEmail, + sequence, + }) } - - const attached = await attachOwnedWorkspacesToOrganizationTx(tx, { - ownerUserId: workspaceSweepPlan.ownerUserId, - organizationId: referenceId, - workspaceIds: currentWorkspaceIds, - externalMemberPolicy: 'external-all', - ownerMatch: 'owner', - includeArchived: true, - }) - attachedWorkspaceIds = attached.attachedWorkspaceIds } // The organization lock is held across the census and all member billing @@ -448,7 +390,10 @@ async function reconcileManualEnterpriseSubscription( operationNewlyApplied, hasCorrelatedOperation: Boolean(correlatedOperation), subscriptionNewlyInserted: !existing, - attachedWorkspaceIds, + queuedWorkspaceCount: + operationNewlyApplied && correlatedOperation + ? correlatedOperation.request.workspaceIds.length + : 0, ...creditLimits, } }) @@ -460,14 +405,11 @@ async function reconcileManualEnterpriseSubscription( operationNewlyApplied, hasCorrelatedOperation, subscriptionNewlyInserted, - attachedWorkspaceIds, + queuedWorkspaceCount, configuredUsageLimitCredits, prepaidCredits, effectiveUsageLimitCredits, } = coreResult - for (const workspaceId of attachedWorkspaceIds) { - invalidateWorkspaceTableLimitsCache(workspaceId) - } const shouldAnnounce = hasCorrelatedOperation ? operationNewlyApplied : subscriptionNewlyInserted logger.info('[subscription.created] Upserted enterprise subscription', { @@ -475,11 +417,11 @@ async function reconcileManualEnterpriseSubscription( referenceId: subscriptionRow.referenceId, plan: subscriptionRow.plan, status: subscriptionRow.status, - monthlyPrice, + invoiceAmountUsd, effectiveUsageLimitCredits, prepaidCredits, seats, - attachedWorkspaceCount: attachedWorkspaceIds.length, + queuedWorkspaceCount, note: 'Seats from metadata, Stripe quantity set to 1', }) @@ -522,7 +464,7 @@ async function reconcileManualEnterpriseSubscription( stripeCustomerId, stripeSubscriptionId: stripeSubscription.id, seats, - monthlyPrice, + invoiceAmountUsd, configuredUsageLimitCredits, effectiveUsageLimitCredits, prepaidCredits, @@ -532,7 +474,8 @@ async function reconcileManualEnterpriseSubscription( captureServerEvent(actorId ?? referenceId, 'enterprise_subscription_created', { reference_id: referenceId, seats, - monthly_price: monthlyPrice, + invoice_amount: invoiceAmountUsd, + billing_interval: subscriptionRow.billingInterval === 'year' ? 'year' : 'month', currency: 'usd', }) } diff --git a/apps/sim/lib/core/outbox/service.test.ts b/apps/sim/lib/core/outbox/service.test.ts index cb2a21d01b2..138e286a1b8 100644 --- a/apps/sim/lib/core/outbox/service.test.ts +++ b/apps/sim/lib/core/outbox/service.test.ts @@ -25,6 +25,7 @@ vi.mock('@sim/utils/id', () => ({ })) import { + deferOutboxHandler, enqueueOrReschedulePendingOutboxEvent, enqueueOutboxEvent, processOutboxEvents, @@ -289,6 +290,45 @@ describe('processOutboxEvents — handler success and retry', () => { expect(scheduledAt.getTime()).toBeLessThan(before + 10_000) }) + it('keeps an acknowledged external wait pending without recording a failure', async () => { + const handler = vi.fn(async () => deferOutboxHandler('waiting for webhook')) + queueTableRows(outboxEvent, [makePendingRow({ attempts: 2 })]) + holdLease() + + const result = await processOutboxEvents({ 'test.event': handler }) + + expect(result.retried).toBe(1) + const deferredUpdate = updateSets().find((set) => set.status === 'pending' && 'attempts' in set) + expect(deferredUpdate).toMatchObject({ attempts: 3, lastError: null, lockedAt: null }) + }) + + it('dead-letters a deferred wait only after its acknowledgement budget is exhausted', async () => { + const handler = vi.fn(async () => deferOutboxHandler('webhook acknowledgement missing')) + queueTableRows(outboxEvent, [makePendingRow({ attempts: 9, maxAttempts: 10 })]) + holdLease() + + const result = await processOutboxEvents({ 'test.event': handler }) + + expect(result.deadLettered).toBe(1) + const deadUpdate = updateSets().find((set) => set.status === 'dead_letter') + expect(deadUpdate).toMatchObject({ + attempts: 10, + lastError: 'webhook acknowledgement missing', + }) + }) + + it('reschedules an internal dependency wait without consuming its attempt budget', async () => { + const handler = vi.fn(async () => deferOutboxHandler('waiting for dependency', 5_000, false)) + queueTableRows(outboxEvent, [makePendingRow({ attempts: 4, maxAttempts: 5 })]) + holdLease() + + const result = await processOutboxEvents({ 'test.event': handler }) + + expect(result.retried).toBe(1) + const deferredUpdate = updateSets().find((set) => set.status === 'pending' && 'attempts' in set) + expect(deferredUpdate).toMatchObject({ attempts: 4, lastError: null, lockedAt: null }) + }) + it('dead-letters on failure when attempts reaches maxAttempts', async () => { const handler = vi.fn(async () => { throw new Error('permanent failure') diff --git a/apps/sim/lib/core/outbox/service.ts b/apps/sim/lib/core/outbox/service.ts index d18cff7945b..9a7b8029dec 100644 --- a/apps/sim/lib/core/outbox/service.ts +++ b/apps/sim/lib/core/outbox/service.ts @@ -74,7 +74,35 @@ export interface OutboxEventContext { * Throwing bumps `attempts` and schedules a retry via exponential * backoff; a successful return transitions the event to `completed`. */ -export type OutboxHandler = (payload: T, context: OutboxEventContext) => Promise +export interface DeferredOutboxHandlerResult { + outcome: 'deferred' + reason: string + minimumBackoffMs?: number + /** + * Defaults to true for an external acknowledgement with a finite retry + * budget. Set false only for an internal dependency whose own outbox row + * independently reaches completed or dead-letter. + */ + consumeAttempt?: boolean +} + +export function deferOutboxHandler( + reason: string, + minimumBackoffMs?: number, + consumeAttempt = true +): DeferredOutboxHandlerResult { + return { + outcome: 'deferred', + reason, + ...(minimumBackoffMs !== undefined ? { minimumBackoffMs } : {}), + ...(consumeAttempt ? {} : { consumeAttempt: false }), + } +} + +export type OutboxHandler = ( + payload: T, + context: OutboxEventContext +) => Promise /** * Map of `eventType` → handler. Register all handlers in one place @@ -464,7 +492,10 @@ async function runHandler( } try { - await runHandlerWithTimeout(handler, event) + const handlerResult = await runHandlerWithTimeout(handler, event) + if (handlerResult?.outcome === 'deferred') { + return scheduleDeferred(event, handlerResult) + } const updated = await updateIfLeaseHeld(event, { status: 'completed', lastError: null, @@ -625,6 +656,52 @@ async function scheduleRetry( return 'pending' } +async function scheduleDeferred( + event: typeof outboxEvent.$inferSelect, + result: DeferredOutboxHandlerResult +): Promise<'pending' | 'dead_letter' | 'lease_lost'> { + const nextAttempts = event.attempts + (result.consumeAttempt === false ? 0 : 1) + if (result.consumeAttempt !== false && nextAttempts >= event.maxAttempts) { + const updated = await updateIfLeaseHeld(event, { + attempts: nextAttempts, + status: 'dead_letter', + lastError: result.reason, + processedAt: new Date(), + lockedAt: null, + }) + if (!updated) return 'lease_lost' + logger.error('Outbox event dead-lettered while awaiting external acknowledgement', { + eventId: event.id, + eventType: event.eventType, + attempts: nextAttempts, + reason: result.reason, + }) + return 'dead_letter' + } + + const backoffMs = Math.max( + result.minimumBackoffMs ?? 0, + Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** nextAttempts) + ) + const nextAvailableAt = new Date(Date.now() + backoffMs) + const updated = await updateIfLeaseHeld(event, { + attempts: nextAttempts, + status: 'pending', + lastError: null, + availableAt: nextAvailableAt, + lockedAt: null, + }) + if (!updated) return 'lease_lost' + logger.info('Outbox event is awaiting external acknowledgement', { + eventId: event.id, + eventType: event.eventType, + attempts: nextAttempts, + backoffMs, + nextAvailableAt: nextAvailableAt.toISOString(), + }) + return 'pending' +} + async function updateProcessingIfLeaseHeld( event: typeof outboxEvent.$inferSelect, patch: { @@ -651,7 +728,7 @@ function runHandlerWithTimeout( handler: OutboxHandler, event: typeof outboxEvent.$inferSelect, timeoutMs: number = DEFAULT_HANDLER_TIMEOUT_MS -): Promise { +): Promise { const controller = new AbortController() const context: OutboxEventContext = { eventId: event.id, diff --git a/apps/sim/lib/logs/execution/logger.ts b/apps/sim/lib/logs/execution/logger.ts index 6e5d8c27d3a..828cdc27de8 100644 --- a/apps/sim/lib/logs/execution/logger.ts +++ b/apps/sim/lib/logs/execution/logger.ts @@ -1301,12 +1301,16 @@ export class ExecutionLogger implements IExecutionLoggerService { payerSubscription.plan, payerSubscription.seats ) - const [{ sum: orgBaselineSum }] = await db - .select({ sum: sql`COALESCE(SUM(${userStats.currentPeriodCost}), 0)` }) - .from(member) - .leftJoin(userStats, eq(member.userId, userStats.userId)) - .where(eq(member.organizationId, organizationId)) - .limit(1) + let orgBaseline = 0 + if (exactBillingContext.billingPeriod.source !== 'reporting') { + const [{ sum }] = await db + .select({ sum: sql`COALESCE(SUM(${userStats.currentPeriodCost}), 0)` }) + .from(member) + .leftJoin(userStats, eq(member.userId, userStats.userId)) + .where(eq(member.organizationId, organizationId)) + .limit(1) + orgBaseline = Number.parseFloat(String(sum ?? '0')) + } const { getBillingPeriodUsageCost } = await import('@/lib/billing/core/usage-log') const orgLedger = await getBillingPeriodUsageCost( billingAttribution.billingEntity, @@ -1317,7 +1321,7 @@ export class ExecutionLogger implements IExecutionLoggerService { organizationId, planName: getDisplayPlanName(payerSubscription.plan), orgLimit, - orgUsageBefore: Number.parseFloat(String(orgBaselineSum ?? '0')) + orgLedger, + orgUsageBefore: orgBaseline + orgLedger, } } else if (billingAttribution?.billingEntity.type === 'user' && usr?.email) { const sub = await getHighestPriorityPersonalSubscription(usr.id) diff --git a/apps/sim/lib/posthog/events.ts b/apps/sim/lib/posthog/events.ts index c822757165b..1a2100853b8 100644 --- a/apps/sim/lib/posthog/events.ts +++ b/apps/sim/lib/posthog/events.ts @@ -788,7 +788,8 @@ export interface PostHogEventMap { enterprise_subscription_created: { reference_id: string seats: number - monthly_price: number + invoice_amount: number + billing_interval: 'month' | 'year' currency: string } diff --git a/packages/db/migrations/0290_workable_jigsaw.sql b/packages/db/migrations/0290_workable_jigsaw.sql new file mode 100644 index 00000000000..2fb0d95d34e --- /dev/null +++ b/packages/db/migrations/0290_workable_jigsaw.sql @@ -0,0 +1,11 @@ +-- usage_log is a hot append-only ledger. Build the reporting-range covering +-- index without taking a table-wide write lock. The migration runner starts a +-- transaction for each pending batch, so end it before CONCURRENTLY. Everything +-- below is replayable even when a failed concurrent build left an INVALID +-- same-named index behind. +COMMIT;--> statement-breakpoint +SET lock_timeout = 0;--> statement-breakpoint +-- migration-safe: replay cleanup for the index introduced by this same unjournaled migration; CONCURRENTLY preserves usage_log writes +DROP INDEX CONCURRENTLY IF EXISTS "usage_log_billing_entity_created_at_cost_idx";--> statement-breakpoint +CREATE INDEX CONCURRENTLY IF NOT EXISTS "usage_log_billing_entity_created_at_cost_idx" ON "usage_log" USING btree ("billing_entity_type","billing_entity_id","created_at","user_id","source","cost") WHERE "usage_log"."billing_entity_type" IS NOT NULL;--> statement-breakpoint +SET lock_timeout = '5s'; diff --git a/packages/db/migrations/meta/0290_snapshot.json b/packages/db/migrations/meta/0290_snapshot.json new file mode 100644 index 00000000000..8131d4d2094 --- /dev/null +++ b/packages/db/migrations/meta/0290_snapshot.json @@ -0,0 +1,19139 @@ +{ + "id": "3967bd75-0a06-4485-b854-827f8dc44ad0", + "prevId": "b381a516-8a98-4877-ae12-f37461e980ef", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.background_work_status": { + "name": "background_work_status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "background_work_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "background_work_status_value", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "background_work_status_workspace_status_idx": { + "name": "background_work_status_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_workflow_status_idx": { + "name": "background_work_status_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_child_ws_idx": { + "name": "background_work_status_meta_child_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'childWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_other_ws_idx": { + "name": "background_work_status_meta_other_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'otherWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "background_work_status_workspace_id_workspace_id_fk": { + "name": "background_work_status_workspace_id_workspace_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "background_work_status_workflow_id_workflow_id_fk": { + "name": "background_work_status_workflow_id_workflow_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "include_thinking": { + "name": "include_thinking", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_tool_calls": { + "name": "include_tool_calls", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_decision": { + "name": "permission_decision", + "type": "copilot_tool_permission_decision", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "permission_decided_at": { + "name": "permission_decided_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "auto_allowed_tools": { + "name": "auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_deleted_partial_idx": { + "name": "copilot_chats_user_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_chats\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_user_created_at_idx": { + "name": "copilot_messages_user_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"role\" = 'user' AND \"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_block": { + "name": "custom_block", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs": { + "name": "inputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_block_organization_id_idx": { + "name": "custom_block_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_workflow_id_idx": { + "name": "custom_block_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_organization_type_unique": { + "name": "custom_block_organization_type_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_block_organization_id_organization_id_fk": { + "name": "custom_block_organization_id_organization_id_fk", + "tableFrom": "custom_block", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_workflow_id_workflow_id_fk": { + "name": "custom_block_workflow_id_workflow_id_fk", + "tableFrom": "custom_block", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_created_by_user_id_fk": { + "name": "custom_block_created_by_user_id_fk", + "tableFrom": "custom_block", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_secret_provenance": { + "name": "document_secret_provenance", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "document_secret_provenance_document_id_document_id_fk": { + "name": "document_secret_provenance_document_id_document_id_fk", + "tableFrom": "document_secret_provenance", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "document_secret_provenance_status_check": { + "name": "document_secret_provenance_status_check", + "value": "\"document_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.embedding_secret_provenance": { + "name": "embedding_secret_provenance", + "schema": "", + "columns": { + "embedding_id": { + "name": "embedding_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "embedding_secret_provenance_embedding_id_embedding_id_fk": { + "name": "embedding_secret_provenance_embedding_id_embedding_id_fk", + "tableFrom": "embedding_secret_provenance", + "tableTo": "embedding", + "columnsFrom": ["embedding_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_secret_provenance_status_check": { + "name": "embedding_secret_provenance_status_check", + "value": "\"embedding_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_references_workflow_id_idx": { + "name": "execution_large_value_references_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_workflow_id_idx": { + "name": "execution_large_values_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.folder": { + "name": "folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "folder_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "folder_user_idx": { + "name": "folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_resource_parent_idx": { + "name": "folder_workspace_resource_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_parent_sort_idx": { + "name": "folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_deleted_at_idx": { + "name": "folder_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_deleted_partial_idx": { + "name": "folder_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"folder\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_resource_parent_name_active_unique": { + "name": "folder_workspace_resource_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"folder\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folder_user_id_user_id_fk": { + "name": "folder_user_id_user_id_fk", + "tableFrom": "folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_workspace_id_workspace_id_fk": { + "name": "folder_workspace_id_workspace_id_fk", + "tableFrom": "folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_parent_id_folder_id_fk": { + "name": "folder_parent_id_folder_id_fk", + "tableFrom": "folder", + "tableTo": "folder", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_folder_id_idx": { + "name": "kb_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_folder_id_folder_id_fk": { + "name": "knowledge_base_folder_id_folder_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory_secret_provenance": { + "name": "memory_secret_provenance", + "schema": "", + "columns": { + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "memory_secret_provenance_memory_id_memory_id_fk": { + "name": "memory_secret_provenance_memory_id_memory_id_fk", + "tableFrom": "memory_secret_provenance", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "memory_secret_provenance_status_check": { + "name": "memory_secret_provenance_status_check", + "value": "\"memory_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_settings_workspace_id_idx": { + "name": "mothership_settings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "session_policy_settings": { + "name": "session_policy_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "security_policy_version": { + "name": "security_policy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["set_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_type_created_idx": { + "name": "outbox_event_type_created_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "automatic_resume_retry_count": { + "name": "automatic_resume_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_name_unique": { + "name": "permission_group_organization_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_workspace": { + "name": "permission_group_workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pinned_item": { + "name": "pinned_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pinned_item_user_workspace_idx": { + "name": "pinned_item_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pinned_item_resource_idx": { + "name": "pinned_item_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pinned_item_user_resource_unique": { + "name": "pinned_item_user_resource_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pinned_item_user_id_user_id_fk": { + "name": "pinned_item_user_id_user_id_fk", + "tableFrom": "pinned_item", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pinned_item_workspace_id_workspace_id_fk": { + "name": "pinned_item_workspace_id_workspace_id_fk", + "tableFrom": "pinned_item", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sandbox_image": { + "name": "sandbox_image", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec_hash": { + "name": "spec_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec": { + "name": "spec", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "sandbox_image_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "image_ref": { + "name": "image_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_image_id": { + "name": "provider_image_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "materialization_generation": { + "name": "materialization_generation", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_detail": { + "name": "error_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sandbox_image_provider_spec_unique": { + "name": "sandbox_image_provider_spec_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spec_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sandbox_image_status_idx": { + "name": "sandbox_image_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sandbox_image_last_used_idx": { + "name": "sandbox_image_last_used_idx", + "columns": [ + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": ["workflow_id", "block_id", "scope_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill_member": { + "name": "skill_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_member_user_id_idx": { + "name": "skill_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "skill_member_unique": { + "name": "skill_member_unique", + "columns": [ + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_member_skill_id_skill_id_fk": { + "name": "skill_member_skill_id_skill_id_fk", + "tableFrom": "skill_member", + "tableTo": "skill", + "columnsFrom": ["skill_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_user_id_user_id_fk": { + "name": "skill_member_user_id_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_invited_by_user_id_fk": { + "name": "skill_member_invited_by_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_domain": { + "name": "sso_domain", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sso_domain_organization_id_idx": { + "name": "sso_domain_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_domain_idx": { + "name": "sso_domain_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_org_domain_unique": { + "name": "sso_domain_org_domain_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_verified_unique": { + "name": "sso_domain_verified_unique", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'verified'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_domain_organization_id_organization_id_fk": { + "name": "sso_domain_organization_id_organization_id_fk", + "tableFrom": "sso_domain", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_domain_created_by_user_id_fk": { + "name": "sso_domain_created_by_user_id_fk", + "tableFrom": "sso_domain", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain_verified": { + "name": "domain_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_jobs": { + "name": "table_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enrichment_details": { + "name": "enrichment_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_views": { + "name": "table_views", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_views_table_created_idx": { + "name": "table_views_table_created_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_views_table_default_unique": { + "name": "table_views_table_default_unique", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_views_table_id_user_table_definitions_id_fk": { + "name": "table_views_table_id_user_table_definitions_id_fk", + "tableFrom": "table_views", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_workspace_id_workspace_id_fk": { + "name": "table_views_workspace_id_workspace_id_fk", + "tableFrom": "table_views", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_created_by_user_id_fk": { + "name": "table_views_created_by_user_id_fk", + "tableFrom": "table_views", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.upload_session": { + "name": "upload_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purpose": { + "name": "purpose", + "type": "upload_session_purpose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "upload_session_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "storage_context": { + "name": "storage_context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "final_key": { + "name": "final_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_provider": { + "name": "storage_provider", + "type": "upload_session_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider_upload_id": { + "name": "provider_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_object_version": { + "name": "provider_object_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "part_count": { + "name": "part_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "upload_session_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'uploading'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "processing_lease_id": { + "name": "processing_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_lease_expires_at": { + "name": "processing_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_file_id": { + "name": "completed_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "upload_session_token_hash_unique": { + "name": "upload_session_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "upload_session_final_key_unique": { + "name": "upload_session_final_key_unique", + "columns": [ + { + "expression": "final_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "upload_session_status_expires_at_idx": { + "name": "upload_session_status_expires_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_period_cost_idx": { + "name": "usage_log_billing_period_cost_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_created_at_cost_idx": { + "name": "usage_log_billing_entity_created_at_cost_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "schema_locked": { + "name": "schema_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "insert_locked": { + "name": "insert_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "update_locked": { + "name": "update_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "delete_locked": { + "name": "delete_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_folder_id_idx": { + "name": "user_table_def_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_folder_id_folder_id_fk": { + "name": "user_table_definitions_folder_id_folder_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_row_secret_provenance": { + "name": "user_table_row_secret_provenance", + "schema": "", + "columns": { + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_table_row_secret_provenance_row_id_user_table_rows_id_fk": { + "name": "user_table_row_secret_provenance_row_id_user_table_rows_id_fk", + "tableFrom": "user_table_row_secret_provenance", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_table_row_secret_provenance_status_check": { + "name": "user_table_row_secret_provenance_status_check", + "value": "\"user_table_row_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_status": { + "name": "registration_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_generation": { + "name": "registration_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "config_fingerprint": { + "name": "config_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prepared_at": { + "name": "prepared_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "routing_key": { + "name": "routing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_routing_key_active_idx": { + "name": "webhook_routing_key_active_idx", + "columns": [ + { + "expression": "routing_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NULL AND \"webhook\".\"routing_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_active_registration_unique": { + "name": "webhook_active_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'active' AND \"webhook\".\"block_id\" IS NOT NULL AND \"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_candidate_registration_unique": { + "name": "webhook_candidate_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'candidate' AND \"webhook\".\"block_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_registration_status_generation_idx": { + "name": "webhook_registration_status_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_registration_status_check": { + "name": "webhook_registration_status_check", + "value": "\"webhook\".\"registration_status\" IS NULL OR \"webhook\".\"registration_status\" IN ('active', 'candidate', 'retired', 'orphaned')" + }, + "webhook_registration_generation_check": { + "name": "webhook_registration_generation_check", + "value": "\"webhook\".\"registration_generation\" IS NULL OR \"webhook\".\"registration_generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.webhook_path_claim": { + "name": "webhook_path_claim", + "schema": "", + "columns": { + "path": { + "name": "path", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_path_claim_workflow_idx": { + "name": "webhook_path_claim_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_path_claim_workflow_id_workflow_id_fk": { + "name": "webhook_path_claim_workflow_id_workflow_id_fk", + "tableFrom": "webhook_path_claim", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_path_claim_generation_check": { + "name": "webhook_path_claim_generation_check", + "value": "\"webhook_path_claim\".\"generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "fork_sync_excluded": { + "name": "fork_sync_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_folder_id_fk": { + "name": "workflow_folder_id_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "error_enabled": { + "name": "error_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "retry": { + "name": "retry", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_operation": { + "name": "workflow_deployment_operation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_active_version_id": { + "name": "previous_active_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preparing'" + }, + "component_readiness": { + "name": "component_readiness", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_deployment_operation_workflow_generation_unique": { + "name": "workflow_deployment_operation_workflow_generation_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_idempotency_unique": { + "name": "workflow_deployment_operation_workflow_idempotency_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_in_flight_unique": { + "name": "workflow_deployment_operation_workflow_in_flight_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_status_idx": { + "name": "workflow_deployment_operation_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_deployment_version_idx": { + "name": "workflow_deployment_operation_deployment_version_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_version_generation_idx": { + "name": "workflow_deployment_operation_workflow_version_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_operation_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_operation_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["previous_active_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_deployment_operation_action_check": { + "name": "workflow_deployment_operation_action_check", + "value": "\"workflow_deployment_operation\".\"action\" IN ('deploy', 'activate')" + }, + "workflow_deployment_operation_status_check": { + "name": "workflow_deployment_operation_status_check", + "value": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating', 'active', 'failed', 'superseded')" + }, + "workflow_deployment_operation_generation_check": { + "name": "workflow_deployment_operation_generation_check", + "value": "\"workflow_deployment_operation\".\"generation\" > 0" + }, + "workflow_deployment_operation_protocol_version_check": { + "name": "workflow_deployment_operation_protocol_version_check", + "value": "\"workflow_deployment_operation\".\"protocol_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "execution_deadline_at": { + "name": "execution_deadline_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_deadline_idx": { + "name": "workflow_execution_logs_running_deadline_idx", + "columns": [ + { + "expression": "execution_deadline_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'running' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_completed_ended_at_idx": { + "name": "workflow_execution_logs_completed_ended_at_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'completed' AND \"workflow_execution_logs\".\"level\" = 'info' AND \"workflow_execution_logs\".\"ended_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "parameter_description_overrides": { + "name": "parameter_description_overrides", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_operation_id": { + "name": "deployment_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_scope": { + "name": "secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "mounted_secrets": { + "name": "mounted_secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk": { + "name": "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_operation", + "columnsFrom": ["deployment_operation_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_secret_scope": { + "name": "inbox_secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "inbox_mounted_secrets": { + "name": "inbox_mounted_secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "organization_assigned_at": { + "name": "organization_assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "forked_from_workspace_id": { + "name": "forked_from_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_forked_from_workspace_id_idx": { + "name": "workspace_forked_from_workspace_id_idx", + "columns": [ + { + "expression": "forked_from_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_inbox_provider_id_idx": { + "name": "workspace_inbox_provider_id_idx", + "columns": [ + { + "expression": "inbox_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace\".\"inbox_provider_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_forked_from_workspace_id_workspace_id_fk": { + "name": "workspace_forked_from_workspace_id_workspace_id_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": ["forked_from_workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_storage_used_bytes_non_negative": { + "name": "workspace_storage_used_bytes_non_negative", + "value": "\"workspace\".\"storage_used_bytes\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_collab_state": { + "name": "workspace_file_collab_state", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "doc_state": { + "name": "doc_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_file_collab_state_file_id_workspace_files_id_fk": { + "name": "workspace_file_collab_state_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_collab_state", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_secret_provenance": { + "name": "workspace_file_secret_provenance", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_file_secret_provenance_file_id_workspace_files_id_fk": { + "name": "workspace_file_secret_provenance_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_secret_provenance", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_file_secret_provenance_status_check": { + "name": "workspace_file_secret_provenance_status_check", + "value": "\"workspace_file_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_folder_id_fk": { + "name": "workspace_files_folder_id_folder_id_fk", + "tableFrom": "workspace_files", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_block_map": { + "name": "workspace_fork_block_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_workflow_id": { + "name": "parent_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_block_id": { + "name": "parent_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_workflow_id": { + "name": "child_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_block_id": { + "name": "child_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_block_map_child_ws_parent_unique": { + "name": "workspace_fork_block_map_child_ws_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_unique": { + "name": "workspace_fork_block_map_child_ws_child_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_parent_wf_idx": { + "name": "workspace_fork_block_map_child_ws_parent_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_wf_idx": { + "name": "workspace_fork_block_map_child_ws_child_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_block_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_block_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_dependent_value": { + "name": "workspace_fork_dependent_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sub_block_key": { + "name": "sub_block_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_dependent_value_child_ws_wf_idx": { + "name": "workspace_fork_dependent_value_child_ws_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_dependent_value_field_unique": { + "name": "workspace_fork_dependent_value_field_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sub_block_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_dependent_value", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_promote_run": { + "name": "workspace_fork_promote_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workspace_id": { + "name": "target_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "workspace_fork_promote_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_promote_run_child_ws_target_unique": { + "name": "workspace_fork_promote_run_child_ws_target_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_promote_run_target_ws_idx": { + "name": "workspace_fork_promote_run_target_ws_idx", + "columns": [ + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_promote_run_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_promote_run_created_by_user_id_fk": { + "name": "workspace_fork_promote_run_created_by_user_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_resource_map": { + "name": "workspace_fork_resource_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "workspace_fork_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_resource_id": { + "name": "parent_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_resource_id": { + "name": "child_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_resource_map_child_ws_idx": { + "name": "workspace_fork_resource_map_child_ws_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_ws_type_idx": { + "name": "workspace_fork_resource_map_child_ws_type_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_type_parent_unique": { + "name": "workspace_fork_resource_map_child_type_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_resource_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_resource_map_created_by_user_id_fk": { + "name": "workspace_fork_resource_map_created_by_user_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_sandbox": { + "name": "workspace_sandbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "sandbox_language", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "dependencies": { + "name": "dependencies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "cli_tools": { + "name": "cli_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "system_packages": { + "name": "system_packages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "spec_hash": { + "name": "spec_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_sandbox_workspace_name_unique": { + "name": "workspace_sandbox_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_sandbox_workspace_idx": { + "name": "workspace_sandbox_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_sandbox_spec_hash_idx": { + "name": "workspace_sandbox_spec_hash_idx", + "columns": [ + { + "expression": "spec_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_sandbox_workspace_id_workspace_id_fk": { + "name": "workspace_sandbox_workspace_id_workspace_id_fk", + "tableFrom": "workspace_sandbox", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_sandbox_created_by_user_id_fk": { + "name": "workspace_sandbox_created_by_user_id_fk", + "tableFrom": "workspace_sandbox", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.background_work_kind": { + "name": "background_work_kind", + "schema": "public", + "values": ["deployment_side_effects", "fork_content_copy", "fork_sync", "fork_rollback"] + }, + "public.background_work_status_value": { + "name": "background_work_status_value", + "schema": "public", + "values": ["pending", "processing", "completed", "completed_with_warnings", "failed"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.copilot_tool_permission_decision": { + "name": "copilot_tool_permission_decision", + "schema": "public", + "values": ["allow", "allow_chat", "always_allow", "skip"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "service_account"] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.folder_resource_type": { + "name": "folder_resource_type", + "schema": "public", + "values": ["workflow", "file", "knowledge_base", "table"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.sandbox_image_status": { + "name": "sandbox_image_status", + "schema": "public", + "values": ["pending", "building", "ready", "failed"] + }, + "public.sandbox_language": { + "name": "sandbox_language", + "schema": "public", + "values": ["javascript", "python"] + }, + "public.upload_session_method": { + "name": "upload_session_method", + "schema": "public", + "values": ["put", "multipart"] + }, + "public.upload_session_provider": { + "name": "upload_session_provider", + "schema": "public", + "values": ["local", "s3", "blob", "gcs"] + }, + "public.upload_session_purpose": { + "name": "upload_session_purpose", + "schema": "public", + "values": [ + "workspace_file", + "table_import", + "knowledge_document", + "profile_picture", + "workspace_logo", + "mothership_attachment", + "execution_attachment" + ] + }, + "public.upload_session_status": { + "name": "upload_session_status", + "schema": "public", + "values": [ + "uploading", + "completing", + "finalizing", + "completed", + "aborting", + "aborted", + "failed", + "expired" + ] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment", + "voice-output" + ] + }, + "public.workspace_fork_promote_direction": { + "name": "workspace_fork_promote_direction", + "schema": "public", + "values": ["push", "pull"] + }, + "public.workspace_fork_resource_type": { + "name": "workspace_fork_resource_type", + "schema": "public", + "values": [ + "workflow", + "oauth_credential", + "service_account_credential", + "env_var", + "table", + "knowledge_base", + "knowledge_document", + "file", + "mcp_server", + "workflow_mcp_server", + "custom_tool", + "skill" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 6e565f3b078..edc5013aa0a 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -2024,6 +2024,13 @@ "when": 1786438204549, "tag": "0289_warm_malice", "breakpoints": true + }, + { + "idx": 290, + "version": "7", + "when": 1786656136687, + "tag": "0290_workable_jigsaw", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index a000b0e81eb..daeb17efaab 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -3679,6 +3679,16 @@ export const usageLog = pgTable( table.cost ) .where(sql`${table.billingEntityType} IS NOT NULL`), + billingEntityCreatedAtCostIdx: index('usage_log_billing_entity_created_at_cost_idx') + .on( + table.billingEntityType, + table.billingEntityId, + table.createdAt, + table.userId, + table.source, + table.cost + ) + .where(sql`${table.billingEntityType} IS NOT NULL`), billingScopeAllOrNone: check( 'usage_log_billing_scope_all_or_none', sql`( diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 58393603987..9cad9e0e847 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 1105, - zodRoutes: 1105, + totalRoutes: 1110, + zodRoutes: 1110, nonZodRoutes: 0, } as const From 9027a6b13e1178ec06338677a07f371862b8ce56 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 18 Aug 2026 18:04:10 -0700 Subject: [PATCH 2/4] fix(billing): harden enterprise reporting flow --- apps/sim/app/api/billing/route.ts | 14 +- .../api/organizations/[id]/members/route.ts | 75 ++++--- apps/sim/app/api/usage/route.ts | 13 +- .../admin/organizations/[id]/billing/route.ts | 8 +- .../team-management/team-management.tsx | 2 +- .../lib/admin/dashboard-organizations.test.ts | 1 - apps/sim/lib/admin/dashboard.ts | 62 ++---- apps/sim/lib/api/contracts/organization.ts | 10 +- .../lib/api/contracts/subscription.test.ts | 11 +- apps/sim/lib/api/contracts/subscription.ts | 11 + .../api/contracts/v1/admin/dashboard.test.ts | 32 +-- .../lib/api/contracts/v1/admin/dashboard.ts | 47 +---- .../calculations/usage-monitor.test.ts | 61 +++++- .../lib/billing/calculations/usage-monitor.ts | 17 +- apps/sim/lib/billing/core/organization.ts | 193 +++++++++++++----- apps/sim/lib/billing/core/reporting-period.ts | 8 +- apps/sim/lib/billing/credits/daily-refresh.ts | 61 +++++- apps/sim/lib/billing/enterprise-outbox.ts | 29 ++- .../billing/enterprise-provisioning.test.ts | 149 ++++++++++++++ .../lib/billing/enterprise-provisioning.ts | 164 ++++++++++++--- .../lib/billing/webhooks/enterprise.test.ts | 99 +++++++-- apps/sim/lib/billing/webhooks/enterprise.ts | 90 +++++--- apps/sim/lib/core/outbox/service.test.ts | 26 +++ apps/sim/lib/core/outbox/service.ts | 25 +++ 24 files changed, 920 insertions(+), 288 deletions(-) diff --git a/apps/sim/app/api/billing/route.ts b/apps/sim/app/api/billing/route.ts index 4a34e511f5d..ff0b352af2f 100644 --- a/apps/sim/app/api/billing/route.ts +++ b/apps/sim/app/api/billing/route.ts @@ -117,7 +117,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { ) if (!parsed.success) return parsed.response - const { context, id: contextId, includeOrg } = parsed.data.query + const { context, id: contextId, includeOrg, memberLimit, memberOffset } = parsed.data.query if (context === 'organization' && !contextId) { return NextResponse.json( { error: 'Organization ID is required when context=organization' }, @@ -190,7 +190,10 @@ export const GET = withRouteHandler(async (request: NextRequest) => { billingStatus, upgradeWorkspaceId, ] = await Promise.all([ - getOrganizationBillingData(organizationId, dbReplica), + getOrganizationBillingData(organizationId, dbReplica, { + limit: memberLimit, + offset: memberOffset, + }), getOrganizationSubscription(organizationId, { executor: dbReplica, onError: 'throw' }), dbReplica .select({ id: organizationTable.id, name: organizationTable.name }) @@ -254,6 +257,13 @@ export const GET = withRouteHandler(async (request: NextRequest) => { rawBillingData?.billingPeriodEnd?.toISOString() ?? displayedSubscription?.periodEnd?.toISOString() ?? null, + membersTotal: rawBillingData?.membersTotal ?? 0, + memberPagination: rawBillingData?.memberPagination ?? { + total: 0, + limit: memberLimit, + offset: memberOffset, + hasMore: false, + }, members: rawBillingData?.members.map((organizationMember) => ({ ...organizationMember, diff --git a/apps/sim/app/api/organizations/[id]/members/route.ts b/apps/sim/app/api/organizations/[id]/members/route.ts index c03740f35c4..9f146e1b9b8 100644 --- a/apps/sim/app/api/organizations/[id]/members/route.ts +++ b/apps/sim/app/api/organizations/[id]/members/route.ts @@ -2,7 +2,7 @@ import { db } from '@sim/db' import { member, user, userStats } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { isOrgAdminRole } from '@sim/platform-authz/workspace' -import { and, eq } from 'drizzle-orm' +import { and, count, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { organizationMemberQuerySchema, @@ -46,6 +46,7 @@ export const GET = withRouteHandler( { status: 400 } ) } + const { limit, offset } = queryResult.data const includeUsage = queryResult.data.include === 'usage' // Verify user has access to this organization @@ -66,7 +67,7 @@ export const GET = withRouteHandler( const hasAdminAccess = isOrgAdminRole(userRole) // Get organization members - const query = db + const memberPageQuery = db .select({ id: member.id, userId: member.userId, @@ -79,30 +80,44 @@ export const GET = withRouteHandler( .from(member) .innerJoin(user, eq(member.userId, user.id)) .where(eq(member.organizationId, organizationId)) + .orderBy(user.name, user.id) + .limit(limit) + .offset(offset) + + const totalQuery = db + .select({ value: count() }) + .from(member) + .where(eq(member.organizationId, organizationId)) // Include usage data if requested and user has admin access if (includeUsage && hasAdminAccess) { - const base = await db - .select({ - id: member.id, - userId: member.userId, - organizationId: member.organizationId, - role: member.role, - createdAt: member.createdAt, - userName: user.name, - userEmail: user.email, - currentPeriodCost: userStats.currentPeriodCost, - currentUsageLimit: userStats.currentUsageLimit, - usageLimitUpdatedAt: userStats.usageLimitUpdatedAt, - }) - .from(member) - .innerJoin(user, eq(member.userId, user.id)) - .leftJoin(userStats, eq(user.id, userStats.userId)) - .where(eq(member.organizationId, organizationId)) + const [base, totalRows] = await Promise.all([ + db + .select({ + id: member.id, + userId: member.userId, + organizationId: member.organizationId, + role: member.role, + createdAt: member.createdAt, + userName: user.name, + userEmail: user.email, + currentPeriodCost: userStats.currentPeriodCost, + currentUsageLimit: userStats.currentUsageLimit, + usageLimitUpdatedAt: userStats.usageLimitUpdatedAt, + }) + .from(member) + .innerJoin(user, eq(member.userId, user.id)) + .leftJoin(userStats, eq(user.id, userStats.userId)) + .where(eq(member.organizationId, organizationId)) + .orderBy(user.name, user.id) + .limit(limit) + .offset(offset), + totalQuery, + ]) const { billingPeriod, includeLegacyBaseline, usageByUser } = await getOrganizationMemberUsageSnapshot(organizationId, { - userIds: base.length <= 1_000 ? base.map((row) => row.userId) : undefined, + userIds: base.map((row) => row.userId), }) const billingPeriodStart = billingPeriod?.start ?? null const billingPeriodEnd = billingPeriod?.end ?? null @@ -117,21 +132,35 @@ export const GET = withRouteHandler( billingPeriodEnd, })) + const total = totalRows[0]?.value ?? 0 return NextResponse.json({ success: true, data: membersWithUsage, - total: membersWithUsage.length, + total, + pagination: { + total, + limit, + offset, + hasMore: offset + membersWithUsage.length < total, + }, userRole, hasAdminAccess, }) } - const members = await query + const [members, totalRows] = await Promise.all([memberPageQuery, totalQuery]) + const total = totalRows[0]?.value ?? 0 return NextResponse.json({ success: true, data: members, - total: members.length, + total, + pagination: { + total, + limit, + offset, + hasMore: offset + members.length < total, + }, userRole, hasAdminAccess, }) diff --git a/apps/sim/app/api/usage/route.ts b/apps/sim/app/api/usage/route.ts index 251e51bde5e..17025a00ec8 100644 --- a/apps/sim/app/api/usage/route.ts +++ b/apps/sim/app/api/usage/route.ts @@ -40,7 +40,13 @@ export const GET = withRouteHandler(async (request: NextRequest) => { ) if (!parsed.success) return parsed.response - const { context, userId = session.user.id, organizationId } = parsed.data.query + const { + context, + userId = session.user.id, + organizationId, + memberLimit, + memberOffset, + } = parsed.data.query if (context === 'user' && userId !== session.user.id) { return NextResponse.json( @@ -62,7 +68,10 @@ export const GET = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ error: 'Permission denied' }, { status: 403 }) } - const org = await getOrganizationBillingData(organizationId, dbReplica) + const org = await getOrganizationBillingData(organizationId, dbReplica, { + limit: memberLimit, + offset: memberOffset, + }) return NextResponse.json({ success: true, context, diff --git a/apps/sim/app/api/v1/admin/organizations/[id]/billing/route.ts b/apps/sim/app/api/v1/admin/organizations/[id]/billing/route.ts index 18bc485fbe6..6fca93b98d1 100644 --- a/apps/sim/app/api/v1/admin/organizations/[id]/billing/route.ts +++ b/apps/sim/app/api/v1/admin/organizations/[id]/billing/route.ts @@ -103,10 +103,6 @@ export const GET = withRouteHandler( return notFoundResponse('Organization or subscription') } - const membersOverLimit = billingData.members.filter((m) => m.isOverLimit).length - const membersNearLimit = billingData.members.filter( - (m) => !m.isOverLimit && m.percentUsed >= 80 - ).length const usagePercentage = billingData.totalUsageLimit > 0 ? Math.round((billingData.totalCurrentUsage / billingData.totalUsageLimit) * 10000) / 100 @@ -127,8 +123,8 @@ export const GET = withRouteHandler( usagePercentage, billingPeriodStart: billingData.billingPeriodStart?.toISOString() ?? null, billingPeriodEnd: billingData.billingPeriodEnd?.toISOString() ?? null, - membersOverLimit, - membersNearLimit, + membersOverLimit: billingData.membersOverLimit, + membersNearLimit: billingData.membersNearLimit, } logger.info(`Admin API: Retrieved billing summary for organization ${organizationId}`) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/team-management.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/team-management.tsx index 53883488cdf..4dd499d7e9c 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/team-management.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/team-management.tsx @@ -96,7 +96,7 @@ export function TeamManagement({ ] const totalSeats = organizationBillingData?.data?.totalSeats ?? 0 - const usedSeats = organizationBillingData?.data?.members?.length ?? 0 + const usedSeats = organizationBillingData?.data?.membersTotal ?? 0 const reservedSeats = organizationBillingData?.data?.usedSeats ?? 0 const pendingSeats = Math.max(0, reservedSeats - usedSeats) diff --git a/apps/sim/lib/admin/dashboard-organizations.test.ts b/apps/sim/lib/admin/dashboard-organizations.test.ts index 2c0414d0644..be2b9edc2e9 100644 --- a/apps/sim/lib/admin/dashboard-organizations.test.ts +++ b/apps/sim/lib/admin/dashboard-organizations.test.ts @@ -206,7 +206,6 @@ describe('getDashboardOrganization', () => { queueTableRows(workspace, [{ value: 3 }]) const result = await getDashboardOrganization('org-1', { - paginated: true, limit: 2, memberOffset: 0, externalCollaboratorOffset: 0, diff --git a/apps/sim/lib/admin/dashboard.ts b/apps/sim/lib/admin/dashboard.ts index 28ea175c8d0..9fec0bf7a1d 100644 --- a/apps/sim/lib/admin/dashboard.ts +++ b/apps/sim/lib/admin/dashboard.ts @@ -390,7 +390,6 @@ export function toDashboardProvisioning(view: EnterpriseProvisioningView) { const { usageLimitCredits, ...rest } = view return { ...rest, - monthlyInvoiceAmountUsd: view.billingInterval === 'month' ? view.invoiceAmountUsd : null, usageLimitDollars: creditsToDollars(usageLimitCredits), } } @@ -454,14 +453,6 @@ function buildDashboardOrganizationSummary({ ? invoiceAmountCents / 100 : (monthlyPrice ?? null) : (teamEconomics?.monthlyInvoiceAmountUsd ?? null), - monthlyInvoiceAmountUsd: - latestSubscription?.plan === 'enterprise' - ? latestSubscription.billingInterval === 'year' - ? null - : invoiceAmountCents !== null - ? invoiceAmountCents / 100 - : (monthlyPrice ?? null) - : (teamEconomics?.monthlyInvoiceAmountUsd ?? null), billingInterval: latestSubscription?.billingInterval === 'year' || latestSubscription?.billingInterval === 'month' @@ -855,13 +846,11 @@ export async function listDashboardOrganizations({ search, limit, offset }: Pagi export async function getDashboardOrganization( organizationId: string, pagination: { - paginated: boolean limit: number memberOffset: number externalCollaboratorOffset: number workspaceOffset: number } = { - paginated: false, limit: 50, memberOffset: 0, externalCollaboratorOffset: 0, @@ -918,23 +907,15 @@ export async function getDashboardOrganization( const [memberRows, externalRows, workspaceRows, workspaceCountRows, configurationIntent] = await Promise.all([ - pagination.paginated - ? memberQuery().limit(pagination.limit).offset(pagination.memberOffset) - : memberQuery(), - pagination.paginated - ? externalCollaboratorQuery() - .limit(pagination.limit) - .offset(pagination.externalCollaboratorOffset) - : externalCollaboratorQuery(), - pagination.paginated - ? workspaceQuery().limit(pagination.limit).offset(pagination.workspaceOffset) - : workspaceQuery(), - pagination.paginated - ? db - .select({ value: count() }) - .from(workspace) - .where(eq(workspace.organizationId, organizationId)) - : Promise.resolve([]), + memberQuery().limit(pagination.limit).offset(pagination.memberOffset), + externalCollaboratorQuery() + .limit(pagination.limit) + .offset(pagination.externalCollaboratorOffset), + workspaceQuery().limit(pagination.limit).offset(pagination.workspaceOffset), + db + .select({ value: count() }) + .from(workspace) + .where(eq(workspace.organizationId, organizationId)), subscriptionRow?.plan === 'enterprise' ? resolveEnterpriseMetadataIntent(db, subscriptionRow.id, subscriptionRow.metadata) : Promise.resolve(null), @@ -964,9 +945,7 @@ export async function getDashboardOrganization( ]) const limits = new Map(limitRows.map((row) => [row.userId, Number(row.limit)])) const usageByUser = usageByOrganization.get(organizationId)?.byUser ?? new Map() - const workspaceTotal = pagination.paginated - ? (workspaceCountRows[0]?.value ?? 0) - : workspaceRows.length + const workspaceTotal = workspaceCountRows[0]?.value ?? 0 return { ...base, configurationUpdate: toDashboardConfigurationUpdate( @@ -988,26 +967,23 @@ export async function getDashboardOrganization( workspaces: workspaceRows, memberPagination: { total: base.memberCount, - limit: pagination.paginated ? pagination.limit : memberRows.length, - offset: pagination.paginated ? pagination.memberOffset : 0, - hasMore: - pagination.paginated && pagination.memberOffset + memberRows.length < base.memberCount, + limit: pagination.limit, + offset: pagination.memberOffset, + hasMore: pagination.memberOffset + memberRows.length < base.memberCount, }, externalCollaboratorPagination: { total: base.externalCollaboratorCount, - limit: pagination.paginated ? pagination.limit : externalRows.length, - offset: pagination.paginated ? pagination.externalCollaboratorOffset : 0, + limit: pagination.limit, + offset: pagination.externalCollaboratorOffset, hasMore: - pagination.paginated && pagination.externalCollaboratorOffset + externalRows.length < - base.externalCollaboratorCount, + base.externalCollaboratorCount, }, workspacePagination: { total: workspaceTotal, - limit: pagination.paginated ? pagination.limit : workspaceRows.length, - offset: pagination.paginated ? pagination.workspaceOffset : 0, - hasMore: - pagination.paginated && pagination.workspaceOffset + workspaceRows.length < workspaceTotal, + limit: pagination.limit, + offset: pagination.workspaceOffset, + hasMore: pagination.workspaceOffset + workspaceRows.length < workspaceTotal, }, subscription: subscriptionRow ? { diff --git a/apps/sim/lib/api/contracts/organization.ts b/apps/sim/lib/api/contracts/organization.ts index bbd1fcf69ce..1a3ee14895b 100644 --- a/apps/sim/lib/api/contracts/organization.ts +++ b/apps/sim/lib/api/contracts/organization.ts @@ -29,7 +29,9 @@ export const organizationMemberParamsSchema = z.object({ export const organizationMemberQuerySchema = z .object({ - include: z.string().optional(), + include: z.enum(['usage']).optional(), + limit: z.coerce.number().int().min(1).max(100).default(50), + offset: z.coerce.number().int().min(0).default(0), }) .passthrough() @@ -320,6 +322,12 @@ export const listOrganizationMembersResponseSchema = z success: z.boolean(), data: z.array(organizationMemberUsageSchema), total: z.number(), + pagination: z.object({ + total: z.number().int().min(0), + limit: z.number().int().min(1).max(100), + offset: z.number().int().min(0), + hasMore: z.boolean(), + }), userRole: organizationRoleSchema, hasAdminAccess: z.boolean(), }) diff --git a/apps/sim/lib/api/contracts/subscription.test.ts b/apps/sim/lib/api/contracts/subscription.test.ts index 05f378250ec..8b6a2ca90cc 100644 --- a/apps/sim/lib/api/contracts/subscription.test.ts +++ b/apps/sim/lib/api/contracts/subscription.test.ts @@ -68,6 +68,13 @@ const ORGANIZATION_BILLING_DATA = { averageUsagePerMember: 10.5, billingPeriodStart: '2026-07-01T00:00:00.000Z', billingPeriodEnd: '2026-08-01T00:00:00.000Z', + membersTotal: 2, + memberPagination: { + total: 2, + limit: 50, + offset: 0, + hasMore: false, + }, members: [], billingBlocked: true, billingBlockedReason: 'payment_failed', @@ -87,7 +94,7 @@ describe('subscription billing contracts', () => { expect(subscriptionBillingDataSchema.safeParse(withoutUpgradeTarget).success).toBe(false) }) - it('requires target organization credits, interval, cancellation, and blocked status', () => { + it('requires target organization billing state and member pagination', () => { expect(organizationBillingDataSchema.safeParse(ORGANIZATION_BILLING_DATA).success).toBe(true) for (const field of [ @@ -95,6 +102,8 @@ describe('subscription billing contracts', () => { 'billingInterval', 'cancelAtPeriodEnd', 'billingBlocked', + 'membersTotal', + 'memberPagination', ] as const) { const incomplete = { ...ORGANIZATION_BILLING_DATA } delete incomplete[field] diff --git a/apps/sim/lib/api/contracts/subscription.ts b/apps/sim/lib/api/contracts/subscription.ts index 8d9831c46b8..9f4c3884b47 100644 --- a/apps/sim/lib/api/contracts/subscription.ts +++ b/apps/sim/lib/api/contracts/subscription.ts @@ -63,6 +63,8 @@ export const billingQuerySchema = z.object({ context: z.enum(['user', 'organization']).optional().default('user'), id: z.string().min(1).optional(), includeOrg: booleanQueryParamSchema, + memberLimit: z.coerce.number().int().min(1).max(100).default(50), + memberOffset: z.coerce.number().int().min(0).default(0), }) export const billingUsageDataSchema = z @@ -157,6 +159,13 @@ export const organizationBillingDataSchema = z averageUsagePerMember: z.number(), billingPeriodStart: z.string().nullable(), billingPeriodEnd: z.string().nullable(), + membersTotal: z.number().int().min(0), + memberPagination: z.object({ + total: z.number().int().min(0), + limit: z.number().int().min(1).max(100), + offset: z.number().int().min(0), + hasMore: z.boolean(), + }), members: z.array(organizationBillingMemberSchema), billingBlocked: z.boolean(), billingBlockedReason: z.enum(['payment_failed', 'dispute']).nullable(), @@ -193,6 +202,8 @@ export const usageQuerySchema = z.object({ context: z.enum(['user', 'organization']).optional().default('user'), userId: z.string().optional(), organizationId: z.string().optional(), + memberLimit: z.coerce.number().int().min(1).max(100).default(50), + memberOffset: z.coerce.number().int().min(0).default(0), }) export const updateUsageLimitBodySchema = z diff --git a/apps/sim/lib/api/contracts/v1/admin/dashboard.test.ts b/apps/sim/lib/api/contracts/v1/admin/dashboard.test.ts index 80042a87304..7f4919e6612 100644 --- a/apps/sim/lib/api/contracts/v1/admin/dashboard.test.ts +++ b/apps/sim/lib/api/contracts/v1/admin/dashboard.test.ts @@ -67,7 +67,6 @@ describe('admin dashboard credit grant contract', () => { effectiveUsageLimitDollars: 0.001, prepaidBalanceDollars: 0.001, invoiceAmountUsd: null, - monthlyInvoiceAmountUsd: null, billingInterval: null, reportingPeriod: { anchorDate: null, @@ -114,37 +113,11 @@ describe('admin dashboard credit grant contract', () => { expect(monthly.billingInterval).toBe('month') }) - it('keeps the legacy monthly issuance request monthly during a rolling deployment', () => { - const legacy = adminDashboardIssueEnterpriseBodySchema.parse({ - ownerUserId: 'owner-1', - monthlyInvoiceAmountUsd: 100, - seats: 10, - }) - - expect(legacy).toMatchObject({ - billingInterval: 'month', - invoiceAmountUsd: 100, - }) - expect(legacy).not.toHaveProperty('monthlyInvoiceAmountUsd') - }) - - it('rejects conflicting legacy and interval invoice amounts', () => { - expect( - adminDashboardIssueEnterpriseBodySchema.safeParse({ - ownerUserId: 'owner-1', - invoiceAmountUsd: 1_200, - monthlyInvoiceAmountUsd: 100, - seats: 10, - }).success - ).toBe(false) - }) - - it('rejects a monthly-named legacy amount with an explicit annual cadence', () => { + it('rejects the removed monthly-named invoice field', () => { expect( adminDashboardIssueEnterpriseBodySchema.safeParse({ ownerUserId: 'owner-1', monthlyInvoiceAmountUsd: 100, - billingInterval: 'year', seats: 10, }).success ).toBe(false) @@ -183,7 +156,6 @@ describe('admin dashboard credit grant contract', () => { it('keeps organization detail unbounded for legacy callers and supports bounded collection pages', () => { expect(adminDashboardOrganizationDetailQuerySchema.parse({})).toEqual({ - paginated: false, limit: 50, memberOffset: 0, externalCollaboratorOffset: 0, @@ -191,14 +163,12 @@ describe('admin dashboard credit grant contract', () => { }) expect( adminDashboardOrganizationDetailQuerySchema.parse({ - paginated: 'true', limit: '25', memberOffset: '50', externalCollaboratorOffset: '75', workspaceOffset: '100', }) ).toEqual({ - paginated: true, limit: 25, memberOffset: 50, externalCollaboratorOffset: 75, diff --git a/apps/sim/lib/api/contracts/v1/admin/dashboard.ts b/apps/sim/lib/api/contracts/v1/admin/dashboard.ts index 33aa3b9a9d1..4745c0a12b2 100644 --- a/apps/sim/lib/api/contracts/v1/admin/dashboard.ts +++ b/apps/sim/lib/api/contracts/v1/admin/dashboard.ts @@ -1,7 +1,6 @@ import { z } from 'zod' import { type ContractJsonResponse, defineRouteContract } from '@/lib/api/contracts/types' import { - adminV1BooleanQuerySchema, adminV1IdParamsSchema, adminV1ListResponseSchema, adminV1PaginationMetaSchema, @@ -67,7 +66,6 @@ export const adminDashboardProvisioningSchema = z.object({ organizationId: z.string(), status: z.enum(['pending', 'processing', 'dead_letter', 'awaiting_webhook', 'applied']), invoiceAmountUsd: z.number(), - monthlyInvoiceAmountUsd: z.number().nullable(), billingInterval: adminDashboardBillingIntervalSchema, reportingPeriodAnchorDate: z.string().nullable(), usageLimitDollars: creditAlignedDollarAmountSchema, @@ -109,7 +107,6 @@ export const adminDashboardOrganizationSummarySchema = z.object({ effectiveUsageLimitDollars: dollarAmountSchema, prepaidBalanceDollars: dollarAmountSchema, invoiceAmountUsd: z.number().nullable(), - monthlyInvoiceAmountUsd: z.number().nullable(), billingInterval: adminDashboardBillingIntervalSchema.nullable(), reportingPeriod: adminDashboardReportingPeriodSchema, usage: adminDashboardUsageSchema, @@ -190,7 +187,6 @@ export const adminDashboardSearchQuerySchema = adminV1PaginationQuerySchema.exte }) export const adminDashboardOrganizationDetailQuerySchema = z.object({ - paginated: adminV1BooleanQuerySchema, limit: adminV1PaginationQuerySchema.shape.limit.default(50), memberOffset: adminV1PaginationQuerySchema.shape.offset.default(0), externalCollaboratorOffset: adminV1PaginationQuerySchema.shape.offset.default(0), @@ -201,9 +197,8 @@ export const adminDashboardIssueEnterpriseBodySchema = z .object({ ownerUserId: z.string().min(1), organizationName: z.string().trim().min(1).max(120).optional(), - invoiceAmountUsd: adminDashboardInvoiceAmountSchema.optional(), - monthlyInvoiceAmountUsd: adminDashboardInvoiceAmountSchema.optional(), - billingInterval: adminDashboardBillingIntervalSchema.optional(), + invoiceAmountUsd: adminDashboardInvoiceAmountSchema, + billingInterval: adminDashboardBillingIntervalSchema.default('year'), reportingPeriodAnchorDate: adminDashboardDateOnlySchema.optional(), workspaceIds: z.array(z.string().min(1)).max(1_000).default([]), usageLimitDollars: creditAlignedDollarAmountSchema.optional(), @@ -217,43 +212,7 @@ export const adminDashboardIssueEnterpriseBodySchema = z .optional(), pausePaymentCollection: z.boolean().optional(), }) - .superRefine((body, context) => { - if (body.invoiceAmountUsd === undefined && body.monthlyInvoiceAmountUsd === undefined) { - context.addIssue({ - code: z.ZodIssueCode.custom, - message: 'Invoice amount is required', - path: ['invoiceAmountUsd'], - }) - } - if ( - body.invoiceAmountUsd !== undefined && - body.monthlyInvoiceAmountUsd !== undefined && - body.invoiceAmountUsd !== body.monthlyInvoiceAmountUsd - ) { - context.addIssue({ - code: z.ZodIssueCode.custom, - message: 'Legacy and interval invoice amounts must match when both are provided', - path: ['monthlyInvoiceAmountUsd'], - }) - } - if ( - body.invoiceAmountUsd === undefined && - body.monthlyInvoiceAmountUsd !== undefined && - body.billingInterval === 'year' - ) { - context.addIssue({ - code: z.ZodIssueCode.custom, - message: 'Annual cadence requires the interval-neutral invoiceAmountUsd field', - path: ['invoiceAmountUsd'], - }) - } - }) - .transform(({ monthlyInvoiceAmountUsd, ...body }) => ({ - ...body, - invoiceAmountUsd: body.invoiceAmountUsd ?? (monthlyInvoiceAmountUsd as number), - billingInterval: - body.billingInterval ?? (body.invoiceAmountUsd === undefined ? ('month' as const) : 'year'), - })) + .strict() export const adminDashboardSeatsBodySchema = z.object({ seats: z.number().int().positive().max(100_000), diff --git a/apps/sim/lib/billing/calculations/usage-monitor.test.ts b/apps/sim/lib/billing/calculations/usage-monitor.test.ts index ef47567b436..dcce6884ac6 100644 --- a/apps/sim/lib/billing/calculations/usage-monitor.test.ts +++ b/apps/sim/lib/billing/calculations/usage-monitor.test.ts @@ -5,12 +5,18 @@ import { dbChainMockFns, resetDbChainMock, resetEnvFlagsMock, setEnvFlags } from import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' const { + mockGetBillingPeriodUsageCost, mockGetOrgMemberUsageForBillingPeriod, mockGetOrgMemberUsageLimit, + mockGetPooledOrgCurrentPeriodCost, + mockGetUserUsageLimit, mockIsOrganizationBillingBlocked, } = vi.hoisted(() => ({ + mockGetBillingPeriodUsageCost: vi.fn(), mockGetOrgMemberUsageForBillingPeriod: vi.fn(), mockGetOrgMemberUsageLimit: vi.fn(), + mockGetPooledOrgCurrentPeriodCost: vi.fn(), + mockGetUserUsageLimit: vi.fn(), mockIsOrganizationBillingBlocked: vi.fn(), })) @@ -26,14 +32,19 @@ vi.mock('@/lib/billing/core/access', () => ({ // core/usage pulls in the email-rendering chain at import; stub the two symbols // usage-monitor imports from it so the module loads in a node test env. vi.mock('@/lib/billing/core/usage', () => ({ - getPooledOrgCurrentPeriodCost: vi.fn(), - getUserUsageLimit: vi.fn(), + getPooledOrgCurrentPeriodCost: mockGetPooledOrgCurrentPeriodCost, + getUserUsageLimit: mockGetUserUsageLimit, +})) + +vi.mock('@/lib/billing/core/usage-log', () => ({ + getBillingPeriodUsageCost: mockGetBillingPeriodUsageCost, })) import { checkBillingBlocked, checkBillingEntityBlocked, checkOrganizationMemberUsageLimit, + checkUsageStatus, } from '@/lib/billing/calculations/usage-monitor' afterAll(() => { @@ -42,6 +53,52 @@ afterAll(() => { afterAll(resetEnvFlagsMock) +describe('checkUsageStatus', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + setEnvFlags({ isHosted: true, isBillingEnabled: true }) + mockGetUserUsageLimit.mockResolvedValue(500) + mockGetBillingPeriodUsageCost.mockResolvedValue(125) + }) + + it('reads reporting-period organization usage without loading the member roster', async () => { + const billingPeriod = { + start: new Date('2026-01-01T00:00:00.000Z'), + end: new Date('2027-01-01T00:00:00.000Z'), + source: 'reporting' as const, + anchorDate: '2026-01-01', + interval: 'year' as const, + } + const subscription = { + referenceId: 'org-1', + plan: 'enterprise', + status: 'active', + seats: 1, + periodStart: billingPeriod.start, + periodEnd: billingPeriod.end, + } + + await expect( + checkUsageStatus('user-1', subscription, { + billingEntity: { type: 'organization', id: 'org-1' }, + billingPeriod, + }) + ).resolves.toMatchObject({ + currentUsage: 125, + limit: 500, + scope: 'organization', + organizationId: 'org-1', + }) + + expect(mockGetBillingPeriodUsageCost).toHaveBeenCalledWith( + { type: 'organization', id: 'org-1' }, + billingPeriod + ) + expect(mockGetPooledOrgCurrentPeriodCost).not.toHaveBeenCalled() + }) +}) + describe('checkBillingBlocked', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/apps/sim/lib/billing/calculations/usage-monitor.ts b/apps/sim/lib/billing/calculations/usage-monitor.ts index 547c1cd2b62..ab07a3a3409 100644 --- a/apps/sim/lib/billing/calculations/usage-monitor.ts +++ b/apps/sim/lib/billing/calculations/usage-monitor.ts @@ -57,9 +57,6 @@ async function computePooledOrgUsage( sub: UsageLimitSubscription, preloadedBillingPeriod?: UsageQueryPeriod ): Promise { - const { memberIds, currentPeriodCost } = await getPooledOrgCurrentPeriodCost(organizationId) - if (memberIds.length === 0) return 0 - const billingPeriod = preloadedBillingPeriod ?? resolveSubscriptionUsagePeriod(sub) ?? { ...defaultBillingPeriod(), @@ -72,12 +69,14 @@ async function computePooledOrgUsage( billingPeriod ) - return applyOrgRefresh( - organizationId, - sub, - (billingPeriod.source === 'reporting' ? 0 : currentPeriodCost) + ledgerUsage, - memberIds - ) + if (billingPeriod.source === 'reporting') { + return ledgerUsage + } + + const { memberIds, currentPeriodCost } = await getPooledOrgCurrentPeriodCost(organizationId) + if (memberIds.length === 0) return ledgerUsage + + return applyOrgRefresh(organizationId, sub, currentPeriodCost + ledgerUsage, memberIds) } /** diff --git a/apps/sim/lib/billing/core/organization.ts b/apps/sim/lib/billing/core/organization.ts index 441bff9577b..2fb2865ac2f 100644 --- a/apps/sim/lib/billing/core/organization.ts +++ b/apps/sim/lib/billing/core/organization.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' -import { member, organization, user, userStats } from '@sim/db/schema' +import { member, organization, usageLog, user, userStats } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { eq } from 'drizzle-orm' +import { and, count, eq, gte, lt, sql } from 'drizzle-orm' import { isOrganizationBillingBlocked } from '@/lib/billing/core/access' import { getOrganizationSubscription, getPlanPricing } from '@/lib/billing/core/billing' import { resolveSubscriptionUsagePeriod } from '@/lib/billing/core/reporting-period' @@ -10,10 +10,7 @@ import { getBillingPeriodUsageCostByUser, type UsageQueryPeriod, } from '@/lib/billing/core/usage-log' -import { - computeDailyRefreshConsumed, - getOrgMemberRefreshBounds, -} from '@/lib/billing/credits/daily-refresh' +import { computeOrganizationDailyRefreshConsumed } from '@/lib/billing/credits/daily-refresh' import { getPlanTierDollars, isEnterprise, isPaid } from '@/lib/billing/plan-helpers' import { getEffectiveSeats, @@ -45,6 +42,15 @@ interface OrganizationUsageData { averageUsagePerMember: number billingPeriodStart: Date | null billingPeriodEnd: Date | null + membersTotal: number + memberPagination: { + total: number + limit: number + offset: number + hasMore: boolean + } + membersOverLimit: number + membersNearLimit: number members: MemberUsageData[] } @@ -94,6 +100,68 @@ export interface OrganizationMemberUsageSnapshot { usageByUser: Map } +const DEFAULT_ORGANIZATION_BILLING_MEMBER_LIMIT = 50 +const MAX_ORGANIZATION_BILLING_MEMBER_LIMIT = 100 + +async function getOrganizationMemberUsageCounts( + organizationId: string, + billingPeriod: UsageQueryPeriod, + includeLegacyBaseline: boolean, + executor: DbClient +): Promise<{ overLimit: number; nearLimit: number }> { + const currentUsage = sql`( + ${includeLegacyBaseline ? sql`coalesce(${userStats.currentPeriodCost}, 0)` : sql`0`} + + coalesce(sum(${usageLog.cost}), 0) + )` + .mapWith(Number) + .as('current_usage') + const usageLimit = sql`coalesce(${userStats.currentUsageLimit}, ${getFreeTierLimit()})` + .mapWith(Number) + .as('usage_limit') + const perMemberUsage = executor + .select({ currentUsage, usageLimit }) + .from(member) + .leftJoin(userStats, eq(userStats.userId, member.userId)) + .leftJoin( + usageLog, + and( + eq(usageLog.userId, member.userId), + eq(usageLog.billingEntityType, 'organization'), + eq(usageLog.billingEntityId, organizationId), + ...(billingPeriod.source === 'reporting' + ? [ + gte(usageLog.createdAt, billingPeriod.start), + lt(usageLog.createdAt, billingPeriod.end), + ] + : [ + eq(usageLog.billingPeriodStart, billingPeriod.start), + eq(usageLog.billingPeriodEnd, billingPeriod.end), + ]) + ) + ) + .where(eq(member.organizationId, organizationId)) + .groupBy(member.userId, userStats.currentPeriodCost, userStats.currentUsageLimit) + .as('organization_member_usage') + + const [counts] = await executor + .select({ + overLimit: + sql`count(*) filter (where ${perMemberUsage.currentUsage} > ${perMemberUsage.usageLimit})`.mapWith( + Number + ), + nearLimit: + sql`count(*) filter (where ${perMemberUsage.usageLimit} > 0 and ${perMemberUsage.currentUsage} <= ${perMemberUsage.usageLimit} and ${perMemberUsage.currentUsage} / ${perMemberUsage.usageLimit} >= 0.8)`.mapWith( + Number + ), + }) + .from(perMemberUsage) + + return { + overLimit: counts?.overLimit ?? 0, + nearLimit: counts?.nearLimit ?? 0, + } +} + /** * Resolves the organization's usage period once and returns the ledger usage * for only the requested actors. Reporting periods never include the legacy @@ -123,7 +191,8 @@ export async function getOrganizationMemberUsageSnapshot( */ export async function getOrganizationBillingData( organizationId: string, - executor: DbClient = db + executor: DbClient = db, + memberPage: { limit?: number; offset?: number } = {} ): Promise { try { // Get organization info @@ -148,30 +217,45 @@ export async function getOrganizationBillingData( return null } - // Get all organization members with their usage data - const membersWithUsage = await executor - .select({ - userId: member.userId, - userName: user.name, - userEmail: user.email, - role: member.role, - joinedAt: member.createdAt, - // User stats fields - currentPeriodCost: userStats.currentPeriodCost, - currentUsageLimit: userStats.currentUsageLimit, - }) - .from(member) - .innerJoin(user, eq(member.userId, user.id)) - .leftJoin(userStats, eq(member.userId, userStats.userId)) - .where(eq(member.organizationId, organizationId)) - - // Per-member current-period usage = userStats baseline + attributed usage_log - // rows. currentPeriodCost is no longer incremented on the hot path, so the - // baseline alone under-reports; add each member's ledger sum for the period. - const { billingPeriod, includeLegacyBaseline, usageByUser } = - await getOrganizationMemberUsageSnapshot(organizationId, { executor }) - - // Process member data + const billingPeriod = resolveSubscriptionUsagePeriod(subscription) + const includeLegacyBaseline = billingPeriod?.source !== 'reporting' + const limit = Math.min( + MAX_ORGANIZATION_BILLING_MEMBER_LIMIT, + Math.max(1, memberPage.limit ?? DEFAULT_ORGANIZATION_BILLING_MEMBER_LIMIT) + ) + const offset = Math.max(0, memberPage.offset ?? 0) + const [memberAggregateRows, membersWithUsage] = await Promise.all([ + executor + .select({ + total: count(), + baseline: sql`coalesce(sum(${userStats.currentPeriodCost}), 0)`, + }) + .from(member) + .leftJoin(userStats, eq(member.userId, userStats.userId)) + .where(eq(member.organizationId, organizationId)), + executor + .select({ + userId: member.userId, + userName: user.name, + userEmail: user.email, + role: member.role, + joinedAt: member.createdAt, + currentPeriodCost: userStats.currentPeriodCost, + currentUsageLimit: userStats.currentUsageLimit, + }) + .from(member) + .innerJoin(user, eq(member.userId, user.id)) + .leftJoin(userStats, eq(member.userId, userStats.userId)) + .where(eq(member.organizationId, organizationId)) + .orderBy(user.name, user.id) + .limit(limit) + .offset(offset), + ]) + const memberIds = membersWithUsage.map((row) => row.userId) + const usageByUser = billingPeriod + ? await getOrgMemberLedgerByUser(organizationId, billingPeriod, executor, memberIds) + : new Map() + const members: MemberUsageData[] = membersWithUsage.map((memberRecord) => { const currentUsage = (includeLegacyBaseline ? Number(memberRecord.currentPeriodCost || 0) : 0) + @@ -192,15 +276,9 @@ export async function getOrganizationBillingData( } }) - // Authoritative org total = member baselines + the org's full usage_log for - // the period (also captures rows from members no longer present). Computed - // from raw baselines, NOT members[].currentUsage — the latter already folds - // in per-member usage_log for display, so summing it AND adding the org - // ledger would double-count. - let totalCurrentUsage = - billingPeriod?.source === 'reporting' - ? 0 - : membersWithUsage.reduce((sum, m) => sum + Number(m.currentPeriodCost || 0), 0) + const memberAggregate = memberAggregateRows[0] + const membersTotal = memberAggregate?.total ?? 0 + let totalCurrentUsage = includeLegacyBaseline ? Number(memberAggregate?.baseline ?? 0) : 0 if (billingPeriod) { totalCurrentUsage += await getBillingPeriodUsageCost( { type: 'organization', id: subscription.referenceId }, @@ -213,21 +291,13 @@ export async function getOrganizationBillingData( if (isPaid(subscription.plan) && subscription.periodStart) { const planDollars = getPlanTierDollars(subscription.plan) if (planDollars > 0) { - const memberIds = members.map((m) => m.userId) - const userBounds = await getOrgMemberRefreshBounds( - subscription.referenceId, - subscription.periodStart, - executor - ) - const refreshConsumed = await computeDailyRefreshConsumed( + const refreshConsumed = await computeOrganizationDailyRefreshConsumed( { - userIds: memberIds, + organizationId: subscription.referenceId, periodStart: subscription.periodStart, periodEnd: subscription.periodEnd ?? null, planDollars, seats: subscription.seats || 1, - userBounds: Object.keys(userBounds).length > 0 ? userBounds : undefined, - billingEntity: { type: 'organization', id: subscription.referenceId }, }, executor ) @@ -263,10 +333,18 @@ export async function getOrganizationBillingData( : minimumBillingAmount } - const averageUsagePerMember = members.length > 0 ? totalCurrentUsage / members.length : 0 + const averageUsagePerMember = membersTotal > 0 ? totalCurrentUsage / membersTotal : 0 const pendingSeats = await countPendingSeatInvitations(organizationId, executor) - const usedSeats = members.length + pendingSeats + const usedSeats = membersTotal + pendingSeats + const memberUsageCounts = billingPeriod + ? await getOrganizationMemberUsageCounts( + organizationId, + billingPeriod, + includeLegacyBaseline, + executor + ) + : { overLimit: 0, nearLimit: 0 } const billingPeriodStart = billingPeriod?.start ?? null const billingPeriodEnd = billingPeriod?.end ?? null @@ -285,7 +363,16 @@ export async function getOrganizationBillingData( averageUsagePerMember: roundCurrency(averageUsagePerMember), billingPeriodStart, billingPeriodEnd, - members: members.sort((a, b) => b.currentUsage - a.currentUsage), // Sort by usage desc + membersTotal, + memberPagination: { + total: membersTotal, + limit, + offset, + hasMore: offset + members.length < membersTotal, + }, + membersOverLimit: memberUsageCounts.overLimit, + membersNearLimit: memberUsageCounts.nearLimit, + members, } } catch (error) { logger.error('Failed to get organization billing data', { organizationId, error }) diff --git a/apps/sim/lib/billing/core/reporting-period.ts b/apps/sim/lib/billing/core/reporting-period.ts index bfaf004912c..cc5bfece04f 100644 --- a/apps/sim/lib/billing/core/reporting-period.ts +++ b/apps/sim/lib/billing/core/reporting-period.ts @@ -14,6 +14,12 @@ export interface ResolvedUsagePeriod { interval: BillingInterval | null } +export interface ResolvedEnterpriseReportingPeriod extends ResolvedUsagePeriod { + source: 'reporting' + anchorDate: string + interval: BillingInterval +} + interface SubscriptionPeriodInput { plan?: string | null billingInterval?: string | null @@ -46,7 +52,7 @@ export function resolveEnterpriseReportingPeriod( anchorDate: string, interval: BillingInterval, now: Date = new Date() -): ResolvedUsagePeriod | null { +): ResolvedEnterpriseReportingPeriod | null { const parsed = parseDateOnly(anchorDate) if (!parsed || parsed.date.getTime() > now.getTime()) return null diff --git a/apps/sim/lib/billing/credits/daily-refresh.ts b/apps/sim/lib/billing/credits/daily-refresh.ts index b39351c204b..7c0910eb230 100644 --- a/apps/sim/lib/billing/credits/daily-refresh.ts +++ b/apps/sim/lib/billing/credits/daily-refresh.ts @@ -14,13 +14,14 @@ import { db } from '@sim/db' import { member, usageLog, userStats } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, eq, gte, inArray, lt, or, sql, sum } from 'drizzle-orm' +import { and, eq, gte, inArray, isNull, lt, lte, or, sql, sum } from 'drizzle-orm' import { DAILY_REFRESH_RATE } from '@/lib/billing/constants' import type { DbClient } from '@/lib/db/types' const logger = createLogger('DailyRefresh') const MS_PER_DAY = 86_400_000 +const MAX_BILLING_PERIOD_DAYS = 370 /** * Optional per-user date window. `usageLog` rows outside @@ -148,6 +149,64 @@ export async function computeDailyRefreshConsumed( return totalConsumed } +export async function computeOrganizationDailyRefreshConsumed( + params: { + organizationId: string + periodStart: Date + periodEnd?: Date | null + planDollars: number + seats?: number + }, + executor: DbClient = db +): Promise { + const { organizationId, periodStart, periodEnd, planDollars, seats = 1 } = params + if (planDollars <= 0) return 0 + + const now = new Date() + const cap = periodEnd && periodEnd < now ? periodEnd : now + if (cap <= periodStart) return 0 + const dayCount = Math.ceil((cap.getTime() - periodStart.getTime()) / MS_PER_DAY) + if (dayCount > MAX_BILLING_PERIOD_DAYS) { + throw new Error('Organization billing period exceeds the supported annual bound') + } + + const dailyRefreshDollars = planDollars * DAILY_REFRESH_RATE * seats + const rows = await executor + .select({ + dayIndex: + sql`FLOOR((EXTRACT(EPOCH FROM ${usageLog.createdAt}) - ${Math.floor(periodStart.getTime() / 1000)}) / 86400)`.as( + 'day_index' + ), + dayTotal: sum(usageLog.cost).as('day_total'), + }) + .from(usageLog) + .innerJoin( + member, + and(eq(member.userId, usageLog.userId), eq(member.organizationId, organizationId)) + ) + .leftJoin(userStats, eq(userStats.userId, member.userId)) + .where( + and( + eq(usageLog.billingEntityType, 'organization'), + eq(usageLog.billingEntityId, organizationId), + eq(usageLog.billingPeriodStart, periodStart), + gte(usageLog.createdAt, periodStart), + lt(usageLog.createdAt, cap), + or( + isNull(userStats.proPeriodCostSnapshotAt), + lte(userStats.proPeriodCostSnapshotAt, periodStart), + gte(usageLog.createdAt, userStats.proPeriodCostSnapshotAt) + ) + ) + ) + .groupBy(sql`day_index`) + + return rows.reduce((total, row) => { + const dayUsage = Number.parseFloat(row.dayTotal ?? '0') + return total + Math.min(dayUsage, dailyRefreshDollars) + }, 0) +} + /** * Get the daily refresh allowance in dollars for a plan. */ diff --git a/apps/sim/lib/billing/enterprise-outbox.ts b/apps/sim/lib/billing/enterprise-outbox.ts index 94547811339..440e8115189 100644 --- a/apps/sim/lib/billing/enterprise-outbox.ts +++ b/apps/sim/lib/billing/enterprise-outbox.ts @@ -10,6 +10,7 @@ import type { DbOrTx } from '@/lib/db/types' export const ENTERPRISE_PROVISION_EVENT_TYPE = 'stripe.provision-enterprise' export const ENTERPRISE_METADATA_SYNC_EVENT_TYPE = 'stripe.sync-enterprise-metadata' export const ENTERPRISE_WORKSPACE_MOVE_EVENT_TYPE = 'enterprise.move-workspace' +export const ENTERPRISE_MEMBER_RECONCILIATION_EVENT_TYPE = 'enterprise.reconcile-members' const nonnegativeInteger = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER) @@ -80,10 +81,29 @@ export const enterpriseMetadataSyncPayloadSchema = z.object({ }) .optional(), stripeProgress: z.object({ priceId: z.string().min(1).optional() }).default({}), + deliveryState: z + .object({ + priorPause: z + .object({ + behavior: z.enum(['keep_as_draft', 'mark_uncollectible', 'void']), + resumesAt: z.number().int().nullable(), + }) + .nullable(), + billingIntervalChanged: z.boolean(), + providerAcceptedAt: z.string().datetime().optional(), + verifiedAt: z.string().datetime().optional(), + }) + .optional(), }) export type EnterpriseMetadataSyncPayload = z.infer +export function enterpriseMetadataDeliveryIsVerified( + payload: EnterpriseMetadataSyncPayload +): boolean { + return payload.deliveryState?.verifiedAt !== undefined +} + function stripeMetadataValueMatches( metadata: Stripe.Metadata, key: string, @@ -139,6 +159,11 @@ export const enterpriseWorkspaceMovePayloadSchema = z.object({ export type EnterpriseWorkspaceMovePayload = z.infer +export const enterpriseMemberReconciliationPayloadSchema = z.object({ + organizationId: z.string().min(1), + afterUserId: z.string().min(1).nullable().default(null), +}) + export type EnterpriseOperationStatus = | 'pending' | 'processing' @@ -359,7 +384,9 @@ export async function resolveEnterpriseMetadataIntent( const appliedOperationId = appliedMetadata.simConfigOperationId const operationApplied = appliedOperationId === latest.id - const providerAccepted = parsed.data.acknowledgement !== undefined + const providerAccepted = + parsed.data.deliveryState?.providerAcceptedAt !== undefined || + parsed.data.acknowledgement !== undefined const hasUnappliedIntent = !operationApplied && (latest.status !== 'dead_letter' || providerAccepted) const desiredMetadata = hasUnappliedIntent ? parsed.data.metadata : appliedMetadata diff --git a/apps/sim/lib/billing/enterprise-provisioning.test.ts b/apps/sim/lib/billing/enterprise-provisioning.test.ts index cc1e64c0418..7e78cf3f853 100644 --- a/apps/sim/lib/billing/enterprise-provisioning.test.ts +++ b/apps/sim/lib/billing/enterprise-provisioning.test.ts @@ -20,6 +20,7 @@ const mocks = vi.hoisted(() => ({ pricesRetrieve: vi.fn(), enqueue: vi.fn(), patchPayload: vi.fn(), + reapplyPaidOrgJoinBillingForExistingMemberTx: vi.fn(), })) vi.mock('@sim/audit', () => ({ @@ -31,6 +32,7 @@ vi.mock('@sim/audit', () => ({ vi.mock('@sim/utils/id', () => ({ generateId: vi.fn(() => 'generated-id') })) vi.mock('@/lib/billing/organizations/membership', () => ({ acquireOrganizationMutationLock: vi.fn(), + reapplyPaidOrgJoinBillingForExistingMemberTx: mocks.reapplyPaidOrgJoinBillingForExistingMemberTx, })) vi.mock('@/lib/billing/organizations/billing-identity-lock', () => ({ acquireUserBillingIdentityLock: vi.fn(), @@ -72,6 +74,7 @@ import { getEnterpriseIssuancePreflight, getLatestEnterpriseProvisionings, provisionEnterpriseInStripe, + reconcileEnterpriseMembers, syncEnterpriseMetadataInStripe, } from '@/lib/billing/enterprise-provisioning' @@ -203,6 +206,26 @@ describe('Enterprise issuance preflight', () => { exceedsLimit: false, }) }) + + it('returns a product error for an invalid reporting anchor', async () => { + queueTableRows(schemaMock.user, [{ id: 'owner-1', name: 'Owner', email: 'owner@example.com' }]) + queueTableRows(schemaMock.member, []) + queueTableRows(schemaMock.workspace, [{ value: 0 }]) + queueTableRows(schemaMock.workspace, []) + queueTableRows(schemaMock.workspace, []) + + await expect( + getEnterpriseIssuancePreflight({ + ownerUserId: 'owner-1', + search: '', + limit: 1, + offset: 0, + invoiceAmountUsd: 1_200, + billingInterval: 'year', + reportingPeriodAnchorDate: '2026-02-30', + }) + ).rejects.toThrow('Reporting period anchor must be a valid UTC date on or before today') + }) }) function operationPayload(overrides: Record = {}) { @@ -442,6 +465,44 @@ describe('Enterprise workspace-move progress', () => { }) }) +describe('Enterprise member reconciliation', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + mocks.reapplyPaidOrgJoinBillingForExistingMemberTx.mockResolvedValue(undefined) + }) + + it('processes at most one bounded member page and checkpoints the cursor', async () => { + queueTableRows( + schemaMock.member, + Array.from({ length: 51 }, (_, index) => ({ + userId: `user-${String(index).padStart(3, '0')}`, + })) + ) + const checkpointPayload = vi.fn() + + await expect( + reconcileEnterpriseMembers( + { organizationId: 'org-1', afterUserId: null }, + { + eventId: 'reconcile-1', + eventType: 'enterprise.reconcile-members', + attempts: 0, + checkpointPayload, + } + ) + ).resolves.toEqual({ + outcome: 'deferred', + reason: 'Continuing bounded Enterprise member reconciliation', + consumeAttempt: false, + }) + + expect(mocks.reapplyPaidOrgJoinBillingForExistingMemberTx).toHaveBeenCalledTimes(50) + expect(checkpointPayload).toHaveBeenCalledWith({ afterUserId: 'user-049' }) + expect(dbChainMockFns.limit).toHaveBeenCalledWith(51) + }) +}) + describe('Enterprise issuance outbox handler', () => { beforeEach(() => { vi.clearAllMocks() @@ -817,6 +878,12 @@ describe('Enterprise metadata outbox handler', () => { startedAt: '2026-08-13T00:00:00.000Z', deadlineAt: '2099-08-13T00:30:00.000Z', }, + deliveryState: { + priorPause: null, + billingIntervalChanged: false, + providerAcceptedAt: '2026-08-13T00:00:00.000Z', + verifiedAt: '2026-08-13T00:00:01.000Z', + }, metadata: { plan: 'enterprise', referenceId: 'org-1', seats: 15 }, } queueTableRows(schemaMock.subscription, [ @@ -853,6 +920,88 @@ describe('Enterprise metadata outbox handler', () => { expect(mocks.subscriptionsUpdate).not.toHaveBeenCalled() }) + it('repairs a paused cadence-change invoice before waiting for the webhook', async () => { + const payload = { + subscriptionId: 'local-sub-1', + revision: 7, + deliveryRevision: 1, + acknowledgement: { + startedAt: '2026-08-13T00:00:00.000Z', + deadlineAt: '2099-08-13T00:30:00.000Z', + }, + metadata: { + plan: 'enterprise', + referenceId: 'org-1', + seats: 15, + invoiceAmountCents: 120000, + }, + terms: { invoiceAmountCents: 120000, billingInterval: 'year' as const }, + stripeProgress: { priceId: 'price_year' }, + deliveryState: { + priorPause: { behavior: 'keep_as_draft' as const, resumesAt: null }, + billingIntervalChanged: true, + }, + } + queueTableRows(schemaMock.subscription, [ + { stripeSubscriptionId: 'sub_1', referenceId: 'org-1', metadata: {} }, + ]) + queueTableRows(schemaMock.subscription, [{ metadata: {} }]) + queueTableRows(schemaMock.outboxEvent, [{ id: 'metadata-event-recovery', payload }]) + queueTableRows(schemaMock.member, [{ value: 10 }]) + mocks.subscriptionsRetrieve.mockResolvedValue({ + id: 'sub_1', + metadata: { + plan: 'enterprise', + referenceId: 'org-1', + seats: '15', + invoiceAmountCents: '120000', + simConfigOperationId: 'metadata-event-recovery', + simConfigRevision: '7', + simConfigDeliveryRevision: '1', + }, + schedule: null, + collection_method: 'send_invoice', + days_until_due: 30, + pause_collection: { behavior: 'keep_as_draft', resumes_at: null }, + latest_invoice: { id: 'in_change', status: 'draft', auto_advance: true }, + items: { + data: [ + { + quantity: 1, + price: { + currency: 'usd', + unit_amount: 120000, + recurring: { interval: 'year', interval_count: 1 }, + }, + }, + ], + }, + }) + const checkpointPayload = vi.fn() + + await expect( + syncEnterpriseMetadataInStripe(payload, { + eventId: 'metadata-event-recovery', + eventType: 'stripe.sync-enterprise-metadata', + attempts: 2, + checkpointPayload, + }) + ).resolves.toMatchObject({ outcome: 'deferred', consumeAttempt: false }) + + expect(mocks.subscriptionsUpdate).not.toHaveBeenCalled() + expect(mocks.invoicesUpdate).toHaveBeenCalledWith( + 'in_change', + { auto_advance: false }, + { idempotencyKey: 'enterprise:metadata-event-recovery:initial-invoice-draft' } + ) + expect(checkpointPayload).toHaveBeenCalledWith({ + deliveryState: expect.objectContaining({ providerAcceptedAt: expect.any(String) }), + }) + expect(checkpointPayload).toHaveBeenCalledWith({ + deliveryState: expect.objectContaining({ verifiedAt: expect.any(String) }), + }) + }) + it('consumes the finite missing-ack budget only after the durable grace deadline', async () => { const payload = { subscriptionId: 'local-sub-1', diff --git a/apps/sim/lib/billing/enterprise-provisioning.ts b/apps/sim/lib/billing/enterprise-provisioning.ts index b4707d3d029..5f4d6fdcc0a 100644 --- a/apps/sim/lib/billing/enterprise-provisioning.ts +++ b/apps/sim/lib/billing/enterprise-provisioning.ts @@ -3,7 +3,7 @@ import { db } from '@sim/db' import { member, organization, outboxEvent, subscription, user, workspace } from '@sim/db/schema' import { generateId } from '@sim/utils/id' import { isRecordLike } from '@sim/utils/object' -import { and, count, desc, eq, ilike, inArray, isNull, notInArray, or, sql } from 'drizzle-orm' +import { and, count, desc, eq, gt, ilike, inArray, isNull, notInArray, or, sql } from 'drizzle-orm' import type Stripe from 'stripe' import { parseBillingConcurrencyLimit } from '@/lib/billing/concurrency-defaults' import { getBillingConcurrencyLimit } from '@/lib/billing/concurrency-limits' @@ -12,12 +12,15 @@ import { getBillingPeriodUsageCost } from '@/lib/billing/core/usage-log' import { dollarsToCredits } from '@/lib/billing/credits/conversion' import { deriveEnterpriseOperationStatus, + ENTERPRISE_MEMBER_RECONCILIATION_EVENT_TYPE, ENTERPRISE_METADATA_SYNC_EVENT_TYPE, ENTERPRISE_PROVISION_EVENT_TYPE, ENTERPRISE_WORKSPACE_MOVE_EVENT_TYPE, + type EnterpriseMetadataSyncPayload, type EnterpriseOperationStatus, type EnterpriseProvisionPayload, type EnterpriseProvisionRequest, + enterpriseMemberReconciliationPayloadSchema, enterpriseMetadataIntentMatchesStripeSubscription, enterpriseMetadataSyncPayloadSchema, enterpriseProvisionPayloadSchema, @@ -29,7 +32,10 @@ import { resolveEnterpriseWorkflowExecutionTimeoutFallbackSeconds, } from '@/lib/billing/execution-timeout-defaults' import { acquireUserBillingIdentityLock } from '@/lib/billing/organizations/billing-identity-lock' -import { acquireOrganizationMutationLock } from '@/lib/billing/organizations/membership' +import { + acquireOrganizationMutationLock, + reapplyPaidOrgJoinBillingForExistingMemberTx, +} from '@/lib/billing/organizations/membership' import { requireStripeClient } from '@/lib/billing/stripe-client' import { TERMINAL_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils' import { withEnterpriseReconciliationLease } from '@/lib/billing/webhooks/enterprise-reconciliation-lease' @@ -47,6 +53,7 @@ const TERMINAL_STATUSES = new Set(TERMINAL_SUBSCRIPTION_STATUSES) const ENTERPRISE_WEBHOOK_ACKNOWLEDGEMENT_GRACE_MS = 30 * 60 * 1000 const ENTERPRISE_WEBHOOK_ACKNOWLEDGEMENT_POLL_MS = 30 * 1000 const MAX_ENTERPRISE_WORKSPACE_SELECTION = 1_000 +const ENTERPRISE_MEMBER_RECONCILIATION_BATCH_SIZE = 50 async function waitForEnterpriseWebhookAcknowledgement( acknowledgement: { startedAt: string; deadlineAt: string } | undefined, @@ -522,6 +529,11 @@ export async function getEnterpriseIssuancePreflight({ reportingPeriodAnchorDate, billingInterval ) + if (!reportingPeriod) { + throw new EnterpriseProvisioningError( + 'Reporting period anchor must be a valid UTC date on or before today' + ) + } const configuredUsageLimitDollars = usageLimitDollars === undefined ? invoiceAmountUsd @@ -1343,6 +1355,68 @@ async function keepInitialEnterpriseInvoiceAsDraft(params: { ) } +type EnterpriseMetadataDeliveryState = NonNullable + +function stripePauseState( + pause: Stripe.Subscription.PauseCollection | null +): EnterpriseMetadataDeliveryState['priorPause'] { + return pause + ? { + behavior: pause.behavior, + resumesAt: pause.resumes_at ?? null, + } + : null +} + +function stripePauseMatchesDeliveryState( + pause: Stripe.Subscription.PauseCollection | null, + expected: EnterpriseMetadataDeliveryState['priorPause'] +): boolean { + const actual = stripePauseState(pause) + return actual === null + ? expected === null + : expected !== null && + actual.behavior === expected.behavior && + actual.resumesAt === expected.resumesAt +} + +async function verifyEnterpriseMetadataDelivery(params: { + stripe: Stripe + subscription: Stripe.Subscription + operationId: string + deliveryState: EnterpriseMetadataDeliveryState + context: OutboxEventContext +}): Promise { + const providerAcceptedAt = params.deliveryState.providerAcceptedAt ?? new Date().toISOString() + const acceptedState = { ...params.deliveryState, providerAcceptedAt } + if (!params.deliveryState.providerAcceptedAt) { + await params.context.checkpointPayload({ deliveryState: acceptedState }) + } + + if ( + !stripePauseMatchesDeliveryState(params.subscription.pause_collection, acceptedState.priorPause) + ) { + throw new Error('Stripe did not preserve Enterprise payment-collection pause settings') + } + + if ( + acceptedState.billingIntervalChanged && + acceptedState.priorPause?.behavior === 'keep_as_draft' + ) { + await keepInitialEnterpriseInvoiceAsDraft({ + stripe: params.stripe, + subscription: params.subscription, + operationId: params.operationId, + }) + } + + if (!acceptedState.verifiedAt) { + await params.context.checkpointPayload({ + deliveryState: { ...acceptedState, verifiedAt: new Date().toISOString() }, + }) + } +} + export const provisionEnterpriseInStripe: OutboxHandler = async (rawPayload, context) => { const parsed = enterpriseProvisionPayloadSchema.safeParse(rawPayload) if (!parsed.success) throw new Error('Invalid Enterprise issuance outbox payload') @@ -1673,6 +1747,17 @@ export const syncEnterpriseMetadataInStripe: OutboxHandler = async ( stripeSubscription ) if (deliveryAlreadyWritten) { + const deliveryState = latestPayload.data.deliveryState + if (!deliveryState) { + throw new Error('Enterprise configuration delivery state was not checkpointed') + } + await verifyEnterpriseMetadataDelivery({ + stripe, + subscription: stripeSubscription, + operationId: context.eventId, + deliveryState, + context, + }) return waitForEnterpriseWebhookAcknowledgement(latestPayload.data.acknowledgement, context) } let priceId = latestPayload.data.stripeProgress.priceId ?? null @@ -1735,6 +1820,12 @@ export const syncEnterpriseMetadataInStripe: OutboxHandler = async ( updateItems = [{ id: currentItem.id, price: priceId, quantity: 1 }] } + const deliveryState: EnterpriseMetadataDeliveryState = { + priorPause: stripePauseState(stripeSubscription.pause_collection), + billingIntervalChanged, + } + await context.checkpointPayload({ deliveryState }) + const updatedSubscription = await stripe.subscriptions.update( stripeSubscriptionId, { @@ -1752,33 +1843,13 @@ export const syncEnterpriseMetadataInStripe: OutboxHandler = async ( idempotencyKey: `enterprise-config:${payload.subscriptionId}:${context.eventId}:delivery:${latestPayload.data.deliveryRevision}:attempt:${context.attempts}`, } ) - - const priorPause = stripeSubscription.pause_collection - const updatedPause = updatedSubscription.pause_collection - const pausePreserved = - priorPause === null - ? updatedPause === null - : priorPause?.behavior === updatedPause?.behavior && - (priorPause?.resumes_at ?? null) === (updatedPause?.resumes_at ?? null) - if (!pausePreserved) { - throw new Error('Stripe did not preserve Enterprise payment-collection pause settings') - } - - // Stripe's keep_as_draft contract handles future invoices itself. Only an - // interval switch creates an immediate full-period invoice; inspect that - // invoice rather than mistaking an older paid invoice for a failed update - // during an amount-only Price replacement. - if ( - terms && - billingIntervalChanged && - updatedSubscription.pause_collection?.behavior === 'keep_as_draft' - ) { - await keepInitialEnterpriseInvoiceAsDraft({ - stripe, - subscription: updatedSubscription, - operationId: context.eventId, - }) - } + await verifyEnterpriseMetadataDelivery({ + stripe, + subscription: updatedSubscription, + operationId: context.eventId, + deliveryState, + context, + }) // Stripe's verified webhook is the only path that applies metadata to the // canonical subscription row. Normal delivery latency has a durable grace @@ -1821,10 +1892,45 @@ export const moveEnterpriseWorkspace: OutboxHandler = async (rawPayload }) } +export const reconcileEnterpriseMembers: OutboxHandler = async (rawPayload, context) => { + const parsed = enterpriseMemberReconciliationPayloadSchema.safeParse(rawPayload) + if (!parsed.success) throw new Error('Invalid Enterprise member-reconciliation payload') + const payload = parsed.data + + const nextCursor = await db.transaction(async (tx) => { + await acquireOrganizationMutationLock(tx, payload.organizationId) + const rows = await tx + .select({ userId: member.userId }) + .from(member) + .where( + and( + eq(member.organizationId, payload.organizationId), + payload.afterUserId ? gt(member.userId, payload.afterUserId) : undefined + ) + ) + .orderBy(member.userId) + .limit(ENTERPRISE_MEMBER_RECONCILIATION_BATCH_SIZE + 1) + + const batch = rows.slice(0, ENTERPRISE_MEMBER_RECONCILIATION_BATCH_SIZE) + for (const row of batch) { + await reapplyPaidOrgJoinBillingForExistingMemberTx(tx, row.userId, payload.organizationId) + } + + return rows.length > ENTERPRISE_MEMBER_RECONCILIATION_BATCH_SIZE + ? (batch.at(-1)?.userId ?? null) + : null + }) + + if (!nextCursor) return + await context.checkpointPayload({ afterUserId: nextCursor }) + return deferOutboxHandler('Continuing bounded Enterprise member reconciliation', undefined, false) +} + export const enterpriseIssuanceOutboxHandlers = { [ENTERPRISE_PROVISION_EVENT_TYPE]: provisionEnterpriseInStripe, [ENTERPRISE_METADATA_SYNC_EVENT_TYPE]: syncEnterpriseMetadataInStripe, [ENTERPRISE_WORKSPACE_MOVE_EVENT_TYPE]: moveEnterpriseWorkspace, + [ENTERPRISE_MEMBER_RECONCILIATION_EVENT_TYPE]: reconcileEnterpriseMembers, } as const export async function getLatestEnterpriseProvisionings( diff --git a/apps/sim/lib/billing/webhooks/enterprise.test.ts b/apps/sim/lib/billing/webhooks/enterprise.test.ts index 501db061365..f4a0ff10a47 100644 --- a/apps/sim/lib/billing/webhooks/enterprise.test.ts +++ b/apps/sim/lib/billing/webhooks/enterprise.test.ts @@ -15,6 +15,7 @@ const mocks = vi.hoisted(() => ({ subscriptionsRetrieve: vi.fn(), patchOutboxEventPayload: vi.fn(), enqueueOutboxEvent: vi.fn(), + enqueueOutboxEvents: vi.fn(), reapplyPaidOrgJoinBillingForExistingMemberTx: vi.fn(), })) @@ -54,6 +55,7 @@ vi.mock('@/lib/billing/webhooks/enterprise-reconciliation-lease', () => ({ vi.mock('@/lib/core/outbox/service', () => ({ enqueueOutboxEvent: mocks.enqueueOutboxEvent, + enqueueOutboxEvents: mocks.enqueueOutboxEvents, patchOutboxEventPayload: mocks.patchOutboxEventPayload, })) @@ -107,7 +109,10 @@ function operationPayload( function stripeSubscription(options: { operationId?: string paused?: boolean + configOperationId?: string + seats?: number }): Stripe.Subscription { + const seats = options.seats ?? 12 return { id: 'sub_1', customer: 'cus_1', @@ -128,9 +133,10 @@ function stripeSubscription(options: { invoiceAmountCents: '12500', monthlyPrice: '125.00', usageLimitCredits: '24000', - seats: '12', + seats: String(seats), concurrencyLimit: '1250', ...(options.operationId ? { enterpriseOperationId: options.operationId } : {}), + ...(options.configOperationId ? { simConfigOperationId: options.configOperationId } : {}), }, items: { data: [ @@ -155,6 +161,7 @@ function eventFor(subscription: Stripe.Subscription): Stripe.Event { function queueSuccessfulExistingSubscriptionReconciliation(options: { operation?: ReturnType + existingMetadata?: Record }) { queueTableRows(schemaMock.organization, [{ creditBalance: '0' }]) if (options.operation) { @@ -169,7 +176,14 @@ function queueSuccessfulExistingSubscriptionReconciliation(options: { queueTableRows(schemaMock.member, [{ value: 1 }]) queueTableRows(schemaMock.member, []) queueTableRows(schemaMock.subscription, []) - queueTableRows(schemaMock.subscription, [{ id: 'local-sub-1', referenceId: 'org-1' }]) + queueTableRows(schemaMock.subscription, [ + { + id: 'local-sub-1', + referenceId: 'org-1', + status: 'active', + metadata: options.existingMetadata ?? {}, + }, + ]) queueTableRows(schemaMock.user, [{ id: 'owner-1', name: 'Owner', email: 'owner@example.com' }]) } @@ -180,6 +194,7 @@ describe('Enterprise webhook issuance correlation', () => { mocks.patchOutboxEventPayload.mockResolvedValue(true) mocks.reapplyPaidOrgJoinBillingForExistingMemberTx.mockResolvedValue(undefined) mocks.enqueueOutboxEvent.mockResolvedValue('move-event') + mocks.enqueueOutboxEvents.mockResolvedValue(['move-event']) }) afterAll(() => { @@ -225,17 +240,18 @@ describe('Enterprise webhook issuance correlation', () => { handleManualEnterpriseSubscription(eventFor(subscription)) ).resolves.toBeUndefined() - expect(mocks.enqueueOutboxEvent).toHaveBeenNthCalledWith( - 1, + expect(mocks.enqueueOutboxEvents).toHaveBeenCalledWith( expect.anything(), 'enterprise.move-workspace', - expect.objectContaining({ workspaceId: 'workspace-1', sequence: 0 }) + [ + expect.objectContaining({ workspaceId: 'workspace-1', sequence: 0 }), + expect.objectContaining({ workspaceId: 'workspace-archived', sequence: 1 }), + ] ) - expect(mocks.enqueueOutboxEvent).toHaveBeenNthCalledWith( - 2, + expect(mocks.enqueueOutboxEvent).toHaveBeenCalledWith( expect.anything(), - 'enterprise.move-workspace', - expect.objectContaining({ workspaceId: 'workspace-archived', sequence: 1 }) + 'enterprise.reconcile-members', + expect.objectContaining({ organizationId: 'org-1', afterUserId: null }) ) expect(mocks.patchOutboxEventPayload).toHaveBeenCalled() }) @@ -251,11 +267,11 @@ describe('Enterprise webhook issuance correlation', () => { handleManualEnterpriseSubscription(eventFor(subscription)) ).resolves.toBeUndefined() - expect(mocks.enqueueOutboxEvent).toHaveBeenCalledTimes(1) - expect(mocks.enqueueOutboxEvent).toHaveBeenCalledWith( + expect(mocks.enqueueOutboxEvents).toHaveBeenCalledTimes(1) + expect(mocks.enqueueOutboxEvents).toHaveBeenCalledWith( expect.anything(), 'enterprise.move-workspace', - expect.objectContaining({ workspaceId: 'workspace-1' }) + [expect.objectContaining({ workspaceId: 'workspace-1' })] ) }) @@ -295,4 +311,63 @@ describe('Enterprise webhook issuance correlation', () => { expect(mocks.subscriptionsRetrieve).toHaveBeenCalledTimes(2) }) + + it('allows a later valid Stripe edit that retains an already-applied config marker', async () => { + const subscription = stripeSubscription({ + configOperationId: 'config-1', + seats: 14, + }) + mocks.subscriptionsRetrieve.mockResolvedValue(subscription) + queueSuccessfulExistingSubscriptionReconciliation({ + existingMetadata: { simConfigOperationId: 'config-1' }, + }) + + await expect( + handleManualEnterpriseSubscription(eventFor(subscription)) + ).resolves.toBeUndefined() + + expect(dbChainMockFns.set).toHaveBeenCalledWith( + expect.objectContaining({ metadata: expect.objectContaining({ seats: '14' }) }) + ) + }) + + it('does not apply an unverified configuration delivery', async () => { + const subscription = stripeSubscription({ configOperationId: 'config-unverified' }) + subscription.metadata.simConfigRevision = '2' + subscription.metadata.simConfigDeliveryRevision = '1' + mocks.subscriptionsRetrieve.mockResolvedValue(subscription) + queueTableRows(schemaMock.organization, [{ creditBalance: '0' }]) + queueTableRows(schemaMock.member, [{ value: 1 }]) + queueTableRows(schemaMock.subscription, []) + queueTableRows(schemaMock.subscription, [ + { + id: 'local-sub-1', + referenceId: 'org-1', + status: 'active', + metadata: {}, + }, + ]) + queueTableRows(schemaMock.outboxEvent, [ + { + eventType: 'stripe.sync-enterprise-metadata', + payload: { + subscriptionId: 'local-sub-1', + revision: 2, + deliveryRevision: 1, + metadata: { plan: 'enterprise', referenceId: 'org-1', seats: 12 }, + stripeProgress: {}, + deliveryState: { + priorPause: null, + billingIntervalChanged: false, + providerAcceptedAt: '2026-08-13T00:00:00.000Z', + }, + }, + }, + ]) + + await expect(handleManualEnterpriseSubscription(eventFor(subscription))).rejects.toThrow( + 'does not exactly match the Stripe subscription' + ) + expect(dbChainMockFns.update).not.toHaveBeenCalled() + }) }) diff --git a/apps/sim/lib/billing/webhooks/enterprise.ts b/apps/sim/lib/billing/webhooks/enterprise.ts index 19c15bd15a1..55b41b17617 100644 --- a/apps/sim/lib/billing/webhooks/enterprise.ts +++ b/apps/sim/lib/billing/webhooks/enterprise.ts @@ -3,32 +3,39 @@ import { db } from '@sim/db' import { member, organization, outboxEvent, subscription, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' -import { and, asc, count, eq, inArray, sql } from 'drizzle-orm' +import { isRecordLike } from '@sim/utils/object' +import { and, count, eq, inArray, sql } from 'drizzle-orm' import type Stripe from 'stripe' import { getEmailSubject, renderEnterpriseSubscriptionEmail } from '@/components/emails' import { deriveEnterpriseCreditLimits } from '@/lib/billing/enterprise-credit-limits' import { + ENTERPRISE_MEMBER_RECONCILIATION_EVENT_TYPE, ENTERPRISE_METADATA_SYNC_EVENT_TYPE, ENTERPRISE_PROVISION_EVENT_TYPE, ENTERPRISE_WORKSPACE_MOVE_EVENT_TYPE, type EnterpriseProvisionPayload, + enterpriseMetadataDeliveryIsVerified, enterpriseMetadataIntentMatchesStripeSubscription, enterpriseMetadataSyncPayloadSchema, enterpriseOperationMatchesStripeSubscription, parseEnterpriseProvisionPayload, } from '@/lib/billing/enterprise-outbox' -import { - acquireOrganizationMutationLock, - reapplyPaidOrgJoinBillingForExistingMemberTx, -} from '@/lib/billing/organizations/membership' +import { acquireOrganizationMutationLock } from '@/lib/billing/organizations/membership' import { requireStripeClient } from '@/lib/billing/stripe-client' -import { ENTITLED_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils' +import { + ENTITLED_SUBSCRIPTION_STATUSES, + hasPaidSubscriptionStatus, +} from '@/lib/billing/subscriptions/utils' import { assertEnterpriseReconciliationLeaseHeld, type EnterpriseReconciliationLease, withEnterpriseReconciliationLease, } from '@/lib/billing/webhooks/enterprise-reconciliation-lease' -import { enqueueOutboxEvent, patchOutboxEventPayload } from '@/lib/core/outbox/service' +import { + enqueueOutboxEvent, + enqueueOutboxEvents, + patchOutboxEventPayload, +} from '@/lib/core/outbox/service' import { sendEmail } from '@/lib/messaging/email/mailer' import { getFromEmailAddress } from '@/lib/messaging/email/utils' import { captureServerEvent } from '@/lib/posthog/server' @@ -42,14 +49,23 @@ export async function handleManualEnterpriseSubscription(event: Stripe.Event) { async function processManualEnterpriseSubscription(event: Stripe.Event) { const eventSubscription = event.data.object as Stripe.Subscription + const rawPreviousAttributes: unknown = event.data.previous_attributes + const previousAttributes: Record = isRecordLike(rawPreviousAttributes) + ? rawPreviousAttributes + : {} return withEnterpriseReconciliationLease(eventSubscription.id, (lease) => - reconcileManualEnterpriseSubscription(eventSubscription, lease) + reconcileManualEnterpriseSubscription(eventSubscription, lease, { + created: event.type === 'customer.subscription.created', + previousStatus: + typeof previousAttributes.status === 'string' ? previousAttributes.status : null, + }) ) } async function reconcileManualEnterpriseSubscription( eventSubscription: Stripe.Subscription, - reconciliationLease: EnterpriseReconciliationLease + reconciliationLease: EnterpriseReconciliationLease, + trigger: { created: boolean; previousStatus: string | null } ) { // Stripe does not promise webhook ordering. Read the current object before // taking DB locks so a delayed created/updated event cannot overwrite newer @@ -268,7 +284,12 @@ async function reconcileManualEnterpriseSubscription( } const [existing] = await tx - .select({ id: subscription.id, referenceId: subscription.referenceId }) + .select({ + id: subscription.id, + referenceId: subscription.referenceId, + status: subscription.status, + metadata: subscription.metadata, + }) .from(subscription) .where(eq(subscription.stripeSubscriptionId, stripeSubscription.id)) .limit(1) @@ -280,7 +301,16 @@ async function reconcileManualEnterpriseSubscription( } const configOperationId = metadata.simConfigOperationId - if (typeof configOperationId === 'string' && configOperationId.length > 0) { + const existingMetadata = isRecordLike(existing?.metadata) ? existing.metadata : {} + const configurationAlreadyApplied = + typeof configOperationId === 'string' && + configOperationId.length > 0 && + existingMetadata.simConfigOperationId === configOperationId + if ( + typeof configOperationId === 'string' && + configOperationId.length > 0 && + !configurationAlreadyApplied + ) { const [configurationRow] = await tx .select({ eventType: outboxEvent.eventType, payload: outboxEvent.payload }) .from(outboxEvent) @@ -295,6 +325,7 @@ async function reconcileManualEnterpriseSubscription( configurationRow?.eventType === ENTERPRISE_METADATA_SYNC_EVENT_TYPE && configurationPayload.success && configurationPayload.data.subscriptionId === existing.id && + enterpriseMetadataDeliveryIsVerified(configurationPayload.data) && enterpriseMetadataIntentMatchesStripeSubscription( configurationPayload.data, configOperationId, @@ -347,28 +378,37 @@ async function reconcileManualEnterpriseSubscription( .where(eq(organization.id, referenceId)) if (operationNewlyApplied && correlatedOperation) { - for (const [sequence, workspaceId] of correlatedOperation.request.workspaceIds.entries()) { - await enqueueOutboxEvent(tx, ENTERPRISE_WORKSPACE_MOVE_EVENT_TYPE, { + await enqueueOutboxEvents( + tx, + ENTERPRISE_WORKSPACE_MOVE_EVENT_TYPE, + correlatedOperation.request.workspaceIds.map((workspaceId, sequence) => ({ provisioningOperationId: operationId, workspaceId, destinationOrganizationId: referenceId, expectedOwnerId: correlatedOperation.request.ownerUserId, adminEmail: correlatedOperation.request.requestedByEmail, sequence, - }) - } + })) + ) } - // The organization lock is held across the census and all member billing - // transitions. Add/remove/accept paths take the same lock, so a departing - // member cannot be re-paused after their removal restores personal Pro. - const existingMembers = await tx - .select({ userId: member.userId }) - .from(member) - .where(eq(member.organizationId, referenceId)) - .orderBy(asc(member.userId)) - for (const existingMember of existingMembers) { - await reapplyPaidOrgJoinBillingForExistingMemberTx(tx, existingMember.userId, referenceId) + const wasEntitled = hasPaidSubscriptionStatus(existing?.status) + const isEntitled = hasPaidSubscriptionStatus(subscriptionRow.status) + const triggerRestoredEntitlement = Boolean( + trigger.previousStatus && !hasPaidSubscriptionStatus(trigger.previousStatus) + ) + if ( + isEntitled && + (operationNewlyApplied || + !existing || + !wasEntitled || + trigger.created || + triggerRestoredEntitlement) + ) { + await enqueueOutboxEvent(tx, ENTERPRISE_MEMBER_RECONCILIATION_EVENT_TYPE, { + organizationId: referenceId, + afterUserId: null, + }) } if (correlatedOperation && typeof operationId === 'string') { diff --git a/apps/sim/lib/core/outbox/service.test.ts b/apps/sim/lib/core/outbox/service.test.ts index 138e286a1b8..e9e46dabcbd 100644 --- a/apps/sim/lib/core/outbox/service.test.ts +++ b/apps/sim/lib/core/outbox/service.test.ts @@ -28,6 +28,7 @@ import { deferOutboxHandler, enqueueOrReschedulePendingOutboxEvent, enqueueOutboxEvent, + enqueueOutboxEvents, processOutboxEvents, } from './service' @@ -93,6 +94,31 @@ describe('enqueueOutboxEvent', () => { future ) }) + + it('inserts a bounded event batch in one statement', async () => { + const ids = await enqueueOutboxEvents(dbChainMock.db, 'test.event', [ + { sequence: 0 }, + { sequence: 1 }, + ]) + + expect(ids).toHaveLength(2) + expect(dbChainMockFns.values).toHaveBeenCalledTimes(1) + expect(dbChainMockFns.values.mock.calls[0][0]).toEqual([ + expect.objectContaining({ eventType: 'test.event', payload: { sequence: 0 } }), + expect.objectContaining({ eventType: 'test.event', payload: { sequence: 1 } }), + ]) + }) + + it('rejects an oversized event batch before inserting', async () => { + await expect( + enqueueOutboxEvents( + dbChainMock.db, + 'test.event', + Array.from({ length: 1_001 }, (_, sequence) => ({ sequence })) + ) + ).rejects.toThrow('Cannot enqueue more than 1000') + expect(dbChainMockFns.insert).not.toHaveBeenCalled() + }) }) describe('enqueueOrReschedulePendingOutboxEvent', () => { diff --git a/apps/sim/lib/core/outbox/service.ts b/apps/sim/lib/core/outbox/service.ts index 9a7b8029dec..4636dfe26dd 100644 --- a/apps/sim/lib/core/outbox/service.ts +++ b/apps/sim/lib/core/outbox/service.ts @@ -9,6 +9,7 @@ import { and, asc, desc, eq, inArray, lte, sql } from 'drizzle-orm' const logger = createLogger('OutboxService') const DEFAULT_MAX_ATTEMPTS = 10 +const MAX_BULK_ENQUEUE_EVENTS = 1_000 const MAX_PERSISTED_ERROR_LENGTH = 500 /** @@ -171,6 +172,30 @@ export async function enqueueOutboxEvent( return id } +export async function enqueueOutboxEvents( + executor: Pick, + eventType: string, + payloads: readonly T[], + options: EnqueueOptions = {} +): Promise { + if (payloads.length === 0) return [] + if (payloads.length > MAX_BULK_ENQUEUE_EVENTS) { + throw new Error(`Cannot enqueue more than ${MAX_BULK_ENQUEUE_EVENTS} outbox events at once`) + } + + const availableAt = options.availableAt ?? new Date() + const rows = payloads.map((payload) => ({ + id: generateId(), + eventType, + payload: payload as never, + maxAttempts: options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS, + availableAt, + })) + await executor.insert(outboxEvent).values(rows) + logger.info('Enqueued outbox event batch', { eventType, count: rows.length }) + return rows.map((row) => row.id) +} + export interface CoalescedOutboxEnqueueOptions extends EnqueueOptions { /** * One scalar payload field that identifies the subject of this event. From 2b16761432cb902e574313ee16edbf6cd56a1a50 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 18 Aug 2026 18:09:14 -0700 Subject: [PATCH 3/4] fix(outbox): preserve handler compatibility --- apps/sim/lib/core/outbox/service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/lib/core/outbox/service.ts b/apps/sim/lib/core/outbox/service.ts index 4636dfe26dd..d796bc2d610 100644 --- a/apps/sim/lib/core/outbox/service.ts +++ b/apps/sim/lib/core/outbox/service.ts @@ -103,7 +103,7 @@ export function deferOutboxHandler( export type OutboxHandler = ( payload: T, context: OutboxEventContext -) => Promise +) => Promise | Promise /** * Map of `eventType` → handler. Register all handlers in one place @@ -784,7 +784,7 @@ function runHandlerWithTimeout( handler(event.payload, context) .then((value) => { clearTimeout(timeout) - resolve(value) + resolve(value ?? undefined) }) .catch((err) => { clearTimeout(timeout) From 288661943b092d833cc90fe1a60d381371b72577 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 18 Aug 2026 18:22:13 -0700 Subject: [PATCH 4/4] fix(billing): bound enterprise provisioning reads --- .../billing/enterprise-provisioning.test.ts | 14 +++ .../lib/billing/enterprise-provisioning.ts | 86 +++++++++++++++++-- 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/apps/sim/lib/billing/enterprise-provisioning.test.ts b/apps/sim/lib/billing/enterprise-provisioning.test.ts index 7e78cf3f853..2bb1a0ca18f 100644 --- a/apps/sim/lib/billing/enterprise-provisioning.test.ts +++ b/apps/sim/lib/billing/enterprise-provisioning.test.ts @@ -463,6 +463,20 @@ describe('Enterprise workspace-move progress', () => { failed: [], }) }) + + it('rejects provisioning lookups larger than one admin page', async () => { + await expect( + getLatestEnterpriseProvisionings(Array.from({ length: 251 }, (_, index) => `org-${index}`)) + ).rejects.toThrow('limited to 250 organizations') + }) + + it('only loads workspace-move failure details for one organization', async () => { + await expect( + getLatestEnterpriseProvisionings(['org-1', 'org-2'], { + includeWorkspaceMoveFailures: true, + }) + ).rejects.toThrow('require exactly one organization') + }) }) describe('Enterprise member reconciliation', () => { diff --git a/apps/sim/lib/billing/enterprise-provisioning.ts b/apps/sim/lib/billing/enterprise-provisioning.ts index 5f4d6fdcc0a..4178422d624 100644 --- a/apps/sim/lib/billing/enterprise-provisioning.ts +++ b/apps/sim/lib/billing/enterprise-provisioning.ts @@ -3,7 +3,20 @@ import { db } from '@sim/db' import { member, organization, outboxEvent, subscription, user, workspace } from '@sim/db/schema' import { generateId } from '@sim/utils/id' import { isRecordLike } from '@sim/utils/object' -import { and, count, desc, eq, gt, ilike, inArray, isNull, notInArray, or, sql } from 'drizzle-orm' +import { + and, + count, + desc, + eq, + gt, + ilike, + inArray, + isNull, + ne, + notInArray, + or, + sql, +} from 'drizzle-orm' import type Stripe from 'stripe' import { parseBillingConcurrencyLimit } from '@/lib/billing/concurrency-defaults' import { getBillingConcurrencyLimit } from '@/lib/billing/concurrency-limits' @@ -53,6 +66,7 @@ const TERMINAL_STATUSES = new Set(TERMINAL_SUBSCRIPTION_STATUSES) const ENTERPRISE_WEBHOOK_ACKNOWLEDGEMENT_GRACE_MS = 30 * 60 * 1000 const ENTERPRISE_WEBHOOK_ACKNOWLEDGEMENT_POLL_MS = 30 * 1000 const MAX_ENTERPRISE_WORKSPACE_SELECTION = 1_000 +const MAX_ENTERPRISE_PROVISIONING_LOOKUP_ORGANIZATIONS = 250 const ENTERPRISE_MEMBER_RECONCILIATION_BATCH_SIZE = 50 async function waitForEnterpriseWebhookAcknowledgement( @@ -1054,10 +1068,63 @@ export async function issueEnterpriseProvisioning( ) .orderBy(desc(outboxEvent.createdAt), desc(outboxEvent.id)) .for('update') - const subscriptionRows = await tx - .select() + .limit(1) + const latestOperation = operationRows[0] + const latestPayload = latestOperation + ? parseEnterpriseProvisionPayload(latestOperation.payload) + : null + if (latestOperation && !latestPayload) { + throw new EnterpriseProvisioningError( + `Existing Enterprise issuance operation ${latestOperation.id} has an invalid payload` + ) + } + + const appliedSubscriptionId = latestPayload?.applicationResult?.subscriptionId ?? null + const subscriptionRows: EnterpriseSubscriptionState[] = [] + if (appliedSubscriptionId) { + const [appliedSubscription] = await tx + .select({ + status: subscription.status, + stripeSubscriptionId: subscription.stripeSubscriptionId, + metadata: subscription.metadata, + }) + .from(subscription) + .where( + and( + eq(subscription.referenceId, organizationId), + eq(subscription.stripeSubscriptionId, appliedSubscriptionId) + ) + ) + .limit(1) + if (appliedSubscription) subscriptionRows.push(appliedSubscription) + } + + const [unrelatedNonterminalSubscription] = await tx + .select({ + status: subscription.status, + stripeSubscriptionId: subscription.stripeSubscriptionId, + metadata: subscription.metadata, + }) .from(subscription) - .where(eq(subscription.referenceId, organizationId)) + .where( + and( + eq(subscription.referenceId, organizationId), + or( + isNull(subscription.status), + notInArray(subscription.status, [...TERMINAL_SUBSCRIPTION_STATUSES]) + ), + appliedSubscriptionId + ? or( + isNull(subscription.stripeSubscriptionId), + ne(subscription.stripeSubscriptionId, appliedSubscriptionId) + ) + : undefined + ) + ) + .limit(1) + if (unrelatedNonterminalSubscription) { + subscriptionRows.push(unrelatedNonterminalSubscription) + } const decision = decideEnterpriseProvisioningIssue(requestKey, operationRows, subscriptionRows) if (decision.kind === 'reuse') { @@ -1939,6 +2006,15 @@ export async function getLatestEnterpriseProvisionings( ) { const result = new Map() if (organizationIds.length === 0) return result + const uniqueOrganizationIds = [...new Set(organizationIds)] + if (uniqueOrganizationIds.length > MAX_ENTERPRISE_PROVISIONING_LOOKUP_ORGANIZATIONS) { + throw new Error( + `Enterprise provisioning lookup is limited to ${MAX_ENTERPRISE_PROVISIONING_LOOKUP_ORGANIZATIONS} organizations` + ) + } + if (options.includeWorkspaceMoveFailures && uniqueOrganizationIds.length !== 1) { + throw new Error('Workspace-move failure details require exactly one organization') + } const organizationIdExpression = sql`${outboxEvent.payload} #>> '{request,organizationId}'` const rows = await db .selectDistinctOn([organizationIdExpression]) @@ -1946,7 +2022,7 @@ export async function getLatestEnterpriseProvisionings( .where( and( eq(outboxEvent.eventType, ENTERPRISE_PROVISION_EVENT_TYPE), - inArray(organizationIdExpression, organizationIds) + inArray(organizationIdExpression, uniqueOrganizationIds) ) ) .orderBy(organizationIdExpression, desc(outboxEvent.createdAt), desc(outboxEvent.id))