# PR Patient Survey System — AI IDE Implementation Prompt

> **Copy this entire file as your system/project prompt in Cursor, Windsurf, GitHub Copilot Workspace, or any AI-assisted IDE before writing a single line of code.**

---

## 1. Project Identity

You are implementing a **production-grade PR Patient Survey System** for a hospital/clinic environment.

- **Backend**: Laravel 11 · PHP 8.2+ · MySQL
- **Mobile Client**: Flutter (Dart) — API consumer only; never a security authority
- **Authentication**: `tymon/jwt-auth` (stateless bearer tokens)
- **Authorization**: `spatie/laravel-permission` (RBAC)
- **PDF Generation**: DomPDF via Blade views
- **Architecture Principles**: DRY · SOLID · Defense-in-Depth

The Laravel backend is the **single source of truth** for identity, authorization, business rules, report visibility, PDF generation, and data scoping. The Flutter app renders only what the backend authorizes.

---

## 2. The Four Roles

| Role | Description | Key Restrictions |
|------|-------------|-----------------|
| `PR` | Front-line PR employee — fills surveys for patients | Only role that can submit responses |
| `PRHeader` | PR supervisor — monitors, reviews | **Cannot** fill surveys. Cannot access Quality/Admin features |
| `Quality` | QA team — manages templates & cycles | Cannot fill surveys. No user management |
| `Admin` | System administrator — full access | Only role that can manage users and assign roles |

---

## 3. Database Schema

Create migrations for every table below **in this exact order** (respect FK dependencies):

```
branches                → add branch_access_key column
departments
department_branches     (pivot: dept ↔ branch)
application_users       → implements JWTSubject
roles                   (Spatie manages runtime; ERD role table = audit log)
user_roles              (audit log: UserId, RoleId, AssignedAt, AssignedBy)
patients                (MedicalRecordNumber UNIQUE, CreatedByUserId FK)
survey_templates        (IsActive, HasResponses, ParentTemplateId, Version)
survey_sections         (DisplayOrder, TemplateId FK)
questions               (QuestionType ENUM('Scored','Text'), SectionId FK)
options                 (ScoreValue INT, QuestionId FK)
survey_cycles           (Status ENUM('Draft','Active','Closed'), TemplateId FK)
responses               (FilledByUserId FK, PatientId FK, BranchId FK, CycleId FK)
answers                 (ResponseId FK, QuestionId FK, OptionId nullable FK, TextValue nullable TEXT)
report_permissions      (RoleId FK, ReportCode VARCHAR, CanView, CanExportPdf)
jwt_blacklist           (token_id, expires_at)
```

**Critical constraint on `answers`**: `OptionId` and `TextValue` are mutually exclusive.  
Exactly one must be non-NULL per row. Enforce this at the API validation layer (`StoreResponseRequest::withValidator()`).

**Key indexes to add**:
```sql
CREATE UNIQUE INDEX UQ_Patient_MRN ON patients(MedicalRecordNumber);
CREATE INDEX IX_Response_CycleUser ON responses(CycleId, FilledByUserId);
CREATE INDEX IX_Response_PatientId ON responses(PatientId);
CREATE INDEX IX_Answer_ResponseId ON answers(ResponseId);
CREATE INDEX IX_Template_IsActive ON survey_templates(IsActive);
CREATE INDEX IX_ReportPerm_RoleCode ON report_permissions(RoleId, ReportCode);
CREATE INDEX IX_UserRole_UserId ON user_roles(UserId);
```

---

## 4. Complete Folder Structure

Generate **every file** listed here. Do not skip any.

