Fix flexible-wish double-booking, extend calendar cache to 2055, add journey tests
Second real bug found by driving the running app while the suites were green. Flexible wishes were funded twice across propose -> apply -> re-propose. Because planned_leave_day carries no wish_id, the engine could not tell that applied days were that wish's own prior occurrence; it treated them as occupied space, routed the candidate search around them, and funded the year again on shifted dates while reporting weeks_funded 1.0. Applying twice turned "one week each spring" into 18 paid days instead of 9. Fixed wishes were unaffected and stay unaffected. Fix (allocation_service.py), per the approved approach of counting already-applied days inside the bounding range rather than adding a wish_id column: - _Ledger._persisted_lookup snapshots existing days BEFORE this run folds in its own freshly-computed windows, so a fixed wish's new placement is never mistaken for a flexible wish's history - _reuse_already_funded_window finds this wish's persisted dates within its bound and reuses the window containing them, via the ordinary fund_window path - full funding therefore yields zero new days and partial funding tops up exactly the remainder, with no duplication and no false shortfall - with no persisted dates, or if the reused window is no longer collision-free, it falls through to the original search, so an unsatisfied year is never frozen Calendar cache extended 2026-2035 -> 2026-2055 (39KB -> 116KB) and the allocator switched to strict, so an uncached year raises UncachedYearError -> ValidationError -> HTTP 422 instead of silently treating Julafton, Nyarsafton and Midsommarafton as working days. Also fixed normalize_day dropping the second name when two holidays share a date (Nationaldagen was being lost). 2049 and 2055 legitimately have 17 red days, not 18: Pingstdagen coincides with Nationaldagen. New backend/tests/integration/test_user_journey.py drives the real app over HTTP across seven journeys: the double-booking guard, the fixed-wish control, apply idempotency, fixed-never-displaced-by-flexible, flexible wishes tracking Easter per year, age-4 arithmetic self-consistency, and a recursive check that no Swedish text reaches the wire now that everything is message_key + data. New backend/scripts/seed_demo.py seeds a realistic workspace through the real service layer for manual testing: demo@example.com / DemoPass123! Backend 414 -> 435 tests. Frontend unchanged at 705. Still not verified in a browser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
Single source of truth for "is this day free" across the app (planning
|
||||
engine, tips engine). Backed by `app/data/swedish_calendar.json` — a
|
||||
validated 10-year cache (2026-2035) built from the Dagsmart API and
|
||||
validated ~30-year cache (2026-2055) built from the Dagsmart API and
|
||||
cross-checked against the `holidays` library. For years outside the cached
|
||||
range, falls back to the `holidays` library directly; the fallback only
|
||||
knows formal röda dagar (+ every Sunday), not de facto days or klämdagar.
|
||||
@@ -107,14 +107,17 @@ class UncachedYearError(RuntimeError):
|
||||
"""Raised by a `strict=True` caller when `year` falls outside
|
||||
`swedish_calendar.json`'s range (or the cache file is missing).
|
||||
|
||||
This should never fire in normal operation: callers that pass
|
||||
`strict=True` (currently only `CalendarService`) are expected to clamp
|
||||
their requested window to `cached_year_bounds()` first. Hitting this is
|
||||
therefore a bug in that clamping, not bad user input — fail loudly
|
||||
instead of silently falling back to the `holidays` library, which gives
|
||||
strictly poorer answers for the same dates (missing de facto days like
|
||||
Julafton/Nyårsafton/Midsommarafton) and would defeat the whole point of
|
||||
the cache (see module docstring; §6 offline/deterministic guarantee).
|
||||
This should never fire in normal operation: `CalendarService` clamps its
|
||||
requested window to `cached_year_bounds()` first, and the planning
|
||||
engine's `AllocationInputLoader` surfaces this as a `ValidationError`
|
||||
instead of letting it escape as a raw `RuntimeError` — see
|
||||
`app.services.allocation_service.AllocationInputLoader.load`. Hitting
|
||||
this in either caller is therefore a bug in that guard, not bad user
|
||||
input — fail loudly instead of silently falling back to the `holidays`
|
||||
library, which gives strictly poorer answers for the same dates (missing
|
||||
de facto days like Julafton/Nyårsafton/Midsommarafton) and would defeat
|
||||
the whole point of the cache (see module docstring; §6 offline/
|
||||
deterministic guarantee).
|
||||
"""
|
||||
|
||||
|
||||
@@ -205,7 +208,7 @@ def cached_year_bounds() -> tuple[int, int] | None:
|
||||
cache file is missing.
|
||||
|
||||
Used by `app.services.calendar_service` to validate/paginate the
|
||||
calendar-days endpoint window without hardcoding the 10-year range.
|
||||
calendar-days endpoint window without hardcoding the cached-year range.
|
||||
"""
|
||||
years = _load_raw_cache()
|
||||
if not years:
|
||||
|
||||
Reference in New Issue
Block a user