import type { OrganizationStatus, Prisma } from "@prisma/client"; export class InactiveOrganizationError extends Error { readonly organizationId: string; readonly status: Exclude; constructor(organizationId: string, status: Exclude) { super(`organization ${organizationId} is ${status}; customer operations require ACTIVE`); this.name = "InactiveOrganizationError"; this.organizationId = organizationId; this.status = status; } } export function inactiveOrganizationReason( organizationId: string, status: OrganizationStatus, ): string | undefined { if (status === "ACTIVE") return undefined; return `organization ${organizationId} is ${status}; customer operations require ACTIVE`; } export function requireActiveOrganizationStatus( organizationId: string, status: OrganizationStatus, ): void { if (status !== "ACTIVE") throw new InactiveOrganizationError(organizationId, status); } /** * Lock the Organization row for a customer operation and verify ACTIVE. * PostgreSQL UPDATE takes a conflicting row lock, so lifecycle transitions * serialize with the entire transaction that called this function. */ export async function lockActiveOrganization( prisma: Prisma.TransactionClient, organizationId: string, ): Promise { const rows = await prisma.$queryRaw>` SELECT "id", "status"::text AS "status" FROM "Organization" WHERE "id" = ${organizationId} FOR SHARE `; const organization = rows[0]; if (organization === undefined) throw new Error(`organization not found: ${organizationId}`); requireActiveOrganizationStatus(organization.id, organization.status); } /** Resolve a Project's tenant inside the transaction, then take the lifecycle lock. */ export async function lockActiveProjectOrganization( prisma: Prisma.TransactionClient, projectId: string, ): Promise { const project = await prisma.project.findUnique({ where: { id: projectId }, select: { organizationId: true }, }); if (project === null) throw new Error(`project not found: ${projectId}`); await lockActiveOrganization(prisma, project.organizationId); return project.organizationId; }