Files
F-r-ldraledigplannerare/information/how-it-works.md
T
jacobh2 6904961bb4 Sort the loose planning notes into folders
Reference notes move to information/, unstarted feature write-ups to
new-features/, so the repo root stops being a pile of markdown.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 11:21:21 +02:00

137 lines
8.2 KiB
Markdown

# How it works: "Your profile" in Settings
"Your profile" (per parent, `ParentProfileInput` in `frontend/src/components/inputs/ParentProfileInput.tsx`)
holds the facts the planner needs about *you* to decide how to spend your days — as opposed to
facts about the child (birth date) or the household (workspace). Two of its fields actively change
the plan the engine produces. One is a warning-only signal. One field exists but nothing reads it.
This file explains each field: what it means, what the planner does with it, and what happens if
you leave it wrong.
---
## Salary type — `monthly` or `hourly`
**What it means.** Whether your employer pays you a fixed monthly salary regardless of which days
you work, or pays you by the hour for hours actually worked.
**What the planner does with it.** This is the one field that changes *which resource gets spent
first* on an ordinary working day. `backend/app/entities/personal_data.py:21` stores it;
`backend/app/models/personal_data.py:18` validates it on the wire as `monthly` or `hourly`.
The decision lives in `_assign_tier3` in `backend/app/services/allocation_service.py:988-1024`
(the "tier-3 interior" resolution — see `PLANNING_ENGINE_SPEC.md` §3.3). For an ordinary spendable
day that isn't free (weekend/holiday) and isn't next to a red-day cluster:
- **`monthly`** → spend a **vacation** day first; a parental-benefit (FP) day only fills what
vacation can't cover (`allocation_service.py:1011-1017`).
- **`hourly`** → spend an **FP** day first; vacation is only a backstop when FP is exhausted
(`allocation_service.py:1019-1024`).
The reasoning (`PLANNING_ENGINE_SPEC.md` §3.3): a monthly-salaried parent's pay doesn't change
whether they take a vacation day or not — they lose nothing by spending vacation, so spending it
first *preserves* FP days for later, when they're worth more (more weeks of leave, or protection
against the age-4 cutoff). An hourly-paid parent has no salary to protect on a vacation day — no
work, no pay — so there's nothing to gain by spending vacation first; FP goes first because it's the
only resource that pays anything on that date, and vacation is kept in reserve as a fallback.
The same field also gates two tips in `backend/app/services/tips_service.py`: "wrap the weekend
with vacation" (`tips_service.py:418-446`, gated at line 422) and "wrap the red days with vacation"
(`tips_service.py:449-...`, gated at line 456) both `continue` past any parent whose `salary_type`
isn't `monthly` (lines 422, 456). Both tips only make sense for a monthly salary — they suggest
spending vacation to protect pay that only a monthly salary actually pays out on those days.
**What happens if it's wrong.** The plan itself doesn't become invalid — it just optimises for the
wrong thing. Set as `hourly` when you're actually salaried, the planner will burn FP days on
ordinary weeks that you could have covered with vacation for free, running out of FP sooner than
necessary. Set as `monthly` when you're actually hourly, it will spend your vacation on days you'd
have been paid nothing for anyway, needlessly depleting a budget you could have kept.
---
## Employment status — `employed`, `jobseeking`, or `none`
**What it means.** Whether you currently have a job, are registered as a jobseeker (with
Arbetsförmedlingen), or neither.
**What the planner does with it.** This field is **advisory only — it never blocks or reshapes a
plan.** It feeds a single SGI (sjukpenninggrundande inkomst) protection check.
SGI is protected automatically for the child's first 12 months (`sgi_protected_first_months` in
`backend/app/config.py:46`, default 12). After that, a parent who is neither employed nor a
registered jobseeker needs to take parental-benefit days on at least
`sgi_min_days_per_week_after_age_1` days per week (`config.py:47`, default 5) to keep their SGI
from being reassessed downward. `sgi_at_risk` in `backend/app/utils/tip_predicates.py:125-153`
implements this: if `employment_status` is `employed` or `jobseeking`, it returns `False`
immediately (line 141) — protected regardless of how few FP days are planned. Only `none` triggers
the intensity check. The allocator's own warning path,
`_sgi_warnings` in `allocation_service.py:2139-...`, applies the identical
`employment_status in ("employed", "jobseeking")` skip at line 2154, and the resulting tip
(`tips_service.py:398-415`, `_rule_sgi_minimum_after_age_1`) is `severity="warning"`, not an error.
**Why only a warning:** the app has no way to verify someone's actual employment or jobseeker
registration — that lives with the employer and Arbetsförmedlingen, not in this planner. So it
warns "you may be at risk of losing SGI protection" and lets you plan around it; it never refuses
to save a plan or force more FP days into a window because of this field.
**What happens if it's wrong.** Set to `employed`/`jobseeking` when you're actually not working or
registered: you lose the SGI warning, and the app gives no signal that your parental-benefit rate
could be reassessed downward after age 1. Set to `none` when you actually are employed or
registered: you'll see a spurious warning telling you to plan more FP days than you actually need
to protect SGI. Either way, the plan's day allocation itself is untouched.
---
## Minimum vacation days per year — `min_vacation_days_per_year`
**What it means.** The semesterlagen floor: the number of vacation days you must *take* each
calendar year. Sweden's statutory minimum is 20 (`min_vacation_days_per_year: int = 20` in
`backend/app/entities/personal_data.py:28` and `backend/app/config.py:63`); some employers set a
higher floor, so it's editable per parent rather than hardcoded.
**What the planner does with it.** It bounds two things, both about *not saving too much*, never
about *not spending enough*:
- **Vacation carry-forward.** `_carry_vacation_forward_remedy` in `allocation_service.py:1467-1492`
only offers to bank vacation days above this floor for a future year — the floor itself must stay
taken, not banked (`allocation_service.py:1480-1482`).
- **"Use less vacation" surplus advice.** When FP days are expiring and vacation could be swapped
in to save them, `allocation_service.py:2100-2137` only proposes reducing vacation usage down to
this floor, never below it (line 2115).
Deliberately **not** used as a cap on how much vacation you may spend. `PlanningService.
_vacation_days_left`'s docstring is explicit about this
(`backend/app/services/planning_service.py:1219-1224`):
> `PersonalData.min_vacation_days_per_year` is deliberately NOT used here: ... it is a semesterlagen
> floor on days that must be *taken* each year (bounding what may be saved forward), not a cap on
> how much may be spent — turning it into a spending cap here would reject perfectly legal plans.
**What happens if it's wrong.** Set too low, the carry-forward and "use less vacation" advice may
suggest banking or swapping away days you're actually required to take, risking a semesterlagen
violation the app itself won't catch (it only enforces the number you gave it). Set too high, the
planner will keep more vacation "reserved" than necessary and under-suggest carrying days forward
or swapping vacation in for expiring FP — conservative, not wrong, but leaves value on the table.
---
## What's stored but not used: `personnummer_enc`
The profile can optionally hold an encrypted personal-identity-number field
(`personnummer_enc` in `backend/app/entities/personal_data.py:29`). Storing it is **discouraged** —
the entity's own docstring says so (`personal_data.py:7-8`) and recommends `None` if you can avoid
it. It's persisted and loaded like any other field
(`backend/app/repositories_impl/sqlite/personal_data_repository.py`), but **no service reads it** —
it plays no role in planning, tips, or SGI logic. There's no benefit to filling it in.
---
## Summary
| Field | Changes the plan? | How |
|---|---|---|
| `salary_type` | Yes | Decides whether vacation or FP is spent first on an ordinary day |
| `employment_status` | No — warning only | Gates one SGI warning after the child turns 1 |
| `min_vacation_days_per_year` | Yes, at the margins | Bounds vacation carry-forward and surplus advice; never a spending cap |
| `personnummer_enc` | No | Stored, never read |