# 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: ```bash 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`:** ```python # 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: ```bash 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