how-it-works.md answers what "Your profile" in the settings is for: salary type is the real lever (monthly spends vacation first, hourly spends parental-benefit days first), employment status only ever warns about SGI, and the vacation floor bounds carry-forward rather than capping spending. dubbeldagar.md answers the question that was asked. The 60 are combined per child, and the code already treats them that way. They are not a separate pot, though: on a doubled date each parent spends one of their own days, so leaving them out of the total would overstate what the family has. The per-parent dubbeldagar_left the scraper stores is never read by the allocator — flagged, not changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ei7gCZzdd751uZtPzb1Pep
8.2 KiB
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_remedyinallocation_service.py:1467-1492only 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-2137only 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_yearis 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 |