refactor(planning): give planning calculations one state owner

PlanningCalculationService now builds the submitted request, owns the
pending and accepted calculation state, and delivers explanations for
the accepted calculation only. The duration estimator reads the same
request. The calendar and leave-balance summary consume the accepted
outcome instead of coordinating writable state on the input service.

Refs #275

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nessarandClaude Opus 5.5 committed 2026-10-17 11:11:37 +02:00
1 parent b69b3c3690
commit 5cb0b93339
12 files changed
+708 -322

No files matched your search

@@ -5,6 +5,7 @@ import { RuleService, RuleType } from './rule-service';
import { UserInputService } from './user-input-service';
import { createPlanningCalculation } from './leave-planner.service';
import { PlanningDurationService } from './planning-duration.service';
import { PlanningCalculationService } from './planning-calculation.service';
import { OPTIMIZATION_PRESETS } from './planning-objectives';
describe('Planning duration estimate', () => {
@@ -31,7 +32,8 @@ describe('Planning duration estimate', () => {
it('offers a coarse seconds range on the first launch without calculating', () => {
const duration = TestBed.inject(PlanningDurationService);
expect(duration.estimate()).toEqual({ minimum: 1, maximum: 3, unit: 'seconds' });
expect(TestBed.inject(UserInputService).computeTriggerSignal()).toBe(0);
expect(TestBed.inject(PlanningCalculationService).isCalculating()).toBe(false);
expect(TestBed.inject(PlanningCalculationService).accepted()).toBeNull();
expect(TestBed.inject(RuleService).rules).toEqual([]);
});
@@ -47,7 +49,7 @@ describe('Planning duration estimate', () => {
});
const duration = TestBed.inject(PlanningDurationService);
expect(Number.isFinite(duration.estimate()!.maximum)).toBe(true);
duration.recordCompletion(duration.request()!, 20_000);
duration.recordCompletion(TestBed.inject(PlanningCalculationService).request()!, 20_000);
expect(duration.estimate()).toEqual({ minimum: 10, maximum: 60, unit: 'seconds' });
});
@@ -81,19 +83,20 @@ describe('Planning duration estimate', () => {
expect(duration.estimate()).toEqual(moreLeave);
settings.setPlanningDates('2026-12-31', '2024-01-01');
expect(duration.estimate()).toBeNull();
expect(input.computeTriggerSignal()).toBe(0);
expect(TestBed.inject(PlanningCalculationService).isCalculating()).toBe(false);
expect(TestBed.inject(PlanningCalculationService).accepted()).toBeNull();
});
it('refines the submitted configuration using elapsed calendar time and displays minutes for a long wait', () => {
const duration = TestBed.inject(PlanningDurationService);
const submitted = duration.request()!;
const submitted = TestBed.inject(PlanningCalculationService).request()!;
duration.recordCompletion(submitted, 120_000);
expect(duration.estimate()).toEqual({ minimum: 1, maximum: 6, unit: 'minutes' });
});
it('calibrates against the submitted snapshot even when the form has since changed', () => {
const duration = TestBed.inject(PlanningDurationService);
const submitted = duration.request()!;
const submitted = TestBed.inject(PlanningCalculationService).request()!;
TestBed.inject(CalendarSettingsService).setPlanningDates('2024-01-01', '2026-12-31');
duration.recordCompletion(submitted, 20_000);
expect(duration.estimate()!.maximum).toBeGreaterThan(60);
@@ -114,14 +117,16 @@ describe('Planning duration estimate', () => {
placeType: SelectableDayType.CP,
});
const duration = TestBed.inject(PlanningDurationService);
const calculation = createPlanningCalculation(duration.request()!);
const calculation = createPlanningCalculation(
TestBed.inject(PlanningCalculationService).request()!,
);
duration.recordCompletion(calculation.snapshot, 20_000);
expect(duration.estimate()).toEqual({ minimum: 10, maximum: 60, unit: 'seconds' });
});
it('uses recent typical waits rather than an isolated slow calculation, and resets on a fresh visit', () => {
const duration = TestBed.inject(PlanningDurationService);
const submitted = duration.request()!;
const submitted = TestBed.inject(PlanningCalculationService).request()!;
for (const elapsed of [20_000, 25_000, 900_000])
duration.recordCompletion(submitted, elapsed);
expect(duration.estimate()).toEqual({ minimum: 12, maximum: 75, unit: 'seconds' });
@@ -136,7 +141,7 @@ describe('Planning duration estimate', () => {
it.each([0, -1, NaN, Infinity])('ignores unusable elapsed measurements: %s', (elapsed) => {
const duration = TestBed.inject(PlanningDurationService);
const initial = duration.estimate();
duration.recordCompletion(duration.request()!, elapsed);
duration.recordCompletion(TestBed.inject(PlanningCalculationService).request()!, elapsed);
expect(duration.estimate()).toEqual(initial);
});
});