Files
simstudioai--sim/apps/sim/lib/api/contracts/subscription.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

388 lines
12 KiB
TypeScript

import { z } from 'zod'
import { defineRouteContract } from '@/lib/api/contracts/types'
const booleanQueryParamSchema = z
.preprocess((value) => {
if (value === 'true') return true
if (value === 'false') return false
return value
}, z.boolean())
.optional()
.default(false)
export const billingUpdateCostBodySchema = z.object({
userId: z.string().min(1, 'User ID is required'),
cost: z.number().min(0, 'Cost must be a non-negative number'),
model: z.string().min(1, 'Model is required'),
inputTokens: z.number().min(0).default(0),
outputTokens: z.number().min(0).default(0),
source: z
.enum(['copilot', 'workspace-chat', 'mcp_copilot', 'mothership_block'])
.default('copilot'),
idempotencyKey: z.string().min(1).optional(),
/**
* Originating workspace, used for org-workspace cost attribution on hosted
* Sim. Best-effort by design: self-hosted and headless clients bill through
* this endpoint with workspace IDs that exist only in their own deployment
* (or with none at all — the Go client omits the field when empty), so the
* value is optional and the route only stamps it onto the ledger when it
* resolves to a workspace in this deployment. Billing is keyed on the
* user's billing entity and must never fail over attribution metadata.
*/
workspaceId: z.string().min(1).optional(),
})
export type BillingUpdateCostBody = z.input<typeof billingUpdateCostBodySchema>
export const billingSwitchPlanBodySchema = z.object({
targetPlanName: z.string(),
interval: z.enum(['month', 'year']).optional(),
})
export type BillingSwitchPlanBody = z.input<typeof billingSwitchPlanBodySchema>
export const billingQuerySchema = z.object({
context: z.enum(['user', 'organization']).optional().default('user'),
id: z.string().min(1).optional(),
includeOrg: booleanQueryParamSchema,
})
export const billingUsageDataSchema = z
.object({
current: z.number(),
limit: z.number(),
percentUsed: z.number(),
isWarning: z.boolean(),
isExceeded: z.boolean(),
billingPeriodStart: z.string().nullable(),
billingPeriodEnd: z.string().nullable(),
lastPeriodCost: z.number(),
lastPeriodCopilotCost: z.number(),
daysRemaining: z.number(),
copilotCost: z.number(),
})
.passthrough()
export const subscriptionBillingDataSchema = z
.object({
type: z.enum(['individual', 'organization']),
plan: z.string(),
currentUsage: z.number(),
usageLimit: z.number(),
percentUsed: z.number(),
isWarning: z.boolean(),
isExceeded: z.boolean(),
daysRemaining: z.number(),
creditBalance: z.number(),
billingInterval: z.enum(['month', 'year']),
isPaid: z.boolean(),
isPro: z.boolean(),
isTeam: z.boolean(),
isEnterprise: z.boolean(),
isOrgScoped: z.boolean(),
organizationId: z.string().nullable(),
status: z.string().nullable(),
seats: z.number().nullable(),
metadata: z.unknown().nullable(),
stripeSubscriptionId: z.string().nullable(),
periodEnd: z.string().nullable(),
cancelAtPeriodEnd: z.boolean().optional(),
usage: billingUsageDataSchema,
billingBlocked: z.boolean().optional(),
billingBlockedReason: z.enum(['payment_failed', 'dispute']).nullable().optional(),
blockedByOrgOwner: z.boolean().optional(),
organization: z
.object({
id: z.string(),
role: z.enum(['owner', 'admin', 'member']),
})
.optional(),
})
.passthrough()
export const subscriptionApiResponseSchema = z
.object({
success: z.boolean(),
context: z.string(),
data: subscriptionBillingDataSchema,
})
.passthrough()
export const organizationBillingMemberSchema = z
.object({
id: z.string().optional(),
userId: z.string().optional(),
userName: z.string().nullable().optional(),
userEmail: z.string().nullable().optional(),
joinedAt: z.string().nullable().optional(),
})
.passthrough()
export const organizationBillingDataSchema = z
.object({
organizationId: z.string(),
organizationName: z.string(),
subscriptionPlan: z.string(),
subscriptionStatus: z.string().nullable(),
totalSeats: z.number(),
usedSeats: z.number(),
seatsCount: z.number(),
totalCurrentUsage: z.number(),
totalUsageLimit: z.number(),
minimumBillingAmount: z.number(),
averageUsagePerMember: z.number(),
billingPeriodStart: z.string().nullable(),
billingPeriodEnd: z.string().nullable(),
members: z.array(organizationBillingMemberSchema),
billingBlocked: z.boolean().optional(),
billingBlockedReason: z.enum(['payment_failed', 'dispute']).nullable().optional(),
blockedByOrgOwner: z.boolean().optional(),
})
.passthrough()
export const organizationBillingApiResponseSchema = z
.object({
success: z.boolean(),
context: z.literal('organization'),
data: organizationBillingDataSchema,
userRole: z.enum(['owner', 'admin', 'member']),
billingBlocked: z.boolean().optional(),
billingBlockedReason: z.enum(['payment_failed', 'dispute']).nullable().optional(),
blockedByOrgOwner: z.boolean().optional(),
})
.passthrough()
export const usageLimitDataSchema = z
.object({
currentLimit: z.number(),
canEdit: z.boolean(),
minimumLimit: z.number(),
plan: z.string(),
updatedAt: z.string().nullable(),
scope: z.enum(['user', 'organization']),
organizationId: z.string().nullable(),
})
.passthrough()
export const usageQuerySchema = z.object({
context: z.enum(['user', 'organization']).optional().default('user'),
userId: z.string().optional(),
organizationId: z.string().optional(),
})
export const updateUsageLimitBodySchema = z
.object({
limit: z.number().min(0, 'Limit must be a non-negative number'),
context: z.enum(['user', 'organization']).optional().default('user'),
organizationId: z.string().optional(),
})
.refine((data) => data.context !== 'organization' || data.organizationId, {
message: 'Organization ID is required when context is organization',
})
export const usageLimitApiResponseSchema = z
.object({
success: z.boolean(),
context: z.string(),
userId: z.string(),
organizationId: z.string().nullable(),
data: usageLimitDataSchema,
})
.passthrough()
export const organizationUsageLimitApiResponseSchema = z
.object({
success: z.boolean(),
context: z.literal('organization'),
userId: z.string(),
organizationId: z.string(),
data: organizationBillingDataSchema.nullable(),
})
.passthrough()
export const purchaseCreditsBodySchema = z.object({
amount: z.number().min(10).max(1000),
requestId: z.string().uuid(),
})
export const billingPortalBodySchema = z.object({
context: z.enum(['user', 'organization']).optional().default('user'),
organizationId: z.string().min(1).optional(),
returnUrl: z.string().min(1).optional(),
})
export const invoicesQuerySchema = z.object({
context: z.enum(['user', 'organization']).optional().default('user'),
organizationId: z.string().min(1).optional(),
})
export const invoiceItemSchema = z.object({
id: z.string(),
number: z.string().nullable(),
/** Invoice creation time as a Unix timestamp in seconds. */
created: z.number(),
/** Invoice total in the currency's minor units (e.g. cents). */
total: z.number(),
/** Amount paid in the currency's minor units (e.g. cents). */
amountPaid: z.number(),
currency: z.string(),
status: z.string().nullable(),
hostedInvoiceUrl: z.string().nullable(),
invoicePdf: z.string().nullable(),
})
export const invoicesApiResponseSchema = z.object({
success: z.boolean(),
invoices: z.array(invoiceItemSchema),
/** True when Stripe has more invoices than the returned page (overflow). */
hasMore: z.boolean(),
})
const successResponseSchema = z.object({
success: z.boolean(),
})
export const getBillingContract = defineRouteContract({
method: 'GET',
path: '/api/billing',
query: billingQuerySchema,
response: {
mode: 'json',
schema: z.union([subscriptionApiResponseSchema, organizationBillingApiResponseSchema]),
},
})
export const getUserBillingContract = defineRouteContract({
method: 'GET',
path: '/api/billing',
query: billingQuerySchema.extend({
context: z.literal('user').optional().default('user'),
}),
response: {
mode: 'json',
schema: subscriptionApiResponseSchema,
},
})
export const getOrganizationBillingContract = defineRouteContract({
method: 'GET',
path: '/api/billing',
query: billingQuerySchema.extend({
context: z.literal('organization'),
id: z.string().min(1),
}),
response: {
mode: 'json',
schema: organizationBillingApiResponseSchema,
},
})
export const getUsageLimitContract = defineRouteContract({
method: 'GET',
path: '/api/usage',
query: usageQuerySchema,
response: {
mode: 'json',
schema: z.union([usageLimitApiResponseSchema, organizationUsageLimitApiResponseSchema]),
},
})
export const getUserUsageLimitContract = defineRouteContract({
method: 'GET',
path: '/api/usage',
query: usageQuerySchema.extend({
context: z.literal('user').optional().default('user'),
}),
response: {
mode: 'json',
schema: usageLimitApiResponseSchema,
},
})
export const updateUsageLimitContract = defineRouteContract({
method: 'PUT',
path: '/api/usage',
body: updateUsageLimitBodySchema,
response: {
mode: 'json',
schema: z.union([usageLimitApiResponseSchema, organizationUsageLimitApiResponseSchema]),
},
})
export const purchaseCreditsContract = defineRouteContract({
method: 'POST',
path: '/api/billing/credits',
body: purchaseCreditsBodySchema,
response: {
mode: 'json',
schema: successResponseSchema,
},
})
export const createBillingPortalContract = defineRouteContract({
method: 'POST',
path: '/api/billing/portal',
body: billingPortalBodySchema,
response: {
mode: 'json',
schema: z.object({
url: z.string().min(1),
}),
},
})
export const getInvoicesContract = defineRouteContract({
method: 'GET',
path: '/api/billing/invoices',
query: invoicesQuerySchema,
response: {
mode: 'json',
schema: invoicesApiResponseSchema,
},
})
export const billingSwitchPlanResponseSchema = z.object({
success: z.literal(true),
plan: z.string().optional(),
interval: z.enum(['month', 'year']).optional(),
message: z.string().optional(),
})
export const billingUpdateCostResponseSchema = z.object({
success: z.literal(true),
message: z.string().optional(),
data: z.object({
userId: z.string().optional(),
cost: z.number().optional(),
billingEnabled: z.boolean().optional(),
processedAt: z.string(),
requestId: z.string(),
}),
})
export const billingSwitchPlanContract = defineRouteContract({
method: 'POST',
path: '/api/billing/switch-plan',
body: billingSwitchPlanBodySchema,
response: {
mode: 'json',
schema: billingSwitchPlanResponseSchema,
},
})
export const billingUpdateCostContract = defineRouteContract({
method: 'POST',
path: '/api/billing/update-cost',
body: billingUpdateCostBodySchema,
response: {
mode: 'json',
schema: billingUpdateCostResponseSchema,
},
})
export type BillingUsageData = z.infer<typeof billingUsageDataSchema>
export type SubscriptionBillingData = z.infer<typeof subscriptionBillingDataSchema>
export type SubscriptionApiResponse = z.infer<typeof subscriptionApiResponseSchema>
export type OrganizationBillingApiResponse = z.infer<typeof organizationBillingApiResponseSchema>
export type UsageLimitApiResponse = z.infer<typeof usageLimitApiResponseSchema>
export type InvoiceItem = z.infer<typeof invoiceItemSchema>
export type InvoicesApiResponse = z.infer<typeof invoicesApiResponseSchema>