# Onboarding & Adoption System — Production Audit

**Audit date:** 2026-05-20  
**Scope:** End-to-end onboarding (wizard, checklist engine, guided tours, empty states, persistence, personas, security)  
**Method:** Static code review, API/policy tracing, translation diff, verification commands. No assumption that passing unit tests imply production correctness.

---

## Executive Summary

The onboarding system has a **solid skeleton** (persistence model, checklist engine, i18n, reusable components, TanStack Query integration) but is **not production-ready** for Department Heads — the primary target persona. Several wizard steps are **non-functional or placeholder**, permission boundaries are **misaligned**, and tests **do not cover the critical failure paths**.

**Production readiness score (onboarding only): 74 / 100** *(updated 2026-05-20 after critical fixes C-01–C-04)*

| Area | Score | Notes |
|------|-------|-------|
| Architecture | 58 | Portal-settings endpoint added; step validation still loose |
| UX / Product | 65 | Fake notification step fixed; close semantics corrected |
| Security | 62 | Portal endpoint scoped; checklist dept_id scoping still open |
| Functional correctness | 78 | Dept head portal step works; wizard close/complete separated |
| Tests | 68 | Portal API, onboarding close/complete, SetupWizard unit, E2E smoke |
| i18n / RTL | 78 | Full key parity EN/AR; RTL positioning gaps in tours |
| Performance | 76 | Onboarding query invalidation after wizard mutations |

---

## 1. Architecture Review

### 1.1 Backend layout

| File | Role | Status |
|------|------|--------|
| `database/migrations/2026_05_20_120000_add_onboarding_fields_to_users_table.php` | DB schema | **Used** |
| `app/Modules/Identity/Models/User.php` | Persistence fields | **Used** |
| `app/Modules/Identity/Enums/OnboardingPersona.php` | Persona enum | **Used** |
| `app/Modules/Identity/Services/OnboardingPersonaResolver.php` | Persona + dept resolution | **Used** — design issues (§4) |
| `app/Modules/Identity/Services/OnboardingService.php` | Step/complete/dismiss | **Used** — no step validation |
| `app/Modules/Identity/Services/ChecklistProgressService.php` | Checklist engine | **Used** — logic gaps (§3) |
| `app/Modules/Identity/Controllers/OnboardingController.php` | HTTP layer | **Used** — no policies |
| `app/Modules/Identity/Resources/OnboardingStatusResource.php` | JSON shape | **Used** |
| `app/Modules/Identity/Requests/UpdateOnboardingStepRequest.php` | Step validation | **Partial** — any string ≤64 chars |
| `app/Modules/Identity/Requests/DismissOnboardingTourRequest.php` | Tour dismiss | **Partial** — any tour_id |
| `app/Modules/Identity/Routes/api.php` | Routes under `/api/v1/onboarding` | **Used** |
| `app/Modules/Identity/Resources/AuthenticatedUserResource.php` | Exposes onboarding fields on `/auth/me` | **Used** |

**Missing (expected for enterprise):**
- No `OnboardingPolicy` or dedicated authorization layer
- No DTOs for checklist/status (arrays used inline)
- No step enum validation server-side
- No audit logging for onboarding completion/dismiss

### 1.2 Frontend layout

| File | Role | Status |
|------|------|--------|
| `features/onboarding/components/SetupWizard.tsx` | Main wizard | **Used** — contains fake steps |
| `features/onboarding/components/OnboardingGate.tsx` | App orchestration | **Used** |
| `features/onboarding/components/OnboardingModal.tsx` | Fullscreen shell | **Used** |
| `features/onboarding/components/WizardResumeBar.tsx` | Minimized wizard bar | **Used** (company admin minimize only) |
| `features/onboarding/components/GuidedTour.tsx` | Tour overlay | **Used** |
| `features/onboarding/components/ChecklistCard.tsx` | Dashboard widget | **Used** |
| `features/onboarding/components/EmptyStateCard.tsx` | Shared empty states | **Used** (6 pages) |
| `features/onboarding/components/WelcomeBanner.tsx` | Resume banner | **DEAD** — exported, never imported |
| `features/onboarding/hooks/useOnboarding.ts` | Primary hook | **Used** (Gate, Wizard, Dashboard, Tour) |
| `features/onboarding/hooks/useGuidedTour.ts` | Tour state | **Used** — `skip()` unused |
| `features/onboarding/hooks/useChecklistProgress.ts` | Standalone checklist | **DEAD** — never imported |
| `features/onboarding/services/onboardingService.ts` | API client | **Used** |
| `features/onboarding/constants/onboarding.constants.ts` | Steps, templates, tours | **Used** |
| `features/onboarding/types/onboarding.types.ts` | TS types | **Partial** — several types unused |
| `features/onboarding/components/index.ts` | Barrel exports | **Partial** — re-exports dead `WelcomeBanner` |
| `app/App.tsx` | Wraps `Router` in `OnboardingGate` | **Used** |
| `features/dashboard/pages/DashboardPage.tsx` | Checklist integration | **Used** |

