# `ParentPromotionController` plan

## Scope
- **Controller**: `app/Http/Controllers/Api/Parents/ParentPromotionController.php`
- **Purpose**: parent portal endpoints for promotion + enrollment workflow.
- **Primary dependencies**:
  - `App\Services\Promotions\PromotionEnrollmentService`
  - `App\Services\Promotions\PromotionQueryService`
- **Models**:
  - `App\Models\StudentPromotionRecord`
  - `App\Models\Student` (ownership verification fallback)

## Routes
All routes are under `v1` + `parents` group:
- Base prefix: `... /api/v1/parents/promotions`
- Group middleware: `parent.access`

Endpoints:
- `GET /api/v1/parents/promotions/` → `overview`
- `GET /api/v1/parents/promotions/{promotionId}` → `show`
- `POST /api/v1/parents/promotions/{promotionId}/start` → `start`
- `PATCH /api/v1/parents/promotions/{promotionId}/steps` → `updateSteps`
- `POST /api/v1/parents/promotions/{promotionId}/submit` → `submit`

## Authentication & authorization
- Parent context resolved via `parentIdOrUnauthorized()`:
  - prefers request attribute `primary_parent_id` (set by `parent.access`)
  - falls back to `auth()->id()`
- Ownership is enforced in `loadOwnedRecord()`:
  - Primary check: `StudentPromotionRecord.parent_id == parentId`
  - Fallback: loads `Student.parent_id` for the record’s `student_id`
  - If not owned: returns `403` `{ ok:false, message:"You are not authorised..." }`

## Requests/validation
- `overview`: `ParentPromotionOverviewRequest` (optional `next_school_year`)
- `updateSteps`: `ParentEnrollmentStepRequest` (`steps()` accessor provides structured step payload)

## Response shapes
- `overview`:
  - `{ ok:true, records, next_school_year, message }`
  - Note: `records` are returned as provided by `PromotionEnrollmentService::overviewForParent(...)` (already shaped for the client)
- `show` / `start` / `updateSteps` / `submit`:
  - `{ ok:true, data: StudentPromotionResource(...) }`

## State transitions (high-level)
- `start` → `PromotionEnrollmentService::startEnrollment(...)`
- `updateSteps` → `PromotionEnrollmentService::updateSteps(...)`
- `submit` → `PromotionEnrollmentService::submitEnrollment(...)`
- Conflicts (invalid transition / deadline / status) surface as `409` with `{ ok:false, message }`.

## Error handling expectations
- Unauthorized → `401` `{ ok:false, message:"Unauthorized." }`
- Record not found → `404` `{ ok:false, message:"Promotion record not found." }`
- Not owned → `403` `{ ok:false, message:"You are not authorised..." }`
- Invalid transition → `409` `{ ok:false, message }`

## Test plan (feature)
- Ownership:
  - Parent cannot access another parent’s record (403).
  - Record with mismatched `parent_id` but student belongs to parent passes ownership check.
- Happy path:
  - `overview` returns list for parent.
  - `start` moves record to started state and returns updated resource.
  - `updateSteps` persists steps and returns updated resource.
  - `submit` transitions to submitted state.
- Conflict cases:
  - Submitting without required steps → 409.
  - Past deadline → 409 (as enforced by service).

