466 lines
20 KiB
Markdown
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
|