### 1.3 Integration points

- **Dashboard:** `ChecklistCard` + `data-tour` targets on stats/recent sections
- **Admin pages:** `EmptyStateCard` on Users, Categories, SLA, Invites
- **Tickets / Notifications / Settings:** empty states + tour targets
- **No backend module registration** beyond Identity routes (appropriate for scope)

---

## 2. Persona & Scoping Review

### 2.1 Persona resolution (`OnboardingPersonaResolver`)

```php
// Priority order (first match wins):
1. is_super_admin OR company.departments.manage → company_admin
2. isDepartmentHead() → department_head
3. else → user
```

| Persona | Intended audience | Actual behavior | Issues |
|---------|-------------------|-----------------|--------|
| **Super admin** | Platform operator | Treated as `company_admin` | No global onboarding; assumes company-scoped checklist |
| **Company admin** | Org setup | 5-step link wizard | Functional as guided links only |
| **Department head** | Dept setup | 5-step wizard | **Portal step broken** (§2.2) |
| **Normal user** | Lightweight welcome | 4 text steps | Mostly non-interactive |

### 2.2 Critical persona leakage / suppression

| Issue | Severity | Detail |
|-------|----------|--------|
| Dual-role suppression | **High** | User with `company.departments.manage` **and** dept head never receives dept head onboarding |
| Super admin → company admin | **Medium** | Super admin checklist uses `company_id` on user row; may not match platform workflow |
| Dept head checklist dept | **Medium** | Uses first managed / primary dept only; multi-dept heads see one dept's progress |
| Checklist `department_id` query param | **High** | `GET /onboarding/checklist?department_id=X` accepts **any UUID** with no authorization — cross-department completion signals leak |

### 2.3 Route access vs checklist CTAs

Department head checklist routes from `ChecklistProgressService`:

| Item | Route | Required permission | Dept head has? |
|------|-------|---------------------|----------------|
| `portal_configured` | `/admin/departments` | `company.departments.manage` | **No** |
| `team_invited` | `/admin/invites` | `users.invite` / dept users manage | **Yes** |
| `categories_created` | `/admin/categories` | `department.categories.manage` | **Yes** |
| `sla_created` | `/settings/sla` | `department.sla.manage` | **Yes** |
| `first_ticket_created` | `/tickets/create` | `department.tickets.create` | **Yes** |

**Result:** Portal checklist CTA navigates to a **forbidden route** (`PermissionRoute` → dashboard redirect).

---

## 3. Wizard Step Verification (By Persona)

### 3.1 Department Head — “Let’s set up your department”

