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:
@@ -194,11 +194,11 @@ class TestKlamdag:
|
||||
assert not is_klamdag(datetime.date(2027, 3, 2))
|
||||
|
||||
def test_klamdag_detected_for_uncached_fallback_year(self) -> None:
|
||||
"""2040 is outside the cached range (2026-2035): 2040-04-30 (Mon) is
|
||||
squeezed between Sunday 4/29 and Första maj (Tue 5/1). `is_klamdag`
|
||||
is computed generically from `is_non_working_day`, so this works even
|
||||
without cached klämdag data for the year."""
|
||||
assert is_klamdag(datetime.date(2040, 4, 30))
|
||||
"""2060 is outside the cached range (2026-2055): 2060-01-02 (Fri) is
|
||||
squeezed between Nyårsdagen (Thu 1/1) and the weekend (Sat 1/3).
|
||||
`is_klamdag` is computed generically from `is_non_working_day`, so
|
||||
this works even without cached klämdag data for the year."""
|
||||
assert is_klamdag(datetime.date(2060, 1, 2))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -234,29 +234,29 @@ class TestGetRedDays:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fallback path for years outside the cached range (2026-2035)
|
||||
# Fallback path for years outside the cached range (2026-2055)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestFallbackForUncachedYear:
|
||||
def test_uncached_year_still_returns_formal_holidays(self) -> None:
|
||||
"""2040 is outside the cache -> falls back to the `holidays` library:
|
||||
"""2060 is outside the cache -> falls back to the `holidays` library:
|
||||
formal röda dagar are still known."""
|
||||
result = get_swedish_holidays(2040)
|
||||
assert datetime.date(2040, 12, 25) in result # Juldagen
|
||||
assert datetime.date(2040, 1, 1) in result # Nyårsdagen (+ Söndag combo)
|
||||
result = get_swedish_holidays(2060)
|
||||
assert datetime.date(2060, 12, 25) in result # Juldagen
|
||||
assert datetime.date(2060, 1, 1) in result # Nyårsdagen (+ Söndag combo)
|
||||
|
||||
def test_uncached_year_does_not_know_de_facto_days(self) -> None:
|
||||
"""Documents the fallback's known limitation: no de facto-day data
|
||||
exists outside the cached range, so julafton is NOT marked free."""
|
||||
assert not is_non_working_day(datetime.date(2040, 12, 24))
|
||||
assert not is_non_working_day(datetime.date(2060, 12, 24))
|
||||
|
||||
def test_uncached_year_easter_sunday_survives_sondag_filter(self) -> None:
|
||||
"""The exact-match trap applies to the fallback path too."""
|
||||
red_days = get_red_days(2040)
|
||||
# Easter 2040: verified against `holidays.Sweden(years=2040)`.
|
||||
red_days = get_red_days(2060)
|
||||
# Easter 2060: verified against `holidays.Sweden(years=2060)`.
|
||||
easter_sunday = next(
|
||||
d for d, name in get_swedish_holidays(2040).items() if "Påskdagen" in name
|
||||
d for d, name in get_swedish_holidays(2060).items() if "Påskdagen" in name
|
||||
)
|
||||
assert easter_sunday in red_days
|
||||
|
||||
|
||||
Reference in New Issue
Block a user