# S03: [A] Consent Data Model + API Endpoints

**Goal:** API accepts per-video consent toggles with versioned audit trail, gated by creator ownership.
**Demo:** After this: API accepts per-video consent toggles with versioned audit trail

## Tasks
- [x] **T01: Added VideoConsent and ConsentAuditLog models with ConsentField enum and Alembic migration 017** — Add the consent data model to the codebase: a mutable `VideoConsent` table (current consent state per video) and an append-only `ConsentAuditLog` table (versioned per-field change history). Create Alembic migration `017_add_consent_tables.py`.

## Steps

1. Read `backend/models.py` to see existing patterns (`_uuid_pk()`, `_now()`, `sa_relationship`, enum patterns).
2. Add `ConsentField` enum to `backend/models.py`: `kb_inclusion`, `training_usage`, `public_display`.
3. Add `VideoConsent` model:
   - UUID PK via `_uuid_pk()`
   - `source_video_id`: UUID FK → `source_videos.id`, unique constraint (one consent record per video)
   - `creator_id`: UUID FK → `creators.id` (denormalized for fast queries)
   - `kb_inclusion`: bool, default False
   - `training_usage`: bool, default False  
   - `public_display`: bool, default True
   - `updated_by`: UUID FK → `users.id`
   - `created_at`, `updated_at`: datetime via `_now()`
   - Relationships: `source_video`, `creator`, `audit_entries`
4. Add `ConsentAuditLog` model:
   - UUID PK via `_uuid_pk()`
   - `video_consent_id`: UUID FK → `video_consents.id`
   - `version`: Integer (auto-incrementing per video_consent_id, assigned in application code)
   - `field_name`: String (which consent field changed)
   - `old_value`: Boolean, nullable
   - `new_value`: Boolean
   - `changed_by`: UUID FK → `users.id`
   - `ip_address`: String(45), nullable
   - `created_at`: datetime via `_now()`
5. Create `alembic/versions/017_add_consent_tables.py` — follow the pattern from `016_add_users_and_invite_codes.py`. Create both tables with proper FKs and the unique constraint on `source_video_id`.
6. Verify: import the models in a Python shell and confirm no import errors. Check that the migration file has valid `upgrade()` and `downgrade()` functions.

## Must-Haves

- [ ] `VideoConsent` model with unique constraint on `source_video_id`
- [ ] `ConsentAuditLog` model with `version` column
- [ ] `ConsentField` enum for field name validation
- [ ] Alembic migration `017` with upgrade and downgrade
- [ ] All timestamps use `_now()` helper (KNOWLEDGE: asyncpg rejects timezone-aware datetimes)

## Verification

- `cd backend && python -c "from models import VideoConsent, ConsentAuditLog, ConsentField; print('OK')"` exits 0
- `python -c "import importlib.util; spec = importlib.util.spec_from_file_location('m', 'alembic/versions/017_add_consent_tables.py'); mod = importlib.util.module_from_spec(spec)"` — migration file is valid Python

## Negative Tests

- **Boundary conditions**: unique constraint on `source_video_id` prevents duplicate consent records per video

## Inputs

- `backend/models.py` — existing model patterns to follow
- `alembic/versions/016_add_users_and_invite_codes.py` — migration pattern to follow

## Expected Output

- `backend/models.py` — updated with VideoConsent, ConsentAuditLog, ConsentField
- `alembic/versions/017_add_consent_tables.py` — new migration file
  - Estimate: 30m
  - Files: backend/models.py, alembic/versions/017_add_consent_tables.py
  - Verify: cd backend && python -c "from models import VideoConsent, ConsentAuditLog, ConsentField; print('OK')"
- [x] **T02: Added consent API router with 5 endpoints (list, get, upsert with audit trail, history, admin summary) and Pydantic schemas, wired into main.py** — Build the consent API: Pydantic schemas for request/response, a new router with 5 endpoints (list, get, update, history, admin summary), ownership verification, and registration in main.py.

## Steps

1. Read `backend/schemas.py` to see existing Pydantic v2 patterns (`ConfigDict(from_attributes=True)`, request vs response models).
2. Add consent schemas to `backend/schemas.py`:
   - `VideoConsentUpdate(BaseModel)`: `kb_inclusion: bool | None = None`, `training_usage: bool | None = None`, `public_display: bool | None = None` (partial update — only non-None fields are changed)
   - `VideoConsentRead(BaseModel)`: all consent fields + `source_video_id`, `video_filename`, `creator_id`, `updated_at`, `model_config = ConfigDict(from_attributes=True)`
   - `ConsentAuditEntry(BaseModel)`: `version`, `field_name`, `old_value`, `new_value`, `changed_by`, `created_at`, `model_config = ConfigDict(from_attributes=True)`
   - `ConsentListResponse(BaseModel)`: `items: list[VideoConsentRead]`, `total: int`
   - `ConsentSummary(BaseModel)`: per-flag counts
3. Create `backend/routers/consent.py`:
   - `APIRouter(prefix="/consent", tags=["consent"])`
   - Helper `_verify_video_ownership(video_id, user, session)` — loads SourceVideo, checks `user.creator_id` matches `video.creator_id`. Raises 403 if user has no `creator_id` or doesn't own video. Admin role (checked via `user.role == UserRole.admin`) bypasses the ownership check.
   - `GET /consent/videos` — list consent for current creator's videos, paginated (offset/limit). Join with SourceVideo to get filename.
   - `GET /consent/videos/{video_id}` — single video consent status
   - `PUT /consent/videos/{video_id}` — upsert consent: create `VideoConsent` if not exists, update changed fields, create `ConsentAuditLog` entry for each changed field with incrementing version number. Use `select ... for update` or compute max version from existing audit entries.
   - `GET /consent/videos/{video_id}/history` — audit trail ordered by version
   - `GET /consent/admin/summary` — admin-only aggregate stats, guarded by `require_role(UserRole.admin)`
   - Add `chrysopedia.consent` logger, log on PUT: `logger.info("Consent updated", extra={"video_id": ..., "fields_changed": ...})`