```
app/
├── Http/
│   ├── Controllers/
│   │   ├── Api/
│   │   │   ├── AuthController.php
│   │   │   ├── UserController.php
│   │   │   ├── PatientController.php
│   │   │   ├── CycleController.php
│   │   │   ├── TemplateController.php
│   │   │   ├── SectionController.php
│   │   │   ├── QuestionController.php
│   │   │   ├── OptionController.php
│   │   │   ├── ResponseController.php
│   │   │   ├── ReportController.php
│   │   │   └── DashboardController.php
│   │   └── Web/
│   │       ├── ReportWebController.php
│   │       └── TemplateWebController.php
│   ├── Middleware/
│   │   ├── AuthenticateJwt.php          # alias: jwt.auth
│   │   ├── EnsureRoleMiddleware.php      # alias: role
│   │   ├── RequireBranchExecution.php   # alias: branch.execution
│   │   └── SecurityHeaders.php
│   └── Requests/
│       ├── LoginRequest.php
│       ├── StoreUserRequest.php
│       ├── StorePatientRequest.php
│       ├── StoreTemplateRequest.php
│       ├── StoreSectionRequest.php
│       ├── StoreQuestionRequest.php
│       ├── StoreOptionRequest.php
│       ├── StoreCycleRequest.php
│       └── StoreResponseRequest.php     # contains XOR answer validation
├── Models/
│   ├── ApplicationUser.php              # implements JWTSubject
│   ├── Role.php
│   ├── UserRole.php
│   ├── Department.php
│   ├── Branch.php
│   ├── DepartmentBranch.php
│   ├── Patient.php
│   ├── SurveyTemplate.php
│   ├── SurveySection.php
│   ├── Question.php
│   ├── Option.php
│   ├── SurveyCycle.php
│   ├── Response.php
│   ├── Answer.php
│   └── ReportPermission.php
├── Policies/
│   ├── SurveyTemplatePolicy.php         # HasResponses lock — used by ALL nested resources
│   └── ResponsePolicy.php              # response visibility + PDF export access
├── Services/
│   ├── Auth/
│   │   ├── JwtAuthService.php
│   │   ├── JwtBlacklistService.php
│   │   └── AuthenticatedUserContext.php
│   ├── Authorization/
│   │   ├── RoleAuthorizationService.php
│   │   ├── BranchExecutionVerifier.php
│   │   └── Strategies/
│   │       ├── UserBranchMatchStrategy.php
│   │       └── BranchAccessKeyStrategy.php
│   ├── TemplateQueryService.php
│   ├── TemplateFilter.php               # value object / specification
│   ├── TemplateCloneService.php
│   ├── TemplateFactory.php              # createFresh() + createFromParent()
│   ├── ReportPermissionService.php      # canView(), canExportPdf(), listForUser()
│   ├── DashboardService.php
│   ├── SurveyService.php               # submitResponse() in DB transaction
│   ├── ResponseSheetDataService.php
│   ├── EmployeeAuditDataService.php
│   ├── CycleAnalysisDataService.php
│   ├── TemplateUsageDataService.php
│   ├── BranchSummaryDataService.php
│   └── FullAuditDataService.php
├── Scopes/
│   ├── ResponseScopeStrategy.php        # interface
│   ├── PrResponseScope.php
│   ├── PrHeaderResponseScope.php
│   ├── QualityResponseScope.php         # DO NOT treat Quality as Admin silently
│   ├── AdminResponseScope.php
│   └── ResponseScopeFactory.php
├── Pdf/
│   ├── AbstractPdfReport.php            # Template Method base class
│   ├── PdfRenderService.php             # registry dispatcher
│   ├── ResponseSheetPdf.php
│   ├── EmployeeAuditPdf.php
│   ├── CycleAnalysisPdf.php
│   ├── TemplateUsagePdf.php
│   ├── BranchSummaryPdf.php
│   └── FullAuditPdf.php
└── Providers/
    ├── SurveyServiceProvider.php
    └── RepositoryServiceProvider.php

database/
├── migrations/          (all tables listed in Section 3)
└── seeders/
    ├── DatabaseSeeder.php
    ├── RoleSeeder.php                   # seeds PR, PRHeader, Quality, Admin
    └── ReportPermissionSeeder.php       # seeds all report codes per role matrix

routes/
├── api.php
└── web.php

resources/views/
├── pdf/
│   ├── partials/
│   │   ├── header.blade.php
│   │   ├── filters-bar.blade.php
│   │   └── footer.blade.php
│   ├── response-sheet.blade.php
│   ├── employee-audit.blade.php
│   ├── cycle-analysis.blade.php
│   ├── template-usage.blade.php
│   ├── branch-summary.blade.php
│   └── full-audit.blade.php
└── reports/
    ├── employee-audit.blade.php
    ├── employee-audit-print.blade.php
    └── cycle-analysis.blade.php

config/
├── reports.php          # performance_bands thresholds (good/acceptable/bad)
└── auth/jwt.php         # ttl, blacklist_enabled, blacklist_grace_period
```

