# 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