# S03 Research: Consent Data Model + API Endpoints

## Summary

Straightforward data modeling and CRUD slice. The consent system tracks per-video consent toggles with a versioned audit trail. The codebase has well-established patterns for models, schemas, routers, auth dependencies, Alembic migrations, and integration tests. No unfamiliar technology.

**Target requirement:** No explicit R-number for consent yet — this slice establishes the consent infrastructure that Pipeline A depends on. The roadmap "After this" is: "API accepts per-video consent toggles with versioned audit trail."

## Recommendation

Follow existing patterns exactly. Two models: a mutable `VideoConsent` (current state per video) and an append-only `ConsentAuditLog` (versioned history). One new router at `/api/v1/consent`. Auth via existing `get_current_user` dependency with creator_id ownership checks.

## Implementation Landscape

### Existing Patterns to Follow

**Models** (`backend/models.py`):
- UUID PKs via `_uuid_pk()`, naive UTC timestamps via `_now()`
- Enums as `str, enum.Enum` with PostgreSQL `Enum(...)` columns
- `sa_relationship` alias (KNOWLEDGE: SQLAlchemy column names that shadow ORM functions)
- All models inherit from `Base` (imported from `database`)

**Schemas** (`backend/schemas.py`):
- Pydantic v2 with `model_config = ConfigDict(from_attributes=True)` for read models
- Request models are plain `BaseModel`, response models use `from_attributes`

**Routers** (`backend/routers/auth.py` is the template):
- `APIRouter(prefix="/consent", tags=["consent"])`
- `Annotated[AsyncSession, Depends(get_session)]` for DB
- `Annotated[User, Depends(get_current_user)]` for auth
- Registered in `backend/main.py` via `app.include_router(consent.router, prefix="/api/v1")`

**Auth** (`backend/auth.py`):
- `get_current_user` returns `User` ORM object with `creator_id` attribute
- `require_role(UserRole.admin)` for admin-only endpoints
- JWT via `OAuth2PasswordBearer`

**Migrations** (`alembic/versions/`):
- Sequential numbering: latest is `016_add_users_and_invite_codes.py`
- Next migration: `017_add_consent_tables.py`
- Run via `docker exec chrysopedia-api alembic upgrade head`

**Tests** (`backend/tests/`):
- pytest-asyncio with `@pytest.mark.asyncio`
- `client` fixture provides httpx `AsyncClient` wired to FastAPI
- `auth_headers` fixture provides `{"Authorization": "Bearer <jwt>"}` for a registered creator user
- `db_engine` fixture creates/drops all tables per test

### Data Model Design

**`VideoConsent` table** (mutable, current state):
```
video_consents
├── id: UUID PK
├── source_video_id: UUID FK → source_videos.id (unique — one consent record per video)
├── creator_id: UUID FK → creators.id (denormalized for fast queries)
├── kb_inclusion: bool (default false — consent to include in knowledge base)
├── training_usage: bool (default false — consent for AI training use)
├── public_display: bool (default true — consent for public display of derived content)
├── updated_by: UUID FK → users.id
├── created_at: datetime
├── updated_at: datetime
```

**`ConsentAuditLog` table** (append-only, versioned history):
```
consent_audit_log
├── id: UUID PK
├── video_consent_id: UUID FK → video_consents.id
├── version: int (auto-incrementing per video_consent_id)
├── field_name: str (which consent field changed)
├── old_value: bool | None
├── new_value: bool
├── changed_by: UUID FK → users.id
├── ip_address: str | None (optional, for legal audit)
├── created_at: datetime
```

**Design rationale:**
- Separate mutable state from audit trail — querying current consent is a simple SELECT on `video_consents` without scanning history
- `creator_id` denormalized on `video_consents` avoids a JOIN through `source_videos` for listing a creator's consent status
- Unique constraint on `source_video_id` enforces one consent record per video
- Audit log captures per-field changes, not full snapshots — more precise for legal review
- `version` column on audit log enables ordered reconstruction of consent history

### API Endpoints

All under `/api/v1/consent`, all require authentication:

1. **`GET /consent/videos`** — List consent status for the authenticated creator's videos
   - Query: `offset`, `limit`
   - Auth: `get_current_user`, filter by `user.creator_id`
   - Response: paginated list of `VideoConsentRead` (video ID, filename, consent flags)

2. **`GET /consent/videos/{video_id}`** — Get consent status for a specific video
   - Auth: ownership check (video's creator_id == user.creator_id)
   - Response: `VideoConsentRead` with audit log entries

3. **`PUT /consent/videos/{video_id}`** — Update consent flags for a video
   - Body: `VideoConsentUpdate` (partial — only changed fields)
   - Auth: ownership check
   - Creates `VideoConsent` if not exists (upsert), creates `ConsentAuditLog` entries for each changed field
   - Response: `VideoConsentRead`

4. **`GET /consent/videos/{video_id}/history`** — Get full audit trail for a video
   - Auth: ownership check
   - Response: list of `ConsentAuditEntry` ordered by version

5. **(Admin) `GET /consent/admin/summary`** — Aggregate consent stats
   - Auth: `require_role(UserRole.admin)`
   - Response: counts of videos with/without consent per flag

### Authorization Pattern

The creator ownership check is a reusable pattern:
```python
async def _verify_video_ownership(video_id: UUID, user: User, session: AsyncSession) -> SourceVideo:
    """Load video and verify the current user owns it via creator_id."""
    video = await session.get(SourceVideo, video_id)
    if video is None:
        raise HTTPException(404, "Video not found")
    if user.creator_id is None or video.creator_id != user.creator_id:
        raise HTTPException(403, "Not your video")
    return video
```

Admin users (role=admin) should bypass the ownership check — they may need to view/manage consent for any video.

### Natural Task Seams

1. **T01: Models + Migration** — Add `VideoConsent` and `ConsentAuditLog` to `models.py`, create Alembic migration `017_add_consent_tables.py`. Verify migration applies cleanly.

2. **T02: Schemas + Router** — Add Pydantic schemas to `schemas.py`, create `backend/routers/consent.py` with all endpoints, register in `main.py`. Includes the ownership verification helper.

3. **T03: Integration Tests** — Add `backend/tests/test_consent.py` with tests for all endpoints including auth, ownership checks, audit trail creation, and edge cases.

### What to Build First

T01 (models + migration) is the foundation — T02 and T03 depend on it. T02 and T03 could theoretically be parallel but the tests need the router, so T01 → T02 → T03 is the natural order.

### Verification

- `alembic upgrade head` succeeds with new migration
- All consent endpoints return correct status codes with/without auth
- PUT creates audit log entries for each changed field
- Ownership check prevents cross-creator access
- Admin role bypasses ownership for read endpoints
- Existing tests still pass (no regression)

### Files Touched

- `backend/models.py` — add VideoConsent, ConsentAuditLog, ConsentField enum
- `backend/schemas.py` — add consent request/response schemas
- `backend/routers/consent.py` — new file, consent endpoints
- `backend/main.py` — register consent router
- `alembic/versions/017_add_consent_tables.py` — new migration
- `backend/tests/test_consent.py` — new test file
- `backend/tests/conftest.py` — add creator+video fixture for consent tests

### Constraints

- KNOWLEDGE: asyncpg rejects timezone-aware datetimes — use `_now()` helper for all timestamp defaults
- KNOWLEDGE: Alembic env.py sys.path needs both local and Docker paths — already handled
- User.creator_id is nullable — endpoints must guard against `user.creator_id is None` (user not linked to a creator)
- No frontend work in this slice — API only