---

## 5. ApplicationUser Model

```php
// app/Models/ApplicationUser.php
class ApplicationUser extends Authenticatable implements JWTSubject
{
    use HasRoles; // Spatie

    protected $table = 'application_users';
    protected $primaryKey = 'Id';
    public $incrementing = false;
    protected $keyType = 'string';

    protected $fillable = [
        'Id', 'UserName', 'Email', 'FirstName', 'LastName',
        'PasswordHash', 'DepartmentId', 'BranchId', 'IsActive',
    ];

    protected $hidden = ['PasswordHash', 'remember_token'];

    public function getJWTIdentifier(): mixed
    {
        return $this->getKey();
    }

    // Embed roles + branch + dept into every JWT token
    public function getJWTCustomClaims(): array
    {
        return [
            'roles'     => $this->getRoleNames(),
            'branch_id' => $this->BranchId,
            'dept_id'   => $this->DepartmentId,
        ];
    }
}
```

---

## 6. JWT Configuration

```php
// config/auth/jwt.php (or config/jwt.php published by tymon)
'ttl'                    => 60,    // minutes
'refresh_ttl'            => 20160, // 2 weeks
'blacklist_enabled'      => true,
'blacklist_grace_period' => 0,     // 0 = reject immediately on logout
```

Logout must call `auth()->logout(true)` — the `true` forces immediate blacklist insertion.

---

## 7. Middleware Aliases (bootstrap/app.php)

```php
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'jwt.auth'         => \App\Http\Middleware\AuthenticateJwt::class,
        'role'             => \Spatie\LaravelPermission\Middlewares\RoleMiddleware::class,
        'branch.execution' => \App\Http\Middleware\RequireBranchExecution::class,
    ]);
    $middleware->append(\App\Http\Middleware\SecurityHeaders::class);
})
```

---

## 8. API Route Groups (routes/api.php)

```
PUBLIC
  POST   /auth/login

AUTHENTICATED (jwt.auth)
  POST   /auth/logout
  GET    /auth/me
  GET    /dashboard

MODULE 1 — Fill Survey (jwt.auth + role:PR + branch.execution)
  GET    /cycles/active
  POST   /responses

MODULE 1 — Patients (jwt.auth + role:PR,PRHeader,Admin)
  GET    /patients
  POST   /patients
  GET    /patients/{id}
  PATCH  /patients/{id}

MODULE 2 — Templates & Cycles (jwt.auth + role:Quality,Admin)
  GET    /templates
  POST   /templates
  GET    /templates/{id}
  PATCH  /templates/{id}
  DELETE /templates/{id}
  PATCH  /templates/{id}/toggle
  POST   /templates/{id}/inherit
  POST   /templates/{id}/sections
  PATCH  /sections/{id}
  DELETE /sections/{id}
  POST   /sections/{id}/questions
  PATCH  /questions/{id}
  DELETE /questions/{id}
  POST   /questions/{id}/options
  PATCH  /options/{id}
  DELETE /options/{id}
  GET    /cycles
  POST   /cycles
  GET    /cycles/{id}
  PATCH  /cycles/{id}/open
  PATCH  /cycles/{id}/close

RESPONSES TABLE (jwt.auth + role:PR,PRHeader,Admin)
  GET    /responses
  GET    /responses/{id}
  GET    /responses/{id}/pdf

REPORTS (jwt.auth)
  GET    /reports
  GET    /reports/{code}
  GET    /reports/{code}/pdf    ← role:PRHeader,Quality,Admin

USER MANAGEMENT (jwt.auth + role:Admin)
  GET    /users
  POST   /users
  GET    /users/{id}
  PATCH  /users/{id}
  PATCH  /users/{id}/toggle
  POST   /users/{id}/roles
  DELETE /users/{id}/roles/{roleId}
```

---

## 9. Core Services — Exact Contracts

### 9.1 TemplateFilter (value object)
```php
class TemplateFilter {
    public static function forFillSurvey(): self       // IsActive=true + active cycle
    public static function forTemplateManagement(): self // all templates + HasResponses flag
    public static function forCycleCreation(): self    // IsActive=true only
}
```

