diff --git a/app/Config/Filters.php b/app/Config/Filters.php index 2cf9a260..00faa273 100755 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -13,6 +13,7 @@ use App\Filters\AuthMVC; use App\Filters\HttpRequestLog; use App\Filters\CloseDbConnection; use App\Filters\AuthClientApi; +use App\Filters\CommissionApiFilter; use App\Filters\AuthJWT; @@ -36,7 +37,8 @@ class Filters extends BaseConfig 'HttpRequestLog' => HttpRequestLog::class, 'authJWT' => AuthJWT::class, 'AuthClientApi' => AuthClientApi::class, - 'CloseDbConnection' => CloseDbConnection::class + 'CloseDbConnection' => CloseDbConnection::class, + 'CommissionApiFilter' => CommissionApiFilter::class ]; /** diff --git a/app/Config/Routes.php b/app/Config/Routes.php index ad1d3874..7b5a1fca 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -34,6 +34,7 @@ $routes->get("updateRenewalData", "ClientController::updateRenewalData"); $routes->get("updateRenewalDataNotExistingClient", "ClientController::updateRenewalDataNotExistingClient"); $routes->get("updateRenewalInsurerData", "ClientController::updateRenewalInsurerData"); $routes->get("sendMutipleToEmails", "MasterController::sendMutipleToEmails"); +$routes->post("getCommission", "InsuranceCommissionController::initiateCommissionCalc",['filter' => 'CommissionApiFilter']); // $routes->post("iAgreeForAddOn", "EmployeeRestController::iAgreeForAddOn"); // $routes->get("sendCroneRemainderMail", "DashboardController::sendCroneRemainderMail"); // $routes->post("employeeUpload", "EmployeeRestController::employeeUpload"); diff --git a/app/Controllers/InsuranceCommissionController.php b/app/Controllers/InsuranceCommissionController.php new file mode 100644 index 00000000..11f61daa --- /dev/null +++ b/app/Controllers/InsuranceCommissionController.php @@ -0,0 +1,274 @@ +myLogger = \Config\Services::mylogger(); + // Load rules file if present in writable config path + // $rulesPath = WRITEPATH . 'config/insurance_rules.json'; + // if (file_exists($rulesPath)) { + // $this->loadRulesFromFile($rulesPath); + // } + } + + /** + * POST /insurance/calculate + * Accepts JSON body with policy data and returns commission calculation + */ + public function initiateCommissionCalc() + { + // Accept POST params (JSON, form-data, x-www-form-urlencoded) + $input = $this->request->getPost(); + + if (empty($input)) { + $json = $this->request->getJSON(true); + if ($json) { + $input = $json; + } + } + + if (empty($input)) { + return $this->failValidationError('No input data received'); + } + // -------- Required Params Check -------- + if (empty($input['policy_issue_date'])) { + return $this->failValidationError('policy_issue_date is required'); + } + + if (empty($input['department'])) { + return $this->failValidationError('department is required'); + } + + if (empty($input['insurer_id'])) { + return $this->failValidationError('insurer_id is required'); + } + + + // -------- Build Dynamic Rules Path -------- + $policyDate = strtotime($input['policy_issue_date']); + if (!$policyDate) { + return $this->failValidationError('Invalid policy_issue_date'); + } + + $month = strtoupper(date('M', $policyDate)); // SEP + $year = date('Y', $policyDate); // 2025 + $folderName = $month . $year; // SEP2025 + + $insurerId = $input['insurer_id']; // 5 + $department = ucfirst(strtolower($input['department'])); // Motor, Health, Fire + + // Final Path: WRITEPATH/rules/SEP2025/5_Motor.json + $rulesPath = WRITEPATH . "uploads/commission/rules/{$folderName}/{$insurerId}_{$department}.json"; + // echo $rulesPath;die(); + + if (!file_exists($rulesPath)) { + return $this->fail("Rules file not found at: {$rulesPath}"); + } + + // Load the dynamic rule set + $this->loadRulesFromFile($rulesPath); + + // -------- Execute Rule Matching & Commission Calculation -------- + try { + $result = $this->calculateCommission($input); + + $comment = isset($result['rule']['name']) + ? "Matched rule: " . $result['rule']['name'] + : "Matched rule: (unnamed rule)"; + + return $this->respond([ + 'success' => true, + 'data' => [ + 'payout' => $result['payout'], + 'rule' => $result['rule'], + 'comment' => $comment, + // 'rules_path_used' => $rulesPath + ] + ]); + + } catch (\Exception $e) { + return $this->fail($e->getMessage()); + } + } + + + /** + * Load rules JSON and normalise department keys to lowercase for lookups + */ + private function loadRulesFromFile(string $filePath) + { + if (!file_exists($filePath)) { + throw new \Exception("Rules file not found: {$filePath}"); + } + + $json = file_get_contents($filePath); + $parsed = json_decode($json, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \Exception('Invalid JSON in rules file: ' . json_last_error_msg()); + } + + // Normalise department keys to lowercase for consistent lookups + $this->rules = []; + foreach ($parsed as $dept => $rules) { + $this->rules[strtolower($dept)] = $rules; + } + } + + public function calculateCommission(array $policyData) + { + $department = $policyData['department'] ?? ''; + $deptKey = strtolower($department); + + if (!isset($this->rules[$deptKey])) { + throw new \Exception("No rules found for department: {$department}"); + } + + $matchingRules = []; + + foreach ($this->rules[$deptKey] as $rule) { + if ($this->evaluateConditions($rule['conditions'] ?? [], $policyData)) { + $matchingRules[] = $rule; + } + } + + if (empty($matchingRules)) { + throw new \Exception('No matching rules found for the policy data'); + } + + // Use the first matching rule. In future you can implement priority/weighting + $applicableRule = $matchingRules[0]; + + $payout = $this->applyCalculation($applicableRule['calculation'], $policyData); + + return ['rule' => $applicableRule, 'payout' => $payout]; + } + + private function evaluateConditions(array $conditions, array $data): bool + { + foreach ($conditions as $condition) { + $field = $condition['field']; + $operator = $condition['operator']; + $expectedValue = $condition['value']; + + if (!array_key_exists($field, $data)) { + return false; + } + + $actualValue = $data[$field]; + + if (!$this->compareValues($actualValue, $operator, $expectedValue)) { + return false; + } + } + + return true; + } + + private function compareValues($actual, string $operator, $expected): bool + { + switch ($operator) { + case '==': + return $actual == $expected; + case '!=': + return $actual != $expected; + case '>': + return $actual > $expected; + case '>=': + return $actual >= $expected; + case '<': + return $actual < $expected; + case '<=': + return $actual <= $expected; + case 'between': + return is_array($expected) && $actual >= $expected[0] && $actual <= $expected[1]; + case 'in': + return is_array($expected) && in_array($actual, $expected); + default: + throw new \Exception("Unsupported operator: {$operator}"); + } + } + + private function applyCalculation(array $calculation, array $policyData) + { + $type = $calculation['type'] ?? null; + + switch ($type) { + case 'percentage': + $percentage = $calculation['value'] ?? 0; + $base = $calculation['on'] ?? null; + + if ($base === null || !isset($policyData[$base])) { + throw new \Exception("Base value for calculation not found: {$base}"); + } + + return ($percentage / 100) * $policyData[$base]; + + case 'composite': + $total = 0; + + foreach ($calculation['components'] as $component) { + $percentage = $component['percentage'] ?? 0; + $base = $component['on'] ?? null; + + if ($base === null || !isset($policyData[$base])) { + throw new \Exception("Base value for calculation not found: {$base}"); + } + + if (!empty($component['only_first_year'])) { + if (!empty($policyData['is_renewal'])) { + continue; // Skip this component for renewals + } + } + + $total += ($percentage / 100) * $policyData[$base]; + } + + return $total; + + case 'fixed': + $fixedAmount = $calculation['value'] ?? 0; + + // If 'on' specified but not needed, return fixed amount as-is + return $fixedAmount; + + default: + throw new \Exception('Unsupported calculation type: ' . $type); + } + } + + public function getVolumeReward(array $premiumData) + { + $annualPremium = $premiumData['annual_premium'] ?? 0; + $department = $premiumData['department'] ?? ''; + + if ($department === 'Fire' || $department === 'Marine' || $department === 'Engineering') { + if ($annualPremium > 20000000) { + return 0.01 * $annualPremium; + } elseif ($annualPremium > 10000000) { + return 0.005 * $annualPremium; + } elseif ($annualPremium > 5000000) { + return 0.0025 * $annualPremium; + } + } elseif ($department === 'Motor') { + if ($annualPremium > 15000000) { + return 0.02 * $annualPremium; + } elseif ($annualPremium > 7500000) { + return 0.01 * $annualPremium; + } + } + + return 0; + } +} diff --git a/app/Controllers/MasterController.php b/app/Controllers/MasterController.php index 76bd1a5a..d2ce1329 100755 --- a/app/Controllers/MasterController.php +++ b/app/Controllers/MasterController.php @@ -1976,6 +1976,9 @@ class MasterController extends AdminController 'lead_files' => WRITEPATH . 'uploads/lead_files/', 'claim_files' => WRITEPATH . 'uploads/claim_files/', 'claim_dump_excel' => WRITEPATH . 'uploads/claim_dump_excel/', + 'commission' => WRITEPATH . 'uploads/commission/', + 'files' => WRITEPATH . 'uploads/commission/files', + 'rules' => WRITEPATH . 'uploads/commission/rules', 'claim_sample_forms' => ROOTPATH . 'public/claim_sample_forms/', ]; diff --git a/app/Filters/CommissionApiFilter.php b/app/Filters/CommissionApiFilter.php new file mode 100644 index 00000000..d7ae956e --- /dev/null +++ b/app/Filters/CommissionApiFilter.php @@ -0,0 +1,55 @@ +getHeaderLine('X'); + $authHeader = $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; + // echo $authHeader;die(); + if (empty($authHeader)) { + return service('response')->setJSON([ + 'success' => false, + 'error' => 'Authorization header missing' + ])->setStatusCode(403); + } + + // Expected format: Bearer YOUR_API_KEY + if (stripos($authHeader, 'Bearer ') !== 0) { + return service('response')->setJSON([ + 'success' => false, + 'error' => 'Invalid Authorization format. Expected: Bearer ' + ])->setStatusCode(403); + } + + $apiKey = trim(substr($authHeader, 7)); // extract token after 'Bearer ' + + $envKeys = getenv('ALLOWED_COMMISSION_API_KEYS'); + // Convert CSV -> Array + $allowedKeys = array_map('trim', explode(',', $envKeys)); + // print_r($allowedKeys);die(); + // Validate + if (!in_array($apiKey, $allowedKeys, true)) { + return service('response')->setJSON([ + 'success' => false, + 'error' => 'Invalid API Key' + ])->setStatusCode(403); + } + + // Allow request to proceed + return null; + } + + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + // Not needed + } +}