chore: import upstream snapshot with attribution
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,140 @@
/**
* @vitest-environment node
*/
import {
authMock,
authMockFns,
createMockRequest,
createSession,
dbChainMock,
dbChainMockFns,
resetDbChainMock,
} from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('@sim/db', () => dbChainMock)
vi.mock('@/lib/auth', () => authMock)
vi.mock('@/lib/billing/plan-helpers', () => ({
isOrgPlan: (plan: string) => plan === 'team' || plan === 'enterprise',
}))
vi.mock('@/lib/billing/subscriptions/utils', () => ({
ENTITLED_SUBSCRIPTION_STATUSES: ['active', 'past_due'],
hasPaidSubscriptionStatus: (status: string) => status === 'active' || status === 'past_due',
}))
import { POST } from '@/app/api/users/me/subscription/[id]/transfer/route'
function makeRequest(body: unknown, id = 'sub-1') {
return POST(
createMockRequest(
'POST',
body,
{},
`http://localhost/api/users/me/subscription/${id}/transfer`
),
{ params: Promise.resolve({ id }) }
)
}
describe('POST /api/users/me/subscription/[id]/transfer', () => {
beforeEach(() => {
vi.clearAllMocks()
resetDbChainMock()
authMockFns.mockGetSession.mockResolvedValue(
createSession({
userId: 'user-1',
email: 'owner@example.com',
name: 'Owner',
})
)
})
it('rejects transfers for non-organization subscriptions', async () => {
dbChainMockFns.for.mockResolvedValueOnce([
{ id: 'sub-1', referenceId: 'user-1', plan: 'pro', status: 'active' },
])
const response = await makeRequest({ organizationId: 'org-1' })
expect(response.status).toBe(400)
await expect(response.json()).resolves.toEqual({
error: 'Only active Team or Enterprise subscriptions can be transferred to an organization.',
})
expect(dbChainMockFns.update).not.toHaveBeenCalled()
})
it('transfers an active organization subscription to an admin-owned organization', async () => {
dbChainMockFns.for
.mockResolvedValueOnce([
{ id: 'sub-1', referenceId: 'user-1', plan: 'team', status: 'active' },
])
.mockResolvedValueOnce([{ id: 'org-1' }])
dbChainMockFns.limit.mockResolvedValueOnce([{ role: 'owner' }]).mockResolvedValueOnce([])
const response = await makeRequest({ organizationId: 'org-1' })
expect(response.status).toBe(200)
await expect(response.json()).resolves.toEqual({
success: true,
message: 'Subscription transferred successfully',
})
expect(dbChainMockFns.update).toHaveBeenCalled()
expect(dbChainMockFns.set).toHaveBeenCalledWith({ referenceId: 'org-1' })
})
it('treats an already-transferred organization subscription as a successful no-op', async () => {
dbChainMockFns.for
.mockResolvedValueOnce([
{ id: 'sub-1', referenceId: 'org-1', plan: 'team', status: 'active' },
])
.mockResolvedValueOnce([{ id: 'org-1' }])
dbChainMockFns.limit.mockResolvedValueOnce([{ role: 'owner' }])
const response = await makeRequest({ organizationId: 'org-1' })
expect(response.status).toBe(200)
await expect(response.json()).resolves.toEqual({
success: true,
message: 'Subscription already belongs to this organization',
})
expect(dbChainMockFns.update).not.toHaveBeenCalled()
})
it('rejects the noop probe when the requester is not a member of the target organization', async () => {
dbChainMockFns.for
.mockResolvedValueOnce([
{ id: 'sub-1', referenceId: 'org-1', plan: 'team', status: 'active' },
])
.mockResolvedValueOnce([{ id: 'org-1' }])
dbChainMockFns.limit.mockResolvedValueOnce([])
const response = await makeRequest({ organizationId: 'org-1' })
expect(response.status).toBe(403)
await expect(response.json()).resolves.toEqual({
error: 'Unauthorized - user is not admin of organization',
})
expect(dbChainMockFns.update).not.toHaveBeenCalled()
})
it('rejects the transfer when the target organization already has an active subscription', async () => {
dbChainMockFns.for
.mockResolvedValueOnce([
{ id: 'sub-1', referenceId: 'user-1', plan: 'team', status: 'active' },
])
.mockResolvedValueOnce([{ id: 'org-1' }])
dbChainMockFns.limit
.mockResolvedValueOnce([{ role: 'owner' }])
.mockResolvedValueOnce([{ id: 'existing-sub' }])
const response = await makeRequest({ organizationId: 'org-1' })
expect(response.status).toBe(409)
await expect(response.json()).resolves.toEqual({
error: 'Organization already has an active subscription',
})
expect(dbChainMockFns.update).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,167 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { db } from '@sim/db'
import { member, organization, subscription } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { and, eq, inArray } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { subscriptionTransferContract } from '@/lib/api/contracts/user'
import { parseRequest } from '@/lib/api/server'
import { getSession } from '@/lib/auth'
import { isOrgPlan } from '@/lib/billing/plan-helpers'
import {
ENTITLED_SUBSCRIPTION_STATUSES,
hasPaidSubscriptionStatus,
} from '@/lib/billing/subscriptions/utils'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { captureServerEvent } from '@/lib/posthog/server'
const logger = createLogger('SubscriptionTransferAPI')
type TransferOutcome =
| { kind: 'error'; status: number; error: string }
| { kind: 'noop'; message: string }
| { kind: 'success'; message: string }
export const POST = withRouteHandler(
async (request: NextRequest, context: { params: Promise<{ id: string }> }) => {
try {
const session = await getSession()
if (!session?.user?.id) {
logger.warn('Unauthorized subscription transfer attempt')
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const parsed = await parseRequest(subscriptionTransferContract, request, context)
if (!parsed.success) return parsed.response
const subscriptionId = parsed.data.params.id
const { organizationId } = parsed.data.body
const userId = session.user.id
logger.info('Processing subscription transfer', { subscriptionId, organizationId })
const outcome = await db.transaction(async (tx): Promise<TransferOutcome> => {
const [sub] = await tx
.select()
.from(subscription)
.where(eq(subscription.id, subscriptionId))
.for('update')
if (!sub) {
return { kind: 'error', status: 404, error: 'Subscription not found' }
}
if (!isOrgPlan(sub.plan) || !hasPaidSubscriptionStatus(sub.status)) {
return {
kind: 'error',
status: 400,
error:
'Only active Team or Enterprise subscriptions can be transferred to an organization.',
}
}
const [org] = await tx
.select({ id: organization.id })
.from(organization)
.where(eq(organization.id, organizationId))
.for('update')
if (!org) {
return { kind: 'error', status: 404, error: 'Organization not found' }
}
const [mem] = await tx
.select({ role: member.role })
.from(member)
.where(and(eq(member.userId, userId), eq(member.organizationId, organizationId)))
.limit(1)
if (!mem || (mem.role !== 'owner' && mem.role !== 'admin')) {
return {
kind: 'error',
status: 403,
error: 'Unauthorized - user is not admin of organization',
}
}
if (sub.referenceId === organizationId) {
return { kind: 'noop', message: 'Subscription already belongs to this organization' }
}
if (sub.referenceId !== userId) {
return {
kind: 'error',
status: 403,
error: 'Unauthorized - subscription does not belong to user',
}
}
const [existingOrgSub] = await tx
.select({ id: subscription.id })
.from(subscription)
.where(
and(
eq(subscription.referenceId, organizationId),
inArray(subscription.status, ENTITLED_SUBSCRIPTION_STATUSES)
)
)
.limit(1)
if (existingOrgSub) {
return {
kind: 'error',
status: 409,
error: 'Organization already has an active subscription',
}
}
await tx
.update(subscription)
.set({ referenceId: organizationId })
.where(eq(subscription.id, subscriptionId))
return { kind: 'success', message: 'Subscription transferred successfully' }
})
if (outcome.kind === 'error') {
return NextResponse.json({ error: outcome.error }, { status: outcome.status })
}
if (outcome.kind === 'success') {
logger.info('Subscription transfer completed', {
subscriptionId,
organizationId,
userId,
})
recordAudit({
actorId: userId,
action: AuditAction.SUBSCRIPTION_TRANSFERRED,
resourceType: AuditResourceType.SUBSCRIPTION,
resourceId: subscriptionId,
description: `Subscription transferred to organization ${organizationId}`,
metadata: {
subscriptionId,
organizationId,
fromEntity: 'user',
toEntity: 'organization',
},
request,
})
captureServerEvent(userId, 'subscription_transferred', {
subscription_id: subscriptionId,
from_entity: 'user',
to_entity: 'organization',
})
}
return NextResponse.json({ success: true, message: outcome.message })
} catch (error) {
logger.error('Error transferring subscription', {
error: toError(error).message,
})
return NextResponse.json({ error: 'Failed to transfer subscription' }, { status: 500 })
}
}
)