### 9.2 TemplateQueryService
```php
class TemplateQueryService {
    public function query(TemplateFilter $filter): Builder
    // Single implementation — called from CycleController, TemplateController, never duplicated
}
```

### 9.3 TemplateFactory
```php
class TemplateFactory {
    public function createFresh(array $data): SurveyTemplate
    // Sets: IsActive=false, HasResponses=false, Version=1, ParentTemplateId=null

    public function createFromParent(SurveyTemplate $parent, array $overrides): SurveyTemplate
    // Delegates to TemplateCloneService — does NOT duplicate clone logic here
}
```

### 9.4 TemplateCloneService
```php
class TemplateCloneService {
    public function clone(SurveyTemplate $source, array $overrides): SurveyTemplate
    // Deep copies: template → sections → questions → options
    // New template gets: IsActive=false, HasResponses=false, ParentTemplateId=$source->TemplateId
}
```

### 9.5 ResponseScopeFactory
```php
class ResponseScopeFactory {
    public static function make(ApplicationUser $user): ResponseScopeStrategy
    // Returns: PrResponseScope | PrHeaderResponseScope | QualityResponseScope | AdminResponseScope
    // DO NOT map Quality → AdminResponseScope — create a separate QualityResponseScope
}
```

### 9.6 ReportPermissionService
```php
class ReportPermissionService {
    public function canView(ApplicationUser $user, string $reportCode): bool
    public function canExportPdf(ApplicationUser $user, string $reportCode): bool
    public function listForUser(ApplicationUser $user): Collection  // returns visible report codes
    // Used by BOTH Api\ReportController AND Web\ReportWebController — never duplicated
}
```

### 9.7 PdfRenderService
```php
class PdfRenderService {
    private array $registry = [
        'RPT-RESPONSE'        => ResponseSheetPdf::class,
        'RPT-EMPLOYEE-AUDIT'  => EmployeeAuditPdf::class,
        'RPT-CYCLE-ANALYSIS'  => CycleAnalysisPdf::class,
        'RPT-TEMPLATE-USAGE'  => TemplateUsagePdf::class,
        'RPT-BRANCH-SUMMARY'  => BranchSummaryPdf::class,
        'RPT-FULL-AUDIT'      => FullAuditPdf::class,
    ];

    public function stream(string $reportCode, array $data, array $filters = []): Response
    public function download(string $reportCode, array $data, array $filters = []): Response
    // Adding new report = add one entry to $registry. PdfRenderService never changes.
}
```

### 9.8 AbstractPdfReport (Template Method)
```php
abstract class AbstractPdfReport {
    final public function generate(array $data, array $filters): string
    {
        $html  = $this->renderHeader();
        $html .= $this->renderFiltersBar();
        $html .= $this->renderBody();      // only this changes per report type
        $html .= $this->renderFooter();
        return $this->toPdf($html);
    }

    abstract protected function title(): string;
    abstract protected function renderBody(): string;

    protected function renderHeader(): string { /* shared Blade partial */ }
    protected function renderFiltersBar(): string { /* shared Blade partial */ }
    protected function renderFooter(): string { /* shared Blade partial */ }
    protected function toPdf(string $html): string { /* DomPDF */ }
    protected function orientation(): string { return 'portrait'; }
}
```

---

## 10. StoreResponseRequest — Answer XOR Rule

```php
class StoreResponseRequest extends FormRequest
{
    public function authorize(): bool
    {
        return auth()->check() && auth()->user()->hasRole('PR');
    }

    public function rules(): array
    {
        return [
            'cycle_id'               => 'required|integer|exists:survey_cycles,CycleId',
            'patient_id'             => 'required|integer|exists:patients,PatientId',
            'branch_id'              => 'required|integer|exists:branches,BranchId',
            'notes'                  => 'nullable|string|max:1000',
            'answers'                => 'required|array|min:1',
            'answers.*.question_id'  => 'required|integer|exists:questions,QuestionId',
            'answers.*.option_id'    => 'nullable|integer|exists:options,OptionId',
            'answers.*.text_value'   => 'nullable|string|max:2000',
        ];
    }

    public function withValidator(Validator $validator): void
    {
        $validator->after(function ($v) {
            foreach ($this->input('answers', []) as $i => $answer) {
                $hasOption = !empty($answer['option_id']);
                $hasText   = !empty($answer['text_value']);
                if ($hasOption && $hasText) {
                    $v->errors()->add("answers.$i", 'Answer must not have both option_id and text_value.');
                }
                if (!$hasOption && !$hasText) {
                    $v->errors()->add("answers.$i", 'Answer must have exactly one of option_id or text_value.');
                }
            }
        });
    }
}
```

