# Policy-Based User Onboarding Engine

Implementation specification for a configurable **Policy-Based User Onboarding Engine** inside the ERPFlow Core Framework.

This is **not** a business module. It is implemented as a reusable **Core Platform Service**.

---

## Table of Contents

1. [Goal](#goal)
2. [Current State & Gaps](#current-state--gaps)
3. [Architecture Rules](#architecture-rules)
4. [Directory Structure](#directory-structure)
5. [Database Schema](#database-schema)
6. [Enums & Constants](#enums--constants)
7. [Policy Logic](#policy-logic)
8. [Login Flow](#login-flow)
9. [Step Flow](#step-flow)
10. [Step Handler Pattern](#step-handler-pattern)
11. [Service Layer](#service-layer)
12. [Repository Layer](#repository-layer)
13. [API Endpoints](#api-endpoints)
14. [Middleware](#middleware)
15. [Events & Jobs](#events--jobs)
16. [Frontend Integration](#frontend-integration)
17. [Implementation Phases](#implementation-phases)
18. [Testing Checklist](#testing-checklist)

---

## Goal

Admin-created users may need to complete a first-login onboarding flow based on policy.

The onboarding flow may include:

1. Mobile OTP verification
2. Email OTP verification
3. New password setup
4. Document upload

**Document upload** is required but **non-blocking for the first 15 days**. After 15 days, if documents are not uploaded, user access must be restricted to document upload only.

**Developer users** must not go through this onboarding flow.

The engine must be generic and reusable — not hardcoded to employee onboarding. Future user types (vendor, customer, agent) must use the same engine.

---

## Current State & Gaps

| Area | Today | Required |
|------|-------|----------|
| User create flow | Seeder only; no `UserService` | Admin user create + onboarding hook |
| User fields | No `mobile`, `user_type`, `department`, `is_developer` | Migration required |
| Login response | Token + user + companies | Onboarding state flags |
| OTP / file upload | Not implemented | `OtpService` + Laravel Storage |
| Middleware | `EnsurePermission` only | `EnsureOnboardingAccess`, `EnsureDocumentDeadline` |
| Frontend | `ProtectedRoute` auth check only | Onboarding guard + dashboard warning |

**Reference pattern:** `ApprovalEngine` — workflow version snapshot, step registry, enum-based status, `DB::transaction`.

**Location note:** The spec suggests `Modules/Core/App/Onboarding/`. In this project, core services live under `backend/app/Core/` (alongside `Auth`, `Users`). Platform services live under `backend/app/Platform/`. Onboarding belongs in **`backend/app/Core/Onboarding/`**.

---

## Architecture Rules

Follow the ERPFlow coding standard (Service Repository Pattern):

| Rule | Requirement |
|------|-------------|
| Service Interface | Mandatory for every service |
| Repository Interface | Mandatory for every repository |
| Controller | Thin — receive request, validate, call service, return response |
| Controller methods | Must use `try/catch` |
| Business logic | Service layer only |
| Write operations | `DB::transaction` in service layer |
| Validation | Form Request mandatory |
| API response | Standardized via `ApiResponseTrait` |
| Complex payloads | Use DTOs |
| Side effects | Events / Listeners / Jobs |
| Status values | Enums, not raw strings |

---

## Directory Structure

```
backend/app/Core/Onboarding/
├── Controllers/
│   ├── OnboardingController.php
│   ├── OnboardingDocumentController.php
│   └── Admin/
│       ├── OnboardingPolicyController.php
│       └── OnboardingPolicyStepController.php
├── Requests/
│   ├── SendMobileOtpRequest.php
│   ├── VerifyMobileOtpRequest.php
│   ├── SendEmailOtpRequest.php
│   ├── VerifyEmailOtpRequest.php
│   ├── ChangePasswordRequest.php
│   ├── UploadDocumentRequest.php
│   └── Admin/
│       ├── StoreOnboardingPolicyRequest.php
│       ├── UpdateOnboardingPolicyRequest.php
│       ├── StoreOnboardingPolicyStepRequest.php
│       └── UpdateOnboardingPolicyStepRequest.php
├── Resources/
│   ├── OnboardingStatusResource.php
│   ├── OnboardingPolicyResource.php
│   ├── OnboardingPolicyStepResource.php
│   └── UserDocumentResource.php
├── Services/
│   ├── Contracts/
│   │   ├── OnboardingServiceInterface.php
│   │   ├── OnboardingPolicyServiceInterface.php
│   │   ├── OnboardingAccessServiceInterface.php
│   │   └── OtpServiceInterface.php
│   ├── OnboardingService.php
│   ├── OnboardingPolicyService.php
│   ├── OnboardingAccessService.php
│   └── OtpService.php
├── Repositories/
│   ├── Contracts/
│   │   ├── OnboardingPolicyRepositoryInterface.php
│   │   ├── OnboardingInstanceRepositoryInterface.php
│   │   ├── OnboardingStepRepositoryInterface.php
│   │   ├── UserOtpRepositoryInterface.php
│   │   └── UserDocumentRepositoryInterface.php
│   ├── OnboardingPolicyRepository.php
│   ├── OnboardingInstanceRepository.php
│   ├── OnboardingStepRepository.php
│   ├── UserOtpRepository.php
│   └── UserDocumentRepository.php
├── Handlers/
│   ├── Contracts/
│   │   └── OnboardingStepHandlerInterface.php
│   ├── MobileOtpStepHandler.php
│   ├── EmailOtpStepHandler.php
│   ├── PasswordChangeStepHandler.php
│   ├── DocumentUploadStepHandler.php
│   └── OnboardingStepHandlerResolver.php
├── Middleware/
│   ├── EnsureOnboardingAccess.php
│   └── EnsureDocumentDeadline.php
├── Enums/
│   ├── OnboardingInstanceStatus.php
│   ├── OnboardingStepStatus.php
│   ├── OnboardingStepKey.php
│   ├── OnboardingPolicyAppliesTo.php
│   ├── OtpType.php
│   └── UserDocumentStatus.php
├── DTOs/
│   ├── CreateOnboardingPolicyData.php
│   ├── UpdateOnboardingPolicyData.php
│   ├── VerifyOtpData.php
│   ├── ChangePasswordData.php
│   ├── UploadDocumentData.php
│   └── OnboardingAccessStateDto.php
├── Events/
│   └── UserOnboardingInitialized.php
├── Listeners/
│   └── InitializeUserOnboardingListener.php
├── Jobs/
│   ├── SendOtpJob.php
│   └── ExpireDocumentUploadDeadlinesJob.php
└── Models/
    ├── OnboardingPolicy.php
    ├── OnboardingPolicyStep.php
    ├── UserOnboardingInstance.php
    ├── UserOnboardingStepProgress.php
    ├── UserOtp.php
    └── UserDocument.php
```

**Prerequisites** (outside Onboarding module):

```
backend/app/Core/Users/
├── Services/
│   ├── Contracts/UserServiceInterface.php
│   └── UserService.php
├── Events/UserCreated.php
└── DTOs/CreateUserData.php
```

**Frontend:**

```
frontend/src/modules/onboarding/
├── api/onboardingApi.ts
├── context/OnboardingContext.tsx
├── components/OnboardingRoute.tsx
├── components/DocumentWarningBanner.tsx
├── pages/
│   ├── MobileOtpPage.tsx
│   ├── EmailOtpPage.tsx
│   ├── ChangePasswordPage.tsx
│   └── DocumentsPage.tsx
└── index.tsx

frontend/src/modules/platform/pages/
├── OnboardingPolicyList.tsx
├── OnboardingPolicyForm.tsx
└── OnboardingPolicyStepBuilder.tsx
```

---

## Database Schema

### Prerequisites: `users` table extensions

Add via migration before onboarding tables:

| Column | Type | Notes |
|--------|------|-------|
| `mobile` | string, nullable, unique | Required for mobile OTP step |
| `user_type` | enum/string | `developer`, `employee`, `vendor`, `customer`, `agent`, … |
| `department_id` | FK, nullable | Future-ready for policy matching |
| `designation_id` | FK, nullable | Future-ready for policy matching |
| `password_changed_at` | timestamp, nullable | Set on password change step |
| `mobile_verified_at` | timestamp, nullable | Set on mobile OTP verify |

---

### `onboarding_policies`

| Column | Type | Notes |
|--------|------|-------|
| `id` | bigint PK | |
| `company_id` | FK, nullable | Null = global policy |
| `name` | string | |
| `description` | text, nullable | |
| `applies_to_type` | string/enum | See [Policy Matching](#policy-matching-priority) |
| `applies_to_value` | string, nullable | Role ID, user ID, user_type value, etc. |
| `is_active` | boolean | Default `true` |
| `created_at` | timestamp | |
| `updated_at` | timestamp | |

**Index:** `(company_id, applies_to_type, is_active)`

**`applies_to_type` values:**

```
specific_user
role
user_type
department
designation
company_default
```

---

### `onboarding_policy_steps`

| Column | Type | Notes |
|--------|------|-------|
| `id` | bigint PK | |
| `policy_id` | FK | |
| `step_key` | string | `mobile_otp`, `email_otp`, `password_change`, `document_upload` |
| `step_name` | string | Display name |
| `step_order` | integer | Execution order |
| `is_required` | boolean | |
| `is_blocking` | boolean | Blocks dashboard access when incomplete |
| `allow_skip` | boolean | |
| `deadline_days` | integer, nullable | Used by `document_upload` (default 15) |
| `config` | json, nullable | OTP length, document types, etc. |
| `is_active` | boolean | |
| `created_at` | timestamp | |
| `updated_at` | timestamp | |

**Unique:** `(policy_id, step_key)`

**Default steps:**

| step_key | is_blocking | deadline_days |
|----------|-------------|---------------|
| `mobile_otp` | true | — |
| `email_otp` | true | — |
| `password_change` | true | — |
| `document_upload` | false | 15 |

---

### `user_onboarding_instances`

| Column | Type | Notes |
|--------|------|-------|
| `id` | bigint PK | |
| `user_id` | FK | |
| `policy_id` | FK, nullable | Null when `not_required` |
| `status` | enum | See [OnboardingInstanceStatus](#onboardinginstancestatus) |
| `current_step_key` | string, nullable | Active step |
| `started_at` | timestamp, nullable | |
| `completed_at` | timestamp, nullable | |
| `expires_at` | timestamp, nullable | |
| `created_at` | timestamp | |
| `updated_at` | timestamp | |

**Unique:** `(user_id)` — one active instance per user

---

### `user_onboarding_step_progress`

Runtime snapshot copied from policy steps at instance creation. Policy changes do not affect in-progress users.

| Column | Type | Notes |
|--------|------|-------|
| `id` | bigint PK | |
| `onboarding_instance_id` | FK | |
| `step_key` | string | |
| `step_name` | string | |
| `step_order` | integer | |
| `status` | enum | See [OnboardingStepStatus](#onboardingstepstatus) |
| `is_required` | boolean | |
| `is_blocking` | boolean | |
| `deadline_at` | timestamp, nullable | Set after password change for `document_upload` |
| `completed_at` | timestamp, nullable | |
| `metadata` | json, nullable | Step-specific data |
| `created_at` | timestamp | |
| `updated_at` | timestamp | |

---

### `user_otps`

| Column | Type | Notes |
|--------|------|-------|
| `id` | bigint PK | |
| `user_id` | FK | |
| `type` | enum | `mobile`, `email` |
| `otp_hash` | string | Never store plain OTP |
| `expires_at` | timestamp | |
| `verified_at` | timestamp, nullable | |
| `attempt_count` | integer | Default `0`, max 5 |
| `status` | string | `pending`, `verified`, `expired` |
| `created_at` | timestamp | |
| `updated_at` | timestamp | |

---

### `user_documents`

| Column | Type | Notes |
|--------|------|-------|
| `id` | bigint PK | |
| `user_id` | FK | |
| `document_type` | string | From policy step `config` |
| `file_path` | string | Laravel Storage path |
| `original_name` | string, nullable | |
| `mime_type` | string, nullable | |
| `size` | integer, nullable | Bytes |
| `status` | enum | `pending`, `uploaded`, `verified`, `rejected` |
| `uploaded_at` | timestamp, nullable | |
| `verified_at` | timestamp, nullable | |
| `rejected_reason` | text, nullable | |
| `created_at` | timestamp | |
| `updated_at` | timestamp | |

---

## Enums & Constants

### `OnboardingInstanceStatus`

```
not_required
pending
in_progress
completed
expired
blocked
```

### `OnboardingStepStatus`

```
pending
in_progress
completed
expired
skipped
```

### `OnboardingStepKey`

```
mobile_otp
email_otp
password_change
document_upload
```

### `OnboardingPolicyAppliesTo`

```
specific_user
role
user_type
department
designation
company_default
```

### `OtpType`

```
mobile
email
```

### `UserDocumentStatus`

```
pending
uploaded
verified
rejected
```

---

## Policy Logic

### User Creation Flow

```
Admin creates user
        ↓
UserService::createUser()
        ↓
event(new UserCreated($user))
        ↓
InitializeUserOnboardingListener
        ↓
OnboardingService::initializeForUser($user)
        ↓
OnboardingPolicyService::shouldSkipOnboarding($user)?
   YES → instance status = not_required
   NO  → resolve applicable policy
              ↓
         policy found?
           YES → create instance + copy steps to step_progress
           NO  → instance status = not_required
```

**Developer bypass:** Implemented in `OnboardingPolicyService::shouldSkipOnboarding(User)` when `user_type === 'developer'`. Not hardcoded in controllers.

### Policy Matching Priority

When multiple active policies match, use the **highest priority** policy:

```
1. specific_user   → user.id matches applies_to_value
2. role            → user role ID matches applies_to_value
3. user_type       → users.user_type matches applies_to_value
4. department      → users.department_id matches applies_to_value
5. designation     → users.designation_id matches applies_to_value
6. company_default → applies_to_type = company_default AND company_id matches
```

### Runtime Step Copy

When an onboarding instance is created, all active policy steps are copied into `user_onboarding_step_progress`. This mirrors the `ApprovalEngine::createWorkflowVersion` snapshot pattern — future policy edits do not break in-progress users.

```php
foreach ($policy->activeSteps as $step) {
    $this->stepRepository->createProgress([
        'step_key'     => $step->step_key,
        'step_name'    => $step->step_name,
        'step_order'   => $step->step_order,
        'is_required'  => $step->is_required,
        'is_blocking'  => $step->is_blocking,
        'status'       => OnboardingStepStatus::Pending,
        'deadline_at'  => null, // set after password_change completes
    ]);
}
```

---

## Login Flow

After successful login, `AuthService::buildAuthPayload()` merges onboarding state from `OnboardingAccessService::resolve($user)`:

```
Check onboarding status
        ↓
No onboarding required          → allow dashboard
Blocking step incomplete        → onboarding_required = true, redirect to current_step
Only document_upload pending
  + deadline NOT expired        → allow dashboard + document_warning = true
document_upload pending
  + deadline expired            → access_restricted = true (upload + logout only)
All steps complete              → allow dashboard
```

### Login Response Extension

```json
{
  "access_token": "...",
  "token_type": "bearer",
  "expires_in": 3600,
  "user": {},
  "companies": [],
  "current_company": {},
  "onboarding": {
    "onboarding_required": true,
    "current_step": "mobile_otp",
    "document_warning": false,
    "document_deadline_at": null,
    "access_restricted": false
  }
}
```

The `/auth/me` endpoint must return the same `onboarding` object for frontend refresh.

---

## Step Flow

### Step 1: Mobile OTP

1. `POST /onboarding/mobile-otp/send` → generate OTP, hash, dispatch `SendOtpJob`
2. `POST /onboarding/mobile-otp/verify` → verify hash, set `mobile_verified_at`
3. Mark step `completed`, advance `current_step_key`

### Step 2: Email OTP

1. `POST /onboarding/email-otp/send`
2. `POST /onboarding/email-otp/verify` → set `email_verified_at`
3. Mark step `completed`, advance

### Step 3: Password Change

1. `POST /onboarding/change-password` → validate, update password
2. Set `password_changed_at`
3. Mark step `completed`
4. **Start document deadline** on `document_upload` step progress:

```php
$deadlineDays = $step->config['deadline_days'] ?? 15;
$progress->update(['deadline_at' => now()->addDays($deadlineDays)]);
```

5. Advance to `document_upload`

### Step 4: Document Upload

- User can upload immediately or defer (non-blocking for `deadline_days`)
- `POST /onboarding/documents/upload` → store file, create `user_documents` record
- When all required documents uploaded → mark step `completed`, instance `completed`
- If `deadline_at` passes without upload → step `expired`, instance `blocked`

---

## Step Handler Pattern

```php
interface OnboardingStepHandlerInterface
{
    public function stepKey(): OnboardingStepKey;

    public function handle(User $user, array $payload): bool;

    public function isCompleted(User $user): bool;
}
```

| Handler | `handle()` | `isCompleted()` |
|---------|-----------|-----------------|
| `MobileOtpStepHandler` | Verify OTP via `OtpService` | `mobile_verified_at` is set |
| `EmailOtpStepHandler` | Verify OTP via `OtpService` | `email_verified_at` is set |
| `PasswordChangeStepHandler` | Update password, set `password_changed_at`, start document deadline | `password_changed_at` is set |
| `DocumentUploadStepHandler` | Store file, create document record | All required docs uploaded |

`OnboardingStepHandlerResolver` maps `step_key` → handler. Register handlers in `CoreServiceProvider`:

```php
$this->app->tag([
    MobileOtpStepHandler::class,
    EmailOtpStepHandler::class,
    PasswordChangeStepHandler::class,
    DocumentUploadStepHandler::class,
], 'onboarding.handlers');
```

---

## Service Layer

### `OnboardingPolicyService`

- Admin CRUD for policies and steps
- `resolveApplicablePolicy(User $user, int $companyId): ?OnboardingPolicy`
- `shouldSkipOnboarding(User $user): bool`

### `OnboardingService`

- `initializeForUser(User $user): UserOnboardingInstance`
- `getStatus(User $user): array`
- `completeStep(User $user, string $stepKey, array $payload): void`
- `advanceToNextStep(User $user): void`

All write methods wrapped in `DB::transaction`.

### `OnboardingAccessService`

Single source of truth for access decisions (used by login, middleware, and frontend):

```php
resolve(User $user): OnboardingAccessStateDto
```

| State | `onboarding_required` | `access_restricted` | `document_warning` |
|-------|----------------------|-------------------|-------------------|
| `not_required` | false | false | false |
| Blocking step pending | true | false | false |
| Document pending, deadline OK | false | false | true |
| Document pending, deadline expired | false | true | false |
| Completed | false | false | false |

### `OtpService`

- `sendMobileOtp(User $user): void`
- `verifyMobileOtp(User $user, string $otp): bool`
- `sendEmailOtp(User $user): void`
- `verifyEmailOtp(User $user, string $otp): bool`

OTP rules: hash storage, 5–10 min expiry, max 5 attempts, dispatch via `SendOtpJob`.

---

## Repository Layer

Repositories handle database access only — no business logic.

| Repository | Key methods |
|------------|-------------|
| `OnboardingPolicyRepository` | `findActiveForCompany()`, `findById()`, `create()`, `update()`, `delete()` |
| `OnboardingInstanceRepository` | `findByUserId()`, `create()`, `updateStatus()` |
| `OnboardingStepRepository` | `createProgress()`, `findByInstanceAndKey()`, `updateProgress()` |
| `UserOtpRepository` | `create()`, `findActiveForUser()`, `incrementAttempts()` |
| `UserDocumentRepository` | `create()`, `findByUserAndType()`, `listForUser()` |

---

## API Endpoints

All routes use the existing `api/v1` prefix.

### User Onboarding Routes

Middleware: `auth:api` (onboarding routes excluded from `onboarding.access` middleware to avoid circular blocks)

| Method | Path | Description |
|--------|------|-------------|
| GET | `/onboarding/status` | Current onboarding state |
| POST | `/onboarding/mobile-otp/send` | Send mobile OTP |
| POST | `/onboarding/mobile-otp/verify` | Verify mobile OTP |
| POST | `/onboarding/email-otp/send` | Send email OTP |
| POST | `/onboarding/email-otp/verify` | Verify email OTP |
| POST | `/onboarding/change-password` | Set new password |
| POST | `/onboarding/documents/upload` | Upload document |
| GET | `/onboarding/documents` | List user documents |

### Admin Routes

Middleware: `auth:api`, `permission:onboarding.manage`

| Method | Path | Description |
|--------|------|-------------|
| GET | `/admin/onboarding/policies` | List policies |
| POST | `/admin/onboarding/policies` | Create policy |
| GET | `/admin/onboarding/policies/{id}` | Show policy |
| PUT | `/admin/onboarding/policies/{id}` | Update policy |
| DELETE | `/admin/onboarding/policies/{id}` | Delete policy |
| POST | `/admin/onboarding/policies/{id}/steps` | Add step |
| PUT | `/admin/onboarding/policies/{id}/steps/{stepId}` | Update step |
| DELETE | `/admin/onboarding/policies/{id}/steps/{stepId}` | Delete step |

### Route Registration

Add to `backend/routes/api.php`:

```php
// Onboarding (accessible during blocking onboarding)
Route::middleware('auth:api')->prefix('onboarding')->group(function () {
    Route::get('status', [OnboardingController::class, 'status']);
    Route::post('mobile-otp/send', [OnboardingController::class, 'sendMobileOtp']);
    // ...
});

// Protected app routes (blocked during onboarding)
Route::middleware(['auth:api', 'onboarding.access'])->group(function () {
    // existing routes...
});

// Admin onboarding
Route::middleware(['auth:api', 'permission:onboarding.manage'])
    ->prefix('admin/onboarding')->group(function () {
    Route::apiResource('policies', OnboardingPolicyController::class);
    // steps nested routes...
});
```

---

## Middleware

Register in `backend/bootstrap/app.php`:

```php
$middleware->alias([
    'onboarding.access'   => \App\Core\Onboarding\Middleware\EnsureOnboardingAccess::class,
    'onboarding.document' => \App\Core\Onboarding\Middleware\EnsureDocumentDeadline::class,
]);
```

### `EnsureOnboardingAccess`

| Condition | Behavior |
|-----------|----------|
| Onboarding not required | Allow |
| Blocking step incomplete | Allow only `/api/v1/onboarding/*` and `/auth/logout` |
| Document deadline expired | Allow only document upload + logout routes |
| Otherwise | Allow |

Uses `OnboardingAccessService::resolve()` — no inline business logic.

### `EnsureDocumentDeadline`

At request time: if `document_upload` step has `deadline_at < now()` and status is `pending`, update step to `expired` and instance to `blocked`.

---

## Events & Jobs

### Events

| Event | Trigger | Listener |
|-------|---------|----------|
| `UserCreated` | `UserService::createUser()` | `InitializeUserOnboardingListener` |
| `UserOnboardingInitialized` | Instance created | Activity log (optional) |

### Jobs

| Job | Schedule | Purpose |
|-----|----------|---------|
| `SendOtpJob` | On demand | Send OTP via email/SMS without blocking request |
| `ExpireDocumentUploadDeadlinesJob` | Hourly/daily | Mark expired document steps and blocked instances |

---

## Frontend Integration

### Module Registration

Register `onboarding` module in `frontend/src/app/registerModules.ts`.

### User Onboarding Pages

| Route | Page |
|-------|------|
| `/onboarding/mobile-otp` | `MobileOtpPage` |
| `/onboarding/email-otp` | `EmailOtpPage` |
| `/onboarding/change-password` | `ChangePasswordPage` |
| `/onboarding/documents` | `DocumentsPage` |

Onboarding pages use a minimal layout (no sidebar), similar to auth pages.

### `OnboardingRoute` Guard

```
After login:
  onboarding_required + blocking step  → redirect to /onboarding/{current_step}
  access_restricted                    → redirect to /onboarding/documents
  document_warning                     → allow dashboard, show banner
  otherwise                            → allow dashboard
```

### `AuthContext` Extension

```typescript
interface OnboardingState {
  onboarding_required: boolean;
  current_step: string | null;
  document_warning: boolean;
  document_deadline_at: string | null;
  access_restricted: boolean;
}
```

Hydrate from login response and `/auth/me`.

### Dashboard Warning Banner

| State | Message |
|-------|---------|
| `document_warning` | "Your required documents must be uploaded within X days." |
| `access_restricted` | "Your access is restricted until required documents are uploaded." |

### Admin UI

Add to platform module (follow `ApprovalWorkflowForm` pattern):

| Route | Page |
|-------|------|
| `/admin/onboarding/policies` | `OnboardingPolicyList` |
| `/admin/onboarding/policies/create` | `OnboardingPolicyForm` |
| `/admin/onboarding/policies/:id/edit` | `OnboardingPolicyForm` |

Step builder fields: `step_key`, `step_order`, `is_required`, `is_blocking`, `allow_skip`, `deadline_days`, `config` (JSON).

---

## Implementation Phases

### Phase 1 — Foundation

- [ ] User table migration (`mobile`, `user_type`, `department_id`, `designation_id`, `password_changed_at`, `mobile_verified_at`)
- [ ] `UserService` + `UserCreated` event
- [ ] `InitializeUserOnboardingListener` (stub)

### Phase 2 — Database & Models

- [ ] 6 onboarding migrations
- [ ] Enums
- [ ] Eloquent models with relationships
- [ ] Repository interfaces + implementations
- [ ] Bind in `CoreServiceProvider`

### Phase 3 — Core Services

- [ ] `OnboardingPolicyService` (matching + CRUD)
- [ ] `OnboardingService` (initialize, step completion, advance)
- [ ] `OnboardingAccessService` (access state resolution)
- [ ] `OtpService`

### Phase 4 — Step Handlers

- [ ] `OnboardingStepHandlerInterface` + 4 handlers
- [ ] `OnboardingStepHandlerResolver`

### Phase 5 — Auth Integration

- [ ] Extend `AuthService::buildAuthPayload()` with onboarding state
- [ ] Extend `/auth/me` response

### Phase 6 — APIs & Middleware

- [ ] User onboarding controllers + form requests + resources
- [ ] Admin policy controllers
- [ ] `EnsureOnboardingAccess` + `EnsureDocumentDeadline` middleware
- [ ] Route registration

### Phase 7 — Jobs

- [ ] `SendOtpJob`
- [ ] `ExpireDocumentUploadDeadlinesJob` + scheduler

### Phase 8 — Frontend (User)

- [ ] `onboarding` module + API client
- [ ] Onboarding pages + `OnboardingRoute` guard
- [ ] `AuthContext` + dashboard warning banner

### Phase 9 — Frontend (Admin)

- [ ] Policy list + form pages
- [ ] Step builder component
- [ ] Menu seeder entry

### Recommended Build Order

Build a vertical slice first:

```
mobile OTP → password change → status API → login response
    → document upload + deadline restriction
    → admin policy CRUD UI
```

---

## Testing Checklist

- [ ] Developer user → `not_required`, no instance created
- [ ] Employee with matching policy → instance created with 4 step progress records
- [ ] Login returns correct `onboarding` flags for each state
- [ ] Blocking step incomplete → non-onboarding API returns 403
- [ ] Password change → `document_upload` deadline starts (15 days)
- [ ] Before deadline → dashboard accessible + `document_warning = true`
- [ ] After deadline → `access_restricted = true`, only upload + logout allowed
- [ ] All required documents uploaded → onboarding `completed`
- [ ] Policy edit → existing user step progress unchanged
- [ ] Admin can CRUD policies and steps
- [ ] OTP: max attempts enforced, expired OTP rejected
- [ ] OTP stored as hash, never plain text

---

## Service Provider Bindings

Add to `backend/app/Core/Providers/CoreServiceProvider.php`:

```php
// Services
$this->app->bind(OnboardingServiceInterface::class, OnboardingService::class);
$this->app->bind(OnboardingPolicyServiceInterface::class, OnboardingPolicyService::class);
$this->app->bind(OnboardingAccessServiceInterface::class, OnboardingAccessService::class);
$this->app->bind(OtpServiceInterface::class, OtpService::class);

// Repositories
$this->app->bind(OnboardingPolicyRepositoryInterface::class, OnboardingPolicyRepository::class);
$this->app->bind(OnboardingInstanceRepositoryInterface::class, OnboardingInstanceRepository::class);
$this->app->bind(OnboardingStepRepositoryInterface::class, OnboardingStepRepository::class);
$this->app->bind(UserOtpRepositoryInterface::class, UserOtpRepository::class);
$this->app->bind(UserDocumentRepositoryInterface::class, UserDocumentRepository::class);

// Step handler resolver
$this->app->singleton(OnboardingStepHandlerResolver::class);
```

Register `UserCreated` → `InitializeUserOnboardingListener` in `boot()`.

---

## Key Design Decisions

| Decision | Rationale |
|----------|-----------|
| `Core/Onboarding` not `Platform` | Core platform capability, not a business module |
| Runtime step copy | Policy edits don't break in-progress users |
| Deadline starts after password change | Per spec — not at instance creation |
| Developer bypass in `OnboardingPolicyService` | Policy logic, not controller hardcoding |
| `OnboardingAccessService` as single source | Consistent state across login, middleware, frontend |
| OTP via Job | Follows coding standard for side effects |
| `company_default` policy type | Company-wide fallback without hardcoding user types |
