import { ok, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { AssignmentNotFoundError, InvalidRowError, ImportSourceInvalidError, } from "../lib/errors.generated"; import type { Schema, TimeClockEvent } from "../lib/types"; import type { WorkforceQueries } from "../module"; /** Only the workforce query importPunches needs: the Assignment each row is filed against. */ type ImportPunchesQueries = 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; // Bulk-import rows may only carry a source that identifies an external feed (issue: IMPORT_SOURCE_INVALID) const IMPORT_SOURCES = ["IMPORT", "IC_CARD"] as const; export interface ImportPunchRow { assignmentId: string; eventType: string; occurredAt: Date; source: string; } export interface ImportPunchesInput { rows: ImportPunchRow[]; } export interface ImportPunchesRejectedRow { index: number; row: ImportPunchRow; error: | InstanceType | InstanceType | InstanceType; } export async function run( db: Transaction, input: ImportPunchesInput, ctx: CommandContext, workforceQueries: ImportPunchesQueries, ) { const validRows: { index: number; row: ImportPunchRow }[] = []; const rejectedRows: ImportPunchesRejectedRow[] = []; // Single ordered pass: schema/source checks, then the workforce Assignment must be in force on // the row's occurredAt (the injected query seam; FK integrity does not check the effective range). let index = 0; for (const row of input.rows) { if ( !row.assignmentId || !row.occurredAt || !(EVENT_TYPES as readonly string[]).includes(row.eventType) ) { rejectedRows.push({ index, row, error: new InvalidRowError(`row ${index}`) }); index++; continue; } if (!(IMPORT_SOURCES as readonly string[]).includes(row.source)) { rejectedRows.push({ index, row, error: new ImportSourceInvalidError(row.source) }); index++; continue; } const assignmentResult = await workforceQueries.getAssignment( db, { id: row.assignmentId }, ctx, ); if (!assignmentResult.ok || !isEffectiveOn(assignmentResult.value.assignment, row.occurredAt)) { rejectedRows.push({ index, row, error: new AssignmentNotFoundError(row.assignmentId) }); index++; continue; } validRows.push({ index, row }); index++; } let imported: TimeClockEvent[] = []; if (validRows.length > 0) { imported = await db .insertInto("TimeClockEvent") .values( validRows.map(({ row }) => ({ assignmentId: row.assignmentId, eventType: row.eventType as (typeof EVENT_TYPES)[number], occurredAt: row.occurredAt, source: row.source as (typeof IMPORT_SOURCES)[number], })), ) .returningAll() .execute(); } return ok({ imported, rejectedRows }); }