---

## 11. SurveyTemplatePolicy — HasResponses Lock

```php
class SurveyTemplatePolicy
{
    public function update(ApplicationUser $user, SurveyTemplate $template): bool
    {
        if ($template->HasResponses) {
            abort(409, 'Template is locked because it has existing responses.');
        }
        return $user->hasAnyRole(['Quality', 'Admin']);
    }

    public function delete(ApplicationUser $user, SurveyTemplate $template): bool
    {
        return $this->update($user, $template); // same rule
    }
}
```

**This policy must be called in TemplateController, SectionController, QuestionController, and OptionController.**  
Do not re-implement the `HasResponses` check anywhere else.

---

## 12. SurveyService — Transactional Submit

```php
public function submitResponse(ApplicationUser $user, array $data): Response
{
    return DB::transaction(function () use ($user, $data) {
        $response = Response::create([
            'CycleId'        => $data['cycle_id'],
            'FilledByUserId' => $user->Id,
            'PatientId'      => $data['patient_id'],
            'BranchId'       => $data['branch_id'],
            'Notes'          => $data['notes'] ?? null,
            'SubmittedAt'    => now(),
        ]);

        foreach ($data['answers'] as $answer) {
            Answer::create([
                'ResponseId' => $response->ResponseId,
                'QuestionId' => $answer['question_id'],
                'OptionId'   => $answer['option_id'] ?? null,
                'TextValue'  => $answer['text_value'] ?? null,
            ]);
        }

        // Lock the template from further edits
        SurveyCycle::find($data['cycle_id'])
            ->template()
            ->update(['HasResponses' => true]);

        return $response;
    });
}
```

---

## 13. Role-Based Response Scoping

```php
// interface
interface ResponseScopeStrategy {
    public function apply(Builder $query, ApplicationUser $user): Builder;
}

class PrResponseScope implements ResponseScopeStrategy {
    public function apply(Builder $query, ApplicationUser $user): Builder {
        return $query->where('FilledByUserId', $user->Id);
    }
}

class PrHeaderResponseScope implements ResponseScopeStrategy {
    public function apply(Builder $query, ApplicationUser $user): Builder {
        return $query->where('BranchId', $user->BranchId);
    }
}

class QualityResponseScope implements ResponseScopeStrategy {
    // Quality sees org-wide but is NOT Admin — keep separate
    public function apply(Builder $query, ApplicationUser $user): Builder {
        return $query; // no filter, but keep class distinct
    }
}

class AdminResponseScope implements ResponseScopeStrategy {
    public function apply(Builder $query, ApplicationUser $user): Builder {
        return $query; // no filter
    }
}

// Factory
class ResponseScopeFactory {
    public static function make(ApplicationUser $user): ResponseScopeStrategy {
        return match(true) {
            $user->hasRole('PR')       => new PrResponseScope(),
            $user->hasRole('PRHeader') => new PrHeaderResponseScope(),
            $user->hasRole('Quality')  => new QualityResponseScope(),
            default                    => new AdminResponseScope(),
        };
    }
}
```

**Reuse `ResponseScopeFactory` in `ResponseController::index`, `DashboardService`, and `ReportController` — do not duplicate the scoping logic.**

---

## 14. Dashboard KPI Aggregation

```php
// DashboardService — push all aggregations to MySQL, never load collections into PHP
class DashboardService {
    public function getKpis(ApplicationUser $user): array
    {
        $scope  = ResponseScopeFactory::make($user);
        $base   = $scope->apply(Response::query(), $user);

        return [
            'avg_satisfaction'      => (clone $base)
                                         ->join('answers', ...)
                                         ->join('options', ...)
                                         ->avg('options.ScoreValue'),
            'total_responses'       => (clone $base)->count(),
            'response_trend'        => (clone $base)
                                         ->selectRaw('DATE(SubmittedAt) as date, COUNT(*) as count')
                                         ->groupBy('date')
                                         ->get(),
            'top_questions'         => $this->topBottomQuestions($base, 'desc'),
            'bottom_questions'      => $this->topBottomQuestions($base, 'asc'),
        ];
    }
}
```

