import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { eligibilityTargetExists, type EligibilityTargetQueries } from "../lib/_eligibilityTarget"; import { AssignmentOverlapError, InvalidTargetError, NotEligibleError, TargetNotFoundError, WorkRuleNotFoundError, } from "../lib/errors.generated"; export interface AssignWorkRuleInput { targetType: "WORKER" | "POSITION" | "JOB_PROFILE" | "EMPLOYMENT_TYPE" | "WORK_REGIME"; /** Id of the target the assignment applies to (Worker/Position/JobProfile/EmploymentType/WorkRegime). */ targetId: string; workRuleId: string; effectiveStart: Date; } /** * Function: assignWorkRule * Description: Binds a WorkRule to a target (worker / position / job-profile / * employment-type / work-regime) as an effective-dated WorkRuleAssignment. The WorkRule must * exist, an EligibilityRule generation in force on the assignment date must grant the target * this WorkRule (ADR-022 absorbed PayRule into WorkRule, so the assignable rule is the * WorkRule itself), and the new generation must not overlap an existing assignment for the * same target. The target's current open assignment, if any, is closed at * effectiveStart - 1 day (ADR-013) before the new generation is inserted. */ export async function run( db: Transaction, input: AssignWorkRuleInput, ctx: CommandContext, queries: EligibilityTargetQueries, ) { const { targetType, targetId, workRuleId, effectiveStart } = input; if (typeof targetId !== "string" || targetId.length === 0) { return err(new InvalidTargetError(targetType)); } // The target is a cross-module workforce id; it must resolve to a real entity of its type (M17). if (!(await eligibilityTargetExists(db, queries, targetType, targetId, ctx))) { return err(new TargetNotFoundError(targetId)); } const workRule = await db .selectFrom("WorkRule") .selectAll() .where("id", "=", workRuleId) .executeTakeFirst(); if (!workRule) { return err(new WorkRuleNotFoundError(workRuleId)); } const eligibilityQuery = db .selectFrom("EligibilityRule") .selectAll() .where("targetType", "=", targetType) .where("grantType", "=", "WORK_RULE") .where("workRuleId", "=", workRuleId) .where("effectiveStart", "<=", effectiveStart) .where("targetId", "=", targetId); const candidates = await eligibilityQuery.execute(); const inForce = candidates.find( (rule) => rule.effectiveEnd === null || rule.effectiveEnd.getTime() >= effectiveStart.getTime(), ); if (!inForce) { return err(new NotEligibleError(workRuleId)); } // Existing generations for the same target, locked for the read-then-write cycle. const existing = await db .selectFrom("WorkRuleAssignment") .selectAll() .where("targetType", "=", targetType) .where("targetId", "=", targetId) .forUpdate() .execute(); // A generation starting on/after the new start, or a closed generation still covering it, // cannot be auto-closed and is an overlap. The open current generation (started earlier, // effectiveEnd NULL) is the one legitimately superseded below. const openCurrent = existing.find( (row) => row.effectiveEnd === null && row.effectiveStart.getTime() < effectiveStart.getTime(), ); const overlapping = existing.find( (row) => row.effectiveStart.getTime() >= effectiveStart.getTime() || (row.effectiveEnd !== null && row.effectiveEnd.getTime() >= effectiveStart.getTime()), ); if (overlapping) { return err(new AssignmentOverlapError(overlapping.id)); } if (openCurrent) { const previousGenerationEnd = new Date(effectiveStart); previousGenerationEnd.setUTCDate(previousGenerationEnd.getUTCDate() - 1); await db .updateTable("WorkRuleAssignment") .set({ effectiveEnd: previousGenerationEnd }) .where("id", "=", openCurrent.id) .execute(); } const id = crypto.randomUUID(); const workRuleAssignment = await db .insertInto("WorkRuleAssignment") .values({ id, targetType, targetId, workRuleId, effectiveStart, effectiveEnd: null, versionOf: openCurrent?.versionOf ?? existing[0]?.versionOf ?? id, }) .returningAll() .executeTakeFirstOrThrow(); return ok({ workRule, workRuleAssignment }); }