'Paid', 'doc_shortfall' => 'Denied due to document shortfall', 'policy_exclusion' => 'Denied due to Policy Exclusion', 'processed' => 'Processed', 'in_process' => 'In Process', ]; private const LANES = [ 'ip_cashless', 'ip_reimb', 'op_cashless', 'op_reimb', ]; private const RELATIONS = [ 'Self', 'Spouse', 'Child', 'Parent', 'Others', ]; private const AGE_BANDS = [ '0-5', '6-10', '11-15', '16-20', '21-25', '26-30', '31-35', '36-40', '41-45', '46-50', '51-55', '56-60', '61-65', '66-70', '71-more', 'Not classified', ]; private const UTIL_BUCKETS = [ '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'Above 10', ]; private const AMOUNT_BANDS = [ 'Upto 10000', '10001 - 25000', '25001 – 50000', '50001 – 75000', '75001 – 100000', '100001 – 200000', '200001 – 300000', '300001 – 400000', '400001 - 500000', '500001 - 750000', '750001 - 1000000', 'Above 1000000', ]; private ClaimReportDashboardModel $kpiModel; private ClaimDumpFileModel $dumpFileModel; public function __construct( ?ClaimReportDashboardModel $kpiModel = null, ?ClaimDumpFileModel $dumpFileModel = null ) { $this->kpiModel = $kpiModel ?? new ClaimReportDashboardModel(); $this->dumpFileModel = $dumpFileModel ?? new ClaimDumpFileModel(); } /** * @return array{status:bool,message?:string,data?:array} */ public function build(int $policyId): array { if ($policyId <= 0) { return ['status' => false, 'message' => 'client_policy_id is required.']; } $db = db_connect(); if (!$db->tableExists('claim_report') || !$db->tableExists(self::DUMP_TABLE)) { return [ 'status' => false, 'message' => 'Required tables claim_report or claims_dump_medi_assist do not exist.', ]; } $dumpRows = $this->getLinkedDumpRows($policyId); if ($dumpRows === []) { return [ 'status' => false, 'message' => 'No Medi Assist dump rows linked via claim_report. Run sync: /util/claims-collection-report/sync', ]; } $policy = $db->table('client_policy cp') ->select('cp.*, c.client_name, i.name AS insurer_name, t.name AS tpa_name') ->join('clients c', 'c.id = cp.client_id', 'left') ->join('insurers i', 'i.id = cp.insurer_id', 'left') ->join('tpa t', 't.id = cp.tpa_id', 'left') ->where('cp.id', $policyId) ->get() ->getRowArray(); if (!$policy) { return ['status' => false, 'message' => 'Policy not found.']; } $exposure = $this->firstRow($this->kpiModel->policy_exposure_summary($policyId)); $premiumRow = $this->firstRow($this->kpiModel->premium_as_on_date($policyId)); $livesRow = $this->firstRow($this->kpiModel->current_emp_lives($policyId)); $experience = $this->firstRow($this->kpiModel->claims_experience_summary($policyId)); $premium = $this->parseFormattedNumber($premiumRow['premium_as_on_date'] ?? null); $earned = $this->parseFormattedNumber($experience['earned_premium'] ?? null); $lives = (int) ($livesRow['current_lives'] ?? 0); $policyStartRaw = $policy['policy_start_date'] ?? $exposure['policy_start_date'] ?? null; $policyEndRaw = $policy['policy_end_date'] ?? $exposure['policy_end_date'] ?? null; $startDate = $this->formatDateLong($policyStartRaw); $endDate = $this->formatDateLong($policyEndRaw); $dumpTs = $this->dumpFileModel->getLatestClaimDumpTimestamp($policyId) ?: time(); $reportAsOn = date('d M Y', $dumpTs); $uploadAt = date('d/m/Y g:i:s A', $dumpTs); $runDays = $this->policyRunDays($policyStartRaw, $policyEndRaw); $statusAgg = $this->aggregateStatus($dumpRows); $claimTypeAgg = $this->aggregateClaimTypes($dumpRows); $pendingAgg = $this->aggregatePendingWith($dumpRows); $ipFrequency = $this->buildIpClaimFrequency($dumpRows, $lives); $errorsCount = $this->countClaimsInError($dumpRows); $pendingSummary = $this->buildPendingSummary($dumpRows); $savings = $this->buildSavings($dumpRows); $topProviders = $this->buildTopProviders($dumpRows, 10); $topAilments = $this->buildTopAilments($dumpRows, 10); $beneficiary = $this->buildBeneficiary($dumpRows); $ageBands = $this->buildAgeBands($dumpRows); $utilEmp = $this->buildUtilization($dumpRows, true); $utilDep = $this->buildUtilization($dumpRows, false); $amountCashless = $this->buildAmountBands($dumpRows, 'ip_cashless'); $amountReimb = $this->buildAmountBands($dumpRows, 'ip_reimb'); $pendingDetail = $this->buildPendingDetail($dumpRows); $totalClaims = count($dumpRows); $totalIncurred = $statusAgg['total']['incurred']; $icrPremium = $premium > 0 ? round(($totalIncurred / $premium) * 100, 2) : 0.0; $icrEarned = $earned > 0 ? round(($totalIncurred / $earned) * 100, 2) : 0.0; $ipClaimCount = ($claimTypeAgg['ip']['total']['count'] ?? 0); $frequency = $lives > 0 ? round(($ipClaimCount / $lives) * 100, 2) : 0.0; $data = [ 'meta' => [ 'policy_id' => $policyId, 'report_as_on' => $reportAsOn, 'units' => 'Actuals', 'version' => self::VERSION, 'generated_at' => date('d-M-Y', $dumpTs), 'generated_by' => '', 'upload_at' => $uploadAt, ], 'header' => [ 'insurer_name' => trim((string) ($policy['insurer_name'] ?? $exposure['insurer_name'] ?? '')), 'corporate_name' => trim((string) ($policy['client_name'] ?? '')), 'policy_number' => trim((string) ($policy['policy_no'] ?? '')), 'policy_holder' => trim((string) ($dumpRows[0]['policy_holder_name'] ?? $policy['client_name'] ?? '')), 'policy_start' => $startDate, 'policy_end' => $endDate, 'policy_period' => trim($startDate . ' To ' . $endDate), 'lives' => $lives, 'total_premium' => $premium, 'earned_premium' => $earned, 'policy_run_days' => $runDays, ], 'index_policies' => [[ 'policy_number' => trim((string) ($policy['policy_no'] ?? '')), 'policy_holder' => trim((string) ($dumpRows[0]['policy_holder_name'] ?? $policy['client_name'] ?? '')), 'policy_start' => $startDate, 'policy_end' => $endDate, 'lives' => $lives, 'total_premium' => $premium, 'earned_premium' => $earned, 'policy_run_days' => $runDays, ]], 'portfolio_summary' => [ 'lives' => $lives, 'premium' => $premium, 'premium_lakh' => $premium / 100000, 'claims_count' => $totalClaims, 'incurred' => $totalIncurred, 'incurred_lakh' => $totalIncurred / 100000, 'icr_premium' => $icrPremium, 'icr_earned' => $icrEarned, 'frequency' => $frequency, 'report_date' => $reportAsOn, ], 'policy_lives' => [ 'inception' => max(0, $lives), 'addition' => 0, 'deletion' => 0, 'current' => $lives, ], 'policy_premium' => [ 'first_time' => $premium, 'addition' => 0.0, 'deletion' => 0.0, 'total' => $premium, 'earned' => $earned, ], 'claim_status_rows' => $statusAgg['rows'], 'claim_type_sections' => $claimTypeAgg, 'ip_claim_frequency' => $ipFrequency, 'claims_in_error' => ['count' => $errorsCount], 'pending_intro' => $pendingSummary['intro'], 'claims_pending_with' => $pendingAgg['rows'], 'savings' => $savings, 'top_providers' => $topProviders, 'top_ailments' => $topAilments, 'beneficiary' => $beneficiary, 'age_bands' => $ageBands, 'utilization_employees' => $utilEmp, 'utilization_dependents' => $utilDep, 'amount_bands_cashless' => $amountCashless, 'amount_bands_reimbursement' => $amountReimb, 'pending_detail' => $pendingDetail, 'lives_movement_summary' => $this->buildLivesMovementSummary($lives), 'premium_movement_summary' => $this->buildPremiumMovementSummary($premium), 'lives_movement_monthly' => $this->buildEmptyMonthlyLives(), 'premium_movement_monthly' => $this->buildEmptyMonthlyPremium($premium), 'glossary' => $this->buildGlossary(), 'dump_row_count' => count($dumpRows), 'fmtNum' => static fn ($n, int $d = 0): string => self::fmtNum($n, $d), 'fmtRs' => static fn ($n): string => self::fmtRs($n), 'fmtPct' => static fn ($n, int $d = 2): string => self::fmtPct($n, $d), ]; $data['view_model'] = $data; return ['status' => true, 'data' => $data]; } /** * @return list> */ public function getLinkedDumpRows(int $policyId): array { $db = db_connect(); $reportRows = $db->table('claim_report') ->select('source_row_id') ->where('client_policy_id', $policyId) ->where('is_active', 1) ->where('source_table', self::DUMP_TABLE) ->where('source_row_id IS NOT NULL', null, false) ->get() ->getResultArray(); if ($reportRows === []) { return []; } $dumpIds = array_values(array_unique(array_filter(array_map( static fn ($r) => (int) ($r['source_row_id'] ?? 0), $reportRows )))); if ($dumpIds === []) { return []; } return $db->table(self::DUMP_TABLE) ->whereIn('id', $dumpIds) ->where('is_active', 1) ->orderBy('id', 'ASC') ->get() ->getResultArray(); } public static function fmtNum(mixed $value, int $decimals = 0): string { $n = is_numeric($value) ? (float) $value : 0.0; if (class_exists(\NumberFormatter::class)) { $fmt = new \NumberFormatter('en_IN', \NumberFormatter::DECIMAL); $fmt->setAttribute(\NumberFormatter::MIN_FRACTION_DIGITS, $decimals); $fmt->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, $decimals); return $fmt->format($n) ?: number_format($n, $decimals); } return number_format($n, $decimals); } public static function fmtRs(mixed $value): string { return '₹' . self::fmtNum($value, 0); } public static function fmtPct(mixed $value, int $decimals = 2): string { $n = is_numeric($value) ? (float) $value : 0.0; return number_format($n, $decimals) . '%'; } // ----------------- aggregations ----------------- /** * @param list> $rows * @return array{rows:array>,total:array} */ private function aggregateStatus(array $rows): array { $buckets = []; foreach (self::STATUS_KEYS as $key) { $buckets[$key] = $this->emptyMetric(); } foreach ($rows as $row) { $key = $this->statusBucket((string) ($row['claim_status'] ?? ''), $row); if (!isset($buckets[$key])) { $key = 'in_process'; } $buckets[$key]['count']++; $buckets[$key]['claim_amount'] += $this->claimAmount($row); $buckets[$key]['incurred'] += $this->incurredAmount($row, $key); } $total = $this->emptyMetric(); foreach ($buckets as $b) { $total['count'] += $b['count']; $total['claim_amount'] += $b['claim_amount']; $total['incurred'] += $b['incurred']; } $out = []; foreach (self::STATUS_KEYS as $key) { $b = $buckets[$key]; $out[$key] = $this->finalizeMetric($key, $b, $total, self::STATUS_LABELS[$key] ?? $key); } $out['total'] = $this->finalizeMetric('total', $total, $total, 'Total'); return ['rows' => $out, 'total' => $out['total']]; } /** * @param list> $rows * @return array */ private function aggregateClaimTypes(array $rows): array { $grid = []; foreach (['ip', 'op'] as $ipOp) { $grid[$ipOp] = [ 'cashless' => ['rows' => [], 'subtotal' => $this->emptyMetric()], 'reimb' => ['rows' => [], 'subtotal' => $this->emptyMetric()], 'total' => $this->emptyMetric(), ]; foreach (['cashless', 'reimb'] as $mode) { foreach (self::STATUS_KEYS as $status) { $grid[$ipOp][$mode]['rows'][$status] = $this->emptyMetric(); } } } $grand = $this->emptyMetric(); foreach ($rows as $row) { $lane = $this->claimLane($row); $ipOp = str_starts_with($lane, 'op_') ? 'op' : 'ip'; $mode = str_contains($lane, 'cashless') ? 'cashless' : 'reimb'; $status = $this->statusBucket((string) ($row['claim_status'] ?? ''), $row); if (!in_array($status, self::STATUS_KEYS, true)) { $status = 'in_process'; } $claimAmt = $this->claimAmount($row); $incurred = $this->incurredAmount($row, $status); $grid[$ipOp][$mode]['rows'][$status]['count']++; $grid[$ipOp][$mode]['rows'][$status]['claim_amount'] += $claimAmt; $grid[$ipOp][$mode]['rows'][$status]['incurred'] += $incurred; $grid[$ipOp][$mode]['subtotal']['count']++; $grid[$ipOp][$mode]['subtotal']['claim_amount'] += $claimAmt; $grid[$ipOp][$mode]['subtotal']['incurred'] += $incurred; $grid[$ipOp]['total']['count']++; $grid[$ipOp]['total']['claim_amount'] += $claimAmt; $grid[$ipOp]['total']['incurred'] += $incurred; $grand['count']++; $grand['claim_amount'] += $claimAmt; $grand['incurred'] += $incurred; } $result = ['ip' => [], 'op' => [], 'grand_total' => $this->finalizeMetric('grand_total', $grand, $grand, 'Grand Total')]; foreach (['ip', 'op'] as $ipOp) { foreach (['cashless', 'reimb'] as $mode) { $sub = $grid[$ipOp][$mode]['subtotal']; $statusRows = []; foreach (self::STATUS_KEYS as $status) { $statusRows[$status] = $this->finalizeMetric( $status, $grid[$ipOp][$mode]['rows'][$status], $sub, self::STATUS_LABELS[$status] ?? $status ); } $result[$ipOp][$mode] = [ 'status_rows' => $statusRows, 'subtotal' => $this->finalizeMetric('subtotal', $sub, $sub, 'Subtotal'), ]; } $result[$ipOp]['total'] = $this->finalizeMetric( 'total', $grid[$ipOp]['total'], $grid[$ipOp]['total'], 'Total (' . strtoupper($ipOp) . ')' ); } return $result; } /** * @param list> $rows * @return array{rows:list>} */ private function aggregatePendingWith(array $rows): array { $keys = [ '9.1 IP' => ['lanes' => ['ip_cashless', 'ip_reimb']], '9.1.1 Cashless' => ['lanes' => ['ip_cashless']], '9.1.2 Reimbursement' => ['lanes' => ['ip_reimb']], '9.2 OP' => ['lanes' => ['op_cashless', 'op_reimb']], '9.2.1 Cashless' => ['lanes' => ['op_cashless']], '9.2.2 Reimbursement' => ['lanes' => ['op_reimb']], ]; $emptyParty = static fn () => ['count' => 0, 'amount' => 0.0]; $grid = []; foreach ($keys as $label => $_) { $grid[$label] = [ 'label' => $label, 'medi_assist' => $emptyParty(), 'insurer' => $emptyParty(), 'member' => $emptyParty(), 'provider' => $emptyParty(), 'total' => $emptyParty(), ]; } foreach ($rows as $row) { $status = $this->statusBucket((string) ($row['claim_status'] ?? ''), $row); if ($status !== 'in_process') { continue; } $lane = $this->claimLane($row); $amount = $this->incurredAmount($row, $status); $matched = []; foreach ($keys as $label => $cfg) { if (in_array($lane, $cfg['lanes'], true)) { $matched[] = $label; } } foreach ($matched as $label) { $grid[$label]['medi_assist']['count']++; $grid[$label]['medi_assist']['amount'] += $amount; $grid[$label]['total']['count']++; $grid[$label]['total']['amount'] += $amount; } } $out = array_values($grid); $out[] = $this->sumPendingRows('Total', $out); return ['rows' => $out]; } /** * @param list> $rows * @return array */ private function buildIpClaimFrequency(array $rows, int $lives): array { $cashless = 0; $reimb = 0; foreach ($rows as $row) { $lane = $this->claimLane($row); if (!str_starts_with($lane, 'ip_')) { continue; } if ($lane === 'ip_cashless') { $cashless++; } else { $reimb++; } } $total = $cashless + $reimb; $rate = static fn (int $c): float => $lives > 0 ? round(($c / $lives) * 100, 2) : 0.0; return [ 'cashless_count' => $cashless, 'reimb_count' => $reimb, 'total' => $total, 'cashless_rate' => $rate($cashless), 'reimb_rate' => $rate($reimb), 'total_rate' => $rate($total), ]; } /** * @param list> $rows */ private function countClaimsInError(array $rows): int { $count = 0; foreach ($rows as $row) { if (trim((string) ($row['error_group'] ?? '')) !== '') { $count++; } } return $count; } /** * @param list> $rows * @return array{intro:string,pending_count:int,pending_amount:float,error_count:int} */ private function buildPendingSummary(array $rows): array { $pendingCount = 0; $pendingAmount = 0.0; $errorCount = 0; foreach ($rows as $row) { $status = $this->statusBucket((string) ($row['claim_status'] ?? ''), $row); if ($status === 'in_process') { $pendingCount++; $pendingAmount += $this->incurredAmount($row, $status); } if (trim((string) ($row['error_group'] ?? '')) !== '') { $errorCount++; } } $intro = sprintf( 'As of %s, %d Claims with an Incurred amount of %s are pending. Out of them %d claims are under error log category', date('d M Y'), $pendingCount, self::fmtRs($pendingAmount), $errorCount ); return [ 'intro' => $intro, 'pending_count' => $pendingCount, 'pending_amount' => $pendingAmount, 'error_count' => $errorCount, ]; } /** * @param list> $rows * @return list> */ private function buildSavings(array $rows): array { $map = [ ['group' => 'Policy Driven', 'label' => 'Proportionate Deduction', 'field' => 'deduction_amount_prorata'], ['group' => 'Policy Driven', 'label' => 'Defined Benefit', 'field' => 'deduction_amount_excess_ailment'], ['group' => 'Policy Driven', 'label' => 'Copay', 'field' => 'deduction_amount_copay'], ['group' => 'Others', 'label' => 'Hospital Discount', 'field' => 'deduction_amount_hospital_discount'], ['group' => 'Others', 'label' => 'Case Management', 'field' => null], ['group' => 'Others', 'label' => '*Fraud Waste and Abuse', 'field' => 'deduction_amount_intimation_penalty'], ]; $out = []; foreach ($map as $item) { $count = 0; $amount = 0.0; if ($item['field'] !== null) { foreach ($rows as $row) { $val = $this->toFloat($row[$item['field']] ?? 0); if ($val > 0) { $count++; $amount += $val; } } } $out[] = [ 'group' => $item['group'], 'label' => $item['label'], 'count' => $count, 'amount' => $amount, ]; } return $out; } /** * @param list> $rows * @return array{rows:list>,total:array} */ private function buildTopProviders(array $rows, int $limit): array { $map = []; foreach ($rows as $row) { if (!$this->isIpAnalyticsRow($row)) { continue; } $name = trim((string) ($row['hospital_name'] ?? '')); if ($name === '') { $name = 'Unknown'; } $key = strtoupper($name); if (!isset($map[$key])) { $map[$key] = ['name' => $name, 'count' => 0, 'amount' => 0.0]; } $map[$key]['count']++; $map[$key]['amount'] += $this->approvedAmount($row); } uasort($map, static fn ($a, $b) => $b['amount'] <=> $a['amount']); $top = array_slice(array_values($map), 0, $limit); $totalCount = array_sum(array_column($top, 'count')); $totalAmt = array_sum(array_column($top, 'amount')); $out = []; foreach ($top as $item) { $out[] = [ 'name' => $item['name'], 'count' => $item['count'], 'count_pct' => $totalCount > 0 ? round(($item['count'] / $totalCount) * 100, 2) : 0.0, 'approved_amount' => $item['amount'], 'amount_pct' => $totalAmt > 0 ? round(($item['amount'] / $totalAmt) * 100, 2) : 0.0, ]; } return [ 'rows' => $out, 'total' => [ 'count' => $totalCount, 'count_pct' => 100.0, 'approved_amount' => $totalAmt, 'amount_pct' => 100.0, ], ]; } /** * @param list> $rows * @return array{rows:list>,total:array} */ private function buildTopAilments(array $rows, int $limit): array { $map = []; foreach ($rows as $row) { if (!$this->isIpAnalyticsRow($row)) { continue; } $name = trim((string) ($row['primary_icd_group'] ?? '')); if ($name === '') { $name = trim((string) ($row['primary_ailment_name'] ?? 'Others')); } if ($name === '') { $name = 'Others'; } $key = strtoupper($name); if (!isset($map[$key])) { $map[$key] = ['name' => $name, 'count' => 0, 'amount' => 0.0]; } $map[$key]['count']++; $map[$key]['amount'] += $this->approvedAmount($row); } uasort($map, static fn ($a, $b) => $b['amount'] <=> $a['amount']); $top = array_slice(array_values($map), 0, $limit); $totalCount = array_sum(array_column($top, 'count')); $totalAmt = array_sum(array_column($top, 'amount')); $out = []; foreach ($top as $item) { $out[] = [ 'name' => $item['name'], 'count' => $item['count'], 'count_pct' => $totalCount > 0 ? round(($item['count'] / $totalCount) * 100, 2) : 0.0, 'approved_amount' => $item['amount'], 'amount_pct' => $totalAmt > 0 ? round(($item['amount'] / $totalAmt) * 100, 2) : 0.0, ]; } return [ 'rows' => $out, 'total' => [ 'count' => $totalCount, 'count_pct' => 100.0, 'approved_amount' => $totalAmt, 'amount_pct' => 100.0, ], ]; } /** * @param list> $rows * @return array{rows:list>,total:array} */ private function buildBeneficiary(array $rows): array { $buckets = array_fill_keys(self::RELATIONS, ['count' => 0, 'amount' => 0.0]); foreach ($rows as $row) { if (!$this->isIpAnalyticsRow($row)) { continue; } $rel = $this->relationLabel($row); $buckets[$rel]['count']++; $buckets[$rel]['amount'] += $this->approvedAmount($row); } $totalCount = array_sum(array_column($buckets, 'count')); $totalAmt = array_sum(array_column($buckets, 'amount')); $out = []; foreach (self::RELATIONS as $rel) { $b = $buckets[$rel]; $out[] = [ 'relation' => $rel, 'count' => $b['count'], 'count_pct' => $totalCount > 0 ? round(($b['count'] / $totalCount) * 100, 2) : 0.0, 'approved_amount' => $b['amount'], 'amount_pct' => $totalAmt > 0 ? round(($b['amount'] / $totalAmt) * 100, 2) : 0.0, ]; } return [ 'rows' => $out, 'total' => [ 'count' => $totalCount, 'count_pct' => 100.0, 'approved_amount' => $totalAmt, 'amount_pct' => 100.0, ], ]; } /** * @param list> $rows * @return array{rows:list>,total:array} */ private function buildAgeBands(array $rows): array { $buckets = []; foreach (self::AGE_BANDS as $band) { $buckets[$band] = ['lives' => 0, 'count' => 0, 'amount' => 0.0]; } $seenLives = []; foreach ($rows as $row) { if (!$this->isIpAnalyticsRow($row)) { continue; } $band = $this->ageBand($row); if (!isset($buckets[$band])) { $band = 'Not classified'; } $buckets[$band]['count']++; $buckets[$band]['amount'] += $this->approvedAmount($row); $memberKey = trim((string) ($row['benef_maid'] ?? '')); if ($memberKey === '') { $memberKey = trim((string) ($row['benef_insurer_id'] ?? '')) . '|' . trim((string) ($row['benef_name'] ?? '')); } if ($memberKey !== '' && !isset($seenLives[$band][$memberKey])) { $seenLives[$band][$memberKey] = true; $buckets[$band]['lives']++; } } $totalCount = 0; $totalLives = 0; $totalAmt = 0.0; $out = []; foreach (self::AGE_BANDS as $band) { $b = $buckets[$band]; $totalCount += $b['count']; $totalLives += $b['lives']; $totalAmt += $b['amount']; $out[] = [ 'band' => $band, 'lives' => $b['lives'], 'count' => $b['count'], 'count_pct' => 0.0, 'approved_amount' => $b['amount'], 'amount_pct' => 0.0, ]; } foreach ($out as &$row) { $row['count_pct'] = $totalCount > 0 ? round(($row['count'] / $totalCount) * 100, 2) : 0.0; $row['amount_pct'] = $totalAmt > 0 ? round(($row['approved_amount'] / $totalAmt) * 100, 2) : 0.0; } unset($row); return [ 'rows' => $out, 'total' => [ 'lives' => $totalLives, 'count' => $totalCount, 'count_pct' => 100.0, 'approved_amount' => $totalAmt, 'amount_pct' => 100.0, ], ]; } /** * @param list> $rows * @return array{rows:list>,total:array} */ private function buildUtilization(array $rows, bool $employees): array { $byMember = []; foreach ($rows as $row) { if (!$this->isIpAnalyticsRow($row)) { continue; } $isSelf = $this->relationLabel($row) === 'Self'; if ($employees && !$isSelf) { continue; } if (!$employees && $isSelf) { continue; } $key = trim((string) ($row['pribenef_employee_code'] ?? '')); if ($key === '') { $key = trim((string) ($row['benef_maid'] ?? '')); } if ($key === '') { $key = 'row:' . ($row['id'] ?? uniqid('', true)); } if (!isset($byMember[$key])) { $byMember[$key] = ['count' => 0, 'amount' => 0.0]; } $byMember[$key]['count']++; $byMember[$key]['amount'] += $this->approvedAmount($row); } $bands = []; foreach (self::UTIL_BUCKETS as $label) { $bands[$label] = ['count' => 0, 'amount' => 0.0]; } foreach ($byMember as $member) { $c = $member['count']; $label = $c >= 11 ? 'Above 10' : (string) $c; if (!isset($bands[$label])) { $label = 'Above 10'; } $bands[$label]['count']++; $bands[$label]['amount'] += $member['amount']; } $totalMembers = array_sum(array_column($bands, 'count')); $totalAmt = array_sum(array_column($bands, 'amount')); $out = []; foreach (self::UTIL_BUCKETS as $label) { $b = $bands[$label]; $out[] = [ 'label' => $label, 'member_count' => $b['count'], 'count_pct' => $totalMembers > 0 ? round(($b['count'] / $totalMembers) * 100, 2) : 0.0, 'approved_amount' => $b['amount'], 'amount_pct' => $totalAmt > 0 ? round(($b['amount'] / $totalAmt) * 100, 2) : 0.0, ]; } return [ 'rows' => $out, 'total' => [ 'member_count' => $totalMembers, 'count_pct' => 100.0, 'approved_amount' => $totalAmt, 'amount_pct' => 100.0, ], ]; } /** * @param list> $rows * @return array{rows:list>,total:array} */ private function buildAmountBands(array $rows, string $lane): array { $rels = self::RELATIONS; $grid = []; foreach (self::AMOUNT_BANDS as $band) { $grid[$band] = [ 'relations' => array_fill_keys($rels, ['count' => 0, 'amount' => 0.0]), 'total' => ['count' => 0, 'amount' => 0.0], ]; } foreach ($rows as $row) { if ($this->claimLane($row) !== $lane) { continue; } if (!$this->isIpAnalyticsRow($row)) { continue; } $amt = $this->approvedAmount($row); $band = $this->amountBand($amt); $rel = $this->relationLabel($row); if (!isset($grid[$band])) { continue; } $grid[$band]['relations'][$rel]['count']++; $grid[$band]['relations'][$rel]['amount'] += $amt; $grid[$band]['total']['count']++; $grid[$band]['total']['amount'] += $amt; } $grandCount = 0; $grandAmt = 0.0; $relTotals = array_fill_keys($rels, ['count' => 0, 'amount' => 0.0]); $out = []; foreach (self::AMOUNT_BANDS as $band) { $g = $grid[$band]; $grandCount += $g['total']['count']; $grandAmt += $g['total']['amount']; foreach ($rels as $rel) { $relTotals[$rel]['count'] += $g['relations'][$rel]['count']; $relTotals[$rel]['amount'] += $g['relations'][$rel]['amount']; } $out[] = [ 'band' => $band, 'relations' => $g['relations'], 'total' => $g['total'] + ['pct' => 0.0], ]; } foreach ($out as &$row) { $row['total']['pct'] = $grandCount > 0 ? round(($row['total']['count'] / $grandCount) * 100, 2) : 0.0; } unset($row); return [ 'rows' => $out, 'total' => [ 'relations' => $relTotals, 'count' => $grandCount, 'amount' => $grandAmt, 'pct' => 100.0, ], ]; } /** * @param list> $rows * @return array{rows:list>,totals:array} */ private function buildPendingDetail(array $rows): array { $labels = [ 'IP' => [ 'Cashless' => [ 'A.1.1 Documents awaited — Intimated to Insurer', 'A.1.2 In process - Decision pending — Intimated to Insurer', 'A.1.3 In process - Denial pending', 'A.1.4 In process - Payment pending — Intimated to Insurer', 'A.1.4 In process - Payment pending — Member Addition', ], 'Reimbursement' => [ 'A.1.1 Documents awaited — Intimated to Insurer', 'A.1.2 In process - Decision pending — Intimated to Insurer', 'A.1.3 In process - Denial pending — Intimated to Insurer', 'A.1.4 In process - Payment pending — Intimated to Insurer', ], ], ]; $emptyCounts = static fn (): array => [ 'medi_assist' => 0, 'insurer' => 0, 'member' => 0, 'provider' => 0, 'total' => 0, ]; $rowsOut = []; $grand = $emptyCounts(); foreach ($labels['IP'] as $mode => $items) { $rowsOut[] = ['label' => $mode, 'is_group' => true, 'counts' => $emptyCounts()]; $subtotal = $emptyCounts(); foreach ($items as $itemLabel) { $counts = $emptyCounts(); $rowsOut[] = ['label' => $itemLabel, 'is_group' => false, 'counts' => $counts]; } $rowsOut[] = ['label' => 'Subtotal', 'is_group' => true, 'counts' => $subtotal]; } $pendingCount = 0; foreach ($rows as $row) { if ($this->statusBucket((string) ($row['claim_status'] ?? ''), $row) === 'in_process') { $pendingCount++; } } if ($pendingCount > 0 && isset($rowsOut[1])) { $rowsOut[1]['counts']['medi_assist'] = $pendingCount; $rowsOut[1]['counts']['total'] = $pendingCount; $grand['medi_assist'] = $pendingCount; $grand['total'] = $pendingCount; } return ['rows' => $rowsOut, 'totals' => $grand]; } /** * @return array */ private function buildLivesMovementSummary(int $lives): array { return [ 'rows' => [ ['particular' => 'Inception Lives', 'inception_addition' => 0, 'deletion' => null, 'current' => 0], ['particular' => 'Addition - Inception Lives', 'inception_addition' => $lives, 'deletion' => null, 'current' => $lives], ['particular' => 'Addition - New', 'inception_addition' => 0, 'deletion' => null, 'current' => 0], ['particular' => 'Deletion', 'inception_addition' => null, 'deletion' => 0, 'current' => 0], ], 'total' => [ 'inception_addition' => $lives, 'deletion' => 0, 'current' => $lives, ], ]; } /** * @return array */ private function buildPremiumMovementSummary(float $premium): array { return [ 'rows' => [ ['particular' => 'Opening Premium - Inception', 'premium' => $premium, 'refund' => null, 'total' => $premium], ['particular' => 'Addition', 'premium' => 0.0, 'refund' => null, 'total' => 0.0], ['particular' => 'Deletion', 'premium' => null, 'refund' => 0.0, 'total' => 0.0], ], 'total' => [ 'premium' => $premium, 'refund' => 0.0, 'total' => $premium, ], ]; } /** * @return array */ private function buildEmptyMonthlyLives(): array { $particulars = [ 'Inception Lives', 'Addition - Inception Lives', 'Addition - New', 'Deletion', ]; $relations = [ 'employee', 'spouse', 'children', 'father', 'mother', 'siblings', 'others', ]; $rows = []; for ($m = 1; $m <= 12; $m++) { foreach ($particulars as $particular) { $row = ['month' => 'M' . $m, 'particular' => $particular]; foreach ($relations as $rel) { $row[$rel . '_add'] = 0; $row[$rel . '_del'] = $particular === 'Deletion' ? 0 : null; } $rows[] = $row; } } $total = ['month' => '', 'particular' => 'Total']; foreach ($relations as $rel) { $total[$rel . '_add'] = 0; $total[$rel . '_del'] = 0; } return ['rows' => $rows, 'total' => $total]; } /** * @return array */ private function buildEmptyMonthlyPremium(float $closingPremium): array { $rows = []; for ($m = 1; $m <= 12; $m++) { if ($m === 1) { $rows[] = ['month' => 'Month-1', 'particular' => 'Opening Premium - Inception', 'premium' => $closingPremium, 'refund' => null, 'total' => $closingPremium]; } $rows[] = ['month' => 'Month-' . $m, 'particular' => 'Addition', 'premium' => 0.0, 'refund' => null, 'total' => 0.0]; $rows[] = ['month' => 'Month-' . $m, 'particular' => 'Deletion', 'premium' => null, 'refund' => 0.0, 'total' => 0.0]; } return [ 'rows' => $rows, 'total' => [ 'month' => '', 'particular' => 'Closing Premium', 'premium' => $closingPremium, 'refund' => 0.0, 'total' => $closingPremium, ], ]; } /** * @return list */ private function buildGlossary(): array { return [ ['label' => '1.0 Policy Lives', 'description' => 'Details of insured members.'], ['label' => '1.1 At Inception & Addition', 'description' => 'Total lives at start and added during policy.'], ['label' => '1.1.1 At Inception', 'description' => 'Lives covered from policy inception date.'], ['label' => '1.1.2 Addition', 'description' => 'Lives added via endorsement.'], ['label' => '1.2 Deletion', 'description' => 'Lives removed via endorsement.'], ['label' => '1.3 Current Lives', 'description' => 'At inception + additions - deletions.'], ['label' => '2.0 Policy Premium', 'description' => 'Breakdown of premium values.'], ['label' => '2.1 First Time', 'description' => 'Initial premium.'], ['label' => '2.2 Addition', 'description' => 'Premium added via endorsement.'], ['label' => '2.3 Deletion', 'description' => 'Premium reduced via endorsement.'], ['label' => '2.4 Total Premium', 'description' => 'First + Additions - Deletions.'], ['label' => '2.5 Earned Premium', 'description' => 'Premium applicable to elapsed policy period.'], ['label' => '3.0 Claim Status', 'description' => 'Indicates processing stage of claims.'], ['label' => '4 IPD Cashless', 'description' => 'In-patient claims where treatment cost is directly settled with the provider as per policy terms.'], ['label' => '4.1 Paid', 'description' => 'Payment completed and details available.'], ['label' => '4.2 Denied – Document Shortfall', 'description' => 'Denied due to missing documents.'], ['label' => '4.3 Denied – Inadmissibility', 'description' => 'Denied as per policy exclusions or inadmissible conditions.'], ['label' => '4.4 Processed', 'description' => 'Claims where processing is complete and ready for payment upload.'], ['label' => '4.5 In Process', 'description' => 'Under review, investigation, or awaiting inputs.'], ['label' => '5 IPD Reimbursement', 'description' => 'In-patient claims where the insured pays first and is reimbursed later as per policy terms.'], ['label' => '6 OPD Cashless', 'description' => 'Out-patient claims where the provider is paid directly.'], ['label' => '7 OPD Reimbursement', 'description' => 'Out-patient claims where the insured pays and is reimbursed.'], ['label' => '9 Pending With', 'description' => 'Claims pending with Medi Assist, Insurer, Member, or Provider.'], ['label' => '8.1 IPD Claim Count', 'description' => 'Total number of IPD claims.'], ['label' => '8.2 Claims per 100 Lives (%)', 'description' => 'Claim frequency per 100 insured lives.'], ['label' => '10 Claims that are in Error', 'description' => 'Claims that are in Error'], ]; } // ----------------- helpers ----------------- /** @return array{count:int,claim_amount:float,incurred:float} */ private function emptyMetric(): array { return ['count' => 0, 'claim_amount' => 0.0, 'incurred' => 0.0]; } /** * @param array{count:int,claim_amount:float,incurred:float} $metric * @param array{count:int,claim_amount:float,incurred:float} $total * @return array */ private function finalizeMetric(string $key, array $metric, array $total, string $label): array { return [ 'key' => $key, 'label' => $label, 'count' => $metric['count'], 'count_pct' => $total['count'] > 0 ? round(($metric['count'] / $total['count']) * 100, 2) : 0.0, 'claim_amount' => $metric['claim_amount'], 'claim_amount_pct' => $total['claim_amount'] > 0 ? round(($metric['claim_amount'] / $total['claim_amount']) * 100, 2) : 0.0, 'incurred' => $metric['incurred'], 'incurred_pct' => $total['incurred'] > 0 ? round(($metric['incurred'] / $total['incurred']) * 100, 2) : 0.0, ]; } /** * @param list> $rows * @return array */ private function sumPendingRows(string $label, array $rows): array { $emptyParty = static fn () => ['count' => 0, 'amount' => 0.0]; $sum = [ 'label' => $label, 'medi_assist' => $emptyParty(), 'insurer' => $emptyParty(), 'member' => $emptyParty(), 'provider' => $emptyParty(), 'total' => $emptyParty(), ]; foreach ($rows as $row) { if (($row['label'] ?? '') === $label) { continue; } foreach (['medi_assist', 'insurer', 'member', 'provider', 'total'] as $party) { $sum[$party]['count'] += (int) ($row[$party]['count'] ?? 0); $sum[$party]['amount'] += (float) ($row[$party]['amount'] ?? 0); } } return $sum; } /** @param array $row */ private function statusBucket(string $status, array $row): string { $s = strtoupper(trim(str_replace(['_', '-'], ' ', $status))); $s = preg_replace('/\s+/', ' ', $s) ?? $s; $denial = strtoupper(trim( (string) ($row['denial_short_description'] ?? '') . ' ' . (string) ($row['denial_description'] ?? '') )); if ( str_contains($s, 'DENIED') || str_contains($s, 'REJECT') || str_contains($s, 'DENIAL') || str_contains($s, 'CANCEL') ) { if ( str_contains($denial, 'DOCUMENT') || str_contains($denial, 'SHORTFALL') || str_contains($s, 'DOCUMENT') || str_contains($s, 'SHORTFALL') || str_contains($s, 'INFORMATION AWAITED') || str_contains($s, 'DEFICIEN') ) { return 'doc_shortfall'; } return 'policy_exclusion'; } if (str_contains($s, 'PAID') || str_contains($s, 'SETTLED') || $s === 'CLOSED') { return 'paid'; } if (str_contains($s, 'PROCESSED') || str_contains($s, 'DEBIT NOTE')) { return 'processed'; } return 'in_process'; } /** @param array $row */ private function claimLane(array $row): string { $type = strtolower(trim((string) ($row['claim_type'] ?? ''))); $sub = strtolower(trim((string) ($row['claim_sub_type'] ?? ''))); $src = trim($type . ' ' . $sub); if ($src === '' && trim((string) ($row['claim_mode_of_rcpt'] ?? '')) !== '') { $src = strtolower((string) $row['claim_mode_of_rcpt']); } $isOp = (bool) preg_match('/\b(opd|op)\b/i', $src); $isCashless = str_contains($src, 'cashless') || str_contains($src, 'cash') || strtoupper(trim((string) ($row['is_cashlessanywhere'] ?? ''))) === 'Y'; if ($isOp) { return $isCashless ? 'op_cashless' : 'op_reimb'; } return $isCashless ? 'ip_cashless' : 'ip_reimb'; } /** @param array $row */ private function claimAmount(array $row): float { return $this->toFloat($row['claim_amount'] ?? 0); } /** @param array $row */ private function incurredAmount(array $row, string $statusBucket): float { if (in_array($statusBucket, ['doc_shortfall', 'policy_exclusion'], true)) { return 0.0; } $incurred = $this->toFloat($row['incurred_amount'] ?? null); if ($incurred > 0) { return $incurred; } return $this->toFloat($row['claim_approved_amount'] ?? 0); } /** @param array $row */ private function approvedAmount(array $row): float { $approved = $this->toFloat($row['claim_approved_amount'] ?? 0); if ($approved > 0) { return $approved; } return $this->incurredAmount($row, $this->statusBucket((string) ($row['claim_status'] ?? ''), $row)); } /** @param array $row */ private function isIpAnalyticsRow(array $row): bool { if (!str_starts_with($this->claimLane($row), 'ip_')) { return false; } $status = $this->statusBucket((string) ($row['claim_status'] ?? ''), $row); return in_array($status, ['paid', 'processed'], true); } /** @param array $row */ private function relationLabel(array $row): string { $r = strtoupper(trim((string) ($row['benef_relation'] ?? ''))); if ($r === '' || str_contains($r, 'SELF') || str_contains($r, 'EMPLOYEE')) { return 'Self'; } if (str_contains($r, 'SPOUSE') || str_contains($r, 'WIFE') || str_contains($r, 'HUSBAND')) { return 'Spouse'; } if (str_contains($r, 'CHILD') || str_contains($r, 'SON') || str_contains($r, 'DAUGHTER')) { return 'Child'; } if (str_contains($r, 'PARENT') || str_contains($r, 'FATHER') || str_contains($r, 'MOTHER') || str_contains($r, 'IN LAW') || str_contains($r, 'IN-LAW')) { return 'Parent'; } return 'Others'; } /** @param array $row */ private function ageBand(array $row): string { $age = (int) $this->toFloat($row['benef_age'] ?? 0); if ($age <= 0) { return 'Not classified'; } if ($age <= 5) { return '0-5'; } if ($age <= 10) { return '6-10'; } if ($age <= 15) { return '11-15'; } if ($age <= 20) { return '16-20'; } if ($age <= 25) { return '21-25'; } if ($age <= 30) { return '26-30'; } if ($age <= 35) { return '31-35'; } if ($age <= 40) { return '36-40'; } if ($age <= 45) { return '41-45'; } if ($age <= 50) { return '46-50'; } if ($age <= 55) { return '51-55'; } if ($age <= 60) { return '56-60'; } if ($age <= 65) { return '61-65'; } if ($age <= 70) { return '66-70'; } return '71-more'; } private function amountBand(float $amt): string { if ($amt <= 10000) { return 'Upto 10000'; } if ($amt <= 25000) { return '10001 - 25000'; } if ($amt <= 50000) { return '25001 – 50000'; } if ($amt <= 75000) { return '50001 – 75000'; } if ($amt <= 100000) { return '75001 – 100000'; } if ($amt <= 200000) { return '100001 – 200000'; } if ($amt <= 300000) { return '200001 – 300000'; } if ($amt <= 400000) { return '300001 – 400000'; } if ($amt <= 500000) { return '400001 - 500000'; } if ($amt <= 750000) { return '500001 - 750000'; } if ($amt <= 1000000) { return '750001 - 1000000'; } return 'Above 1000000'; } private function policyRunDays(mixed $start, mixed $end): int { $startTs = strtotime((string) $start); $endTs = strtotime((string) $end); if ($startTs === false || $endTs === false || $endTs < $startTs) { return 0; } return (int) floor(($endTs - $startTs) / 86400) + 1; } private function toFloat(mixed $value): float { if (is_numeric($value)) { return (float) $value; } $s = preg_replace('/[^0-9.\-]/', '', (string) $value) ?? ''; return $s === '' || $s === '-' || $s === '.' ? 0.0 : (float) $s; } private function parseFormattedNumber(mixed $value): float { return $this->toFloat($value); } /** @param list> $rows */ private function firstRow(array $rows): array { return $rows[0] ?? []; } private function formatDateLong(mixed $value): string { if ($value === null || $value === '' || $value === '0000-00-00') { return ''; } $ts = strtotime((string) $value); return $ts ? date('F j, Y', $ts) : (string) $value; } private function formatDateShort(mixed $value): string { if ($value === null || $value === '' || $value === '0000-00-00') { return date('d M Y'); } $ts = strtotime((string) $value); return $ts ? date('d M Y', $ts) : (string) $value; } }