nhance/nonebapi.md
2026-03-31 12:04:35 +05:30

466 lines
20 KiB
Markdown

# Non-EB Claims — External API Dev Doc & TODO
**Purpose:** Expose four read/write endpoints to external applications (client portals, mobile apps, integrations) for Non-EB Claims.
---
## Endpoints to Build
| # | Method | URL | Purpose |
|---|---|---|---|
| 1 | POST | `/api/v1/non-eb-claim/create` | Create a new Non-EB claim |
| 2 | POST | `/api/v1/non-eb-claim/list` | List claims with optional filter params |
| 3 | GET | `/api/v1/non-eb-claim/history/{claim_id}` | Get field change history of a claim |
| 4 | POST | `/api/v1/non-eb-claim/{claim_id}/upload-required-doc` | Upload a file against a required document checklist item |
---
## Architecture Decision — New Controller
Create a **separate API controller**: `app/Controllers/Api/NonEbClaimApiController.php`
**Why separate, not reusing `NonEbClaimController`:**
- Existing controller uses session-based auth (`get_session_userid()`, `set_session_context()`) — not safe for API
- API needs token/key auth middleware (no session)
- API responses must be pure JSON always — no HTML rendering
- `createClaim()` currently calls `get_session_userid()` via model callbacks (`checkAndADDCreatedByValue`) — needs to be overridden for API context
- Existing `claimSearch()` reads `$this->request->getPost()` directly — API version should accept clean JSON body
**Reuse:**
- `NonEbTicketMasterModel`, `NonEbClaimAssetModel`, `TicketHistoryModel`, `ClaimFilesModel` — reuse as-is
- `claimHistory()` logic — copy and adapt (remove priority label mapping that uses internal arrays)
- `getValidationRules()` — reuse for create, with relaxed `insured_contact_*` rules since those come from external context
- `formatDatesForClaim()` — reuse as-is
- `checkDuplicateNonEbClaim()` — reuse as-is
- `handleAssetFileUpload()` — reuse as-is
- `saveAssets()` — reuse as-is
---
## TODO Checklist
### Phase 1 — Route Registration
- [ ] Add `/api/v1` route group in `app/Config/Routes.php` (use existing auth filter — no `authMVC`):
```php
$routes->group('api/v1', ['filter' => 'your-existing-api-filter'], function($routes) {
$routes->group('non-eb-claim', function($routes) {
$routes->post('create', 'Api\NonEbClaimApiController::createClaim');
$routes->post('list', 'Api\NonEbClaimApiController::listClaims');
$routes->get('history/(:num)', 'Api\NonEbClaimApiController::claimHistory/$1');
$routes->post('(:num)/upload-required-doc', 'Api\NonEbClaimApiController::uploadRequiredDoc/$1');
});
});
```
- [ ] Confirm CI4 namespace resolution for `App\Controllers\Api\NonEbClaimApiController`
---
### Phase 2 — Controller Skeleton
- [ ] Create `app/Controllers/Api/NonEbClaimApiController.php`
- Namespace: `App\Controllers\Api`
- Extends `BaseController`, uses `ResponseTrait`
- Import: `NonEbTicketMasterModel`, `NonEbClaimAssetModel`, `TicketHistoryModel`, `TicketClaimStatusModel`, `ClaimFilesModel`
- No session calls — `created_by` should be the resolved `api_client_id` (or a fixed system user ID for API)
- All responses: `Content-Type: application/json`
---
### Phase 3 — API 1: Create Claim
**POST** `/api/v1/non-eb-claim/create`
**Context:** The user is already logged in and has selected a client policy from the interface. They are triggering a claim with only the loss details — what happened, where, and when. This is a **one-shot create with no edit step**. The controller is responsible for fetching all other mandatory data from the DB and assembling the full record before insert. The caller sends the minimum possible.
---
**What the caller sends (user-facing fields only):**
| Field | Required | Type | Notes |
|---|---|---|---|
| `client_policy_id` | Yes | int | Selected in UI before opening the claim form; all other FK fields derived from this |
| `nature_of_loss` | Yes | string | min 3 chars — what happened |
| `loss_location` | Yes | string | where it happened |
| `loss_date` | Yes | string | `DD-MM-YYYY` — when it happened |
| `loss_description` | No | string | Additional details; required if `asset_file` is sent |
| `loss_estimate` | No | numeric | Approximate loss value |
| `claim_number` | No | string | `^[a-zA-Z0-9\/_-]+$` — if already known |
| `asset_file` | No | file | xlsx/xls/csv/pdf |
| `asset_id_code[]` | No | array | asset rows |
| `serial_no[]` | No | array | |
| `vehicle_no[]` | No | array | |
| `asset_description[]` | No | array | |
> Surveyor fields, settlement fields, `nhance_claim_ref_no`, `priority` are **not** part of this create flow — they are filled by staff later via the internal web interface.
---
**What the controller fetches and inserts (caller does NOT send these):**
| Field | Source |
|---|---|
| `client_id` | `client_policy.client_id` |
| `branch_id` | `client_policy.client_branch_id` |
| `policy_type_id` | `client_policy.policy_type_id` |
| `insurer_id` | `client_policy.insurer_id` |
| `policy_no` | `client_policy.policy_no` |
| `acm_id` | `client_rm` where `client_id = cp.client_id AND level = 3 AND is_active = 1` — first result |
| `claim_status_id` | First row of `ticket_claim_status` where `ticket_type = policy_type_id ORDER BY id ASC` |
| `insured_contact_name` | Logged-in user's name from auth (resolved via auth middleware) |
| `insured_contact_number` | Logged-in user's mobile from auth |
| `insured_contact_email` | Logged-in user's email from auth |
| `priority` | Hard-coded default: `1` (Low) |
| `created_by` | API system user ID (explicit — not from session) |
---
**Success Response `200`:**
```json
{
"status": true,
"claim_id": 123,
"message": "Non-EB Claim created successfully"
}
```
**Error Response `400` — validation failure:**
```json
{
"status": false,
"message": "Input validation failed",
"errors": {
"nature_of_loss": "Nature of Loss is required",
"loss_date": "Loss Date is required"
}
}
```
**Error Response `404` — policy not found:**
```json
{
"status": false,
"message": "Client policy not found or inactive"
}
```
**Error Response `422` — policy type not allowed:**
```json
{
"status": false,
"message": "Only Non-EB or Marine policy types are allowed"
}
```
**Conflict Response `409`:**
```json
{
"status": false,
"message": "Duplicate claim found for Client + Loss Date + Policy No combination"
}
```
---
**TODOs:**
- [ ] Validate only the user-facing fields — write a trimmed validation rule set for API (not reusing `getValidationRules()` directly since that includes staff-side fields); required: `client_policy_id`, `nature_of_loss`, `loss_location`, `loss_date`
- [ ] Fetch `client_policy` row — return `404` if not found or `is_active != 1`
- [ ] Validate `policy_type.allocg IN ('Non-EB', 'Marine')` — return `422` if EB type sent
- [ ] Fetch `acm_id` from `client_rm` (level=3, is_active=1) for the derived `client_id` — set null and log warning if none found
- [ ] Auto-set `claim_status_id` — first `ticket_claim_status` row for derived `policy_type_id ORDER BY id ASC` (same pattern as `initiateClaim` in `EmployeeRestController`)
- [ ] Resolve insured contact fields from the authenticated user (name, mobile, email) via auth middleware — these are mandatory DB fields but the user does not type them
- [ ] Handle `created_by` explicitly in insert data — model's `beforeInsert` callback calls `get_session_userid()` which returns null for API; CI4 uses the explicitly passed value
- [ ] Duplicate check via `checkDuplicateNonEbClaim()` — runs after derivation so `client_id` and `policy_no` are already populated
- [ ] Date format: `DD-MM-YYYY``formatDatesForClaim()` converts to `Y-m-d` for DB
- [ ] Call `saveAssets()` after insert if asset fields present
- [ ] Call `putHistoryAfterInsert()` after insert
- [ ] Skip `sendAutoMailTrigger()` for this initial create — claim is at first status, mail trigger is for status transitions; document this
- [ ] Handle `multipart/form-data` when `asset_file` is included
- [ ] Return `claim_id` in response so the caller can reference the created claim
---
### Phase 4 — API 2: List Claims
**POST** `/api/v1/non-eb-claim/list`
**Request Body (JSON):**
| Field | Required | Type | Notes |
|---|---|---|---|
| `page` | No | int | Default 1 |
| `per_page` | No | int | Default 20, max 100 |
| `policy_type_id` | No | int | Filter by policy type |
| `claim_status_id` | No | int | Filter by status |
| `client_id` | No | int | Filter by client |
| `insurer_id` | No | int | Filter by insurer |
| `claim_number` | No | string | LIKE search |
| `nhance_claim_ref_no` | No | string | LIKE search |
| `date_type` | No | string | `created_date` or `updated_date` |
| `start_date` | No | string | `DD-MM-YYYY`, used with `date_type` |
| `end_date` | No | string | `DD-MM-YYYY`, used with `date_type` |
| `show_closed` | No | bool | Default `false` — if false, excludes Settled/Closed/Rejected/Withdrawn |
**Success Response `200`:**
```json
{
"status": true,
"total": 87,
"page": 1,
"per_page": 20,
"data": [
{
"id": 123,
"claim_number": "CLM/2026/001",
"nhance_claim_ref_no": "NEB/2026/00123",
"client_name": "ABC Corp",
"insurer_name": "New India Assurance",
"policy_type_name": "All Risk",
"policy_no": "POL/1234/2026",
"status": "Claim Intimation - Insured",
"status_display": "Claim Intimation - Insured",
"loss_date": "15-03-2026",
"loss_location": "Chennai",
"nature_of_loss": "Fire damage",
"loss_estimate": "500000",
"insured_contact_name": "John Doe",
"insured_contact_number": "9876543210",
"acm_name": "Ravi Kumar",
"created_date": "01-03-2026",
"updated_date": "20-03-2026"
}
]
}
```
**TODOs:**
- [ ] Reuse the `claimSearch()` query from existing controller — extract it into a shared private method or duplicate in API controller
- [ ] Add **pagination**: `LIMIT` + `OFFSET` based on `page` and `per_page`; also run a `COUNT(*)` variant of the same query for `total`
- [ ] Accept JSON body (`$this->request->getJSON(true)`) not `getPost()` — existing controller uses `getPost()`
- [ ] Cap `per_page` at 100 to prevent abuse
- [ ] Strip HTML from `nature_of_loss`, `loss_description` before returning (use existing `convertHtmlToText()` or `esc()`)
- [ ] Decide which fields to expose — **do not return**: `google_drive_links`, `documents_*`, `required_docs`, `is_active`, `created_by`, `updated_by`
- [ ] `show_closed: false` (default) mirrors the existing default list behaviour (`whereNotIn` on terminal statuses)
- [ ] Add `sort_by` param later (optional/phase 2): `loss_date`, `created_at`, `updated_at` with `sort_dir: asc|desc`
---
### Phase 5 — API 3: Claim History
**GET** `/api/v1/non-eb-claim/history/{claim_id}`
**URL Param:** `claim_id` — integer, required
**What is shown vs hidden:**
This endpoint does **not** expose the full raw `ticket_history` table. It follows the same pattern as the EB `claimView` API — only **status progression is shown to the user**, and only statuses that have a `display_name` in `ticket_claim_status` are included. Internal status names, field-level changes (surveyor, ACM, priority etc.), and who made the change are all hidden.
The response is a **chronological status timeline** — oldest to newest — showing when the claim moved through each user-visible stage.
**Filtering logic (mirrors EB `claimView`):**
1. Fetch all `ticket_history` rows for `ticket_id`
2. Keep only rows where `field_name = 'claim_status_id'`
3. For each row, look up `new_value` against `ticket_claim_status` — only include it if that status has a non-null `display_name`
4. Map to `display_name` for output — never expose raw internal `claim_status` string
5. Reverse to chronological order (oldest first for timeline display)
6. Do not expose `modified_by` — who changed it is internal
**URL Param:** `claim_id` — integer, required
**Success Response `200`:**
```json
{
"status": true,
"claim_id": 123,
"history": [
{
"status": "Claim Intimation - Insured",
"changed_at": "01-03-2026 10:15 AM"
},
{
"status": "Under Process",
"changed_at": "05-03-2026 02:30 PM"
},
{
"status": "Claim Settled",
"changed_at": "20-03-2026 04:45 PM"
}
]
}
```
> If a status does not have a `display_name` in `ticket_claim_status` it is silently skipped — it is an internal-only status not meant for user visibility.
**Error Response `404`:**
```json
{
"status": false,
"message": "Claim not found"
}
```
**TODOs:**
- [ ] Do NOT reuse `claimHistory()` from `NonEbClaimController` as-is — that returns all field changes for internal staff view. Write a separate method that fetches only `claim_status_id` history rows
- [ ] Fetch all `ticket_history` where `ticket_id = claim_id AND field_name = 'claim_status_id' AND is_active = 1 ORDER BY created_at ASC`
- [ ] For each row join or look up `ticket_claim_status` on `new_value = id` — filter out rows where `display_name IS NULL`
- [ ] Verify claim exists in `non_eb_ticket_master` (`is_active = 1`) before querying history — return `404` if not
- [ ] Format `created_at` as `DD-MM-YYYY HH:MM AM/PM` (matches EB pattern: `date('d-m-Y h:i A', strtotime(...))`)
- [ ] Output only `status` (display_name) and `changed_at` per row — no `field_name`, no `old_value`, no `modified_by`
- [ ] Scope check: verify the claim's `client_id` matches the authenticated user's client before returning — prevents users from fetching other clients' claim history
---
### Phase 6 — API 4: Upload Required Document
**POST** `/api/v1/non-eb-claim/{claim_id}/upload-required-doc`
**URL Param:** `claim_id` — integer, required
**Request Body (`multipart/form-data`):**
| Field | Required | Type | Notes |
|---|---|---|---|
| `document_name` | Yes | string | Must exactly match one of the `document_name` values in `required_docs.docs[]` |
| `file` | Yes | file | Allowed types: same as `UPLOAD_EXT_CLAIM_DOCS` |
**`required_docs` JSON structure (stored in `non_eb_ticket_master.required_docs`):**
```json
{
"is_action_freeze": false,
"docs": [
{ "document_name": "Invoice Copy", "document_received": false },
{ "document_name": "Survey Report", "document_received": true }
]
}
```
**Behavior:**
1. Validate claim exists (`non_eb_ticket_master.id = claim_id`, `is_active = 1`) — `404` if not
2. Fetch `required_docs` JSON from the ticket
3. If `required_docs` is empty or null — return `422` (no checklist configured for this claim)
4. If `is_action_freeze = true` — return `423` (checklist is locked, uploads not allowed)
5. Find the doc entry where `document_name` matches exactly — `404` if not found in list
6. Upload file → insert row into `claim_files` (`ticket_type = 2`, `file_type = 2`, `ticket_id = claim_id`, `doc_name = document_name`)
7. Update `required_docs`: set `document_received = true` for the matched doc entry, write back to `non_eb_ticket_master.required_docs`
8. Return success with the full updated `required_docs` object
**Success Response `200`:**
```json
{
"status": true,
"message": "Document uploaded successfully",
"claim_id": 123,
"document_name": "Invoice Copy",
"required_docs": {
"is_action_freeze": false,
"docs": [
{ "document_name": "Invoice Copy", "document_received": true },
{ "document_name": "Survey Report", "document_received": true }
]
}
}
```
**Error Response `404` — claim not found:**
```json
{ "status": false, "message": "Claim not found" }
```
**Error Response `422` — no checklist configured:**
```json
{ "status": false, "message": "No required documents checklist configured for this claim" }
```
**Error Response `423` — checklist locked:**
```json
{ "status": false, "message": "Document checklist is locked for this claim" }
```
**Error Response `404` — document_name not in list:**
```json
{ "status": false, "message": "Document 'Invoice Copy' not found in required documents list" }
```
**TODOs:**
- [ ] Use `claim_files` model for file insert — same structure as existing `uploadFile()` in `NonEbClaimController` (`ticket_type = 2`, `file_type = 2`)
- [ ] File upload path: `WRITEPATH . 'uploads/claim_files/'` — same as web controller
- [ ] `created_by`: use API system user ID (same pattern as create endpoint)
- [ ] `document_name` match is **case-sensitive exact match** — document this for API consumers
- [ ] Allow re-upload if `document_received` is already `true` — overwrite the previous `claim_files` entry (soft-delete old, insert new) OR just insert new and keep both; decide and document
- [ ] Do NOT expose the `claim_files.url` file path directly — return download URL via `base_url('downloadClaimFile/') . $file_id` so internal paths are not leaked
- [ ] Validate file extension against `UPLOAD_EXT_CLAIM_DOCS` constant — reject unsupported types with `415`
- [ ] The `required_docs` update must be atomic — update the JSON and `claim_files` insert in a DB transaction; roll back file insert if JSON update fails
---
### Phase 7 — Cross-cutting Concerns
- [ ] **Rate limiting**: add a simple request counter per `api_key` per minute in a cache table or Redis. Block at 60 req/min.
- [ ] **Request logging**: log every API request (api_client_id, endpoint, status_code, ip, timestamp) into an `api_request_log` table for audit
```sql
CREATE TABLE api_request_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
api_client_id INT,
endpoint VARCHAR(100),
method VARCHAR(10),
status_code SMALLINT,
ip_address VARCHAR(45),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
```
- [ ] **CORS headers**: if external app is browser-based, add `Access-Control-Allow-Origin` + preflight `OPTIONS` handling in a filter
- [ ] **API versioning**: prefix as `/api/v1/non-eb-claim/...` from day one — easier to version-bump later without breaking consumers
- [ ] **Error envelope**: all errors must follow a consistent shape:
```json
{ "status": false, "code": 400, "message": "...", "errors": {} }
```
- [ ] **Postman collection**: create and share a Postman collection with all 4 endpoints, sample request bodies, and expected responses
- [ ] **DB changes** — add to `db.md`:
- `api_request_log` table
---
## Field Reference — What the External App Must Know
### Required IDs (need lookup endpoints or a handshake step)
These are FK IDs that the external app must send — they need to either be pre-agreed or fetched from lookup endpoints (future phase):
| ID Field | Lookup Source |
|---|---|
| `client_id` | `clients` table |
| `branch_id` | `client_branch` table, filtered by `client_id` |
| `policy_type_id` | `policy_type` table, `allocg IN ('Non-EB','Marine')` |
| `client_policy_id` | `client_policy` table, filtered by `branch_id` |
| `insurer_id` | `insurers` table |
| `acm_id` | `user_profiles` table, `role = 3` |
| `claim_status_id` | `ticket_claim_status` table, filtered by `ticket_type = policy_type_id` |
**TODO (future phase):** Build read-only lookup endpoints (`/api/v1/lookup/clients`, `/api/v1/lookup/policy-types`, etc.) so the external app can populate dropdowns without hardcoding IDs.
---
### Claim Status Flow (for external app awareness)
The trigger-based DB trigger (`non_eb_ticket_master_after_update`) auto-logs status changes into `ticket_history`. History API reflects these changes. External app should not attempt to set arbitrary statuses — only statuses listed in `allowed_status` of the current status are valid transitions.
**TODO:** Document valid status transitions in a separate handshake endpoint response or static doc section.
---
## Files to Create / Modify
| File | Action |
|---|---|
| `app/Controllers/Api/NonEbClaimApiController.php` | **Create** |
| `app/Config/Routes.php` | **Modify** — add `/api/v1` route group |
| `db.md` | **Modify** — add `api_request_log` DDL |
---
## Out of Scope (this phase)
- Update claim via API — not requested; skip for now
- Delete/remove claim via API — internal operation only
- Claim file download via API — separate phase
- IR Documents (required_docs) CRUD via API — separate phase (upload-required-doc only marks received + stores file; full checklist management is out of scope)
- Webhook callbacks to external app on status change — separate phase