Merge branch 'main' of bitbucket.org:jubilian/nhance_partner_be
This commit is contained in:
commit
053e27eb63
@ -64,8 +64,11 @@ $routes->group('api', ['filter' => ["jwtAuth:1,2,3,4,agent","appSignature"] ], f
|
||||
$routes->get('agent/agentList', 'AgentController::agentList');
|
||||
$routes->get('agent/agentListForEnquiryCreationDropdown', 'AgentController::agentListForEnquiryCreationDropdown');
|
||||
$routes->get('agent/findAgent', 'AgentController::findAgent');
|
||||
$routes->get('agent/partnerVehicleTypeList', 'AgentController::partnerVehicleTypeList');
|
||||
$routes->post('agent/createAgent', 'AgentController::createAgent');
|
||||
$routes->post('agent/updateAgent', 'AgentController::updateAgent');
|
||||
$routes->post('agent/updateAgentVehicleRetention', 'AgentController::updateAgentVehicleRetention');
|
||||
$routes->post('agent/saveAgentRetentionRatesBulk', 'AgentController::saveAgentRetentionRatesBulk');
|
||||
$routes->post('agent/updateDeviceToken', 'AgentController::updateDeviceToken');
|
||||
$routes->post('agent/changeAgentStatus', 'AgentController::changeAgentStatus');
|
||||
$routes->get('agent/downloadAgentCertificateFile', 'AgentController::downloadAgentCertificateFile');
|
||||
|
||||
@ -5,16 +5,282 @@ use CodeIgniter\RESTful\ResourceController;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\AgentModel;
|
||||
use App\Models\AgentIncentiveFileModel;
|
||||
use App\Models\PartnerRetentionRateModel;
|
||||
use App\Models\PartnerVehicleTypeModel;
|
||||
|
||||
class AgentController extends ResourceController
|
||||
{
|
||||
protected $AgentModel;
|
||||
protected $AgentIncentiveFileModel;
|
||||
protected $PartnerRetentionRateModel;
|
||||
protected $PartnerVehicleTypeModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->AgentModel = new AgentModel();
|
||||
$this->AgentIncentiveFileModel = new AgentIncentiveFileModel();
|
||||
$this->PartnerRetentionRateModel = new PartnerRetentionRateModel();
|
||||
$this->PartnerVehicleTypeModel = new PartnerVehicleTypeModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{vehicle_type_id:int|float, retention_rate:float}>
|
||||
*/
|
||||
protected function parseRetentionRatesFromRequest(): array
|
||||
{
|
||||
$raw = $this->request->getPost('retention_rates');
|
||||
if ($raw === null || $raw === '') {
|
||||
return [];
|
||||
}
|
||||
if (is_string($raw)) {
|
||||
$decoded = json_decode($raw, true);
|
||||
} else {
|
||||
$decoded = $raw;
|
||||
}
|
||||
if (!is_array($decoded)) {
|
||||
return [];
|
||||
}
|
||||
$out = [];
|
||||
foreach ($decoded as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$vtId = $row['vehicle_type_id'] ?? $row['vehicleTypeId'] ?? null;
|
||||
if ($vtId === null || $vtId === '') {
|
||||
continue;
|
||||
}
|
||||
$rateRaw = $row['retention_rate'] ?? $row['retentionRate'] ?? null;
|
||||
if ($rateRaw === null || $rateRaw === '') {
|
||||
continue;
|
||||
}
|
||||
$rate = (float) $rateRaw;
|
||||
if ($rate < 0 || $rate > 100) {
|
||||
continue;
|
||||
}
|
||||
$out[] = [
|
||||
'vehicle_type_id' => (int) $vtId,
|
||||
'retention_rate' => round($rate, 2),
|
||||
];
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace retention rows for an agent from the parsed list (main Save).
|
||||
*/
|
||||
protected function syncRetentionRatesForAgent(int $agentId, array $rates, ?int $userId): void
|
||||
{
|
||||
$this->PartnerRetentionRateModel->where('agent_id', $agentId)->delete();
|
||||
$now = date('Y-m-d H:i:s');
|
||||
foreach ($rates as $r) {
|
||||
$this->PartnerRetentionRateModel->insert([
|
||||
'agent_id' => $agentId,
|
||||
'vehicle_type_id' => $r['vehicle_type_id'],
|
||||
'retention_rate' => $r['retention_rate'],
|
||||
'is_active' => 1,
|
||||
'created_by' => $userId,
|
||||
'created_on' => $now,
|
||||
'updated_by' => null,
|
||||
'updated_on' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/** Active vehicle types for partner retention UI */
|
||||
public function partnerVehicleTypeList()
|
||||
{
|
||||
try {
|
||||
$data = $this->PartnerVehicleTypeModel
|
||||
->where('is_active', 1)
|
||||
->orderBy('vehicle_type', 'ASC')
|
||||
->findAll();
|
||||
|
||||
return $this->respond(['status' => 'success', 'code' => 200, 'data' => $data], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->respond(['status' => 'failed', 'code' => 500, 'data' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert one vehicle-type retention row (inline edit on agent details).
|
||||
*/
|
||||
public function updateAgentVehicleRetention()
|
||||
{
|
||||
try {
|
||||
$data = $this->request->getJSON(true);
|
||||
if (!is_array($data) || $data === []) {
|
||||
$raw = $this->request->getRawInput();
|
||||
if (is_array($raw) && $raw !== []) {
|
||||
$data = $raw;
|
||||
} elseif (is_string($raw) && $raw !== '') {
|
||||
$decoded = json_decode($raw, true);
|
||||
if (is_array($decoded)) {
|
||||
$data = $decoded;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!is_array($data) || $data === []) {
|
||||
$data = $this->request->getPost();
|
||||
}
|
||||
if (!is_array($data)) {
|
||||
$data = [];
|
||||
}
|
||||
|
||||
if (!isset($data['agent_id'], $data['vehicle_type_id'], $data['retention_rate'])) {
|
||||
return $this->respond(['status' => 'failed', 'code' => 200, 'data' => 'agent_id, vehicle_type_id and retention_rate are required'], 200);
|
||||
}
|
||||
$agentId = (int) $data['agent_id'];
|
||||
$vtId = (int) $data['vehicle_type_id'];
|
||||
$rate = (float) $data['retention_rate'];
|
||||
if ($rate < 0 || $rate > 100) {
|
||||
return $this->respond(['status' => 'failed', 'code' => 200, 'data' => 'retention_rate must be between 0 and 100'], 200);
|
||||
}
|
||||
$agent = $this->AgentModel->find($agentId);
|
||||
if (!$agent) {
|
||||
return $this->respond(['status' => 'failed', 'code' => 200, 'data' => 'Agent not found'], 200);
|
||||
}
|
||||
$updatedBy = isset($data['updated_by']) ? (int) $data['updated_by'] : null;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
$existing = $this->PartnerRetentionRateModel
|
||||
->where('agent_id', $agentId)
|
||||
->where('vehicle_type_id', $vtId)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$this->PartnerRetentionRateModel->update((int) $existing['id'], [
|
||||
'retention_rate' => round($rate, 2),
|
||||
'is_active' => 1,
|
||||
'updated_by' => $updatedBy,
|
||||
'updated_on' => $now,
|
||||
]);
|
||||
} else {
|
||||
$this->PartnerRetentionRateModel->insert([
|
||||
'agent_id' => $agentId,
|
||||
'vehicle_type_id' => $vtId,
|
||||
'retention_rate' => round($rate, 2),
|
||||
'is_active' => 1,
|
||||
'created_by' => $updatedBy,
|
||||
'created_on' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->respond(['status' => 'success', 'code' => 200, 'data' => []], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->respond(['status' => 'failed', 'code' => 500, 'data' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk upsert retention rows from the retention table (Save all).
|
||||
* POST JSON or form: agent_id, updated_by, retention_rates (array of {vehicle_type_id, retention_rate}).
|
||||
* Skips invalid entries; empty retention_rates array returns an error.
|
||||
*/
|
||||
public function saveAgentRetentionRatesBulk()
|
||||
{
|
||||
try {
|
||||
$data = $this->request->getJSON(true);
|
||||
if (!is_array($data) || $data === []) {
|
||||
$raw = $this->request->getRawInput();
|
||||
if (is_array($raw) && $raw !== []) {
|
||||
$data = $raw;
|
||||
} elseif (is_string($raw) && $raw !== '') {
|
||||
$decoded = json_decode($raw, true);
|
||||
if (is_array($decoded)) {
|
||||
$data = $decoded;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!is_array($data) || $data === []) {
|
||||
$data = $this->request->getPost();
|
||||
}
|
||||
if (!is_array($data)) {
|
||||
$data = [];
|
||||
}
|
||||
|
||||
if (!isset($data['agent_id'])) {
|
||||
return $this->respond(['status' => 'failed', 'code' => 200, 'data' => 'agent_id is required'], 200);
|
||||
}
|
||||
|
||||
$agentId = (int) $data['agent_id'];
|
||||
$agent = $this->AgentModel->find($agentId);
|
||||
if (!$agent) {
|
||||
return $this->respond(['status' => 'failed', 'code' => 200, 'data' => 'Agent not found'], 200);
|
||||
}
|
||||
|
||||
$ratesRaw = $data['retention_rates'] ?? null;
|
||||
if ($ratesRaw === null || $ratesRaw === '') {
|
||||
return $this->respond(['status' => 'failed', 'code' => 200, 'data' => 'retention_rates is required'], 200);
|
||||
}
|
||||
if (is_string($ratesRaw)) {
|
||||
$rates = json_decode($ratesRaw, true);
|
||||
} else {
|
||||
$rates = $ratesRaw;
|
||||
}
|
||||
if (!is_array($rates) || $rates === []) {
|
||||
return $this->respond(['status' => 'failed', 'code' => 200, 'data' => 'retention_rates must be a non-empty array'], 200);
|
||||
}
|
||||
|
||||
$updatedBy = isset($data['updated_by']) ? (int) $data['updated_by'] : null;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$saved = 0;
|
||||
|
||||
foreach ($rates as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$vtId = $row['vehicle_type_id'] ?? $row['vehicleTypeId'] ?? null;
|
||||
if ($vtId === null || $vtId === '') {
|
||||
continue;
|
||||
}
|
||||
$rateRaw = $row['retention_rate'] ?? $row['retentionRate'] ?? null;
|
||||
if ($rateRaw === null || $rateRaw === '') {
|
||||
continue;
|
||||
}
|
||||
$rate = (float) $rateRaw;
|
||||
if ($rate < 0 || $rate > 100) {
|
||||
continue;
|
||||
}
|
||||
$vtId = (int) $vtId;
|
||||
|
||||
$existing = $this->PartnerRetentionRateModel
|
||||
->where('agent_id', $agentId)
|
||||
->where('vehicle_type_id', $vtId)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$this->PartnerRetentionRateModel->update((int) $existing['id'], [
|
||||
'retention_rate' => round($rate, 2),
|
||||
'is_active' => 1,
|
||||
'updated_by' => $updatedBy,
|
||||
'updated_on' => $now,
|
||||
]);
|
||||
} else {
|
||||
$this->PartnerRetentionRateModel->insert([
|
||||
'agent_id' => $agentId,
|
||||
'vehicle_type_id' => $vtId,
|
||||
'retention_rate' => round($rate, 2),
|
||||
'is_active' => 1,
|
||||
'created_by' => $updatedBy,
|
||||
'created_on' => $now,
|
||||
]);
|
||||
}
|
||||
$saved++;
|
||||
}
|
||||
|
||||
if ($saved === 0) {
|
||||
return $this->respond(['status' => 'failed', 'code' => 200, 'data' => 'No valid retention rows to save'], 200);
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'code' => 200,
|
||||
'data' => ['saved_count' => $saved],
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return $this->respond(['status' => 'failed', 'code' => 500, 'data' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
// List of all agents
|
||||
@ -29,6 +295,29 @@ class AgentController extends ResourceController
|
||||
->where('partner_agent.manager_id' , $id )
|
||||
->findAll();
|
||||
|
||||
if ($data !== []) {
|
||||
$ids = array_column($data, 'id');
|
||||
$db = db_connect();
|
||||
$avgs = $db->table('partner_retention_rate')
|
||||
->select('agent_id, ROUND(AVG(retention_rate), 2) AS avg_retention', false)
|
||||
->where('is_active', 1)
|
||||
->whereIn('agent_id', $ids)
|
||||
->groupBy('agent_id')
|
||||
->get()
|
||||
->getResultArray();
|
||||
$avgMap = [];
|
||||
foreach ($avgs as $a) {
|
||||
$avgMap[(int) $a['agent_id']] = $a['avg_retention'];
|
||||
}
|
||||
foreach ($data as &$row) {
|
||||
$aid = (int) $row['id'];
|
||||
if (isset($avgMap[$aid])) {
|
||||
$row['retention_rate'] = $avgMap[$aid];
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
}
|
||||
|
||||
return $this->respond(['status' => 'success', 'code' => 200, 'data' => $data], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
@ -66,6 +355,15 @@ class AgentController extends ResourceController
|
||||
return $this->respond(['status' => 'failed', 'code' => 404, 'data' => []], 200);
|
||||
}
|
||||
|
||||
$rates = $this->PartnerRetentionRateModel
|
||||
->select('partner_retention_rate.id as retention_row_id, partner_retention_rate.vehicle_type_id, partner_retention_rate.retention_rate, partner_vehicle_type.vehicle_type')
|
||||
->join('partner_vehicle_type', 'partner_vehicle_type.id = partner_retention_rate.vehicle_type_id', 'left')
|
||||
->where('partner_retention_rate.agent_id', (int) $id)
|
||||
->where('partner_retention_rate.is_active', 1)
|
||||
->orderBy('partner_vehicle_type.vehicle_type', 'ASC')
|
||||
->findAll();
|
||||
$record['retention_by_vehicle'] = $rates;
|
||||
|
||||
return $this->respond(['status' => 'success', 'code' => 200, 'data' => $record], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
@ -94,6 +392,8 @@ class AgentController extends ResourceController
|
||||
$certificateFile->move($uploadPath, $certificateFileName);
|
||||
}
|
||||
|
||||
$rates = $this->parseRetentionRatesFromRequest();
|
||||
|
||||
$insertData = [
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
@ -104,12 +404,18 @@ class AgentController extends ResourceController
|
||||
'sales_executive_id' => $data['sales_executive_id'],
|
||||
'certificate_file_name' => $certificateFileName,
|
||||
'created_by' => $data['created_by'],
|
||||
'retention_rate' => $data['retention_rate'] ?? 0
|
||||
'retention_rate' => null,
|
||||
];
|
||||
|
||||
$this->AgentModel->insert($insertData);
|
||||
$newId = (int) $this->AgentModel->getInsertID();
|
||||
|
||||
return $this->respond(['status' => 'success', 'code' => 200, 'data' => []], 200);
|
||||
if ($rates !== [] && $newId > 0) {
|
||||
$createdBy = isset($data['created_by']) ? (int) $data['created_by'] : null;
|
||||
$this->syncRetentionRatesForAgent($newId, $rates, $createdBy);
|
||||
}
|
||||
|
||||
return $this->respond(['status' => 'success', 'code' => 200, 'data' => ['agent_id' => $newId]], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->respond(['status' => 'failed', 'code' => 500, 'data' => $e->getMessage()], 500);
|
||||
@ -145,7 +451,7 @@ class AgentController extends ResourceController
|
||||
'agent_code' => $data['agent_code'] ?? null,
|
||||
'sales_executive_id' => $data['sales_executive_id'] ?? null,
|
||||
'updated_by' => $data['updated_by'] ?? null,
|
||||
'retention_rate' => $data['retention_rate'] ?? $agent['retention_rate']
|
||||
'retention_rate' => null,
|
||||
];
|
||||
|
||||
if ($certificateFile && $certificateFile->isValid()) {
|
||||
@ -158,6 +464,12 @@ class AgentController extends ResourceController
|
||||
$updateData['certificate_file_name'] = $certificateFileName;
|
||||
}
|
||||
|
||||
if (array_key_exists('retention_rates', $data)) {
|
||||
$rates = $this->parseRetentionRatesFromRequest();
|
||||
$updatedBy = isset($data['updated_by']) ? (int) $data['updated_by'] : null;
|
||||
$this->syncRetentionRatesForAgent((int) $id, $rates, $updatedBy);
|
||||
}
|
||||
|
||||
$this->AgentModel->update($id, $updateData);
|
||||
|
||||
return $this->respond(['status' => 'success', 'code' => 200, 'data' => []], 200);
|
||||
|
||||
@ -9,6 +9,7 @@ use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
|
||||
use App\Libraries\PartnerPayoutGridRetention;
|
||||
use App\Models\AgentIncentiveFileModel;
|
||||
use App\Models\PartnerInsurancePayoutGridModel;
|
||||
use App\Models\AgentModel;
|
||||
@ -358,7 +359,16 @@ class AgentIncentiveController extends ResourceController
|
||||
}
|
||||
}
|
||||
|
||||
$gridResults = $builder->get()->getResult();
|
||||
$gridResults = $builder->get()->getResultArray();
|
||||
|
||||
// Partner (Agent) login: per–vehicle-type retention from partner_retention_rate (+ partner_vehicle_type name match to grid vehicle_type)
|
||||
if (strtolower(trim((string) $role)) === 'agent') {
|
||||
$agentId = (int) trim((string) ($request->getGet('logged_id') ?? $request->getGet('agent_id') ?? 0));
|
||||
if ($agentId > 0) {
|
||||
$db = \Config\Database::connect();
|
||||
$gridResults = PartnerPayoutGridRetention::applyToRows($gridResults, $agentId, $db);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Combine Grid Results with Dropdown Meta-data
|
||||
$responseData = [
|
||||
|
||||
@ -1211,7 +1211,9 @@ public function partnerDetails($id)
|
||||
pa.mobile,
|
||||
pa.email,
|
||||
pa.is_active,
|
||||
ps.name AS manager_name
|
||||
ps.name AS manager_name,
|
||||
ps.mobile AS manager_mobile,
|
||||
ps.email AS manager_email
|
||||
')
|
||||
->join('partner_staff ps', 'ps.id = pa.manager_id', 'left')
|
||||
->where('pa.id', $id)
|
||||
@ -1221,24 +1223,57 @@ public function partnerDetails($id)
|
||||
throw new \RuntimeException('Partner not found.', 404);
|
||||
}
|
||||
|
||||
// -- 2. Policy counts + premium + commission
|
||||
// mapped_policies = ALL policies under this agent (226)
|
||||
// issued_policies = policy_number IS NOT NULL AND is_active = 1 (217)
|
||||
// pending_policies = policy_number IS NULL (9 — raised but not yet issued)
|
||||
// total_premium / commission = from issued policies only
|
||||
// -- 2. Overview = current Indian financial year (Apr–Mar, Asia/Kolkata)
|
||||
$fy = $this->indianFinancialYearBounds(null);
|
||||
$fyStart = $fy['start'];
|
||||
$fyEnd = $fy['end'];
|
||||
$overviewFy = $fy['label'];
|
||||
|
||||
$policyStats = $this->db->table('partner_policy pp')
|
||||
->select('
|
||||
COUNT(pp.id) AS mapped_policies,
|
||||
SUM(CASE WHEN pp.policy_number IS NOT NULL AND pp.is_active = 1 AND pp.premium_amount IS NOT NULL THEN 1 ELSE 0 END) AS issued_policies,
|
||||
SUM(CASE WHEN pp.policy_number IS NULL THEN 1 ELSE 0 END) AS pending_policies,
|
||||
COALESCE(SUM(CASE WHEN pp.policy_number IS NOT NULL THEN pp.premium_amount ELSE 0 END), 0) AS total_premium,
|
||||
COALESCE(SUM(CASE WHEN pp.policy_number IS NOT NULL THEN pp.premium_amount * 0.15 ELSE 0 END), 0) AS commission_earned
|
||||
')
|
||||
->select("
|
||||
SUM(CASE WHEN pp.policy_number IS NOT NULL AND pp.issued_date >= '{$fyStart}' AND pp.issued_date <= '{$fyEnd}' THEN 1 ELSE 0 END) AS issued_policies,
|
||||
SUM(CASE WHEN pp.policy_number IS NULL AND DATE(pp.created_on) >= '{$fyStart}' AND DATE(pp.created_on) <= '{$fyEnd}' THEN 1 ELSE 0 END) AS pending_policies,
|
||||
COALESCE(SUM(CASE WHEN pp.policy_number IS NOT NULL AND pp.issued_date >= '{$fyStart}' AND pp.issued_date <= '{$fyEnd}' THEN pp.premium_amount ELSE 0 END), 0) AS total_premium,
|
||||
COALESCE(SUM(CASE WHEN pp.policy_number IS NOT NULL AND pp.issued_date >= '{$fyStart}' AND pp.issued_date <= '{$fyEnd}' THEN COALESCE(pp.commission_amount, 0) ELSE 0 END), 0) AS commission_earned
|
||||
", false)
|
||||
->where('pp.agent_id', $id)
|
||||
->get()->getRowArray();
|
||||
|
||||
// ── 3. Enquiry counts
|
||||
// enquiry_status enum: To be assigned | Assigned | In progress | Completed
|
||||
$policyStats['mapped_policies'] = (int) ($policyStats['issued_policies'] ?? 0) + (int) ($policyStats['pending_policies'] ?? 0);
|
||||
|
||||
$agentId = (int) $id;
|
||||
|
||||
$paidRow = $this->db->table('partner_invoice_items pii')
|
||||
->select('COALESCE(SUM(COALESCE(pii.commission_amount, 0)), 0) AS commission_paid', false)
|
||||
->join('partner_policy pp', 'pp.id = pii.policy_id', 'inner')
|
||||
->join('partner_invoice pi', 'pi.id = pii.invoice_id AND pi.is_active = 1', 'inner')
|
||||
->where('pp.agent_id', $agentId)
|
||||
->where('pii.is_active', 1)
|
||||
->where('pp.is_active', 1)
|
||||
->where('pp.policy_number IS NOT NULL')
|
||||
->where('pp.issued_date >=', $fyStart)
|
||||
->where('pp.issued_date <=', $fyEnd)
|
||||
->where(
|
||||
'JSON_SEARCH(pi.agent_id, \'one\', CAST(' . $agentId . ' AS CHAR)) IS NOT NULL',
|
||||
null,
|
||||
false
|
||||
)
|
||||
->where(
|
||||
'EXISTS (SELECT 1 FROM partner_invoice_utr piu WHERE piu.invoice_id = pi.id AND piu.is_active = 1 AND piu.amount > 0)',
|
||||
null,
|
||||
false
|
||||
)
|
||||
->get()->getRowArray();
|
||||
|
||||
$commissionEarned = (float) ($policyStats['commission_earned'] ?? 0);
|
||||
$commissionPaidRaw = (float) ($paidRow['commission_paid'] ?? 0);
|
||||
$commissionPaid = min($commissionPaidRaw, $commissionEarned);
|
||||
$commissionUnpaid = max(0, $commissionEarned - $commissionPaid);
|
||||
|
||||
$policyStats['commission_paid'] = $commissionPaid;
|
||||
$policyStats['commission_unpaid'] = $commissionUnpaid;
|
||||
|
||||
// ── 3. Enquiry counts (created in current FY)
|
||||
$enquiryStats = $this->db->table('partner_enquiry pe')
|
||||
->select('
|
||||
COUNT(pe.id) AS enquiry_total,
|
||||
@ -1247,10 +1282,11 @@ public function partnerDetails($id)
|
||||
')
|
||||
->where('pe.agent_id', $id)
|
||||
->where('pe.is_active', 1)
|
||||
->where('pe.created_on >=', $fyStart . ' 00:00:00')
|
||||
->where('pe.created_on <=', $fyEnd . ' 23:59:59')
|
||||
->get()->getRowArray();
|
||||
|
||||
// ── 4. Endorsement counts
|
||||
// status is varchar(20) — adjust "Completed" to match your actual values
|
||||
// ── 4. Endorsement counts (created in current FY)
|
||||
$endorseStats = $this->db->table('partner_endorsement_request per')
|
||||
->select('
|
||||
COUNT(per.id) AS endorsement_total,
|
||||
@ -1259,14 +1295,16 @@ public function partnerDetails($id)
|
||||
')
|
||||
->where('per.agent_id', $id)
|
||||
->where('per.is_active', 1)
|
||||
->where('per.created_at >=', $fyStart . ' 00:00:00')
|
||||
->where('per.created_at <=', $fyEnd . ' 23:59:59')
|
||||
->get()->getRowArray();
|
||||
|
||||
// ── Merge everything
|
||||
$data = array_merge(
|
||||
$agent,
|
||||
[
|
||||
'status' => $agent['is_active'] ? 'Active' : 'Inactive',
|
||||
'commission_rate' => 15,
|
||||
'status' => $agent['is_active'] ? 'Active' : 'Inactive',
|
||||
'overview_financial_year' => $overviewFy,
|
||||
],
|
||||
$policyStats ?? [],
|
||||
$enquiryStats ?? [],
|
||||
@ -1490,13 +1528,17 @@ public function partnerRenewals($id)
|
||||
// GET partner/{id}/earnings
|
||||
// partner_policy.agent_id = $id
|
||||
// Groups by issued_date month → month_key (YYYY-MM), month_label (Month YYYY)
|
||||
// paid = MAX(is_data_accuracy_checked) — 1 if all policies in month are checked
|
||||
// No month_key param → returns ALL months (FY filtering done client-side in Dart)
|
||||
// payout = SUM(partner_policy.commission_amount) — total commission for the month
|
||||
// payout_paid = SUM(partner_invoice_items.commission_amount) where policy belongs to agent,
|
||||
// invoice lists this agent in agent_id JSON, and invoice has UTR payment (amount > 0)
|
||||
// payout_unpaid = payout − payout_paid (floored at 0)
|
||||
// FY filter is client-side
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
public function partnerEarnings($id)
|
||||
{
|
||||
$ref = [];
|
||||
$monthKey = $this->request->getGet('month_key') ?? null;
|
||||
$fyParam = $this->request->getGet('financial_year') ?? null;
|
||||
|
||||
try {
|
||||
if (empty($id)) {
|
||||
@ -1508,6 +1550,13 @@ public function partnerEarnings($id)
|
||||
], 200);
|
||||
}
|
||||
|
||||
$agentId = (int) $id;
|
||||
|
||||
$fyBounds = null;
|
||||
if (!empty($fyParam)) {
|
||||
$fyBounds = $this->indianFinancialYearBounds($fyParam);
|
||||
}
|
||||
|
||||
$builder = $this->db->table('partner_policy pp');
|
||||
|
||||
$builder->select("
|
||||
@ -1515,17 +1564,19 @@ public function partnerEarnings($id)
|
||||
DATE_FORMAT(pp.issued_date, '%M %Y') AS month_label,
|
||||
COUNT(pp.id) AS policies,
|
||||
COALESCE(SUM(pp.premium_amount), 0) AS premium,
|
||||
COALESCE(SUM(pp.premium_amount * 0.15), 0) AS commission,
|
||||
COALESCE(SUM(pp.premium_amount * 0.15 * 0.10), 0) AS tds,
|
||||
COALESCE(SUM(pp.premium_amount * 0.15 * 0.90), 0) AS net_payout,
|
||||
MAX(pp.is_data_accuracy_checked) AS paid
|
||||
COALESCE(SUM(COALESCE(pp.commission_amount, 0)), 0) AS payout
|
||||
");
|
||||
|
||||
$builder->where('pp.agent_id', $id);
|
||||
$builder->where('pp.agent_id', $agentId);
|
||||
$builder->where('pp.is_active', 1);
|
||||
$builder->where('pp.policy_number IS NOT NULL');
|
||||
$builder->where('pp.premium_amount IS NOT NULL');
|
||||
|
||||
if ($fyBounds !== null) {
|
||||
$builder->where('pp.issued_date >=', $fyBounds['start']);
|
||||
$builder->where('pp.issued_date <=', $fyBounds['end']);
|
||||
}
|
||||
|
||||
if (!empty($monthKey)) {
|
||||
$builder->where("DATE_FORMAT(pp.issued_date, '%Y-%m')", $monthKey);
|
||||
}
|
||||
@ -1535,13 +1586,63 @@ public function partnerEarnings($id)
|
||||
|
||||
$results = $builder->get()->getResultArray();
|
||||
|
||||
$ref['month_filter'] = $monthKey ?? 'all';
|
||||
$ref['total_records'] = count($results);
|
||||
// Paid commission: invoice line items tied to this agent’s policies, invoice includes agent, UTR settled
|
||||
$paidBuilder = $this->db->table('partner_invoice_items pii');
|
||||
$paidBuilder->select("
|
||||
DATE_FORMAT(pp.issued_date, '%Y-%m') AS month_key,
|
||||
COALESCE(SUM(COALESCE(pii.commission_amount, 0)), 0) AS payout_paid
|
||||
", false);
|
||||
$paidBuilder->join('partner_policy pp', 'pp.id = pii.policy_id', 'inner');
|
||||
$paidBuilder->join('partner_invoice pi', 'pi.id = pii.invoice_id AND pi.is_active = 1', 'inner');
|
||||
$paidBuilder->where('pp.agent_id', $agentId);
|
||||
$paidBuilder->where('pii.is_active', 1);
|
||||
$paidBuilder->where('pp.is_active', 1);
|
||||
$paidBuilder->where('pp.policy_number IS NOT NULL');
|
||||
$paidBuilder->where('pp.premium_amount IS NOT NULL');
|
||||
$paidBuilder->where(
|
||||
'JSON_SEARCH(pi.agent_id, \'one\', CAST(' . $agentId . ' AS CHAR)) IS NOT NULL',
|
||||
null,
|
||||
false
|
||||
);
|
||||
$paidBuilder->where(
|
||||
'EXISTS (SELECT 1 FROM partner_invoice_utr piu WHERE piu.invoice_id = pi.id AND piu.is_active = 1 AND piu.amount > 0)',
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
if (empty($results)) {
|
||||
throw new \RuntimeException('No earning data found for this partner.', 404);
|
||||
if ($fyBounds !== null) {
|
||||
$paidBuilder->where('pp.issued_date >=', $fyBounds['start']);
|
||||
$paidBuilder->where('pp.issued_date <=', $fyBounds['end']);
|
||||
}
|
||||
|
||||
if (!empty($monthKey)) {
|
||||
$paidBuilder->where("DATE_FORMAT(pp.issued_date, '%Y-%m')", $monthKey);
|
||||
}
|
||||
|
||||
$paidBuilder->groupBy("DATE_FORMAT(pp.issued_date, '%Y-%m')");
|
||||
$paidRows = $paidBuilder->get()->getResultArray();
|
||||
|
||||
$paidMap = [];
|
||||
foreach ($paidRows as $pr) {
|
||||
$paidMap[$pr['month_key']] = (float) $pr['payout_paid'];
|
||||
}
|
||||
|
||||
foreach ($results as &$row) {
|
||||
$mk = $row['month_key'];
|
||||
$payout = (float) $row['payout'];
|
||||
$payoutPaid = min((float) ($paidMap[$mk] ?? 0), $payout);
|
||||
$row['payout'] = $payout;
|
||||
$row['payout_paid'] = $payoutPaid;
|
||||
$row['payout_unpaid'] = max(0, $payout - $payoutPaid);
|
||||
$row['premium'] = (float) $row['premium'];
|
||||
$row['policies'] = (int) $row['policies'];
|
||||
}
|
||||
unset($row);
|
||||
|
||||
$ref['month_filter'] = $monthKey ?? 'all';
|
||||
$ref['financial_year'] = ($fyBounds !== null) ? $fyBounds['label'] : 'all';
|
||||
$ref['total_records'] = count($results);
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'code' => 200,
|
||||
@ -1581,4 +1682,45 @@ public function partnerEarnings($id)
|
||||
], 200);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indian financial year Apr 1 (Y) → Mar 31 (Y+1).
|
||||
*
|
||||
* @param string|null $fyStr e.g. "2025-2026" (first year is the April year)
|
||||
* @return array{start: string, end: string, label: string} Y-m-d bounds + label "2025-2026"
|
||||
*/
|
||||
private function indianFinancialYearBounds(?string $fyStr = null): array
|
||||
{
|
||||
if ($fyStr !== null && preg_match('/^(\d{4})-(\d{4})$/', trim($fyStr), $m)) {
|
||||
$y1 = (int) $m[1];
|
||||
$y2 = (int) $m[2];
|
||||
if ($y2 !== $y1 + 1) {
|
||||
$y1 = $y1 - 1;
|
||||
$y2 = $y1 + 1;
|
||||
}
|
||||
$start = sprintf('%04d-04-01', $y1);
|
||||
$end = sprintf('%04d-03-31', $y2);
|
||||
|
||||
return [
|
||||
'start' => $start,
|
||||
'end' => $end,
|
||||
'label' => $y1 . '-' . $y2,
|
||||
];
|
||||
}
|
||||
|
||||
$tz = new \DateTimeZone('Asia/Kolkata');
|
||||
$now = new \DateTime('now', $tz);
|
||||
$y = (int) $now->format('Y');
|
||||
$mo = (int) $now->format('n');
|
||||
$y1 = $mo >= 4 ? $y : $y - 1;
|
||||
$y2 = $y1 + 1;
|
||||
$start = sprintf('%04d-04-01', $y1);
|
||||
$end = sprintf('%04d-03-31', $y2);
|
||||
|
||||
return [
|
||||
'start' => $start,
|
||||
'end' => $end,
|
||||
'label' => $y1 . '-' . $y2,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Libraries\PartnerPayoutGridRetention;
|
||||
use CodeIgniter\RESTful\ResourceController;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
@ -2580,16 +2581,10 @@ class ExcelExportController extends ResourceController
|
||||
|
||||
$isAgent = strtolower($role) === 'agent';
|
||||
|
||||
// ✅ Get retention rate (only for Agent)
|
||||
$retentionRate = 0;
|
||||
// ✅ Per–vehicle retention map (Agent only); partner_agent.retention_rate is unused
|
||||
$retentionMap = [];
|
||||
if ($isAgent && $loggedId !== '') {
|
||||
$agent = $this->db->table('partner_agent')
|
||||
->select('retention_rate')
|
||||
->where('id', $loggedId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
$retentionRate = isset($agent['retention_rate']) ? (float)$agent['retention_rate'] : 0;
|
||||
$retentionMap = PartnerPayoutGridRetention::buildRetentionMap($this->db, (int) $loggedId);
|
||||
}
|
||||
|
||||
// ✅ Main query
|
||||
@ -2669,15 +2664,22 @@ class ExcelExportController extends ResourceController
|
||||
|
||||
foreach ($rows as $row) {
|
||||
|
||||
$comp = isset($row['comp']) ? (float)$row['comp'] : 0;
|
||||
$tp = isset($row['tp']) ? (float)$row['tp'] : 0;
|
||||
$od = isset($row['od']) ? (float)$row['od'] : 0;
|
||||
$comp = isset($row['comp']) ? (float) $row['comp'] : 0;
|
||||
$tp = isset($row['tp']) ? (float) $row['tp'] : 0;
|
||||
$od = isset($row['od']) ? (float) $row['od'] : 0;
|
||||
|
||||
// ✅ Apply retention only for Agent
|
||||
if ($isAgent) {
|
||||
$comp -= $retentionRate;
|
||||
$tp -= $retentionRate;
|
||||
$od -= $retentionRate;
|
||||
$compOut = $comp;
|
||||
$tpOut = $tp;
|
||||
$odOut = $od;
|
||||
|
||||
// ✅ Agent: subtract partner_retention_rate when grid vehicle_type matches partner_vehicle_type for this agent
|
||||
if ($isAgent && $retentionMap !== []) {
|
||||
$rate = PartnerPayoutGridRetention::retentionForRow($row, $retentionMap);
|
||||
if ($rate !== null) {
|
||||
$compOut = PartnerPayoutGridRetention::adjustPayoutValue($row['comp'] ?? 0, $rate);
|
||||
$tpOut = PartnerPayoutGridRetention::adjustPayoutValue($row['tp'] ?? 0, $rate);
|
||||
$odOut = PartnerPayoutGridRetention::adjustPayoutValue($row['od'] ?? 0, $rate);
|
||||
}
|
||||
}
|
||||
|
||||
$line = [
|
||||
@ -2691,12 +2693,12 @@ class ExcelExportController extends ResourceController
|
||||
// ✅ Dynamic columns (Agent only)
|
||||
if ($isAgent) {
|
||||
if ($planType === 'comp') {
|
||||
$line[] = $comp;
|
||||
$line[] = $compOut;
|
||||
} elseif ($planType === 'tp') {
|
||||
$line[] = $tp;
|
||||
$line[] = $tpOut;
|
||||
} elseif ($planType === 'od') {
|
||||
$line[] = $od;
|
||||
}
|
||||
$line[] = $odOut;
|
||||
}
|
||||
} else {
|
||||
// ✅ Manager / Accounts → always all
|
||||
$line[] = $comp;
|
||||
|
||||
166
app/Libraries/PartnerPayoutGridRetention.php
Normal file
166
app/Libraries/PartnerPayoutGridRetention.php
Normal file
@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
|
||||
/**
|
||||
* Partner (agent) grid: match payout grid vehicle_type to partner_vehicle_type via
|
||||
* partner_retention_rate (agent_id + vehicle_type_id), subtract retention from comp/tp/od.
|
||||
* Values <= 0 after subtraction are returned as '-'.
|
||||
*/
|
||||
class PartnerPayoutGridRetention
|
||||
{
|
||||
public static function normalizeVehicleTypeLabel(?string $s): string
|
||||
{
|
||||
if ($s === null || $s === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return strtolower(trim(preg_replace('/\s+/u', ' ', $s)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, float> normalized name => rate, and id:{vehicle_type_id} => rate
|
||||
*/
|
||||
public static function buildRetentionMap(BaseConnection $db, int $agentId): array
|
||||
{
|
||||
if ($agentId <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = $db->table('partner_retention_rate prr')
|
||||
->select('prr.vehicle_type_id, prr.retention_rate, pvt.vehicle_type')
|
||||
->join('partner_vehicle_type pvt', 'pvt.id = prr.vehicle_type_id', 'left')
|
||||
->where('prr.agent_id', $agentId)
|
||||
->where('prr.is_active', 1)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$map = [];
|
||||
foreach ($rows as $r) {
|
||||
if (! array_key_exists('retention_rate', $r) || $r['retention_rate'] === null || $r['retention_rate'] === '') {
|
||||
continue;
|
||||
}
|
||||
$rate = (float) $r['retention_rate'];
|
||||
$name = isset($r['vehicle_type']) ? trim((string) $r['vehicle_type']) : '';
|
||||
if ($name !== '') {
|
||||
$map[self::normalizeVehicleTypeLabel($name)] = $rate;
|
||||
}
|
||||
$vid = isset($r['vehicle_type_id']) ? (int) $r['vehicle_type_id'] : 0;
|
||||
if ($vid > 0) {
|
||||
$map['id:' . $vid] = $rate;
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public static function applyToRows(array $rows, int $agentId, BaseConnection $db): array
|
||||
{
|
||||
if ($agentId <= 0 || $rows === []) {
|
||||
return $rows;
|
||||
}
|
||||
|
||||
$map = self::buildRetentionMap($db, $agentId);
|
||||
if ($map === []) {
|
||||
return $rows;
|
||||
}
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
$key = self::normalizeVehicleTypeLabel(isset($row['vehicle_type']) ? (string) $row['vehicle_type'] : '');
|
||||
$rate = null;
|
||||
if ($key !== '' && isset($map[$key])) {
|
||||
$rate = $map[$key];
|
||||
} else {
|
||||
$vid = isset($row['vehicle_type_id']) ? (int) $row['vehicle_type_id'] : 0;
|
||||
if ($vid > 0 && isset($map['id:' . $vid])) {
|
||||
$rate = $map['id:' . $vid];
|
||||
}
|
||||
}
|
||||
if ($rate === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (['comp', 'tp', 'od'] as $col) {
|
||||
if (! array_key_exists($col, $row)) {
|
||||
continue;
|
||||
}
|
||||
$raw = $row[$col];
|
||||
if ($raw === null || $raw === '' || $raw === '-') {
|
||||
continue;
|
||||
}
|
||||
$base = self::toFloat($raw);
|
||||
if ($base === null) {
|
||||
continue;
|
||||
}
|
||||
$adj = $base - $rate;
|
||||
if ($adj <= 0) {
|
||||
$row[$col] = '-';
|
||||
} elseif (abs($adj - round($adj)) < 0.00001) {
|
||||
$row[$col] = (string) (int) round($adj);
|
||||
} else {
|
||||
$row[$col] = rtrim(rtrim(number_format($adj, 2, '.', ''), '0'), '.');
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public static function retentionForRow(array $row, array $map): ?float
|
||||
{
|
||||
$key = self::normalizeVehicleTypeLabel(isset($row['vehicle_type']) ? (string) $row['vehicle_type'] : '');
|
||||
if ($key !== '' && isset($map[$key])) {
|
||||
return $map[$key];
|
||||
}
|
||||
$vid = isset($row['vehicle_type_id']) ? (int) $row['vehicle_type_id'] : 0;
|
||||
if ($vid > 0 && isset($map['id:' . $vid])) {
|
||||
return $map['id:' . $vid];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float|int|string|null '-' when adjusted <= 0, else numeric display
|
||||
*/
|
||||
public static function adjustPayoutValue($raw, float $rate)
|
||||
{
|
||||
if ($raw === null || $raw === '' || $raw === '-') {
|
||||
return $raw;
|
||||
}
|
||||
$base = self::toFloat($raw);
|
||||
if ($base === null) {
|
||||
return $raw;
|
||||
}
|
||||
$adj = $base - $rate;
|
||||
if ($adj <= 0) {
|
||||
return '-';
|
||||
}
|
||||
if (abs($adj - round($adj)) < 0.00001) {
|
||||
return (int) round($adj);
|
||||
}
|
||||
|
||||
return rtrim(rtrim(number_format($adj, 2, '.', ''), '0'), '.');
|
||||
}
|
||||
|
||||
private static function toFloat($raw): ?float
|
||||
{
|
||||
if (is_numeric($raw)) {
|
||||
return (float) $raw;
|
||||
}
|
||||
$clean = preg_replace('/[^0-9.\-]/', '', (string) $raw);
|
||||
if ($clean === '' || $clean === '-' || $clean === '.') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (float) $clean;
|
||||
}
|
||||
}
|
||||
23
app/Models/PartnerRetentionRateModel.php
Normal file
23
app/Models/PartnerRetentionRateModel.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PartnerRetentionRateModel extends Model
|
||||
{
|
||||
protected $table = 'partner_retention_rate';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'agent_id',
|
||||
'vehicle_type_id',
|
||||
'retention_rate',
|
||||
'is_active',
|
||||
'created_by',
|
||||
'created_on',
|
||||
'updated_by',
|
||||
'updated_on',
|
||||
];
|
||||
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
18
app/Models/PartnerVehicleTypeModel.php
Normal file
18
app/Models/PartnerVehicleTypeModel.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PartnerVehicleTypeModel extends Model
|
||||
{
|
||||
protected $table = 'partner_vehicle_type';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'vehicle_type',
|
||||
'is_active',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user