---

## 15. Standard JSON Response Envelope

Every API controller must return this exact structure:

```json
// Success
{ "success": true,  "data": { ... },  "message": "Human readable",  "errors": null }

// Failure
{ "success": false, "data": null, "message": "Validation failed", "errors": { "field": ["error detail"] } }
```

HTTP codes: `200` GET/PATCH, `201` POST create, `400` validation, `401` unauthenticated, `403` forbidden, `404` not found, `409` business conflict (locked template).

---

## 16. Report Permission Seed Data

```php
// Seed ALL combinations from this matrix:
// Code                | PR    | PRHeader         | Quality          | Admin
// RPT-MY-RESPONSES    | view  | view             | view             | view + pdf
// RPT-BRANCH-SUMMARY  | —     | view + pdf       | view + pdf       | view + pdf
// RPT-CYCLE-ANALYSIS  | —     | —                | view + pdf       | view + pdf
// RPT-TEMPLATE-USAGE  | —     | —                | view + pdf       | view + pdf
// RPT-EMPLOYEE-AUDIT  | —     | view + pdf       | view + pdf       | view + pdf
// RPT-FULL-AUDIT      | —     | —                | —                | view + pdf
```

---

## 17. Branch Execution Verification (Module 1)

```
Every GET /cycles/active and POST /responses request must pass:
  1. jwt.auth       — valid non-blacklisted JWT
  2. role:PR        — user has PR role
  3. branch.execution — server re-verifies branch authorization

RequireBranchExecution middleware → BranchExecutionVerifier → strategy:
  - UserBranchMatchStrategy: user's BranchId matches request context
  - BranchAccessKeyStrategy: X-Branch-Key header matches branch.branch_access_key in DB

The client assists in collecting the branch proof, but Laravel is the FINAL authority.
Never trust client-asserted branch context without server-side verification.
```

---

## 18. Security Checklist

```
☐ APP_DEBUG=false in production
☐ JWT blacklist_enabled=true, grace_period=0
☐ auth()->logout(true) on POST /auth/logout
☐ $fillable allowlist on every model — no $guarded = []
☐ Form Request validation on every endpoint — never validate in controllers
☐ Rate limit: 5/min on /auth/login, 60/min on all other API routes
☐ SecurityHeaders middleware: X-Frame-Options, X-Content-Type-Options, HSTS, CSP
☐ CORS: allowedOrigins restricted to Flutter app domain in production
☐ Never raw SQL string interpolation — always Eloquent parameterized queries
☐ SurveyTemplatePolicy checked in Template, Section, Question, Option controllers
☐ ResponsePolicy checked in Response show + PDF endpoints
☐ Defense-in-depth: role check in middleware AND inside controller/request
```

---

## 19. Dual Delivery (API + Web/MVC)

Every data service (`EmployeeAuditDataService`, `CycleAnalysisDataService`, etc.) must be consumed by **both**:

- `Api\ReportController` → returns JSON for Flutter
- `Web\ReportWebController` → returns Blade view for web admin panel

**No query logic may be duplicated between the two surfaces.** The service layer is the only implementation.

```
Flutter → Api\ReportController → ReportPermissionService → DataService → JSON
Browser → Web\ReportWebController → (same) DataService → Blade view
Flutter → Api\ReportController → (same) DataService → PdfRenderService → stream PDF
```

---

## 20. Build Order

Implement files in this order. Each step depends on the previous.

