import { type CommonReservedFields, type NoReservedFields, defaultGqlPermission, defaultPermission, } from "@tailor-platform/erp-kit/core"; import { db, type TailorAnyDBField } from "@tailor-platform/sdk"; import { holidayCalendar } from "./holidayCalendar"; // Normalized rounding direction, configured independently per clock-in/clock-out/break const ROUNDING_DIRECTIONS = ["UP", "DOWN", "NEAREST"] as const; const builtins = { // UI-only label; never load-bearing for calculation (same pattern as TimeEntryCode.displayName) displayName: db .string() .description("Human-facing label for UI only; carries no calculation semantics"), // Intra-module FK, optional: the HolidayCalendar used to classify holiday work under this rule. // null means this rule applies no holiday calendar — no date is treated as a holiday. holidayCalendarId: db .uuid({ optional: true }) .relation({ type: "n-1", toward: { type: holidayCalendar, as: "holidayCalendar" }, backward: "workRules", }) .description("Foreign key to the HolidayCalendar this rule uses for holiday classification"), // Rounding — clock-in clockInRoundingUnitMinutes: db .int() .description("Rounding unit, in minutes, applied to clock-in time"), clockInRoundingDirection: db .enum(ROUNDING_DIRECTIONS) .description("Rounding direction applied to clock-in time"), // Rounding — clock-out clockOutRoundingUnitMinutes: db .int() .description("Rounding unit, in minutes, applied to clock-out time"), clockOutRoundingDirection: db .enum(ROUNDING_DIRECTIONS) .description("Rounding direction applied to clock-out time"), // Rounding — break breakRoundingUnitMinutes: db .int() .description("Rounding unit, in minutes, applied to break time"), breakRoundingDirection: db .enum(ROUNDING_DIRECTIONS) .description("Rounding direction applied to break time"), // Break deduction (minute-based rule) breakDeductionMinutes: db.int().description("Minutes deducted from reported time for break"), // Daily overtime accrual-start threshold dailyOvertimeThresholdMinutes: db .int() .description( "Minutes per day beyond which time is treated as daily (statutory-excess) overtime, e.g. 480 for an 8-hour day", ), // Night-work window, minutes-from-midnight bounds (0-1439) nightWindowStart: db .int() .description("Night-work window start, minutes-from-midnight (0-1439), e.g. 1320 for 22:00"), nightWindowEnd: db .int() .description("Night-work window end, minutes-from-midnight (0-1439), e.g. 300 for 05:00"), // TimeEntryCode category/key pins for premium determination, keyed by category/key — never by // display name (ADR-015). No rate multiplication exists in this codebase; issue #39 dropped the // unused inner ratePercent int, so these are category/key pins with no percent value. premiumRatePercent: db .object( { category: db.string().description("TimeEntryCode category this premium rate applies to"), key: db .string({ optional: true }) .description("Optional TimeEntryCode key narrowing the premium rate within the category"), }, { array: true }, ) .description( "TimeEntryCode category/key pins used by premium determination, never keyed by display name", ), // Effective-dating per ADR-013 effectiveStart: db.date().description("Date this WorkRule generation becomes effective"), effectiveEnd: db .date({ optional: true }) .description( "Date this WorkRule generation stops being effective; null means current generation", ), versionOf: db .uuid() .description("Stable key grouping all generations of the same logical WorkRule across time"), }; type ReservedFields = keyof typeof builtins | CommonReservedFields; export interface CreateWorkRuleTypeParams> { fields?: NoReservedFields; } export function createWorkRuleType>( params: CreateWorkRuleTypeParams, ) { return db .table("WorkRule", { ...builtins, ...((params.fields ?? {}) as F), ...db.fields.timestamps(), }) .permission(defaultPermission) .gqlPermission(defaultGqlPermission); } export const workRule = createWorkRuleType({});