diff --git a/app/Config/Routes.php b/app/Config/Routes.php index a63928e..57f24fe 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -76,6 +76,8 @@ $routes->group('api', ['filter' => ["jwtAuth:1,2,3,4,agent","appSignature"] ], f $routes->post('agent/uploadAgentIncentiveFile', 'AgentController::uploadAgentIncentiveFile'); $routes->get('agent/deleteAgentIncentiveFile', 'AgentController::deleteAgentIncentiveFile'); $routes->get('agent/downloadAgentIncentiveFile', 'AgentController::downloadAgentIncentiveFile'); + $routes->get('agent/exportRetentionRateExcel', 'ExcelExportController::exportAgentRetentionRateExcel'); + $routes->post('agent/importRetentionRateExcel', 'ExcelExportController::importAgentRetentionRateExcel'); $routes->get('grid', 'AgentIncentiveController::getGridData'); $routes->post('grid/upload', 'AgentIncentiveController::uploadGridFile'); diff --git a/app/Controllers/AgentIncentiveController.php b/app/Controllers/AgentIncentiveController.php index 123c211..a126741 100644 --- a/app/Controllers/AgentIncentiveController.php +++ b/app/Controllers/AgentIncentiveController.php @@ -330,6 +330,9 @@ class AgentIncentiveController extends ResourceController $builder->select('id, insurer, vehicle_type, segment, rto, comp, tp, od, fuel,broker_name,comp,tp,od,remarks,broker_comp, broker_tp,broker_od,created_by,created_at,updated_by,updated_at'); } + } else { + // Agent (and any other role): always return vehicle_type + payout columns for retention / partner_VT matching + $builder->select('id, insurer, vehicle_type, segment, rto, comp, tp, od, fuel, broker_name, remarks, broker_comp, broker_tp, broker_od, partner_agent_incentive_file_id, created_at, updated_at', false); } if ($file_id) { @@ -361,12 +364,35 @@ class AgentIncentiveController extends ResourceController $gridResults = $builder->get()->getResultArray(); - // Partner (Agent) login: per–vehicle-type retention from partner_retention_rate (+ partner_vehicle_type name match to grid vehicle_type) + // Manager / Accounts: comp/tp/od — no calculation; invalid → '-' + if (in_array($role, ['Manager', 'Accounts'], true)) { + $gridResults = PartnerPayoutGridRetention::applyManagerAccountsDisplayToRows($gridResults); + } + + // Partner (Agent): partner_VT / partner_RR + adjusted comp/tp/od when logged_id is present 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(); + $db = \Config\Database::connect(); $gridResults = PartnerPayoutGridRetention::applyToRows($gridResults, $agentId, $db); + } else { + foreach ($gridResults as &$r) { + $r['oa'] = $r['comp'] ?? null; + $r['ob'] = $r['tp'] ?? null; + $r['oc'] = $r['od'] ?? null; + $r['partner_VT'] = '-'; + $r['partner_RR'] = '-'; + if (array_key_exists('comp', $r)) { + $r['comp'] = '-'; + } + if (array_key_exists('tp', $r)) { + $r['tp'] = '-'; + } + if (array_key_exists('od', $r)) { + $r['od'] = '-'; + } + } + unset($r); } } diff --git a/app/Controllers/ExcelExportController.php b/app/Controllers/ExcelExportController.php index 9001fde..e372cc4 100644 --- a/app/Controllers/ExcelExportController.php +++ b/app/Controllers/ExcelExportController.php @@ -7,6 +7,8 @@ use CodeIgniter\RESTful\ResourceController; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Xlsx; +use PhpOffice\PhpSpreadsheet\IOFactory; +use PhpOffice\PhpSpreadsheet\Cell\DataValidation; use PhpOffice\PhpSpreadsheet\Style\Border; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; // <--- THIS MUST BE HERE use PhpOffice\PhpSpreadsheet\Style\Alignment; @@ -2581,10 +2583,13 @@ class ExcelExportController extends ResourceController $isAgent = strtolower($role) === 'agent'; - // ✅ Per–vehicle retention map (Agent only); partner_agent.retention_rate is unused - $retentionMap = []; + // ✅ partner_VT / partner_RR + Agent comp/tp/od = partner_RR − original (Agent only) + $metaMap = []; + $masterVt = []; if ($isAgent && $loggedId !== '') { - $retentionMap = PartnerPayoutGridRetention::buildRetentionMap($this->db, (int) $loggedId); + $agentPk = (int) $loggedId; + $metaMap = PartnerPayoutGridRetention::buildPartnerMetaMap($this->db, $agentPk); + $masterVt = PartnerPayoutGridRetention::buildMasterNormLabelToVehicleTypeIdMap($this->db); } // ✅ Main query @@ -2637,7 +2642,9 @@ class ExcelExportController extends ResourceController // ========================= // ✅ HEADERS // ========================= - $headers = ['S.No', 'Insurer', 'Vehicle Type', 'Segment', 'RTO']; + $headers = ['S.No', 'Insurer', 'Vehicle Type']; + $headers[] = 'Segment'; + $headers[] = 'RTO'; if ($isAgent) { if ($planType === 'comp') { @@ -2664,21 +2671,27 @@ 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; + $compRaw = $row['comp'] ?? null; + $tpRaw = $row['tp'] ?? null; + $odRaw = $row['od'] ?? null; - $compOut = $comp; - $tpOut = $tp; - $odOut = $od; + $compOut = PartnerPayoutGridRetention::gridPayoutDisplayManagerAccounts($compRaw); + $tpOut = PartnerPayoutGridRetention::gridPayoutDisplayManagerAccounts($tpRaw); + $odOut = PartnerPayoutGridRetention::gridPayoutDisplayManagerAccounts($odRaw); - // ✅ 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); + $pm = null; + // ✅ Agent + logged_id: PRR match + per-column calc only when original value is positive + if ($isAgent && $loggedId !== '') { + $pm = PartnerPayoutGridRetention::resolvePartnerMetaForGridRow($row, $metaMap, $masterVt); + if ($pm === null) { + $compOut = '-'; + $tpOut = '-'; + $odOut = '-'; + } else { + $rr = (float) $pm['partner_RR']; + $compOut = PartnerPayoutGridRetention::agentPayoutColumnFromPartnerRr($compRaw, $rr); + $tpOut = PartnerPayoutGridRetention::agentPayoutColumnFromPartnerRr($tpRaw, $rr); + $odOut = PartnerPayoutGridRetention::agentPayoutColumnFromPartnerRr($odRaw, $rr); } } @@ -2700,10 +2713,10 @@ class ExcelExportController extends ResourceController $line[] = $odOut; } } else { - // ✅ Manager / Accounts → always all - $line[] = $comp; - $line[] = $tp; - $line[] = $od; + // ✅ Manager / Accounts → formatted comp/tp/od (invalid → '-') + $line[] = $compOut; + $line[] = $tpOut; + $line[] = $odOut; } $line[] = $row['remarks'] ?? '-'; @@ -2725,6 +2738,388 @@ class ExcelExportController extends ResourceController } } + public function exportAgentRetentionRateExcel() + { + try { + $vehicleTypes = $this->db->table('partner_vehicle_type') + ->select('id, vehicle_type') + ->where('is_active', 1) + ->orderBy('vehicle_type', 'ASC') + ->get() + ->getResultArray(); + + $agents = $this->db->table('partner_agent') + ->select('id, agent_code') + ->where('is_active', 1) + ->where('agent_code IS NOT NULL', null, false) + ->where('agent_code !=', '') + ->orderBy('agent_code', 'ASC') + ->get() + ->getResultArray(); + + $rates = $this->db->table('partner_retention_rate') + ->select('agent_id, vehicle_type_id, retention_rate') + ->where('is_active', 1) + ->get() + ->getResultArray(); + + $rateMap = []; + foreach ($rates as $r) { + $aId = (int) ($r['agent_id'] ?? 0); + $vId = (int) ($r['vehicle_type_id'] ?? 0); + if ($aId <= 0 || $vId <= 0) { + continue; + } + $rateMap[$aId . '_' . $vId] = (float) ($r['retention_rate'] ?? 0); + } + + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $sheet->setCellValue('A1', 'Agent Code / Vehicle Type'); + $colIndex = 2; + foreach ($vehicleTypes as $vt) { + $cell = Coordinate::stringFromColumnIndex($colIndex++) . '1'; + $sheet->setCellValue($cell, (string) ($vt['vehicle_type'] ?? '')); + } + + $rowIndex = 2; + foreach ($agents as $agent) { + $agentId = (int) ($agent['id'] ?? 0); + $sheet->setCellValue('A' . $rowIndex, (string) ($agent['agent_code'] ?? '')); + + $colIndex = 2; + foreach ($vehicleTypes as $vt) { + $vtId = (int) ($vt['id'] ?? 0); + $key = $agentId . '_' . $vtId; + $cell = Coordinate::stringFromColumnIndex($colIndex++) . $rowIndex; + $sheet->setCellValue($cell, (float) ($rateMap[$key] ?? 0)); + } + $rowIndex++; + } + + $lastCol = Coordinate::stringFromColumnIndex(max(1, count($vehicleTypes) + 1)); + $lastRow = max(1, $rowIndex - 1); + $sheet->getStyle("A1:{$lastCol}1")->getFont()->setBold(true); + $sheet->getStyle("A1:{$lastCol}{$lastRow}")->getBorders()->getAllBorders()->setBorderStyle(Border::BORDER_THIN); + + // Input validation for retention cells: + // - Applies to B2:lastCol(lastRow) + // - Allows decimal value between 0 and 100 only + // - Blocks negatives, values > 100, and text/special characters + if (count($vehicleTypes) > 0 && $lastRow >= 2) { + $validation = $sheet->getCell('B2')->getDataValidation(); + $validation->setType(DataValidation::TYPE_DECIMAL); + $validation->setErrorStyle(DataValidation::STYLE_STOP); + $validation->setAllowBlank(true); + $validation->setShowInputMessage(true); + $validation->setShowErrorMessage(true); + $validation->setOperator(DataValidation::OPERATOR_BETWEEN); + $validation->setFormula1('0'); + $validation->setFormula2('100'); + $validation->setPromptTitle('Valid retention rate'); + $validation->setPrompt('Enter a number between 0 and 100'); + $validation->setErrorTitle('Invalid value'); + $validation->setError('Only numeric values from 0 to 100 are allowed.'); + for ($row = 2; $row <= $lastRow; $row++) { + for ($col = 2; $col <= (count($vehicleTypes) + 1); $col++) { + $cell = Coordinate::stringFromColumnIndex($col) . $row; + $sheet->getCell($cell)->setDataValidation(clone $validation); + } + } + } + + for ($i = 1; $i <= count($vehicleTypes) + 1; $i++) { + $col = Coordinate::stringFromColumnIndex($i); + $sheet->getColumnDimension($col)->setAutoSize(true); + } + + $fileName = 'RentationRate_' . date('Ymd_His') . '.xlsx'; + while (ob_get_level() > 0) { + ob_end_clean(); + } + header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + header('Content-Disposition: attachment; filename="' . $fileName . '"'); + header('Cache-Control: max-age=0'); + $writer = new Xlsx($spreadsheet); + $writer->save('php://output'); + exit(); + } catch (\Throwable $e) { + return $this->response->setJSON([ + 'status' => 'failed', + 'code' => 500, + 'data' => $e->getMessage(), + ]); + } + } + + public function importAgentRetentionRateExcel() + { + try { + $file = $this->request->getFile('retention_excel'); + if (!$file || !$file->isValid()) { + return $this->respond([ + 'status' => 'failed', + 'code' => 200, + 'data' => 'Valid Excel file is required in retention_excel', + ], 200); + } + + $spreadsheet = IOFactory::load($file->getTempName()); + $sheet = $spreadsheet->getActiveSheet(); + $rows = $sheet->toArray(null, true, true, false); + + if (empty($rows) || empty($rows[0])) { + return $this->respond([ + 'status' => 'failed', + 'code' => 200, + 'data' => 'Excel file is empty', + ], 200); + } + + $headerRow = $rows[0]; + $headerVehicleTypes = []; + for ($col = 1; $col < count($headerRow); $col++) { + $name = trim((string) ($headerRow[$col] ?? '')); + if ($name === '') { + continue; + } + $headerVehicleTypes[$col] = strtolower($name); + } + + if (empty($headerVehicleTypes)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 200, + 'data' => 'Vehicle type headers are missing in row 1', + ], 200); + } + + $vehicleTypeRows = $this->db->table('partner_vehicle_type') + ->select('id, vehicle_type') + ->where('is_active', 1) + ->get() + ->getResultArray(); + $vehicleTypeMap = []; + foreach ($vehicleTypeRows as $vt) { + $key = strtolower(trim((string) ($vt['vehicle_type'] ?? ''))); + if ($key !== '') { + $vehicleTypeMap[$key] = (int) $vt['id']; + } + } + + $agentRows = $this->db->table('partner_agent') + ->select('id, agent_code') + ->where('is_active', 1) + ->where('agent_code IS NOT NULL', null, false) + ->where('agent_code !=', '') + ->get() + ->getResultArray(); + $agentMap = []; + foreach ($agentRows as $agent) { + $key = strtolower(trim((string) ($agent['agent_code'] ?? ''))); + if ($key !== '') { + $agentMap[$key] = (int) $agent['id']; + } + } + + $unknownVehicleTypes = []; + $validHeaderVehicleTypes = []; + foreach ($headerVehicleTypes as $colIndex => $vehicleTypeName) { + if (isset($vehicleTypeMap[$vehicleTypeName])) { + $validHeaderVehicleTypes[$colIndex] = $vehicleTypeMap[$vehicleTypeName]; + } else { + $unknownVehicleTypes[] = trim((string) ($headerRow[$colIndex] ?? '')); + } + } + + if (empty($validHeaderVehicleTypes)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 200, + 'data' => 'No valid vehicle type headers found in DB', + 'report' => [ + 'unknown_vehicle_types' => array_values(array_unique($unknownVehicleTypes)), + ], + ], 200); + } + + $existingRows = $this->db->table('partner_retention_rate') + ->select('id, agent_id, vehicle_type_id, retention_rate') + ->get() + ->getResultArray(); + + $existingMap = []; + foreach ($existingRows as $er) { + $key = ((int) $er['agent_id']) . '_' . ((int) $er['vehicle_type_id']); + $existingMap[$key] = [ + 'id' => (int) $er['id'], + 'rate' => (float) ($er['retention_rate'] ?? 0), + ]; + } + + $now = date('Y-m-d H:i:s'); + $inserted = 0; + $updated = 0; + $skippedZero = 0; + $skippedSame = 0; + $skippedEmptyRow = 0; + $skippedEmptyCell = 0; + $skippedUnknownAgentRow = 0; + $invalidRange = 0; + $invalidNumber = 0; + $unknownAgentCodes = []; + $invalidCells = []; + $agentWiseSkippedCounts = []; + + $this->db->transStart(); + + for ($r = 1; $r < count($rows); $r++) { + $row = $rows[$r]; + $agentCode = strtolower(trim((string) ($row[0] ?? ''))); + if ($agentCode === '') { + $skippedEmptyRow++; + continue; + } + if (!isset($agentMap[$agentCode])) { + $skippedUnknownAgentRow++; + $unknownAgentCodes[] = (string) ($row[0] ?? ''); + continue; + } + $agentId = $agentMap[$agentCode]; + $agentCodeOriginal = trim((string) ($row[0] ?? '')); + if ($agentCodeOriginal !== '' && !isset($agentWiseSkippedCounts[$agentCodeOriginal])) { + $agentWiseSkippedCounts[$agentCodeOriginal] = 0; + } + + foreach ($validHeaderVehicleTypes as $colIndex => $vehicleTypeId) { + $rawRate = $row[$colIndex] ?? null; + $rawText = trim((string) $rawRate); + + if ($rawText === '') { + $skippedEmptyCell++; + if ($agentCodeOriginal !== '') { + $agentWiseSkippedCounts[$agentCodeOriginal]++; + } + continue; + } + + if (!is_numeric($rawText)) { + $invalidNumber++; + if ($agentCodeOriginal !== '') { + $agentWiseSkippedCounts[$agentCodeOriginal]++; + } + if (count($invalidCells) < 20) { + $invalidCells[] = "R" . ($r + 1) . "C" . ($colIndex + 1) . " invalid number"; + } + continue; + } + + $rate = (float) $rawText; + if (!is_finite($rate)) { + $invalidNumber++; + if ($agentCodeOriginal !== '') { + $agentWiseSkippedCounts[$agentCodeOriginal]++; + } + if (count($invalidCells) < 20) { + $invalidCells[] = "R" . ($r + 1) . "C" . ($colIndex + 1) . " invalid number"; + } + continue; + } + + if ($rate < 0 || $rate > 100) { + $invalidRange++; + if ($agentCodeOriginal !== '') { + $agentWiseSkippedCounts[$agentCodeOriginal]++; + } + if (count($invalidCells) < 20) { + $invalidCells[] = "R" . ($r + 1) . "C" . ($colIndex + 1) . " out of range (0-100)"; + } + continue; + } + + if ((float) $rate == 0.0) { + $skippedZero++; + if ($agentCodeOriginal !== '') { + $agentWiseSkippedCounts[$agentCodeOriginal]++; + } + continue; + } + + $key = $agentId . '_' . $vehicleTypeId; + $existing = $existingMap[$key] ?? null; + + if ($existing) { + if (round((float) $existing['rate'], 2) === round((float) $rate, 2)) { + $skippedSame++; + if ($agentCodeOriginal !== '') { + $agentWiseSkippedCounts[$agentCodeOriginal]++; + } + continue; + } + $this->db->table('partner_retention_rate') + ->where('id', (int) $existing['id']) + ->update([ + 'retention_rate' => round($rate, 2), + 'is_active' => 1, + 'updated_on' => $now, + ]); + $existingMap[$key]['rate'] = round($rate, 2); + $updated++; + } else { + $this->db->table('partner_retention_rate') + ->insert([ + 'agent_id' => $agentId, + 'vehicle_type_id' => $vehicleTypeId, + 'retention_rate' => round($rate, 2), + 'is_active' => 1, + 'created_on' => $now, + ]); + $existingMap[$key] = [ + 'id' => (int) $this->db->insertID(), + 'rate' => round($rate, 2), + ]; + $inserted++; + } + } + } + + $this->db->transComplete(); + if ($this->db->transStatus() === false) { + throw new \RuntimeException('Import transaction failed'); + } + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => [ + 'inserted_cells' => $inserted, + 'updated_cells' => $updated, + 'skipped_zero' => $skippedZero, + 'skipped_same' => $skippedSame, + 'skipped_empty_row' => $skippedEmptyRow, + 'skipped_empty_cell' => $skippedEmptyCell, + 'skipped_unknown_agent' => $skippedUnknownAgentRow, + 'invalid_range' => $invalidRange, + 'invalid_number' => $invalidNumber, + ], + 'report' => [ + 'unknown_agent_codes' => array_values(array_unique($unknownAgentCodes)), + 'unknown_vehicle_types'=> array_values(array_unique($unknownVehicleTypes)), + 'invalid_cells' => $invalidCells, + 'agent_wise_skipped_counts' => $agentWiseSkippedCounts, + ], + 'message' => 'Retention rate import completed', + ], 200); + } catch (\Throwable $e) { + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'data' => $e->getMessage(), + ], 500); + } + } + } diff --git a/app/Libraries/PartnerPayoutGridRetention.php b/app/Libraries/PartnerPayoutGridRetention.php index 2e108bf..e5655a9 100644 --- a/app/Libraries/PartnerPayoutGridRetention.php +++ b/app/Libraries/PartnerPayoutGridRetention.php @@ -5,12 +5,39 @@ 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 '-'. + * Partner (agent) grid: same data as + * SELECT prr.retention_rate AS partner_RR, prr.vehicle_type_id, pvt.vehicle_type AS partner_VT + * FROM partner_retention_rate prr + * LEFT JOIN partner_vehicle_type pvt ON pvt.id = prr.vehicle_type_id + * WHERE prr.agent_id = ? AND prr.is_active = 1 + * Matched grid row (vehicle type aligns with that PRR row): partner_VT, partner_RR set from DB; + * comp/tp/od = partner_RR − original (null/empty base → 0); result ≤ 0 → '-'. + * No matching PRR row for this grid vehicle type: partner_VT, partner_RR, comp, tp, od → '-'. + * + * Manager/Accounts: applyManagerAccountsDisplayToRows() — comp/tp/od null/empty/zero/negative → '-', no math. */ class PartnerPayoutGridRetention { + /** + * Active partner_retention_rate rows with joined partner_vehicle_type (one query for maps + agent grid meta). + * + * @return array> + */ + private static function fetchPrrPvtRows(BaseConnection $db, int $agentId): array + { + if ($agentId <= 0) { + return []; + } + + return $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(); + } + public static function normalizeVehicleTypeLabel(?string $s): string { if ($s === null || $s === '') { @@ -21,24 +48,181 @@ class PartnerPayoutGridRetention } /** - * @return array normalized name => rate, and id:{vehicle_type_id} => rate + * @param array> $prrRows + * + * @return array */ - public static function buildRetentionMap(BaseConnection $db, int $agentId): array + private static function partnerMetaMapFromPrrRows(array $prrRows): array { - if ($agentId <= 0) { - return []; + $meta = []; + foreach ($prrRows as $r) { + if (! array_key_exists('retention_rate', $r) || $r['retention_rate'] === null || $r['retention_rate'] === '') { + continue; + } + $partnerRr = (float) $r['retention_rate']; + $vtRaw = isset($r['vehicle_type']) ? trim((string) $r['vehicle_type']) : ''; + $partnerVt = $vtRaw !== '' ? $vtRaw : 'N/A'; + $entry = [ + 'partner_VT' => $partnerVt, + 'partner_RR' => $partnerRr, + ]; + if ($vtRaw !== '') { + $meta[self::normalizeVehicleTypeLabel($vtRaw)] = $entry; + } + $vid = isset($r['vehicle_type_id']) ? (int) $r['vehicle_type_id'] : 0; + if ($vid > 0) { + $meta['id:' . $vid] = $entry; + } } - $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) + return $meta; + } + + /** + * partner_vehicle_type.vehicle_type as partner_VT, partner_retention_rate.retention_rate as partner_RR. + * Keys: normalized vehicle name, and id:{vehicle_type_id}. Rows without retention_rate are omitted. + * + * @return array + */ + public static function buildPartnerMetaMap(BaseConnection $db, int $agentId): array + { + return self::partnerMetaMapFromPrrRows(self::fetchPrrPvtRows($db, $agentId)); + } + + /** + * Normalized master label → partner_vehicle_type.id (active rows only). + * Used when grid row text must be resolved to an id for partner_retention_rate lookup. + * + * @return array + */ + public static function buildMasterNormLabelToVehicleTypeIdMap(BaseConnection $db): array + { + $rows = $db->table('partner_vehicle_type') + ->select('id, vehicle_type') + ->where('is_active', 1) ->get() ->getResultArray(); - $map = []; + $out = []; foreach ($rows as $r) { + $k = self::normalizeVehicleTypeLabel($r['vehicle_type'] ?? ''); + if ($k === '') { + continue; + } + $out[$k] = (int) $r['id']; + } + + return $out; + } + + /** + * @param array $metaMap + * @param array|null $masterNormLabelToVtId from buildMasterNormLabelToVehicleTypeIdMap() + * + * @return array{partner_VT: string, partner_RR: float}|null null = no row in partner_retention_rate for this grid vehicle type (agent) + */ + public static function resolvePartnerMetaForGridRow(array $row, array $metaMap, ?array $masterNormLabelToVtId = null): ?array + { + $key = self::normalizeVehicleTypeLabel(isset($row['vehicle_type']) ? (string) $row['vehicle_type'] : ''); + if ($key !== '' && isset($metaMap[$key])) { + return $metaMap[$key]; + } + $vid = isset($row['vehicle_type_id']) ? (int) $row['vehicle_type_id'] : 0; + if ($vid > 0 && isset($metaMap['id:' . $vid])) { + return $metaMap['id:' . $vid]; + } + // Grid text matches master partner_vehicle_type but meta was only keyed by id (e.g. weak pvt join on prr) + if ($masterNormLabelToVtId !== null && $key !== '') { + $mid = $masterNormLabelToVtId[$key] ?? 0; + if ($mid > 0 && isset($metaMap['id:' . $mid])) { + return $metaMap['id:' . $mid]; + } + } + + return null; + } + + /** + * Manager / Accounts: no math — null, empty, '-', unparseable, zero, or negative → '-'; + * else return formatted positive value. + */ + public static function gridPayoutDisplayManagerAccounts($raw): string + { + if ($raw === null || $raw === '' || $raw === '-') { + return '-'; + } + $v = self::toFloat($raw); + if ($v === null || $v <= 0) { + return '-'; + } + + return self::formatGridNumericDisplay($v); + } + + /** + * Agent: only if original comp/tp/od is a positive number → partner_RR − value. + * Original null / empty / '-' / zero / negative / unparseable → '-'. + * Result ≤ 0 → '-'. + */ + public static function agentPayoutColumnFromPartnerRr($raw, float $partnerRr): string + { + if ($raw === null || $raw === '' || $raw === '-') { + return '-'; + } + $base = self::toFloat($raw); + if ($base === null || $base <= 0) { + return '-'; + } + + $result = $partnerRr - $base; + if ($result < 0 || abs($result) < 0.00001) { + return '-'; + } + + return self::formatGridNumericDisplay($result); + } + + /** + * @param array> $rows + * + * @return array> + */ + public static function applyManagerAccountsDisplayToRows(array $rows): array + { + foreach ($rows as &$row) { + if (array_key_exists('comp', $row)) { + $row['comp'] = self::gridPayoutDisplayManagerAccounts($row['comp'] ?? null); + } + if (array_key_exists('tp', $row)) { + $row['tp'] = self::gridPayoutDisplayManagerAccounts($row['tp'] ?? null); + } + if (array_key_exists('od', $row)) { + $row['od'] = self::gridPayoutDisplayManagerAccounts($row['od'] ?? null); + } + } + unset($row); + + return $rows; + } + + private static function formatGridNumericDisplay(float $v): string + { + if (abs($v - round($v)) < 0.00001) { + return (string) (int) round($v); + } + + return rtrim(rtrim(number_format($v, 2, '.', ''), '0'), '.'); + } + + /** + * @param array> $prrRows + * + * @return array normalized name => rate, and id:{vehicle_type_id} => rate + */ + private static function retentionMapFromPrrRows(array $prrRows): array + { + $map = []; + foreach ($prrRows as $r) { if (! array_key_exists('retention_rate', $r) || $r['retention_rate'] === null || $r['retention_rate'] === '') { continue; } @@ -56,6 +240,14 @@ class PartnerPayoutGridRetention return $map; } + /** + * @return array normalized name => rate, and id:{vehicle_type_id} => rate + */ + public static function buildRetentionMap(BaseConnection $db, int $agentId): array + { + return self::retentionMapFromPrrRows(self::fetchPrrPvtRows($db, $agentId)); + } + /** * @param array> $rows * @@ -67,46 +259,47 @@ class PartnerPayoutGridRetention return $rows; } - $map = self::buildRetentionMap($db, $agentId); - if ($map === []) { - return $rows; - } + $prrRows = self::fetchPrrPvtRows($db, $agentId); + $metaMap = self::partnerMetaMapFromPrrRows($prrRows); + $master = self::buildMasterNormLabelToVehicleTypeIdMap($db); 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]; + $oa = $row['comp'] ?? null; + $ob = $row['tp'] ?? null; + $oc = $row['od'] ?? null; + $row['oa'] = $oa; + $row['ob'] = $ob; + $row['oc'] = $oc; + + $pm = self::resolvePartnerMetaForGridRow($row, $metaMap, $master); + if ($pm === null) { + $row['partner_VT'] = '-'; + $row['partner_RR'] = '-'; + if (array_key_exists('comp', $row)) { + $row['comp'] = '-'; } - } - if ($rate === null) { + if (array_key_exists('tp', $row)) { + $row['tp'] = '-'; + } + if (array_key_exists('od', $row)) { + $row['od'] = '-'; + } + 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'), '.'); - } + $row['partner_VT'] = $pm['partner_VT']; + $row['partner_RR'] = $pm['partner_RR']; + $rr = (float) $pm['partner_RR']; + + if (array_key_exists('comp', $row)) { + $row['comp'] = self::agentPayoutColumnFromPartnerRr($oa, $rr); + } + if (array_key_exists('tp', $row)) { + $row['tp'] = self::agentPayoutColumnFromPartnerRr($ob, $rr); + } + if (array_key_exists('od', $row)) { + $row['od'] = self::agentPayoutColumnFromPartnerRr($oc, $rr); } } unset($row);