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>
8.5 KiB
Dubbeldagar: combined pot, and a corrected premise
Verdict
- Combined, yes. The 60-day dubbeldagar cap is one pot per child, shared by both parents — the code gets this right.
- Not a separate pot — this is the premise to correct. A dubbeldag is not a bonus day on top of the 480. On a doubled date, each parent spends one of their own parental-benefit (FP) days. The 60 caps how many dates may be doubled, not an extra allowance. Excluding dubbeldagar from "the total pot of days" would overstate how many days the family actually has — it would double-count nothing, because there's nothing extra to exclude.
- A per-parent
dubbeldagar_leftvalue is scraped from Försäkringskassan and stored, but the allocator never reads it — see §3 below. Flagged, not fixed.
This answers the question in todo.md: "Is it clear that dubbeldagar is something that two
parents have combined ... not that parent A has e.g. 60 days and parent B has 60 additional days?
... And these days shouldn't be included in the total pot of days to use for parental leave."
1. Combined: one pot per child
The cap is tracked per child, not per parent. In backend/app/services/allocation_service.py:739:
self._dubbeldagar_used: dict[UUID, int] = {}
keyed by child.id. It's checked against _MAX_DUBBELDAGAR = settings.rules.max_dubbeldagar
(default 60, backend/app/config.py:34), and only within
settings.rules.dubbeldagar_months_limit = 15 months from the child's birth date
(config.py:35).
Both checks happen together in _fund_both (allocation_service.py:1225-1234):
eligible_end = child.birth_date + relativedelta(months=_DUBBELDAGAR_MONTHS_LIMIT)
used = self._dubbeldagar_used.get(child.id, 0)
dubbeldagar_legally_available = (
pool_a is not None
and pool_b is not None
and any(d <= eligible_end for d in dates)
and used < _MAX_DUBBELDAGAR
...
)
and the running count is written back once per window at allocation_service.py:1334:
self._dubbeldagar_used[child.id] = used. There is exactly one counter per child; neither parent
has their own 60.
This is independently re-checked at apply time (when a plan is actually saved, not just
proposed) by AnalyticsService.validate_dubbeldagar (backend/app/services/analytics_service.py:221-254),
via PlannedLeaveRepository.count_dubbeldagar(child_id) — again scoped to the child, not a parent,
and again compared against settings.rules.max_dubbeldagar.
So: yes, it's clear in the code, and correct. One combined 60-day pot per child.
2. Not a separate pot — correcting the premise
The todo.md framing — "these days shouldn't be included in the total pot of days to use for
parental leave" — treats a dubbeldag as if it were a bonus day, separate from the 480
(390 sjukpenning + 90 lägstanivå) each parent has. That isn't how the engine models it, and it
isn't how the Swedish rule works: a dubbeldag is a convenience, not an allowance.
Look at the actual funding branch, _fund_both (allocation_service.py:1263-1289). On a date
that's funded as a dubbeldag, both parents get an ordinary ProposedDay charged against their
own FP pool:
self._commit_fp_tier(pool_a, tier_a)
self._commit_fp_tier(pool_b, tier_b)
used += 1
cost[tier_a] += 1
cost[tier_b] += 1
days.append(ProposedDay(d, parent_a.uuid, child.id, tier_a, True, wish.id, REASON_DUBBELDAG))
days.append(ProposedDay(d, parent_b.uuid, child.id, tier_b, True, wish.id, REASON_DUBBELDAG))
self._commit_fp_tier(pool_a, tier_a) decrements parent A's own sjukpenning/lagsta pool by
one; the identical call for pool_b decrements parent B's own pool by one, for the same calendar
date. Two FP days are spent — one from each parent's existing 480 — not one shared day pulled from
somewhere else. PlannedLeaveDay.is_dubbeldag (backend/app/entities/planned_leave.py:16) just
tags that row as counting toward the 60-date quota; it doesn't change which pool it draws from.
So the 60 is a cap on how many dates may be doubled, not extra days on top of the 480+480 the two parents already have. Both parents being off on the same date normally costs the family one FP day and one vacation day (co-leave, see §4). A dubbeldag instead costs two FP days — it lets you trade a vacation day for an extra FP day, for up to 60 dates, early in the child's life. It is not free time; it's a different way to spend time you already had a claim to.
Practical consequence: excluding dubbeldagar from "the total pot of days" would be wrong, because there is no separate pot to exclude — every dubbeldagar-tagged day is already counted inside the ordinary sjukpenning/lägstanivå totals it was drawn from. Subtracting it again would understate the family's real entitlement, not correct for a double-count that doesn't exist.
3. A loose end: a per-parent dubbeldagar_left that nothing consults
There's a field that looks like it might contradict §1 — ParentalLeaveFetch.dubbeldagar_left
(backend/app/entities/parental_leave.py:41, column at
backend/migrations/versions/001_initial_schema.py:132). It's scraped from Försäkringskassan
per (parent, child) (backend/app/scrapers/fk_scraper.py:640-663) and validated on ingest by
backend/app/services/leave_snapshot.py:146-158 (non-negative, ≤ rules.max_dubbeldagar).
It's carried into the allocator's input as DayPool.dubbeldagar_left
(allocation_service.py:2457-2460, sourced from latest.dubbeldagar_left at line 2430, or
defaulted to rules.max_dubbeldagar when there's no FK snapshot yet, line 2438), and again into
the mutable _MutablePool.dubbeldagar_left (allocation_service.py:649, 732).
Grepping every place that field is actually read (\.dubbeldagar_left as an attribute access,
not a constructor argument) turns up exactly four sites, and all four are construction/copy
sites — _MutablePool build-out (line 732), the candidate-probe scratch copy (line 1688), the
DayPool build-out (line 2209), and the initial assignment from the FK snapshot (line 2430).
None of the decision logic in _fund_both — the eligibility check at lines 1225-1234, or the
spend at lines 1263-1289 — ever reads pool.dubbeldagar_left. It relies exclusively on
self._dubbeldagar_used[child.id] and _MAX_DUBBELDAGAR, described in §1. The scraped per-parent
number is loaded, validated, and passed all the way through the pipeline, and then never
consulted.
ARCHITECTURE.md:491 still shows illustrative pseudocode from an earlier design that does
consult it (if latest_fetch.dubbeldagar_left - await self._planned_dubbeldagar(child_id) <= 0) —
that shape was superseded by the combined per-child counter this doc describes, and the
architecture doc wasn't updated to match.
This also makes the field conceptually wrong given §1: FK reports it per parent, but the real
constraint is per child, shared. Two parents' FK snapshots could show two different
dubbeldagar_left numbers for the same child, and neither would be authoritative — the allocator's
own _dubbeldagar_used counter is. Flagging this; not changing it (no code changes in this pass).
TODO: decide whether to (a) stop scraping/storing per-parent dubbeldagar_left and derive
max_dubbeldagar - count_dubbeldagar(child_id) everywhere instead, or (b) keep it only as a
display/cross-check value and make that explicit in the UI. Not decided here — a wrong number is
worse than no number, per CLAUDE.md.
4. Dubbeldagar vs. the default co-leave behaviour
Since the 2026-09-18 correction (PLANNING_ENGINE_SPEC.md §3.5, "co-leave"), a
parent_mode == 'both' window does not default to dubbeldagar. It defaults to co-leave:
one parent funded on FP, the other on vacation, same date (strategy = 'co_leave',
allocation_service.py:1291-1329). Dubbeldagar (strategy = 'dubbeldag') only fire when the
wish's wants_dubbeldag flag is explicitly set (allocation_service.py:1235, 1263) and the
date is legally eligible (§1).
The practical difference, for a single date both parents are home:
| Strategy | Parent A spends | Parent B spends | Net cost |
|---|---|---|---|
| Co-leave (default) | 1 FP day | 1 vacation day | 1 FP + 1 vacation |
| Dubbeldagar (opt-in) | 1 FP day | 1 FP day | 2 FP, 0 vacation |
Dubbeldagar trade a vacation day for an extra FP day — worth doing only while the 15-month/60-date window is open and only if FP days are more plentiful than vacation for that family. Co-leave is the safe default that works for every date, indefinitely, with no quota to track.