# 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 ```bash uv sync # install deps (incl. dev group) # --reload-exclude keeps the dev SQLite file (and its -wal/-shm/-journal # siblings) out of the watched tree — a write-heavy request otherwise # self-triggers a reload mid-request and drops the connection, returning a # 503 *after* the write already committed (see frontend/package.json's # dev:backend/login:backend for the same flags). uv run uvicorn app.main:app --reload --reload-exclude '*.db' --reload-exclude '*.db-shm' --reload-exclude '*.db-wal' --reload-exclude '*.db-journal' # 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. ```bash 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).