4. Register router in `backend/main.py`: `from routers import consent` and `app.include_router(consent.router, prefix="/api/v1")`
5. Verify: start the app and confirm `/docs` shows the consent endpoints (or just verify the import chain works).

## Must-Haves

- [ ] Partial update schema (only non-None fields trigger changes)
- [ ] Ownership verification helper with admin bypass
- [ ] PUT creates audit log entries per changed field with incrementing versions
- [ ] GET /consent/videos returns video filename alongside consent flags
- [ ] All endpoints require authentication via `get_current_user`
- [ ] Users without `creator_id` get 403 (not 500)
- [ ] Admin summary endpoint requires admin role
- [ ] Router registered in main.py

## Failure Modes

| Dependency | On error | On timeout | On malformed response |
|------------|----------|-----------|----------------------|
| PostgreSQL | SQLAlchemy raises, FastAPI returns 500 | Connection pool timeout → 500 | N/A (ORM handles) |
| Auth (JWT) | 401 Unauthorized | N/A (local decode) | 401 (invalid token) |

## Negative Tests

- **Malformed inputs**: non-UUID video_id in path → 422. Empty PUT body (no fields) → no changes, no audit entries.
- **Error paths**: unauthenticated → 401, no creator_id → 403, wrong creator → 403, nonexistent video → 404
- **Boundary conditions**: first PUT for a video creates VideoConsent + audit entries. Repeated PUT with same values → no new audit entries.

## Inputs

- `backend/models.py` — VideoConsent, ConsentAuditLog, ConsentField models from T01
- `backend/schemas.py` — existing schema patterns
- `backend/routers/auth.py` — auth router pattern to follow
- `backend/auth.py` — get_current_user, require_role dependencies
- `backend/main.py` — router registration pattern

## Expected Output

- `backend/schemas.py` — updated with consent schemas
- `backend/routers/consent.py` — new consent router
- `backend/main.py` — updated with consent router registration
  - Estimate: 45m
  - Files: backend/schemas.py, backend/routers/consent.py, backend/main.py
  - Verify: cd backend && python -c "from routers.consent import router; print(f'{len(router.routes)} routes OK')"
- [x] **T03: Add 22 integration tests for consent endpoints covering auth, ownership, CRUD, audit trail, idempotency, pagination, and admin access** — Write comprehensive integration tests covering all consent endpoints, auth/authz edge cases, and audit trail correctness.

## Steps

1. Read `backend/tests/conftest.py` for existing fixture patterns (`client`, `auth_headers`, `registered_user`, `db_engine`).
2. Add new fixtures to `backend/tests/conftest.py`:
   - `creator_with_videos`: creates a Creator + 2 SourceVideo records in the test DB, returns dict with `creator_id`, `video_ids`
   - `creator_user_auth`: registers a user linked to that creator (set `creator_id` on User after registration), returns auth headers
   - `admin_auth`: registers an admin-role user, returns auth headers
3. Create `backend/tests/test_consent.py` with these test cases:
   - **Auth tests**: unauthenticated GET/PUT → 401. User without creator_id → 403 on all consent endpoints.
   - **Ownership tests**: Creator A can't read/update Creator B's video consent. Returns 403.
   - **Happy path — PUT**: First PUT creates VideoConsent + audit entries. Response has correct flags.
   - **Happy path — GET list**: Returns creator's videos with consent status. Pagination works.
   - **Happy path — GET single**: Returns consent for specific video.
   - **Audit trail**: PUT changing 2 fields creates 2 audit entries with correct version numbers. GET history returns them ordered.
   - **Idempotency**: PUT with same values as current state creates no new audit entries.
   - **Partial update**: PUT with only `kb_inclusion=True` changes only that field, leaves others unchanged.
   - **Admin access**: Admin can read any creator's consent via GET endpoints. Admin summary returns correct counts.
   - **Nonexistent video**: GET/PUT for random UUID → 404.
4. Run the full test suite to confirm no regressions.

## Must-Haves

- [ ] Tests for 401 (no auth), 403 (no creator_id), 403 (wrong creator), 404 (no video)
- [ ] Tests for PUT creating audit entries with incrementing versions
- [ ] Test for idempotent PUT (no audit entries when values unchanged)
- [ ] Test for admin bypass on read endpoints
- [ ] Test for GET list pagination
- [ ] All existing tests still pass

## Verification

- `cd backend && python -m pytest tests/test_consent.py -v` — all consent tests pass
- `cd backend && python -m pytest tests/ -v --timeout=60` — full suite passes, no regressions

## Inputs

- `backend/tests/conftest.py` — existing test fixtures
- `backend/routers/consent.py` — consent router from T02
- `backend/schemas.py` — consent schemas from T02
- `backend/models.py` — consent models from T01

## Expected Output

- `backend/tests/conftest.py` — updated with creator_with_videos, creator_user_auth, admin_auth fixtures
- `backend/tests/test_consent.py` — new test file with comprehensive consent endpoint tests
  - Estimate: 45m
  - Files: backend/tests/test_consent.py, backend/tests/conftest.py
  - Verify: cd backend && python -m pytest tests/test_consent.py -v && python -m pytest tests/ -v --timeout=60
