Files
F-r-ldraledigplannerare/backend
jacobh2 e39ec9a77b Stop the test runner tripping the rate limiter, and level the rows
The end-to-end suite had never been run. Its first run failed six ways,
all of them the page sitting on the login screen: every browser page load
re-obtains an access token, the refresh endpoint allows thirty a minute
per address, and a twenty-test run makes about fifty of them from one
address. Half came back 429, and a failed refresh is indistinguishable
from being logged out. Raising the ceiling would only move it and make a
green suite depend on how fast the machine is, so the limiter is switched
off for the test backend alone — it measures a client, and the runner is
not one. It stays on everywhere else.

With that gone, one real failure was left, and it belonged to the commit
that claimed to have fixed it: cell widths were held, but a week whose
cell carries a holiday name still stood taller than its neighbours,
because each week is its own grid and none can see another's height. The
weeks now share one grid whose rows are all a single fraction, so the
tallest sets the rest and no cell can grow its own row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ei7gCZzdd751uZtPzb1Pep
2026-09-19 16:35:23 +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).