Files
F-r-ldraledigplannerare/backend/app/config.py
T
jacobh2 76b1e712b9 Complete NextSteps roadmap: planning engine UI, calendar cluster, flexible wishes
Works through every numbered item in NextSteps260913.md section 3, plus the
three known verification issues in section 4.1.

Surface what the backend already computed:
- 3.1 render surplus_advice in PlanResults, mirroring the shortfall remedies
- 3.2 surface VacationBalance.is_assumed so assumed statutory years are visible
- 3.3 age-4 expiry given the proposed plan, via a new Age4Projection reusing
  the existing _age4_projections arithmetic (Dashboard stat verified, not rebuilt)

Calendar cluster (one pass over the holiday data):
- new GET /api/calendar/days exposing roda dagar, klamdagar and day overrides,
  paginated four years at a time
- 3.4 shade roda dagar/klamdagar, with R/K badges so colour is not the only signal
- 3.6 click-to-override behind an explicit mode toggle, leaving drag multi-select intact
- 3.7 click month/year to advance
- 3.8 per-year red-days list in Settings, statutory days plus overrides

i18n:
- 3.5 five backend-generated Swedish surfaces converted to message_key + data,
  following the tips_service pattern; old string fields replaced, not kept

Flexible wishes (3.9):
- PLANNING_ENGINE_SPEC section 10: window_mode, candidate search, cost function,
  deterministic tie-break, two-pass ordering, test matrix rows 15-24
- backend: entity, migration 006, _FULL_SCHEMA sync, repository, allocator
- frontend: season-preset toggle in WishEditor, unplaced outcome rendering

Fixes (4.1):
- pin typecheck to the local tsc rather than npx resolving a newer TypeScript
- ignore generated coverage/ and dist/ in the ESLint flat config
- fix the flaky usePlanApply test properly, with waitFor rather than a retry

Tests: backend 361 -> 404, frontend 540 -> 704. Coverage 89.47% -> 96.52% lines.

Not yet verified in a browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
2026-09-14 02:33:03 +02:00

98 lines
3.9 KiB
Python

"""Application configuration (pydantic-settings).
`Settings` loads from environment variables (prefix ``APP_``) and an optional
``.env`` file, falling back to the defaults below for local dev. Keep secrets
(JWT_SECRET, HMAC_PEPPER, scraper credentials, ...) in the environment / a
secret store, never in source.
The Swedish parental-leave rule values live in the nested, frozen ``LeaveRules``
model. They are domain defaults rather than routine deployment config, but are
modelled as settings so they're typed, validated, and overridable should a rule
year change (e.g. ``APP_RULES__MAX_SAVABLE_DAYS_AT_AGE_4=96``). NOTE: these are
the common-case figures — children born before 2014, multiples, and single
custody differ and should become explicit config when tackled.
"""
from pydantic import BaseModel, ConfigDict
from pydantic_settings import BaseSettings, SettingsConfigDict
class LeaveRules(BaseModel):
"""Frozen Swedish föräldrapenning rule defaults (single source of truth)."""
model_config = ConfigDict(frozen=True)
total_days_per_child: int = 480
sjukpenning_days: int = 390 # income-based tier
lagsta_niva_days: int = 90 # flat low-rate tier
days_per_parent: int = 240 # default joint-custody split
reserved_days_per_parent: int = 90 # non-transferable ("reserverade dagar")
max_dubbeldagar: int = 60 # both parents, same child-day
dubbeldagar_months_limit: int = 15 # only within the child's first 15 months
max_savable_days_at_age_4: int = 96 # age-4 ceiling
hard_expiry_age_years: int = 12
sgi_protected_first_months: int = 12
sgi_min_days_per_week_after_age_1: int = 5
# Default reserve for preschool planning-day ("studiedagar") closures — the
# per-child value is user-editable; this is just the seeded default.
preschool_planning_days_per_year: int = 6
# Swedish statutory semester entitlement (semesterlagen). Used by
# `AllocationInputLoader` to default a vacation-year the user has not
# entered yet — "not entered" must never be read as "zero days".
statutory_vacation_days_per_year: int = 25
# Semesterlagen requires at least this many vacation days to be *taken*
# each year; only days above it may be saved (up to 5 years). An employer
# may set a higher floor, so this is only the seeded default — the
# per-parent value on PersonalData.min_vacation_days_per_year is what the
# allocator actually enforces (PLANNING_ENGINE_SPEC.md §9.2).
min_vacation_days_per_year: int = 20
@property
def transferable_days_per_parent(self) -> int:
return self.days_per_parent - self.reserved_days_per_parent
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="APP_",
env_nested_delimiter="__",
env_file=".env",
extra="ignore",
)
# 'sqlite' for local dev, 'postgres' for production (swap repository impls).
database_type: str = "sqlite"
sqlite_path: str = "./foraldraledig.db"
# Lock CORS down to the frontend origin(s).
cors_origins: list[str] = ["http://localhost:5173"]
# Auth — change jwt_secret in production (min 32 chars).
jwt_secret: str = "dev-secret-change-in-production-min-32-chars"
jwt_algorithm: str = "HS256"
jwt_access_token_expire_minutes: int = 15
jwt_refresh_token_expire_days: int = 7
log_level: str = "DEBUG"
# FK scraper — legal/ToS review completed (2026-06). Default is False (conservative).
# Set APP_SCRAPER_ENABLED=true in the environment or backend/.env to enable.
# Playwright chromium binaries must be installed: `playwright install chromium`.
scraper_enabled: bool = False
scraper_headless: bool = True
scraper_session_ttl_seconds: int = 180 # auto-cancel awaiting_scan/authenticating sessions
# Set to None to disable debug artifact saving. Files may contain personal data.
scraper_debug_dir: str | None = "./debug/scraper"
rules: LeaveRules = LeaveRules()
settings = Settings()