import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { AssignmentNotFoundError, InvalidEventTypeError, OccurredAtRequiredError, } from "../lib/errors.generated"; import type { WorkforceQueries } from "../module"; /** Only the workforce query recordPunch needs: the Assignment the punch is filed against. */ type RecordPunchQueries = Pick; function isEffectiveOn( row: { effectiveStart: Date; effectiveEnd: Date | null }, at: Date, ): boolean { return ( row.effectiveStart.getTime() <= at.getTime() && (row.effectiveEnd === null || row.effectiveEnd.getTime() >= at.getTime()) ); } const EVENT_TYPES = [ "CLOCK_IN", "CLOCK_OUT", "BREAK_START", "BREAK_END", "STEP_OUT", "STEP_IN", ] as const; export interface RecordPunchInput { assignmentId: string; eventType: string; occurredAt: Date; source: string; } export async function run( db: Transaction, input: RecordPunchInput, ctx: CommandContext, workforceQueries: RecordPunchQueries, ) { if (!input.occurredAt) { return err(new OccurredAtRequiredError(input.assignmentId)); } if (!(EVENT_TYPES as readonly string[]).includes(input.eventType)) { return err(new InvalidEventTypeError(input.eventType)); } // `source` is catalog data (a free-form punch-source key), not a fixed enum — no core validation. // The punch must reference a workforce Assignment that is in force on occurredAt (ADR-023): // the injected workforce query is the seam; FK integrity alone does not check the effective range. const assignmentResult = await workforceQueries.getAssignment( db, { id: input.assignmentId }, ctx, ); if (!assignmentResult.ok || !isEffectiveOn(assignmentResult.value.assignment, input.occurredAt)) { return err(new AssignmentNotFoundError(input.assignmentId)); } const timeClockEvent = await db .insertInto("TimeClockEvent") .values({ assignmentId: input.assignmentId, eventType: input.eventType as (typeof EVENT_TYPES)[number], occurredAt: input.occurredAt, source: input.source, }) .returningAll() .executeTakeFirstOrThrow(); return ok({ timeClockEvent }); }