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:
@@ -24,7 +24,10 @@ import httpx
|
||||
# Configuration
|
||||
DAGSMART_API_BASE = "https://api.dagsmart.se/v3/holidays"
|
||||
CURRENT_YEAR = datetime.date.today().year
|
||||
YEARS_TO_FETCH = 10 # Fetch 10 years forward from current year (inclusive)
|
||||
YEARS_TO_FETCH = 30 # Fetch 30 years forward from current year (inclusive).
|
||||
# Bump this constant (and rerun this script) whenever the planning horizon
|
||||
# risks approaching the end of the cached range; see AllocationInputLoader's
|
||||
# strict=True check in app/services/allocation_service.py.
|
||||
RATE_LIMIT_DELAY = 2.5 # 5 requests per 10 seconds; conservative margin
|
||||
OUTPUT_DIR = Path(__file__).parent.parent / "app" / "data"
|
||||
OUTPUT_FILE = OUTPUT_DIR / "swedish_calendar.json"
|
||||
@@ -102,22 +105,32 @@ def normalize_day(day: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
Normalizes category to consistent "de_facto" spelling and lowercases names.
|
||||
|
||||
A date can carry more than one `observances` entry when two distinct
|
||||
holidays coincide on the same calendar date (e.g. Pingstdagen and
|
||||
Sveriges nationaldag both land on 2049-06-06 and 2055-06-06, since
|
||||
Pentecost is Easter + 49 days and National Day is fixed at June 6). All
|
||||
observance names are joined with "; " — mirroring the same convention
|
||||
`app/utils/holidays.py` already uses for a holiday that coincides with a
|
||||
Sunday — so a date-collision year never silently loses one of the two
|
||||
names the way taking only `observances[0]` would.
|
||||
|
||||
Args:
|
||||
day: Day record from Dagsmart API response
|
||||
|
||||
Returns:
|
||||
Normalized day record with our schema
|
||||
"""
|
||||
# Extract name from the first observance
|
||||
# Join every observance's name (usually just one) so a same-date
|
||||
# collision between two distinct holidays never drops one silently.
|
||||
observances = day.get("observances", [])
|
||||
name_sv = "Unknown"
|
||||
name_en = "Unknown"
|
||||
|
||||
if observances:
|
||||
primary_observance = observances[0]
|
||||
name_data = primary_observance.get("name", {})
|
||||
name_sv = name_data.get("sv", "Unknown")
|
||||
name_en = name_data.get("en", "Unknown")
|
||||
sv_names = [o.get("name", {}).get("sv", "") for o in observances]
|
||||
en_names = [o.get("name", {}).get("en", "") for o in observances]
|
||||
name_sv = "; ".join(n for n in sv_names if n) or "Unknown"
|
||||
name_en = "; ".join(n for n in en_names if n) or "Unknown"
|
||||
|
||||
# Normalize category: vendor uses "defacto", we use "de_facto"
|
||||
category = day["holidayType"]
|
||||
|
||||
Reference in New Issue
Block a user