| Step | UI promise | Backend / real action | Functional? |
|------|------------|----------------------|-------------|
| **1. Portal** | Save name, slug, descriptions, enable portal | `PATCH /api/v1/departments/{id}` via `updateDepartment()` | **NO — 403** |
| | | `DepartmentPolicy::update()` requires `company.departments.manage` | |
| | | Dept heads have `department.settings.manage` but policy **does not use it** | |
| **2. Invite team** | Send invite, show pending | `POST /api/v1/admin/invites` | **Yes** (with caveats) |
| | Email send | `SendUserInviteEmailJob` queued | **Yes** |
| | Role assignment | `role_name` → pivot `role` on accept (`agent`/`viewer`/`manager`) | **Yes** |
| | Dept scoping | `normalizeInvitePayload()` forces inviter's managed dept | **Yes** |
| | Head checkbox | Hidden unless `department.heads.manage`; forced false for dept heads | **Yes** |
| | Skip | Advances step without invite | **Yes** |
| **3. Workflow** | Categories + priorities + SLA | Creates categories via template only | **Partial** |
| | Priorities | **Not implemented** in wizard | **No** |
| | SLA assignment | **Not implemented** | **No** |
| | Custom template | Empty — user can continue with 0 categories | **Misleading** |
| **4. Notifications & SLA** | Enable email, SLA warning, realtime | **Local React state only** | **NO — FAKE** |
| | | No call to notification preferences API | |
| | | No SLA policy creation | |
| **5. Go live** | Copy URL, test ticket, dashboard | Clipboard + navigate | **Yes** (if portal saved) |
| | Confetti | Static `✦` character | Cosmetic only |

**Resume / state:**
- `onboarding_step` persisted via `PATCH /onboarding/step` — **works**
- Refresh resumes at saved step — **works**
- Close (✕) calls `complete()` — **marks done without finishing (bug)**
- Company admin can minimize wizard (`WizardResumeBar`); dept head **cannot minimize**

### 3.2 Company Admin — “Set up your organization”

| Step | UI | Real action | Functional? |
|------|-----|-------------|-------------|
| departments | Text + “Manage departments” link | Navigates to admin page | **Link only** |
| heads | Link to users | **Link only** |
| settings | Link to settings | **Link only** |
| users | Link to users | **Link only** |
| review | Link to SLA + Finish | **Link only** |

No in-wizard CRUD. Progress is **not** verified before "Finish". User can click Continue through all steps without doing anything — then `complete()` fires.

**Minimize flow:** `openManagePage()` sets `wizardMinimized=true`, shows `WizardResumeBar` — **works** for link-out pattern.

### 3.3 Normal User — Welcome flow

| Step | UI | Real action | Functional? |
|------|-----|-------------|-------------|
| welcome | Intro text | Continue | **Placeholder** |
| department | “Choose your department” | **No selector** — text only | **Fake** |
| ticket | “Create first ticket” | Navigates to **`/profile/my-tickets`** (view, not create) | **Misleading CTA** |
| track | Track requests | Finish → dashboard | **Placeholder** |

**Wizard visibility:** Shown if `onboarding_completed_at` is null AND (`onboarding_step` set OR account age < 72h OR `last_login_at` null). Returning users after 72h may **never** see wizard — may be intentional but undocumented.

### 3.4 Super Admin

No distinct flow. Receives **company admin** wizard and checklist. Platform-level tasks (companies, global settings) not addressed.

---

## 4. Checklist Engine Review

### 4.1 Department head rules

| Key | Completion condition | Accurate? | Scoping | Issues |
|-----|---------------------|-----------|---------|--------|
| `portal_configured` | `portal_enabled && slug` | **Yes** | Per dept | CTA route wrong (§2.3) |
| `team_invited` | `activeUsers > 1` OR `pendingInvites >= 1` | **Mostly** | Per dept | Head alone = incomplete until invite |
| `categories_created` | `Category::exists()` for dept | **Yes** | Per dept | Includes soft-deleted? **No** (default scope) |
| `sla_created` | `SlaPolicy::exists()` for dept | **Yes** | Per dept | |
| `first_ticket_created` | `Ticket::exists()` for dept | **Yes** | Per dept | Any ticket, not necessarily by head |

**Missing from spec:** No `notifications_enabled` checklist item despite wizard step.

### 4.2 Company admin rules

| Key | Condition | False positive risk |
|-----|-----------|---------------------|
| `departments_created` | `departments.count >= 1` | **High** — true immediately if any dept exists |
| `department_heads_assigned` | Any dept has heads | **OK** |
| `company_settings_configured` | Any company-scoped setting row | **High** — seeded defaults may auto-complete |
| `active_users_reviewed` | `active users >= 2` | **Medium** — admin + one user completes |
| `sla_notifications_reviewed` | Any SLA in company OR any notification preference | **High** — very low bar |

