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

8.8 KiB
Raw Blame History

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:

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.