Files
F-r-ldraledigplannerare/senior_skaffolding_1.md
T
2026-06-01 16:48:21 +02:00

12 KiB
Raw Blame History

Senior Scaffolding Handoff — Session 2

Context: Session 1 implemented the full backend foundation (Steps A–G in the plan) plus the frontend project scaffold and CI. This document tells the next session what to do with the uv run pytest output, what predictable failures to expect and how to fix them, and what work remains after the tests are green.


1. What was built in Session 1

backend/
  pyproject.toml              ← added argon2-cffi, PyJWT, alembic, slowapi, holidays, dateutil
  alembic.ini
  migrations/
    env.py                    ← async Alembic env (aiosqlite)
    versions/001_initial_schema.py  ← full 9-table DDL (workspace → planned_leave_day)
  app/
    config.py                 ← added jwt_secret, jwt_*, scraper_enabled
    db/connection.py          ← ensure_schema() (full schema, tests), run_migrations() (Alembic)
    entities/                 ← all 9 entities added
    repositories/base.py      ← added SoftDeleteRepository[T] with soft_delete/hard_delete
    repositories/             ← 7 new abstract interfaces
    repositories_impl/sqlite/ ← 7 new SQLite impls (personal_data, user, workspace,
                                 child, parental_leave, planned_leave, break_period)
    repositories_impl/postgres/vacation_days_repository.py  ← NotImplementedError skeleton
    services/
      auth_service.py         ← register (creates workspace), login, JWT tokens
      workspace_service.py    ← workspace CRUD + invitation token flow (SHA-256 hash)
      fetch_data_service.py   ← FetchDataService (abstract) + ManualFetchService
      analytics_service.py    ← days-left-at-age-4 + dubbeldagar validation
      tips_service.py         ← 12-rule registry, predicates wired, stubs return None
    scrapers/fk_scraper.py    ← NotImplementedError stub, legal gate guard
    routers/
      auth.py / workspace.py / fetch_data.py / analytics.py / settings.py
    utils/
      errors.py               ← added AuthError, UnauthorizedError, ConflictError, InvitationError
      tip_predicates.py       ← pure predicate helpers (weekly_intensity, sgi_at_risk, etc.)
    models/
      auth.py / workspace.py / parental_leave.py / analytics.py
    main.py                   ← all routers registered, slowapi rate-limiter attached
  tests/
    conftest.py               ← full-schema connection fixture + fixtures for all new repos
    unit/test_services/
      test_auth_service.py    ← register, login, token round-trip tests
      test_analytics_service.py  ← age-4 projection, leap year, zero-data cases

frontend/                     ← NEW project (not installed yet — npm install required)
  package.json / tsconfig.json / vite.config.ts / vitest.config.ts / eslint.config.js
  src/
    types/entities.ts + api.ts + index.ts   ← full §7 TypeScript types
    services/api.ts + errors.ts             ← typed fetch client
    context/AuthContext.tsx + WorkspaceContext.tsx
    hooks/useVacationDays.ts                ← REFERENCE HOOK
    components/vacation-days/
      VacationDaysList.tsx + VacationDaysList.test.tsx   ← REFERENCE COMPONENT + TEST
    pages/{Auth,Dashboard,Calendar,Tips,Settings}.tsx    ← stubs
  tests/setup.ts + fixtures/mockData.ts

.github/workflows/ci.yml      ← backend (ruff + pytest 90%) + frontend (tsc + lint + vitest 90%)

2. Predictable first-run failures and how to fix them

Run this first and paste the full output:

cd backend && uv sync && uv run pytest --tb=short && uv run ruff check .

2a. EXPECTED FAILURE: FK violations in the existing vacation-days tests

Symptom: test_vacation_days_repository.py fails with FOREIGN KEY constraint failed on every vacation_repo.create(entity) call.

Why: The full schema has vacation_days.workspace_id → workspace(id) and vacation_days.personal_data_uuid → personal_data(uuid). The old _make() factory generates random UUIDs that don't exist in those tables. The test connection now has FK enforcement on for the full schema.

Fix — update tests/unit/test_vacation_days_repository.py:

# Add at top of file, after existing imports:
from datetime import datetime
from app.entities.personal_data import PersonalData
from app.entities.workspace import Workspace
from app.repositories_impl.sqlite.personal_data_repository import SQLitePersonalDataRepository
from app.repositories_impl.sqlite.workspace_repository import SQLiteWorkspaceRepository
import aiosqlite

# Add this fixture (or add ws_id/pd_uuid parameters to the existing ones):
@pytest_asyncio.fixture
async def seeded_ids(connection: aiosqlite.Connection):
    """Pre-create a workspace + personal_data row so FK constraints pass."""
    ws_repo = SQLiteWorkspaceRepository(connection)
    pd_repo = SQLitePersonalDataRepository(connection)
    ws = await ws_repo.create(Workspace(id=uuid4(), name="Test WS", created_at=datetime.now()))
    pd = await pd_repo.create(PersonalData(
        uuid=uuid4(), workspace_id=ws.id, name="Test Parent", created_at=datetime.now()
    ))
    return {"workspace_id": ws.id, "personal_data_uuid": pd.uuid}

Then update _make() to accept workspace_id and personal_data_uuid from the fixture, and update every test class to take seeded_ids as a fixture and pass those IDs to _make().

Alternatively — the simplest surgical fix if you want tests green quickly: add a conftest.py fixture in tests/unit/ that seeds the rows and yields a (workspace_id, personal_data_uuid) namedtuple, then update _make() to default to those values when provided.

