147 lines
8.8 KiB
Markdown
147 lines
8.8 KiB
Markdown
# 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.
|