import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { EmploymentNotFoundError, EmploymentTerminatedError, OutsideEmploymentRangeError, PositionNotFoundError, PositionNotEffectiveError, DuplicatePrimaryAssignmentError, PositionHeadcountExceededError, } from "../lib/errors.generated"; export interface CreateAssignmentInput { workerEmploymentId: string; positionId: string; effectiveStart: Date; isPrimary: boolean; } // Whether `date` falls within [start, end] where a null end means open-ended. function isEffectiveOn(date: Date, start: Date, end: Date | null): boolean { return start.getTime() <= date.getTime() && (end === null || date.getTime() <= end.getTime()); } /** * Function: run * Description: Places an active WorkerEmployment into a Position, opening the * initial Assignment generation — as the employment's primary post, or as an * additional concurrent post. */ export async function run(db: Transaction, input: CreateAssignmentInput, _ctx: CommandContext) { const { workerEmploymentId, positionId, effectiveStart, isPrimary } = input; const employment = await db .selectFrom("WorkerEmployment") .selectAll() .where("id", "=", workerEmploymentId) .executeTakeFirst(); if (!employment) return err(new EmploymentNotFoundError(workerEmploymentId)); // A newly opened Assignment is open-ended [effectiveStart, ∞). If the employment is already // terminated, that range necessarily extends past the employment's end — reject it outright // rather than let an Assignment outlive the employment it belongs to (M03). if (employment.terminationDate !== null) { return err(new EmploymentTerminatedError(workerEmploymentId)); } if (!isEffectiveOn(effectiveStart, employment.effectiveStart, employment.effectiveEnd)) { return err(new OutsideEmploymentRangeError(workerEmploymentId)); } const position = await db .selectFrom("Position") .selectAll() .where("id", "=", positionId) .executeTakeFirst(); if (!position) return err(new PositionNotFoundError(positionId)); if (!isEffectiveOn(effectiveStart, position.effectiveStart, position.effectiveEnd)) { return err(new PositionNotEffectiveError(positionId)); } if (isPrimary) { const existingPrimary = await db .selectFrom("Assignment") .selectAll() .where("workerEmploymentId", "=", workerEmploymentId) .where("isPrimary", "=", true) .where("effectiveEnd", "is", null) .forUpdate() .executeTakeFirst(); if (existingPrimary) return err(new DuplicatePrimaryAssignmentError(workerEmploymentId)); const openPrimaryAssignments = await db .selectFrom("Assignment") .selectAll() .where("positionId", "=", positionId) .where("isPrimary", "=", true) .where("effectiveEnd", "is", null) .forUpdate() .execute(); if (openPrimaryAssignments.length >= position.headcount) { return err(new PositionHeadcountExceededError(positionId)); } } const id = crypto.randomUUID(); const assignment = await db .insertInto("Assignment") .values({ id, workerEmploymentId, positionId, isPrimary, effectiveStart, effectiveEnd: null, versionOf: id, }) .returningAll() .executeTakeFirstOrThrow(); return ok({ assignment }); }