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 = storage_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); } $rawBody = $this->request->getBody(); $isJson = str_contains($this->request->getHeaderLine('Content-Type'), 'application/json'); $body = ($isJson || ($rawBody !== '' && $rawBody !== null)) ? (json_decode($rawBody, true) ?? $this->request->getPost()) : $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][createClaim] 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 = storage_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][createClaim] 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); } try { $body = $this->request->getJSON(true) ?? []; } catch (\Throwable $e) { return $this->respond(['status' => false, 'code' => 400, 'message' => 'Invalid JSON body'], 400); } $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('md5(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(); $this->myLogger->logme('error', "[NON_EB_API][listClaims] user: {$authUser['id']}, page: $page, total: $total"); 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); } // Fetch full claim row with joins (client, insurer, status, policy type, ACM, branch) $raw = $this->nonEbTicketModel->getTicketDataByTicketID($claim_id); if (!$raw) { return $this->respond(['status' => false, 'code' => 404, 'message' => 'Claim not found'], 404); } // claims_data — major display fields only $claims_data = [ 'id' => $raw['id'], 'nhance_claim_ref_no' => $raw['nhance_claim_ref_no'], 'claim_number' => $raw['claim_number'], 'policy_no' => $raw['policy_no'], 'policy_type_name' => $raw['policy_type_name'], 'client_name' => $raw['client_name'], 'branch_name' => $raw['branch_name'], 'insurer_name' => $raw['insurer_name'], 'claim_status' => $raw['claim_status'], 'status_display_name' => $raw['status_display_name'], 'insured_contact_name' => $raw['insured_contact_name'], 'insured_contact_number' => $raw['insured_contact_number'], 'insured_contact_email' => $raw['insured_contact_email'], 'nature_of_loss' => $raw['nature_of_loss'], 'loss_location' => $raw['loss_location'], 'loss_date' => $raw['loss_date'], 'loss_description' => $raw['loss_description'], 'loss_estimate' => $raw['loss_estimate'], 'loss_assessed_value' => $raw['loss_assessed_value'], 'settled_amount' => $raw['settled_amount'], 'settlement_utr' => $raw['settlement_utr'], 'intimation_recd_date' => $raw['intimation_recd_date'], 'intimated_to_insurer_date'=> $raw['intimated_to_insurer_date'], 'surveyor_name' => $raw['surveyor_name'], 'surveyor_contact_person' => $raw['surveyor_contact_person'], 'surveyor_contact_number' => $raw['surveyor_contact_number'], 'acm' => $raw['acm'], 'acm_mobile' => $raw['acm_mobile'], 'asset_file' => $raw['asset_file'], 'priority' => $raw['priority'], 'created_at' => $raw['created_at'], 'updated_at' => $raw['updated_at'], ]; // required_docs — decoded separately $required_docs = json_decode($raw['required_docs'] ?? '{}', true) ?: []; // claims_files $claims_files = $this->claimFilesModel ->select('id, doc_name, file_name, mime_type, file_type, docs_for_ir, created_at') ->where('ticket_id', $claim_id) ->where('ticket_type', 2) ->where('is_active', 1) ->findAll(); // ticket_data — claim status history, 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(); $statuses = $this->claimStatusModel ->select('id, claim_status, display_name') ->where('ticket_type', $raw['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'); $ticket_data = []; foreach ($history_rows as $row) { $status_id = (int)$row['new_value']; $display_name = $display_map[$status_id] ?? null; if (!$display_name) continue; $ticket_data[] = [ 'status' => $display_name, 'changed_at' => date('d-m-Y h:i A', strtotime($row['created_at'])), ]; } $this->myLogger->logme('error', "[NON_EB_API][claimHistory] claim_id: $claim_id, steps: " . count($ticket_data) . ", user: {$authUser['id']}"); return $this->respond([ 'status' => true, 'code' => 200, 'claim_id' => $claim_id, 'claims_data' => $claims_data, 'required_docs' => $required_docs, 'claims_files' => $claims_files, 'ticket_data' => $ticket_data, ], 200); } /** * GET /api/v1/non-eb-claim/statuses * Returns Non-EB claim statuses (ticket_type = 50). */ public function listClaimStatuses() { $authUser = $this->getAuthUser(); if (!$authUser) { return $this->respond(['status' => false, 'code' => 401, 'message' => 'Unauthorized'], 401); } $statuses = $this->claimStatusModel ->select('id, claim_status, display_name') ->where('ticket_type', 50) ->where('is_active', 1) ->orderBy('id', 'ASC') ->findAll(); $this->myLogger->logme('error', "[NON_EB_API][listClaimStatuses] user: {$authUser['id']}, count: " . count($statuses)); return $this->respond([ 'status' => true, 'code' => 200, 'data' => $statuses, ], 200); } /** * POST /api/v1/non-eb-claim/policies * Returns Non-EB / Marine policies for a given client (md5) + branch. * * Body: * client_id string MD5 hash of the client's numeric id (required) * client_branch_id int client branch id (required) */ public function listPolicies() { $authUser = $this->getAuthUser(); if (!$authUser) { return $this->respond(['status' => false, 'code' => 401, 'message' => 'Unauthorized'], 401); } try { $body = $this->request->getJSON(true) ?? $this->request->getPost(); } catch (\Throwable $e) { return $this->respond(['status' => false, 'code' => 400, 'message' => 'Invalid JSON body'], 400); } $client_id_md5 = trim($body['client_id'] ?? ''); $client_branch_id = (int)($body['client_branch_id'] ?? 0); $errors = []; if (empty($client_id_md5)) { $errors['client_id'] = 'client_id is required'; } elseif (!preg_match('/^[a-f0-9]{32}$/i', $client_id_md5)) { $errors['client_id'] = 'client_id must be a valid MD5 hash'; } if ($client_branch_id <= 0) { $errors['client_branch_id'] = 'client_branch_id is required'; } if (!empty($errors)) { return $this->respond(['status' => false, 'code' => 400, 'message' => 'Input validation failed', 'errors' => $errors], 400); } $db = db_connect(); $builder = $db->table('client_policy cp'); $builder->select([ 'cp.id', 'cp.policy_no', 'cp.policy_type_id', 'pt.policy_type AS policy_type_name', 'cp.insurer_id', 'i.name AS insurer_name', 'i.short_name AS insurer_short_name', 'DATE_FORMAT(cp.policy_start_date, "%d-%m-%Y") AS policy_start_date', 'DATE_FORMAT(cp.policy_end_date, "%d-%m-%Y") AS policy_end_date', ]); $builder->join('policy_type pt', 'pt.id = cp.policy_type_id', 'left'); $builder->join('insurers i', 'i.id = cp.insurer_id AND i.is_active = 1', 'left'); $builder->where('MD5(cp.client_id)', $client_id_md5); $builder->where('cp.client_branch_id', $client_branch_id); $builder->where('cp.is_active', 1); $builder->whereIn('pt.allocg', ['Non-EB', 'Marine']); $builder->orderBy('cp.id', 'DESC'); $policies = $builder->get()->getResultArray(); $this->myLogger->logme('error', "[NON_EB_API][listPolicies] user: {$authUser['id']}, client_md5: $client_id_md5, branch: $client_branch_id, count: " . count($policies)); return $this->respond([ 'status' => true, 'code' => 200, 'total' => count($policies), 'data' => $policies, ], 200); } public function uploadRequiredDoc() { $authUser = $this->getAuthUser(); if (!$authUser) { return $this->respond(['status' => false, 'code' => 401, 'message' => 'Unauthorized'], 401); } $ticket_id = $this->request->getPost('ticket_id') ?? null; $get_file_data = $this->request->getFiles('claim_docs') ?? null; $get_docs_name = $this->request->getPost('claim_doc_names') ?? []; $required_docs = $this->request->getPost('required_docs') ?? null; if (is_string($get_docs_name)) { $decoded = json_decode($get_docs_name, true); $get_docs_name = json_last_error() === JSON_ERROR_NONE ? $decoded : []; } elseif (!is_array($get_docs_name)) { $get_docs_name = []; } $file_data = []; if (!empty($get_file_data)) { $file_path = WRITEPATH . 'uploads/claim_files/'; $file_data = storage_multi_file_Upload($get_file_data, $file_path, $get_docs_name, UPLOAD_EXT_CLAIM_DOCS); } if (!empty($file_data) && !empty($ticket_id)) { foreach ($file_data as $value) { $this->claimFilesModel->insert([ 'ticket_id' => $ticket_id, 'ticket_type' => 2, 'doc_name' => $value['doc_name'], 'file_name' => $value['file_name'], 'url' => $value['file_path'], 'file_type' => 2, 'mime_type' => getMimeTypeByFileName($value['file_name']), 'docs_for_ir' => 1, 'is_active' => 1, 'created_by' => self::API_SYSTEM_USER_ID, ]); } db_connect()->query( 'UPDATE non_eb_ticket_master SET required_docs = ? WHERE id = ?', [$required_docs, $ticket_id] ); $this->myLogger->logme('error', "[NON_EB_API][uploadRequiredDoc] Docs uploaded. ticket_id: $ticket_id, count: " . count($file_data) . ", user: {$authUser['id']}"); return $this->respond(['status' => true, 'code' => 200, 'message' => 'Files uploaded successfully'], 200); } return $this->respond(['status' => false, 'code' => 400, 'message' => 'Failed to upload the file'], 400); } public function uploadRequiredDoc_v1(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/'; $uploaded = storage_claim_file_Upload($file, $upload_path, UPLOAD_EXT_CLAIM_DOCS); if ($uploaded === '') { $db->transRollback(); return $this->respond(['status' => false, 'code' => 500, 'message' => 'File upload failed'], 500); } $disk_name = $uploaded['disk_name']; $display_name = $uploaded['display_name']; // Insert into claim_files $this->claimFilesModel->insert([ 'ticket_id' => $claim_id, 'ticket_type' => 2, 'doc_name' => $document_name, 'file_name' => $display_name, 'url' => $disk_name, 'file_type' => 2, 'mime_type' => getMimeTypeByFileName($display_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()) { $this->myLogger->logme('error', "[NON_EB_API][uploadRequiredDoc] Transaction failed. claim_id: $claim_id, doc: $document_name, user: {$authUser['id']}"); 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; $this->myLogger->logme('error', "[NON_EB_API][uploadRequiredDoc] Doc uploaded. claim_id: $claim_id, doc: $document_name, file: $disk_name, user: {$authUser['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); } }