import type { Namespace, Transaction } from "../generated/kysely-tailordb"; type TimeEntryCodeCategory = Namespace["main-db"]["TimeEntryCode"]["category"]; import { InvalidNightWindowError, InvalidRoundingDirectionError, InvalidTimeEntryCodeReferenceError, } from "./errors.generated"; const ROUNDING_DIRECTIONS = ["UP", "DOWN", "NEAREST"] as const; const NIGHT_WINDOW_MIN = 0; const NIGHT_WINDOW_MAX = 1439; export interface WorkRulePremiumRatePercentInput { category: string; key?: string | null; } export interface WorkRuleValidatableFields { clockInRoundingDirection: string; clockOutRoundingDirection: string; breakRoundingDirection: string; nightWindowStart: number; nightWindowEnd: number; premiumRatePercent: readonly WorkRulePremiumRatePercentInput[]; } /** Validates rounding direction is UP / DOWN / NEAREST for clock-in, clock-out, and break. */ export function validateRoundingDirections( fields: Pick< WorkRuleValidatableFields, "clockInRoundingDirection" | "clockOutRoundingDirection" | "breakRoundingDirection" >, ): InstanceType | null { const directions = [ fields.clockInRoundingDirection, fields.clockOutRoundingDirection, fields.breakRoundingDirection, ]; for (const direction of directions) { if (!(ROUNDING_DIRECTIONS as readonly string[]).includes(direction)) { return new InvalidRoundingDirectionError(direction); } } return null; } /** Validates night-window bounds are minutes-from-midnight within 0-1439. */ export function validateNightWindow( fields: Pick, ): InstanceType | null { const bounds = [fields.nightWindowStart, fields.nightWindowEnd]; for (const bound of bounds) { if (bound < NIGHT_WINDOW_MIN || bound > NIGHT_WINDOW_MAX) { return new InvalidNightWindowError(String(bound)); } } return null; } /** * Validates each premiumRatePercent entry resolves to a TimeEntryCode by * category/key (never by display name), via a same-module read. */ export async function validateTimeEntryCodeReferences( db: Transaction, premiumRatePercent: readonly WorkRulePremiumRatePercentInput[], ): Promise | null> { for (const entry of premiumRatePercent) { let query = db .selectFrom("TimeEntryCode") .selectAll() .where("category", "=", entry.category as TimeEntryCodeCategory); if (entry.key) { query = query.where("key", "=", entry.key); } const match = await query.executeTakeFirst(); if (!match) { const reference = entry.key ? `${entry.category}/${entry.key}` : entry.category; return new InvalidTimeEntryCodeReferenceError(reference); } } return null; }