Files
Nexus/packages/shared/src/__tests__/schema-blueprint-orgunit-misc.test.ts
T
Hartmut d0926601ea test(shared): add 215 schema validation tests covering all 17 Zod schemas
Phase 3a: raises shared schema coverage from 5.5% to ~95%. Tests cover
valid roundtrips, invalid rejection, edge cases for refinements, defaults,
date coercion, and the generateDynamicZodSchema runtime builder.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 15:47:53 +02:00

845 lines
29 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
CreateBlueprintSchema,
UpdateBlueprintSchema,
BlueprintFieldDefinitionSchema,
generateDynamicZodSchema,
} from "../schemas/blueprint.schema.js";
import { CreateOrgUnitSchema, UpdateOrgUnitSchema } from "../schemas/org-unit.schema.js";
import {
CreateManagementLevelGroupSchema,
UpdateManagementLevelGroupSchema,
CreateManagementLevelSchema,
UpdateManagementLevelSchema,
} from "../schemas/management-level.schema.js";
import {
CreateUtilizationCategorySchema,
UpdateUtilizationCategorySchema,
} from "../schemas/utilization-category.schema.js";
import {
CreateHolidayCalendarSchema,
UpdateHolidayCalendarSchema,
CreateHolidayCalendarEntrySchema,
UpdateHolidayCalendarEntrySchema,
PreviewResolvedHolidaysSchema,
} from "../schemas/holiday-calendar.schema.js";
import {
CreateCalculationRuleSchema,
UpdateCalculationRuleSchema,
} from "../schemas/calculation-rules.schema.js";
import { BlueprintTarget, FieldType } from "../types/enums.js";
// ─── BlueprintFieldDefinitionSchema ─────────────────────────────────────────
describe("BlueprintFieldDefinitionSchema", () => {
const validField = {
id: "field-1",
label: "Project Name",
key: "project_name",
type: FieldType.TEXT,
order: 0,
};
it("accepts a minimal valid field definition", () => {
const result = BlueprintFieldDefinitionSchema.parse(validField);
expect(result.id).toBe("field-1");
expect(result.required).toBe(false);
});
it("accepts a full field with options and validation", () => {
const result = BlueprintFieldDefinitionSchema.parse({
...validField,
required: true,
description: "The name of the project",
placeholder: "Enter name…",
defaultValue: "Untitled",
options: [{ value: "a", label: "Option A", color: "#ff0000" }],
validation: { minLength: 2, maxLength: 50 },
group: "General",
});
expect(result.required).toBe(true);
expect(result.options).toHaveLength(1);
expect(result.validation?.maxLength).toBe(50);
expect(result.group).toBe("General");
});
it("rejects key that does not match snake_case pattern", () => {
expect(() =>
BlueprintFieldDefinitionSchema.parse({ ...validField, key: "Project-Name" }),
).toThrow();
});
it("rejects key starting with a digit", () => {
expect(() =>
BlueprintFieldDefinitionSchema.parse({ ...validField, key: "1bad_key" }),
).toThrow();
});
it("rejects negative order value", () => {
expect(() => BlueprintFieldDefinitionSchema.parse({ ...validField, order: -1 })).toThrow();
});
it("rejects label longer than 200 characters", () => {
expect(() =>
BlueprintFieldDefinitionSchema.parse({ ...validField, label: "x".repeat(201) }),
).toThrow();
});
});
// ─── CreateBlueprintSchema ───────────────────────────────────────────────────
describe("CreateBlueprintSchema", () => {
it("accepts minimal valid input and applies defaults", () => {
const result = CreateBlueprintSchema.parse({
name: "Project Template",
target: BlueprintTarget.PROJECT,
});
expect(result.name).toBe("Project Template");
expect(result.target).toBe("PROJECT");
expect(result.fieldDefs).toEqual([]);
expect(result.defaults).toEqual({});
expect(result.validationRules).toEqual([]);
expect(result.description).toBeUndefined();
});
it("accepts full input with fieldDefs and validationRules", () => {
const result = CreateBlueprintSchema.parse({
name: "Resource Blueprint",
target: BlueprintTarget.RESOURCE,
description: "Used for all 3D artists",
fieldDefs: [
{
id: "f1",
label: "Skill Level",
key: "skill_level",
type: FieldType.SELECT,
order: 0,
options: [
{ value: "junior", label: "Junior" },
{ value: "senior", label: "Senior" },
],
},
],
defaults: { skill_level: "junior" },
validationRules: [{ field: "skill_level", rule: "required_if", params: { other: "active" } }],
});
expect(result.fieldDefs).toHaveLength(1);
expect(result.defaults.skill_level).toBe("junior");
expect(result.validationRules[0]?.rule).toBe("required_if");
});
it("rejects empty name", () => {
expect(() =>
CreateBlueprintSchema.parse({ name: "", target: BlueprintTarget.PROJECT }),
).toThrow();
});
it("rejects name longer than 200 characters", () => {
expect(() =>
CreateBlueprintSchema.parse({ name: "x".repeat(201), target: BlueprintTarget.PROJECT }),
).toThrow();
});
it("rejects invalid BlueprintTarget enum value", () => {
expect(() => CreateBlueprintSchema.parse({ name: "Test", target: "TEAM" })).toThrow();
});
});
// ─── UpdateBlueprintSchema ───────────────────────────────────────────────────
describe("UpdateBlueprintSchema", () => {
it("accepts an empty object (all fields optional)", () => {
const result = UpdateBlueprintSchema.parse({});
expect(result.name).toBeUndefined();
expect(result.target).toBeUndefined();
});
it("accepts a partial update with only name", () => {
const result = UpdateBlueprintSchema.parse({ name: "Updated Name" });
expect(result.name).toBe("Updated Name");
expect(result.fieldDefs).toBeUndefined();
});
it("accepts a partial update with only target", () => {
const result = UpdateBlueprintSchema.parse({ target: BlueprintTarget.RESOURCE });
expect(result.target).toBe("RESOURCE");
});
it("rejects invalid target even in partial update", () => {
expect(() => UpdateBlueprintSchema.parse({ target: "INVALID" })).toThrow();
});
});
// ─── generateDynamicZodSchema ────────────────────────────────────────────────
describe("generateDynamicZodSchema", () => {
it("generates a TEXT field as optional string by default", () => {
const schema = generateDynamicZodSchema([
{ id: "f1", label: "Title", key: "title", type: FieldType.TEXT, order: 0, required: false },
]);
const result = schema.parse({});
expect(result.title).toBeUndefined();
const withValue = schema.parse({ title: "Hello" });
expect(withValue.title).toBe("Hello");
});
it("generates a TEXT field as required when required=true", () => {
const schema = generateDynamicZodSchema([
{ id: "f1", label: "Title", key: "title", type: FieldType.TEXT, order: 0, required: true },
]);
expect(() => schema.parse({})).toThrow();
expect(schema.parse({ title: "Hello" }).title).toBe("Hello");
});
it("generates a NUMBER field with min/max validation", () => {
const schema = generateDynamicZodSchema([
{
id: "f2",
label: "Rating",
key: "rating",
type: FieldType.NUMBER,
order: 0,
required: true,
validation: { min: 1, max: 10 },
},
]);
expect(schema.parse({ rating: 5 }).rating).toBe(5);
expect(() => schema.parse({ rating: 0 })).toThrow();
expect(() => schema.parse({ rating: 11 })).toThrow();
});
it("generates a BOOLEAN field", () => {
const schema = generateDynamicZodSchema([
{
id: "f3",
label: "Active",
key: "is_active",
type: FieldType.BOOLEAN,
order: 0,
required: true,
},
]);
expect(schema.parse({ is_active: true }).is_active).toBe(true);
expect(schema.parse({ is_active: false }).is_active).toBe(false);
expect(() => schema.parse({ is_active: "yes" })).toThrow();
});
it("generates a DATE field using coerce", () => {
const schema = generateDynamicZodSchema([
{
id: "f4",
label: "Start Date",
key: "start_date",
type: FieldType.DATE,
order: 0,
required: true,
},
]);
const result = schema.parse({ start_date: "2026-01-15" });
expect(result.start_date).toBeInstanceOf(Date);
});
it("generates a SELECT field restricted to defined option values", () => {
const schema = generateDynamicZodSchema([
{
id: "f5",
label: "Status",
key: "status",
type: FieldType.SELECT,
order: 0,
required: true,
options: [
{ value: "draft", label: "Draft" },
{ value: "active", label: "Active" },
],
},
]);
expect(schema.parse({ status: "draft" }).status).toBe("draft");
expect(() => schema.parse({ status: "unknown" })).toThrow();
});
it("generates a MULTI_SELECT field as an array of valid enum values", () => {
const schema = generateDynamicZodSchema([
{
id: "f6",
label: "Tags",
key: "tags",
type: FieldType.MULTI_SELECT,
order: 0,
required: true,
options: [
{ value: "cg", label: "CG" },
{ value: "vfx", label: "VFX" },
],
},
]);
expect(schema.parse({ tags: ["cg", "vfx"] }).tags).toEqual(["cg", "vfx"]);
expect(() => schema.parse({ tags: ["cg", "invalid"] })).toThrow();
});
it("generates an EMAIL field that validates email format", () => {
const schema = generateDynamicZodSchema([
{ id: "f7", label: "Email", key: "email", type: FieldType.EMAIL, order: 0, required: true },
]);
expect(schema.parse({ email: "user@example.com" }).email).toBe("user@example.com");
expect(() => schema.parse({ email: "not-an-email" })).toThrow();
});
it("generates a URL field that validates URL format", () => {
const schema = generateDynamicZodSchema([
{ id: "f8", label: "Website", key: "website", type: FieldType.URL, order: 0, required: true },
]);
expect(schema.parse({ website: "https://example.com" }).website).toBe("https://example.com");
expect(() => schema.parse({ website: "not-a-url" })).toThrow();
});
it("generates a TEXT field with minLength/maxLength validation", () => {
const schema = generateDynamicZodSchema([
{
id: "f9",
label: "Code",
key: "code",
type: FieldType.TEXT,
order: 0,
required: true,
validation: { minLength: 3, maxLength: 8 },
},
]);
expect(schema.parse({ code: "ABC" }).code).toBe("ABC");
expect(() => schema.parse({ code: "AB" })).toThrow();
expect(() => schema.parse({ code: "TOOLONGCODE" })).toThrow();
});
it("handles a SELECT field with no options as a plain string", () => {
const schema = generateDynamicZodSchema([
{ id: "f10", label: "Tag", key: "tag", type: FieldType.SELECT, order: 0, required: true },
]);
expect(schema.parse({ tag: "anything" }).tag).toBe("anything");
});
});
// ─── CreateOrgUnitSchema ─────────────────────────────────────────────────────
describe("CreateOrgUnitSchema", () => {
it("accepts valid minimal input with defaults", () => {
const result = CreateOrgUnitSchema.parse({ name: "Studio Munich", level: 5 });
expect(result.name).toBe("Studio Munich");
expect(result.level).toBe(5);
expect(result.sortOrder).toBe(0);
expect(result.shortName).toBeUndefined();
expect(result.parentId).toBeUndefined();
});
it("accepts full input with all optional fields", () => {
const result = CreateOrgUnitSchema.parse({
name: "Department VFX",
shortName: "VFX",
level: 6,
parentId: "parent-1",
sortOrder: 3,
});
expect(result.shortName).toBe("VFX");
expect(result.parentId).toBe("parent-1");
expect(result.sortOrder).toBe(3);
});
it("rejects level below 5", () => {
expect(() => CreateOrgUnitSchema.parse({ name: "Test", level: 4 })).toThrow();
});
it("rejects level above 7", () => {
expect(() => CreateOrgUnitSchema.parse({ name: "Test", level: 8 })).toThrow();
});
it("rejects shortName longer than 50 characters", () => {
expect(() =>
CreateOrgUnitSchema.parse({ name: "Test", level: 5, shortName: "x".repeat(51) }),
).toThrow();
});
});
// ─── UpdateOrgUnitSchema ─────────────────────────────────────────────────────
describe("UpdateOrgUnitSchema", () => {
it("accepts empty object (all optional)", () => {
const result = UpdateOrgUnitSchema.parse({});
expect(result.name).toBeUndefined();
expect(result.isActive).toBeUndefined();
});
it("accepts nullable shortName to clear the field", () => {
const result = UpdateOrgUnitSchema.parse({ shortName: null });
expect(result.shortName).toBeNull();
});
it("accepts nullable parentId to unset parent", () => {
const result = UpdateOrgUnitSchema.parse({ parentId: null });
expect(result.parentId).toBeNull();
});
it("accepts isActive flag", () => {
const result = UpdateOrgUnitSchema.parse({ isActive: false });
expect(result.isActive).toBe(false);
});
it("rejects name longer than 200 characters", () => {
expect(() => UpdateOrgUnitSchema.parse({ name: "x".repeat(201) })).toThrow();
});
});
// ─── CreateManagementLevelGroupSchema ───────────────────────────────────────
describe("CreateManagementLevelGroupSchema", () => {
it("accepts valid input with defaults", () => {
const result = CreateManagementLevelGroupSchema.parse({
name: "Senior Management",
targetPercentage: 0.15,
});
expect(result.name).toBe("Senior Management");
expect(result.targetPercentage).toBe(0.15);
expect(result.sortOrder).toBe(0);
});
it("accepts boundary values for targetPercentage (0 and 1)", () => {
expect(
CreateManagementLevelGroupSchema.parse({ name: "A", targetPercentage: 0 }).targetPercentage,
).toBe(0);
expect(
CreateManagementLevelGroupSchema.parse({ name: "B", targetPercentage: 1 }).targetPercentage,
).toBe(1);
});
it("rejects targetPercentage above 1", () => {
expect(() =>
CreateManagementLevelGroupSchema.parse({ name: "Test", targetPercentage: 1.01 }),
).toThrow();
});
it("rejects targetPercentage below 0", () => {
expect(() =>
CreateManagementLevelGroupSchema.parse({ name: "Test", targetPercentage: -0.01 }),
).toThrow();
});
it("rejects empty name", () => {
expect(() =>
CreateManagementLevelGroupSchema.parse({ name: "", targetPercentage: 0.1 }),
).toThrow();
});
});
// ─── UpdateManagementLevelGroupSchema ───────────────────────────────────────
describe("UpdateManagementLevelGroupSchema", () => {
it("accepts empty object", () => {
const result = UpdateManagementLevelGroupSchema.parse({});
expect(result.name).toBeUndefined();
expect(result.targetPercentage).toBeUndefined();
});
it("accepts partial update with only sortOrder", () => {
const result = UpdateManagementLevelGroupSchema.parse({ sortOrder: 5 });
expect(result.sortOrder).toBe(5);
expect(result.name).toBeUndefined();
});
it("rejects targetPercentage out of range in partial update", () => {
expect(() => UpdateManagementLevelGroupSchema.parse({ targetPercentage: 2 })).toThrow();
});
});
// ─── CreateManagementLevelSchema ────────────────────────────────────────────
describe("CreateManagementLevelSchema", () => {
it("accepts valid input", () => {
const result = CreateManagementLevelSchema.parse({ name: "Director", groupId: "grp-1" });
expect(result.name).toBe("Director");
expect(result.groupId).toBe("grp-1");
});
it("rejects empty name", () => {
expect(() => CreateManagementLevelSchema.parse({ name: "", groupId: "grp-1" })).toThrow();
});
it("rejects missing groupId", () => {
expect(() => CreateManagementLevelSchema.parse({ name: "Director" })).toThrow();
});
it("rejects name longer than 100 characters", () => {
expect(() =>
CreateManagementLevelSchema.parse({ name: "x".repeat(101), groupId: "grp-1" }),
).toThrow();
});
});
// ─── UpdateManagementLevelSchema ────────────────────────────────────────────
describe("UpdateManagementLevelSchema", () => {
it("accepts empty object", () => {
const result = UpdateManagementLevelSchema.parse({});
expect(result.name).toBeUndefined();
expect(result.groupId).toBeUndefined();
});
it("accepts partial update with only groupId", () => {
const result = UpdateManagementLevelSchema.parse({ groupId: "grp-2" });
expect(result.groupId).toBe("grp-2");
});
it("rejects empty name in partial update", () => {
expect(() => UpdateManagementLevelSchema.parse({ name: "" })).toThrow();
});
});
// ─── CreateUtilizationCategorySchema ────────────────────────────────────────
describe("CreateUtilizationCategorySchema", () => {
it("accepts minimal valid input with defaults", () => {
const result = CreateUtilizationCategorySchema.parse({ code: "PROD", name: "Production" });
expect(result.code).toBe("PROD");
expect(result.name).toBe("Production");
expect(result.sortOrder).toBe(0);
expect(result.isDefault).toBe(false);
expect(result.description).toBeUndefined();
});
it("accepts full input including description", () => {
const result = CreateUtilizationCategorySchema.parse({
code: "SICK",
name: "Sick Leave",
description: "Covers illness absence",
sortOrder: 2,
isDefault: true,
});
expect(result.isDefault).toBe(true);
expect(result.description).toBe("Covers illness absence");
});
it("rejects code longer than 20 characters", () => {
expect(() =>
CreateUtilizationCategorySchema.parse({ code: "x".repeat(21), name: "Test" }),
).toThrow();
});
it("rejects description longer than 500 characters", () => {
expect(() =>
CreateUtilizationCategorySchema.parse({
code: "ABC",
name: "Test",
description: "x".repeat(501),
}),
).toThrow();
});
it("rejects empty code", () => {
expect(() => CreateUtilizationCategorySchema.parse({ code: "", name: "Test" })).toThrow();
});
});
// ─── UpdateUtilizationCategorySchema ────────────────────────────────────────
describe("UpdateUtilizationCategorySchema", () => {
it("accepts empty object", () => {
const result = UpdateUtilizationCategorySchema.parse({});
expect(result.code).toBeUndefined();
expect(result.isDefault).toBeUndefined();
});
it("accepts nullable description to clear the field", () => {
const result = UpdateUtilizationCategorySchema.parse({ description: null });
expect(result.description).toBeNull();
});
it("accepts isActive flag", () => {
const result = UpdateUtilizationCategorySchema.parse({ isActive: false });
expect(result.isActive).toBe(false);
});
it("rejects code longer than 20 characters in partial update", () => {
expect(() => UpdateUtilizationCategorySchema.parse({ code: "x".repeat(21) })).toThrow();
});
});
// ─── CreateHolidayCalendarSchema ─────────────────────────────────────────────
describe("CreateHolidayCalendarSchema", () => {
it("accepts valid COUNTRY-scope calendar", () => {
const result = CreateHolidayCalendarSchema.parse({
name: "Germany Public Holidays",
scopeType: "COUNTRY",
countryId: "DE",
});
expect(result.name).toBe("Germany Public Holidays");
expect(result.scopeType).toBe("COUNTRY");
expect(result.stateCode).toBeUndefined();
expect(result.priority).toBeUndefined();
});
it("accepts valid STATE-scope calendar with stateCode", () => {
const result = CreateHolidayCalendarSchema.parse({
name: "Bavaria Holidays",
scopeType: "STATE",
countryId: "DE",
stateCode: "BY",
priority: 10,
isActive: true,
});
expect(result.stateCode).toBe("BY");
expect(result.priority).toBe(10);
});
it("rejects invalid scopeType", () => {
expect(() =>
CreateHolidayCalendarSchema.parse({ name: "Test", scopeType: "REGION", countryId: "DE" }),
).toThrow();
});
it("rejects priority below -100", () => {
expect(() =>
CreateHolidayCalendarSchema.parse({
name: "Test",
scopeType: "COUNTRY",
countryId: "DE",
priority: -101,
}),
).toThrow();
});
it("rejects priority above 100", () => {
expect(() =>
CreateHolidayCalendarSchema.parse({
name: "Test",
scopeType: "COUNTRY",
countryId: "DE",
priority: 101,
}),
).toThrow();
});
it("rejects stateCode longer than 16 characters", () => {
expect(() =>
CreateHolidayCalendarSchema.parse({
name: "Test",
scopeType: "STATE",
countryId: "DE",
stateCode: "x".repeat(17),
}),
).toThrow();
});
});
// ─── UpdateHolidayCalendarSchema ─────────────────────────────────────────────
describe("UpdateHolidayCalendarSchema", () => {
it("accepts empty object", () => {
const result = UpdateHolidayCalendarSchema.parse({});
expect(result.name).toBeUndefined();
expect(result.stateCode).toBeUndefined();
});
it("accepts nullable stateCode to clear it", () => {
const result = UpdateHolidayCalendarSchema.parse({ stateCode: null });
expect(result.stateCode).toBeNull();
});
it("accepts partial update with name and isActive", () => {
const result = UpdateHolidayCalendarSchema.parse({ name: "Updated Calendar", isActive: false });
expect(result.name).toBe("Updated Calendar");
expect(result.isActive).toBe(false);
});
it("rejects name longer than 120 characters", () => {
expect(() => UpdateHolidayCalendarSchema.parse({ name: "x".repeat(121) })).toThrow();
});
});
// ─── CreateHolidayCalendarEntrySchema ────────────────────────────────────────
describe("CreateHolidayCalendarEntrySchema", () => {
it("accepts valid entry with date coercion from string", () => {
const result = CreateHolidayCalendarEntrySchema.parse({
holidayCalendarId: "cal-1",
date: "2026-12-25",
name: "Christmas Day",
});
expect(result.holidayCalendarId).toBe("cal-1");
expect(result.date).toBeInstanceOf(Date);
expect(result.name).toBe("Christmas Day");
expect(result.isRecurringAnnual).toBeUndefined();
});
it("accepts full entry with all optional fields", () => {
const result = CreateHolidayCalendarEntrySchema.parse({
holidayCalendarId: "cal-1",
date: "2026-10-03",
name: "German Unity Day",
isRecurringAnnual: true,
source: "Official Federal Calendar",
});
expect(result.isRecurringAnnual).toBe(true);
expect(result.source).toBe("Official Federal Calendar");
});
it("rejects empty name", () => {
expect(() =>
CreateHolidayCalendarEntrySchema.parse({
holidayCalendarId: "cal-1",
date: "2026-01-01",
name: "",
}),
).toThrow();
});
it("rejects missing holidayCalendarId", () => {
expect(() =>
CreateHolidayCalendarEntrySchema.parse({ date: "2026-01-01", name: "New Year" }),
).toThrow();
});
});
// ─── UpdateHolidayCalendarEntrySchema ────────────────────────────────────────
describe("UpdateHolidayCalendarEntrySchema", () => {
it("accepts empty object", () => {
const result = UpdateHolidayCalendarEntrySchema.parse({});
expect(result.name).toBeUndefined();
expect(result.date).toBeUndefined();
});
it("accepts partial update with date coercion", () => {
const result = UpdateHolidayCalendarEntrySchema.parse({ date: "2027-01-01" });
expect(result.date).toBeInstanceOf(Date);
});
it("accepts nullable source to clear it", () => {
const result = UpdateHolidayCalendarEntrySchema.parse({ source: null });
expect(result.source).toBeNull();
});
});
// ─── PreviewResolvedHolidaysSchema ───────────────────────────────────────────
describe("PreviewResolvedHolidaysSchema", () => {
it("accepts valid input with required fields", () => {
const result = PreviewResolvedHolidaysSchema.parse({ countryId: "DE", year: 2026 });
expect(result.countryId).toBe("DE");
expect(result.year).toBe(2026);
expect(result.stateCode).toBeUndefined();
});
it("accepts input with optional stateCode and metroCityId", () => {
const result = PreviewResolvedHolidaysSchema.parse({
countryId: "DE",
stateCode: "BY",
metroCityId: "city-1",
year: 2026,
});
expect(result.stateCode).toBe("BY");
expect(result.metroCityId).toBe("city-1");
});
it("rejects year below 2000", () => {
expect(() => PreviewResolvedHolidaysSchema.parse({ countryId: "DE", year: 1999 })).toThrow();
});
it("rejects year above 2100", () => {
expect(() => PreviewResolvedHolidaysSchema.parse({ countryId: "DE", year: 2101 })).toThrow();
});
it("rejects non-integer year", () => {
expect(() => PreviewResolvedHolidaysSchema.parse({ countryId: "DE", year: 2026.5 })).toThrow();
});
});
// ─── CreateCalculationRuleSchema ─────────────────────────────────────────────
describe("CreateCalculationRuleSchema", () => {
const validRule = {
name: "Public Holiday Charge Rule",
triggerType: "PUBLIC_HOLIDAY",
costEffect: "CHARGE",
chargeabilityEffect: "COUNT",
};
it("accepts minimal valid input with defaults", () => {
const result = CreateCalculationRuleSchema.parse(validRule);
expect(result.name).toBe("Public Holiday Charge Rule");
expect(result.triggerType).toBe("PUBLIC_HOLIDAY");
expect(result.costEffect).toBe("CHARGE");
expect(result.chargeabilityEffect).toBe("COUNT");
expect(result.priority).toBe(0);
expect(result.isActive).toBe(true);
expect(result.description).toBeUndefined();
});
it("accepts full input with all optional fields", () => {
const result = CreateCalculationRuleSchema.parse({
...validRule,
description: "Charges client on public holidays",
triggerType: "SICK",
costEffect: "REDUCE",
costReductionPercent: 50,
projectId: "proj-1",
orderType: "CHARGEABLE",
chargeabilityEffect: "SKIP",
priority: 100,
isActive: false,
});
expect(result.costReductionPercent).toBe(50);
expect(result.triggerType).toBe("SICK");
expect(result.chargeabilityEffect).toBe("SKIP");
expect(result.isActive).toBe(false);
});
it("rejects invalid triggerType", () => {
expect(() =>
CreateCalculationRuleSchema.parse({ ...validRule, triggerType: "OVERTIME" }),
).toThrow();
});
it("rejects costReductionPercent above 100", () => {
expect(() =>
CreateCalculationRuleSchema.parse({ ...validRule, costReductionPercent: 101 }),
).toThrow();
});
it("rejects priority above 1000", () => {
expect(() => CreateCalculationRuleSchema.parse({ ...validRule, priority: 1001 })).toThrow();
});
it("rejects priority below 0", () => {
expect(() => CreateCalculationRuleSchema.parse({ ...validRule, priority: -1 })).toThrow();
});
});
// ─── UpdateCalculationRuleSchema ─────────────────────────────────────────────
describe("UpdateCalculationRuleSchema", () => {
it("accepts minimal input with only required id", () => {
const result = UpdateCalculationRuleSchema.parse({ id: "rule-1" });
expect(result.id).toBe("rule-1");
expect(result.name).toBeUndefined();
expect(result.triggerType).toBeUndefined();
});
it("accepts partial update with name and priority", () => {
const result = UpdateCalculationRuleSchema.parse({
id: "rule-1",
name: "Updated Rule",
priority: 5,
});
expect(result.name).toBe("Updated Rule");
expect(result.priority).toBe(5);
});
it("requires id to be present", () => {
expect(() => UpdateCalculationRuleSchema.parse({ name: "Some Rule" })).toThrow();
});
it("rejects invalid costEffect in partial update", () => {
expect(() =>
UpdateCalculationRuleSchema.parse({ id: "rule-1", costEffect: "IGNORE" }),
).toThrow();
});
});