### 4.3 Normal user rules

| Key | Condition | Issue |
|-----|-----------|-------|
| `department_selected` | User has any dept membership | **OK** |
| `first_ticket_created` | User is requester on ≥1 ticket | **OK** |
| `track_requests` | **Same condition as above** | **Duplicate** — always same completion state |

### 4.4 Cache & race conditions

| Concern | Finding |
|---------|---------|
| Frontend stale checklist | **Yes** — `useOnboarding` staleTime 30s; wizard mutations **do not invalidate** `ONBOARDING_QUERY_KEY` after portal/invite/category actions |
| Backend races | Checklist is read-time computed — **no race** on write |
| Multi-tab | Step updates last-write-wins on `users.onboarding_step` — **no conflict handling** |
| `updateStep` clears `onboarding_completed_at` | Re-opening wizard resets completion timestamp |

---

## 5. Onboarding State Machine

| Field | Behavior | Issues |
|-------|----------|--------|
| `onboarding_step` | Saved on each step change | Any string accepted; invalid steps possible |
| `onboarding_completed_at` | Set on `POST /complete` | **No proof-of-work** — trivially spoofed |
| `dismissed_tours` | JSON array append | No max length; no allowlist of tour IDs |

### State transition scenarios

| Scenario | Expected | Actual |
|----------|----------|--------|
| Refresh mid-step | Resume | **Works** |
| Logout / login | Resume if not complete | **Works** |
| Browser back | No URL routing for steps | **No effect** (modal overlay) |
| Close wizard (✕) | Dismiss / save progress | **Completes onboarding** — **bug** |
| Role change mid-wizard | Persona may change | **Undefined** — cached step may not match new persona steps |
| Department deleted | Wizard portal step | **Undefined** — `departmentId` may 404; no graceful handling |
| Portal disabled mid-setup | Checklist updates | Checklist reflects; wizard preview may show disabled portal |
| Multiple tabs | Two wizards | Both can advance steps — last write wins |
| Switch user | Clean state | New session fetch — **OK** |

### Onboarding loop risk

- Dept head / company admin: `should_show_wizard` true until `onboarding_completed_at` set — **cannot escape except complete or ✕ (which completes)**
- No "remind me later" that preserves incomplete state without completing

---

## 6. Guided Tours Review

### 6.1 Tour coverage

| Tour ID | Route | Targets | Dept portal tour? |
|---------|-------|---------|-------------------|
| `dashboard` | `/dashboard` | stats, recent, checklist | **No** |
| `tickets` | `/tickets` | list, create btn | **No** |
| `notifications` | `/notifications` | inbox | **No** |
| `settings` | `/settings*` | settings panel | **No** |

**Missing (per original spec):** ticket detail, department portal, admin nav.

### 6.2 Target existence

| Target | When missing | Behavior |
|--------|--------------|----------|
| `[data-tour="getting-started"]` | Checklist 100% complete (`ChecklistCard` returns null) | Spotlight null; tooltip still shows at fallback position |
| `[data-tour="settings-panel"]` | Empty settings list | **Missing target** — degraded tour |
| `[data-tour="dashboard-stats"]` | Duplicated on empty card + stats grid | Highlights first DOM match — ambiguous |
| `[data-tour="tickets-list"]` | No tickets (empty state wrapper) | **Works** |

### 6.3 UX / technical

| Check | Result |
|-------|--------|
| Missing target handling | Tooltip falls back to (24, 80) — **poor UX** |
| Mobile | Fixed pixel tooltip; `max-width: 320px` — **acceptable** |
| RTL | Uses physical `left`/`top` — **not RTL-aware** |
| z-index | Tour `modal+5`, Wizard `modal+10` — wizard wins when both shown |
| Modal overlap | Tour disabled while wizard active (`!shouldShowWizard`) — **OK** |
| Scroll listeners | Added/removed on step change — **OK** |
| Route change cleanup | Timer cleared; tour re-evaluates — **OK** |
| Memory leaks | Listeners cleaned up — **OK** |
| Dismiss persistence | `POST /onboarding/tours/dismiss` — **works** |
| Backdrop click | Dismisses forever — may be aggressive |

