Files
F-r-ldraledigplannerare/backend/app/config.py
T
jacobh2 c15cd44cb4 Warn when saving unevenly costs the family days
Only ninety-six days survive a child's fourth birthday, and with two parents
that is forty-eight each rather than a shared pool. A parent sitting on sixty
days while the other has ten therefore forfeits twelve that the family could
have kept, and nothing said so.

The advice leads with taking the days as leave before the birthday, which
turns them into actual time at home, and offers transferring only as the
fallback that merely preserves them. The transferable figure is bounded by
what Försäkringskassan actually reports as transferable, since the reserved
days cannot move, and the copy says plainly that a transfer happens there and
not in this app. Without the partner's figures the tip hedges rather than
inventing a number.
2026-09-17 22:31:37 +02:00

111 lines
4.7 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 — per CHILD (pooled across parents)
# Age-4 ceiling — per PARENT. The 96-day figure above is a per-child pool;
# when a child has two parents, each parent's own savable balance is
# capped at half of that (48), not 96 each (which would let a two-parent
# family save 192 combined). `app.utils.tip_predicates.resolve_age4_savable_cap`
# is the single place that decides which of the two applies.
max_savable_days_per_parent_at_age_4: int = 48
hard_expiry_age_years: int = 12
# A couple's per-parent age-4 pooling-loss tip (see
# `app.utils.tip_predicates.age4_pooling_loss`) only starts warning once
# the child has turned this many years old — the first couple of years
# are too early to usefully act on, so don't nag before then.
age4_pooling_tip_min_child_age_years: int = 2
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); cleared for use.
# Default is True. Set APP_SCRAPER_ENABLED=false to disable (e.g. an
# environment without Playwright chromium installed).
# Playwright chromium binaries must be installed: `playwright install chromium`.
scraper_enabled: bool = True
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()