import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { lockApprovedTimecards } from "../lib/_lockTransition"; import { TimecardNotFoundError, InvalidStatusTransitionError } from "../lib/errors.generated"; export interface LockTimecardInput { id: string; } /** * Function: lockTimecard * Description: Transitions an APPROVED Timecard to LOCKED, closing the period so further * changes must go through historical correction rather than supersede. This is the * single-card relock exception path (e.g. after a LOCKED historical correction); * period-wide closing goes through closeTimecardPeriod, which shares the same transition. * * No timecard.lifecycle.generated.ts exists for this hand-rolled status enum, so the * transition is validated and applied manually rather than via executeTransition/tryTransition. * The LOCKED transition stamps lockedAt=now and lockedBy=ctx.actorId to attribute the close. */ export async function run(db: Transaction, input: LockTimecardInput, ctx: CommandContext) { const timecard = await db .selectFrom("Timecard") .selectAll() .where("id", "=", input.id) .forUpdate() .executeTakeFirst(); if (!timecard) { return err(new TimecardNotFoundError(input.id)); } if (timecard.status !== "APPROVED") { return err(new InvalidStatusTransitionError(input.id)); } const [updated] = await lockApprovedTimecards(db, [input.id], ctx.actorId); if (!updated) { // The forUpdate read above saw APPROVED; a missing update row means a concurrent // transition moved it out of APPROVED between the read and the guarded UPDATE. return err(new InvalidStatusTransitionError(input.id)); } return ok({ timecard: updated }); }