import { type CommonReservedFields, type NoReservedFields, defaultGqlPermission, defaultPermission, } from "@tailor-platform/erp-kit/core"; import { db, type TailorAnyDBField, type TailorAnyDBType } from "@tailor-platform/sdk"; // Normalized shift kind (ADR-006) — describes the day-part character, orthogonal to segment // count. OFF (公休/休み) represents the explicit absence of a work slot and carries zero segments. const SHIFT_PATTERN_KINDS = ["DAY", "EARLY", "LATE", "NIGHT", "ON_CALL", "OFF"] as const; const builtins = (params: { scopeType?: TailorAnyDBType }) => { // Cross-module FK, optional: scopes `code` uniqueness to a single Site; null = tenant-shared. // Uniqueness of (scopeId, code) is enforced by the composite unique index below; the // command-level forUpdate check exists only to surface a friendly error before the insert. // Caveat: Postgres unique indexes treat NULL as distinct, so two tenant-shared (scopeId = // null) patterns with the same code are NOT caught by the index — the forUpdate check is // the only guard for that case, so it remains subject to the same race Copilot flagged. const scopeId = db .uuid({ optional: true }) .description( "Foreign key to the organization Site this pattern's code is scoped to; null = tenant-shared", ); return { // Stable key — selection and downstream labeling bind to code, never to name (ADR-015; #14 // 反面教師). Not globally unique: uniqueness is the pair (scopeId, code) below. code: db.string().description("Stable key identifying this shift pattern within its scope"), scopeId: params.scopeType ? scopeId.relation({ type: "n-1", toward: { type: params.scopeType, as: "scope" }, backward: "shiftPatterns", }) : scopeId, name: db .string() .description("Display name of the shift pattern (e.g. early, late, day, night, or on-call)"), kind: db .enum(SHIFT_PATTERN_KINDS) .description("Normalized day-part kind: DAY / EARLY / LATE / NIGHT / ON_CALL / OFF"), // Ordered embedded work intervals composing this pattern (ADR-022: formerly the // independent ShiftPatternSegment entity). A single-part pattern has one element; // a 中抜け pattern has two or more separated by a positive gap; an OFF pattern has none. segments: db .object( { // 1-based order within the pattern, contiguous and consistent with time order sequence: db.int().description("1-based ordering of this segment within the pattern"), // Minutes-from-midnight, [0, 1440) startTime: db.int().description("Segment start time, minutes from midnight in [0, 1440)"), endTime: db.int().description("Segment end time, minutes from midnight in [0, 1440)"), // Intra-segment rest, distinct from the inter-segment 中抜け gap breakMinutes: db .int() .description( "Non-negative intra-segment break minutes, strictly less than the segment's gross span", ), // True exactly when endTime <= startTime spansMidnight: db .bool() .description("Whether this segment crosses midnight (endTime <= startTime)"), }, { array: true }, ) .description("Ordered embedded work intervals (ADR-022); empty only when kind = OFF"), }; }; type ReservedFields = keyof ReturnType | CommonReservedFields; export interface CreateShiftPatternTypeParams> { fields?: NoReservedFields; scopeType?: TailorAnyDBType; } export function createShiftPatternType>( params: CreateShiftPatternTypeParams, ) { return db .table("ShiftPattern", { ...builtins(params), ...((params.fields ?? {}) as F), ...db.fields.timestamps(), }) .indexes({ fields: ["scopeId", "code"], unique: true, name: "shift_pattern_scope_code_unique_idx", }) .permission(defaultPermission) .gqlPermission(defaultGqlPermission); } // Codegen instance with no cross-module type; real scope FK injected via module.ts export const shiftPattern = createShiftPatternType({});