2b. Possible ruff import-order or line-length issues

settings.py and a few other new files had imports added mid-file. Ruff will flag these. Run uv run ruff check --fix . to auto-fix import ordering and any trailing whitespace — then commit the result.

2c. Missing models/personal_data.py

The dependencies.py and workspace models reference PersonalDataResponse which is defined inline in models/workspace.py. This is fine, but the architecture doc listed models/personal_data.py as a separate file. If ruff or any import complains, create it with the PersonalDataResponse class moved out of workspace.py. Not strictly required — just keep it consistent.


3. What's left after the tests are green

3a. Child CRUD (good first junior task)

The Child entity, abstract repo, and SQLite impl all exist. What's missing:

  • services/child_service.py — create, get, list_by_workspace, update (birth_date, preschool_start_date)
  • models/child.py — ChildCreate, ChildUpdate, ChildResponse schemas
  • routers/settings.py — add Child CRUD routes under /api/settings/child/{child_id} and POST /api/children

Pattern: copy services/vacation_days_service.py and routers/vacation_days.py.

3b. PlannedLeaveDay CRUD + PlanningService

The PlannedLeaveDay entity, abstract repo, and SQLite impl exist. What's missing:

  • services/planning_service.py — add_planned_day with full validation (day budget, dubbeldagar rules). The validation logic is described in ARCHITECTURE.md §4.3 — wire it to analytics_service.validate_dubbeldagar. Call analytics_service for budget checks.
  • models/planned_leave.py — request/response schemas
  • routers/planned_days.py — POST, GET, PUT, DELETE as per §5

The dubbeldagar validation is already implemented in AnalyticsService.validate_dubbeldagar — inject it.

3c. PersonalData settings endpoint

PUT /api/settings/parent/{personal_data_uuid} — update salary_type and employment_status. Needs a thin service method on top of PersonalDataRepository.update.

3d. Frontend: remaining hooks

Following the useVacationDays reference:

  • useParentalLeave.ts — calls api.saveManualFetch / api.getLatestFetch
  • usePlannedDays.ts — calls planned-days endpoints
  • useAnalytics.ts — calls api.getWorkspaceOverview
  • useOptimizationTips.ts — calls tips endpoint

Each needs a matching *.test.tsx following the VacationDaysList.test.tsx pattern.

3e. Wire the 12 tips rules

services/tips_service.py has 12 rule functions registered. Most return None (stub). Each is self-contained — the junior can fill them in one at a time. The predicates they need are in utils/tip_predicates.py:

Rule Status Key predicate
use_vacation_before_expiry ✅ wired has_vacation_near_expiry
balance_between_parents stub compare days_used across parents
preserve_for_age_4 stub analytics_service.get_days_left_at_age_4
use_dubbeldagar_strategically stub scan planned_days for same-date overlaps
lagsta_niva_coverage stub compare tier usage
stretch_time_at_home stub weekly_intensity < threshold
skip_fp_first_12_months ✅ wired is_child_under_age
sgi_minimum_after_age_1 ✅ wired sgi_at_risk
wrap_weekend_with_vacation stub monthly salary + FP spanning weekend
wrap_red_days_with_vacation stub red_day_clusters_in_range
long_break_parent_swap stub match break_periods to calendar gaps
reserve_for_preschool_planning_days stub preschool_buffer_needed

3f. Postgres implementations

repositories_impl/postgres/vacation_days_repository.py is a NotImplementedError skeleton. Every other entity needs a matching Postgres repo once the app is production-ready. Pattern: copy the SQLite impl and replace aiosqlite with asyncpg parameter style ($1, $2 instead of ?).

3g. CI: frontend lockfile

The CI runs npm ci, which requires package-lock.json. Run this once locally:

cd frontend && npm install

Then commit the resulting package-lock.json. Until then the frontend CI job will fail.


4. Known sharp edges to be aware of

Issue Location Notes
BreakPeriodUpdate in models/break_period.py has required fields (not Optional) routers/settings.py PUT The original scaffolded model didn't make fields optional. Either update the model to use Optional fields or accept full-replacement semantics for now.
_rule_skip_fp_first_12_months in tips_service services/tips_service.py line ~125 Uses _relativedelta imported at top — verify it resolves correctly at runtime.
TipsContext.workspace_planning_goal hardcoded to "balanced" services/tips_service.py:_build_context WorkspaceRepository isn't injected into TipsService. Either inject it or pass planning_goal into get_tips() as a parameter.
auth_service.register creates workspace named "{display_name}s planering" services/auth_service.py This uses the Swedish genitive s directly — fine for now, revisit for i18n.
scraper_enabled check in fetch_data.py routes routers/fetch_data.py The guard is actually just a 501 on the scraper routes — the settings.scraper_enabled flag isn't checked yet. Add the guard once the FKScraperService is ready.

5. Handoff gate for junior

Before handing off:

  • uv run pytest all green, ≥90% coverage
  • uv run ruff check . clean (0 errors)
  • FK violation fix in test_vacation_days_repository.py applied
  • cd frontend && npm install run and package-lock.json committed
  • JUNIOR_ONBOARDING.md already updated (was done in Session 1)

The junior's first tasks (ordered by dependency):

  1. Child CRUD — simplest slice to copy (no business rules)
  2. PlannedLeaveDay CRUD — requires Child to exist first
  3. Frontend hook stubs — can run in parallel once the backend is green
  4. Wire individual tips rules — self-contained, good for parallel work