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 { EligibilityRuleNotFoundError, InvalidTargetError, InvalidGrantError, EffectiveDateOverlapError, TargetNotFoundError, } from "../lib/errors.generated"; import type { EligibilityTargetType, EligibilityGrantType } from "./createEligibilityRule"; export interface UpdateEligibilityRuleInput { id: string; 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(); } /** Function: dayBefore * Description: Returns the calendar day immediately preceding the given date (UTC), used to * close the prior generation the day before a new generation's effectiveStart. */ function dayBefore(date: Date): Date { const result = new Date(date); result.setUTCDate(result.getUTCDate() - 1); return result; } export async function run( db: Transaction, input: UpdateEligibilityRuleInput, ctx: CommandContext, queries: EligibilityTargetQueries, ) { const current = await db .selectFrom("EligibilityRule") .selectAll() .where("id", "=", input.id) .forUpdate() .executeTakeFirst(); if (!current) { return err(new EligibilityRuleNotFoundError(input.id)); } 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 (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, current.id, ); if (overlapping.length > 0) { return err(new EffectiveDateOverlapError(input.targetId ?? input.targetType)); } await db .updateTable("EligibilityRule") .set({ effectiveEnd: dayBefore(input.effectiveStart) }) .where("id", "=", current.id) .execute(); const eligibilityRule = await db .insertInto("EligibilityRule") .values({ targetType: input.targetType, targetId: input.targetId, grantType: input.grantType, workRuleId: input.workRuleId ?? null, effectiveStart: input.effectiveStart, effectiveEnd: null, versionOf: current.versionOf, }) .returningAll() .executeTakeFirstOrThrow(); return ok({ eligibilityRule }); }