Files
F-r-ldraledigplannerare/SENIOR_REPORT_3.md
T
2026-06-14 09:58:30 +02:00

147 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Senior Developer — Session 4 Report
**Date:** 2026-06-01
**Baseline:** `SENIOR_FOLLOWUP_2.md` (2 items).
**Outcome:** Item 1 (tips-engine rules) fully closed — all 9 stubbed rules implemented and tested. Item 2 (frontend integration review) is pending the junior's completion of the wiring work and cannot be closed yet.
**Test suite: 159 passed, 0 failed, 52% coverage** (above the raised 50% CI gate; was 44%/40% before this session).
---
## Item 1 — Wire tips-engine rules (`services/tips_service.py`)
### What was done
All 9 rules that previously returned `None` are now implemented. The three already-wired rules (`use_vacation_before_expiry`, `skip_fp_first_12_months`, `sgi_minimum_after_age_1`) were left untouched and used as the reference shape.
No new dependencies were injected into `TipsService`. Every rule works with data already loaded by `_build_context` and calls one or two predicate helpers from `utils/tip_predicates.py`. The pure `days_left_at_age_4_projection()` helper stood in for `analytics_service.get_days_left_at_age_4()` where the age-4 calculation was needed — this avoids coupling the tips engine to the analytics service and keeps the rules trivially unit-testable.
New imports added to `tips_service.py`:
```python
from collections import defaultdict
from app.utils.tip_predicates import (
days_left_at_age_4_projection, # was missing
preschool_buffer_needed, # was missing
red_day_clusters_in_range, # was missing
weekly_intensity, # was missing
# existing four kept
)
```
---
### Rule-by-rule summary
#### `balance_between_parents`
Compares `days_used` from `ctx.leave_by_parent` across all parents. Returns an `info` tip when the gap between the heaviest and lightest user exceeds 30 days. Guards: requires ≥2 parents each with leave data.
---
#### `preserve_for_age_4`
Per parent: counts planned FP days before the child's 4th birthday, calls `days_left_at_age_4_projection(days_used, planned_fp_before_b4, total_days)`, fires a `warning` tip when `expiring_at_b4 > 0`. The 96-day savable ceiling comes from `settings.rules.max_savable_days_at_age_4`.
---
#### `use_dubbeldagar_strategically`
Builds a `date → set[parent_uuid]` map for all FP days within the child's first 15 months (`settings.rules.dubbeldagar_months_limit`). For any date where ≥2 parent UUIDs appear and none of the corresponding `PlannedLeaveDay` records has `is_dubbeldag=True`, fires an `action` tip pointing at that date.
---
#### `lagsta_niva_coverage`
Per parent: reads `tier_data.sjukpenning_left` and `tier_data.lagsta_niva_left` from the latest leave snapshot. Fires an `info` tip when sjukpenning is below 30 days but lägstanivå days remain. Surfaces the common case where parents exhaust the high-rate tier without realising the low-rate tier could cover additional days.
---
#### `stretch_time_at_home`
Per parent: computes `weekly_intensity()` over the next four weeks, then checks `days_left_at_age_4_projection`. Fires an `info` tip when **both** conditions hold: intensity ≥7 days/week AND projected days remaining at age 4 <60. The double gate avoids noise — high intensity is fine if there are plenty of days left.
---
#### `wrap_weekend_with_vacation`
Salary gate: **monthly-salaried parents only** (per ARCHITECTURE.md §6 and product intent). Only fires when `workspace_planning_goal == "max_money"`. Looks 8 weeks ahead for FP days planned on a **Friday where the following Monday is also FP**. A monthly-salaried parent gets the same take-home either way, so swapping either boundary day to vacation saves an FP day at no income cost.
---
#### `wrap_red_days_with_vacation`
Same salary and goal gates as above. Looks 12 weeks ahead, calls `red_day_clusters_in_range(today, look_ahead)` (uses `holidays.Sweden`), and fires when a parent has an FP day immediately adjacent to a cluster (the day before `cluster_start` or the day after `cluster_end`). Tip is advisory per the ARCHITECTURE.md caveat about kollektivavtal.
---
#### `long_break_parent_swap`
Iterates future `BreakPeriod` entries. For each break, counts working days in the window and counts each parent's planned days (any type) that fall within it. If a parent covers fewer than 50% of working days, fires an `info` tip naming both the under-covered parent and their partner. The suggestion: one takes FP while the other uses vacation, so both are home for the break.
---
#### `reserve_for_preschool_planning_days`
Calls `preschool_buffer_needed(child, settings.rules.preschool_planning_days_per_year)` — returns 0 if the child has no preschool start date or preschool hasn't begun yet. If a buffer is needed and any parent's `days_left < buffer_needed * 2`, fires an `info` tip. The `× 2` factor gives an early-warning window before the days are completely gone.
---
### Tests — `tests/unit/test_services/test_tips_service.py` (new file, 34 tests)
All rules are tested by constructing `TipsContext` directly — no database, no fixtures, instant feedback. Helper factories (`_make_child`, `_make_parent`, `_make_leave`, `_make_fp_day`, `_make_break`, `_make_ctx`) keep individual test bodies short.
Every rule has at minimum one fire case and one no-fire case; rules with multiple guard branches have a test per guard:
| Rule | Tests |
|---|---|
| `balance_between_parents` | 4 (fires at gap=60; no-fire gap=15; single parent; missing leave data) |
| `preserve_for_age_4` | 4 (fires; no-fire; no children; planned days counted correctly) |
| `use_dubbeldagar_strategically` | 4 (fires; no-fire is_dubbeldag=True; single parent; outside window) |
| `lagsta_niva_coverage` | 3 (fires; no-fire ample sjukpenning; no-fire lagsta exhausted) |
| `stretch_time_at_home` | 4 (fires; no-fire low intensity; no-fire ample days; no children) |
| `wrap_weekend_with_vacation` | 4 (fires; no-fire hourly; no-fire wrong goal; no-fire Friday only) |
| `wrap_red_days_with_vacation` | 3 (fires; no-fire wrong goal; no-fire hourly) |
| `long_break_parent_swap` | 4 (fires; no-fire both covered; single parent; past break) |
| `reserve_for_preschool_planning_days` | 4 (fires; no preschool date; days sufficient; preschool future) |
The `wrap_red_days_with_vacation` fire-case test finds the first weekend cluster dynamically via `red_day_clusters_in_range(today, today+12w)` — it is robust to any run date without relying on hardcoded calendar entries. The `wrap_weekend_with_vacation` tests use a `_next_weekday()` helper to locate the next Friday/Monday pair within the look-ahead window.
`tips_service.py` coverage went from 0% (all stubs) to **75%**. The uncovered lines are the `TipsService` class methods (`get_tips`, `_build_context`) which require real repositories — these are integration-level and appropriate to cover later with API tests.
---
## Item 2 — Frontend ↔ backend integration review
**Status: Pending junior completion.** The junior is still working through `JUNIOR_FOLLOWUP_2.md` (broken build + page-stub wiring). The two seams flagged for senior review remain:
1. **Auth flow** — register/login → JWT handling → `AuthContext` token storage and refresh; CORS in `main.py` matching the real frontend origin.
2. **Manual parental-leave entry path** — `FetchDataService` manual path, `useParentalLeave` hook, and the new `getParentalLeave` API client aligning with the backend contract.
When the junior signals the wiring is done: smoke-test `uv run uvicorn app.main:app --reload` + `npm run dev`, walk create-child → plan-days → view-analytics, and check CORS headers and JWT refresh in the browser network tab.
---
## CI gate
Raised `--cov-fail-under` from 40 → 50. Current total is 51.67%, so there is a small positive buffer. The 90% target requires covering routers (currently 0%) and the workspace service (30%) — that needs API-level integration tests, which are a natural next step once the frontend wiring lands.
---
## Final state
| Metric | Before this session | After |
|--------|---------------------|-------|
| Tests passing | 125 | **159** (+34 tips tests) |
| Coverage | 44% | **52%** |
| CI gate | 40% | **50%** |
| Rules implemented | 3 / 12 | **12 / 12** |
| `tips_service.py` coverage | 0% (all stubs) | **75%** |
| Frontend integration review | Not yet | **Pending junior wiring** |
## What remains
- **Frontend integration review (Item 2)** — unblocked once the junior finishes `JUNIOR_FOLLOWUP_2.md`. Quick smoke test + read-through of the auth/manual-entry seams.
- **CI gate → 90%** — requires router integration tests (API layer). Natural follow-up once the frontend is wired and there's a working end-to-end test harness to build on.
- **Postgres repository impls** — pattern is in place (copy SQLite impl, swap `aiosqlite` → `asyncpg`, `?` → `$1, $2`). Deferred until SQLite hits a scaling wall.
- **FK scraper / BankID** — blocked by legal/ToS gate; `scraper_enabled` flag is the activation path.