Let a household choose to be paid for the red days
Asking for a full week off never spent anything on the Saturday, the Sunday or a holiday, and a test now says so outright rather than leaving it to be rediscovered. Those days are free because vacation cannot be taken on a day that is not a working day. Parental benefit is a payment rather than a schedule, though, so it can be drawn on a red day to turn an unpaid day off into a paid one — at the cost of a day the family would otherwise keep. That is now a workspace setting, off by default, and it never reaches for a vacation day. With it off the planner behaves exactly as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ei7gCZzdd751uZtPzb1Pep
This commit is contained in:
@@ -25,7 +25,8 @@ _FULL_SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS workspace (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
take_pl_on_red_days BOOLEAN NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS personal_data (
|
||||
|
||||
@@ -10,3 +10,9 @@ class Workspace:
|
||||
id: UUID
|
||||
name: str
|
||||
created_at: datetime | None = None
|
||||
# §todo item 2 — per-workspace opt-in, default False: allow the planning
|
||||
# engine to fund a free day (Saturday, Sunday, public holiday) with a
|
||||
# parental-benefit day instead of leaving it unpaid. Never vacation (see
|
||||
# `app.services.allocation_service.REASON_FP_RED_DAY`). Threaded into the
|
||||
# allocator by `AllocationInputLoader.load` as `AllocationInput.take_pl_on_red_days`.
|
||||
take_pl_on_red_days: bool = False
|
||||
|
||||
@@ -15,12 +15,17 @@ class WorkspaceCreate(BaseModel):
|
||||
|
||||
class WorkspaceUpdate(BaseModel):
|
||||
name: str | None = Field(default=None, min_length=1, max_length=100)
|
||||
# §todo item 2 — `None` means "leave unchanged"; only an explicit
|
||||
# true/false updates the stored flag (mirrors `name`'s omit-to-keep
|
||||
# convention above).
|
||||
take_pl_on_red_days: bool | None = None
|
||||
|
||||
|
||||
class WorkspaceResponse(BaseModel):
|
||||
id: UUID
|
||||
name: str
|
||||
created_at: datetime | None
|
||||
take_pl_on_red_days: bool
|
||||
|
||||
@classmethod
|
||||
def from_entity(cls, ws: Workspace) -> "WorkspaceResponse":
|
||||
@@ -28,6 +33,7 @@ class WorkspaceResponse(BaseModel):
|
||||
id=ws.id,
|
||||
name=ws.name,
|
||||
created_at=ws.created_at,
|
||||
take_pl_on_red_days=ws.take_pl_on_red_days,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -26,19 +26,21 @@ class SQLiteWorkspaceRepository(WorkspaceRepository):
|
||||
id=UUID(row["id"]),
|
||||
name=row["name"],
|
||||
created_at=datetime.fromisoformat(row["created_at"]),
|
||||
take_pl_on_red_days=bool(row["take_pl_on_red_days"]),
|
||||
)
|
||||
|
||||
async def create(self, item: Workspace) -> Workspace:
|
||||
item.created_at = item.created_at or datetime.now()
|
||||
await self._conn.execute(
|
||||
"""
|
||||
INSERT INTO workspace (id, name, created_at)
|
||||
VALUES (?, ?, ?)
|
||||
INSERT INTO workspace (id, name, created_at, take_pl_on_red_days)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
str(item.id),
|
||||
item.name,
|
||||
item.created_at.isoformat(),
|
||||
int(item.take_pl_on_red_days),
|
||||
),
|
||||
)
|
||||
await commit(self._conn)
|
||||
@@ -51,8 +53,8 @@ class SQLiteWorkspaceRepository(WorkspaceRepository):
|
||||
|
||||
async def update(self, id: UUID, item: Workspace) -> Workspace:
|
||||
cursor = await self._conn.execute(
|
||||
"UPDATE workspace SET name = ? WHERE id = ?",
|
||||
(item.name, str(id)),
|
||||
"UPDATE workspace SET name = ?, take_pl_on_red_days = ? WHERE id = ?",
|
||||
(item.name, int(item.take_pl_on_red_days), str(id)),
|
||||
)
|
||||
await commit(self._conn)
|
||||
if cursor.rowcount == 0:
|
||||
|
||||
@@ -84,7 +84,9 @@ async def update_workspace(
|
||||
service: WorkspaceServiceDep,
|
||||
) -> WorkspaceResponse:
|
||||
try:
|
||||
ws = await service.update_workspace(workspace_id, payload.name)
|
||||
ws = await service.update_workspace(
|
||||
workspace_id, payload.name, take_pl_on_red_days=payload.take_pl_on_red_days
|
||||
)
|
||||
except (NotFoundError, ValidationError) as exc:
|
||||
code = (
|
||||
status.HTTP_404_NOT_FOUND
|
||||
|
||||
@@ -62,6 +62,15 @@ REASON_FP_LAGSTA_STRETCH = "fp_lagsta_stretch"
|
||||
REASON_SWAP_FIRST_HALF = "swap_first_half"
|
||||
REASON_SWAP_SECOND_HALF = "swap_second_half"
|
||||
REASON_DUBBELDAG = "dubbeldag"
|
||||
# §todo item 2 — a free day (Saturday, Sunday or public holiday) funded with a
|
||||
# parental-benefit day instead of being left free, opted into per-workspace via
|
||||
# `AllocationInput.take_pl_on_red_days` (default False — see `_fund_solo`/
|
||||
# `_fund_both`). Vacation is deliberately never used here: Swedish law does not
|
||||
# let you "take vacation" on a day that already isn't a working day, but FP is
|
||||
# a benefit payment, not a scheduling construct, so it can still be drawn on a
|
||||
# red day to replace the (unpaid) day off with a paid one — at the cost of
|
||||
# spending a day the household would otherwise have kept.
|
||||
REASON_FP_RED_DAY = "fp_red_day"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Surplus advice kinds (§9.2) — the mirror of a shortfall remedy: emitted
|
||||
@@ -219,6 +228,15 @@ class AllocationInput:
|
||||
# despite the calendar. `is_free_day` is the single place these are
|
||||
# merged on top of `holidays`.
|
||||
day_overrides: dict[date, bool] = field(default_factory=dict)
|
||||
# §todo item 2 — per-workspace opt-in (default False, byte-identical to
|
||||
# pre-existing behaviour when unset): allow a free day (Saturday, Sunday,
|
||||
# public holiday) inside a funded window to be paid with an FP day
|
||||
# (never vacation — see `REASON_FP_RED_DAY`). Read only by `_fund_solo`/
|
||||
# `_fund_both`; deliberately NOT consulted by `is_free_day` itself, which
|
||||
# stays the single, opt-in-agnostic definition of "is this day free" (it
|
||||
# is also used for costing/klämdag-cluster detection, where the day must
|
||||
# stay classified as free regardless of whether it ends up FP-funded).
|
||||
take_pl_on_red_days: bool = False
|
||||
# §fix (parent-reassignment moves dates, decision B) — the FULL,
|
||||
# never-filtered snapshot of every persisted `planned_leave_day` row for
|
||||
# this workspace, independent of `existing_planned` above (which
|
||||
@@ -1057,6 +1075,25 @@ class _Ledger:
|
||||
continue
|
||||
|
||||
if is_free_day(d, self._input.holidays, self._input.day_overrides):
|
||||
fp_on_red_day = (
|
||||
self._spend_fp(pool_key, prefer="sjukpenning")
|
||||
if self._input.take_pl_on_red_days and pool_key is not None
|
||||
else None
|
||||
)
|
||||
if fp_on_red_day is not None:
|
||||
cost[fp_on_red_day] += 1
|
||||
days.append(
|
||||
ProposedDay(
|
||||
d,
|
||||
parent.uuid,
|
||||
child_id,
|
||||
fp_on_red_day,
|
||||
False,
|
||||
wish.id,
|
||||
REASON_FP_RED_DAY,
|
||||
)
|
||||
)
|
||||
continue
|
||||
cost["free"] += 1
|
||||
reason = free_reason_override or (
|
||||
REASON_FREE_RED_DAY
|
||||
@@ -1248,14 +1285,42 @@ class _Ledger:
|
||||
continue
|
||||
|
||||
if is_free_day(d, self._input.holidays, self._input.day_overrides):
|
||||
cost["free"] += 1
|
||||
reason = (
|
||||
# §todo item 2 — each parent's own FP pool is tried
|
||||
# independently (never vacation, never a peeked-then-
|
||||
# committed-together pair like dubbeldagar/co-leave above):
|
||||
# one parent running out of FP must never stop the other
|
||||
# from still getting paid for this same red day.
|
||||
free_reason = (
|
||||
REASON_FREE_RED_DAY
|
||||
if _is_real_red_day(d, self._input.holidays)
|
||||
else REASON_FREE_WEEKEND
|
||||
)
|
||||
days.append(ProposedDay(d, parent_a.uuid, child.id, "free", False, wish.id, reason))
|
||||
days.append(ProposedDay(d, parent_b.uuid, child.id, "free", False, wish.id, reason))
|
||||
for red_parent, red_pool in ((parent_a, pool_a), (parent_b, pool_b)):
|
||||
fp_on_red_day = (
|
||||
self._spend_fp((red_parent.uuid, child.id), prefer="sjukpenning")
|
||||
if self._input.take_pl_on_red_days and red_pool is not None
|
||||
else None
|
||||
)
|
||||
if fp_on_red_day is not None:
|
||||
cost[fp_on_red_day] += 1
|
||||
days.append(
|
||||
ProposedDay(
|
||||
d,
|
||||
red_parent.uuid,
|
||||
child.id,
|
||||
fp_on_red_day,
|
||||
False,
|
||||
wish.id,
|
||||
REASON_FP_RED_DAY,
|
||||
)
|
||||
)
|
||||
else:
|
||||
cost["free"] += 1
|
||||
days.append(
|
||||
ProposedDay(
|
||||
d, red_parent.uuid, child.id, "free", False, wish.id, free_reason
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
funded = False
|
||||
@@ -2550,6 +2615,7 @@ class AllocationInputLoader:
|
||||
existing_planned=existing_planned,
|
||||
holidays=holidays_map,
|
||||
day_overrides=day_overrides,
|
||||
take_pl_on_red_days=ws.take_pl_on_red_days,
|
||||
all_existing_planned=all_existing_planned,
|
||||
)
|
||||
|
||||
|
||||
@@ -82,10 +82,14 @@ class WorkspaceService:
|
||||
self,
|
||||
workspace_id: UUID,
|
||||
name: str | None = None,
|
||||
*,
|
||||
take_pl_on_red_days: bool | None = None,
|
||||
) -> Workspace:
|
||||
ws = await self.get_workspace(workspace_id)
|
||||
if name is not None:
|
||||
ws.name = name
|
||||
if take_pl_on_red_days is not None:
|
||||
ws.take_pl_on_red_days = take_pl_on_red_days
|
||||
return await self._ws.update(workspace_id, ws)
|
||||
|
||||
async def list_members(self, workspace_id: UUID) -> list[PersonalData]:
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Add workspace.take_pl_on_red_days.
|
||||
|
||||
Per-workspace opt-in (default off) letting the planning engine fund a free
|
||||
day (Saturday, Sunday, public holiday) with a parental-benefit day instead of
|
||||
leaving it unpaid — never with a vacation day, since Swedish law does not let
|
||||
you "take vacation" on a day that already isn't a working day. See
|
||||
`app.services.allocation_service.REASON_FP_RED_DAY` and
|
||||
`AllocationInput.take_pl_on_red_days`.
|
||||
|
||||
A plain `ADD COLUMN` suffices here (unlike 002's CHECK-constraint change,
|
||||
which needed a full table rebuild) since SQLite supports adding a NOT NULL
|
||||
column with a constant DEFAULT directly.
|
||||
|
||||
Must stay byte-for-byte equivalent to the `workspace` table in
|
||||
`app/db/connection.py`'s `_FULL_SCHEMA` — `tests/integration/test_schema_parity.py`
|
||||
asserts exactly that.
|
||||
|
||||
Revision ID: 003
|
||||
Revises: 002
|
||||
Create Date: 2026-09-19
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "003"
|
||||
down_revision: str | None = "002"
|
||||
branch_labels: str | None = None
|
||||
depends_on: str | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute("ALTER TABLE workspace ADD COLUMN take_pl_on_red_days BOOLEAN NOT NULL DEFAULT 0")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("ALTER TABLE workspace DROP COLUMN take_pl_on_red_days")
|
||||
@@ -25,6 +25,7 @@ from app.entities.leave_wish import LeaveWish
|
||||
from app.entities.planned_leave import PlannedLeaveDay
|
||||
from app.services.allocation_service import (
|
||||
REASON_DUBBELDAG,
|
||||
REASON_FP_RED_DAY,
|
||||
REASON_VACATION_WRAP_RED_DAY,
|
||||
REASON_VACATION_WRAP_WEEKEND,
|
||||
STRATEGY_UNPLACED,
|
||||
@@ -140,6 +141,7 @@ def _build_input(
|
||||
extra_holiday_years: tuple[int, ...] = (),
|
||||
day_overrides: dict[date, bool] | None = None,
|
||||
age4_savable_cap: int | None = None,
|
||||
take_pl_on_red_days: bool = False,
|
||||
) -> AllocationInput:
|
||||
years = {today.year, horizon_end.year, horizon_end.year + 1, *extra_holiday_years}
|
||||
if age4_savable_cap is None:
|
||||
@@ -161,6 +163,7 @@ def _build_input(
|
||||
existing_planned=existing_planned or [],
|
||||
holidays=_holidays_for(*years),
|
||||
day_overrides=day_overrides or {},
|
||||
take_pl_on_red_days=take_pl_on_red_days,
|
||||
all_existing_planned=all_existing_planned,
|
||||
)
|
||||
|
||||
@@ -2520,3 +2523,245 @@ class TestCase29FixedYearlyWishPreservesWeekdayAlignment:
|
||||
assert wish.window_for_year(2026) == (date(2026, 3, 1), date(2026, 5, 31))
|
||||
assert wish.window_for_year(2027) == (date(2027, 3, 1), date(2027, 5, 31))
|
||||
assert wish.window_for_year(2028) == (date(2028, 3, 1), date(2028, 5, 31))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# todo.md item 2, Part 1 — regression: a free day (Saturday, Sunday, public
|
||||
# holiday) must never spend a vacation or FP day unless the workspace
|
||||
# explicitly opts in via `take_pl_on_red_days`.
|
||||
#
|
||||
# Window: Mon 2027-03-22 -> Sun 2027-03-28 (verified against
|
||||
# `get_swedish_holidays(2027)` directly):
|
||||
# Mon 22, Tue 23, Wed 24, Thu 25 -- ordinary spendable weekdays, no holiday.
|
||||
# Fri 26 -- Långfredagen (a REAL weekday public holiday).
|
||||
# Sat 27 -- Påskafton (weekend; also separately named).
|
||||
# Sun 28 -- "Påskdagen; Söndag" (weekend; a real holiday, not just the
|
||||
# generic Sunday marker).
|
||||
# 4 spendable days, 3 free days (one of which is a real red day even before
|
||||
# counting the weekend).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_RED_WEEK = ((3, 22), (3, 28))
|
||||
_RED_WEEK_FREE_DATES = (date(2027, 3, 26), date(2027, 3, 27), date(2027, 3, 28))
|
||||
|
||||
|
||||
class TestCase30FreeDaysNeverSpendResourcesByDefault:
|
||||
def test_weekend_and_red_day_spend_nothing_solo(self) -> None:
|
||||
child = _child(birth_date=date(2020, 1, 1))
|
||||
parent = _parent(salary_type="monthly")
|
||||
wish = _wish(
|
||||
child_id=child.id, start=_RED_WEEK[0], end=_RED_WEEK[1], weeks=1, first_year=2027
|
||||
)
|
||||
alloc_input = _build_input(
|
||||
today=date(2027, 1, 1),
|
||||
horizon_end=date(2027, 12, 31),
|
||||
children=[child],
|
||||
parents=[parent],
|
||||
pools={(parent.uuid, child.id): DayPool(200, 90, 60)},
|
||||
vacation={(parent.uuid, 2027): VacationBudget(25, 0, 0)},
|
||||
wishes=[wish],
|
||||
# take_pl_on_red_days defaults to False -- the point of this test.
|
||||
)
|
||||
|
||||
proposal = Allocator().propose(alloc_input)
|
||||
|
||||
days_by_date = {d.date: d for d in proposal.days}
|
||||
for free_date in _RED_WEEK_FREE_DATES:
|
||||
assert days_by_date[free_date].day_type == "free"
|
||||
|
||||
outcome = proposal.outcomes[0]
|
||||
assert outcome.cost_breakdown["free"] == 3
|
||||
# The 4 ordinary weekdays (Mon-Thu) are the only days that spend
|
||||
# anything at all -- all via vacation (monthly-salary tier-3 policy).
|
||||
assert outcome.cost_breakdown["vacation"] == 4
|
||||
assert outcome.cost_breakdown["sjukpenning"] == 0
|
||||
assert outcome.cost_breakdown["lagsta"] == 0
|
||||
|
||||
def test_weekend_and_red_day_spend_nothing_both_parents(self) -> None:
|
||||
child = _child(birth_date=date(2020, 1, 1))
|
||||
parent_a = _parent(name="A")
|
||||
parent_b = _parent(name="B")
|
||||
wish = _wish(
|
||||
child_id=child.id,
|
||||
start=_RED_WEEK[0],
|
||||
end=_RED_WEEK[1],
|
||||
weeks=1,
|
||||
first_year=2027,
|
||||
parent_mode="both",
|
||||
)
|
||||
alloc_input = _build_input(
|
||||
today=date(2027, 1, 1),
|
||||
horizon_end=date(2027, 12, 31),
|
||||
children=[child],
|
||||
parents=[parent_a, parent_b],
|
||||
pools={
|
||||
(parent_a.uuid, child.id): DayPool(200, 90, 60),
|
||||
(parent_b.uuid, child.id): DayPool(200, 90, 60),
|
||||
},
|
||||
vacation={
|
||||
(parent_a.uuid, 2027): VacationBudget(25, 0, 0),
|
||||
(parent_b.uuid, 2027): VacationBudget(25, 0, 0),
|
||||
},
|
||||
wishes=[wish],
|
||||
)
|
||||
|
||||
proposal = Allocator().propose(alloc_input)
|
||||
|
||||
# The 4 ordinary weekdays go through the normal co-leave split
|
||||
# (irrelevant to this test); only the 3 free dates matter here.
|
||||
for free_date in _RED_WEEK_FREE_DATES:
|
||||
entries = [d for d in proposal.days if d.date == free_date]
|
||||
assert len(entries) == 2
|
||||
assert all(e.day_type == "free" for e in entries)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# todo.md item 2, Part 2 — `take_pl_on_red_days=True`: a free day may now be
|
||||
# paid with a parental-benefit day (sjukpenning preferred over lägstanivå),
|
||||
# but NEVER with vacation -- Swedish law does not let you "take vacation" on
|
||||
# a day that already isn't a working day.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCase31TakePlOnRedDaysSolo:
|
||||
def test_red_days_funded_with_fp_when_available(self) -> None:
|
||||
child = _child(birth_date=date(2020, 1, 1))
|
||||
parent = _parent(salary_type="monthly")
|
||||
wish = _wish(
|
||||
child_id=child.id, start=_RED_WEEK[0], end=_RED_WEEK[1], weeks=1, first_year=2027
|
||||
)
|
||||
alloc_input = _build_input(
|
||||
today=date(2027, 1, 1),
|
||||
horizon_end=date(2027, 12, 31),
|
||||
children=[child],
|
||||
parents=[parent],
|
||||
pools={(parent.uuid, child.id): DayPool(200, 90, 60)},
|
||||
vacation={(parent.uuid, 2027): VacationBudget(25, 0, 0)},
|
||||
wishes=[wish],
|
||||
take_pl_on_red_days=True,
|
||||
)
|
||||
|
||||
proposal = Allocator().propose(alloc_input)
|
||||
|
||||
days_by_date = {d.date: d for d in proposal.days}
|
||||
for free_date in _RED_WEEK_FREE_DATES:
|
||||
day = days_by_date[free_date]
|
||||
assert day.day_type == "sjukpenning"
|
||||
assert day.reason == REASON_FP_RED_DAY
|
||||
|
||||
outcome = proposal.outcomes[0]
|
||||
assert outcome.cost_breakdown["free"] == 0
|
||||
assert outcome.cost_breakdown["vacation"] == 4 # unaffected: the 4 ordinary weekdays
|
||||
assert outcome.cost_breakdown["sjukpenning"] == 3
|
||||
assert outcome.shortfall_days == 0
|
||||
|
||||
def test_red_days_never_spend_vacation_even_when_fp_exhausted(self) -> None:
|
||||
child = _child(birth_date=date(2020, 1, 1))
|
||||
parent = _parent(salary_type="monthly")
|
||||
wish = _wish(
|
||||
child_id=child.id, start=_RED_WEEK[0], end=_RED_WEEK[1], weeks=1, first_year=2027
|
||||
)
|
||||
alloc_input = _build_input(
|
||||
today=date(2027, 1, 1),
|
||||
horizon_end=date(2027, 12, 31),
|
||||
children=[child],
|
||||
parents=[parent],
|
||||
pools={(parent.uuid, child.id): DayPool(0, 0, 60)},
|
||||
vacation={(parent.uuid, 2027): VacationBudget(25, 0, 0)},
|
||||
wishes=[wish],
|
||||
take_pl_on_red_days=True,
|
||||
)
|
||||
|
||||
proposal = Allocator().propose(alloc_input)
|
||||
|
||||
days_by_date = {d.date: d for d in proposal.days}
|
||||
for free_date in _RED_WEEK_FREE_DATES:
|
||||
assert days_by_date[free_date].day_type == "free"
|
||||
|
||||
outcome = proposal.outcomes[0]
|
||||
assert outcome.cost_breakdown["free"] == 3
|
||||
# The 4 ordinary weekdays still spend vacation as usual -- only the
|
||||
# red-day-FP branch is affected by the empty pool.
|
||||
assert outcome.cost_breakdown["vacation"] == 4
|
||||
|
||||
|
||||
class TestCase32TakePlOnRedDaysBothParents:
|
||||
def test_each_parent_funded_independently_from_own_fp_pool(self) -> None:
|
||||
"""Parent B's FP pool is empty, Parent A's is not. Each parent's own
|
||||
pool is tried independently on a red day (never a peeked-then-
|
||||
committed-together pair like dubbeldagar/co-leave) -- one parent
|
||||
running out must never stop the other from still getting paid, and
|
||||
must never fall back to vacation for the exhausted one either.
|
||||
"""
|
||||
child = _child(birth_date=date(2020, 1, 1))
|
||||
parent_a = _parent(name="A")
|
||||
parent_b = _parent(name="B")
|
||||
wish = _wish(
|
||||
child_id=child.id,
|
||||
start=_RED_WEEK[0],
|
||||
end=_RED_WEEK[1],
|
||||
weeks=1,
|
||||
first_year=2027,
|
||||
parent_mode="both",
|
||||
)
|
||||
alloc_input = _build_input(
|
||||
today=date(2027, 1, 1),
|
||||
horizon_end=date(2027, 12, 31),
|
||||
children=[child],
|
||||
parents=[parent_a, parent_b],
|
||||
pools={
|
||||
(parent_a.uuid, child.id): DayPool(200, 90, 60),
|
||||
(parent_b.uuid, child.id): DayPool(0, 0, 60),
|
||||
},
|
||||
vacation={
|
||||
(parent_a.uuid, 2027): VacationBudget(25, 0, 0),
|
||||
(parent_b.uuid, 2027): VacationBudget(25, 0, 0),
|
||||
},
|
||||
wishes=[wish],
|
||||
take_pl_on_red_days=True,
|
||||
)
|
||||
|
||||
proposal = Allocator().propose(alloc_input)
|
||||
|
||||
for free_date in _RED_WEEK_FREE_DATES:
|
||||
entries = {d.parent_personal_data_uuid: d for d in proposal.days if d.date == free_date}
|
||||
assert entries[parent_a.uuid].day_type == "sjukpenning"
|
||||
assert entries[parent_a.uuid].reason == REASON_FP_RED_DAY
|
||||
assert entries[parent_b.uuid].day_type == "free"
|
||||
|
||||
def test_both_parents_funded_when_both_have_fp_left(self) -> None:
|
||||
child = _child(birth_date=date(2020, 1, 1))
|
||||
parent_a = _parent(name="A")
|
||||
parent_b = _parent(name="B")
|
||||
wish = _wish(
|
||||
child_id=child.id,
|
||||
start=_RED_WEEK[0],
|
||||
end=_RED_WEEK[1],
|
||||
weeks=1,
|
||||
first_year=2027,
|
||||
parent_mode="both",
|
||||
)
|
||||
alloc_input = _build_input(
|
||||
today=date(2027, 1, 1),
|
||||
horizon_end=date(2027, 12, 31),
|
||||
children=[child],
|
||||
parents=[parent_a, parent_b],
|
||||
pools={
|
||||
(parent_a.uuid, child.id): DayPool(200, 90, 60),
|
||||
(parent_b.uuid, child.id): DayPool(200, 90, 60),
|
||||
},
|
||||
vacation={
|
||||
(parent_a.uuid, 2027): VacationBudget(25, 0, 0),
|
||||
(parent_b.uuid, 2027): VacationBudget(25, 0, 0),
|
||||
},
|
||||
wishes=[wish],
|
||||
take_pl_on_red_days=True,
|
||||
)
|
||||
|
||||
proposal = Allocator().propose(alloc_input)
|
||||
|
||||
for free_date in _RED_WEEK_FREE_DATES:
|
||||
entries = [d for d in proposal.days if d.date == free_date]
|
||||
assert len(entries) == 2
|
||||
assert all(e.day_type == "sjukpenning" for e in entries)
|
||||
assert all(e.reason == REASON_FP_RED_DAY for e in entries)
|
||||
|
||||
@@ -69,6 +69,30 @@ class TestWorkspaceUpdate:
|
||||
with pytest.raises(NotFoundError):
|
||||
await workspace_service.update_workspace(uuid4(), name="Whatever")
|
||||
|
||||
async def test_take_pl_on_red_days_defaults_to_false(
|
||||
self,
|
||||
workspace_service: WorkspaceService,
|
||||
) -> None:
|
||||
ws = await workspace_service.create_workspace("New Workspace")
|
||||
assert ws.take_pl_on_red_days is False
|
||||
|
||||
async def test_update_take_pl_on_red_days(
|
||||
self,
|
||||
workspace_service: WorkspaceService,
|
||||
) -> None:
|
||||
ws = await workspace_service.create_workspace("Workspace")
|
||||
updated = await workspace_service.update_workspace(ws.id, take_pl_on_red_days=True)
|
||||
assert updated.take_pl_on_red_days is True
|
||||
|
||||
# Persisted, not just returned in-memory -- re-fetch to be sure.
|
||||
refetched = await workspace_service.get_workspace(ws.id)
|
||||
assert refetched.take_pl_on_red_days is True
|
||||
|
||||
# `None` (the default) means "leave unchanged", mirroring `name`'s
|
||||
# omit-to-keep convention.
|
||||
unchanged = await workspace_service.update_workspace(ws.id, name="Renamed")
|
||||
assert unchanged.take_pl_on_red_days is True
|
||||
|
||||
|
||||
class TestRenameMember:
|
||||
"""Rename a workspace member's display name (placeholder or self)."""
|
||||
|
||||
Reference in New Issue
Block a user