207 lines
12 KiB
Markdown
207 lines
12 KiB
Markdown
# 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
|