import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import { shiftScheduleLifecycle } from "../db/shiftSchedule.lifecycle.generated"; import type { Transaction } from "../generated/kysely-tailordb"; import { ShiftScheduleNotFoundError, ShiftScheduleAlreadyConfirmedError, } from "../lib/errors.generated"; export interface ConfirmShiftScheduleInput { id: string; } /** * Function: confirmShiftSchedule * Description: Commits a whole shiftSchedule period to workers in one act (公開) — the operation that * used to require calling publishShift once per slot. Confirming freezes the period's shift * definitions; staffing (ShiftPlacement) stays changeable afterwards, because a same-day * substitution is a change of plan, not a re-publication of the table. */ export async function run(db: Transaction, input: ConfirmShiftScheduleInput, _ctx: CommandContext) { const shiftSchedule = await db .selectFrom("ShiftSchedule") .selectAll() .where("id", "=", input.id) .forUpdate() .executeTakeFirst(); if (!shiftSchedule) { return err(new ShiftScheduleNotFoundError(input.id)); } // The admissible transition comes from the generated lifecycle (docs/model/ShiftSchedule.md's // State Transitions table), so the state machine lives in one place instead of being restated // as a status comparison here. The module's own error is kept: "already confirmed" says more to // a caller than a generic invalid-transition code, and CONFIRMED is the only other state. const nextStatus = shiftScheduleLifecycle.tryTransition(shiftSchedule.status, "confirm"); if (!nextStatus) { return err(new ShiftScheduleAlreadyConfirmedError(input.id)); } const updated = await db .updateTable("ShiftSchedule") .set({ status: nextStatus, confirmedAt: new Date() }) .where("id", "=", input.id) .returningAll() .executeTakeFirstOrThrow(); return ok({ shiftSchedule: updated }); }