import { describe, expect, it } from 'vitest'; import { calculatePlanningInternal, type PlanningArtifacts, type PlanningRequest, } from './leave-planner.core'; import { OPTIMIZATION_PRESETS } from './planning-objectives'; import { createOptimizationPresetRules } from './rule-service'; const presets = createOptimizationPresetRules(); const composedRequest: PlanningRequest & { calculationDate: string } = { calculationDate: '2024-04-01', horizon: { start: '2024-05-06', end: '2024-05-18' }, balances: [ { id: 'cp', type: 'cp', quantity: 3, availableFrom: '2024-05-06', expiryDate: '2024-05-17', }, ], rules: presets, }; describe('integrated optimization and earned leave', () => { it.each(presets)( 'makes disabled $name identical to its absence in the complete configuration', (disabled) => { const absent = calculatePlanningInternal({ ...composedRequest, rules: presets.filter(({ id }) => id !== disabled.id), }); const inactive = calculatePlanningInternal({ ...composedRequest, rules: presets.map((rule) => rule.id === disabled.id ? { ...rule, enabled: false } : rule, ), }); expect(absent.kind).toBe('Plan'); expect(inactive).toEqual(absent); }, ); it('uses the complete configuration after the sole obligation state', () => { let artifacts: PlanningArtifacts = {}; const outcome = calculatePlanningInternal( composedRequest, undefined, (value) => (artifacts = value), ); expect(outcome).toMatchObject({ kind: 'Plan', placements: [{ date: '2024-05-06' }, { date: '2024-05-07' }, { date: '2024-05-10' }], metrics: { unplacedAtRisk: 0, holidayWeekValue: 5, nonWorkingDayGain: 6 }, }); if (artifacts.explanation?.kind !== 'PlanExplanation') throw new Error('Expected an explanation.'); expect( artifacts.explanation.placements[0].objectiveVector.objectives.map(({ id }) => id), ).toEqual([ 'obligation-validity', 'optimization:default-unplaced-at-risk', 'optimization:default-holiday-week-value', 'optimization:default-non-working-day-gain', 'optimization:default-longest-work-sequence', 'optimization:default-short-sequence-deficit', 'optimization:default-supporting-placements', 'balance-expiry', 'placement-date', ]); }); it.each([ { name: 'holiday value first', rules: [presets[1], presets[0], ...presets.slice(2)], dates: ['2024-05-06', '2024-05-07', '2024-05-10'], values: [0, 5, 0, 6, 5, 0, 0], decisive: 'optimization:default-holiday-week-value', }, { name: 'non-working-day gain first', rules: [presets[2], presets[0], presets[1], ...presets.slice(3)], dates: ['2024-05-03', '2024-05-10', '2024-05-17'], values: [0, 9, 0, 0, 4, 4, 0], decisive: 'optimization:default-non-working-day-gain', }, { name: 'maximum holiday value after loss prevention', rules: [ presets[0], { ...presets[1], configuration: { ...presets[1].configuration, aggregation: 'maximum' as const, }, }, ...presets.slice(2), ], dates: ['2024-05-06', '2024-05-07', '2024-05-10'], values: [0, 0, 5, 6, 5, 0, 0], decisive: 'optimization:default-unplaced-at-risk', }, ])('preserves complete vectors and placement explanations with $name', (fixture) => { let artifacts: PlanningArtifacts = {}; const outcome = calculatePlanningInternal( { ...composedRequest, horizon: { start: '2024-04-29', end: '2024-05-18' }, balances: [{ id: 'cp', type: 'cp', quantity: 3, expiryDate: '2024-05-17' }], rules: fixture.rules, }, undefined, (value) => (artifacts = value), ); if (outcome.kind !== 'Plan' || artifacts.explanation?.kind !== 'PlanExplanation') throw new Error('Expected a complete plan and explanation.'); expect(outcome.placements.map(({ date }) => date)).toEqual(fixture.dates); for (const placement of artifacts.explanation.placements) { expect(placement.objectiveVector.objectives.map(({ value }) => value)).toEqual([ ...fixture.values, '2024-05-17|2024-05-17|2024-05-17', fixture.dates.join('|'), ]); expect(placement.firstDecisiveObjective).toBe(fixture.decisive); expect(placement.ruleIds).toEqual([fixture.decisive.slice('optimization:'.length)]); } }); it('preserves the first equivalent funder while evaluating typed window alternatives', () => { const outcome = calculatePlanningInternal({ ...composedRequest, balances: ['first-cp', 'second-cp', 'rtt'].map((id) => ({ id, type: id === 'rtt' ? 'rtt' : 'cp', quantity: 1, availableFrom: '2024-05-06', expiryDate: '2024-05-17', })), rules: [ presets[0], { id: 'typed-window', type: 'time_window', startDate: '2024-05-07', endDate: '2024-05-07', placeDays: 1, placeType: 'rtt', }, ...presets.slice(1), ], }); expect(outcome).toMatchObject({ kind: 'Plan', placements: [ { date: '2024-05-06', balanceId: 'second-cp' }, { date: '2024-05-07', balanceId: 'rtt' }, { date: '2024-05-10', balanceId: 'first-cp' }, ], preferences: [{ ruleId: 'typed-window', satisfied: 1 }], metrics: { unplacedAtRisk: 0, holidayWeekValue: 5, nonWorkingDayGain: 6 }, }); }); it.each(['maximum', 'unsafe-total'] as const)( 'keeps distinct funding evaluations when equivalence cannot be proved (%s)', (control) => { const outcome = calculatePlanningInternal({ calculationDate: '2024-01-01', horizon: { start: '2024-01-02', end: '2024-01-03' }, balances: [ { id: 'first', type: 'cp', quantity: control === 'maximum' ? 1 : Number.MAX_SAFE_INTEGER, expiryDate: '2024-01-02', }, { id: 'second', type: 'rtt', quantity: control === 'maximum' ? 2 : 1, expiryDate: '2024-01-02', }, ], rules: [ presets[0], ...(control === 'maximum' ? [ { id: 'largest-balance', type: 'optimization', configuration: { ...OPTIMIZATION_PRESETS.unplacedAtRisk, aggregation: 'maximum', }, }, ] : []), ], }); expect(outcome).toMatchObject({ kind: 'Plan', placements: [{ balanceId: control === 'maximum' ? 'second' : 'first' }], }); }, ); it('finishes a renamed first quantity rule at zero and still evaluates later priorities and final ties', () => { const loss = { ...presets[0], id: 'personal-quantity', name: 'Personal quantity' }; const run = (rules: PlanningRequest['rules']) => { const outcome = calculatePlanningInternal({ ...composedRequest, rules }); return { outcome }; }; const window = { id: 'already-satisfied', type: 'time_window', startDate: '2024-05-06', endDate: '2024-05-06', placeDays: 1, placeType: 'cp', }; const first = run([loss, window, ...presets.slice(1)]); expect(first.outcome).toMatchObject({ kind: 'Plan', placements: [{ date: '2024-05-06' }, { date: '2024-05-07' }, { date: '2024-05-10' }], metrics: { unplacedAtRisk: 0, holidayWeekValue: 5, nonWorkingDayGain: 6 }, }); const reordered = run([presets[1], loss, ...presets.slice(2)]); expect(reordered.outcome.kind).toBe('Plan'); }); it.each(['sum', 'count', 'maximum'] as const)( 'finishes a configured maximize holiday prefix at its evaluator bound (%s)', (aggregation) => { const holiday = { ...presets[1], id: 'personal-holiday', configuration: { ...OPTIMIZATION_PRESETS.holidayWeekValue, aggregation }, }; const outcome = calculatePlanningInternal({ ...composedRequest, rules: [presets[0], holiday, ...presets.slice(2)], }); expect(outcome).toMatchObject({ kind: 'Plan', placements: [ { date: '2024-05-06' }, { date: '2024-05-07' }, { date: '2024-05-10' }, ], metrics: { unplacedAtRisk: 0, holidayWeekValue: 5, nonWorkingDayGain: 6 }, }); }, ); it('commits a strict holiday bundle before deferring old generic alternatives and preserves higher rules', () => { const outcome = calculatePlanningInternal({ calculationDate: '2026-11-01', horizon: { start: '2026-11-02', end: '2026-11-14' }, balances: [{ id: 'cp', type: 'cp', quantity: 3, expiryDate: '2026-11-30' }], declaredDays: ['2026-11-09', '2026-11-10'].map((date) => ({ date, type: 'other' })), rules: [ { id: 'mandatory', type: 'time_window', force: 'obligation', startDate: '2026-11-02', endDate: '2026-11-02', placeDays: 1, placeType: 'cp', }, presets[0], { id: 'earlier-window', type: 'time_window', startDate: '2026-11-02', endDate: '2026-11-02', placeDays: 1, placeType: 'cp', }, ...presets.slice(1), ], }); expect(outcome).toMatchObject({ kind: 'Plan', placements: [{ date: '2026-11-02' }, { date: '2026-11-12' }, { date: '2026-11-13' }], preferences: [{ ruleId: 'earlier-window', satisfied: 1 }], metrics: { unplacedAtRisk: 0, holidayWeekValue: 3 }, }); }); it.each([false, true])( 'earns an already-complete preference occurrence before comparisons (minimize every balance: %s)', (minimizeEveryBalance) => { const request: PlanningRequest & { calculationDate: string } = { calculationDate: '2026-11-01', horizon: { start: '2026-11-09', end: '2026-11-14' }, balances: [{ id: 'cp', type: 'cp', quantity: 1, expiryDate: '2027-01-01' }], rules: [ { id: 'mandatory', type: 'time_window', force: 'obligation', startDate: '2026-11-12', endDate: '2026-11-12', placeDays: 1, placeType: 'cp', }, ...(minimizeEveryBalance ? [ { id: 'all-balances', type: 'optimization' as const, configuration: { ...OPTIMIZATION_PRESETS.unplacedAtRisk, conditions: [], }, }, ] : []), { id: 'producer', type: 'if_then', dayOfWeek: 3, placeDays: 1, placeType: 'cp', placement: 'after', rewardDays: 1, rewardType: 'rtt', }, ], }; let artifacts: PlanningArtifacts = {}; const outcome = calculatePlanningInternal( request, undefined, (value) => (artifacts = value), ); expect(outcome).toMatchObject({ kind: 'Plan', placements: [ { date: '2026-11-12', type: 'cp', ruleIds: ['mandatory', 'producer'] }, ], preferences: [{ ruleId: 'producer', satisfied: 1 }], remainingBalances: [ { id: 'earned:producer:2026-11-11', type: 'rtt', quantity: 1, availableFrom: '2026-11-13', expiryDate: '2027-11-13', sourceRuleId: 'producer', }, ], }); expect(artifacts.finalState?.earnedActivations).toEqual([ { id: 'earned:producer:2026-11-11', ruleId: 'producer', occurrence: '2026-11-11', dates: ['2026-11-12'], }, ]); if (artifacts.explanation?.kind !== 'PlanExplanation') throw new Error('Expected an explanation.'); expect(artifacts.explanation.placements[0].contributions).toContainEqual( expect.objectContaining({ kind: 'rule', ruleId: 'producer' }), ); expect(artifacts.explanation.ifThenContributions).toContainEqual( expect.objectContaining({ ruleId: 'producer', earnedEntitlement: { id: 'earned:producer:2026-11-11', type: 'rtt', quantity: 1, availableFrom: '2026-11-13', expiryDate: '2027-11-13', added: true, }, }), ); if (minimizeEveryBalance) expect( artifacts.explanation.placements[0].objectiveVector.objectives, ).toContainEqual( expect.objectContaining({ id: 'optimization:all-balances', value: 1 }), ); }, ); });