FEAT_EXPENSE_MODULE_AND_TPA_DUMP_UPLOAD_FIXES
This commit is contained in:
parent
e25f929b01
commit
3765a2d865
@ -32,6 +32,8 @@ class Acl
|
||||
'#^/metaTpaDashboardDemo#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]],
|
||||
'#^/sales/dashboard#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]],
|
||||
'#^/sales#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]],
|
||||
'#^/expense#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]],
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -444,7 +444,9 @@ $routes->group("/util", ["filter" => "authMVC"], function ($routes) {
|
||||
$routes->get('proceedExcelFileDataValidation', 'EmployeeController::proceedExcelFileDataValidation');
|
||||
$routes->get('checkTpaApiEnable', 'EmployeeRestController::checkTpaApiEnable');
|
||||
$routes->get('generateDemographyDataTable', 'LeadsController::generateDemographyDataTable');
|
||||
$routes->post('insufficientCdBalanceHrMailSend', 'EmployeeController::insufficientCdBalanceHrMailSend');
|
||||
$routes->post('insufficientCdBalanceHrMailSend', 'EmployeeController::insufficientCdBalanceHrMailSend');
|
||||
$routes->get('getTpaClaimDumpErrorData/(:any)', 'TicketServiceController::getTpaClaimDumpErrorData/$1');
|
||||
|
||||
});
|
||||
|
||||
$routes->post("policy_tranction/sendInstallmentRemainderMail","PolicyTransactionController::sendInstallmentRemainderMail");
|
||||
@ -1002,4 +1004,13 @@ $routes->group('sales', function($routes) {
|
||||
$routes->get('salesManagerLevelDashboard', 'SalesController::salesManagerLevelDashboard');
|
||||
});
|
||||
|
||||
// Expence Module Route Group
|
||||
$routes->group('expense', ["filter" => "authMVC", 'namespace' => 'App\Controllers'], static function($routes) {
|
||||
$routes->get('/', 'ExpenseController::index');
|
||||
$routes->post('save', 'ExpenseController::save');
|
||||
$routes->get('get/(:num)', 'ExpenseController::getExpense/$1');
|
||||
$routes->post('delete/(:num)', 'ExpenseController::delete/$1');
|
||||
$routes->get('client-policies', 'ExpenseController::clientPolicies');
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -6955,16 +6955,21 @@ class ClientController extends AdminController
|
||||
// $response = $ticketServiceController->getClaimExcelErrorData(["file_id" => 41]);
|
||||
// $response = $ticketServiceController->claimDumpOnBoardProcess(["file_id" => 17]);
|
||||
// $response = $ticketServiceController->extractExcelData("claims_dump_form_client.xlsx");
|
||||
|
||||
// $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 51]); //abhi
|
||||
// $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 50]); //fhpl
|
||||
// $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 53]); //icici
|
||||
// $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 54]); //mediassist
|
||||
// $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 52]); //reliance
|
||||
// $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 48]); //vidal
|
||||
// $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 48]); //vidal
|
||||
// $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 54]); //mediassist
|
||||
|
||||
// $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 4]); //abhi
|
||||
// $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 51]); //abhi
|
||||
// $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 50]); //fhpl
|
||||
// $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 53]); //icici
|
||||
// $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 54]); //mediassist
|
||||
// $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 52]); //reliance
|
||||
// $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 48]); //vidal
|
||||
// $response = $ticketServiceController->getTpaClaimDumpErrorData(["file_id" => 48]);
|
||||
// dd($response);
|
||||
|
||||
// ---------- TICKET CONTROLLER --------------------------------------------------------------------------------
|
||||
|
||||
412
app/Controllers/ExpenseController.php
Normal file
412
app/Controllers/ExpenseController.php
Normal file
@ -0,0 +1,412 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\ExpenseModel;
|
||||
use App\Models\ClientModel;
|
||||
use App\Models\ClientPolicyModel;
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
|
||||
class ExpenseController extends AdminController
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
protected $myLogger;
|
||||
protected $expenseModel;
|
||||
protected $clientModel;
|
||||
protected $clientPolicyModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
set_session_context('Expense');
|
||||
|
||||
$this->myLogger = \Config\Services::mylogger();
|
||||
$this->expenseModel = new ExpenseModel();
|
||||
$this->clientModel = new ClientModel();
|
||||
$this->clientPolicyModel = new ClientPolicyModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Web list + form view
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
try {
|
||||
$data['tab_name'] = 'Expense';
|
||||
$data['page_name'] = 'Expense';
|
||||
$descriptionPattern = '/^[a-zA-Z0-9\s\.\,\-\(\)\/\&\+]+$/u';
|
||||
|
||||
// Raw GET filters
|
||||
$rawFilters = $this->request->getGet() ?? [];
|
||||
$rawFilters = is_array($rawFilters) ? $rawFilters : [];
|
||||
|
||||
// Sanitize input array using existing helper
|
||||
$sanitized = sanitizeInputArrayAdvanced($rawFilters);
|
||||
|
||||
$filters = [
|
||||
'client_id' => trim($sanitized['client_id'] ?? ''),
|
||||
'client_policy_id' => trim($sanitized['client_policy_id'] ?? ''),
|
||||
'approved_by' => trim($sanitized['approved_by'] ?? ''),
|
||||
'description' => trim($sanitized['description'] ?? ''),
|
||||
'amount' => trim($sanitized['amount'] ?? ''),
|
||||
'expense_date' => trim($sanitized['expense_date'] ?? ''),
|
||||
];
|
||||
|
||||
$validationErrors = [];
|
||||
|
||||
// Basic type / format validation for filters
|
||||
if ($filters['client_id'] !== '' && ! ctype_digit($filters['client_id'])) {
|
||||
$validationErrors[] = 'Invalid client selected for search.';
|
||||
$filters['client_id'] = '';
|
||||
}
|
||||
|
||||
if ($filters['client_policy_id'] !== '' && ! ctype_digit($filters['client_policy_id'])) {
|
||||
$validationErrors[] = 'Invalid policy selected for search.';
|
||||
$filters['client_policy_id'] = '';
|
||||
}
|
||||
|
||||
if ($filters['approved_by'] !== '' && ! ctype_digit($filters['approved_by'])) {
|
||||
$validationErrors[] = 'Invalid approver selected for search.';
|
||||
$filters['approved_by'] = '';
|
||||
}
|
||||
|
||||
if ($filters['description'] !== '' && ! preg_match($descriptionPattern, $filters['description'])) {
|
||||
$validationErrors[] = 'Description filter contains invalid characters.';
|
||||
$filters['description'] = '';
|
||||
}
|
||||
|
||||
if ($filters['amount'] !== '') {
|
||||
if (! is_numeric($filters['amount']) || (float) $filters['amount'] < 0) {
|
||||
$validationErrors[] = 'Amount filter must be a non-negative number.';
|
||||
$filters['amount'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
if ($filters['expense_date'] !== '') {
|
||||
$dt = \DateTime::createFromFormat('d-m-Y', $filters['expense_date']);
|
||||
$errors = $dt ? \DateTime::getLastErrors() : ['warning_count' => 1, 'error_count' => 1];
|
||||
if (! $dt || ! empty($errors['warning_count']) || ! empty($errors['error_count'])) {
|
||||
$validationErrors[] = 'Expense Date filter must be in DD-MM-YYYY format.';
|
||||
$filters['expense_date'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
$data['filters'] = $filters;
|
||||
$data['validation_errors'] = $validationErrors;
|
||||
|
||||
// Clients for dropdown
|
||||
$data['clients'] = $this->clientModel
|
||||
->select('id, client_name, short_name')
|
||||
->where('is_active', 1)
|
||||
->orderBy('client_name', 'ASC')
|
||||
->findAll();
|
||||
|
||||
// Approved by (users) dropdown
|
||||
$db = db_connect();
|
||||
$data['approved_users'] = $db->table('user_profiles')
|
||||
->select('id, first_name')
|
||||
->where('is_active', 1)
|
||||
->whereIn('id', [7, 8])
|
||||
->orderBy('first_name', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
// Policies for filter dropdown (when client filter is selected)
|
||||
$data['policies_for_filter'] = [];
|
||||
if ($filters['client_id'] !== '') {
|
||||
$data['policies_for_filter'] = $this->clientPolicyModel
|
||||
->select('id, policy_no')
|
||||
->where('client_id', (int) $filters['client_id'])
|
||||
->where('is_active', 1)
|
||||
->orderBy('policy_no', 'ASC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
// Existing expenses with optional filters
|
||||
$builder = $this->expenseModel
|
||||
->select('
|
||||
expenses.*,
|
||||
clients.client_name,
|
||||
clients.short_name,
|
||||
client_policy.policy_no,
|
||||
user_profiles.first_name AS approved_by_name
|
||||
')
|
||||
->join('clients', 'clients.id = expenses.client_id', 'left')
|
||||
->join('client_policy', 'client_policy.id = expenses.client_policy_id', 'left')
|
||||
->join('user_profiles', 'user_profiles.id = expenses.approved_by', 'left')
|
||||
->where('expenses.is_active', 1);
|
||||
|
||||
if ($filters['client_id'] !== '') {
|
||||
$builder->where('expenses.client_id', (int) $filters['client_id']);
|
||||
}
|
||||
|
||||
if ($filters['client_policy_id'] !== '') {
|
||||
$builder->where('expenses.client_policy_id', (int) $filters['client_policy_id']);
|
||||
}
|
||||
|
||||
if ($filters['approved_by'] !== '') {
|
||||
$builder->where('expenses.approved_by', (int) $filters['approved_by']);
|
||||
}
|
||||
|
||||
if ($filters['description'] !== '') {
|
||||
$builder->like('expenses.description', (string) $filters['description']);
|
||||
}
|
||||
|
||||
if ($filters['amount'] !== '') {
|
||||
$builder->where('expenses.amount', (float) $filters['amount']);
|
||||
}
|
||||
|
||||
if ($filters['expense_date'] !== '') {
|
||||
$dt = \DateTime::createFromFormat('d-m-Y', $filters['expense_date']);
|
||||
if ($dt) {
|
||||
$builder->where('expenses.expense_date', $dt->format('Y-m-d'));
|
||||
}
|
||||
}
|
||||
|
||||
$data['expenses'] = $builder
|
||||
->orderBy('expenses.id', 'DESC')
|
||||
->findAll();
|
||||
|
||||
return $this->loadLayout('expense_list', $data);
|
||||
} catch (\Throwable $e) {
|
||||
return handle_exception($e, $this->myLogger, $this->response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create / update expense (AJAX)
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
try {
|
||||
if ($this->request->getMethod() !== 'post') {
|
||||
return $this->response
|
||||
->setStatusCode(405)
|
||||
->setJSON([
|
||||
'status' => false,
|
||||
'message' => 'Invalid request method',
|
||||
]);
|
||||
}
|
||||
|
||||
$rawData = $this->request->getPost();
|
||||
$data = sanitizeInputArrayAdvanced($rawData);
|
||||
|
||||
$id = isset($data['id']) && $data['id'] !== '' ? (int) $data['id'] : null;
|
||||
|
||||
$rules = [
|
||||
'client_id' => [
|
||||
'rules' => 'required|is_natural_no_zero',
|
||||
'errors' => [
|
||||
'required' => 'Client is required',
|
||||
],
|
||||
],
|
||||
'client_policy_id' => [
|
||||
'rules' => 'required|is_natural_no_zero',
|
||||
'errors' => [
|
||||
'required' => 'Policy is required',
|
||||
],
|
||||
],
|
||||
'description' => [
|
||||
'rules' => 'required|string|min_length[1]|max_length[2000]|regex_match[/^[a-zA-Z0-9\s\.\,\-\(\)\/\&\+]+$/]',
|
||||
'errors' => [
|
||||
'required' => 'Description is required',
|
||||
'min_length' => 'Description cannot be empty',
|
||||
'regex_match' => 'Description contains invalid characters.',
|
||||
],
|
||||
],
|
||||
'expense_date' => [
|
||||
'rules' => 'required|valid_date[d-m-Y]',
|
||||
'errors' => [
|
||||
'required' => 'Expense Date is required',
|
||||
'valid_date' => 'Expense Date must be in DD-MM-YYYY format',
|
||||
],
|
||||
],
|
||||
'approved_by' => [
|
||||
'rules' => 'required|is_natural_no_zero',
|
||||
'errors' => [
|
||||
'required' => 'Approved By is required',
|
||||
],
|
||||
],
|
||||
'amount' => [
|
||||
'rules' => 'required|numeric|greater_than_equal_to[0]',
|
||||
'errors' => [
|
||||
'required' => 'Amount is required',
|
||||
'numeric' => 'Amount must be numeric',
|
||||
'greater_than_equal_to' => 'Amount cannot be negative',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (! $this->validate($rules)) {
|
||||
return $this->response->setStatusCode(400)->setJSON([
|
||||
'status' => false,
|
||||
'message' => 'Input validation failed',
|
||||
'errors' => $this->validator->getErrors(),
|
||||
]);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'client_id' => (int) ($data['client_id'] ?? 0),
|
||||
'client_policy_id' => (int) ($data['client_policy_id'] ?? 0),
|
||||
'description' => $data['description'] ?? null,
|
||||
'approved_by' => (int) ($data['approved_by'] ?? 0),
|
||||
'amount' => $data['amount'] ?? null,
|
||||
'is_active' => 1,
|
||||
];
|
||||
|
||||
$expenseDate = $data['expense_date'] ?? null;
|
||||
if (! empty($expenseDate)) {
|
||||
$dt = \DateTime::createFromFormat('d-m-Y', $expenseDate);
|
||||
$payload['expense_date'] = $dt ? $dt->format('Y-m-d') : null;
|
||||
} else {
|
||||
$payload['expense_date'] = null;
|
||||
}
|
||||
|
||||
if ($id === null) {
|
||||
$insertId = $this->expenseModel->insert($payload, true);
|
||||
$success = ! empty($insertId);
|
||||
$id = $insertId;
|
||||
$message = $success
|
||||
? 'Expense created successfully'
|
||||
: 'Unable to create expense. Please try again.';
|
||||
} else {
|
||||
$success = $this->expenseModel->update($id, $payload);
|
||||
$message = $success
|
||||
? 'Expense updated successfully'
|
||||
: 'Unable to update expense. Please try again.';
|
||||
}
|
||||
|
||||
return $this->response
|
||||
->setStatusCode($success ? 200 : 400)
|
||||
->setJSON([
|
||||
'status' => (bool) $success,
|
||||
'message' => $message,
|
||||
'id' => $id,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return handle_exception($e, $this->myLogger, $this->response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get single expense (AJAX)
|
||||
*/
|
||||
public function getExpense($id = null)
|
||||
{
|
||||
try {
|
||||
$id = (int) $id;
|
||||
|
||||
if (empty($id)) {
|
||||
return $this->response->setStatusCode(400)->setJSON([
|
||||
'status' => false,
|
||||
'message' => 'Invalid expense id',
|
||||
]);
|
||||
}
|
||||
|
||||
$expense = $this->expenseModel
|
||||
->select('
|
||||
expenses.*,
|
||||
clients.client_name,
|
||||
clients.short_name,
|
||||
client_policy.policy_no,
|
||||
user_profiles.first_name AS approved_by_name
|
||||
')
|
||||
->join('clients', 'clients.id = expenses.client_id', 'left')
|
||||
->join('client_policy', 'client_policy.id = expenses.client_policy_id', 'left')
|
||||
->join('user_profiles', 'user_profiles.id = expenses.approved_by', 'left')
|
||||
->where('expenses.id', $id)
|
||||
->where('expenses.is_active', 1)
|
||||
->first();
|
||||
|
||||
if (empty($expense)) {
|
||||
return $this->response->setStatusCode(404)->setJSON([
|
||||
'status' => false,
|
||||
'message' => 'Expense not found',
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->response->setStatusCode(200)->setJSON([
|
||||
'status' => true,
|
||||
'data' => $expense,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return handle_exception($e, $this->myLogger, $this->response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft delete expense (AJAX)
|
||||
*/
|
||||
public function delete($id = null)
|
||||
{
|
||||
try {
|
||||
if ($this->request->getMethod() !== 'post') {
|
||||
return $this->response
|
||||
->setStatusCode(405)
|
||||
->setJSON([
|
||||
'status' => false,
|
||||
'message' => 'Invalid request method',
|
||||
]);
|
||||
}
|
||||
|
||||
$id = (int) $id;
|
||||
|
||||
if (empty($id)) {
|
||||
return $this->response->setStatusCode(400)->setJSON([
|
||||
'status' => false,
|
||||
'message' => 'Invalid expense id',
|
||||
]);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'is_active' => 0,
|
||||
];
|
||||
|
||||
$success = $this->expenseModel->update($id, $payload);
|
||||
|
||||
return $this->response
|
||||
->setStatusCode($success ? 200 : 400)
|
||||
->setJSON([
|
||||
'status' => (bool) $success,
|
||||
'message' => $success
|
||||
? 'Expense deleted successfully'
|
||||
: 'Unable to delete expense. Please try again.',
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return handle_exception($e, $this->myLogger, $this->response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get policies by client for dropdown (AJAX)
|
||||
*/
|
||||
public function clientPolicies()
|
||||
{
|
||||
try {
|
||||
$clientId = (int) ($this->request->getGet('client_id') ?? 0);
|
||||
|
||||
if (empty($clientId)) {
|
||||
return $this->response->setStatusCode(400)->setJSON([
|
||||
'status' => false,
|
||||
'message' => 'Client is required',
|
||||
]);
|
||||
}
|
||||
|
||||
$policies = $this->clientPolicyModel
|
||||
->select('client_policy.id, client_policy.policy_no, policy_type.policy_type')
|
||||
->join('policy_type', 'policy_type.id = client_policy.policy_type_id AND policy_type.is_active = 1')
|
||||
->where('client_policy.client_id', $clientId)
|
||||
->where('client_policy.is_active', 1)
|
||||
->orderBy('client_policy.id', 'ASC')
|
||||
->findAll();
|
||||
|
||||
return $this->response->setStatusCode(200)->setJSON([
|
||||
'status' => true,
|
||||
'data' => $policies,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return handle_exception($e, $this->myLogger, $this->response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,8 +30,9 @@ use App\Models\TicketMasterModel;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||
|
||||
class TicketServiceController extends BaseController
|
||||
class TicketServiceController extends AdminController
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
@ -1840,39 +1841,86 @@ class TicketServiceController extends BaseController
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// --------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
public function tpaClaimDumpImporter($params)
|
||||
/**
|
||||
* Resolve and validate Claim Dump file metadata and physical file for TPA imports.
|
||||
*
|
||||
* @param int|null $fileId
|
||||
* @return array{status:bool,message?:string,fileData?:array,filePath?:string}
|
||||
*/
|
||||
private function resolveTpaClaimDumpFile(?int $fileId): array
|
||||
{
|
||||
$file_id = $params['file_id'] ?? null; // Move outside try to ensure catch can see it
|
||||
if (empty($fileId)) {
|
||||
return [
|
||||
'status' => false,
|
||||
'message' => 'File ID is missing',
|
||||
];
|
||||
}
|
||||
|
||||
$fileData = $this->claimDumpFileModel->find((int) $fileId);
|
||||
if (!$fileData) {
|
||||
return [
|
||||
'status' => false,
|
||||
'message' => 'Invalid file ID. No file data found',
|
||||
];
|
||||
}
|
||||
|
||||
$filePath = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'claim_dump_excel' . DIRECTORY_SEPARATOR . $fileData['file_name'];
|
||||
|
||||
if (!is_file($filePath)) {
|
||||
return [
|
||||
'status' => false,
|
||||
'message' => 'Claim dump file not found',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => true,
|
||||
'fileData' => $fileData,
|
||||
'filePath' => $filePath,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* First TPA-wise job – parse Excel and push data into TPA staging table.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array
|
||||
*/
|
||||
public function tpaClaimDumpImporter(array $params)
|
||||
{
|
||||
$file_id = isset($params['file_id']) ? (int) $params['file_id'] : null;
|
||||
|
||||
try {
|
||||
$file_path = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'claim_dump_excel' . DIRECTORY_SEPARATOR;
|
||||
$resolved = $this->resolveTpaClaimDumpFile($file_id);
|
||||
|
||||
if (!$file_id) {
|
||||
return ['status' => false, 'message' => 'File ID is missing'];
|
||||
if ($resolved['status'] === false) {
|
||||
return [
|
||||
'status' => false,
|
||||
'message' => $resolved['message'] ?? 'Unable to resolve claim dump file',
|
||||
];
|
||||
}
|
||||
|
||||
$fileData = $this->claimDumpFileModel->find((int)$file_id);
|
||||
if (!$fileData) {
|
||||
return ['status' => false, 'message' => 'Invalid file ID. No file data found'];
|
||||
}
|
||||
|
||||
$file_full_path = $file_path . $fileData['file_name'];
|
||||
if (!is_file($file_full_path)) {
|
||||
return ['status' => false, 'message' => 'Claim dump file not found'];
|
||||
}
|
||||
$fileData = $resolved['fileData'];
|
||||
$filePath = $resolved['filePath'];
|
||||
|
||||
$handler = TpaClaimsImportFactory::make($fileData['tpa_id'] ?? 0);
|
||||
$result = $handler->runTpaClaimDumpInsert($file_full_path, $file_id);
|
||||
$result = $handler->runTpaClaimDumpInsert($filePath, $file_id);
|
||||
|
||||
if (!empty($result['status']) && $result['status'] === true) {
|
||||
Jobs::addJob(['job_name' => 'tpaClaimDumpToTicketMasterImporters', 'payload' => ['file_id' => $file_id]]);
|
||||
Jobs::addJob([
|
||||
'job_name' => 'tpaClaimDumpToTicketMasterImporters',
|
||||
'payload' => ['file_id' => $file_id],
|
||||
]);
|
||||
} else {
|
||||
// FORCE FAIL LOGIC
|
||||
$this->markAsFailed($file_id, $result['message'] ?? 'System error contact admin', $fileData['created_by'] ?? null);
|
||||
$this->markAsFailed(
|
||||
$file_id,
|
||||
$result['message'] ?? 'System error contact admin',
|
||||
$fileData['created_by'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
@ -1884,9 +1932,9 @@ class TicketServiceController extends BaseController
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => false,
|
||||
'message' => 'TPA Claim dump import failed',
|
||||
'error_data' => $th->getMessage()
|
||||
'status' => false,
|
||||
'message' => 'TPA Claim dump import failed',
|
||||
'error_data' => $th->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1908,30 +1956,37 @@ class TicketServiceController extends BaseController
|
||||
return $this->claimDumpFileModel->update($file_id, $data);
|
||||
}
|
||||
|
||||
public function tpaClaimDumpToTicketMasterImporters($params)
|
||||
/**
|
||||
* Second TPA-wise job – move data from TPA staging into ticket_master.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array
|
||||
*/
|
||||
public function tpaClaimDumpToTicketMasterImporters(array $params)
|
||||
{
|
||||
$file_id = $params['file_id'] ?? null;
|
||||
$file_id = isset($params['file_id']) ? (int) $params['file_id'] : null;
|
||||
$fileData = null;
|
||||
|
||||
try {
|
||||
if (!$file_id) {
|
||||
return ['status' => false, 'message' => 'File ID is missing'];
|
||||
$resolved = $this->resolveTpaClaimDumpFile($file_id);
|
||||
|
||||
if ($resolved['status'] === false) {
|
||||
return [
|
||||
'status' => false,
|
||||
'message' => $resolved['message'] ?? 'Unable to resolve claim dump file',
|
||||
];
|
||||
}
|
||||
|
||||
$fileData = $this->claimDumpFileModel->where('id', $file_id)->first();
|
||||
|
||||
if (!$fileData) {
|
||||
return ['status' => false, 'message' => 'Invalid file ID. No file data found to import'];
|
||||
}
|
||||
$fileData = $resolved['fileData'];
|
||||
|
||||
$handler = TpaClaimsImportFactory::make($fileData['tpa_id'] ?? 0);
|
||||
$result = $handler->runTicketMasterInsert($params);
|
||||
$result = $handler->runTicketMasterInsert($params);
|
||||
|
||||
if (!empty($result['status']) && $result['status'] === true) {
|
||||
// Success: Update the status to success
|
||||
$this->claimDumpFileModel->update($file_id, [
|
||||
'status' => 'success',
|
||||
'reason' => null
|
||||
'reason' => null,
|
||||
]);
|
||||
} else {
|
||||
// Logic failure: The runTicketMasterInsert returned status false
|
||||
@ -1957,8 +2012,8 @@ class TicketServiceController extends BaseController
|
||||
'message' => $th->getMessage(),
|
||||
'error_data' => [
|
||||
'line' => $th->getLine(),
|
||||
'file' => $th->getFile()
|
||||
]
|
||||
'file' => $th->getFile(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -2011,5 +2066,176 @@ class TicketServiceController extends BaseController
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
public function getTpaClaimDumpErrorData($file_id)
|
||||
{
|
||||
$file_id = (int) $file_id;
|
||||
|
||||
// Ensure we always have a Response object, even if controller was instantiated manually
|
||||
$response = $this->response ?? service('response');
|
||||
|
||||
if ($file_id <= 0) {
|
||||
return $response
|
||||
->setStatusCode(ResponseInterface::HTTP_BAD_REQUEST)
|
||||
->setJSON(['status' => false, 'message' => 'Invalid file id']);
|
||||
}
|
||||
|
||||
$fileData = $this->claimDumpFileModel->find($file_id);
|
||||
|
||||
if (!$fileData) {
|
||||
return $response
|
||||
->setStatusCode(ResponseInterface::HTTP_NOT_FOUND)
|
||||
->setJSON(['status' => false, 'message' => 'File record not found']);
|
||||
}
|
||||
|
||||
$tpaId = (int) ($fileData['tpa_id'] ?? 0);
|
||||
|
||||
if ($tpaId <= 0) {
|
||||
return $response
|
||||
->setStatusCode(ResponseInterface::HTTP_BAD_REQUEST)
|
||||
->setJSON(['status' => false, 'message' => 'TPA not linked with this file']);
|
||||
}
|
||||
|
||||
// Resolve TPA staging table based on configured TPA IDs
|
||||
$tableName = match ($tpaId) {
|
||||
(int) env('VIDAL_PRIMARY_KEY_CONSTANT') => 'claims_dump_vidal',
|
||||
(int) env('ABHI_PRIMARY_KEY_CONSTANT') => 'claims_dump_abhi',
|
||||
(int) env('MEDI_ASSIST_PRIMARY_KEY_CONSTANT') => 'claims_dump_medi_assist',
|
||||
(int) env('FHPL_PRIMARY_KEY_CONSTANT') => 'claims_dump_fhpl',
|
||||
(int) env('R_CARE_PRIMARY_KEY_CONSTANT') => 'claims_dump_reliance',
|
||||
(int) env('ICICI_PRIMARY_KEY_CONSTANT') => 'claims_dump_icici',
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($tableName === null) {
|
||||
return $response
|
||||
->setStatusCode(ResponseInterface::HTTP_BAD_REQUEST)
|
||||
->setJSON(['status' => false, 'message' => 'Unsupported TPA for error dump export']);
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$builder = $db->table($tableName);
|
||||
|
||||
// Fetch only records belonging to this file and having a rejection reason
|
||||
$rows = $builder
|
||||
->where('file_id', $file_id)
|
||||
->where('is_active', 1)
|
||||
->where('master_reject_reason IS NOT NULL', null, false)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
if (empty($rows)) {
|
||||
// No error records – return a small Excel file with just a message
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->setTitle('Errors');
|
||||
$sheet->setCellValue('A1', 'Message');
|
||||
$sheet->setCellValue('A2', 'No rejected records found for this file.');
|
||||
|
||||
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
|
||||
ob_start();
|
||||
$writer->save('php://output');
|
||||
$excelOutput = ob_get_clean();
|
||||
|
||||
$filename = 'tpa_claim_dump_errors_' . $file_id . '.xlsx';
|
||||
|
||||
return $response
|
||||
->setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||
->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"')
|
||||
->setHeader('Cache-Control', 'max-age=0')
|
||||
->setBody($excelOutput);
|
||||
}
|
||||
|
||||
// Columns that must NOT be included in the Excel export
|
||||
$excludedColumns = [
|
||||
'id',
|
||||
'client_id',
|
||||
'client_policy_id',
|
||||
'ticket_id',
|
||||
'file_id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
$firstRow = $rows[0];
|
||||
|
||||
// Build header mapping and track the "Rejected Reason" column index
|
||||
$dbColumnOrder = [];
|
||||
$displayHeaderLabels = [];
|
||||
$rejectedReasonColIdx = null; // 1-based index for Excel column
|
||||
|
||||
foreach ($firstRow as $columnName => $_) {
|
||||
if (in_array($columnName, $excludedColumns, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dbColumnOrder[] = $columnName;
|
||||
|
||||
if ($columnName === 'master_reject_reason' || $columnName === 'master_rejection_reason_key') {
|
||||
$displayHeaderLabels[] = 'Rejected Reason';
|
||||
$rejectedReasonColIdx = count($displayHeaderLabels); // current column index (1-based)
|
||||
} else {
|
||||
$displayHeaderLabels[] = $columnName;
|
||||
}
|
||||
}
|
||||
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->setTitle('Errors');
|
||||
|
||||
// Header row
|
||||
$rowIndex = 1;
|
||||
foreach ($displayHeaderLabels as $colIndex => $headerLabel) {
|
||||
$columnLetter = Coordinate::stringFromColumnIndex($colIndex + 1);
|
||||
$cellAddress = $columnLetter . $rowIndex;
|
||||
$sheet->setCellValue($cellAddress, $headerLabel);
|
||||
}
|
||||
|
||||
// Data rows
|
||||
$rowIndex = 2;
|
||||
foreach ($rows as $row) {
|
||||
foreach ($dbColumnOrder as $i => $columnName) {
|
||||
$colIndex = $i + 1;
|
||||
$columnLetter = Coordinate::stringFromColumnIndex($colIndex);
|
||||
$cellAddress = $columnLetter . $rowIndex;
|
||||
$value = $row[$columnName] ?? null;
|
||||
|
||||
$sheet->setCellValue($cellAddress, $value);
|
||||
|
||||
// Highlight the "Rejected Reason" values
|
||||
if ($rejectedReasonColIdx !== null && $colIndex === $rejectedReasonColIdx) {
|
||||
$sheet->getStyle($cellAddress)
|
||||
->getFill()
|
||||
->setFillType(Fill::FILL_SOLID)
|
||||
->getStartColor()
|
||||
->setARGB('FFFFF4B2'); // light yellow
|
||||
}
|
||||
}
|
||||
|
||||
$rowIndex++;
|
||||
}
|
||||
|
||||
// Autosize columns for better readability
|
||||
$highestColumnIndex = count($displayHeaderLabels);
|
||||
for ($col = 1; $col <= $highestColumnIndex; $col++) {
|
||||
$columnLetter = Coordinate::stringFromColumnIndex($col);
|
||||
$sheet->getColumnDimension($columnLetter)->setAutoSize(true);
|
||||
}
|
||||
|
||||
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
|
||||
ob_start();
|
||||
$writer->save('php://output');
|
||||
$excelOutput = ob_get_clean();
|
||||
|
||||
$filename = 'tpa_claim_dump_errors_' . $file_id . '.xlsx';
|
||||
|
||||
return $response
|
||||
->setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||
->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"')
|
||||
->setHeader('Cache-Control', 'max-age=0')
|
||||
->setBody($excelOutput);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -119,6 +119,19 @@ class AbhiClaimImportService extends BaseTpaClaimImportService
|
||||
'Settled' => 11,
|
||||
'Rejected' => 8,
|
||||
'Cancelled' => 13,
|
||||
'Approved' => 9,
|
||||
'Closed' => 12,
|
||||
'Query' => 4,
|
||||
'Required Information' => 4,
|
||||
'In-Progress' => 5,
|
||||
'Paid' => 11,
|
||||
'Denied' => 8,
|
||||
'Cancelled' => 13,
|
||||
'Processed' => 61,
|
||||
'Information Awaited' => 4,
|
||||
'Denied Letter Sent' => 66,
|
||||
'Cashless Document Awaited' => 3,
|
||||
'RI Cancelled' => 13,
|
||||
];
|
||||
|
||||
protected $dateColumns = [
|
||||
@ -164,14 +177,30 @@ class AbhiClaimImportService extends BaseTpaClaimImportService
|
||||
}
|
||||
|
||||
|
||||
public function updateTicketIdInTPATable(): bool
|
||||
public function updateTicketIdInTPATable(int $fileId): bool
|
||||
{
|
||||
if (empty($data)) {
|
||||
return false;
|
||||
$rows = $this->db->table('claims_dump_abhi cd')
|
||||
->select('cd.id, tm.id AS ticket_id')
|
||||
->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner')
|
||||
->where('cd.is_active', 1)
|
||||
->where('cd.file_id', $fileId)
|
||||
->where('cd.ticket_id IS NULL')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
if (empty($rows)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$builder = $this->db->table('claims_dump_abhi');
|
||||
return $builder->updateBatch($data, 'id');
|
||||
$updateData = [];
|
||||
foreach ($rows as $row) {
|
||||
$updateData[] = [
|
||||
'id' => $row['id'],
|
||||
'ticket_id' => $row['ticket_id'],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->db->table('claims_dump_abhi')->updateBatch($updateData, 'id') !== false;
|
||||
}
|
||||
|
||||
|
||||
@ -329,7 +358,7 @@ class AbhiClaimImportService extends BaseTpaClaimImportService
|
||||
}
|
||||
|
||||
// Meta fields
|
||||
$item['claim_status_id'] = $statusMapping[$row['claim_status']] ?? 61;
|
||||
$item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['claim_status']);
|
||||
$item['file_id'] = $file_id;
|
||||
$item['claim_dump_ref_id'] = $row['id'];
|
||||
$item['created_by'] = $file_data['created_by'] ?? null;
|
||||
|
||||
@ -81,7 +81,7 @@ abstract class BaseTpaClaimImportService
|
||||
$this->db->transBegin();
|
||||
|
||||
try {
|
||||
$file_id = $params['file_id'];
|
||||
$file_id = $params['file_id'];
|
||||
$ticketMasterData = $this->mapClaimMasterData($file_id);
|
||||
|
||||
// Check if mapping failed
|
||||
@ -101,6 +101,12 @@ abstract class BaseTpaClaimImportService
|
||||
$this->db->transRollback();
|
||||
return ['status' => false, 'message' => 'Ticket Master Claim bulk insert failed'];
|
||||
}
|
||||
// Map newly created ticket IDs back to the TPA staging table
|
||||
if (!$this->updateTicketIdInTPATable($file_id)) {
|
||||
$this->db->transRollback();
|
||||
return ['status' => false, 'message' => 'Updating ticket_id in TPA table failed'];
|
||||
}
|
||||
|
||||
$message .= 'Ticket Master Claim bulk insert success. ';
|
||||
$hasExecutedTask = true;
|
||||
}else{
|
||||
@ -328,6 +334,16 @@ abstract class BaseTpaClaimImportService
|
||||
return $employeeData ?? [];
|
||||
}
|
||||
|
||||
public function checkStatusMapping($statusArray, $statusString)
|
||||
{
|
||||
foreach ($statusArray as $key => $value) {
|
||||
if (strtolower($statusString) == strtolower($key) || strtolower($statusString) == strtolower(trim($key))) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Excel rows to TPA table structure
|
||||
*/
|
||||
@ -349,9 +365,9 @@ abstract class BaseTpaClaimImportService
|
||||
abstract protected function importClaimMaster(array $data): bool;
|
||||
|
||||
/**
|
||||
* Update TPA table with ticket_master primary key
|
||||
* Update TPA table with ticket_master primary key for a given file.
|
||||
*/
|
||||
abstract protected function updateTicketIdInTPATable(): bool;
|
||||
abstract protected function updateTicketIdInTPATable(int $fileId): bool;
|
||||
|
||||
/**
|
||||
* Update TPA table with ticket_master insert rejected reason
|
||||
|
||||
@ -181,6 +181,13 @@ class FhplClaimImportService extends BaseTpaClaimImportService
|
||||
protected $statusMapping = [
|
||||
'Settled' => 11,
|
||||
'Rejected' => 8,
|
||||
'Paid' => 11,
|
||||
'Approved' => 9,
|
||||
'Closed' => 12,
|
||||
'Under Process' => 5,
|
||||
'Query' => 4,
|
||||
'Required Information' => 4,
|
||||
'In-Progress' => 5,
|
||||
];
|
||||
|
||||
protected $dateColumns = [
|
||||
@ -235,14 +242,31 @@ class FhplClaimImportService extends BaseTpaClaimImportService
|
||||
}
|
||||
|
||||
|
||||
public function updateTicketIdInTPATable(): bool
|
||||
public function updateTicketIdInTPATable(int $fileId): bool
|
||||
{
|
||||
if (empty($data)) {
|
||||
return false;
|
||||
// Join ticket_master with FHPL staging on file_id + claim_dump_ref_id -> id
|
||||
$rows = $this->db->table('claims_dump_fhpl cd')
|
||||
->select('cd.id, tm.id AS ticket_id')
|
||||
->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner')
|
||||
->where('cd.is_active', 1)
|
||||
->where('cd.file_id', $fileId)
|
||||
->where('cd.ticket_id IS NULL')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
if (empty($rows)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$builder = $this->db->table('claims_dump_fhpl');
|
||||
return $builder->updateBatch($data, 'id');
|
||||
|
||||
$updateData = [];
|
||||
foreach ($rows as $row) {
|
||||
$updateData[] = [
|
||||
'id' => $row['id'],
|
||||
'ticket_id' => $row['ticket_id'],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->db->table('claims_dump_fhpl')->updateBatch($updateData, 'id') !== false;
|
||||
}
|
||||
|
||||
|
||||
@ -400,7 +424,7 @@ class FhplClaimImportService extends BaseTpaClaimImportService
|
||||
}
|
||||
|
||||
// Meta fields
|
||||
$item['claim_status_id'] = $statusMapping[$row['current_claim_status']] ?? 61;
|
||||
$item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['current_claim_status']) ?? 61;
|
||||
$item['file_id'] = $file_id;
|
||||
$item['claim_dump_ref_id'] = $row['id'];
|
||||
$item['created_by'] = $file_data['created_by'] ?? null;
|
||||
|
||||
@ -106,7 +106,13 @@ class IciciClaimImportService extends BaseTpaClaimImportService
|
||||
|
||||
protected $statusMapping = [
|
||||
'PAID' => 11,
|
||||
'SETTLED' => 11,
|
||||
'REJECTED' => 8,
|
||||
'APPROVED' => 9,
|
||||
'CLOSED' => 12,
|
||||
'QUERY' => 4,
|
||||
'REQUIRED INFORMATION' => 4,
|
||||
'IN-PROGRESS' => 5,
|
||||
];
|
||||
|
||||
protected $dateColumns = [
|
||||
@ -152,14 +158,30 @@ class IciciClaimImportService extends BaseTpaClaimImportService
|
||||
}
|
||||
|
||||
|
||||
public function updateTicketIdInTPATable(): bool
|
||||
public function updateTicketIdInTPATable(int $fileId): bool
|
||||
{
|
||||
if (empty($data)) {
|
||||
return false;
|
||||
$rows = $this->db->table('claims_dump_icici cd')
|
||||
->select('cd.id, tm.id AS ticket_id')
|
||||
->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner')
|
||||
->where('cd.is_active', 1)
|
||||
->where('cd.file_id', $fileId)
|
||||
->where('cd.ticket_id IS NULL')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
if (empty($rows)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$builder = $this->db->table('claims_dump_icici');
|
||||
return $builder->upsertBatch($data, 'id');
|
||||
|
||||
$updateData = [];
|
||||
foreach ($rows as $row) {
|
||||
$updateData[] = [
|
||||
'id' => $row['id'],
|
||||
'ticket_id' => $row['ticket_id'],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->db->table('claims_dump_icici')->updateBatch($updateData, 'id') !== false;
|
||||
}
|
||||
|
||||
|
||||
@ -287,7 +309,7 @@ class IciciClaimImportService extends BaseTpaClaimImportService
|
||||
$item['tpa_id'] = $client_policy_data['tpa_id'] ?? null;
|
||||
$item['acm_id'] = $client_policy_data['acm_id'] ?? null;
|
||||
$item['policy_no'] = $client_policy_data['policy_no'] ?? null;
|
||||
$item['relationship'] = $this->convertRelation($row['relation_group'] ?? '');
|
||||
$item['relationship'] = $this->convertRelation($row['relation'] ?? '');
|
||||
|
||||
$employee_data = $this->getEmployeeDetails($file_data['client_id'], $file_data['client_policy_id'], $row['employee_member_id'], $item['relationship']);
|
||||
|
||||
@ -317,7 +339,7 @@ class IciciClaimImportService extends BaseTpaClaimImportService
|
||||
}
|
||||
|
||||
// Meta fields
|
||||
$item['claim_status_id'] = $statusMapping[$row['updated_status']] ?? 61;
|
||||
$item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['updated_status']) ?? 61;
|
||||
$item['claim_dump_ref_id'] = $row['id'];
|
||||
$item['file_id'] = $file_id;
|
||||
$item['created_by'] = $file_data['created_by'] ?? null;
|
||||
|
||||
@ -158,12 +158,15 @@ class MediAssistClaimImportService extends BaseTpaClaimImportService
|
||||
protected $statusMapping = [
|
||||
'Settled' => 11,
|
||||
'Rejected' => 8,
|
||||
'Paid' => 11,
|
||||
'Denied' => 8,
|
||||
'Cancelled' => 13,
|
||||
'Processed' => 61,
|
||||
'Information Awaited' => 4,
|
||||
'Denied Letter Sent' => 66,
|
||||
'Cashless Document Awaited' => 3,
|
||||
'Approved' => 9,
|
||||
'Closed' => 12,
|
||||
];
|
||||
|
||||
/**
|
||||
@ -192,16 +195,30 @@ class MediAssistClaimImportService extends BaseTpaClaimImportService
|
||||
}
|
||||
|
||||
|
||||
public function updateTicketIdInTPATable(): bool
|
||||
public function updateTicketIdInTPATable(int $fileId): bool
|
||||
{
|
||||
if (empty($data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$builder = $this->db->table('claims_dump_medi_assist');
|
||||
$builder->insertBatch($data);
|
||||
$rows = $this->db->table('claims_dump_medi_assist cd')
|
||||
->select('cd.id, tm.id AS ticket_id')
|
||||
->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner')
|
||||
->where('cd.is_active', 1)
|
||||
->where('cd.file_id', $fileId)
|
||||
->where('cd.ticket_id IS NULL')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
return true;
|
||||
if (empty($rows)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$updateData = [];
|
||||
foreach ($rows as $row) {
|
||||
$updateData[] = [
|
||||
'id' => $row['id'],
|
||||
'ticket_id' => $row['ticket_id'],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->db->table('claims_dump_medi_assist')->updateBatch($updateData, 'id') !== false;
|
||||
}
|
||||
|
||||
|
||||
@ -356,8 +373,8 @@ class MediAssistClaimImportService extends BaseTpaClaimImportService
|
||||
|
||||
// Meta fields
|
||||
$item['claim_dump_ref_id'] = $row['id'];
|
||||
$item['file_id'] = $file_id;
|
||||
$item['claim_status_id'] = $statusMapping[$row['claim_status']] ?? 61;
|
||||
$item['file_id'] = $file_id;
|
||||
$item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['claim_status']) ?? 61;
|
||||
$item['created_by'] = $file_data['created_by'] ?? null;
|
||||
$item['claim_type'] = 1;
|
||||
$item['priority'] = 1;
|
||||
|
||||
@ -96,14 +96,15 @@ class RcareClaimImportService extends BaseTpaClaimImportService
|
||||
];
|
||||
|
||||
protected $statusMapping = [
|
||||
'CL Paid with Settlement Letter' => 11,
|
||||
'Settled' => 11,
|
||||
'Paid' => 11,
|
||||
'Rejected' => 8,
|
||||
'Denied' => 8,
|
||||
'Cancelled' => 13,
|
||||
'Processed' => 61,
|
||||
'Information Awaited' => 4,
|
||||
'Denied Letter Sent' => 66,
|
||||
'Cashless Document Awaited' => 3,
|
||||
'Approved' => 9,
|
||||
'Closed' => 12,
|
||||
'CL Rejected' => 8,
|
||||
'CL Approved' => 9,
|
||||
'AL Closed' => 12,
|
||||
];
|
||||
|
||||
protected $dateColumns = [
|
||||
@ -143,14 +144,30 @@ class RcareClaimImportService extends BaseTpaClaimImportService
|
||||
}
|
||||
|
||||
|
||||
public function updateTicketIdInTPATable(): bool
|
||||
public function updateTicketIdInTPATable(int $fileId): bool
|
||||
{
|
||||
if (empty($data)) {
|
||||
return false;
|
||||
$rows = $this->db->table('claims_dump_reliance cd')
|
||||
->select('cd.id, tm.id AS ticket_id')
|
||||
->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner')
|
||||
->where('cd.is_active', 1)
|
||||
->where('cd.file_id', $fileId)
|
||||
->where('cd.ticket_id IS NULL')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
if (empty($rows)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$builder = $this->db->table('claims_dump_reliance');
|
||||
return $builder->updateBatch($data, 'id');
|
||||
|
||||
$updateData = [];
|
||||
foreach ($rows as $row) {
|
||||
$updateData[] = [
|
||||
'id' => $row['id'],
|
||||
'ticket_id' => $row['ticket_id'],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->db->table('claims_dump_reliance')->updateBatch($updateData, 'id') !== false;
|
||||
}
|
||||
|
||||
|
||||
@ -307,7 +324,7 @@ class RcareClaimImportService extends BaseTpaClaimImportService
|
||||
}
|
||||
|
||||
// Meta fields
|
||||
$item['claim_status_id'] = $statusMapping[$row['final_status']] ?? 61;
|
||||
$item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['final_status']) ?? 61;
|
||||
$item['file_id'] = $file_id;
|
||||
$item['created_by'] = $file_data['created_by'] ?? null;
|
||||
$item['claim_dump_ref_id'] = $row['id'];
|
||||
|
||||
@ -349,10 +349,17 @@ class VidalClaimImportService extends BaseTpaClaimImportService
|
||||
];
|
||||
|
||||
protected $statusMapping = [
|
||||
'CL Paid with Settlement Letter' => 11,
|
||||
'CL Rejected' => 8,
|
||||
'CL Approved' => 9,
|
||||
'AL Closed' => 12,
|
||||
'Settled' => 11,
|
||||
'Rejected' => 8,
|
||||
'Paid' => 11,
|
||||
'Denied' => 8,
|
||||
'Cancelled' => 13,
|
||||
'Processed' => 61,
|
||||
'Information Awaited' => 4,
|
||||
'Denied Letter Sent' => 66,
|
||||
'Cashless Document Awaited' => 3,
|
||||
'Approved' => 9,
|
||||
'Closed' => 12,
|
||||
];
|
||||
|
||||
|
||||
@ -382,14 +389,30 @@ class VidalClaimImportService extends BaseTpaClaimImportService
|
||||
}
|
||||
|
||||
|
||||
public function updateTicketIdInTPATable(): bool
|
||||
public function updateTicketIdInTPATable(int $fileId): bool
|
||||
{
|
||||
if (empty($data)) {
|
||||
return false;
|
||||
$rows = $this->db->table('claims_dump_vidal cd')
|
||||
->select('cd.id, tm.id AS ticket_id')
|
||||
->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner')
|
||||
->where('cd.is_active', 1)
|
||||
->where('cd.file_id', $fileId)
|
||||
->where('cd.ticket_id IS NULL')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
if (empty($rows)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$builder = $this->db->table('claims_dump_vidal');
|
||||
return $builder->updateBatch($data, 'id');
|
||||
|
||||
$updateData = [];
|
||||
foreach ($rows as $row) {
|
||||
$updateData[] = [
|
||||
'id' => $row['id'],
|
||||
'ticket_id' => $row['ticket_id'],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->db->table('claims_dump_vidal')->updateBatch($updateData, 'id') !== false;
|
||||
}
|
||||
|
||||
|
||||
@ -542,7 +565,7 @@ class VidalClaimImportService extends BaseTpaClaimImportService
|
||||
}
|
||||
|
||||
// Meta fields
|
||||
$item['claim_status_id'] = $statusMapping[$row['claim_status']] ?? 61;
|
||||
$item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['claim_status']) ?? 61;
|
||||
$item['file_id'] = $file_id;
|
||||
$item['claim_dump_ref_id'] = $row['id'];
|
||||
$item['created_by'] = $file_data['created_by'] ?? null;
|
||||
|
||||
73
app/Models/ExpenseModel.php
Normal file
73
app/Models/ExpenseModel.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class ExpenseModel extends Model
|
||||
{
|
||||
protected $table = 'expenses';
|
||||
protected $primaryKey = 'id';
|
||||
protected $DBGroup = 'default';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
|
||||
protected $allowedFields = [
|
||||
'id',
|
||||
'client_id',
|
||||
'client_policy_id',
|
||||
'description',
|
||||
'approved_by',
|
||||
'amount',
|
||||
'expense_date',
|
||||
'created_at',
|
||||
'created_by',
|
||||
'updated_at',
|
||||
'updated_by',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
// Callbacks
|
||||
protected $allowCallbacks = true;
|
||||
protected $beforeInsert = ['setCreatedBy'];
|
||||
protected $afterInsert = [];
|
||||
protected $beforeUpdate = ['setUpdatedBy'];
|
||||
protected $afterUpdate = [];
|
||||
protected $beforeFind = [];
|
||||
protected $afterFind = [];
|
||||
protected $beforeDelete = [];
|
||||
protected $afterDelete = [];
|
||||
|
||||
protected function setCreatedBy(array $data): array
|
||||
{
|
||||
if (empty($data['data']['created_by'])) {
|
||||
$data['data']['created_by'] = get_session_userid();
|
||||
}
|
||||
|
||||
if (empty($data['data']['created_at'])) {
|
||||
$data['data']['created_at'] = date('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
if (! isset($data['data']['is_active'])) {
|
||||
$data['data']['is_active'] = 1;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function setUpdatedBy(array $data): array
|
||||
{
|
||||
if (empty($data['data']['updated_by'])) {
|
||||
$data['data']['updated_by'] = get_session_userid();
|
||||
}
|
||||
|
||||
if (empty($data['data']['updated_at'])) {
|
||||
$data['data']['updated_at'] = date('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,6 +24,66 @@
|
||||
.dataTables_length label {height: 21px !important;}
|
||||
|
||||
.readonly-select { background-color: #f3f3f3 !important; cursor: not-allowed; pointer-events: none; }
|
||||
|
||||
.custom-tooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
font-family: Georgia, serif;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.tooltiptext {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
position: fixed; /* fixed instead of absolute to escape modal overflow */
|
||||
background: #333;
|
||||
color: #fff;
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
white-space: normal; /* allow text wrapping */
|
||||
width: 320px; /* fixed width for multiline */
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
line-height: 1.6;
|
||||
z-index: 99999;
|
||||
transition: opacity 0.2s ease;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
top: 20px !important;
|
||||
left: 155px !important;
|
||||
}
|
||||
|
||||
.tooltiptext::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 20px;
|
||||
border: 6px solid transparent;
|
||||
border-top-color: #333;
|
||||
}
|
||||
|
||||
.custom-tooltip:hover .tooltiptext {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<!-- <div class="row">
|
||||
@ -117,6 +177,16 @@
|
||||
class="mdi mdi-dots-horizontal"></i></a>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a target="_blank" class="dropdown-item" href="<?= base_url("util/download_claim_dump_file/") . $file['file_id']; ?>"><i class="mdi mdi-download mr-2 text-muted font-18 vertical-middle"></i>Download</a>
|
||||
|
||||
<?php if ($file['status'] === 'failed') : ?>
|
||||
<a href="javascript:void(0);"
|
||||
class="dropdown-item"
|
||||
onclick="downloadTpaErrorFile(<?= (int) $file['file_id']; ?>)">
|
||||
<i class="mdi mdi-file-excel mr-2 text-danger font-18 vertical-middle"></i>
|
||||
Download Error File
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- <a data-id="<?php echo $file['file_id'] ?>" data-toggle="modal" data-target="#full-width-modal-emp-list" class="dropdown-item view_emp_list" href="#"><i class="mdi mdi-eye mr-2 text-muted font-18 vertical-middle"></i>View</a> -->
|
||||
</div>
|
||||
</div>
|
||||
@ -151,7 +221,16 @@
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="myCenterModalLabel">Claim Dump Upload</h4>
|
||||
<h4 class="modal-title" id="myCenterModalLabel">
|
||||
Claim Dump Upload
|
||||
<i class="info-icon"
|
||||
data-toggle="tooltip"
|
||||
data-placement="right"
|
||||
data-html="true"
|
||||
title="1. If you want to upload a TPA-wise claim dump, select the client and policy, then upload the TPA's Excel file. <br><br> 2. The second option without selecting the client and policy allows you to upload multiple claims. You can download the sample file provided below, fill it in, and upload it. This is for bulk dump upload.">
|
||||
i
|
||||
</i>
|
||||
</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@ -219,6 +298,20 @@
|
||||
let policyListByClient = null;
|
||||
let client_list = null;
|
||||
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
});
|
||||
|
||||
document.querySelectorAll('.custom-tooltip').forEach(function(tooltip) {
|
||||
tooltip.addEventListener('mouseenter', function() {
|
||||
var icon = this.querySelector('.info-icon');
|
||||
var tip = this.querySelector('.tooltiptext');
|
||||
var rect = icon.getBoundingClientRect();
|
||||
tip.style.left = rect.left + 'px';
|
||||
tip.style.top = (rect.top - tip.offsetHeight - 10) + 'px';
|
||||
});
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
getClientAndBranchAndPolicy();
|
||||
@ -232,10 +325,17 @@
|
||||
|
||||
let client_id = $('#client_id').val() ?? null;
|
||||
let client_policy_id = $('#client_policy_id').val() ?? null;
|
||||
let tpa_id = $('#tpa_id').val() ?? null;
|
||||
|
||||
if(client_id){
|
||||
|
||||
if(!client_policy_id){
|
||||
toastr.warning('Please select the client policy', 'Warning');
|
||||
toastr.warning('Please select the client policy', 'WARNING');
|
||||
return;
|
||||
}
|
||||
|
||||
if(!tpa_id){
|
||||
toastr.warning('TPA is required', 'WARNING');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -251,9 +351,15 @@
|
||||
'application/vnd.oasis.opendocument.spreadsheet'
|
||||
];
|
||||
|
||||
if (!fileInput.files.length) {
|
||||
toastr.warning('Please select an Excel file', 'WARNING');
|
||||
return false;
|
||||
}
|
||||
|
||||
var selectedFile = fileInput.files[0];
|
||||
|
||||
if (!allowedTypes.includes(selectedFile.type)) {
|
||||
alert('Please upload a valid Excel file (.xlsx, .xls, .ods)');
|
||||
toastr.warning('Please upload a valid Excel file (.xlsx, .xls, .ods)', 'WARNING');
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -500,6 +606,70 @@
|
||||
|
||||
}
|
||||
|
||||
function downloadTpaErrorFile(file_id) {
|
||||
if (!file_id) {
|
||||
toastr.error('Invalid file reference for error download', 'Error');
|
||||
return;
|
||||
}
|
||||
|
||||
var url = '<?= base_url("util/getTpaClaimDumpErrorData"); ?>/' + file_id;
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'GET',
|
||||
xhrFields: { responseType: 'blob' },
|
||||
success: function (data, status, xhr) {
|
||||
var disposition = xhr.getResponseHeader('Content-Disposition') || '';
|
||||
var contentType = xhr.getResponseHeader('Content-Type') || '';
|
||||
|
||||
// If server sent JSON (error), parse it and show via Toastr
|
||||
if (contentType.indexOf('application/json') !== -1) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function () {
|
||||
try {
|
||||
var json = JSON.parse(reader.result);
|
||||
var msg = json.message || 'Unable to download error file.';
|
||||
toastr.error(msg, 'Error');
|
||||
} catch (e) {
|
||||
toastr.error('Unable to download error file.', 'Error');
|
||||
}
|
||||
};
|
||||
reader.readAsText(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise assume it is Excel and trigger a download
|
||||
var filename = 'tpa_claim_dump_errors_' + file_id + '.xlsx';
|
||||
var match = /filename="?([^"]+)"?/.exec(disposition);
|
||||
if (match && match[1]) {
|
||||
filename = match[1];
|
||||
}
|
||||
|
||||
var blob = new Blob([data], {
|
||||
type: contentType || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
});
|
||||
var link = document.createElement('a');
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
toastr.success('Error file download started', 'Success');
|
||||
},
|
||||
error: function (xhr) {
|
||||
var msg = 'Unable to download error file.';
|
||||
try {
|
||||
var json = JSON.parse(xhr.responseText);
|
||||
if (json.message) {
|
||||
msg = json.message;
|
||||
}
|
||||
} catch (e) {}
|
||||
toastr.error(msg, 'Error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$('.close').click(function(){
|
||||
$('#modal_body').empty()
|
||||
let html = `<div class="spinner-border text-primary" role="status" style="position: relative; left: 200px;"></div>`
|
||||
|
||||
586
app/Views/expense_list.php
Normal file
586
app/Views/expense_list.php
Normal file
@ -0,0 +1,586 @@
|
||||
<div class="row" id="expense_module">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-6 d-flex align-items-center">
|
||||
<!-- <h4 class="mb-0">Expense</h4> -->
|
||||
</div>
|
||||
<div class="col-6 text-right">
|
||||
<!-- <button type="button" class="btn app-btn-secondary" id="btnResetExpense">
|
||||
<i class="mdi mdi-refresh mr-1"></i>Reset
|
||||
</button> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="custom-form mb-4">
|
||||
<form role="form" class="parsley-examples" method="get" id="expense_form">
|
||||
<input type="hidden" value="<?= csrf_hash() ?>" name="<?= csrf_token() ?>" />
|
||||
<input type="hidden" name="id" id="expense_id" />
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-4">
|
||||
<label for="client_id">Client Name <span class="text-danger">*</span></label>
|
||||
<select class="form-control" id="client_id" name="client_id" required>
|
||||
<option value="">Select Client</option>
|
||||
<?php if (! empty($clients)) : ?>
|
||||
<?php foreach ($clients as $client) : ?>
|
||||
<option value="<?= $client['id']; ?>" <?= ! empty($filters['client_id']) && (int) $filters['client_id'] === (int) $client['id'] ? 'selected' : ''; ?>>
|
||||
<?= esc($client['client_name']); ?>
|
||||
<?= ! empty($client['short_name']) ? ' (' . esc($client['short_name']) . ')' : ''; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-4">
|
||||
<label for="client_policy_id">Policy No <span class="text-danger">*</span></label>
|
||||
<select class="form-control" id="client_policy_id" name="client_policy_id" required>
|
||||
<option value="">Select Policy</option>
|
||||
<?php if (! empty($policies_for_filter)) : ?>
|
||||
<?php foreach ($policies_for_filter as $policy) : ?>
|
||||
<option value="<?= $policy['id']; ?>" <?= ! empty($filters['client_policy_id']) && (int) $filters['client_policy_id'] === (int) $policy['id'] ? 'selected' : ''; ?>>
|
||||
<?= ! empty($policy['policy_type']) ? esc($policy['policy_type']) . ' - ' : ''; ?> <?= esc($policy['policy_no']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-4">
|
||||
<label for="approved_by">Approved By <span class="text-danger">*</span></label>
|
||||
<select class="form-control" id="approved_by" name="approved_by" required>
|
||||
<option value="">Select User</option>
|
||||
<?php if (! empty($approved_users)) : ?>
|
||||
<?php foreach ($approved_users as $user) : ?>
|
||||
<option value="<?= $user['id']; ?>" <?= ! empty($filters['approved_by']) && (int) $filters['approved_by'] === (int) $user['id'] ? 'selected' : ''; ?>>
|
||||
<?= esc($user['first_name']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
|
||||
<div class="form-group col-md-4">
|
||||
<label for="expense_date">Expense Date <span class="text-danger">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="expense_date"
|
||||
name="expense_date"
|
||||
placeholder="DD-MM-YYYY"
|
||||
value="<?= isset($filters['expense_date']) ? esc($filters['expense_date']) : ''; ?>"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-4">
|
||||
<label for="amount">Amount <span class="text-danger">*</span></label>
|
||||
<input type="number" step="0.01" min="0" class="form-control" id="amount" name="amount" placeholder="Enter amount" value="<?= isset($filters['amount']) ? esc($filters['amount']) : ''; ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-4">
|
||||
<label for="description">Description <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="description" name="description" rows="3" placeholder="Enter description" required><?= isset($filters['description']) ? esc($filters['description']) : ''; ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group text-right m-b-0">
|
||||
<!-- <button type="button" class="btn app-btn-secondary waves-effect waves-light mr-1" id="btnSearchExpense">
|
||||
Search
|
||||
</button> -->
|
||||
<button type="submit" class="btn app-btn-secondary waves-effect waves-light mr-1" id="btnSubmitExpense">
|
||||
Save
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary waves-effect" id="btnCancelExpense">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<table data-custom-table-css="table" class="table mb-0 nowrap w-100 table-centered" cellspacing="0" id="expense-table">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="font-weight-medium">S.No</th>
|
||||
<th class="font-weight-medium">Client</th>
|
||||
<th class="font-weight-medium">Policy No</th>
|
||||
<th class="font-weight-medium">Description</th>
|
||||
<th class="font-weight-medium">Approved By</th>
|
||||
<th class="font-weight-medium">Amount</th>
|
||||
<th class="font-weight-medium">Expense Date</th>
|
||||
<th class="font-weight-medium">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (! empty($expenses)) : ?>
|
||||
<?php foreach ($expenses as $index => $row) : ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= $index + 1; ?></td>
|
||||
<td>
|
||||
<?= esc($row['client_name'] ?? ''); ?>
|
||||
<?= ! empty($row['short_name']) ? ' (' . esc($row['short_name']) . ')' : ''; ?>
|
||||
</td>
|
||||
<td><?= esc($row['policy_no'] ?? ''); ?></td>
|
||||
<td><?= esc($row['description'] ?? ''); ?></td>
|
||||
<td><?= esc($row['approved_by_name'] ?? ''); ?></td>
|
||||
<td><?= number_format((float) ($row['amount'] ?? 0), 2); ?></td>
|
||||
<td>
|
||||
<?php if (! empty($row['expense_date'])): ?>
|
||||
<?= date('d-m-Y', strtotime($row['expense_date'])); ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group dropdown">
|
||||
<a href="javascript:void(0);" class="dropdown-toggle arrow-none btn btn-light btn-sm" data-toggle="dropdown" aria-expanded="false">
|
||||
<i class="mdi mdi-dots-horizontal"></i>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" href="javascript:void(0);" onclick="editExpense(<?= (int) $row['id']; ?>)">
|
||||
<i class="mdi mdi-pencil mr-2 text-muted font-18 vertical-middle"></i>Edit
|
||||
</a>
|
||||
<a class="dropdown-item" href="javascript:void(0);" onclick="deleteExpense(<?= (int) $row['id']; ?>)">
|
||||
<i class="mdi mdi-delete mr-2 text-muted font-18 vertical-middle"></i>Delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const expenseValidationErrors = <?= ! empty($validation_errors) ? json_encode(array_values($validation_errors)) : '[]'; ?>;
|
||||
let expensePoliciesCache = {};
|
||||
|
||||
function resetExpenseForm() {
|
||||
$('#client_id').val('').trigger('change');
|
||||
$('#client_policy_id').empty().append('<option value="">Select Policy</option>').trigger('change');
|
||||
$('#approved_by').val('').trigger('change');
|
||||
$('#description').val('');
|
||||
$('#amount').val('');
|
||||
$('#expense_date').val('');
|
||||
$('#btnSubmitExpense').text('Save');
|
||||
window.location.href = '<?= base_url("expense"); ?>';
|
||||
}
|
||||
|
||||
function validateExpenseSave() {
|
||||
const errors = [];
|
||||
const clientId = $('#client_id').val();
|
||||
const policyId = $('#client_policy_id').val();
|
||||
const approvedBy = $('#approved_by').val();
|
||||
const desc = ($('#description').val() || '').trim();
|
||||
const amount = ($('#amount').val() || '').trim();
|
||||
const expenseDate= ($('#expense_date').val() || '').trim();
|
||||
|
||||
const idPattern = /^[0-9]+$/;
|
||||
const descPattern = /^[a-zA-Z0-9\s\.\,\-\(\)\/\&\+]+$/;
|
||||
const datePattern = /^[0-9]{2}-[0-9]{2}-[0-9]{4}$/;
|
||||
|
||||
if (!clientId) {
|
||||
errors.push('Client is required.');
|
||||
} else if (!idPattern.test(clientId)) {
|
||||
errors.push('Invalid client selected.');
|
||||
}
|
||||
|
||||
if (!policyId) {
|
||||
errors.push('Policy is required.');
|
||||
} else if (!idPattern.test(policyId)) {
|
||||
errors.push('Invalid policy selected.');
|
||||
}
|
||||
|
||||
if (!approvedBy) {
|
||||
errors.push('Approved By is required.');
|
||||
} else if (!idPattern.test(approvedBy)) {
|
||||
errors.push('Invalid approver selected.');
|
||||
}
|
||||
|
||||
if (!desc) {
|
||||
errors.push('Description is required.');
|
||||
} else if (!descPattern.test(desc)) {
|
||||
errors.push('Description contains invalid characters.');
|
||||
}
|
||||
|
||||
if (!amount) {
|
||||
errors.push('Amount is required.');
|
||||
} else if (isNaN(amount) || Number(amount) < 0) {
|
||||
errors.push('Amount must be a non-negative number.');
|
||||
}
|
||||
|
||||
if (!expenseDate) {
|
||||
errors.push('Expense Date is required.');
|
||||
} else if (!datePattern.test(expenseDate)) {
|
||||
errors.push('Expense Date must be in DD-MM-YYYY format.');
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
toastr.warning(errors.join('<br>'), 'Validation');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateExpenseSearchFilters() {
|
||||
const errors = [];
|
||||
const clientId = $('#client_id').val();
|
||||
const policyId = $('#client_policy_id').val();
|
||||
const approvedBy = $('#approved_by').val();
|
||||
const desc = ($('#description').val() || '').trim();
|
||||
const amount = ($('#amount').val() || '').trim();
|
||||
const expenseDate= ($('#expense_date').val() || '').trim();
|
||||
|
||||
const idPattern = /^[0-9]+$/;
|
||||
const descPattern = /^[a-zA-Z0-9\s\.\,\-\(\)\/\&\+]+$/;
|
||||
const datePattern = /^[0-9]{2}-[0-9]{2}-[0-9]{4}$/;
|
||||
|
||||
if (clientId && !idPattern.test(clientId)) {
|
||||
errors.push('Invalid client selected for search.');
|
||||
}
|
||||
|
||||
if (policyId && !idPattern.test(policyId)) {
|
||||
errors.push('Invalid policy selected for search.');
|
||||
}
|
||||
|
||||
if (approvedBy && !idPattern.test(approvedBy)) {
|
||||
errors.push('Invalid approver selected for search.');
|
||||
}
|
||||
|
||||
if (desc && !descPattern.test(desc)) {
|
||||
errors.push('Description filter contains invalid characters.');
|
||||
}
|
||||
|
||||
if (amount) {
|
||||
if (isNaN(amount) || Number(amount) < 0) {
|
||||
errors.push('Amount filter must be a non-negative number.');
|
||||
}
|
||||
}
|
||||
|
||||
if (expenseDate && !datePattern.test(expenseDate)) {
|
||||
errors.push('Expense Date filter must be in DD-MM-YYYY format.');
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
toastr.warning(errors.join('<br>'), 'Validation');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function populatePolicies(clientId, selectedPolicyId = null) {
|
||||
if (!clientId) {
|
||||
$('#client_policy_id').empty().append('<option value="">Select Policy</option>').trigger('change');
|
||||
return;
|
||||
}
|
||||
|
||||
if (expensePoliciesCache[clientId]) {
|
||||
const policies = expensePoliciesCache[clientId];
|
||||
let optionsHtml = '<option value=\"\">Select Policy</option>';
|
||||
|
||||
policies.forEach(function (p) {
|
||||
optionsHtml += `<option value=\"${p.id}\">${p.policy_no}</option>`;
|
||||
});
|
||||
|
||||
$('#client_policy_id').html(optionsHtml);
|
||||
|
||||
if (selectedPolicyId) {
|
||||
$('#client_policy_id').val(selectedPolicyId).trigger('change');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '<?= base_url("expense/client-policies"); ?>',
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
data: {client_id: clientId},
|
||||
success: function (res) {
|
||||
if (res.status) {
|
||||
expensePoliciesCache[clientId] = res.data || [];
|
||||
|
||||
let optionsHtml = '<option value=\"\">Select Policy</option>';
|
||||
(res.data || []).forEach(function (p) {
|
||||
optionsHtml += `<option value=\"${p.id}\">${p.policy_no}</option>`;
|
||||
});
|
||||
|
||||
$('#client_policy_id').html(optionsHtml);
|
||||
|
||||
if (selectedPolicyId) {
|
||||
$('#client_policy_id').val(selectedPolicyId).trigger('change');
|
||||
}
|
||||
} else {
|
||||
toastr.warning(res.message || 'Unable to load policies', 'WARNING');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
toastr.error('Unexpected error while loading policies', 'ERROR');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function editExpense(id) {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '<?= base_url("expense/get"); ?>/' + id,
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
success: function (res) {
|
||||
if (res.status && res.data) {
|
||||
const data = res.data;
|
||||
|
||||
$('#expense_id').val(data.id);
|
||||
$('#description').val(data.description || '');
|
||||
$('#amount').val(data.amount || '');
|
||||
if (data.expense_date) {
|
||||
const parts = (data.expense_date || '').split('-'); // Y-m-d
|
||||
if (parts.length === 3) {
|
||||
const formatted = parts[2] + '-' + parts[1] + '-' + parts[0];
|
||||
$('#expense_date').val(formatted);
|
||||
if (window.expenseDatePicker) {
|
||||
window.expenseDatePicker.setDate(formatted, true, 'd-m-Y');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.approved_by) {
|
||||
$('#approved_by').val(data.approved_by).trigger('change');
|
||||
}
|
||||
|
||||
if (data.client_id) {
|
||||
$('#client_id').val(data.client_id).trigger('change');
|
||||
populatePolicies(data.client_id, data.client_policy_id || null);
|
||||
}
|
||||
|
||||
$('#btnSubmitExpense').text('Update');
|
||||
$('html, body').animate({
|
||||
scrollTop: $('#expense_form').offset().top - 100
|
||||
}, 400);
|
||||
} else {
|
||||
toastr.warning(res.message || 'Expense not found', 'WARNING');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
toastr.error('Unable to fetch expense details', 'ERROR');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteExpense(id) {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: "Are you sure?",
|
||||
text: "You want to delete this expense",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#3085d6",
|
||||
cancelButtonColor: "#d33",
|
||||
confirmButtonText: "Yes",
|
||||
}).then((result) => {
|
||||
if (!result.isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const postData = {};
|
||||
postData['<?= csrf_token() ?>'] = '<?= csrf_hash() ?>';
|
||||
|
||||
$.ajax({
|
||||
url: '<?= base_url("expense/delete"); ?>/' + id,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: postData,
|
||||
success: function (res) {
|
||||
if (res.status) {
|
||||
toastr.success(res.message || 'Expense deleted successfully', 'SUCCESS');
|
||||
setTimeout(function () {
|
||||
location.reload();
|
||||
}, 800);
|
||||
} else {
|
||||
toastr.warning(res.message || 'Unable to delete expense', 'WARNING');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
toastr.error('Unable to delete expense', 'ERROR');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$('#client_id').select2();
|
||||
$('#client_policy_id').select2();
|
||||
$('#approved_by').select2();
|
||||
|
||||
window.expenseDatePicker = flatpickr('#expense_date', {
|
||||
dateFormat: 'd-m-Y',
|
||||
allowInput: true
|
||||
});
|
||||
|
||||
$('#expense-table').DataTable({
|
||||
dom: "<'row'<'col-12 d-flex justify-content-between align-items-center'<'datatable-search dataTables_filter'f><'datatable-buttons dt-buttons'B>>>" +
|
||||
"<'row'<'col-sm-12'tr>>" +
|
||||
"<'row align-items-center'<'col-5 text-start'i><'col-7 d-flex justify-content-end align-items-center'<'me-2'l>p>>",
|
||||
lengthMenu: [[10, 20, 50, 100], [10, 20, 50, 100]],
|
||||
buttons: [
|
||||
{
|
||||
extend: 'collection',
|
||||
text: '<span class=" btn-custom"> Export </span><i class="mdi mdi-menu-down"></i>',
|
||||
className: 'btn app-btn-secondary ',
|
||||
buttons: [
|
||||
{
|
||||
extend: 'csv',
|
||||
text: '<i class="mdi mdi-file-delimited"></i><span class="btn-custom"> CSV </span>',
|
||||
title: 'Expense List',
|
||||
className: 'app-btn-primary ',
|
||||
exportOptions: {
|
||||
columns: ':not(:last-child)'
|
||||
}
|
||||
},
|
||||
{
|
||||
extend: 'excel',
|
||||
title: 'Expense-List',
|
||||
sheetName: 'Expense-List',
|
||||
text: '<i class="mdi mdi-file-excel"></i><span class="btn-custom"> EXCEL </span>',
|
||||
className: 'app-btn-primary ',
|
||||
exportOptions: {
|
||||
orthogonal: 'sort',
|
||||
columns: ':not(:last-child)'
|
||||
}
|
||||
},
|
||||
{
|
||||
extend: 'pdf',
|
||||
text: '<i class="mdi mdi-file-pdf"></i><span class="btn-custom"> PDF </span>',
|
||||
title: 'Expense List',
|
||||
className: 'app-btn-primary ',
|
||||
exportOptions: {
|
||||
columns: ':not(:last-child)'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
language: {
|
||||
search: `
|
||||
<div class="datatable-search-wrapper" style="position:relative; display:inline-block;">
|
||||
_INPUT_
|
||||
<i class="mdi mdi-magnify datatable-search-icon"
|
||||
style="position:absolute; right:11px; top:50%; transform:translateY(-53%); color:#666;"></i>
|
||||
<i class="mdi mdi-close-circle datatable-clear-icon"
|
||||
style="position:absolute; right:11px; top:50%; transform:translateY(-53%); color:#666; display:none;"></i>
|
||||
</div>`,
|
||||
searchPlaceholder: "Search",
|
||||
emptyTable: '<div class="text-center text-muted">No Data found</div>'
|
||||
}
|
||||
});
|
||||
|
||||
$('.dataTables_length label').css('height', '21px');
|
||||
|
||||
// Show any backend validation errors from search
|
||||
if (Array.isArray(expenseValidationErrors) && expenseValidationErrors.length) {
|
||||
expenseValidationErrors.forEach(function (msg) {
|
||||
toastr.warning(msg, 'Validation');
|
||||
});
|
||||
}
|
||||
|
||||
$('#client_id').on('change', function () {
|
||||
const clientId = $(this).val();
|
||||
populatePolicies(clientId);
|
||||
});
|
||||
|
||||
$('#btnSearchExpense').on('click', function () {
|
||||
if (!validateExpenseSearchFilters()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const params = {};
|
||||
const clientId = $('#client_id').val();
|
||||
const policyId = $('#client_policy_id').val();
|
||||
const approvedBy = $('#approved_by').val();
|
||||
const desc = $('#description').val();
|
||||
const amount = $('#amount').val();
|
||||
const expenseDate = $('#expense_date').val();
|
||||
|
||||
if (clientId) {
|
||||
params.client_id = clientId;
|
||||
}
|
||||
if (policyId) {
|
||||
params.client_policy_id = policyId;
|
||||
}
|
||||
if (approvedBy) {
|
||||
params.approved_by = approvedBy;
|
||||
}
|
||||
if (desc) {
|
||||
params.description = desc;
|
||||
}
|
||||
if (amount) {
|
||||
params.amount = amount;
|
||||
}
|
||||
if (expenseDate) {
|
||||
params.expense_date = expenseDate;
|
||||
}
|
||||
|
||||
const query = $.param(params);
|
||||
const baseUrl = '<?= base_url("expense"); ?>';
|
||||
window.location.href = query ? baseUrl + '?' + query : baseUrl;
|
||||
});
|
||||
|
||||
$('#btnSubmitExpense').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateExpenseSave()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = $('#expense_form').serialize();
|
||||
|
||||
$.ajax({
|
||||
url: '<?= base_url("expense/save"); ?>',
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: formData,
|
||||
success: function (res) {
|
||||
if (res.status) {
|
||||
toastr.success(res.message || 'Expense saved successfully', 'SUCCESS');
|
||||
setTimeout(function () {
|
||||
location.reload();
|
||||
}, 800);
|
||||
} else {
|
||||
const errors = res.errors || {};
|
||||
let errorMsg = res.message || 'Unable to save expense';
|
||||
|
||||
if (Object.keys(errors).length) {
|
||||
errorMsg = Object.values(errors).join('<br>');
|
||||
}
|
||||
|
||||
toastr.warning(errorMsg, 'WARNING');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
toastr.error('Unexpected error while saving expense', 'ERROR');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#btnResetExpense, #btnCancelExpense').on('click', function () {
|
||||
resetExpenseForm();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -1876,6 +1876,17 @@
|
||||
</li>
|
||||
<?php } ?>
|
||||
|
||||
<!-- Expence -->
|
||||
<?php if (in_array(get_role_id(), [1, 5])) { ?>
|
||||
<li class="li-seperate" id="clients-li">
|
||||
<a href="<?= base_url('expense') ?>" class="img-inactive">
|
||||
<img style="border-radius: 5px;"
|
||||
src="<?= base_url() . "public"; ?>/assets/images/expense_icon.png" alt="Logo" height="24">
|
||||
<span> Expense </span>
|
||||
</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
|
||||
<!-- BDS -->
|
||||
<?php if ((get_role_id() == 1 || get_role_id() == 5 || get_role_id() == 4) || (in_array(MANAGEMENT_TEAM_ID, user_team()) || in_array(FINANCE_TEAM_ID, user_team()) || in_array(BUSINESS_TEAM_ID, user_team()) || in_array(POS_TEAM_ID, user_team()))) { ?>
|
||||
<li class="li-seperate" id="policy-transactions-li">
|
||||
|
||||
BIN
public/assets/images/expense_icon.png
Normal file
BIN
public/assets/images/expense_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
399
tests/unit/ExpenseControllerTest.php
Normal file
399
tests/unit/ExpenseControllerTest.php
Normal file
@ -0,0 +1,399 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOption {
|
||||
/**
|
||||
* Lightweight stub of PhpOption\Option used only for testing env() handling.
|
||||
* This is intentionally minimal and should not be relied on in application code.
|
||||
*/
|
||||
class Option
|
||||
{
|
||||
private $value;
|
||||
|
||||
public function __construct($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public static function fromValue($value): self
|
||||
{
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
public function map($callback): self
|
||||
{
|
||||
if ($this->value !== null) {
|
||||
$this->value = $callback($this->value);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOrCall($callback)
|
||||
{
|
||||
return $this->value !== null ? $this->value : $callback();
|
||||
}
|
||||
|
||||
public function getOrThrow($exception)
|
||||
{
|
||||
if ($this->value === null) {
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace App\Filters {
|
||||
use CodeIgniter\Filters\FilterInterface;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
// Test stubs for application filters to bypass logging, ACL, and security checks during tests.
|
||||
|
||||
class AclFilter implements FilterInterface
|
||||
{
|
||||
public function before(RequestInterface $request, $arguments = null) { return null; }
|
||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; }
|
||||
}
|
||||
|
||||
class HttpRequestLog implements FilterInterface
|
||||
{
|
||||
public function before(RequestInterface $request, $arguments = null) { return null; }
|
||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; }
|
||||
}
|
||||
|
||||
class SecurityInputFilter implements FilterInterface
|
||||
{
|
||||
public function before(RequestInterface $request, $arguments = null) { return null; }
|
||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; }
|
||||
}
|
||||
|
||||
class GlobalPostFileUploadGuard implements FilterInterface
|
||||
{
|
||||
public function before(RequestInterface $request, $arguments = null) { return null; }
|
||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; }
|
||||
}
|
||||
|
||||
class Cors implements FilterInterface
|
||||
{
|
||||
public function before(RequestInterface $request, $arguments = null) { return null; }
|
||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; }
|
||||
}
|
||||
}
|
||||
|
||||
namespace Dotenv\Repository {
|
||||
interface RepositoryInterface
|
||||
{
|
||||
public function get($name);
|
||||
|
||||
public function set($name, $value);
|
||||
}
|
||||
|
||||
class RepositoryBuilder
|
||||
{
|
||||
public static function createWithDefaultAdapters(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function addAdapter($adapter): self
|
||||
{
|
||||
// No-op for testing.
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function immutable(): self
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function make(): RepositoryInterface
|
||||
{
|
||||
return new class() implements RepositoryInterface {
|
||||
public function get($name)
|
||||
{
|
||||
$value = getenv($name);
|
||||
|
||||
return $value === false ? null : $value;
|
||||
}
|
||||
|
||||
public function set($name, $value)
|
||||
{
|
||||
putenv("$name=$value");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Dotenv\Repository\Adapter {
|
||||
class PutenvAdapter
|
||||
{
|
||||
// Stub class used only to satisfy Illuminate\Support\Env references.
|
||||
}
|
||||
}
|
||||
|
||||
namespace Tests\unit {
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use CodeIgniter\Test\FeatureTestTrait;
|
||||
|
||||
class ExpenseControllerTest extends CIUnitTestCase
|
||||
{
|
||||
use FeatureTestTrait;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Override routes for testing to avoid filters/middleware.
|
||||
$this->withRoutes([
|
||||
['get', 'expense', 'ExpenseController::index'],
|
||||
['post', 'expense/save', 'ExpenseController::save'],
|
||||
['get', 'expense/get/(:num)', 'ExpenseController::getExpense/$1'],
|
||||
['post', 'expense/delete/(:num)','ExpenseController::delete/$1'],
|
||||
['get', 'expense/client-policies', 'ExpenseController::clientPolicies'],
|
||||
]);
|
||||
|
||||
// Clean up any test data from previous runs
|
||||
$db = db_connect('default');
|
||||
$db->table('expenses')->like('description', 'CI4_TEST_', 'after')->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to insert an expense row directly into the database.
|
||||
*/
|
||||
private function createExpenseRecord(array $overrides = []): int
|
||||
{
|
||||
$db = db_connect('default');
|
||||
|
||||
$data = array_merge([
|
||||
'client_id' => 1,
|
||||
'client_policy_id' => 1,
|
||||
'description' => 'CI4_TEST_' . uniqid('', true),
|
||||
'approved_by' => 1,
|
||||
'amount' => 100.00,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'is_active' => 1,
|
||||
], $overrides);
|
||||
|
||||
$db->table('expenses')->insert($data);
|
||||
|
||||
return (int) $db->insertID();
|
||||
}
|
||||
|
||||
public function testCreateExpenseSuccess(): void
|
||||
{
|
||||
$payload = [
|
||||
'client_id' => '1',
|
||||
'client_policy_id' => '1',
|
||||
'approved_by' => '1',
|
||||
'description' => 'CI4_TEST_Valid Description 123-ABC',
|
||||
'expense_date' => '12-01-2026',
|
||||
'amount' => '250.75',
|
||||
];
|
||||
|
||||
$result = $this->post('expense/save', $payload);
|
||||
|
||||
$result->assertStatus(200);
|
||||
|
||||
$json = json_decode($result->getJSON(), true);
|
||||
|
||||
$this->assertIsArray($json);
|
||||
$this->assertTrue($json['status'] ?? false);
|
||||
$this->assertNotEmpty($json['id'] ?? null);
|
||||
|
||||
$insertedId = (int) $json['id'];
|
||||
|
||||
$db = db_connect('default');
|
||||
$row = $db->table('expenses')
|
||||
->where('description', $payload['description'])
|
||||
->orderBy('id', 'DESC')
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
$this->assertNotEmpty($row);
|
||||
$this->assertSame(1, (int) $row['is_active']);
|
||||
$this->assertSame($payload['description'], $row['description']);
|
||||
$this->assertEquals(250.75, (float) $row['amount']);
|
||||
}
|
||||
|
||||
public function testCreateExpenseValidationFailureMissingFields(): void
|
||||
{
|
||||
$payload = [
|
||||
// all required fields missing / empty
|
||||
];
|
||||
|
||||
$result = $this->post('expense/save', $payload);
|
||||
|
||||
$result->assertStatus(400);
|
||||
|
||||
$json = json_decode($result->getJSON(), true);
|
||||
|
||||
$this->assertFalse($json['status'] ?? true);
|
||||
$this->assertSame('Input validation failed', $json['message'] ?? '');
|
||||
$this->assertArrayHasKey('client_id', $json['errors'] ?? []);
|
||||
$this->assertArrayHasKey('client_policy_id', $json['errors'] ?? []);
|
||||
$this->assertArrayHasKey('approved_by', $json['errors'] ?? []);
|
||||
$this->assertArrayHasKey('description', $json['errors'] ?? []);
|
||||
$this->assertArrayHasKey('amount', $json['errors'] ?? []);
|
||||
$this->assertArrayHasKey('expense_date', $json['errors'] ?? []);
|
||||
}
|
||||
|
||||
public function testCreateExpenseValidationFailureInvalidDescription(): void
|
||||
{
|
||||
$payload = [
|
||||
'client_id' => '1',
|
||||
'client_policy_id' => '1',
|
||||
'approved_by' => '1',
|
||||
'description' => 'Invalid @ Description <>', // invalid characters
|
||||
'expense_date' => '12-01-2026',
|
||||
'amount' => '100.00',
|
||||
];
|
||||
|
||||
$result = $this->post('expense/save', $payload);
|
||||
|
||||
$result->assertStatus(400);
|
||||
|
||||
$json = json_decode($result->getJSON(), true);
|
||||
|
||||
$this->assertFalse($json['status'] ?? true);
|
||||
$this->assertArrayHasKey('description', $json['errors'] ?? []);
|
||||
$this->assertStringContainsString(
|
||||
'invalid characters',
|
||||
strtolower($json['errors']['description'] ?? '')
|
||||
);
|
||||
}
|
||||
|
||||
public function testUpdateExpenseSuccess(): void
|
||||
{
|
||||
$id = $this->createExpenseRecord([
|
||||
'description' => 'CI4_TEST_Original Description',
|
||||
'amount' => 50.00,
|
||||
]);
|
||||
|
||||
$payload = [
|
||||
'id' => (string) $id,
|
||||
'client_id' => '1',
|
||||
'client_policy_id'=> '1',
|
||||
'approved_by' => '1',
|
||||
'description' => 'Updated Description 456',
|
||||
'expense_date' => '13-01-2026',
|
||||
'amount' => '75.50',
|
||||
];
|
||||
|
||||
$result = $this->post('expense/save', $payload);
|
||||
|
||||
$result->assertStatus(200);
|
||||
|
||||
$json = json_decode($result->getJSON(), true);
|
||||
|
||||
$this->assertTrue($json['status'] ?? false);
|
||||
$this->assertSame($id, (int) ($json['id'] ?? 0));
|
||||
|
||||
$db = db_connect('default');
|
||||
$row = $db->table('expenses')->where('id', $id)->get()->getRowArray();
|
||||
|
||||
$this->assertNotEmpty($row);
|
||||
$this->assertSame('Updated Description 456', $row['description']);
|
||||
$this->assertEquals(75.50, (float) $row['amount']);
|
||||
}
|
||||
|
||||
public function testUpdateExpenseValidationFailureInvalidAmount(): void
|
||||
{
|
||||
$id = $this->createExpenseRecord();
|
||||
|
||||
$payload = [
|
||||
'id' => (string) $id,
|
||||
'client_id' => '1',
|
||||
'client_policy_id'=> '1',
|
||||
'approved_by' => '1',
|
||||
'description' => 'Another Valid Description',
|
||||
'expense_date' => '14-01-2026',
|
||||
'amount' => '-10', // invalid negative
|
||||
];
|
||||
|
||||
$result = $this->post('expense/save', $payload);
|
||||
|
||||
$result->assertStatus(400);
|
||||
|
||||
$json = json_decode($result->getJSON(), true);
|
||||
|
||||
$this->assertFalse($json['status'] ?? true);
|
||||
$this->assertArrayHasKey('amount', $json['errors'] ?? []);
|
||||
$this->assertStringContainsString(
|
||||
'cannot be negative',
|
||||
strtolower($json['errors']['amount'] ?? '')
|
||||
);
|
||||
}
|
||||
|
||||
public function testDeleteExpenseSoftDeleteSuccess(): void
|
||||
{
|
||||
$id = $this->createExpenseRecord();
|
||||
|
||||
$result = $this->post('expense/delete/' . $id);
|
||||
|
||||
$result->assertStatus(200);
|
||||
|
||||
$json = json_decode($result->getJSON(), true);
|
||||
|
||||
$this->assertTrue($json['status'] ?? false);
|
||||
|
||||
$db = db_connect();
|
||||
$row = $db->table('expenses')->where('id', $id)->get()->getRowArray();
|
||||
|
||||
$this->assertNotEmpty($row);
|
||||
$this->assertSame(0, (int) $row['is_active']);
|
||||
}
|
||||
|
||||
public function testDeleteExpenseInvalidId(): void
|
||||
{
|
||||
$result = $this->post('expense/delete/0');
|
||||
|
||||
$result->assertStatus(400);
|
||||
|
||||
$json = json_decode($result->getJSON(), true);
|
||||
|
||||
$this->assertFalse($json['status'] ?? true);
|
||||
$this->assertSame('Invalid expense id', $json['message'] ?? '');
|
||||
}
|
||||
|
||||
public function testSearchFilterByClientAndDescription(): void
|
||||
{
|
||||
// Create two distinct expenses
|
||||
$this->createExpenseRecord([
|
||||
'client_id' => 10,
|
||||
'description' => 'FilterMatch Description',
|
||||
]);
|
||||
|
||||
$this->createExpenseRecord([
|
||||
'client_id' => 20,
|
||||
'description' => 'Other Description',
|
||||
]);
|
||||
|
||||
$result = $this->get('expense?client_id=10&description=FilterMatch');
|
||||
|
||||
$result->assertStatus(200);
|
||||
|
||||
$body = $result->getBody();
|
||||
|
||||
$this->assertStringContainsString('FilterMatch Description', $body);
|
||||
$this->assertStringNotContainsString('Other Description', $body);
|
||||
}
|
||||
|
||||
public function testSearchFilterValidationInvalidCharacters(): void
|
||||
{
|
||||
$result = $this->get('expense?description=<script>alert(1)</script>');
|
||||
|
||||
$result->assertStatus(200);
|
||||
|
||||
$body = $result->getBody();
|
||||
|
||||
$this->assertStringContainsString('Description filter contains invalid characters.', $body);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user