import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { AssignmentNotEffectiveError, MissingRequiredFieldError, NegativeMinutesError, WorkScheduleAlreadyExistsError, } from "../lib/errors.generated"; // Whether `date` falls within [start, end] on a whole-day granularity; a null end is open-ended. function isEffectiveOn(date: Date, start: Date, end: Date | null): boolean { const day = (d: Date) => Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()); const on = day(date); return day(start) <= on && (end === null || day(end) >= on); } export interface CreateWorkScheduleInput { assignmentId: string; scheduledDailyMinutes: number; scheduledWeeklyMinutes: number; effectiveStart: Date; } /** * Function: run * Description: Opens the initial 所定 (WorkSchedule) generation for an * Assignment — the first generation of the PLANNED baseline declaring the * assignment's scheduled daily/weekly minutes. */ export async function run>( db: Transaction, input: CreateWorkScheduleInput & CF, _ctx: CommandContext, ) { const { assignmentId, scheduledDailyMinutes, scheduledWeeklyMinutes, effectiveStart, ...customFields } = input; if ( !assignmentId || scheduledDailyMinutes === undefined || scheduledDailyMinutes === null || scheduledWeeklyMinutes === undefined || scheduledWeeklyMinutes === null || !effectiveStart ) { return err(new MissingRequiredFieldError(assignmentId ?? "unknown")); } if (scheduledDailyMinutes < 0 || scheduledWeeklyMinutes < 0) { return err(new NegativeMinutesError(assignmentId)); } // Now that WorkSchedule lives beside Assignment, the effective-date check the cross-module // version could not make is a plain read: a baseline must not open on a date its Assignment // generation does not cover. Effective dates are whole days, so compare the date parts. const assignment = await db .selectFrom("Assignment") .selectAll() .where("id", "=", assignmentId) .executeTakeFirst(); if ( !assignment || !isEffectiveOn(effectiveStart, assignment.effectiveStart, assignment.effectiveEnd) ) { return err(new AssignmentNotEffectiveError(assignmentId)); } // The generation inserted below is open-ended (effectiveEnd = null), so it overlaps every // generation that is still open or that ends on/after its start — including one that starts // *later*. Testing `effectiveStart <= effectiveStart` as well would let exactly those through // and leave the assignment with two open generations, which the readers resolving "the current // generation" (updateWorkSchedule / endWorkSchedule / listActiveWorkSchedules) cannot // disambiguate. const overlapping = await db .selectFrom("WorkSchedule") .selectAll() .where("assignmentId", "=", assignmentId) .where((eb) => eb.or([eb("effectiveEnd", "is", null), eb("effectiveEnd", ">=", effectiveStart)]), ) .forUpdate() .executeTakeFirst(); if (overlapping) { return err(new WorkScheduleAlreadyExistsError(assignmentId)); } const id = crypto.randomUUID(); const workSchedule = await db .insertInto("WorkSchedule") .values({ ...(customFields as Record), id, assignmentId, scheduledDailyMinutes, scheduledWeeklyMinutes, effectiveStart, effectiveEnd: null, versionOf: id, }) .returningAll() .executeTakeFirstOrThrow(); return ok({ workSchedule }); }