Initial commit

This commit is contained in:
2026-06-01 16:48:21 +02:00
commit 53d157535a
159 changed files with 13641 additions and 0 deletions
+206
View File
@@ -0,0 +1,206 @@
# 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