---

## 7. Empty States Review

| Page | Component | CTA | Route correct? | i18n | Issues |
|------|-----------|-----|----------------|------|--------|
| Admin Users | `EmptyStateCard` | Invite team | `window.location.assign(/admin/invites)` | **Yes** | Full reload; emoji icon |
| Admin Categories | `EmptyStateCard` | Create category | Opens modal | **Yes** | **Good** |
| Admin SLA | `EmptyStateCard` | Create SLA | Opens modal | **Yes** | **Good** |
| Admin Invites | `EmptyStateCard` | Focus email field | `#invite-email` | **Yes** | **Good** |
| Tickets | `EmptyStateCard` | Create ticket | `/tickets/create` | **Yes** | **Good** |
| Notifications | `EmptyStateCard` | Configure notifications | `/notifications/preferences` | **Yes** | **Good** |

**Not covered:** Admin Departments, Audit, Companies, Ticket Meta, Assignee Performance — still use plain text empty messages.

**Tone:** Emoji icons (👥📁⏱✉️🎫🔔) feel less enterprise than the rest of MVNexus.

**Dark mode:** Uses theme tokens — **OK**.

**RTL:** `text-align: start` used — **OK**.

---

## 8. Translation Review

| Check | Result |
|-------|--------|
| EN/AR key parity | **131 / 131 keys — complete** |
| Hardcoded English in onboarding code | **None found** in components |
| `resumeHint` | EN: "Organization setup is in progress" — **wrong for dept head / user** |
| Terminology consistency | Uses "Department", "Portal", "SLA", "Ticket" consistently |
| Interpolation | `{{completed}}/{{total}}`, step progress — **OK** |
| AR wizard strings | Present and natural |

**Gap:** `welcome.*` keys exist for `WelcomeBanner` but component is unused.

---

## 9. Security Review

| Check | Result | Severity |
|-------|--------|----------|
| Onboarding APIs require auth | `auth:sanctum` on all routes | **Pass** |
| Dedicated policies | **None** — only `$request->user() !== null` on step/dismiss | **Medium** |
| Completion spoofing | Any authenticated user can `POST /complete` | **Medium** |
| Step injection | Any string step accepted | **Low** |
| Privilege escalation via onboarding | **No** direct escalation | **Pass** |
| Invite abuse in wizard | Uses same `UserInviteService` guards | **Pass** |
| Checklist info leak | `department_id` param not authorized | **High** |
| Frontend trusts local state | Wizard uses API for persistence; `should_show_wizard` from server | **Pass** |
| User profile update onboarding fields | Not in `UpdateUserRequest` | **Pass** |
| Mass assignment | Onboarding fields in `$fillable` but only updated via `OnboardingService` | **Pass** |
| Rate limiting on onboarding endpoints | **None** | **Low** |

---

## 10. Performance Review

| Check | Finding |
|-------|---------|
| API calls on load | One `GET /onboarding/status` per authenticated session (30s stale) via `OnboardingGate` |
| Duplicate hooks | `useOnboarding` in Gate + Dashboard + GuidedTour — **deduped by query key** |
| `useChecklistProgress` | Dead — no extra calls |
| Bundle impact | Onboarding in main chunk (~158 KB gzip ~43 KB total main) — moderate |
| Tour listeners | Window resize/scroll per active step — negligible |
| Wizard queries | Dept fetch, invites, categories — lazy per step | **Good** |
| Missing invalidation | Checklist stale up to 30s after wizard actions | **Medium UX** |

---

## 11. Dead Code & Cleanup Inventory

| Item | Path | Recommendation |
|------|------|----------------|
| `WelcomeBanner` | `components/WelcomeBanner.tsx` | Wire up or delete |
| `useChecklistProgress` | `hooks/useChecklistProgress.ts` | Delete or use for dept picker |
| `skip()` in `useGuidedTour` | `hooks/useGuidedTour.ts` | Remove export |
| Barrel export of WelcomeBanner | `components/index.ts` | Clean up |
| Unused TS types | `TourDefinition`, `CategoryTemplate`, wizard step unions | Keep if planned, else trim |
| Duplicate checklist logic | Status embeds checklist; standalone endpoint duplicates | Acceptable |
| `ONBOARDING_AND_ADOPTION_SYSTEM_REPORT.md` | Claims features not fully implemented | Update after fixes |

