12 KiB
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:
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:
# 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 schemasrouters/settings.py— add Child CRUD routes under/api/settings/child/{child_id}andPOST /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_daywith full validation (day budget, dubbeldagar rules). The validation logic is described inARCHITECTURE.md §4.3— wire it toanalytics_service.validate_dubbeldagar. Callanalytics_servicefor budget checks.models/planned_leave.py— request/response schemasrouters/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— callsapi.saveManualFetch/api.getLatestFetchusePlannedDays.ts— calls planned-days endpointsuseAnalytics.ts— callsapi.getWorkspaceOverviewuseOptimizationTips.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:
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 pytestall green, ≥90% coverageuv run ruff check .clean (0 errors)- FK violation fix in
test_vacation_days_repository.pyapplied cd frontend && npm installrun andpackage-lock.jsoncommittedJUNIOR_ONBOARDING.mdalready updated (was done in Session 1)
The junior's first tasks (ordered by dependency):
- Child CRUD — simplest slice to copy (no business rules)
- PlannedLeaveDay CRUD — requires Child to exist first
- Frontend hook stubs — can run in parallel once the backend is green
- Wire individual tips rules — self-contained, good for parallel work