```
Step  1  Install packages: tymon/jwt-auth, spatie/laravel-permission, barryvdh/laravel-dompdf
Step  2  Publish configs: jwt, permission, dompdf
Step  3  Run all migrations (Section 3 order)
Step  4  Create ApplicationUser model (Section 5) — implements JWTSubject
Step  5  Create all remaining Eloquent models with $fillable and relationships
Step  6  Register middleware aliases (Section 7)
Step  7  Create Auth services: JwtAuthService, JwtBlacklistService, AuthenticatedUserContext
Step  8  Create Authorization services: RoleAuthorizationService, BranchExecutionVerifier + strategies
Step  9  Create all Form Requests (LoginRequest, StoreResponseRequest with XOR rule, etc.)
Step 10  Create SurveyTemplatePolicy + ResponsePolicy and register in AuthServiceProvider
Step 11  Create TemplateFilter, TemplateQueryService, TemplateCloneService, TemplateFactory
Step 12  Create ResponseScopeStrategy interface + all 4 concrete scopes + ResponseScopeFactory
Step 13  Create ReportPermissionService, DashboardService, SurveyService, all DataServices
Step 14  Create AbstractPdfReport, all 6 concrete PDF classes, PdfRenderService registry
Step 15  Create SurveyServiceProvider + RepositoryServiceProvider — bind all services via IoC
Step 16  Define all API routes with correct middleware chains (Section 8)
Step 17  Create all thin controllers (Api/ and Web/)
Step 18  Run seeders: RoleSeeder, ReportPermissionSeeder, DatabaseSeeder
Step 19  Create Blade views: PDF partials + report screens
Step 20  Write feature tests (see Section 21)
```

---

## 21. Required Feature Tests

Write a PHPUnit feature test for **each** of the following:

```
Auth
  ☐ PR can login and receives JWT with roles in payload
  ☐ Invalid credentials return HTTP 401
  ☐ Logout blacklists token — subsequent request with same token returns 401

Module 1 — Fill Survey
  ☐ PR can GET /cycles/active and receives only Active cycles with IsActive templates
  ☐ PRHeader receives HTTP 403 on GET /cycles/active
  ☐ PR can POST /responses with valid payload
  ☐ Answer with both option_id and text_value returns HTTP 400
  ☐ Answer with neither option_id nor text_value returns HTTP 400
  ☐ Submitting a response sets SurveyTemplate.HasResponses = true
  ☐ Branch execution check fails when X-Branch-Key is invalid → HTTP 403

Module 2 — Templates
  ☐ Quality can GET /templates
  ☐ PR receives HTTP 403 on GET /templates
  ☐ PATCH /templates/{id} on a template with HasResponses=true returns HTTP 409
  ☐ DELETE /templates/{id} on a locked template returns HTTP 409
  ☐ POST /templates/{id}/inherit creates a deep clone with correct ParentTemplateId

Dashboard
  ☐ PR receives only own responses in dashboard KPIs
  ☐ PRHeader receives only branch-level data
  ☐ Quality receives org-wide data
  ☐ Admin receives org-wide data

Reports
  ☐ GET /reports returns only report codes where CanView=true for the caller's role
  ☐ GET /reports/RPT-CYCLE-ANALYSIS returns HTTP 403 for PR
  ☐ GET /reports/RPT-CYCLE-ANALYSIS/pdf returns HTTP 403 when CanExportPdf=false

Responses Table
  ☐ Quality receives HTTP 403 on GET /responses
  ☐ PR sees only own responses
  ☐ PRHeader sees only branch responses
  ☐ Admin sees all responses
```

---

## 22. Acceptance Definition

The implementation is **complete and acceptable** only when:

1. All 20 build steps are done and `php artisan test` passes with 0 failures.
2. The same `ResponseScopeFactory` is used in `DashboardService`, `ResponseController`, and `ReportController` — no duplicated scope logic.
3. The `HasResponses` lock exists only in `SurveyTemplatePolicy` — not in controllers.
4. The answer XOR rule exists only in `StoreResponseRequest::withValidator()` — not in services or controllers.
5. `TemplateFactory::createFromParent()` calls `TemplateCloneService` — no clone code in the controller.
6. `PdfRenderService` dispatches via registry — no `if/else reportCode` chains.
7. All data services are shared between `Api\` and `Web\` controllers with zero duplication.
8. Every API route has backend role/permission enforcement regardless of what the Flutter UI shows.
9. Logout invalidates the JWT server-side and the token is rejected on next use.
10. Branch execution is re-verified by Laravel on every Module 1 request.

**Do not generate placeholder stubs. Generate complete, compilable, production-ready Laravel files.**

---

*Generated for PR Patient Survey System — Laravel 11 + MySQL + Flutter — May 2026*
