import { ok, err } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { RoleNotActiveError, RoleNotFoundError, UserNotActiveError, UserNotFoundError, } from "../lib/errors.generated"; import { recomputeUserPermissions } from "../lib/recomputeUserPermissions"; export interface AssignRoleToUserInput { userId: string; roleId: string; } /** * Function: assignRoleToUser * * Assigns a role to a user. PENDING and ACTIVE users can receive role assignments; INACTIVE users are rejected. * Operation is idempotent - assigning the same role twice does not create * duplicate records or error. */ export async function run(db: Transaction, input: AssignRoleToUserInput) { const user = await db .selectFrom("User") .selectAll() .where("id", "=", input.userId) .executeTakeFirst(); if (!user) { return err(new UserNotFoundError(input.userId)); } const role = await db .selectFrom("Role") .selectAll() .where("id", "=", input.roleId) .executeTakeFirst(); if (!role) { return err(new RoleNotFoundError(input.roleId)); } if (role.status !== "ACTIVE") { return err(new RoleNotActiveError(input.roleId)); } if (user.status === "INACTIVE") { return err(new UserNotActiveError(`${input.userId} (status: ${user.status})`)); } // Idempotent: return existing assignment, recompute permissions for consistency const existingAssignment = await db .selectFrom("UserRole") .selectAll() .where("userId", "=", input.userId) .where("roleId", "=", input.roleId) .forUpdate() .executeTakeFirst(); if (existingAssignment) { const updatedUser = await recomputeUserPermissions(db, input.userId); return ok({ userRole: existingAssignment, user: updatedUser }); } const userRole = await db .insertInto("UserRole") .values({ userId: input.userId, roleId: input.roleId, }) .returningAll() .executeTakeFirstOrThrow(); const updatedUser = await recomputeUserPermissions(db, input.userId); return ok({ userRole, user: updatedUser }); }