# LifeData API Documentation

## Table of Contents

- [Base Information](#base-information)
- [Authentication & Security](#authentication--security)
  - [Admin Authentication](#admin-authentication)
  - [Portal Authentication](#portal-authentication)
  - [Two-Factor Authentication (2FA)](#two-factor-authentication-2fa)
- [Response & Error Format](#response--error-format)
- [Admin: Company Management](#admin-company-management)
- [Admin: Portal User Management](#admin-portal-user-management)
- [Corporate KYC (Group Life) - Company Registrations](#corporate-kyc-group-life---company-registrations)
  - [Public Company Registration (Unauthenticated)](#1-public-company-registration-unauthenticated)
  - [Admin: List Pending Registrations](#2-admin-list-pending-registrations)
  - [Admin: List All Registrations](#3-admin-list-all-registrations)
  - [Admin: View Single Registration](#4-admin-view-single-registration)
  - [Admin: Update Registration](#5-admin-update-registration)
  - [Admin: Approve Registration](#6-admin-approve-registration)
  - [Admin: Reject Registration](#7-admin-reject-registration)
  - [Admin: Bulk Company Registration via CSV](#8-admin-bulk-company-registration-via-csv)
- [Corporate KYC (Group Life) - Employee Registrations](#corporate-kyc-group-life---employee-registrations)
  - [Public: Get Companies List](#1-public-get-companies-list-unauthenticated)
  - [Public: Employee Registration](#2-public-employee-registration-unauthenticated)
  - [Public: Verify Employee Submission Token](#2b-public-verify-employee-submission-token-unauthenticated)
  - [Public: Submit Employee Details](#2c-public-submit-employee-details-unauthenticated)
  - [Admin: List Pending Employee Registrations](#3-admin-list-pending-employee-registrations)
  - [Admin: List All Employee Registrations](#4-admin-list-all-employee-registrations)
  - [Admin: View Single Employee Registration](#5-admin-view-single-employee-registration)
  - [Admin: Update Employee Registration](#6-admin-update-employee-registration)
  - [Admin: Approve Employee Registration](#7-admin-approve-employee-registration-optional)
  - [Admin: Send Submission Link (Send Invitation)](#8-admin-send-submission-link-send-invitation)
  - [Admin: Reject Employee Registration](#9-admin-reject-employee-registration)
  - [Admin: Verify Employee Registration](#10-admin-verify-employee-registration)
  - [Admin: AML/PEP Screen Employee Registration](#11-admin-amlpep-screen-employee-registration)
  - [Admin: Bulk Employee Registration via CSV](#admin-bulk-employee-registration-via-csv)
- [Full Company Profile](#full-company-profile)
- [Portal: Company Employee Management](#portal-company-employee-management)
  - [Create Employee](#1-create-employee)
  - [List Employees](#2-list-employees)
  - [View Employee](#3-view-employee)
- [Portal: Encrypted NIN Verification](#portal-encrypted-nin-verification)
- [Portal: Beneficiary Management](#portal-beneficiary-management)
- [Portal: Employee CSV Export](#portal-employee-csv-export)
- [CAC Verification](#cac-verification)
- [Public: NIN Verification (No Auth)](#public-nin-verification-no-auth)
  - [Get RSA Public Key (Public)](#get-rsa-public-key-public)
  - [Verify NIN by NIN Number (Public)](#verify-nin-by-nin-number-public)
  - [Verify NIN by Phone Number (Public)](#verify-nin-by-phone-number-public)
  - [Verify NIN by Phone Number (Admin)](#verify-nin-by-phone-number-admin)
- [Public: BVN Verification (No Auth)](#public-bvn-verification-no-auth)
  - [Verify BVN (Public)](#verify-bvn-public)
  - [Verify BVN (Admin)](#verify-bvn-admin--auth-required)
- [NIN RSA Public Key Endpoint (Portal)](#nin-rsa-public-key-endpoint-portal--auth-required)
- [NIN Verification Flow & Encryption](#nin-verification-flow--encryption)
- [Pagination](#pagination)
- [Environment Variables](#environment-variables)
- [Analytics / Dashboard Endpoints](#analytics--dashboard-endpoints)
  - [Employee Registrations by Month](#1-employee-registrations-by-month)
  - [Registration Status Summary](#2-registration-status-summary)
  - [Company Registrations by Month](#3-company-registrations-by-month)
  - [Current Week Data](#4-current-week-data)
  - [Recent Registrations](#5-recent-registrations)
- [Individual Registration API](#individual-registration-api)
  - [Submit Individual Registration (Public)](#individual-submit-registration)
  - [List Pending Individual Registrations](#individual-list-pending-registrations)
  - [List All Individual Registrations](#individual-list-all-registrations)
  - [Export Individual Registrations (CSV)](#export-individual-registrations-csv)
  - [Get Single Individual Registration](#individual-get-single-registration)
  - [Update Individual Registration](#individual-update-registration)
  - [Approve Individual Registration](#individual-approve-registration)
  - [Send Individual Invitation](#individual-send-invitation)
  - [Verify Individual Registration](#individual-verify-registration)
  - [Reject Individual Registration](#individual-reject-registration)
- [Audit Log System](#audit-log-system)
  - [Query Audit Logs](#query-audit-logs)
- [Admin: Document Management](#admin-document-management)
- [Admin: Roles & Permissions](#admin-roles--permissions)
  - [Get Authenticated Admin (me)](#1-get-authenticated-admin-me)
  - [List All Permissions](#2-list-all-permissions)
  - [List All Roles](#3-list-all-roles)
  - [Get Single Role](#4-get-single-role)
  - [Create Custom Role](#5-create-custom-role)
  - [Rename Role](#6-rename-role)
  - [Delete Role](#7-delete-role)
  - [Sync Role Permissions](#8-sync-role-permissions)
- [Admin: Internal User Management](#admin-internal-user-management)
  - [List Admin Users](#1-list-admin-users)
  - [Get Single Admin User](#2-get-single-admin-user)
  - [Create Admin User](#3-create-admin-user)
  - [Update Admin User](#4-update-admin-user)
  - [Delete Admin User](#5-delete-admin-user)
  - [Assign Roles to Admin User](#6-assign-roles-to-admin-user)

---

## Base Information

- **Base URL (Production)**:
  `https://empdata.josbiz.com/api`

- **JSON APIs – Required Headers**:

```http
Content-Type: application/json
Accept: application/json
Origin: https://your-frontend-domain
```

- **CSV Upload/Download**:
  - Uploads: `multipart/form-data`
  - Downloads: `Accept: text/csv`

- **Authentication Header (Protected Endpoints)**:

```http
Authorization: Bearer {token}
```

- **CORS**:
  - All external requests must send an `Origin` in the allowed list (`ALLOWED_ORIGINS`).

---

## Authentication & Security

### Admin Authentication

Admin auth uses the default `sanctum` guard and lives under `/api/auth/*`.

#### Admin Login

- **Method & Path**: `POST /api/auth/login`
- **Body**:

```json
{
  "email": "admin@josbiz.com",
  "password": "password123"
}
```

#### Admin Forgot Password

- **Method & Path**: `POST /api/auth/forgot-password`
- **Body**:

```json
{
  "email": "admin@josbiz.com"
}
```

The email contains a reset link pointing to your frontend, built from the request `Origin`.

#### Admin Reset Password

- **Method & Path**: `POST /api/auth/reset-password`
- **Body**:

```json
{
  "token": "reset_token_from_email",
  "email": "admin@josbiz.com",
  "password": "newpassword123",
  "password_confirmation": "newpassword123"
}
```

---

### Portal Authentication

Portal auth uses the `portal_api` guard and lives under `/api/portal/auth`.

#### 1. Set Password from Invitation

- **Method & Path**: `POST /api/portal/auth/set-password`
- **Body**:

```json
{
  "token": "invitation_token",
  "email": "user@company.com",
  "password": "SecurePassword123",
  "password_confirmation": "SecurePassword123"
}
```

**Effects**:

- Marks the invitation as used.
- Sets the portal user's password.
- If the user is an employee, sets their `employment_status` to `"active"`.

#### 2. Portal Login

- **Method & Path**: `POST /api/portal/auth/login`
- **Body**:

```json
{
  "email": "user@company.com",
  "password": "SecurePassword123"
}
```

**Success – Company Admin**:

```json
{
  "success": true,
  "message": "Login successful",
  "data": {
    "user": {
      "id": 1,
      "name": "ABC Insurance Company",
      "email": "admin@abc.com",
      "role": "company_admin",
      "email_verified_at": "2025-11-22T13:00:00Z",
      "created_at": "2025-11-22T12:00:00Z"
    },
    "token": "2|xyz123..."
  }
}
```

**Success – Employee** (includes `employee_id` and NIN status):

```json
{
  "success": true,
  "message": "Login successful",
  "data": {
    "user": {
      "id": 2,
      "name": "John Doe",
      "email": "john@abc.com",
      "role": "employee",
      "employee_id": 5,
      "email_verified_at": "2025-11-22T13:00:00Z",
      "created_at": "2025-11-22T12:00:00Z",
      "nin_verification": {
        "has_submitted_nin": true,
        "is_nin_verified": true,
        "is_validated": true,
        "nin_verified_at": "2025-11-23T10:30:00Z"
      }
    },
    "token": "3|abc456..."
  }
}
```

> **Important**: For employee-related portal endpoints, always use `employee_id` (not `user.id`).

#### 3. Get Authenticated Portal User

- **Method & Path**: `GET /api/portal/auth/me`

Returns the authenticated portal user with:

- `employee_id` and `nin_verification` block for employees.

#### 4. Portal Logout

- **Method & Path**: `POST /api/portal/auth/logout`
- **Effect**: Revokes the current portal token.

---

### Two-Factor Authentication (2FA)

LifeData API implements email-based two-factor authentication (2FA) using one-time passwords (OTP).

#### Overview

- **2FA Method**: Email-based 6-digit OTP codes
- **Code Expiration**: 10 minutes
- **Session Expiration**: 30 minutes (for partial authentication state)
- **Rate Limiting**: Maximum 5 verification attempts per code
- **Admin Users**: 2FA is **mandatory** and cannot be disabled
- **Portal Users**: 2FA is **enabled by default** but can be disabled by the user

#### Security Features

- OTP codes are hashed with bcrypt before storage
- One-time use enforcement (codes cannot be reused)
- Automatic cleanup of expired codes and sessions (runs hourly)
- IP address and user agent tracking for sessions
- Failed attempt tracking with lockout after 5 attempts

#### 3. Get Authenticated Portal User

- **Method & Path**: `GET /api/portal/auth/me`

Returns the authenticated portal user with:

- `employee_id` and `nin_verification` block for employees.

#### 4. Portal Logout

- **Method & Path**: `POST /api/portal/auth/logout`
- **Effect**: Revokes the current portal token.

---

## Response & Error Format

### Success

```json
{
  "success": true,
  "message": "Operation successful",
  "data": {
    // endpoint-specific payload
  }
}
```

### Error

```json
{
  "success": false,
  "message": "Error message",
  "errors": {
    "field_name": ["Validation or domain error messages"]
  }
}
```

Typical HTTP status codes:

- **401** – Unauthenticated
- **403** – Forbidden (origin or permissions)
- **404** – Route/resource not found
- **405** – Method not allowed
- **422** – Validation / business rule failure
- **500** – Internal server error

---

## Admin: Company Management

Admin company endpoints use `auth:sanctum` and live under `/api/admin`.

### Create Company

- **Method & Path**: `POST /api/admin/companies`

```json
{
  "company_name": "ABC Insurance Company",
  "company_email": "hr@abc.com"
}
```

Behavior:

- Creates a record in `companies`.
- Creates a portal user (role `company_admin`) linked to the company.
- Sends an invitation email to set password.

### List Companies (Paginated)

#### Admin 2FA Flow

Admin users must complete 2FA on every login. The flow is:

1. **Login** → Returns `requires_2fa: true` with `session_token`
2. **Check Email** → Receive 6-digit OTP code
3. **Verify 2FA** → Submit code and session token → Receive auth token
4. **Use Token** → Make authenticated requests
- **Method & Path**: `GET /api/admin/companies`
- **Query**:
  - `page` (default `1`)
  - `per_page` (default `15`, min `1`, max `100`)

Returns `data` and `pagination`.

### View Company

- **Method & Path**: `GET /api/admin/companies/{company}`

Returns company details, active admins, and employee counts.

### Full Company Profile

**`GET /api/portal/companies/{company}/full`** `auth:sanctum,portal_api`

Returns **everything** about a company — admins, all employees (with portal user, beneficiaries, NIN data), employees summary, and KYC submissions.

**Access rules:**
- **Admin** (`auth:sanctum`) — can request any company
- **Company admin** (`auth:portal_api`) — can only request their own company (403 otherwise)

See the [Full Company Profile response schema](#full-company-profile) in the Portal section for the full response structure.

##### 1. Admin Login (with 2FA)

When 2FA is required, the login endpoint returns a session token instead of an auth token:

- **Method & Path**: `POST /api/auth/login`
- **Body**:

```json
{
  "email": "admin@josbiz.com",
  "password": "password123"
}
```

**Success Response (2FA Required)**:

```json
{
  "success": true,
  "message": "Login successful",
  "data": {
    "requires_2fa": true,
    "session_token": "a1b2c3d4e5f6...64_character_token",
    "user": {
      "id": 1,
      "email": "admin@josbiz.com"
    },
    "message": "Verification code sent to your email."
  }
}
```

> A 6-digit OTP code is sent to the user's email address. The code is valid for 10 minutes.

##### 2. Verify 2FA Code (Admin)

- **Method & Path**: `POST /api/auth/verify-2fa`
- **Body**:

```json
{
  "email": "admin@josbiz.com",
  "code": "123456",
  "session_token": "a1b2c3d4e5f6...64_character_token"
}
```

**Success Response**:

```json
{
  "success": true,
  "message": "Two-factor authentication successful",
  "data": {
    "user": {
      "id": 1,
      "name": "Admin User",
      "email": "admin@josbiz.com",
      "email_verified_at": "2025-11-22T13:00:00Z",
      "two_factor_enabled": true
    },
    "token": "1|xyz123..."
  }
}
```

**Error Responses**:

- **Invalid Code** (422):

```json
{
  "success": false,
  "message": "Verification failed",
  "errors": {
    "code": ["Invalid or expired verification code."]
  }
}
```

- **Expired Session** (422):

```json
{
  "success": false,
  "message": "Verification failed",
  "errors": {
    "session_token": ["Session has expired. Please log in again."]
  }
}
```

- **Too Many Attempts** (422):

```json
{
  "success": false,
  "message": "Verification failed",
  "errors": {
    "code": ["Too many failed attempts. Please request a new code."]
  }
}
```

##### 3. Resend 2FA Code (Admin)

If the user doesn't receive the code or it expires:

- **Method & Path**: `POST /api/auth/resend-2fa-code`
- **Body**:

```json
{
  "email": "admin@josbiz.com",
  "session_token": "a1b2c3d4e5f6...64_character_token"
}
## Admin: Portal User Management

Portal user endpoints are under `/api/admin` and require admin token.

### List Portal Users

- **Method & Path**: `GET /api/admin/portal-users`

### Get Portal User

- **Method & Path**: `GET /api/admin/portal-users/{portalUser}`

### Resend Invitation

- **Method & Path**: `POST /api/admin/portal-users/resend-invitation`

```json
{
  "portal_user_id": 1
}
```

Behavior:

- Resends invitation email for a portal user that has not set password yet.

---

## Corporate KYC (Group Life) - Company Registrations

All KYC endpoints live under `/api/admin/grouplife/...` for admins and `/api/public/grouplife/...` for public submissions.

### Status System

The system uses a **dual-status approach**:

1. **`status`** - Approval status of the registration:
   - `pending_approval` - Awaiting admin review
   - `approved` - Admin has approved (but invitation not sent yet)
   - `rejected` - Admin has rejected

2. **`account_status`** - Account lifecycle status:
   - `pending` - Registration approved but invitation not sent
   - `invited` - Invitation sent, awaiting password setup
   - `active` - Company has set password and account is active

3. **`verification_status`** - Document verification status:
   - `not_verified` - Documents not yet verified
   - `verified` - Documents manually verified by admin

### The Flow:

1. **Public submissions** create registrations with `status: pending_approval`, `account_status: pending`, `verification_status: not_verified`.
2. **Admins review and approve/reject** registrations (approval sets `status: approved` but doesn't create company).
3. **Admins manually send invitation** which creates the company account and portal user (`account_status: invited`).
4. **Company clicks invite link** and sets password (`account_status: active`).
5. **Admins verify documents** manually (`verification_status: verified`).
6. **Admins can upload registrations in bulk** via CSV/Excel.

### 1. Public Company Registration (Unauthenticated)

- **Method & Path**: `POST /api/public/grouplife/company/register`
- **Authentication**: None (origin-restricted).
- **Content Type**: `multipart/form-data`

High-level fields:

- **Company Information**
  - `company_name`
  - `rc_number`
  - `country`, `state`, `city`
  - `phone_number`, `secondary_phone`
  - `email_address`
  - `house_address`, `previous_address`
- **Director Information**
  - `director_name`
  - `encrypted_bvn` — RSA-OAEP (SHA-1) encrypted BVN string from frontend
  - `identity_card_type` – one of:
    - `National Identity Number`
    - `National ID`
    - `International Passport`
    - `Driver's License`
    - `Voter's Card`
    - `Permanent Voter's Card`
  - **Identity Card Number**:
    - If `identity_card_type = "National Identity Number"`: `encrypted_nin` — RSA-OAEP (SHA-1) encrypted NIN string from frontend
    - Else: plain `identity_card_number`
- **Documents (files)**
  - `director_identity_cards[]`
  - `cac_document`
  - `director_passport_photograph`
- **Consent / Tracking**
  - `consent_given` (boolean)

Behavior:

- Creates a `company_registrations` row with:
  - `submission_type = "public_form"`
  - `status = "pending_approval"`
- BVN and NIN values are only stored in encrypted form.

---

### 2. Admin: List Pending Registrations

- **Method & Path**: `GET /api/admin/grouplife/company-registrations/pending`
- **Query**:
  - `per_page` (default `15`, range `1–100`)

Returns paginated pending submissions.

---

### 3. Admin: List All Registrations

- **Method & Path**: `GET /api/admin/grouplife/company-registrations`
- **Query Parameters**:
  - `status` (optional: `pending_approval`, `approved`, `rejected`)
  - `submission_type` (optional: `public_form`, `bulk_upload`)
  - `account_status` (optional: `pending`, `invited`, `active`)
  - `verification_status` (optional: `not_verified`, `verified`)
  - `from_date` (optional: `YYYY-MM-DD` — filter submitted on or after this date)
  - `to_date` (optional: `YYYY-MM-DD` — filter submitted on or before this date)
  - `per_page` (default `15`, range `1-100`)

Returns paginated submissions with summary info.

---

### Export Company Registrations (CSV)

- **Method & Path**: `GET /api/admin/grouplife/company-registrations/export/csv`
- **Auth**: `auth:sanctum`
- **Query Parameters**: Same as list (`status`, `submission_type`, `account_status`, `verification_status`, `from_date`, `to_date`)
- **Response**: Streams a CSV file download

**CSV Columns:** ID, Company Name, Email, Phone, Country, State, City, Submission Type, Status, Account Status, Verification Status, Submitted At, Approved At, Rejected At

---

### Querying Companies by Status

This endpoint supports flexible filtering using multiple query parameters. You can combine these filters to find specific groups of companies.

#### Available Filters

**1. Approval Status (`status`)**
- `pending_approval` - Awaiting admin review
- `approved` - Admin has approved the registration
- `rejected` - Admin has rejected the registration

**2. Account Status (`account_status`)**
- `pending` - Approved but invitation not sent yet
- `invited` - Invitation sent, awaiting password setup
- `active` - Company has set password and account is active

**3. Verification Status (`verification_status`)**
- `not_verified` - Documents not yet verified by admin
- `verified` - Documents manually verified by admin

**4. Submission Type (`submission_type`)**
- `public_form` - Submitted via public registration form
- `bulk_upload` - Uploaded via CSV/Excel by admin

#### Common Query Examples

**By Approval Status:**
```
GET /api/admin/grouplife/company-registrations?status=pending_approval
GET /api/admin/grouplife/company-registrations?status=approved
GET /api/admin/grouplife/company-registrations?status=rejected
```

**By Account Status:**
```
# Companies approved but invitation not sent
GET /api/admin/grouplife/company-registrations?account_status=pending

# Companies with invitation sent, awaiting password
GET /api/admin/grouplife/company-registrations?account_status=invited

# Companies with active accounts (password set)
GET /api/admin/grouplife/company-registrations?account_status=active
```

**By Verification Status:**
```
# Companies with unverified documents
GET /api/admin/grouplife/company-registrations?verification_status=not_verified

# Companies with verified documents
GET /api/admin/grouplife/company-registrations?verification_status=verified
```

**By Submission Type:**
```
# Companies registered via public form
GET /api/admin/grouplife/company-registrations?submission_type=public_form

# Companies uploaded via CSV
GET /api/admin/grouplife/company-registrations?submission_type=bulk_upload
```

#### Combined Filter Examples

**Get fully onboarded and verified companies:**
```
GET /api/admin/grouplife/company-registrations?account_status=active&verification_status=verified
```

**Get approved companies awaiting invitation:**
```
GET /api/admin/grouplife/company-registrations?status=approved&account_status=pending
```

**Get invited companies that haven't set password:**
```
GET /api/admin/grouplife/company-registrations?status=approved&account_status=invited
```

**Get active companies with unverified documents:**
```
GET /api/admin/grouplife/company-registrations?account_status=active&verification_status=not_verified
```

**Get rejected public form submissions:**
```
GET /api/admin/grouplife/company-registrations?status=rejected&submission_type=public_form
```

**Get all bulk uploaded companies:**
```
GET /api/admin/grouplife/company-registrations?submission_type=bulk_upload
```

**With pagination:**
```
GET /api/admin/grouplife/company-registrations?account_status=active&per_page=50&page=2
```

#### Response Example

**Success Response**:

```json
{
  "success": true,
  "message": "Verification code resent successfully"
```json
{
  "success": true,
  "data": [
    {
      "id": 1,
      "company_name": "ABC Insurance Company",
      "email_address": "hr@abc.com",
      "phone_number": "+2348012345678",
      "country": "Nigeria",
      "state": "Lagos",
      "city": "Ikeja",
      "submission_type": "public_form",
      "status": "approved",
      "account_status": "active",
      "verification_status": "verified",
      "submitted_at": "2025-12-16T10:30:00Z"
    }
  ],
  "pagination": {
    "current_page": 1,
    "last_page": 3,
    "per_page": 15,
    "total": 42,
    "from": 1,
    "to": 15
  }
}
```

> A new 6-digit code is generated and sent to the user's email. Previous codes are invalidated.

---

#### Portal 2FA Flow

Portal users (company admins and employees) have 2FA enabled by default but can disable it.

##### 1. Portal Login (with 2FA)

Same as admin login - when 2FA is enabled, returns `session_token`:

- **Method & Path**: `POST /api/portal/auth/login`
- **Body**:

```json
{
  "email": "user@company.com",
  "password": "SecurePassword123"
}
```

**Success Response (2FA Required)**:

```json
{
  "success": true,
  "message": "Login successful",
  "data": {
    "requires_2fa": true,
    "session_token": "x1y2z3w4...64_character_token",
    "user": {
      "id": 5,
      "email": "user@company.com"
    },
    "message": "Verification code sent to your email."
  }
}
```

**Success Response (2FA Disabled)**:

If the portal user has disabled 2FA, login proceeds normally:

```json
{
  "success": true,
  "message": "Login successful",
  "data": {
    "user": {
      "id": 5,
      "name": "John Doe",
      "email": "user@company.com",
      "role": "employee",
      "two_factor_enabled": false,
      "email_verified_at": "2025-11-22T13:00:00Z",
      "created_at": "2025-11-22T12:00:00Z"
    },
    "token": "5|abc789..."
  }
}
```

##### 2. Verify 2FA Code (Portal)

- **Method & Path**: `POST /api/portal/auth/verify-2fa`
- **Body**:

```json
{
  "email": "user@company.com",
  "code": "654321",
  "session_token": "x1y2z3w4...64_character_token"
}
```

**Success Response**:

```json
{
  "success": true,
  "message": "Two-factor authentication successful",
  "data": {
    "user": {
      "id": 5,
      "name": "John Doe",
      "email": "user@company.com",
      "role": "employee",
      "two_factor_enabled": true,
      "email_verified_at": "2025-11-22T13:00:00Z",
      "created_at": "2025-11-22T12:00:00Z"
    },
    "token": "5|xyz456..."
  }
}
```

##### 3. Resend 2FA Code (Portal)

- **Method & Path**: `POST /api/portal/auth/resend-2fa-code`
### 4. Admin: View Single Registration

- **Method & Path**: `GET /api/admin/grouplife/company-registrations/{registration}`

Response includes:

- Status & tracking:
  - `submission_type` - how the registration was submitted (`public_form` or `bulk_upload`)
  - `status` - approval status (`pending_approval`, `approved`, `rejected`)
  - `account_status` - account lifecycle status (`pending`, `invited`, `active`)
  - `verification_status` - document verification status (`not_verified`, `verified`)
  - `rejection_reason`, `submitted_at`, `approved_at`, `rejected_at`, `approved_by`, `rejected_by`
- Company info: `company_name`, `rc_number`, `country`, `state`, `city`, `phone_number`, `secondary_phone`, `email_address`, `house_address`, `previous_address`.
- Director info:
  - `director_name`
  - `identity_card_type`
  - `identity_card_number` (never exposes plaintext NIN).
- Document URLs:
  - `director_identity_cards`
  - `cac_document`
  - `director_passport_photograph`

---

### 5. Admin: Update Registration

- **Method & Path**: `PUT /api/admin/grouplife/company-registrations/{registration}`
- **Content Type**: `multipart/form-data`
- **Auth**: Admin or authorized portal user (company) – resolved by `UpdateCompanyRegistrationRequest`.

Use this to:

- Amend **pending** registrations.
- Propose updates for **approved** registrations (e.g. updated address, contact, director docs).

Behavior:

- For approved registrations:
  - The associated company remains active.
  - The registration is updated and treated as awaiting (re)approval.
- Response includes:
  - `updated_fields`: list of fields changed.
  - `updated_data`: new values.
  - `previous_data`: prior values for comparison.

---

### 6. Admin: Approve Registration (OPTIONAL)

- **Method & Path**: `POST /api/admin/grouplife/company-registrations/{registration}/approve`

**Note:** This endpoint is now optional. You can skip the approval step and go directly to sending the invitation, which will automatically approve the registration.

Behavior:

- Validates that the submission is not already approved or rejected.
- Sets `status = "approved"`, `approved_at`, `approved_by`.
- **Does NOT create company or send invitation** (account_status remains `pending`).
- Admin must explicitly send invitation via the `/send-invitation` endpoint (see below).

Response:
```json
{
  "success": true,
  "message": "Company registration approved successfully. You can now send an invitation to create their account.",
  "data": {
    "id": 1,
    "company_name": "...",
    "email_address": "...",
    "status": "approved",
    "account_status": "pending",
    "approved_at": "2025-12-16T..."
  }
}
```

---

### 7. Admin: Send Invitation

- **Method & Path**: `POST /api/admin/grouplife/company-registrations/{registration}/send-invitation`
- **Body** (optional):

```json
{
  "email": "custom@example.com"
}
```

**Request Parameters:**
- `email` (optional, string): Custom email address to send the invitation to. **IMPORTANT:** This email is ONLY used for sending the invitation notification and is NOT saved to the database. The company and portal user records will always use the company's registered email address from the registration. If not provided, the invitation will be sent to the company's registered email address.

Behavior:

- **Automatically approves the registration** if not already approved (sets `status = "approved"`, `approved_at`, `approved_by`).
- Validates that the registration is not rejected.
- Validates that invitation hasn't already been sent (`account_status` must be `pending`).
- Creates a `companies` row with the company's registered email address.
- Creates a portal user (company admin) with the company's registered email address and `password = null`.
- Sends invitation email to the specified custom email address (if provided) or to the company's registered email.
- **Note:** The invitation link will still contain the company's registered email for token validation, not the custom recipient email.
- Sets `account_status = "invited"` and links `company_id`.
- After invitation is sent, various verifications can then happen.

Response:
```json
{
  "success": true,
  "message": "Registration approved and invitation sent successfully. Company account created and invitation email sent.",
  "data": {
    "submission_id": 1,
    "company_id": 5,
    "company_name": "...",
    "company_email": "...",
    "status": "approved",
    "account_status": "invited"
  }
}
```

**Status Progression (New Workflow):**
1. Registration submitted → `status: pending_approval`, `account_status: pending`
2. Admin sends invitation → **Automatically approves** (`status: approved`) + Creates account (`account_status: invited`)
3. Company sets password → `status: approved`, `account_status: active`
4. Various verifications can then happen after account is active

**Alternative Status Progression (Using Separate Approval):**
1. Registration submitted → `status: pending_approval`, `account_status: pending`
2. Admin approves (optional) → `status: approved`, `account_status: pending`
3. Admin sends invitation → `status: approved`, `account_status: invited`
4. Company sets password → `status: approved`, `account_status: active`

---

### 8. Admin: Reject Registration

- **Method & Path**: `POST /api/admin/grouplife/company-registrations/{registration}/reject`
- **Body**:

```json
{
  "email": "user@company.com",
  "session_token": "x1y2z3w4...64_character_token"
}
```

**Success Response**:

```json
{
  "success": true,
  "message": "Verification code resent successfully"
}
```

##### 4. Enable 2FA (Portal Users Only)

**Authenticated** portal users can enable 2FA:

- **Method & Path**: `POST /api/portal/auth/enable-2fa`
- **Authentication**: Required (`Bearer {token}`)

**Success Response**:

```json
{
  "success": true,
  "message": "Two-factor authentication has been enabled",
  "data": {
    "two_factor_enabled": true
  }
}
```

**Error Response (Already Enabled)**:

```json
{
  "success": false,
  "message": "Two-factor authentication is already enabled"
}
```

> User will receive a confirmation email (TwoFactorEnabledNotification).

##### 5. Disable 2FA (Portal Users Only)

**Authenticated** portal users can disable 2FA:

- **Method & Path**: `POST /api/portal/auth/disable-2fa`
- **Authentication**: Required (`Bearer {token}`)

**Success Response**:

```json
{
  "success": true,
  "message": "Two-factor authentication has been disabled",
  "data": {
    "two_factor_enabled": false
  }
}
```

**Error Responses**:

- **Not Enabled** (422):

```json
{
  "success": false,
  "message": "Two-factor authentication is not enabled"
}
```
  "rejection_reason": "KYC documents incomplete"
}
```

Behavior:

- Sets `status = "rejected"`, `rejected_at`, `rejected_by`.
- Stores optional `rejection_reason`.
- Sends rejection notification email.

---

### 9. Admin: AML/PEP Screen Company Registration

- **Method & Path**: `POST /api/admin/grouplife/company-registrations/{registration}/aml-screen`
- **Authentication**: `auth:sanctum`
- **Permission**: `run_aml_screening`
- **Rate Limit**: 10 req/min

Screens the company director against PEP, sanctions, and adverse media lists via Dojah. Uses `director_name` and `country` from the registration (individual schema). Result is stored and linked to the registration. Re-screening is supported.

**Request Body**: None required.

**Response `200 OK`**: See [AML Screening Response](#response-200-ok-2).

---

### 10. Admin: Bulk Company Registration via CSV

- **Method & Path**: `POST /api/admin/grouplife/company-registrations/bulk-upload`
- **Content Type**: `multipart/form-data`
- **Fields**:
  - `csv_file` (required)
  - `send_invitations` (optional boolean)

Behavior:

- Parses CSV and processes each row as a registration.
- Returns per-row success/failure details and summary counts.

---

## Corporate KYC (Group Life) - Employee Registrations

All employee KYC endpoints live under `/api/admin/grouplife/employee-registrations/...` for admins and `/api/public/grouplife/...` for public submissions.

### Status System

The employee registration system uses the same **dual-status approach** as company registrations:

1. **`status`** - Approval status of the registration:
   - `pending_approval` - Awaiting admin review
   - `approved` - Admin has approved (but invitation not sent yet)
   - `rejected` - Admin has rejected

2. **`account_status`** - Account lifecycle status:
   - `pending` - Registration approved but invitation not sent
   - `invited` - Invitation sent, awaiting password setup
   - `active` - Employee has set password and account is active

3. **`verification_status`** - Document verification status:
   - `not_verified` - Documents not yet verified
   - `verified` - Documents manually verified by admin

### The Flow:

**One-go path (NIN + beneficiaries submitted at registration):**
1. Admin or company admin generates a company registration link (`POST /api/portal/companies/{id}/generate-registration-link`).
2. Company admin shares the link with employees — the link contains a `registration_token` that identifies the company.
3. Employee opens the link, fills in all KYC fields including NIN and beneficiaries, and submits.
4. Admin reviews and approves — on approval, NIN is decrypted/re-encrypted and stored on `CompanyEmployee`, beneficiary records are created, and `account_status` is set to `active` immediately.
5. Admin verifies documents (`verification_status: verified`).

**Two-step path (NIN + beneficiaries submitted later):**
1. Admin generates company registration link and shares with employees.
2. Employee fills in KYC fields only (skips NIN/beneficiaries) and submits.
3. Admin reviews and approves — `CompanyEmployee` record created, `account_status: pending`.
4. Admin sends submission link (`POST …/send-invitation`) — employee receives a one-time token link.
5. Employee submits NIN + beneficiaries via `POST /api/public/employee/submit-details`.

**Bulk upload:** Admins can upload employees via CSV/Excel (documents must be uploaded later manually).

### 1. Generate Company Registration Link

- **Method & Path**: `POST /api/portal/companies/{company}/generate-registration-link`
- **Authentication**: `auth:sanctum` (admin) or `auth:portal_api` (company admin)
- **Rate Limit**: 20 req/min

| Caller | Access |
|--------|--------|
| Admin (`auth:sanctum`) | Any company — requires `generate_company_registration_link` permission |
| Company admin (`auth:portal_api`) | Own company only — 403 if `company_id` doesn't match |

Generates (or regenerates) a 64-character registration token for the company. The token is used as a URL parameter in the employee registration form link. Regenerating invalidates any previous token.

**Response:**
```json
{
  "success": true,
  "message": "Registration link generated successfully.",
  "data": {
    "company_id": 5,
    "company_name": "ABC Corp",
    "registration_token": "abc123...64chars"
  }
}
```

> The frontend constructs the shareable URL, e.g. `https://app.example.com/register/employee?token=<registration_token>`

---

### 2. Public: Employee Registration (Unauthenticated)

- **Method & Path**: `POST /api/public/grouplife/employee/register`
- **Authentication**: None (origin-restricted).
- **Content Type**: `multipart/form-data`

High-level fields:

- **Company Token (REQUIRED)**
  - `token` — 64-char registration token from the company invite link. Resolves the company; no `company_id` needed.

- **BIO-DATA INFORMATION**
  - `title` (optional)
  - `first_name`, `last_name`, `middle_name` (optional)
  - `gender` (optional: Male, Female, Other)
  - `date_of_birth`
  - `nationality`
  - `phone_number`, `confirm_phone_number`
  - `foreign_number` (optional)
  - `email_address`, `confirm_email_address`
  - `country`, `state`, `city`
  - `chn` (optional - Clearing House Number)
  - `house_address`, `previous_address` (optional)

- **BANK INFORMATION**
  - `encrypted_bvn` — RSA-OAEP (SHA-1) encrypted BVN string from frontend
  - `bank_name` (optional)
  - `bank_account_number` (optional)
  - `tin_or_pin` (optional - Tax/Payer ID)
  - `bank_details_consent` (optional boolean)

- **IDENTITY INFORMATION**
  - `identity_card_type` - one of:
    - `National Identity Number`
    - `National ID`
    - `International Passport`
    - `Driver's License`
    - `Voter's Card`
    - `Permanent Voter's Card`
  - **Identity Card Number**:
    - If `identity_card_type = "National Identity Number"`: `encrypted_nin` — RSA-OAEP (SHA-1) encrypted NIN string from frontend
    - Else: plain `identity_card_number`

- **NIN (One-go path — optional)**
  - `encrypted_nin` — RSA-OAEP (SHA-1) encrypted NIN (base64, 300–400 chars). Optional if submitting all at once. If omitted, admin must send a submission link later.

- **Beneficiaries (One-go path — optional)**

  Submit as an array. If provided, total `percentage_allocation` must equal 100.

  | Field | Required | Description |
  |-------|----------|-------------|
  | `beneficiaries[0][first_name]` | Yes | First name |
  | `beneficiaries[0][last_name]` | Yes | Last name |
  | `beneficiaries[0][date_of_birth]` | Yes | Date (before today) |
  | `beneficiaries[0][address]` | Yes | Address |
  | `beneficiaries[0][percentage_allocation]` | Yes | 1–100, all entries must sum to 100 |

- **Documents (files)**
  - `identity_card` (required: PDF, JPG, JPEG, PNG, max 5MB)
  - `scanned_signature` (required: PDF, JPG, JPEG, PNG, max 5MB)
  - `passport_photograph` (required: JPG, JPEG, PNG, max 5MB)
  - `nin_document` (required if identity_card_type is "National Identity Number": PDF, JPG, JPEG, PNG, max 5MB)

- **Consent**
  - `consent_checkbox` (required, must be accepted)

Behavior:

- Validates the `token` against `companies.registration_token` — company must be active.
- Checks email uniqueness across companies, portal users, and employees.
- Creates an `employee_registrations` row with `submission_type = "public_form"`, `status = "pending"`.
- If `encrypted_nin` provided, stored temporarily on the registration record.
- If `beneficiaries` provided, stored as JSON on the registration record.
- BVN and NIN values are only stored in encrypted form.
- On **admin approval**: if NIN + beneficiaries were provided, they are transferred to `CompanyEmployee` automatically and `account_status` is set to `active` — no submission link needed.
- On **admin approval** without NIN/beneficiaries: `account_status` remains `pending` and admin must send a submission link.
- Sends confirmation email to employee.

**Response:**
```json
{
  "success": true,
  "message": "Employee registration received successfully. You will receive an email confirmation shortly.",
  "data": {
    "registration_id": 1,
    "first_name": "John",
    "last_name": "Doe",
    "email_address": "john.doe@example.com",
    "company_id": 5,
    "status": "pending_approval",
    "submitted_at": "2025-12-19T..."
  }
}
```

---

### 2b. Public: Verify Employee Submission Token (Unauthenticated)

Validates a one-time submission token and returns the employee's name and company name to pre-populate the submission form on the frontend.

- **Method & Path**: `GET /api/public/employee/submission-form?token=<token>`
- **Auth**: None
- **Rate Limit**: 20 req/min

**Query Parameters:**

| Parameter | Required | Description |
|-----------|----------|-------------|
| `token` | Yes | 64-char submission token from the invitation email |

**Response `200 OK`** — token valid, show form:

```json
{
  "success": true,
  "data": {
    "employee_name": "John Doe",
    "company_name": "Acme Corp"
  }
}
```

**Error responses:**

| `error_code` | HTTP | Meaning |
|---|---|---|
| `token_invalid` | 404 | Token does not exist |
| `token_expired` | 422 | Token older than 7 days — admin must resend |
| `token_used` | 422 | Employee has already submitted |

---

### 2c. Public: Submit Employee Details (Unauthenticated)

One-time endpoint for an employee to submit their NIN and beneficiary details via the tokenised link. The token is marked as used after a successful submission.

- **Method & Path**: `POST /api/public/employee/submit-details`
- **Auth**: None
- **Rate Limit**: 5 req/min
- **Content-Type**: `application/json`

**Request Body:**

```json
{
  "token": "64-char-token-from-email",
  "encrypted_nin": "<RSA-OAEP base64 encoded NIN>",
  "beneficiaries": [
    {
      "first_name": "Jane",
      "last_name": "Doe",
      "date_of_birth": "1990-05-15",
      "address": "12 Example Street, Lagos",
      "percentage_allocation": 100
    }
  ]
}
```

**Validation rules:**
- `token` — required, 64 chars
- `encrypted_nin` — required, RSA-OAEP base64 (300–400 chars, base64 chars only). See [Encrypted NIN section](#portal-encrypted-nin-verification) for encryption details.
- `beneficiaries` — required, minimum 1 item
- `beneficiaries[].percentage_allocation` — 1–100; **all beneficiaries must sum to exactly 100**

**Behavior:**
1. Validates token (not expired, not used)
2. Decrypts NIN via RSA-OAEP, validates 11-digit format
3. Verifies NIN against external API (Dojah)
4. Re-encrypts NIN with AES-256-GCM for storage
5. Saves NIN data to the employee record
6. Creates beneficiary records
7. Marks token as used (one-time use)
8. Sets `account_status = "active"` on the EmployeeRegistration

**Response `200 OK`:**

```json
{
  "success": true,
  "message": "Your details have been submitted successfully."
}
```

**Error responses:**

| Condition | HTTP | Response |
|---|---|---|
| Invalid/expired/used token | 404 / 422 | `error_code` field set |
| Invalid encrypted NIN format | 422 | `message` |
| NIN format not 11 digits | 422 | `message` |
| NIN verification failed (bad/unknown NIN) | 422 | `message` from Dojah |
| NIN verification service unavailable | 503 + `service_down: true` | `message` — retry later |
| Beneficiary allocations ≠ 100% | 422 | Validation error |

---

### 3. Admin: List Pending Employee Registrations

- **Method & Path**: `GET /api/admin/grouplife/employee-registrations/pending`
- **Query**:
  - `per_page` (default `15`, range `1–100`)

Returns paginated pending employee submissions.

- **Cannot Disable** (403) - if admin tries to disable:

```json
{
  "success": false,
  "message": "You cannot disable two-factor authentication"
}
```

> User will receive a confirmation email (TwoFactorDisabledNotification).

---

#### Admin: Reset User 2FA

Admins can reset a portal user's 2FA if they're locked out or having issues:

- **Method & Path**: `POST /api/admin/two-factor/reset-user`
- **Authentication**: Required (`Bearer {admin_token}`)
- **Body**:

```json
{
  "user_id": 5
}
```

**Success Response**:

```json
{
  "success": true,
  "message": "Two-factor authentication has been reset for this user. They will be prompted to set it up on their next login."
}
```

**Error Response**:

```json
{
  "success": false,
  "message": "Resource not found",
  "errors": {
    "user_id": ["The specified user does not exist."]
  }
}
```

**Effects**:
- Deletes all existing 2FA codes and sessions for the user
- Resets `two_factor_setup_at` to null
- User must complete 2FA setup on next login
### 4. Admin: List All Employee Registrations

- **Method & Path**: `GET /api/admin/grouplife/employee-registrations`
- **Query Parameters**:
  - `status` (optional: `pending_approval`, `approved`, `rejected`)
  - `submission_type` (optional: `public_form`, `bulk_upload`)
  - `account_status` (optional: `pending`, `invited`, `active`)
  - `verification_status` (optional: `not_verified`, `verified`)
  - `company_id` (optional: filter by company)
  - `from_date` (optional: `YYYY-MM-DD` — filter submitted on or after this date)
  - `to_date` (optional: `YYYY-MM-DD` — filter submitted on or before this date)
  - `per_page` (default `15`, range `1-100`)

Returns paginated employee submissions with summary info.

---

### Export Employee Registrations (CSV)

- **Method & Path**: `GET /api/admin/grouplife/employee-registrations/export/csv`
- **Auth**: `auth:sanctum`
- **Query Parameters**: Same as list (`status`, `submission_type`, `account_status`, `company_id`, `from_date`, `to_date`)
- **Response**: Streams a CSV file download

**CSV Columns:** ID, Company, First Name, Last Name, Email, Phone, Country, State, City, Submission Type, Status, Account Status, Verification Status, Submitted At, Approved At, Rejected At

---

### Querying Employees by Status

This endpoint supports flexible filtering using multiple query parameters. You can combine these filters to find specific groups of employees.

#### Available Filters

**1. Approval Status (`status`)**
- `pending_approval` - Awaiting admin review
- `approved` - Admin has approved the registration
- `rejected` - Admin has rejected the registration

**2. Account Status (`account_status`)**
- `pending` - Approved but invitation not sent yet
- `invited` - Invitation sent, awaiting password setup (deprecated flow)
- `active` - Employee has set password and account is active (deprecated flow)

**3. Verification Status (`verification_status`)**
- `not_verified` - Documents not yet verified by admin
- `verified` - Documents manually verified by admin

**4. Submission Type (`submission_type`)**
- `public_form` - Submitted via public registration form
- `bulk_upload` - Uploaded via CSV/Excel by admin

**5. Company Filter (`company_id`)**
- Filter employees by specific company ID

#### Common Query Examples

**By Approval Status:**
```
GET /api/admin/grouplife/employee-registrations?status=pending_approval
GET /api/admin/grouplife/employee-registrations?status=approved
GET /api/admin/grouplife/employee-registrations?status=rejected
```

**By Account Status:**
```
# Employees approved but not yet invited
GET /api/admin/grouplife/employee-registrations?account_status=pending

#### 2FA Best Practices

1. **Session Tokens**: Store the `session_token` securely in your frontend (e.g., sessionStorage, not localStorage)
2. **Code Input**: Implement a 6-digit input field with auto-submit on completion
3. **Expiration Handling**: Show countdown timer (10 minutes for code, 30 minutes for session)
4. **Resend Throttling**: Implement frontend throttling for resend requests (e.g., 30 seconds between resends)
5. **Error Handling**: Handle expired sessions gracefully by redirecting back to login
6. **User Feedback**: Show clear messages about code delivery and verification status

---

### 5. Admin: View Single Employee Registration

- **Method & Path**: `GET /api/admin/grouplife/employee-registrations/{registration}`
- **Authentication**: `auth:sanctum`
- **Permission**: `view_employee_registrations`

Returns full registration details including all bio-data, bank information, identity information, document URLs, and linked company/employee records.

**Response `200 OK`:**

```json
{
  "success": true,
  "data": {
    "id": 1,
    "company_id": 5,
    "company_employee_id": null,
    "submission_type": "public_form",
    "status": "pending_approval",
    "account_status": "pending",
    "verification_status": "not_verified",
    "rejection_reason": null,
    "title": "Mr",
    "first_name": "John",
    "last_name": "Doe",
    "middle_name": null,
    "gender": "Male",
    "date_of_birth": "1990-05-15",
    "nationality": "Nigerian",
    "phone_number": "08011111111",
    "foreign_number": null,
    "email_address": "john.doe@example.com",
    "country": "Nigeria",
    "state": "Lagos",
    "city": "Lagos",
    "chn": null,
    "house_address": "123 Street",
    "previous_address": null,
    "bank_name": "First Bank",
    "bank_account_number": "1234567890",
    "tin_or_pin": null,
    "bank_details_consent": true,
    "identity_card_type": "National Identity Number",
    "identity_card_number": null,
    "documents": {
      "identity_card": "https://storage.example.com/...",
      "scanned_signature": "https://storage.example.com/...",
      "passport_photograph": "https://storage.example.com/...",
      "nin_document": "https://storage.example.com/..."
    },
    "consent_given": true,
    "submitted_at": "2026-04-06T00:00:00+00:00",
    "approved_at": null,
    "rejected_at": null,
    "company": {
      "id": 5,
      "name": "Acme Corp",
      "email": "info@acme.com"
    },
    "employee": null,
    "approved_by": null,
    "rejected_by": null
  }
}
```

---

### 6. Admin: Update Employee Registration

- **Method & Path**: `PUT /api/admin/grouplife/employee-registrations/{registration}` or `POST` (for file uploads)
- **Authentication**: `auth:sanctum`
- **Permission**: `edit_employee_registrations`
- **Content-Type**: `application/json` or `multipart/form-data` (when uploading documents)

Updates any field on a registration. Cannot update a rejected registration.

**Updatable fields** (all optional): Same bio-data, bank, identity, and document fields as the public submission form. File fields (`identity_card`, `scanned_signature`, `passport_photograph`, `nin_document`) replace existing files when provided.

**Response `200 OK`:**

```json
{
  "success": true,
  "message": "Employee registration updated successfully.",
  "data": {
    "id": 1,
    "first_name": "John",
    "last_name": "Doe",
    "email_address": "john.doe@example.com",
    "phone_number": "08011111111",
    "status": "pending_approval"
  }
}
```

---

### 7. Admin: Approve Employee Registration (Optional)

- **Method & Path**: `POST /api/admin/grouplife/employee-registrations/{registration}/approve`
- **Authentication**: `auth:sanctum`
- **Permission**: `approve_employee_registrations`
- **Rate Limit**: 30 req/min

Creates a `CompanyEmployee` record without sending a portal invitation. Use when you want to approve first and send the invitation later.

**Approve behavior — one-go path:**
If `encrypted_nin` and `beneficiaries` were submitted at registration time:
- NIN is decrypted (RSA-OAEP) and re-encrypted (AES-256-GCM) onto the `CompanyEmployee` record.
- Beneficiary records are created.
- `account_status` is set to `active` immediately — no submission link needed.

**Approve behavior — two-step path:**
If NIN/beneficiaries were not submitted at registration:
- `CompanyEmployee` record created.
- `account_status` remains `pending` — admin must send a submission link via `send-invitation`.

**Request Body**: None required.

**Response `200 OK`:**

```json
{
  "success": true,
  "message": "Employee registration approved successfully. Account created and visible to company.",
  "data": {
    "id": 1,
    "first_name": "John",
    "last_name": "Doe",
    "email_address": "john.doe@example.com",
    "status": "approved",
    "account_status": "pending",
    "company_employee_id": 12,
    "approved_at": "2026-04-06T00:00:00+00:00"
  }
}
```

**Error responses:**

| Condition | HTTP | Message |
|-----------|------|---------|
| Already approved or verified | 422 | `This submission has already been approved.` |
| Already rejected | 422 | `This submission has been rejected and cannot be approved.` |

---

### 8. Admin: Send Submission Link (Send Invitation)

- **Method & Path**: `POST /api/admin/grouplife/employee-registrations/{registration}/send-invitation`
- **Authentication**: `auth:sanctum`
- **Permission**: `send_employee_invite`
- **Rate Limit**: 20 req/min

Approves the registration (if not already approved), creates the `CompanyEmployee` portal account, and sends an invitation email. This is the preferred one-step alternative to calling approve and then invite separately.

**Request Body** (optional):

```json
{
  "email": "alternative@example.com"
}
```

If `email` is omitted, the invitation is sent to `email_address` from the registration.

**Response `200 OK`:**

```json
{
  "success": true,
  "message": "Registration approved and invitation sent successfully. Employee account created and invitation email sent.",
  "data": {
    "submission_id": 1,
    "employee_id": 12,
    "employee_name": "John Doe",
    "employee_email": "john.doe@example.com",
    "status": "approved",
    "account_status": "invited"
  }
}
```

**Error responses:**

| Condition | HTTP | Message |
|-----------|------|---------|
| Registration is rejected | 422 | `Cannot send invitation to a rejected registration.` |
| Already invited or active | 422 | `Invitation has already been sent or account is already active.` |

---

### 9. Admin: Reject Employee Registration

- **Method & Path**: `POST /api/admin/grouplife/employee-registrations/{registration}/reject`
- **Authentication**: `auth:sanctum`
- **Permission**: `reject_employee_registrations`
- **Rate Limit**: 30 req/min

Sets `status → rejected` and sends a rejection email to the employee.

**Request Body:**

```json
{
  "rejection_reason": "Documents are incomplete and unreadable."
}
```

`rejection_reason` is optional (max 1000 characters). Cannot reject an already approved or previously rejected registration.

**Response `200 OK`:**

```json
{
  "success": true,
  "message": "Employee registration rejected successfully. Rejection email sent.",
  "data": {
    "submission_id": 1,
    "employee_name": "John Doe",
    "rejection_reason": "Documents are incomplete and unreadable.",
    "rejected_at": "2026-04-06T00:00:00+00:00"
  }
}
```

**Error responses:**

| Condition | HTTP | Message |
|-----------|------|---------|
| Already approved | 422 | `This submission has already been approved and cannot be rejected.` |
| Already rejected | 422 | `This submission has already been rejected.` |

---

### 10. Admin: Verify Employee Registration

- **Method & Path**: `POST /api/admin/grouplife/employee-registrations/{registration}/verify`
- **Authentication**: `auth:sanctum`
- **Permission**: `verify_employee_registrations`
- **Rate Limit**: 30 req/min

Marks the registration as document-verified (`verification_status → verified`). Only approved registrations can be verified.

**Request Body**: None required.

**Response `200 OK`:**

```json
{
  "success": true,
  "message": "Employee registration verified successfully.",
  "data": {
    "id": 1,
    "first_name": "John",
    "last_name": "Doe",
    "email_address": "john.doe@example.com",
    "status": "approved"
  }
}
```

**Error responses:**

| Condition | HTTP | Message |
|-----------|------|---------|
| Already verified | 422 | `This registration has already been verified.` |
| Not yet approved | 422 | `Only approved registrations can be verified.` |

---

### 11. Admin: AML/PEP Screen Employee Registration

- **Method & Path**: `POST /api/admin/grouplife/employee-registrations/{registration}/aml-screen`
- **Authentication**: `auth:sanctum`
- **Permission**: `run_aml_screening`
- **Rate Limit**: 10 req/min

Screens the employee against PEP, sanctions, and adverse media lists via Dojah. Uses `first_name + last_name`, `date_of_birth`, `nationality`, and `gender` from the registration. Result is stored and linked to the registration. Re-screening is supported.

**Request Body**: None required.

**Response `200 OK`**: See [AML Screening Response](#response-200-ok-2).

---

## Full Company Profile

**`GET /api/portal/companies/{company}/full`** `auth:sanctum,portal_api`

Returns company details, admins, employee stats (counts), and the 10 most recently added employees.

**Access rules:**
- **Admin** (`auth:sanctum`) — any company
- **Company admin** (`auth:portal_api`) — own company only (403 if accessing another)

#### Response `200 OK`

```json
{
  "success": true,
  "data": {
    "id": 1,
    "name": "Acme Ltd",
    "email": "info@acme.com",
    "phone": "08011111111",
    "rc_number": "RC123456",
    "address": "12 Marina St",
    "city": "Lagos",
    "state": "Lagos",
    "country": "Nigeria",
    "insurance_type": "Life",
    "registration_date": "2020-01-01",
    "license_number": "NAICOM/001",
    "status": "active",
    "created_at": "2026-01-01T00:00:00+00:00",
    "updated_at": "2026-01-01T00:00:00+00:00",
    "admins": [
      {
        "id": 1,
        "name": "Acme Admin",
        "email": "admin@acme.com",
        "password_set": true,
        "invitation_sent_at": "2026-01-01T00:00:00+00:00",
        "two_factor_enabled": true,
        "created_at": "2026-01-01T00:00:00+00:00"
      }
    ],
    "employee_stats": {
      "total": 50,
      "active": 40,
      "inactive": 8,
      "terminated": 2,
      "nin_verified": 30,
      "no_portal_access": 5,
      "invitation_sent": 20,
      "password_set": 25
    },
    "recent_employees": [
      {
        "id": 12,
        "name": "Jane Smith",
        "email": "jane@acme.com",
        "position": "Manager",
        "joined_at": "2026-03-01"
      }
    ]
  }
}
```

> **Notes:**
> - `employee_stats` contains counts only — no individual employee data
> - `no_portal_access`: employees with no portal account created yet
> - `invitation_sent`: employees with a portal account but password not yet set
> - `password_set`: employees who have completed their portal registration
> - `recent_employees`: last 10 employees added, ordered newest first
> - `joined_at`: uses `hire_date` if set, otherwise falls back to `created_at`

---

## Portal: Company Employee Management

Employee endpoints are under `/api/portal/employees` and use the combined middleware `auth:sanctum,portal_api`.

### 1. Create Employee

- **Method & Path**: `POST /api/portal/employees`

**System Admin example**:

```json
{
  "company_id": 1,
  "first_name": "John",
  "last_name": "Doe",
  "email": "john.doe@company.com",
  "date_of_birth": "1990-05-15"
}
```

**Company Admin example** (no `company_id`):

```json
{
  "first_name": "John",
  "last_name": "Doe",
  "email": "john.doe@company.com",
  "date_of_birth": "1990-05-15"
}
```

Employee is created with `employment_status = "inactive"` and becomes `active` after setting password.

---

### 2. List Employees

- **Method & Path**: `GET /api/portal/employees`
- **Query**:
  - `page`, `per_page`
  - `search` (name, email, employee number)
  - `employment_status` (`active` / `inactive`)
  - `nin_verified` (`true` / `false`)

System admins see all; company admins see only their own employees.

---

### 3. View Employee

- **Method & Path**: `GET /api/portal/employees/{companyEmployee}`

Returns full employee data, including NIN verification and optional `nin_data` when verified.

---

## Portal: Encrypted NIN Verification

- **Method & Path**: `POST /api/portal/employees/{companyEmployee}/submit-nin`
- **Auth**: System admin or company admin with access to that employee.
- **Body**:

```json
{
  "encrypted_nin": "<base64-encoded RSA-OAEP ciphertext>"
}
```

The frontend obtains the server's RSA public key from `GET /api/nin/public-key`, then encrypts the NIN using RSA-OAEP (WebCrypto API). Only the server's private key can decrypt it.

Flow:

1. RSA-OAEP decrypt `encrypted_nin` using `NIN_PRIVATE_KEY`.
2. Validate format (exactly 11 digits).
3. AES-256-GCM re-encrypt the plaintext NIN for database storage.
4. Call Dojah NIN verification service (`GET /api/v1/kyc/nin`).
5. Store:
   - `nin_encrypted` (AES-encrypted NIN for DB — internal only, never returned in responses)
   - NIN verification metadata (name, birth info, residence info, photo, report_id).
6. Set:
   - `is_nin_verified = true`
   - `nin_verified_at`
   - `is_validated` – true only if:
     - first name matches,
     - surname matches,
     - date of birth matches.

Re-verification:

- Allowed. If called again, the latest successful verification overwrites previous NIN-related data.

**Error responses:**

| Condition | HTTP | Response |
|---|---|---|
| Invalid encrypted NIN format | 422 | `message` |
| NIN format not 11 digits | 422 | `message` |
| NIN not found or verification failed | 422 | `message` from Dojah |
| NIN service unavailable / down | 503 + `service_down: true` | `message` — retry later |

---

## Portal: Beneficiary Management

Beneficiaries are managed by employees for themselves.

### Save / Update Beneficiaries

- **Method & Path**: `POST /api/portal/employees/{companyEmployee}/beneficiaries`
- **Content Type**: `multipart/form-data`
- **Who can call**:
  - The employee themselves (`portal_users.role = "employee"` with matching `employee_id`).

Payload (shape):

```json
{
  "beneficiaries": [
    {
      "id": 1,
      "first_name": "Jane",
      "last_name": "Doe",
      "address": "123 Street",
      "date_of_birth": "1995-01-01",
      "percentage_allocation": 60,
      "utility_bill": "<file>",
      "identification_document": "<file>"
    },
    {
      "first_name": "Mark",
      "last_name": "Doe",
      "address": "456 Road",
      "date_of_birth": "1998-02-02",
      "percentage_allocation": 40
    }
  ]
}
```

Rules:

- Up to **5** beneficiaries.
- Total `percentage_allocation` across all beneficiaries must equal **100**.
- Existing beneficiaries not present in the list are **deleted** (sync pattern).
- For updates, `id` identifies which beneficiary to update.

Response includes:

- `count`
- `total_allocation`
- Array of beneficiaries with:
  - `utility_bill_url`
  - `identification_url`

---

### Get Beneficiaries

- **Method & Path**: `GET /api/portal/employees/{companyEmployee}/beneficiaries`

Access:

- Employee:
  - Only their own beneficiaries.
- Company Admin:
  - Beneficiaries of employees in their company.
- System Admin:
  - All.

Returns:

- Employee summary.
- `count`, `total_allocation`.
- List of beneficiaries with document URLs.

---

## Portal: Employee CSV Export

- **Method & Path**: `GET /api/portal/employees/export/csv`
- **Auth**: System admin or company admin.
- **Headers**:

```http
Accept: text/csv
Authorization: Bearer {token}
Origin: https://your-frontend-domain
```

- **Query Parameters**:
  - `company_id`:
    - Required for **system admins**.
    - Ignored for **company admins** (company inferred from auth).
  - `search`
  - `employment_status`
  - `nin_verified`

Behavior:

- System Admin:
  - Must provide `company_id`.
- Company Admin:
  - Exports their company's employees, no `company_id` needed.
- CSV includes:
  - Core employee fields.
  - NIN status (`has_submitted_nin`, `is_nin_verified`, `is_validated`, `nin_verified_at`).
  - Up to 5 beneficiaries per employee (name, address, DOB, allocation %).
- Response headers:

```http
Content-Type: text/csv; charset=UTF-8
Content-Disposition: attachment; filename="employees_{CompanyName}_{YYYY-MM-DD_HHMMSS}.csv"
```

---

## CAC Verification

Verify a company against the CAC registry via Dojah's Basic lookup. Available on three routes with different auth levels — same logic, nothing stored.

| Route | Auth |
|---|---|
| `GET /api/public/cac/verify` | None |
| `GET /api/portal/cac/verify` | `auth:portal_api` or `auth:sanctum` |
| `GET /api/admin/cac/verify` | `auth:sanctum` |

- **Rate limit**: 10 requests/minute (calls external paid Dojah API)
- **Method**: GET with query parameters

**Query Parameters:**

| Param | Required | Description |
|---|---|---|
| `rc_number` | Yes | CAC registration number — digits only, no "RC" prefix (e.g. `1261103`, not `RC1261103`) |
| `company_type` | Yes | One of: `COMPANY`, `BUSINESS_NAME`, `INCORPORATED_TRUSTEES`, `LIMITED_PARTNERSHIP`, `LIMITED_LIABILITY_PARTNERSHIP` |

**Success Response (200):**

```json
{
  "success": true,
  "message": "Company verified successfully.",
  "data": {
    "company_name": "Acme Nigeria Limited",
    "type_of_company": "COMPANY",
    "rc_number": "1261103",
    "business_number": null,
    "status": "Active",
    "email": "info@acme.ng",
    "address": "1 Example Street, Lagos",
    "state": "Lagos",
    "city": "Ikeja",
    "lga": "Ikeja"
  }
}
```

**Error Responses:**

| Condition | HTTP | Message |
|---|---|---|
| Company not found or wrong `company_type` | 422 | `"Company not found. Please check the RC number and company type."` |
| Invalid RC number / bad request | 422 | `"Company not found. Please check the RC number and company type."` |
| CAC service unavailable / Dojah down | 503 + `service_down: true` | `"CAC verification service is currently unavailable. Please try again later."` |

> **Note on `company_type`**: A 422 "Company not found" may mean the RC number exists but the wrong type was sent (e.g., sent `COMPANY` but it's actually a `BUSINESS_NAME`). The frontend should allow the user to change the company type and retry.

---

## Public: NIN Verification (No Auth)

For public registration flows where users need to verify their NIN before or during form submission — no login required, nothing stored.

### Get RSA Public Key (Public)

- **Method & Path**: `GET /api/public/nin/public-key`
- **Auth**: None
- **Rate limit**: 30 requests/minute

**Response:**

```json
{
  "success": true,
  "public_key": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
}
```

### Verify NIN (Public)

- **Method & Path**: `POST /api/public/nin/verify`
- **Auth**: None
- **Rate limit**: 10 requests/minute (calls external paid API)
- **Content-Type**: `application/json`

**Request Body:**

```json
{
  "encrypted_nin": "<base64-encoded RSA-OAEP ciphertext>"
}
```

Encrypt the NIN using RSA-OAEP (SHA-1) with the public key from `GET /api/public/nin/public-key` via the WebCrypto API.

**Success Response (200):**

```json
{
  "success": true,
  "message": "NIN verified successfully.",
  "data": {
    "first_name": "John",
    "middle_name": "Doe",
    "last_name": "Smith",
    "phone_number": "080xxxxxxxx",
    "gender": "m",
    "date_of_birth": "1990-01-01",
    "birthcountry": "NGA",
    "birthstate": "Lagos",
    "birthlga": "Ikeja",
    "residence_state": "Lagos",
    "residence_town": "Ikeja",
    "residence_lga": "Ikeja",
    "residence_address": "1 Example Street",
    "photo": "<base64-encoded passport photo>"
  }
}
```

**Error Response (422) — NIN not found or verification failed:**

```json
{
  "success": false,
  "message": "NIN verification failed."
}
```

**Error Response (503) — NIN service is down:**

```json
{
  "success": false,
  "service_down": true,
  "message": "NIN verification service is currently unavailable. Please try again later."
}
```

The `503` is returned when Dojah cannot be reached, the service is temporarily unavailable, or the upstream API returns an error (connection timeout, rate limit, insufficient wallet balance, etc.). The `service_down: true` flag is always present on 503 responses — the frontend can check this flag to distinguish a service outage from a failed verification and surface a "try again later" message.

Nothing is stored. The response is passed directly back to the frontend for display or pre-filling form fields.

### Verify NIN by Phone Number (Public)

- **Method & Path**: `GET /api/public/nin/verify-by-phone`
- **Auth**: None
- **Rate limit**: 10 requests/minute (calls external paid API)

**Query Parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `phone_number` | string | Yes | Phone number to look up. Accepts local format (`08012345678`), international without plus (`2348012345678`), or with plus (`+2348012345678`). 10–14 digits. |

**Success Response (200):**

```json
{
  "success": true,
  "message": "NIN record retrieved successfully.",
  "data": {
    "nin": "12345678901",
    "first_name": "John",
    "middle_name": "Doe",
    "last_name": "Smith",
    "phone_number": "08012345678",
    "date_of_birth": "1990-01-01",
    "gender": "m",
    "photo": "<base64-encoded passport photo>"
  }
}
```

**Error Response (422) — no record found or invalid phone number:**

```json
{
  "success": false,
  "message": "No NIN record found for this phone number."
}
```

**Error Response (503) — NIN service is down:**

```json
{
  "success": false,
  "service_down": true,
  "message": "NIN verification service is currently unavailable. Please try again later."
}
```

Nothing is stored. The `service_down: true` flag is always present on 503 responses.

### Verify NIN by Phone Number (Admin)

- **Method & Path**: `GET /api/admin/nin/verify-by-phone`
- **Auth**: Required (`auth:sanctum`)
- **Rate limit**: 10 requests/minute (calls external paid API)

**Query Parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `phone_number` | string | Yes | Phone number to look up. Same formats as public endpoint. |

Response structure is identical to the public endpoint above. Nothing is stored.

---

## Public: BVN Verification (No Auth)

For flows where users need to verify their BVN — no login required, nothing stored. The BVN is encrypted on the frontend using the same RSA public key as NIN (`GET /api/public/nin/public-key`).

### Verify BVN (Public)

- **Method & Path**: `POST /api/public/bvn/verify`
- **Auth**: None
- **Rate limit**: 10 requests/minute (calls external paid API)
- **Content-Type**: `application/json`

**Request Body:**

```json
{
  "encrypted_bvn": "<base64-encoded RSA-OAEP ciphertext>"
}
```

Encrypt the BVN using RSA-OAEP (SHA-1) with the public key from `GET /api/public/nin/public-key` via the WebCrypto API — same key and encryption method as NIN.

**Success Response (200):**

```json
{
  "success": true,
  "message": "BVN verified successfully.",
  "data": {
    "bvn": "22222222222",
    "first_name": "John",
    "middle_name": "Doe",
    "last_name": "Smith",
    "gender": "Male",
    "date_of_birth": "1990-01-01",
    "phone_number1": "080xxxxxxxx",
    "phone_number2": null,
    "image": "<base64-encoded photo>"
  }
}
```

**Error Response (422) — BVN not found or verification failed:**

```json
{
  "success": false,
  "message": "BVN verification failed."
}
```

**Error Response (503) — BVN service is down:**

```json
{
  "success": false,
  "service_down": true,
  "message": "BVN verification service is currently unavailable. Please try again later."
}
```

The `service_down: true` flag is present on all 503 responses — check this flag to distinguish a service outage from a failed verification.

Nothing is stored. The response is passed directly back to the frontend.

### Verify BVN (Admin — Auth Required)

- **Method & Path**: `POST /api/admin/bvn/verify`
- **Auth**: Required (`auth:sanctum`)
- **Rate limit**: 10 requests/minute

Request body and response structure are identical to the public endpoint above. Nothing is stored.

---

## NIN RSA Public Key Endpoint (Portal — Auth Required)

- **Method & Path**: `GET /api/portal/nin/public-key`
- **Auth**: Required (`auth:sanctum` or `auth:portal_api`)
- **Rate limit**: 30 requests/minute.

**Response:**

```json
{
  "success": true,
  "public_key": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
}
```

The frontend uses this public key with the WebCrypto API (`RSA-OAEP`, `SHA-1`) to encrypt the NIN before sending it to `POST /api/portal/employees/{companyEmployee}/submit-nin`.

---

## NIN Verification Flow & Encryption

### Overview

1. **Client fetches RSA public key**:
   - `GET /api/nin/public-key` returns the server's RSA-2048 public key.
   - The private key stays on the server only — never shared with the frontend.

2. **Client-side encryption**:
   - NIN is encrypted using RSA-OAEP (SHA-1) with the server public key via WebCrypto.
   - Client sends `{ "encrypted_nin": "<base64>" }`.

3. **Backend decryption & validation**:
   - Server decrypts `encrypted_nin` using `NIN_PRIVATE_KEY` (RSA-OAEP).
   - Validates NIN is exactly 11 digits.
   - Re-encrypts with AES-256-GCM (`NIN_ENCRYPTION_KEY`) for database storage.

4. **External verification**:
   - Sends decrypted NIN to Dojah (`GET /api/v1/kyc/nin?nin=...`).
   - Receives official NIMC record: names, date of birth, gender, phone, address, photo, etc.

5. **Storage**:
   - Only stores:
     - `nin_encrypted` (AES-256-GCM — internal only, never returned in API responses).
     - Verification metadata on `company_employees`.
   - Plain NIN is never persisted.

6. **Flags**:
   - `has_submitted_nin` – true once NIN has been submitted and stored.
   - `is_nin_verified` – result of verification.
   - `is_validated` – whether NIN data matches employee record (names + DOB).

### Sandbox vs Live (Dojah)

Controlled entirely by `.env` — no code changes needed between environments.

| `DOJAH_ENV` | Base URL | Use |
|---|---|---|
| `sandbox` | `https://sandbox.dojah.io` | Development / testing. Use test NIN `70123456789`. |
| `live` | `https://api.dojah.io` | Production. Real NINs, funded Dojah wallet required. |

Required env vars (from Dojah dashboard → Developers → Configuration → My Apps):
- `DOJAH_ENV` — `sandbox` or `live`
- `DOJAH_APP_ID` — App ID for that environment
- `DOJAH_SECRET` — Secret key for that environment (server-side only, never expose to client)
- `NIN_PRIVATE_KEY` — Base64-encoded RSA-2048 private key. Generate with `php artisan nin:generate-keypair`.
- `NIN_ENCRYPTION_KEY` — 64 hex chars (AES-256-GCM key for database-side NIN storage).

### Security Highlights

- Asymmetric encryption: only the server can decrypt NIN — the frontend never holds the decryption key.
- RSA-OAEP in transit, AES-256-GCM at rest.
- No plaintext NIN/BVN stored in the database.
- Keys held only in environment variables, never committed.

---

## Pagination

Paginated endpoints:

- `GET /api/admin/companies`
- `GET /api/admin/grouplife/company-registrations/pending`
- `GET /api/admin/grouplife/company-registrations`
- `GET /api/portal/employees`

### Query Parameters

| Parameter  | Type    | Default | Range  | Description      |
|-----------|---------|---------|--------|------------------|
| `page`    | integer | 1       | 1+     | Page number      |
| `per_page`| integer | 15      | 1–100  | Items per page   |

### Response Shape

```json
{
  "success": true,
  "data": [ /* items */ ],
  "pagination": {
    "current_page": 1,
    "last_page": 5,
    "per_page": 15,
    "total": 67,
    "from": 1,
    "to": 15
  }
}
```

---

## Environment Variables

### Core

| Variable          | Description                             |
|-------------------|-----------------------------------------|
| `APP_KEY`         | Laravel application key                 |
| `DB_CONNECTION`   | Database driver                         |
| `DB_HOST`         | Database host                           |
| `DB_PORT`         | Database port                           |
| `DB_DATABASE`     | Database name                           |
| `DB_USERNAME`     | Database user                           |
| `DB_PASSWORD`     | Database password                       |
| `ALLOWED_ORIGINS` | Comma-separated allowed CORS origins    |

### NIN & KYC

| Variable            | Description                                                                           |
|---------------------|---------------------------------------------------------------------------------------|
| `NIN_PRIVATE_KEY`   | Base64-encoded RSA-2048 private key. Generate: `php artisan nin:generate-keypair`     |
| `NIN_ENCRYPTION_KEY`| 64 hex chars (AES-256-GCM key for database-side NIN storage)                         |
| `DOJAH_ENV`         | `sandbox` or `live` — selects Dojah API environment                                  |
| `DOJAH_APP_ID`      | Dojah App ID for the selected environment                                             |
| `DOJAH_SECRET`      | Dojah Secret key (server-side only)                                                   |
| `NIN_API_TIMEOUT`   | NIN API timeout (seconds)                                                             |

---

## AML / PEP Screening

AML (Anti-Money Laundering) and PEP (Politically Exposed Person) screening is powered by the Dojah AML v2 API. A single call returns PEP, Sanctions, and Adverse Media results in one response — the frontend filters by `source_type` as needed.

There are two tiers of AML endpoints:

**1. Standalone (stateless) — public & admin**
Pass any name/details directly. Nothing is stored. Used for ad-hoc checks.

**2. Registration-linked — admin only**
Linked to a specific registration record. Result is stored in `aml_screening_results` and can be retrieved later. Multiple screenings can be run on the same registration (re-screening is supported).

---

### 1. Standalone AML Screen (Public — No Auth)

- **Method & Path**: `POST /api/public/aml/screen`
- **Authentication**: None (origin-restricted)
- **Rate Limit**: 10 req/min

### 2. Standalone AML Screen (Admin — Auth Required)

- **Method & Path**: `POST /api/admin/aml/screen`
- **Authentication**: `auth:sanctum`
- **Rate Limit**: 10 req/min

Both endpoints accept the same request body and return the same response.

**Request Body:**

| Field | Required | Description |
|-------|----------|-------------|
| `names` | **Yes** | Full name to screen |
| `schema` | No | `individual` (default) or `organization` |
| `date_of_birth` | No | `YYYY-MM-DD` — individual only |
| `nationality` | No | Country code e.g. `NG` |
| `gender` | No | `male` or `female` |
| `registration_number` | No | RC number — organization only |
| `country` | No | Country code — organization only |

---

### 3. Registration-Linked AML Screen (Admin Only)

Triggers a screening against a specific registration record. Result is stored and linked to the registration.

**Permission required**: `run_aml_screening`

| Method | Path | Screens |
|--------|------|---------|
| `POST` | `/api/admin/grouplife/company-registrations/{id}/aml-screen` | Company director (individual schema) |
| `POST` | `/api/admin/grouplife/employee-registrations/{id}/aml-screen` | Employee |
| `POST` | `/api/admin/individual/registrations/{id}/aml-screen` | Individual |

- **Authentication**: `auth:sanctum`
- **Rate Limit**: 10 req/min
- **Request Body**: None required

### Response `200 OK`

```json
{
  "success": true,
  "message": "AML screening completed.",
  "data": {
    "id": 1,
    "risk_level": "Medium",
    "match_status": "Potential Match",
    "total_results": 3,
    "total_articles": 1,
    "results": [
      {
        "name": ["John Doe", "Johnny Doe"],
        "entity_type": "individual",
        "source_type": "PEP",
        "match": true,
        "positions": ["Senator", "Minister of Finance"],
        "country": ["ng"],
        "date_of_birth": ["1965-04-12"],
        "urls": ["https://source.example.com"]
      },
      {
        "name": ["John Doe"],
        "entity_type": "individual",
        "source_type": "SANCTIONS",
        "match": true,
        "positions": [],
        "country": ["ng"],
        "date_of_birth": [],
        "urls": []
      }
    ],
    "screened_at": "2026-04-06T12:00:00.000000Z"
  }
}
```

### `results[].source_type` values

| Value | Meaning |
|-------|---------|
| `PEP` | Politically Exposed Person hit |
| `SANCTIONS` | Matched on a sanctions list (OFAC, EU, UN, etc.) |
| `ADVERSE_MEDIA` | Matched in adverse news/media sources |

Frontend filters `results` by `source_type` to show PEP, Sanctions, or Adverse Media separately.

### `risk_level` values

`Low` / `Medium` / `High`

### `match_status` values

`No Match` / `Potential Match` / `Confirmed Match`

### Service unavailable (503)

```json
{
  "success": false,
  "service_down": true,
  "message": "AML screening service is currently unavailable."
}
```

---

## Analytics / Dashboard Endpoints

All analytics endpoints require admin authentication (`Authorization: Bearer {token}`).

**Base URL:** `/api/admin/analytics`

---

### 1. Monthly Employee Registrations

Returns employee registration counts grouped by month for the current year. All 12 months are always returned; months with no registrations have `count: 0`.

**Endpoint:** `GET /api/admin/analytics/employee-registrations/monthly`

**Headers:**
```
Authorization: Bearer {admin_token}
```

**Response:**
```json
{
  "success": true,
  "data": {
    "year": 2026,
    "data": [
      { "month": "Jan", "count": 12 },
      { "month": "Feb", "count": 34 },
      { "month": "Mar", "count": 7 },
      { "month": "Apr", "count": 0 },
      { "month": "May", "count": 0 },
      { "month": "Jun", "count": 0 },
      { "month": "Jul", "count": 0 },
      { "month": "Aug", "count": 0 },
      { "month": "Sep", "count": 0 },
      { "month": "Oct", "count": 0 },
      { "month": "Nov", "count": 0 },
      { "month": "Dec", "count": 0 }
    ]
  }
}
```

---

### 2. Registration Status Summary

Returns combined registration counts broken down by each status dimension for both company and employee registrations.

**Endpoint:** `GET /api/admin/analytics/registrations/status-summary`

**Headers:**
```
Authorization: Bearer {admin_token}
```

**Response:**
```json
{
  "success": true,
  "data": {
    "company_registrations": {
      "total": 120,
      "by_status": {
        "pending_approval": 50,
        "approved": 60,
        "rejected": 10
      },
      "by_account_status": {
        "pending": 40,
        "invited": 30,
        "active": 50
      },
      "by_verification_status": {
        "not_verified": 70,
        "verified": 50
      }
    },
    "employee_registrations": {
      "total": 340,
      "by_status": {
        "pending_approval": 120,
        "approved": 180,
        "rejected": 40
      },
      "by_account_status": {
        "pending": 100,
        "invited": 80,
        "active": 160
      },
      "by_verification_status": {
        "not_verified": 200,
        "verified": 140
      }
    },
    "combined_total": 460
  }
}
```

---

### 3. Monthly Company Registrations with Status Breakdown

Returns company registration counts grouped by month for the current year, with a full breakdown across all three status dimensions per month. All 12 months are always returned with zero-filled values for empty months.

**Endpoint:** `GET /api/admin/analytics/company-registrations/monthly`

**Headers:**
```
Authorization: Bearer {admin_token}
```

**Response:**
```json
{
  "success": true,
  "data": {
    "year": 2026,
    "data": [
      {
        "month": "Jan",
        "total": 17,
        "by_status": {
          "pending_approval": 5,
          "approved": 10,
          "rejected": 2
        },
        "by_account_status": {
          "pending": 7,
          "invited": 4,
          "active": 6
        },
        "by_verification_status": {
          "not_verified": 12,
          "verified": 5
        }
      },
      {
        "month": "Feb",
        "total": 0,
        "by_status": { "pending_approval": 0, "approved": 0, "rejected": 0 },
        "by_account_status": { "pending": 0, "invited": 0, "active": 0 },
        "by_verification_status": { "not_verified": 0, "verified": 0 }
      }
    ]
  }
}
```

---

### 4. Current Week Data

Returns registration counts for each day of the current week (Monday–Sunday), broken down by status for both companies and employees. All 7 days are always returned; future days in the week have zero counts.

**Endpoint:** `GET /api/admin/analytics/current-week`

**Headers:**
```
Authorization: Bearer {admin_token}
```

**Response:**
```json
{
  "success": true,
  "data": {
    "week_start": "2026-03-09",
    "week_end": "2026-03-15",
    "data": [
      {
        "day": "Mon",
        "date": "2026-03-09",
        "companies": {
          "total": 3,
          "by_status": { "pending_approval": 2, "approved": 1, "rejected": 0 },
          "by_account_status": { "pending": 2, "invited": 1, "active": 0 },
          "by_verification_status": { "not_verified": 3, "verified": 0 }
        },
        "employees": {
          "total": 8,
          "by_status": { "pending_approval": 5, "approved": 3, "rejected": 0 },
          "by_account_status": { "pending": 4, "invited": 2, "active": 2 },
          "by_verification_status": { "not_verified": 6, "verified": 2 }
        }
      },
      {
        "day": "Tue",
        "date": "2026-03-10",
        "companies": {
          "total": 0,
          "by_status": { "pending_approval": 0, "approved": 0, "rejected": 0 },
          "by_account_status": { "pending": 0, "invited": 0, "active": 0 },
          "by_verification_status": { "not_verified": 0, "verified": 0 }
        },
        "employees": {
          "total": 0,
          "by_status": { "pending_approval": 0, "approved": 0, "rejected": 0 },
          "by_account_status": { "pending": 0, "invited": 0, "active": 0 },
          "by_verification_status": { "not_verified": 0, "verified": 0 }
        }
      }
    ]
  }
}
```

---

### 5. Recent Registrations

Returns all company and employee registrations submitted within the last N hours, merged and ordered by newest first. Useful for the "Recent Activity" / "Recent Registrations" section on the dashboard.

**Endpoint:** `GET /api/admin/analytics/recent-registrations`

**Headers:**
```
Authorization: Bearer {admin_token}
```

**Query Parameters:**

| Parameter | Type    | Default | Range   | Description                   |
|-----------|---------|---------|---------|-------------------------------|
| `hours`   | integer | 7       | 1–168   | Look-back window in hours     |

**Example:** `GET /api/admin/analytics/recent-registrations?hours=24`

**Response:**
```json
{
  "success": true,
  "data": {
    "hours": 7,
    "since": "2026-03-13T05:00:00.000000Z",
    "total": 4,
    "data": [
      {
        "id": 42,
        "type": "company",
        "name": "Acme Group Ltd",
        "email": "info@acme.com",
        "status": "pending_approval",
        "account_status": "pending",
        "verification_status": "not_verified",
        "submission_type": "public_form",
        "submitted_at": "2026-03-13T11:45:00.000000Z"
      },
      {
        "id": 87,
        "type": "employee",
        "name": "John Doe",
        "email": "john.doe@acme.com",
        "status": "pending_approval",
        "account_status": "pending",
        "verification_status": "not_verified",
        "submission_type": "public_form",
        "submitted_at": "2026-03-13T10:22:00.000000Z"
      }
    ]
  }
}
```

**Notes:**
- `type` is either `"company"` or `"employee"`
- Results from both registration types are merged and sorted by `submitted_at` descending
- Maximum look-back window is 168 hours (7 days)
- When `hours` is outside the valid range, it is clamped (below 1 → 1, above 168 → 168)
}
```

---

## Individual Registration API

Allows individuals (not tied to a company) to submit KYC registration forms. Admins review, approve, reject, verify, and send portal invitations to create `individual`-role portal accounts.

### Status System

Individual registrations use the same tri-status system as employee registrations:

| Field | Values | Description |
|---|---|---|
| `status` | `pending`, `approved`, `verified`, `rejected` | Overall registration status |
| `account_status` | `pending`, `invited`, `active` | Portal account lifecycle |
| `verification_status` | `not_verified`, `verified` | Identity verification state |

---

### Public Endpoint

#### Individual: Submit Registration

```
POST /api/public/individual/register
```

No authentication required. Accepts `multipart/form-data`.

**Request Fields:**

| Field | Type | Required | Description |
|---|---|---|---|
| `title` | string | No | Mr, Mrs, Miss, Dr, Engr |
| `first_name` | string | Yes | |
| `last_name` | string | Yes | |
| `middle_name` | string | No | |
| `gender` | string | No | Male, Female, Other |
| `date_of_birth` | date | Yes | Must be before today |
| `nationality` | string | Yes | |
| `phone_number` | string | Yes | |
| `confirm_phone_number` | string | Yes | Must match `phone_number` |
| `foreign_number` | string | No | |
| `email_address` | string | Yes | Unique across portal_users and active individual_registrations |
| `confirm_email_address` | string | Yes | Must match `email_address` |
| `country` | string | Yes | |
| `state` | string | Yes | |
| `city` | string | Yes | |
| `chn` | string | No | Clearing House Number |
| `house_address` | string | Yes | |
| `previous_address` | string | No | |
| `encrypted_bvn` | string | Yes | RSA-OAEP (SHA-1) encrypted BVN from frontend |
| `bank_name` | string | No | |
| `bank_account_number` | string | No | |
| `tin_or_pin` | string | No | |
| `bank_details_consent` | boolean | No | |
| `identity_card_type` | string | Yes | National Identity Number, National ID, International Passport, Driver's License, Voter's Card, Permanent Voter's Card |
| `identity_card_number` | string | Required unless NIN | Plain-text card number (not used for NIN) |
| `encrypted_nin` | string | Required if NIN | RSA-OAEP (SHA-1) encrypted NIN from frontend |
| `identity_card` | file | Yes | PDF/JPG/PNG, max 5MB |
| `scanned_signature` | file | Yes | PDF/JPG/PNG, max 5MB |
| `passport_photograph` | file | Yes | JPG/PNG, max 5MB |
| `nin_document` | file | Required if NIN | PDF/JPG/PNG, max 5MB |
| `consent_checkbox` | boolean | Yes | Must be `true` |

**Response 201:**

```json
{
  "success": true,
  "message": "Individual registration received successfully. You will receive an email confirmation shortly.",
  "data": {
    "registration_id": 1,
    "first_name": "Jane",
    "last_name": "Doe",
    "email_address": "jane.doe@example.com",
    "status": "pending",
    "submitted_at": "2026-03-13T02:00:00+00:00"
  }
}
```

A confirmation email is sent to the registrant on success.

---

### Admin Endpoints

All admin individual registration endpoints require `Authorization: Bearer {token}` (system admin, `auth:sanctum`).

Base path: `/api/admin/individual/registrations`

---

#### Individual: List Pending Registrations

```
GET /api/admin/individual/registrations/pending
```

**Query params:** `per_page` (1–100, default 15)

**Response 200:**

```json
{
  "success": true,
  "data": [
    {
      "id": 1,
      "first_name": "Jane",
      "last_name": "Doe",
      "email_address": "jane.doe@example.com",
      "phone_number": "08012345678",
      "country": "Nigeria",
      "state": "Lagos",
      "city": "Ikeja",
      "submission_type": "public_form",
      "status": "pending",
      "account_status": "pending",
      "verification_status": "not_verified",
      "submitted_at": "2026-03-13T02:00:00+00:00"
    }
  ],
  "pagination": {
    "current_page": 1,
    "last_page": 1,
    "per_page": 15,
    "total": 1,
    "from": 1,
    "to": 1
  }
}
```

---

#### Individual: List All Registrations

```
GET /api/admin/individual/registrations
```

**Query params:**

| Param | Description |
|---|---|
| `status` | Filter by status: `pending`, `approved`, `verified`, `rejected` |
| `submission_type` | Filter by `public_form` or `bulk_upload` |
| `account_status` | Filter by `pending`, `invited`, `active` |
| `from_date` | Filter submitted on or after `YYYY-MM-DD` |
| `to_date` | Filter submitted on or before `YYYY-MM-DD` |
| `per_page` | Results per page (1–100, default 15) |

Response structure same as pending list but also includes `approved_at` and `rejected_at`.

---

#### Export Individual Registrations (CSV)

```
GET /api/admin/individual/registrations/export/csv
```

**Query params:** Same as list (`status`, `submission_type`, `account_status`, `from_date`, `to_date`)

**Response:** Streams a CSV file download.

**CSV Columns:** ID, First Name, Last Name, Email, Phone, Country, State, City, Submission Type, Status, Account Status, Verification Status, Submitted At, Approved At, Rejected At

---

#### Individual: Get Single Registration

```
GET /api/admin/individual/registrations/{id}
```

**Response 200:**

```json
{
  "success": true,
  "data": {
    "id": 1,
    "submission_type": "public_form",
    "status": "pending",
    "account_status": "pending",
    "verification_status": "not_verified",
    "rejection_reason": null,
    "title": "Ms",
    "first_name": "Jane",
    "last_name": "Doe",
    "middle_name": "Mary",
    "gender": "Female",
    "date_of_birth": "1990-05-15",
    "nationality": "Nigeria",
    "phone_number": "08012345678",
    "foreign_number": null,
    "email_address": "jane.doe@example.com",
    "country": "Nigeria",
    "state": "Lagos",
    "city": "Ikeja",
    "chn": null,
    "house_address": "123 Test Street",
    "previous_address": null,
    "bank_name": "GTBank",
    "bank_account_number": "0123456789",
    "tin_or_pin": null,
    "bank_details_consent": true,
    "identity_card_type": "International Passport",
    "identity_card_number": "A12345678",
    "documents": {
      "identity_card": "https://...",
      "scanned_signature": "https://...",
      "passport_photograph": "https://...",
      "nin_document": null
    },
    "consent_given": true,
    "submitted_at": "2026-03-13T02:00:00+00:00",
    "approved_at": null,
    "rejected_at": null,
    "approved_by": null,
    "rejected_by": null
  }
}
```

---

#### Individual: Update Registration

```
PUT  /api/admin/individual/registrations/{id}
POST /api/admin/individual/registrations/{id}
```

Use `PUT` for JSON, `POST` (multipart/form-data) when uploading files. All fields are optional (`sometimes`). Cannot update a rejected registration.

**Response 200:**

```json
{
  "success": true,
  "message": "Individual registration updated successfully.",
  "data": {
    "id": 1,
    "first_name": "Jane",
    "last_name": "Doe",
    "email_address": "jane.doe@example.com",
    "phone_number": "08012345678",
    "status": "pending"
  }
}
```

---

#### Individual: Approve Registration

```
POST /api/admin/individual/registrations/{id}/approve
```

Sets `status → approved`, `account_status → pending`. Cannot approve an already approved/verified/rejected registration.

**Response 200:**

```json
{
  "success": true,
  "message": "Individual registration approved successfully.",
  "data": {
    "id": 1,
    "first_name": "Jane",
    "last_name": "Doe",
    "email_address": "jane.doe@example.com",
    "status": "approved",
    "account_status": "pending",
    "approved_at": "2026-03-13T02:00:00+00:00"
  }
}
```

---

#### Individual: Send Invitation

```
POST /api/admin/individual/registrations/{id}/send-invitation
```

Creates a `PortalUser` with `role=individual` and sends the invitation email. If the registration is still `pending`, it is automatically approved first. If already `invited`, resends to the existing portal user.

**Request Body (optional):**

```json
{
  "email": "alternate@example.com"
}
```

If `email` is omitted, the invitation is sent to `email_address` on the registration.

**Response 200:**

```json
{
  "success": true,
  "message": "Registration approved and invitation sent successfully.",
  "data": {
    "submission_id": 1,
    "portal_user_id": 5,
    "individual_name": "Jane Doe",
    "individual_email": "jane.doe@example.com",
    "status": "approved",
    "account_status": "invited"
  }
}
```

**Error cases (422):**
- Registration is rejected
- Account is already active

---

#### Individual: Verify Registration

```
POST /api/admin/individual/registrations/{id}/verify
```

Sets `status → verified`. Only approved registrations can be verified.

**Response 200:**

```json
{
  "success": true,
  "message": "Individual registration verified successfully.",
  "data": {
    "id": 1,
    "first_name": "Jane",
    "last_name": "Doe",
    "email_address": "jane.doe@example.com",
    "status": "verified"
  }
}
```

---

#### Individual: Reject Registration

```
POST /api/admin/individual/registrations/{id}/reject
```

Sets `status → rejected` and sends a rejection email to the registrant.

**Request Body:**

```json
{
  "rejection_reason": "Documents are incomplete and unreadable."
}
```

`rejection_reason` is required (10–1000 characters). Cannot reject an already approved/verified/rejected registration.

**Response 200:**

```json
{
  "success": true,
  "message": "Individual registration rejected successfully. Rejection email sent.",
  "data": {
    "submission_id": 1,
    "individual_name": "Jane Doe",
    "rejection_reason": "Documents are incomplete and unreadable.",
    "rejected_at": "2026-03-13T02:00:00+00:00"
  }
}
```

---

#### Individual: AML/PEP Screen

```
POST /api/admin/individual/registrations/{id}/aml-screen
```

Screens the individual against PEP, sanctions, and adverse media lists via Dojah. Uses `first_name + last_name`, `date_of_birth`, `nationality`, and `gender` from the registration. Result is stored and linked to the registration. Re-screening is supported.

**Permission required**: `run_aml_screening`

**Request Body**: None required.

**Response `200 OK`**: See [AML Screening Response](#response-200-ok-2).

---

### Individual Portal Users

When an invitation is sent, a `PortalUser` with `role=individual` is created. This user can:
- Log in via `POST /api/portal/auth/login`
- Set password via `POST /api/portal/auth/set-password` (from invitation link)
- Access all portal auth endpoints (me, logout, enable/disable 2FA)

The portal user is linked to the registration via `individual_registration_id` on the `portal_users` table.
---

## Audit Log System

Every significant action in the system is recorded in the `audit_logs` table. Each entry is tamper-evident via a SHA-256 hash chain.

### Audit Log Entry Structure

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Auto-increment primary key |
| `timestamp` | datetime | When the action occurred (ISO 8601) |
| `action` | string | Action type constant (see table below) |
| `actor_type` | enum | `admin` \| `portal_user` \| `system` \| `public` |
| `actor_id` | integer\|null | ID of the authenticated user (null for public/system) |
| `actor_email` | string\|null | Email of the actor at time of action |
| `ip_address` | string\|null | Request IP address (IPv4 or IPv6) |
| `record_type` | string\|null | Model name of the affected entity (e.g. `CompanyRegistration`) |
| `record_id` | integer\|null | Primary key of the affected entity |
| `status` | enum | `SUCCESS` \| `FAILED` |
| `metadata` | json\|null | Action-specific extra data (never contains NIN/BVN values) |
| `prev_hash` | string\|null | SHA-256 of the previous entry's `current_hash` (tamper chain) |
| `current_hash` | string | SHA-256 of this entry's canonical content |

### Action Constants

#### Authentication
| Action | Triggered By |
|--------|-------------|
| `LOGIN_SUCCESS` | Admin or portal user authenticated successfully |
| `LOGIN_FAILED` | Authentication failed (wrong credentials) |
| `LOGOUT` | User logged out |
| `2FA_CODE_SENT` | 2FA OTP sent (initial or resend) |
| `2FA_VERIFIED` | 2FA code verified successfully |
| `2FA_FAILED` | 2FA code verification failed |
| `2FA_RESET` | Admin reset a portal user's 2FA |
| `2FA_ENABLED` | Portal user enabled 2FA |
| `2FA_DISABLED` | Portal user disabled 2FA |
| `PASSWORD_RESET_REQUESTED` | Forgot-password link requested |
| `PASSWORD_RESET_COMPLETED` | Password reset completed |
| `SET_PASSWORD` | Portal user set password from invitation link |

#### Company Registration
| Action | Triggered By |
|--------|-------------|
| `COMPANY_REGISTRATION_SUBMITTED` | Public form submission |
| `COMPANY_REGISTRATION_VIEWED` | Admin viewed a specific registration |
| `COMPANY_REGISTRATION_UPDATED` | Admin updated registration data |
| `COMPANY_REGISTRATION_APPROVED` | Admin approved the registration |
| `COMPANY_REGISTRATION_REJECTED` | Admin rejected the registration |
| `COMPANY_INVITATION_SENT` | Admin sent invitation + created company account |
| `COMPANY_BULK_UPLOAD` | Admin uploaded CSV for bulk company registration |

#### Employee Registration
| Action | Triggered By |
|--------|-------------|
| `EMPLOYEE_REGISTRATION_SUBMITTED` | Public form submission |
| `EMPLOYEE_REGISTRATION_VIEWED` | Admin viewed a specific registration |
| `EMPLOYEE_REGISTRATION_UPDATED` | Admin updated registration data |
| `EMPLOYEE_REGISTRATION_APPROVED` | Admin approved the registration |
| `EMPLOYEE_REGISTRATION_VERIFIED` | Admin verified the registration |
| `EMPLOYEE_REGISTRATION_REJECTED` | Admin rejected the registration |
| `EMPLOYEE_INVITATION_SENT` | Admin sent invitation + created employee account |
| `EMPLOYEE_BULK_UPLOAD` | Admin uploaded CSV for bulk employee registration |

#### Individual Registration
| Action | Triggered By |
|--------|-------------|
| `INDIVIDUAL_REGISTRATION_SUBMITTED` | Public form submission |
| `INDIVIDUAL_REGISTRATION_VIEWED` | Admin viewed a specific registration |
| `INDIVIDUAL_REGISTRATION_UPDATED` | Admin updated registration data |
| `INDIVIDUAL_REGISTRATION_APPROVED` | Admin approved the registration |
| `INDIVIDUAL_REGISTRATION_VERIFIED` | Admin verified the registration |
| `INDIVIDUAL_REGISTRATION_REJECTED` | Admin rejected the registration |
| `INDIVIDUAL_INVITATION_SENT` | Admin sent invitation + created portal user (role=individual) |

#### NIN / Sensitive Data
| Action | Triggered By |
|--------|-------------|
| `NIN_SUBMITTED` | Employee submitted encrypted NIN |
| `NIN_VERIFICATION_CALLED` | System called external NIN verification API |
| `NIN_VERIFICATION_SUCCESS` | External API confirmed NIN is valid |
| `NIN_VERIFICATION_FAILED` | External API rejected NIN or call failed |
| `NIN_VALIDATED` | Employee data matched NIN data (name + DOB check passed) |
| `NIN_VALIDATION_FAILED` | Employee data did not match NIN data |

> **Note:** NIN/BVN values are **never** stored in audit log metadata. Only IDs, report_ids, and status flags are recorded.

#### Portal Operations
| Action | Triggered By |
|--------|-------------|
| `PORTAL_USER_CREATED` | System created a portal user (via invitation flow) |
| `INVITATION_RESENT` | Admin resent an invitation email |
| `EMPLOYEE_CREATED` | Company admin created a new employee via portal |
| `BENEFICIARIES_SUBMITTED` | Employee submitted/updated beneficiary records |
| `EMPLOYEE_CSV_EXPORTED` | Company admin or system admin exported employee CSV |
| `COMPANY_CREATED` | Admin created a company directly |

### Hash Chain Integrity

Each entry's `current_hash` is a SHA-256 of:
```
timestamp | action | actor_type | actor_id | actor_email | ip_address | record_type | record_id | status | metadata_json | prev_hash
```

The `prev_hash` of each entry is the `current_hash` of the immediately preceding entry. Any tampering with a past entry will break the chain from that point forward.

### Security Notes
- Audit log entries are **append-only** — no `updated_at` column
- NIN, BVN, and passwords are **never** written to `metadata`
- All sensitive view operations (e.g. viewing a KYC record with documents) are logged as `*_VIEWED` entries

---

### Query Audit Logs

**`GET /api/admin/audit-logs`** `auth:sanctum`

Returns a paginated list of audit log entries, newest first.

#### Query Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `action` | string | No | Filter by exact action constant (e.g. `LOGIN_SUCCESS`) |
| `actor_type` | string | No | `admin` \| `portal_user` \| `system` \| `public` |
| `actor_email` | string | No | Filter by exact actor email |
| `record_type` | string | No | Model name of the affected entity (e.g. `CompanyRegistration`) |
| `record_id` | integer | No | Primary key of the affected entity |
| `status` | string | No | `SUCCESS` \| `FAILED` (case-insensitive) |
| `date_from` | string | No | `Y-m-d` — include entries from this date (inclusive) |
| `date_to` | string | No | `Y-m-d` — include entries up to this date (inclusive) |
| `per_page` | integer | No | Items per page (10–100, default 50) |

#### Response `200 OK`

```json
{
  "success": true,
  "data": {
    "current_page": 1,
    "data": [
      {
        "id": 42,
        "timestamp": "2026-03-14T09:30:00.000000Z",
        "action": "COMPANY_REGISTRATION_APPROVED",
        "actor_type": "admin",
        "actor_id": 1,
        "actor_email": "admin@example.com",
        "ip_address": "192.168.1.1",
        "record_type": "CompanyRegistration",
        "record_id": 17,
        "status": "SUCCESS",
        "metadata": { "company_name": "Acme Ltd" },
        "prev_hash": "abc123...",
        "current_hash": "def456..."
      }
    ],
    "total": 256,
    "per_page": 50,
    "last_page": 6,
    "from": 1,
    "to": 50
  }
}
```

#### Example Requests

```
# All failed actions
GET /api/admin/audit-logs?status=FAILED

# All actions on a specific company registration
GET /api/admin/audit-logs?record_type=CompanyRegistration&record_id=17

# Login activity for a specific user
GET /api/admin/audit-logs?action=LOGIN_SUCCESS&actor_email=user@example.com

# Everything that happened on a specific day
GET /api/admin/audit-logs?date_from=2026-03-14&date_to=2026-03-14
```

---

## Admin: Document Management

Admins can upload, list, view, download, and delete documents. All endpoints require `auth:sanctum`.

All actions are recorded in the audit log (`DOCUMENT_UPLOADED`, `DOCUMENT_VIEWED`, `DOCUMENT_DOWNLOADED`, `DOCUMENT_DELETED`).

---

### 1. Upload Document

**`POST /api/admin/documents`** — multipart/form-data

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `name` | string | Yes | Display name (max 255 chars) |
| `file` | file | Yes | Max 50MB. Allowed: pdf, doc, docx, xls, xlsx, csv, txt, jpg, jpeg, png, gif, webp, zip |
| `description` | string | No | Optional description (max 1000 chars) |

**Response 201:**
```json
{
  "success": true,
  "message": "Document uploaded successfully.",
  "data": {
    "id": 1,
    "name": "Policy Guidelines 2026",
    "original_filename": "policy_guidelines.pdf",
    "extension": "pdf",
    "mime_type": "application/pdf",
    "file_size": 524288,
    "file_size_formatted": "512.0 KB",
    "description": "Annual policy guidelines",
    "url": "http://localhost/storage/admin-documents/uuid.pdf",
    "uploaded_by": {
      "id": 1,
      "name": "John Admin",
      "email": "admin@example.com"
    },
    "uploaded_at": "2026-03-14T10:00:00+00:00"
  }
}
```

---

### 2. List Documents

**`GET /api/admin/documents`**

| Query Param | Type | Notes |
|-------------|------|-------|
| `per_page` | integer | Default 15, max 100 |
| `search` | string | Searches name, original_filename, description |

**Response 200:**
```json
{
  "success": true,
  "data": [
    {
      "id": 1,
      "name": "Policy Guidelines 2026",
      "original_filename": "policy_guidelines.pdf",
      "extension": "pdf",
      "mime_type": "application/pdf",
      "file_size": 524288,
      "file_size_formatted": "512.0 KB",
      "description": null,
      "url": "http://localhost/storage/admin-documents/uuid.pdf",
      "uploaded_by": { "id": 1, "name": "John Admin", "email": "admin@example.com" },
      "uploaded_at": "2026-03-14T10:00:00+00:00"
    }
  ],
  "pagination": {
    "current_page": 1,
    "last_page": 1,
    "per_page": 15,
    "total": 1,
    "from": 1,
    "to": 1
  }
}
```

---

### 3. View Document Metadata

**`GET /api/admin/documents/{id}`**

Returns the same `data` object as the upload response.

**Response 404** if document not found.

---

### 4. Download Document

**`GET /api/admin/documents/{id}/download`**

Returns the file as a forced download (`Content-Disposition: attachment`). The `Content-Type` matches the file's MIME type.

**Response 404** if document not found or file missing from storage.

---

### 5. Delete Document

**`DELETE /api/admin/documents/{id}`**

Deletes the database record and the physical file from storage.

**Response 200:**
```json
{
  "success": true,
  "message": "Document deleted successfully."
}
```

**Response 404** if document not found.

---

### File Size Formatting

The `file_size_formatted` field uses the following scale:
- `< 1 KB` → `N B`
- `< 1 MB` → `N.N KB`
- `< 1 GB` → `N.N MB`
- `≥ 1 GB` → `N.NN GB`

---

## Admin: Roles & Permissions

All endpoints require `Authorization: Bearer <admin_token>` (auth:sanctum).
Only `super-admin` users have access to role and permission management endpoints.
The `super-admin` role bypasses all permission checks automatically.

### Predefined System Roles

| Role | Description |
|------|-------------|
| `super-admin` | Full access — bypasses all permission checks |
| `kyc-manager` | Full KYC workflow: view, edit, approve, reject, verify, invite, bulk upload, export |
| `kyc-reviewer` | View + approve/reject/verify — no edit or invite |
| `external` | Read-only access across all sections |
| `analytics-viewer` | Analytics endpoints only |
| `auditor` | Audit log read-only |

System roles (`is_system: true`) cannot be renamed or deleted. The `super-admin` role permissions cannot be modified.

### All Permissions (45 total)

| Group | Permissions |
|-------|------------|
| Company Registrations | `view_company_registrations`, `edit_company_registrations`, `approve_company_registrations`, `reject_company_registrations`, `send_company_invite`, `bulk_upload_company_registrations`, `export_company_registrations` |
| Employee Registrations | `view_employee_registrations`, `edit_employee_registrations`, `approve_employee_registrations`, `reject_employee_registrations`, `verify_employee_registrations`, `send_employee_invite`, `bulk_upload_employee_registrations`, `export_employee_registrations` |
| Individual Registrations | `view_individual_registrations`, `edit_individual_registrations`, `approve_individual_registrations`, `reject_individual_registrations`, `verify_individual_registrations`, `send_individual_invite`, `export_individual_registrations` |
| Companies | `view_companies`, `create_companies`, `generate_company_registration_link`, `run_aml_screening` |
| Portal Users | `view_portal_users`, `resend_portal_user_invitation` |
| Analytics | `view_analytics` |
| Audit Logs | `view_audit_logs` |
| Documents & KYC Files | `view_documents`, `upload_documents`, `download_documents`, `delete_documents` |
| 2FA Management | `reset_user_2fa` |
| Admin User Management | `view_admin_users`, `create_admin_users`, `edit_admin_users`, `delete_admin_users`, `assign_roles` |
| Role & Permission Management | `view_roles`, `create_roles`, `edit_roles`, `delete_roles`, `assign_permissions` |

---

### 1. Get Authenticated Admin (me)

Returns the currently authenticated admin user with their roles and all resolved permissions.

```
GET /api/admin/me
```

**No special permission required — available to all authenticated admins.**

**Response 200 OK**
```json
{
  "data": {
    "id": 1,
    "name": "Super Admin",
    "email": "admin@josbiz.com",
    "two_factor_setup": true,
    "roles": ["super-admin"],
    "direct_permissions": [],
    "all_permissions": ["view_companies", "approve_company_registrations", "...43 total"],
    "created_at": "2026-01-01T00:00:00.000Z"
  }
}
```

---

### 2. List All Permissions

Returns all 43 available permission names — used by frontend to populate permission checkboxes.

```
GET /api/admin/permissions
```

**Required permission:** `view_roles`

**Response 200 OK**
```json
{
  "data": [
    "approve_company_registrations",
    "approve_employee_registrations",
    "approve_individual_registrations",
    "assign_permissions",
    "assign_roles",
    "..."
  ]
}
```

---

### 3. List All Roles

Returns all roles with their assigned permissions and a flag indicating if the role is a system role.

```
GET /api/admin/roles
```

**Required permission:** `view_roles`

**Response 200 OK**
```json
{
  "data": [
    {
      "id": 1,
      "name": "super-admin",
      "is_system": true,
      "permissions": [],
      "created_at": "2026-01-01T00:00:00.000Z"
    },
    {
      "id": 2,
      "name": "kyc-manager",
      "is_system": true,
      "permissions": ["view_company_registrations", "edit_company_registrations", "..."],
      "created_at": "2026-01-01T00:00:00.000Z"
    }
  ]
}
```

---

### 4. Get Single Role

```
GET /api/admin/roles/{role}
```

**Required permission:** `view_roles`

**Response 200 OK** — same shape as a single item from the list above.

---

### 5. Create Custom Role

Creates a new custom role and optionally assigns permissions to it.

```
POST /api/admin/roles
```

**Required permission:** `create_roles`

**Request Body**
```json
{
  "name": "compliance-officer",
  "permissions": ["view_company_registrations", "view_audit_logs"]
}
```

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `name` | string | Yes | Unique, max 100 chars |
| `permissions` | array | No | Must be valid existing permission names |

**Response 201 Created**
```json
{
  "message": "Role created successfully.",
  "data": {
    "id": 7,
    "name": "compliance-officer",
    "is_system": false,
    "permissions": ["view_company_registrations", "view_audit_logs"],
    "created_at": "2026-03-20T10:00:00.000Z"
  }
}
```

**Response 422** if name is duplicate or permissions are invalid.

---

### 6. Rename Role

Renames a custom role. System roles cannot be renamed.

```
PUT /api/admin/roles/{role}
```

**Required permission:** `edit_roles`

**Request Body**
```json
{
  "name": "new-role-name"
}
```

**Response 200 OK** — returns updated role.

**Response 422** if role is a system role or name is taken.

---

### 7. Delete Role

Deletes a custom role. System roles cannot be deleted.
Users with only this role will have no role after deletion.

```
DELETE /api/admin/roles/{role}
```

**Required permission:** `delete_roles`

**Response 200 OK**
```json
{ "message": "Role deleted successfully." }
```

**Response 422** if role is a system role.

---

### 8. Sync Role Permissions

Replaces all permissions on a role with the provided set. Pass an empty array to remove all permissions. The `super-admin` role cannot be modified.

```
PUT /api/admin/roles/{role}/permissions
```

**Required permission:** `assign_permissions`

**Request Body**
```json
{
  "permissions": ["view_analytics", "view_audit_logs", "view_companies"]
}
```

**Response 200 OK** — returns updated role with new permission list.

**Response 422** if role is `super-admin` or permissions are invalid.

---

## Admin: Internal User Management

All endpoints require `Authorization: Bearer <admin_token>` (auth:sanctum).

### 1. List Admin Users

```
GET /api/admin/users
```

**Required permission:** `view_admin_users`

**Response 200 OK**
```json
{
  "data": [
    {
      "id": 1,
      "name": "Super Admin",
      "email": "admin@josbiz.com",
      "two_factor_setup": true,
      "roles": ["super-admin"],
      "created_at": "2026-01-01T00:00:00.000Z"
    }
  ]
}
```

---

### 2. Get Single Admin User

Returns the user with full role and permission detail.

```
GET /api/admin/users/{user}
```

**Required permission:** `view_admin_users`

**Response 200 OK**
```json
{
  "data": {
    "id": 2,
    "name": "Emeka Okafor",
    "email": "emeka@josbiz.com",
    "two_factor_setup": false,
    "roles": ["kyc-manager"],
    "direct_permissions": [],
    "all_permissions": ["view_company_registrations", "approve_company_registrations", "..."],
    "created_at": "2026-01-01T00:00:00.000Z"
  }
}
```

---

### 3. Create Admin User

Creates a new admin user. A random temporary password is set — the user must use Forgot Password to set their own. At least one role must be assigned.

```
POST /api/admin/users
```

**Required permission:** `create_admin_users`

**Request Body**
```json
{
  "name": "Ngozi Adeyemi",
  "email": "ngozi@josbiz.com",
  "roles": ["kyc-reviewer"]
}
```

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `name` | string | Yes | Max 255 chars |
| `email` | string | Yes | Unique email address |
| `roles` | array | Yes | Min 1 role — must be valid existing role names |

**Response 201 Created**
```json
{
  "message": "Admin user created successfully.",
  "data": {
    "id": 5,
    "name": "Ngozi Adeyemi",
    "email": "ngozi@josbiz.com",
    "two_factor_setup": false,
    "roles": ["kyc-reviewer"],
    "created_at": "2026-03-20T10:00:00.000Z"
  }
}
```

**Response 422** if email is taken or roles are invalid.

---

### 4. Update Admin User

Updates a user's name and/or email.

```
PUT /api/admin/users/{user}
```

**Required permission:** `edit_admin_users`

**Request Body** (all fields optional)
```json
{
  "name": "Updated Name",
  "email": "new-email@josbiz.com"
}
```

**Response 200 OK** — returns updated user.

---

### 5. Delete Admin User

Deletes an admin user. Cannot delete your own account or the root super-admin (`admin@josbiz.com`).

```
DELETE /api/admin/users/{user}
```

**Required permission:** `delete_admin_users`

**Response 200 OK**
```json
{ "message": "Admin user deleted successfully." }
```

**Response 422** if attempting to delete own account or root super-admin.

---

### 6. Assign Roles to Admin User

Syncs roles on a user — **replaces** all current roles with the provided set. The root super-admin's roles cannot be modified.

```
PUT /api/admin/users/{user}/roles
```

**Required permission:** `assign_roles`

**Request Body**
```json
{
  "roles": ["kyc-reviewer", "auditor"]
}
```

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `roles` | array | Yes | Min 1 role — must be valid existing role names |

**Response 200 OK**
```json
{
  "message": "Roles assigned successfully.",
  "data": {
    "id": 2,
    "name": "Emeka Okafor",
    "email": "emeka@josbiz.com",
    "two_factor_setup": true,
    "roles": ["auditor", "kyc-reviewer"],
    "created_at": "2026-01-01T00:00:00.000Z"
  }
}
```

**Response 422** if roles are invalid or attempting to modify root super-admin.
