import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { eligibilityTargetExists, type EligibilityTargetQueries } from "../lib/_eligibilityTarget"; import { InvalidTargetError, InvalidGrantError, EffectiveDateOverlapError, TargetNotFoundError, } from "../lib/errors.generated"; export type EligibilityTargetType = | "WORKER" | "POSITION" | "JOB_PROFILE" | "EMPLOYMENT_TYPE" | "WORK_REGIME"; export type EligibilityGrantType = "WORK_RULE"; export interface CreateEligibilityRuleInput { targetType: EligibilityTargetType; /** Id of the target (Worker/Position/JobProfile/EmploymentType/WorkRegime), per targetType. */ targetId: string; grantType: EligibilityGrantType; workRuleId?: string | null; effectiveStart: Date; } interface TargetShape { targetType: EligibilityTargetType; targetId: string; } interface GrantShape { grantType: EligibilityGrantType; workRuleId?: string | null; } /** * Function: isValidTarget * Description: Validates the polymorphic target of an EligibilityRule. Every target type is * referenced by id (a Worker/Position/JobProfile identity, or an EmploymentType/WorkRegime * catalog entry — all cross-module workforce ids). Following the createWorker.ts precedent for * the cross-module wiring gap, we only validate that a non-empty id was supplied. */ function isValidTarget(target: TargetShape): boolean { return typeof target.targetId === "string" && target.targetId.length > 0; } /** * Function: resolveGrant * Description: Resolves the WorkRule grant of an EligibilityRule against same-module data — the * WorkRule is looked up by id directly. (The TIME_ENTRY_CODE grant kind was dropped in #39.) */ async function resolveGrant(db: Transaction, grant: GrantShape): Promise { if (grant.grantType === "WORK_RULE") { if (!grant.workRuleId) return false; const workRule = await db .selectFrom("WorkRule") .select("id") .where("id", "=", grant.workRuleId) .executeTakeFirst(); return !!workRule; } return false; } /** * Function: findOverlappingGenerations * Description: Finds EligibilityRule generations for the same target + grant combination whose * effective range overlaps a new/updated generation starting at `effectiveStart` (open-ended). */ function findOverlappingGenerations( db: Transaction, target: TargetShape, grant: GrantShape, effectiveStart: Date, excludeId?: string, ) { let query = db .selectFrom("EligibilityRule") .selectAll() .where("targetType", "=", target.targetType) .where("targetId", "=", target.targetId) .where("grantType", "=", grant.grantType); query = grant.workRuleId != null ? query.where("workRuleId", "=", grant.workRuleId) : query.where("workRuleId", "is", null); query = query.where((eb) => eb.or([eb("effectiveEnd", "is", null), eb("effectiveEnd", ">=", effectiveStart)]), ); if (excludeId) { query = query.where("id", "!=", excludeId); } return query.forUpdate().execute(); } export async function run( db: Transaction, input: CreateEligibilityRuleInput, ctx: CommandContext, queries: EligibilityTargetQueries, ) { if (!isValidTarget(input)) { return err(new InvalidTargetError(input.targetId ?? input.targetType)); } // The target is a cross-module workforce id; it must resolve to a real entity of its type, not // just be a non-empty string (M17). if (!(await eligibilityTargetExists(db, queries, input.targetType, input.targetId, ctx))) { return err(new TargetNotFoundError(input.targetId)); } const grantResolved = await resolveGrant(db, input); if (!grantResolved) { return err(new InvalidGrantError(input.workRuleId ?? input.grantType)); } const overlapping = await findOverlappingGenerations(db, input, input, input.effectiveStart); if (overlapping.length > 0) { return err(new EffectiveDateOverlapError(input.targetId ?? input.targetType)); } // The initial generation is its own version root: id and versionOf share the same value so // later generations (created by updateEligibilityRule) can be grouped by versionOf. const id = crypto.randomUUID(); const eligibilityRule = await db .insertInto("EligibilityRule") .values({ id, targetType: input.targetType, targetId: input.targetId, grantType: input.grantType, workRuleId: input.workRuleId ?? null, effectiveStart: input.effectiveStart, effectiveEnd: null, versionOf: id, }) .returningAll() .executeTakeFirstOrThrow(); return ok({ eligibilityRule }); }