Initial commit
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user