**Not dead (confirmed used):** `WizardResumeBar`, `OnboardingModal`, `SetupWizard`, `GuidedTour`, `ChecklistCard`, `EmptyStateCard`, `OnboardingGate`.

---

## 12. Critical Bugs

> **Update (2026-05-20):** C-01 through C-04 have been **fixed**. See `docs/ONBOARDING_CRITICAL_FIXES_REPORT.md`.

| ID | Severity | Issue | Status |
|----|----------|-------|--------|
| **C-01** | ~~Critical~~ | Department head **cannot save portal settings** in wizard | **Fixed** — `PATCH /departments/{id}/portal-settings` |
| **C-02** | ~~Critical~~ | **Notifications & SLA wizard step is fake** | **Fixed** — real preference API + SLA CTA |
| **C-03** | ~~Critical~~ | Closing wizard (✕) calls `complete()` prematurely | **Fixed** — save and exit |
| **C-04** | ~~Critical~~ | Checklist portal CTA routes dept heads to `/admin/departments` | **Fixed** — `/dashboard?onboarding=portal` |

---

## 13. High / Medium / Low Issues

### High

| ID | Issue |
|----|-------|
| H-01 | Dual-role company admin + dept head never gets dept head onboarding |
| H-02 | `GET /onboarding/checklist?department_id=` lacks authorization |
| H-03 | Company admin wizard is entirely non-functional (link slideshow) |
| H-04 | User wizard steps are non-interactive / misleading CTAs |
| H-05 | ~~Wizard actions don't invalidate onboarding status query~~ **Fixed** |
| H-06 | Company admin checklist items have high false-positive rate |
| H-07 | Workflow step omits priorities (spec requirement) |
| H-08 | ~~No tests for dept head portal update 403~~ **Fixed** |

### Medium

| ID | Issue |
|----|-------|
| M-01 | Super admin forced into company admin persona |
| M-02 | `track_requests` checklist duplicates `first_ticket_created` |
| M-03 | Tour targets missing when checklist complete / settings empty |
| M-04 | RTL tour positioning uses physical coordinates |
| M-05 | Fullscreen wizard blocks all routes (including `/d/:slug` portal preview) for incomplete users |
| M-06 | `resumeHint` copy assumes company admin context |
| M-07 | Emoji empty-state icons |
| M-08 | AdminUsers empty CTA uses full page reload |
| M-09 | ~~No Playwright onboarding smoke tests~~ **Fixed** — `onboarding-wizard-smoke.spec.ts` |
| M-10 | `updateStep` clears `onboarding_completed_at` without audit trail |

### Low

| ID | Issue |
|----|-------|
| L-01 | `dismissed_tours` accepts arbitrary IDs, unbounded array |
| L-02 | No server-side step enum validation |
| L-03 | 72-hour user wizard window heuristic undocumented |
| L-04 | ESLint failure in unrelated `AdminUsersPage.test.tsx` |
| L-05 | 7 OTP tests fail (429 rate limit) — unrelated to onboarding |

---

## 14. Recommended Cleanup

1. **Delete or integrate dead code:** `WelcomeBanner`, `useChecklistProgress`, unused exports.
2. **Align checklist routes with permissions:** Portal item → settings or dedicated dept portal admin surface.
3. **Fix `DepartmentPolicy::update`** to allow dept heads with `department.settings.manage` OR add dedicated portal settings endpoint.
4. **Remove fake notification step** until wired to `notification-preferences` API — or implement persistence.
5. **Replace ✕ behavior** with "Save & exit" (persist step) vs "Mark complete".
6. **Authorize checklist `department_id`** against `managedDepartmentIds()` / company scope.
7. **Invalidate** `ONBOARDING_QUERY_KEY` after wizard mutations.
8. **Add Playwright smoke:** dept head login → wizard visible → invite step → checklist on dashboard.

---

## 15. Recommended Onboarding Simplification

