Files
F-r-ldraledigplannerare/backend
jacobh2 11f2a17b43 Say what would close a shortfall, and where the free days are
A year that cannot afford the whole week was already funded as far as it
goes rather than dropped; there is now a test that says so, across four
years of a repeating wish where only three and a half are affordable.

The advice to move a window onto the red days was looking three days
either side, which can never find Easter. It now searches four weeks
either way — enough for Easter's drift plus the weekday snap — and ranks
placements with the same cost model flexible periods are already placed
with, naming the dates and the days gained.

None of this reached the screen before. The overview now lists each short
window with its gap and the ranked remedies underneath, one line each.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ei7gCZzdd751uZtPzb1Pep
2026-09-19 15:12:32 +02:00
..
2026-06-01 16:48:21 +02:00
2026-06-14 09:58:30 +02:00
2026-06-01 16:48:21 +02:00
2026-06-14 09:58:30 +02:00

Backend — Föräldraledigplannerare

FastAPI + Python 3.14, packages managed with uv. Layered architecture (entities → repositories → services → routers) with dependency injection. Postgres is a partial stub, not a working swap today: only repositories_impl/postgres/vacation_days_repository.py exists; every other app/dependencies.py repository factory raises ValueError for any database_type other than "sqlite". SQLite is the only usable backend until the remaining Postgres repositories land.

Setup

uv sync                 # install deps (incl. dev group)
uv run uvicorn app.main:app --reload   # run dev server on :8000
uv run pytest           # run tests with coverage
uv run ruff check .     # lint

uv run pytest alone does not enforce a coverage floor (pyproject.toml's addopts only requests a report). CI additionally passes --cov-fail-under=80 (see the root CLAUDE.md) — a locally green run can still fail in CI if coverage dropped.

Configuration

Env vars (prefix APP_, nested via __; see .env.example). Settings lives in app/config.py — that file's module docstring mentions a HMAC_PEPPER setting; no such setting exists, so do not look for it here or set it expecting an effect.

  • APP_ENV — development (default) | test | production. In production, Settings refuses to start unless APP_JWT_SECRET is a random value of at least 32 characters (the shipped dev default is rejected). Production also disables /docs, /redoc, and /openapi.json.
  • APP_DATABASE_TYPE — sqlite (default) | postgres (not usable yet — see above).
  • APP_SQLITE_PATH — default ./foraldraledig.db.
  • APP_CORS_ORIGINS — JSON list; default ["http://localhost:5173"].
  • APP_JWT_SECRET — required to be a strong, random secret in production.
  • APP_JWT_ALGORITHM — default HS256.
  • APP_JWT_ACCESS_TOKEN_EXPIRE_MINUTES — default 15.
  • APP_JWT_REFRESH_TOKEN_EXPIRE_DAYS — default 7.
  • APP_LOG_LEVEL — default INFO.
  • APP_SCRAPER_ENABLED — defaults to true. Legal/ToS review of FK scraping + BankID automation completed 2026-06; the scraper is cleared for use. Set to false in any environment without Playwright's Chromium installed (uv run playwright install chromium).
  • APP_SCRAPER_HEADLESS — default true.
  • APP_SCRAPER_SESSION_TTL_SECONDS — default 180; auto-cancels a stalled scraper session.
  • APP_SCRAPER_DEBUG_ENABLED — default false. When enabled, writes personal data to disk (an HTML dump + full-page screenshot of the logged-in FK page — name, balances, payout amounts). Requires APP_SCRAPER_DEBUG_DIR to also be set. Local debugging only; never enable in a shared or production environment.
  • APP_SCRAPER_DEBUG_DIR — where debug artifacts are written when the above is enabled.
  • APP_SCRAPER_DEBUG_DUMP_ON_SUCCESS — default false. Also writes the same debug artifacts (HTML + full-page screenshot) after a successful extraction, not just on error — a capture aid for confirming FK's real DOM (e.g. the family panel structure app/scrapers/fk_scraper.py currently assumes). Same personal-data warning as APP_SCRAPER_DEBUG_ENABLED applies; only takes effect when both APP_SCRAPER_DEBUG_ENABLED and APP_SCRAPER_DEBUG_DIR are also set.
  • APP_SCRAPER_RESULT_RETENTION_SECONDS — default 300; how long a finished scraper session's result stays in memory before being purged.
  • APP_RULES__* — the Swedish parental-leave rule constants (LeaveRules, nested under rules), overridable per field, e.g. APP_RULES__MAX_SAVABLE_DAYS_AT_AGE_4=96. Defaults: 390 sjukpenning days, 90 lägstanivå days, 90 reserved days/parent, 60 max dubbeldagar over the first 15 months, 96 max savable at age 4 (48 per parent when the child has two or more parents — see resolve_age4_savable_cap), age-12 hard expiry, 6 preschool planning days/year, 25 statutory vacation days/year, 20 minimum vacation days/year that must be taken. These are common-case figures only — children born before 2014, multiples, and single custody differ and are not modeled.

Layout

app/
  config.py            # pydantic-settings (app config + frozen LeaveRules)
  dependencies.py      # DI factories (pick repo impl by settings.database_type)
  entities/            # plain dataclasses (domain)
  models/              # pydantic API schemas
  repositories/        # abstract interfaces
  repositories_impl/   # concrete impls per backend (sqlite/, postgres/)
  services/            # business logic
  routers/             # FastAPI routes
  db/connection.py     # connection + (dev) schema bootstrap
  utils/               # errors, logger

Demo data (manual/browser testing)

scripts/seed_demo.py seeds a realistic, ready-to-explore workspace through the normal service/repository layers (no raw SQL) — a couple workspace with a partner, a child born ~6 months ago, vacation rows for only some years (so the is_assumed badge is visible), day overrides, and a spread of leave wishes (fixed, flexible, and one deliberately underfunded to show a shortfall + remedies + surplus advice). Login credentials are fixed and printed at the end.

uv run python scripts/seed_demo.py            # create once; re-running is a no-op
uv run python scripts/seed_demo.py --reset     # wipe the demo workspace and rebuild

Then open http://localhost:5173 and log in with the printed demo@example.com credentials. Safe to re-run: without --reset it detects the existing demo user and changes nothing; --reset only ever deletes the one demo workspace.

Reference vertical slice

VacationDays is implemented end-to-end (entity → schema → repo interface → SQLite impl → service → router → tests). Copy this slice when adding a new entity.

The inline schema in db/connection.py exists only so dev/tests are self-contained. Production schema is owned by Alembic migrations (a senior task).