# `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).