| Current | Recommendation |
|---------|----------------|
| 5-step dept head wizard with fake notification step | **4 steps:** Portal → Invite → Workflow → Go Live (move notifications to post-setup checklist item) |
| 5-step company admin link slideshow | **Single checklist widget** + optional 1-screen "Org setup guide" with deep links |
| 4-step user slideshow | **1 welcome modal** + checklist (dept + first ticket) |
| Separate wizard + checklist tracking same things | **Checklist-driven setup** with wizard only for first-run portal + invite |
| Fullscreen blocking modal | **Allow dismiss/resume** without completing; show `WelcomeBanner` or `WizardResumeBar` for all personas |
| Company admin + dept head personas | **Multi-track onboarding** — show dept head track if `managedDepartmentIds().isNotEmpty()` regardless of company admin |

**Merge candidates:**
- User steps `ticket` + `track` → one "Submit and track requests" step
- Company admin steps `heads` + `users` → one "Team & roles" step
- Dept head `notifications` → defer to settings checklist item

**Auto-skip candidates:**
- `portal_configured` if slug auto-generated on dept create
- `departments_created` for company admin when ≥1 dept exists (mark informational, not blocking)
- User `department_selected` when invite flow already assigned dept

---

## 16. Verification Results

Commands run during audit:

| Command | Result |
|---------|--------|
| `php artisan test` | **150 / 157 pass** — 7 failures in `OtpAuthTest` (HTTP 429 rate limit / job queue), unrelated to onboarding |
| `php artisan test tests/Feature/Identity/OnboardingApiTest.php` | **4 / 4 pass** |
| `npm run type-check` | **Pass** |
| `npm run lint` | **Fail** — unused `waitFor` in `AdminUsersPage.test.tsx` (pre-existing) |
| `npm run build` | **Pass** |
| `npx vitest run resources/js/src/__tests__/unit` | **72 / 72 pass** |
| Playwright onboarding specs | **None exist** |

### Recommended screenshots for UAT (manual)

1. Dept head first login — fullscreen wizard step 1 (portal)
2. Portal save error (network tab 403) — documents C-01
3. Notifications step toggles — before/after refresh showing no persistence (C-02)
4. Dashboard checklist widget — partial completion
5. Guided tour on dashboard — spotlight on stats
6. Guided tour with completed checklist — missing third step target
7. Empty state — Admin Categories
8. Wizard minimize + `WizardResumeBar` (company admin)
9. AR locale — wizard step 1 RTL layout
10. Mobile viewport — wizard + tour tooltip positioning

---

## 17. Test Gap Analysis

| Scenario | Covered? |
|----------|----------|
| Step persistence | **Yes** — API test |
| Tour dismiss persistence | **Yes** — API test |
| Checklist auto-complete | **Partial** — dept head, 4/5, no team_invited |
| Dept head portal update | **Yes** — `DepartmentPortalSettingsApiTest` |
| Notifications step persistence | **Yes** — `SetupWizard.test.tsx` |
| Permission denied paths | **Partial** — portal-settings forbidden paths |
| Persona resolution edge cases | **No** |
| Frontend wizard integration | **Partial** — `SetupWizard.test.tsx` |
| Guided tour rendering | **No** |
| E2E onboarding smoke | **Yes** — `onboarding-wizard-smoke.spec.ts` |

---

## 18. Production Readiness Verdict

**Department Head UAT:** Critical blockers C-01–C-04 are resolved — **safe to proceed with Department Head UAT**. See `docs/ONBOARDING_CRITICAL_FIXES_REPORT.md`.

The system remains suitable for company admin checklist/link flows. General production hardening (checklist scoping, dual-role personas, company admin wizard depth) is still recommended before a full enterprise rollout.

**Score: 74 / 100** (was 42/100)

To reach **80+** (full production-ready):
1. Add checklist scoping authorization (H-02)
2. Resolve dual-role persona leakage (H-01)
3. Improve company admin / user wizard depth (H-03, H-04)
4. Add server-side step enum validation
5. Remove dead onboarding code

---

*Auditor: automated static + command verification audit. Manual browser QA recommended to confirm 403 on portal save and tour behavior.*
