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

207 lines
12 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 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