nhance/app/Controllers/Api/NonEbClaimApiController.php
2026-03-31 12:04:35 +05:30

621 lines
24 KiB
PHP

<?php
namespace App\Controllers\Api;
use App\Controllers\BaseController;
use CodeIgniter\API\ResponseTrait;
use App\Models\NonEbTicketMasterModel;
use App\Models\NonEbClaimAssetModel;
use App\Models\TicketHistoryModel;
use App\Models\TicketClaimStatusModel;
use App\Models\ClaimFilesModel;
use App\Models\ClientPolicyModel;
use App\Models\PolicyTypeModel;
use App\Models\ClientRMModel;
use App\Models\EmployeeModel;
use App\Models\LevelContactModel;
use App\Helpers\JWTToken;
class NonEbClaimApiController extends BaseController
{
use ResponseTrait;
protected $nonEbTicketModel;
protected $assetModel;
protected $ticketHistoryModel;
protected $claimStatusModel;
protected $claimFilesModel;
protected $clientPolicyModel;
protected $policyTypeModel;
protected $clientRMModel;
protected $myLogger;
// API system user ID — used as created_by for all inserts (no session)
const API_SYSTEM_USER_ID = 0;
public function __construct()
{
$this->nonEbTicketModel = new NonEbTicketMasterModel();
$this->assetModel = new NonEbClaimAssetModel();
$this->ticketHistoryModel = new TicketHistoryModel();
$this->claimStatusModel = new TicketClaimStatusModel();
$this->claimFilesModel = new ClaimFilesModel();
$this->clientPolicyModel = new ClientPolicyModel();
$this->policyTypeModel = new PolicyTypeModel();
$this->clientRMModel = new ClientRMModel();
$this->myLogger = \Config\Services::mylogger();
}
// ===================== AUTH HELPER =====================
/**
* Decode the JWT from Authorization header and return the resolved user row.
* Returns array with normalised keys: id, name, email, mobile.
* Returns null if token is missing or invalid.
*/
protected function getAuthUser(): ?array
{
$authHeader = $this->request->getHeaderLine('Authorization');
if (empty($authHeader)) {
return null;
}
$result = JWTToken::validateJWT($authHeader);
if ($result['status'] !== true) {
return null;
}
$decoded = $result['decoded'];
if (isset($decoded['emp_code'])) {
$model = new EmployeeModel();
$user = $model->find($decoded['id'] ?? null);
if (!$user) return null;
return [
'id' => $user['id'],
'name' => $user['name'] ?? '',
'email' => $user['email_corporate'] ?? $user['email'] ?? '',
'mobile' => $user['mobile'] ?? '',
];
}
$model = new LevelContactModel();
$user = $model->find($decoded['post_hr_id'] ?? null);
if (!$user) return null;
return [
'id' => $user['id'],
'name' => $user['name'] ?? '',
'email' => $user['email'] ?? '',
'mobile' => $user['mobile'] ?? '',
];
}
// ===================== SHARED HELPERS =====================
/**
* Convert DD-MM-YYYY date fields to Y-m-d for DB storage.
*/
protected function formatDatesForClaim(array $data): array
{
$dateFields = ['loss_date', 'intimation_recd_date', 'intimated_to_insurer_date', 'eta_for_documents'];
foreach ($dateFields as $field) {
if (empty($data[$field])) {
$data[$field] = null;
} else {
$data[$field] = change_date_format($data[$field], null, 'Y-m-d');
}
}
return $data;
}
/**
* Duplicate check: same client + loss_date + policy_no.
*/
protected function checkDuplicateNonEbClaim(array $data): bool
{
$client_id = $data['client_id'] ?? null;
$loss_date = $data['loss_date'] ?? null;
$policy_no = $data['policy_no'] ?? null;
if (empty($client_id) || empty($loss_date)) return false;
$query = $this->nonEbTicketModel
->where('client_id', $client_id)
->where('loss_date', $loss_date)
->where('is_active', 1);
if (!empty($policy_no)) {
$query->where('policy_no', $policy_no);
}
return !empty($query->first());
}
/**
* Upload the asset_file from the request. Returns filename or null.
*/
protected function handleAssetFileUpload(): ?string
{
$file = $this->request->getFile('asset_file');
if ($file === null || !$file->isValid() || $file->hasMoved()) {
return null;
}
$uploadPath = WRITEPATH . 'uploads/non_eb_asset_files/';
$fileName = file_Upload($file, $uploadPath, UPLOAD_EXT_ASSET_FILES);
return !empty($fileName) ? $fileName : null;
}
/**
* Save asset rows (asset_id_code[], serial_no[], etc.) linked to a claim.
*/
protected function saveAssets(int $claim_id, array $post_data): void
{
$this->assetModel->where('non_eb_ticket_id', $claim_id)->set(['is_active' => 0])->update();
$codes = $post_data['asset_id_code'] ?? [];
$serials = $post_data['serial_no'] ?? [];
$vehicles = $post_data['vehicle_no'] ?? [];
$descriptions = $post_data['asset_description'] ?? [];
if (!is_array($codes)) return;
for ($i = 0; $i < count($codes); $i++) {
$code = trim($codes[$i] ?? '');
$serial = trim($serials[$i] ?? '');
$vehicle = trim($vehicles[$i] ?? '');
$desc = trim($descriptions[$i] ?? '');
if (empty($code) && empty($serial) && empty($vehicle) && empty($desc)) continue;
$this->assetModel->insert([
'non_eb_ticket_id' => $claim_id,
'asset_id_code' => $code,
'serial_no' => $serial,
'vehicle_no' => $vehicle,
'asset_description' => $desc,
'is_active' => 1,
]);
}
}
/**
* Insert the initial history row after a new claim is created.
*/
protected function putHistoryAfterInsert(array $ticket_data, int $ticket_id): void
{
if (!empty($ticket_data)) {
$this->ticketHistoryModel->insert([
'ticket_id' => $ticket_id,
'field_name' => 'claim_status_id',
'display_name' => 'Claim Created',
'old_value' => null,
'new_value' => $ticket_data['claim_status_id'],
'created_by' => self::API_SYSTEM_USER_ID,
'is_active' => 1,
]);
}
}
// ===================== ENDPOINTS =====================
public function createClaim()
{
$authUser = $this->getAuthUser();
if (!$authUser) {
return $this->respond(['status' => false, 'code' => 401, 'message' => 'Unauthorized'], 401);
}
$body = $this->request->getJSON(true) ?? $this->request->getPost();
// Validate minimal user-facing fields only
$rules = [
'client_policy_id' => ['rules' => 'required|is_natural_no_zero', 'errors' => ['required' => 'Client Policy is required']],
'nature_of_loss' => ['rules' => 'required|min_length[3]', 'errors' => ['required' => 'Nature of Loss is required']],
'loss_location' => ['rules' => 'required', 'errors' => ['required' => 'Loss Location is required']],
'loss_date' => ['rules' => 'required', 'errors' => ['required' => 'Loss Date is required']],
'loss_description' => ['rules' => 'permit_empty'],
'loss_estimate' => ['rules' => 'permit_empty|numeric'],
'claim_number' => ['rules' => 'permit_empty|regex_match[/^[a-zA-Z0-9\/_-]+$/]'],
];
if (!$this->validateData($body, $rules)) {
return $this->respond([
'status' => false, 'code' => 400,
'message' => 'Input validation failed',
'errors' => $this->validator->getErrors(),
], 400);
}
// Fetch client_policy — derive all FK fields from it
$cp = $this->clientPolicyModel
->where('id', (int)$body['client_policy_id'])
->where('is_active', 1)
->first();
if (!$cp) {
return $this->respond(['status' => false, 'code' => 404, 'message' => 'Client policy not found or inactive'], 404);
}
// Validate policy type is Non-EB or Marine
$policyType = $this->policyTypeModel
->select('allocg')
->where('id', $cp['policy_type_id'])
->where('is_active', 1)
->first();
if (!$policyType || !in_array($policyType['allocg'], ['Non-EB', 'Marine'])) {
return $this->respond(['status' => false, 'code' => 422, 'message' => 'Only Non-EB or Marine policy types are allowed'], 422);
}
// Fetch ACM from client_rm (level 3)
$acm = $this->clientRMModel
->where('client_id', $cp['client_id'])
->where('level', 3)
->where('is_active', 1)
->first();
if (!$acm) {
$this->myLogger->logme('error', "[NON_EB_API] No ACM found for client_id: {$cp['client_id']}");
}
// Auto-set first claim_status_id for this policy type
$firstStatus = $this->claimStatusModel
->select('id')
->where('ticket_type', $cp['policy_type_id'])
->orderBy('id', 'ASC')
->first();
if (!$firstStatus) {
return $this->respond(['status' => false, 'code' => 422, 'message' => 'No claim status configured for this policy type'], 422);
}
// Build ticket data — merge user input with server-derived values
$ticket_data = [
'client_policy_id' => (int)$cp['id'],
'client_id' => (int)$cp['client_id'],
'branch_id' => (int)$cp['client_branch_id'],
'policy_type_id' => (int)$cp['policy_type_id'],
'insurer_id' => (int)$cp['insurer_id'],
'policy_no' => $cp['policy_no'] ?? null,
'acm_id' => $acm ? (int)$acm['user_id'] : null,
'claim_status_id' => (int)$firstStatus['id'],
'insured_contact_name' => $authUser['name'],
'insured_contact_number' => $authUser['mobile'],
'insured_contact_email' => $authUser['email'],
'nature_of_loss' => $body['nature_of_loss'],
'loss_location' => $body['loss_location'],
'loss_date' => $body['loss_date'],
'loss_description' => $body['loss_description'] ?? null,
'loss_estimate' => $body['loss_estimate'] ?? null,
'claim_number' => $body['claim_number'] ?? null,
'priority' => 1,
'created_by' => self::API_SYSTEM_USER_ID,
];
$ticket_data = $this->formatDatesForClaim($ticket_data);
// Handle optional asset file upload
$assetFileName = null;
$assetFile = $this->request->getFile('asset_file');
if ($assetFile !== null && $assetFile->isValid() && !$assetFile->hasMoved()) {
$ext = strtolower($assetFile->getClientExtension());
$allowed = UPLOAD_EXT_ASSET_FILES; // ['pdf', 'xls', 'xlsx', 'csv']
if (!in_array($ext, $allowed)) {
return $this->respond([
'status' => false, 'code' => 415,
'message' => 'Unsupported file type: ' . $ext . '. Allowed: ' . implode(', ', $allowed),
], 415);
}
if (empty(trim($ticket_data['loss_description'] ?? ''))) {
return $this->respond([
'status' => false, 'code' => 400,
'message' => 'Input validation failed',
'errors' => ['loss_description' => 'Loss Description is required when uploading an asset file.'],
], 400);
}
$uploadPath = WRITEPATH . 'uploads/non_eb_asset_files/';
$assetFileName = file_Upload($assetFile, $uploadPath, $allowed);
if (empty($assetFileName)) {
return $this->respond(['status' => false, 'code' => 500, 'message' => 'Asset file upload failed'], 500);
}
$ticket_data['asset_file'] = $assetFileName;
}
// Duplicate check (runs after derivation so client_id + policy_no are populated)
if ($this->checkDuplicateNonEbClaim($ticket_data)) {
return $this->respond([
'status' => false, 'code' => 409,
'message' => 'Duplicate claim found for Client + Loss Date + Policy No combination',
], 409);
}
$claim_id = $this->nonEbTicketModel->insert($ticket_data);
if (!$claim_id) {
return $this->respond(['status' => false, 'code' => 500, 'message' => 'Failed to create claim'], 500);
}
$this->saveAssets($claim_id, $body);
$this->putHistoryAfterInsert($ticket_data, $claim_id);
$this->myLogger->logme('error', "[NON_EB_API] Claim created. ID: $claim_id, user: {$authUser['id']}");
return $this->respond([
'status' => true,
'code' => 200,
'claim_id' => $claim_id,
'asset_file' => $assetFileName,
'message' => 'Non-EB Claim created successfully',
], 200);
}
public function listClaims()
{
$authUser = $this->getAuthUser();
if (!$authUser) {
return $this->respond(['status' => false, 'code' => 401, 'message' => 'Unauthorized'], 401);
}
$body = $this->request->getJSON(true) ?? [];
$page = max(1, (int)($body['page'] ?? 1));
$per_page = min(100, max(1, (int)($body['per_page'] ?? 20)));
$offset = ($page - 1) * $per_page;
$db = db_connect();
$builder = $db->table('non_eb_ticket_master tm');
$builder->select([
'tm.id',
'tm.claim_number',
'tm.nhance_claim_ref_no',
'tm.policy_no',
'tm.policy_type_id',
'tm.claim_status_id',
'tcs.claim_status AS status',
'tcs.display_name AS status_display',
'pt.policy_type AS policy_type_name',
'c.client_name',
'i.name AS insurer_name',
'tm.loss_date',
'tm.loss_location',
'tm.nature_of_loss',
'tm.loss_estimate',
'tm.insured_contact_name',
'tm.insured_contact_number',
'(SELECT first_name FROM user_profiles WHERE user_profiles.id = tm.acm_id) AS acm_name',
'DATE_FORMAT(tm.created_at, "%d-%m-%Y") AS created_date',
'DATE_FORMAT(tm.updated_at, "%d-%m-%Y") AS updated_date',
]);
$builder->join('clients c', 'c.id = tm.client_id AND c.is_active = 1', 'left');
$builder->join('insurers i', 'i.id = tm.insurer_id AND i.is_active = 1', 'left');
$builder->join('ticket_claim_status tcs','tcs.id = tm.claim_status_id AND tcs.is_active = 1', 'left');
$builder->join('policy_type pt', 'pt.id = tm.policy_type_id AND pt.is_active = 1', 'left');
$builder->where('tm.is_active', 1);
// Filters
if (!empty($body['client_id'])) $builder->where('tm.client_id', (int)$body['client_id']);
if (!empty($body['insurer_id'])) $builder->where('tm.insurer_id', (int)$body['insurer_id']);
if (!empty($body['policy_type_id'])) $builder->where('tm.policy_type_id', (int)$body['policy_type_id']);
if (!empty($body['claim_status_id'])) $builder->where('tm.claim_status_id', (int)$body['claim_status_id']);
if (!empty($body['claim_number'])) $builder->like('tm.claim_number', $body['claim_number']);
if (!empty($body['nhance_claim_ref_no'])) $builder->like('tm.nhance_claim_ref_no', $body['nhance_claim_ref_no']);
// Date range filter
if (!empty($body['date_type']) && !empty($body['start_date']) && !empty($body['end_date'])) {
$col = $body['date_type'] === 'updated_date' ? 'tm.updated_at' : 'tm.created_at';
$start = date('Y-m-d 00:00:00', strtotime(str_replace('-', '/', $body['start_date'])));
$end = date('Y-m-d 23:59:59', strtotime(str_replace('-', '/', $body['end_date'])));
$builder->where("$col BETWEEN '$start' AND '$end'");
}
// Exclude terminal statuses by default
if (empty($body['show_closed'])) {
$builder->whereNotIn('tcs.display_name', ['Claim Settled', 'Claim Closed', 'Claim Rejected', 'Claim Withdrawn']);
}
// COUNT for pagination (clone before limit)
$countBuilder = clone $builder;
$total = $countBuilder->countAllResults(false);
$builder->orderBy('tm.id', 'DESC');
$builder->limit($per_page, $offset);
$data = $builder->get()->getResultArray();
return $this->respond([
'status' => true,
'code' => 200,
'total' => (int)$total,
'page' => $page,
'per_page' => $per_page,
'data' => $data,
], 200);
}
public function claimHistory(int $claim_id)
{
$authUser = $this->getAuthUser();
if (!$authUser) {
return $this->respond(['status' => false, 'code' => 401, 'message' => 'Unauthorized'], 401);
}
// Verify claim exists
$claim = $this->nonEbTicketModel
->select('id, client_id, policy_type_id')
->where('id', $claim_id)
->where('is_active', 1)
->first();
if (!$claim) {
return $this->respond(['status' => false, 'code' => 404, 'message' => 'Claim not found'], 404);
}
// Fetch only claim_status_id history rows, oldest first
$history_rows = $this->ticketHistoryModel
->select('new_value, created_at')
->where('ticket_id', $claim_id)
->where('field_name', 'claim_status_id')
->where('is_active', 1)
->orderBy('created_at', 'ASC')
->findAll();
if (empty($history_rows)) {
return $this->respond(['status' => true, 'code' => 200, 'claim_id' => $claim_id, 'history' => []], 200);
}
// Build display_name map for this policy type — only statuses with a display_name are user-visible
$statuses = $this->claimStatusModel
->select('id, claim_status, display_name')
->where('ticket_type', $claim['policy_type_id'])
->where('display_name IS NOT NULL')
->where("display_name != ''")
->where('is_active', 1)
->findAll();
$display_map = array_column($statuses, 'display_name', 'id');
// Filter and format — skip statuses with no display_name
$history = [];
foreach ($history_rows as $row) {
$status_id = (int)$row['new_value'];
$display_name = $display_map[$status_id] ?? null;
if (!$display_name) continue;
$history[] = [
'status' => $display_name,
'changed_at' => date('d-m-Y h:i A', strtotime($row['created_at'])),
];
}
return $this->respond([
'status' => true,
'code' => 200,
'claim_id' => $claim_id,
'history' => $history,
], 200);
}
public function uploadRequiredDoc(int $claim_id)
{
$authUser = $this->getAuthUser();
if (!$authUser) {
return $this->respond(['status' => false, 'code' => 401, 'message' => 'Unauthorized'], 401);
}
// Verify claim exists
$claim = $this->nonEbTicketModel
->select('id, client_id, required_docs')
->where('id', $claim_id)
->where('is_active', 1)
->first();
if (!$claim) {
return $this->respond(['status' => false, 'code' => 404, 'message' => 'Claim not found'], 404);
}
// Validate required_docs checklist is configured
$required_docs = json_decode($claim['required_docs'] ?? '{}', true);
if (empty($required_docs) || empty($required_docs['docs'])) {
return $this->respond(['status' => false, 'code' => 422, 'message' => 'No required documents checklist configured for this claim'], 422);
}
// Check if checklist is locked
if (!empty($required_docs['is_action_freeze'])) {
return $this->respond(['status' => false, 'code' => 423, 'message' => 'Document checklist is locked for this claim'], 423);
}
// Validate inputs
$document_name = trim($this->request->getPost('document_name') ?? '');
if (empty($document_name)) {
return $this->respond(['status' => false, 'code' => 400, 'message' => 'Input validation failed', 'errors' => ['document_name' => 'document_name is required']], 400);
}
$file = $this->request->getFile('file');
if (!$file || !$file->isValid() || $file->hasMoved()) {
return $this->respond(['status' => false, 'code' => 400, 'message' => 'Input validation failed', 'errors' => ['file' => 'A valid file is required']], 400);
}
// Validate file extension
$allowed = defined('UPLOAD_EXT_CLAIM_DOCS') ? UPLOAD_EXT_CLAIM_DOCS : ['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx', 'xls', 'xlsx'];
$ext = strtolower($file->getClientExtension());
if (is_array($allowed) && !in_array($ext, $allowed)) {
return $this->respond(['status' => false, 'code' => 415, 'message' => 'Unsupported file type: ' . $ext], 415);
}
// Find matching doc in checklist (exact case-sensitive match)
$matched_index = null;
foreach ($required_docs['docs'] as $i => $doc) {
if (($doc['document_name'] ?? '') === $document_name) {
$matched_index = $i;
break;
}
}
if ($matched_index === null) {
return $this->respond([
'status' => false, 'code' => 404,
'message' => "Document '{$document_name}' not found in required documents list",
], 404);
}
// Upload file and update required_docs atomically
$db = db_connect();
$db->transStart();
$upload_path = WRITEPATH . 'uploads/claim_files/';
$file_name = file_Upload($file, $upload_path, UPLOAD_EXT_CLAIM_DOCS);
if (empty($file_name)) {
$db->transRollback();
return $this->respond(['status' => false, 'code' => 500, 'message' => 'File upload failed'], 500);
}
// Insert into claim_files
$this->claimFilesModel->insert([
'ticket_id' => $claim_id,
'ticket_type' => 2,
'doc_name' => $document_name,
'file_name' => $file_name,
'url' => $upload_path . $file_name,
'file_type' => 2,
'mime_type' => getMimeTypeByFileName($file_name),
'is_active' => 1,
'created_by' => self::API_SYSTEM_USER_ID,
]);
// Mark document as received in required_docs JSON
$required_docs['docs'][$matched_index]['document_received'] = true;
$updated_json = json_encode($required_docs);
$db->query('UPDATE non_eb_ticket_master SET required_docs = ? WHERE id = ?', [$updated_json, $claim_id]);
$db->transComplete();
if (!$db->transStatus()) {
return $this->respond(['status' => false, 'code' => 500, 'message' => 'Failed to save document. Please try again.'], 500);
}
$file_id = $this->claimFilesModel->insertID();
$download_url = base_url('downloadClaimFile/') . $file_id;
return $this->respond([
'status' => true,
'code' => 200,
'message' => 'Document uploaded successfully',
'claim_id' => $claim_id,
'document_name' => $document_name,
'download_url' => $download_url,
'required_docs' => $required_docs,
], 200);
}
}