20 KiB
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 callsget_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-isclaimHistory()logic — copy and adapt (remove priority label mapping that uses internal arrays)getValidationRules()— reuse for create, with relaxedinsured_contact_*rules since those come from external contextformatDatesForClaim()— reuse as-ischeckDuplicateNonEbClaim()— reuse as-ishandleAssetFileUpload()— reuse as-issaveAssets()— reuse as-is
TODO Checklist
Phase 1 — Route Registration
- Add
/api/v1route group inapp/Config/Routes.php(use existing auth filter — noauthMVC):$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, usesResponseTrait - Import:
NonEbTicketMasterModel,NonEbClaimAssetModel,TicketHistoryModel,TicketClaimStatusModel,ClaimFilesModel - No session calls —
created_byshould be the resolvedapi_client_id(or a fixed system user ID for API) - All responses:
Content-Type: application/json
- Namespace:
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,priorityare 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:
{
"status": true,
"claim_id": 123,
"message": "Non-EB Claim created successfully"
}
Error Response 400 — validation failure:
{
"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:
{
"status": false,
"message": "Client policy not found or inactive"
}
Error Response 422 — policy type not allowed:
{
"status": false,
"message": "Only Non-EB or Marine policy types are allowed"
}
Conflict Response 409:
{
"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_policyrow — return404if not found oris_active != 1 - Validate
policy_type.allocg IN ('Non-EB', 'Marine')— return422if EB type sent - Fetch
acm_idfromclient_rm(level=3, is_active=1) for the derivedclient_id— set null and log warning if none found - Auto-set
claim_status_id— firstticket_claim_statusrow for derivedpolicy_type_id ORDER BY id ASC(same pattern asinitiateClaiminEmployeeRestController) - 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_byexplicitly in insert data — model'sbeforeInsertcallback callsget_session_userid()which returns null for API; CI4 uses the explicitly passed value - Duplicate check via
checkDuplicateNonEbClaim()— runs after derivation soclient_idandpolicy_noare already populated - Date format:
DD-MM-YYYY→formatDatesForClaim()converts toY-m-dfor 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-datawhenasset_fileis included - Return
claim_idin 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:
{
"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+OFFSETbased onpageandper_page; also run aCOUNT(*)variant of the same query fortotal - Accept JSON body (
$this->request->getJSON(true)) notgetPost()— existing controller usesgetPost() - Cap
per_pageat 100 to prevent abuse - Strip HTML from
nature_of_loss,loss_descriptionbefore returning (use existingconvertHtmlToText()oresc()) - 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 (whereNotInon terminal statuses)- Add
sort_byparam later (optional/phase 2):loss_date,created_at,updated_atwithsort_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):
- Fetch all
ticket_historyrows forticket_id - Keep only rows where
field_name = 'claim_status_id' - For each row, look up
new_valueagainstticket_claim_status— only include it if that status has a non-nulldisplay_name - Map to
display_namefor output — never expose raw internalclaim_statusstring - Reverse to chronological order (oldest first for timeline display)
- Do not expose
modified_by— who changed it is internal
URL Param: claim_id — integer, required
Success Response 200:
{
"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_nameinticket_claim_statusit is silently skipped — it is an internal-only status not meant for user visibility.
Error Response 404:
{
"status": false,
"message": "Claim not found"
}
TODOs:
- Do NOT reuse
claimHistory()fromNonEbClaimControlleras-is — that returns all field changes for internal staff view. Write a separate method that fetches onlyclaim_status_idhistory rows - Fetch all
ticket_historywhereticket_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_statusonnew_value = id— filter out rows wheredisplay_name IS NULL - Verify claim exists in
non_eb_ticket_master(is_active = 1) before querying history — return404if not - Format
created_atasDD-MM-YYYY HH:MM AM/PM(matches EB pattern:date('d-m-Y h:i A', strtotime(...))) - Output only
status(display_name) andchanged_atper row — nofield_name, noold_value, nomodified_by - Scope check: verify the claim's
client_idmatches 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):
{
"is_action_freeze": false,
"docs": [
{ "document_name": "Invoice Copy", "document_received": false },
{ "document_name": "Survey Report", "document_received": true }
]
}
Behavior:
- Validate claim exists (
non_eb_ticket_master.id = claim_id,is_active = 1) —404if not - Fetch
required_docsJSON from the ticket - If
required_docsis empty or null — return422(no checklist configured for this claim) - If
is_action_freeze = true— return423(checklist is locked, uploads not allowed) - Find the doc entry where
document_namematches exactly —404if not found in list - Upload file → insert row into
claim_files(ticket_type = 2,file_type = 2,ticket_id = claim_id,doc_name = document_name) - Update
required_docs: setdocument_received = truefor the matched doc entry, write back tonon_eb_ticket_master.required_docs - Return success with the full updated
required_docsobject
Success Response 200:
{
"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:
{ "status": false, "message": "Claim not found" }
Error Response 422 — no checklist configured:
{ "status": false, "message": "No required documents checklist configured for this claim" }
Error Response 423 — checklist locked:
{ "status": false, "message": "Document checklist is locked for this claim" }
Error Response 404 — document_name not in list:
{ "status": false, "message": "Document 'Invoice Copy' not found in required documents list" }
TODOs:
- Use
claim_filesmodel for file insert — same structure as existinguploadFile()inNonEbClaimController(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_namematch is case-sensitive exact match — document this for API consumers- Allow re-upload if
document_receivedis alreadytrue— overwrite the previousclaim_filesentry (soft-delete old, insert new) OR just insert new and keep both; decide and document - Do NOT expose the
claim_files.urlfile path directly — return download URL viabase_url('downloadClaimFile/') . $file_idso internal paths are not leaked - Validate file extension against
UPLOAD_EXT_CLAIM_DOCSconstant — reject unsupported types with415 - The
required_docsupdate must be atomic — update the JSON andclaim_filesinsert 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_keyper 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_logtable for auditCREATE 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+ preflightOPTIONShandling 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:
{ "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_logtable
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