cwFactory = new CloudWatchLoggerFactory(); } public function createBrePayload($id) { $payload = array( 'identifier' => $id, 'grade' => array( 'rules' => null ) ); return $payload; } public function get_exhaustive_criteria_json($file_path, $application_id){ try { $s3 = Storage::disk('s3'); $this->cwFactory->logger(json_encode(['title' => 'readFile checking', 'file_path' => $file_path, 'application_id'=>$application_id])); if ($s3->exists($file_path)) { $this->cwFactory->logger(json_encode(['title' => 'exhaustive_json file readFile exists', 'file_path' => $file_path, 'application_id'=>$application_id])); return $s3->get($file_path); } return false; } catch (\Exception $e) { $this->cwFactory->logger(json_encode(['title' => 'exhaustive_json file readFile does not exists', 'file_path' => $file_path, 'error' =>$e, 'application_id'=>$application_id])); return false; } } public function create_rule_list($application_id, $grade_rule, $req_id, $tableOfOffers, $exhaustive_criteria_json, $pre_eligibility=null){ $rule_details = json_decode($grade_rule->rules); $rule_details = (array)$rule_details; if (array_key_exists('thin_file', $rule_details)) { // Skip the rules for NTC unset($rule_details['score']); } ksort($rule_details); $rule_details = (object)$rule_details; $rule_list = []; $applicationEmployment = DB::selectOne("SELECT employee_status from consumer_application where id = ?", [$application_id]) ?: (object) ['employee_status' => null]; $applicationElement = DB::selectOne("SELECT electricity_name_change from consumer_application_element where application_id = ?", [$application_id]) ?: (object) ['electricity_name_change' => null]; foreach ($rule_details as $key => $value) { try{ if (!empty($pre_eligibility)) { $pre_eligibility_key = $pre_eligibility.$key; $key = !isset($exhaustive_criteria_json[$pre_eligibility_key]) ? $key : $pre_eligibility_key; } if (!in_array($exhaustive_criteria_json[$key], $rule_list)) { if (($key == 'age_salaried' && $applicationEmployment->employee_status != 'SALARIED')) { continue; } if ($key == 'age_self_employed' && !in_array($applicationEmployment->employee_status, ['Self Employed - Business', 'Self Employed - Professional'])) { continue; } if ((str_contains($key, 'foir') || str_contains($key, 'cf_foir')) && $tableOfOffers) { continue; } if ($key == 'e_bill_fuzzy_match' && isset($applicationElement->electricity_name_change) && $applicationElement->electricity_name_change == 1) { continue; } $rule_list[] = $exhaustive_criteria_json[$key]; // Add the value to the rule list if ($exhaustive_criteria_json[$key]){ $fallback_rule = $exhaustive_criteria_json[$key]['fallback'] ?? null; if (is_array($fallback_rule) && isset($fallback_rule['rule'])){ $rule_list[] = $exhaustive_criteria_json[$fallback_rule['rule']]; } } } } catch (\Exception $e) { $this->bre_rules_ignored[] = ['rule' => $key]; (new Slack(new Request(['title' => 'Skipped a rule', 'fields' => ['error' => $e->getMessage(), 'line' => $e->getLine(), 'trace' => $e->getTraceAsString(), 'application_id' => $application_id], 'group' => Slack::$STAGE_TESTING])))->send(); } } // add loan to value limit rule by default $rule_list[] = $exhaustive_criteria_json['loan_to_value_limit']; return $rule_list; } public function create_rejection_rule_list($application_id, $rejectRules, $exhaustive_criteria_json, $applicant_type=1, $nach_bounce="") { try{ $rule_list = []; foreach ($rejectRules->new_rejection_rules as $key=>$reject_rule) { if (in_array($key, ['MAX_SCORE', 'MIN_SCORE'])) { $rule_name = $applicant_type == 1 ? 'rejection_score' : 'co_applicant_rejection_score'; } else if (in_array($key, ['MAX_AGE', 'MIN_AGE'])) { $rule_name = $applicant_type == 1 ? 'rejection_age' : 'co_applicant_rejection_age'; } else if ($key == 'NEGATIVE_AREA') { if ($reject_rule) { $rule_name = $applicant_type == 1 ? 'negative_area' : 'co_applicant_negative_area'; } } else if ($key == 'APPROVED_PINCODE') { if ($reject_rule) { $rule_name = $applicant_type == 1 ? 'is_pincode_approved_or_risky' : 'co_applicant_is_pincode_approved_or_risky'; } } elseif ($applicant_type == 2 && $key == 'MONTHLY_INCOME') { $rule_name = 'co_applicant_rejection_monthly_income'; } else { $rule_name = 'rejection_'.strtolower($key); } if (!in_array($exhaustive_criteria_json[$rule_name], $rule_list)) { if ($rule_name == 'rejection_nach_bounce' && empty($nach_bounce)) { continue; } $rule_list[] = $exhaustive_criteria_json[$rule_name]; if ($exhaustive_criteria_json[$rule_name]){ $fallback_rule = $exhaustive_criteria_json[$rule_name]['fallback'] ?? null; if (is_array($fallback_rule) && isset($fallback_rule['rule'])){ $rule_list[] = $exhaustive_criteria_json[$fallback_rule['rule']]; } } } } return $rule_list; } catch (\Exception $e) { $this->bre_rules_ignored[] = ['rule' => $key]; (new Slack(new Request(['title' => 'Skipped a rule', 'fields' => ['error' => $e->getMessage(), 'line' => $e->getLine(), 'trace' => $e->getTraceAsString(), 'application_id' => $application_id], 'group' => Slack::$STAGE_TESTING])))->send(); } } public function runJsonBreApproval($application_id, $req_id, $gradeID, $tableOfOffers, $json_bre_for_log=false, $request=null) { try{ $sql = "SELECT id, rule_name, rules from approval_rule where grade_id = ? and status = 1"; $bindings = [$gradeID]; $rule_filter = $request->attributes->get('rule_filter'); if ($rule_filter) { $sql .= " AND rules LIKE ?"; $bindings[] = '%' . $rule_filter . '%'; } $grade_rules = DB::select($sql, $bindings); $bre_api_payload = $this->createBrePayload($application_id); $bre_api_payload['applicant_type'] = 1; // Default for applicant in this method $is_pre_eligibility = $request && $request->attributes->get('pre_eligibility') ? true : false; if ($is_pre_eligibility) { $bre_api_payload['pre_eligibility'] = true; $application = PreEligibilityApplication::with('creditReport', 'merchant')->where('id', $application_id)->first(); if ($application) { // shim properties so the shared policy-payload block can read uniform field names $application->approved_amount = $application->loan_amount; $application->name = $application->customer_name; } $netLoanAmount = $application ? (int) $application->loan_amount : 0; $pre_eligibility_prefix = 'pre_eligibility_'; } else { $application = ConsumerApplication::with('emi', 'application_element', 'creditReport')->where('id', $application_id)->first(); $netLoanAmount = $application->approved_amount - $application->emi->upfront_from_customer; $pre_eligibility_prefix = ''; } $pirimidData = DB::selectOne( "SELECT id, tracking_id, response FROM pirimid_bank_analysis WHERE application_id = ? AND analytics_fetch = 1 AND applicant_type = 1 ORDER BY created_at DESC LIMIT 1", [$application_id] ); $trackingId = null; if (isset($pirimidData->id)) { if (!empty($pirimidData->tracking_id)) { $trackingId = $pirimidData->tracking_id; } else if (!empty($pirimidData->response)) { $response = json_decode($pirimidData->response, 1); $trackingId = $response['trackingId'] ?? null; } } if ($trackingId) { $bre_api_payload['pirimid_tracking_id'] = $trackingId; } $accountAggregatorId = $request ? $request->attributes->get('account_aggregator_id') : null; if (is_null($trackingId) && $accountAggregatorId) { $bre_api_payload['account_aggregator_id'] = $accountAggregatorId; } $file_path = _env('JSON_BRE_EXHAUSTIVE_JSON'); $exhaustive_criteria_json = $this->get_exhaustive_criteria_json($file_path, $application_id); $exhaustive_criteria_json = json_decode($exhaustive_criteria_json, 1); foreach ($grade_rules as $grade_rule) { $rule = json_decode($grade_rule->rules, 1); $policy = []; if (isset($rule['emi_multiplier_on_abb']) && !empty($rule['emi_multiplier_on_abb']) && $rule['emi_multiplier_on_abb'] != 0) { if (empty($trackingId) && empty($accountAggregatorId)) { $this->cwFactory->logger(json_encode([ 'title' => 'Skipping ABB Policy - No Pirimid Tracking ID/Bank Statement', 'application_id' => $application_id, 'rule_id' => $grade_rule->id ])); continue; } $this->adjustedABBMultiplier = (float)$rule['emi_multiplier_on_abb']; $policy_name = "policy-adjusted-abb-" . $grade_rule->id; } elseif (isset($rule['power_savings'])) { $policy_name = "policy-power-savings-" . $grade_rule->id; } elseif (isset($rule['bureau_surrogate'])) { $policy_name = "policy-bureau-surrogate-" . $grade_rule->id; } else { $policy_name = "policy-" . $grade_rule->id; } $rule_list = $this->create_rule_list($application_id, $grade_rule, $req_id, $tableOfOffers, $exhaustive_criteria_json); if (isset($accountAggregatorId) && !empty($accountAggregatorId)) { $rule_list = array_map(function($item) { $encoded = json_encode($item); $replaced = str_replace('pirimid', 'account_aggregator', $encoded); return json_decode($replaced, true); }, $rule_list); } if($rule_list){ $policy['rule'] = $rule_list; } else{ $policy['rule'] = []; } $policy['payload'] = array( 'identifier' => $application_id , 'applicant_type' => 1 , 'rule_name' => $grade_rule->rule_name , 'loan_amount' => isset($application->approved_amount) ? (int)($application->approved_amount) : null // , 'max_loan_amount' => isset($rule['max_loan_amount']) ? (int)($rule['max_loan_amount']) : null // , 'min_loan_amount' => isset($rule['min_loan_amount']) ? (int)($rule['min_loan_amount']) : null , 'employee_status' => isset($rule['employee_status']) ? ($rule['employee_status']) : null , 'past_dues' => isset($rule['past_dues']) ? (int)($rule['past_dues']) : null , 'credit_card_percentage' => isset($rule['credit_card_amount_paid']) ? (int)($rule['credit_card_amount_paid']) : null , 'age' => isset($rule['age']) ? (int)($rule['age']) : null , 'ageAtLastEMI' => isset($rule['ageAtLastEMI']) ? (int)($rule['ageAtLastEMI']) : null , 'monthly_income' => isset($rule['monthly_income']) ? (int)($rule['monthly_income']) : null , 'score' => isset($rule['score']) ? (int)($rule['score']) : null // , 'tenure' => isset($rule['tenure']) ? (int)($rule['tenure']) : null // , 'cf_foir' => isset($rule['cf_foir']) ? (int)($rule['cf_foir']) : null // , 'foir' => isset($rule['foir']) ? (int)($rule['foir']) : null , 'case_type' => isset($rule['caseType']) ? ($rule['caseType']) : null , 'dpd_years_above_0' => isset($rule['dpd_years_above_zero']) ? (int)($rule['dpd_years_above_zero']) : null , 'dpd_count_above_0' => isset($rule['dpd_count_above_zero']) ? (int)($rule['dpd_count_above_zero']) : null , 'pre_dpd_years' => isset($rule['pre_dpd_years']) ? (int)($rule['pre_dpd_years']) : null , 'pre_dpd_count' => isset($rule['pre_dpd_count']) ? (int)($rule['pre_dpd_count']) : null , 'pre_dpd_keywords' => isset($rule['pre_dpd_keywords']) ? ($rule['pre_dpd_keywords']) : null , 'post_dpd_year' => isset($rule['post_dpd_year']) ? (int)($rule['post_dpd_year']) : null , 'post_dpd_count' => isset($rule['post_dpd_count']) ? (int)($rule['post_dpd_count']) : null , 'post_dpd_keywords' => isset($rule['post_dpd_keywords']) ? ($rule['post_dpd_keywords']) : null // , 'surrogate_percentage' => isset($rule['bureau_surrogate']) ? (int)($rule['bureau_surrogate']) : null , 'term_loan_amount_paid_percentage' => isset($rule['term_loan_amount_paid']) ? (int)($rule['term_loan_amount_paid']) : null , 'power_savings' => isset($application->application_element->inverter_power_savings) ? (float)($application->application_element->inverter_power_savings) : null , 'emi' => isset($application->emi->emi) ? (int)($application->emi->emi) : null , 'zero_downpayment' => 0 , 'bank_validation' => isset($rule['bank_validation']) ? (int)($rule['bank_validation']) : null // New rules payload added , 'solar_financing_for' => isset($rule['solar_financing_for']) ? $rule['solar_financing_for'] : null , 'solar_power_type' => isset($rule['solar_power_type']) ? $rule['solar_power_type'] : null , 'kw_capping' => isset($rule['kw_capping']) ? $rule['kw_capping'] : false , 'bureau_type' => $application->creditReport[0]->bureau , 'keyword_rules' => isset($rule['keyword_rules']) ? ($rule['keyword_rules']) : null , 'account_status_rules' => isset($rule['account_status_rules']) ? ($rule['account_status_rules']) : null , 'dpd_rules' => isset($rule['dpd_rules']) ? ($rule['dpd_rules']) : null , 'past_dues_rules' => isset($rule['past_dues_rules']) ? ($rule['past_dues_rules']) : null , 'principal_written_off_rules' => isset($rule['principal_written_off_rules']) ? ($rule['principal_written_off_rules']) : null , 'bureau_surrogate' => isset($rule['bureau_surrogate']) ? $rule['bureau_surrogate'] : null , 'age_self_employed' => isset($rule['age_self_employed']) ? array_map('intval', $rule['age_self_employed']) : null , 'age_salaried' => isset($rule['age_salaried']) ? array_map('intval', $rule['age_salaried']) : null , 'max_net_tenure' => isset($rule['max_net_tenure']) ? (int)($rule['max_net_tenure']) : null , 'min_net_tenure' => isset($rule['min_net_tenure']) ? (int)($rule['min_net_tenure']) : null , 'max_net_loan_amount' => isset($rule['max_net_loan_amount']) ? (int)($rule['max_net_loan_amount']) : null , 'min_net_loan_amount' => isset($rule['min_net_loan_amount']) ? (int)($rule['min_net_loan_amount']) : null , 'track_record' => isset($rule['track_record']) ? $rule['track_record'] : null , 'e_bill_fuzzy_match' => isset($rule['e_bill_fuzzy_match']) ? (int)$rule['e_bill_fuzzy_match'] : 0 , 'net_loan_amount' => $netLoanAmount ?? 0 , 'declared_monthly_income' => isset($rule['speculative_txn_cap_pct']) ? (int)($application->monthly_income) : null , 'applicant_name' => isset($rule['name_match_threshold_pct']) ? $application->name : null , 'min_banking_months' => isset($rule['min_banking_months']) ? (int)$rule['min_banking_months'] : null , 'name_match_threshold_pct' => isset($rule['name_match_threshold_pct']) ? (int)$rule['name_match_threshold_pct'] : null , 'emi_bounce_rule' => isset($rule['emi_bounce_rule']['max_emi_bounces']) ? (int)$rule['emi_bounce_rule']['max_emi_bounces'] : null , 'emi_bounce_lookback_months' => isset($rule['emi_bounce_rule']['emi_bounce_lookback_months']) ? (int)$rule['emi_bounce_rule']['emi_bounce_lookback_months'] : 3 , 'speculative_txn_cap_pct' => isset($rule['speculative_txn_cap_pct']) ? (float)$rule['speculative_txn_cap_pct'] : null , 'loan_to_value_limit' => $application->merchant->loan_to_value_limit ); if (isset($rule['thin_file']) || isset($rule['pre_eligibility_thin_file'])) { $policy['payload']['thin_file_score'] = [-1, 10]; // thin file values set // unset($policy['payload']['score']); } if (!$tableOfOffers) { $policy['payload']['tenure'] = isset($rule['tenure']) ? (int)($rule['tenure']) : null; $policy['payload']['cf_foir'] = isset($rule['cf_foir']) ? (int)($rule['cf_foir']) : null; $policy['payload']['foir'] = isset($rule['foir']) ? (int)($rule['foir']) : null; } if ($is_pre_eligibility) { $policy['payload']['tenure'] = $application->tenure; } $this->cwFactory->logger(json_encode(['title' => 'BRE_API payload addition attempted', 'application_id' => $application_id, 'rule'=>$grade_rule->id])); if (isset($policy)){ $bre_api_payload['grade']['rules'][$policy_name] = $policy; } } if($json_bre_for_log) { // Rejection rules for JSON BRE $auto_reject_rule = DB::selectOne("SELECT modifications from auto_reject_rule where grade_id = ? order by created_at desc", [$gradeID]); $reject_rules = json_decode($auto_reject_rule->modifications); $rejectionPolicy = []; $rejectionPolicy['payload'] = array( 'identifier' => $application_id, 'applicant_type' => 1, 'rule_name' => 'auto-rejection-rule', 'age_range' => [(int)$reject_rules->new_rejection_rules->MIN_AGE, (int)$reject_rules->new_rejection_rules->MAX_AGE], 'monthly_income' => isset($rule['monthly_income']) ? (int)($rule['monthly_income']) : null, 'credit_score_range' => [(int)$reject_rules->new_rejection_rules->MIN_SCORE, (int)$reject_rules->new_rejection_rules->MAX_SCORE], 'bureau_type' => $application->creditReport[0]->bureau, 'dpd_rejection_rules' => $reject_rules->new_rejection_rules->DPD[0], 'keyword_rejection_rules' => $reject_rules->new_rejection_rules->KEYWORD[0], 'account_status_rejection_rules' => $reject_rules->new_rejection_rules->ACCOUNT_STATUS[0], 'principal_write_off_rules' => $reject_rules->new_rejection_rules->PRINCIPAL_WRITTEN_OFF_AMOUNT[0], 'total_past_dues_rules' => $reject_rules->new_rejection_rules->TOTAL_PAST_DUES[0], 'nach_bounce_rule' => $reject_rules->new_rejection_rules->NACH_BOUNCE ); $policy_name = "policy-rejection"; $rule_list = $this->create_rejection_rule_list($application_id, $reject_rules, $exhaustive_criteria_json, 1, $trackingId); if($rule_list){ $rejectionPolicy['rule'] = $rule_list; } $bre_api_payload['grade']['rules'][$policy_name] = $rejectionPolicy; } try{ $response = $this->runAutoDecisionBRE($bre_api_payload); if(!$json_bre_for_log){ $this->store_jsonbre_response($response, $req_id, false, false, (int)$is_pre_eligibility, self::resolveApprovalLogUserId($request)); } return $response; } catch(\Exception $e) { $this->cwFactory->logger(json_encode(['title' => 'BRE_API run Failed', 'application_id' => $application_id, 'req_id'=> $req_id])); } } catch(\Exception $e) { $this->cwFactory->logger(json_encode(['title' => 'Create BRE Payload failed', 'application_id' => $application_id, 'error'=>$e->getMessage(), 'line' => $e->getLine(), 'trace' => $e->getTraceAsString()])); } } public function runRejection($application_id, $req_id, $auto_reject_rule, $json_bre_for_log=false, $pirimid_tracking_id='', $applicant_type = 1, $account_aggregator_id='') { try{ $bre_api_payload = $this->createBrePayload($application_id); $bre_api_payload['applicant_type'] = $applicant_type; $file_path = _env('JSON_BRE_EXHAUSTIVE_JSON'); $exhaustive_criteria_json = $this->get_exhaustive_criteria_json($file_path, $application_id); $exhaustive_criteria_json = json_decode($exhaustive_criteria_json, 1); $creditReport = DB::selectOne("SELECT id, bureau from credit_report where application_id = ? and applicant_type = ?", [$application_id, $applicant_type]); $modifications = json_decode($auto_reject_rule->modifications, true); $policiesToCheck = []; if ($applicant_type == 2) { // For co-applicant, we might have both income and signing rules in the same JSON if (isset($modifications['co_applicant_for_income'])) { $policiesToCheck['policy-rejection-income'] = $modifications['co_applicant_for_income']; } if (isset($modifications['co_applicant_for_signing'])) { $policiesToCheck['policy-rejection-signing'] = $modifications['co_applicant_for_signing']; } // Backward compatibility for old co-applicant records if (empty($policiesToCheck)) { $policiesToCheck['policy-rejection'] = $modifications; } } else { $policiesToCheck['policy-rejection'] = $modifications; } foreach ($policiesToCheck as $policyName => $reject_rules) { $reject_rules = (object)$reject_rules; $rejectionPolicy = []; $rejectionPolicy['payload'] = array( 'identifier' => $application_id, 'applicant_type' => $applicant_type, 'rule_name' => 'auto-rejection-rule', 'age_range' => [(int)$reject_rules->new_rejection_rules['MIN_AGE'], (int)$reject_rules->new_rejection_rules['MAX_AGE']], 'monthly_income' => isset($reject_rules->new_rejection_rules['MONTHLY_INCOME']) ? (int)($reject_rules->new_rejection_rules['MONTHLY_INCOME']) : null, 'credit_score_range' => [(int)$reject_rules->new_rejection_rules['MIN_SCORE'], (int)$reject_rules->new_rejection_rules['MAX_SCORE']], 'bureau_type' => $creditReport->bureau, 'dpd_rejection_rules' => $reject_rules->new_rejection_rules['DPD'][0], 'keyword_rejection_rules' => $reject_rules->new_rejection_rules['KEYWORD'][0], 'account_status_rejection_rules' => $reject_rules->new_rejection_rules['ACCOUNT_STATUS'][0], 'principal_write_off_rules' => $reject_rules->new_rejection_rules['PRINCIPAL_WRITTEN_OFF_AMOUNT'][0], 'total_past_dues_rules' => $reject_rules->new_rejection_rules['TOTAL_PAST_DUES'][0] ); if ($pirimid_tracking_id == ''){ $pirimidAnalysisAvailable = DB::selectOne("SELECT id, response from pirimid_bank_analysis where application_id = ? and applicant_type = ? and analytics_fetch = 1 order by created_at desc", [$application_id, $applicant_type]); if (isset($pirimidAnalysisAvailable->id)) { $pResponse = json_decode($pirimidAnalysisAvailable->response, 1); if (isset($pResponse['trackingId'])) { $pirimid_tracking_id = $pResponse['trackingId']; } } } // TODO - Incase pirimid tracking id is present then the account aggregator flow will not work. if (isset($pirimid_tracking_id) && $pirimid_tracking_id != '') { $bre_api_payload['pirimid_tracking_id'] = $pirimid_tracking_id; $rejectionPolicy['payload']['nach_bounce_rule'] = $reject_rules->new_rejection_rules['NACH_BOUNCE']; } if ($pirimid_tracking_id == '' && isset($account_aggregator_id) && $account_aggregator_id != '') { $bre_api_payload['account_aggregator_id'] = $account_aggregator_id; } $rule_list = $this->create_rejection_rule_list($application_id, (object)$reject_rules, $exhaustive_criteria_json, $applicant_type,$pirimid_tracking_id); if($rule_list){ $rejectionPolicy['rule'] = $rule_list; } $bre_api_payload['grade']['rules'][$policyName] = $rejectionPolicy; } $response = $this->runAutoDecisionBRE($bre_api_payload); if(!$json_bre_for_log){ $this->store_jsonbre_response($response, $req_id, true); } return $response; } catch(\Exception $e){ $this->cwFactory->logger(json_encode(['application_id'=> $application_id, 'error'=>$e->getMessage(), 'line' => $e->getLine(), 'trace' => $e->getTraceAsString()])); } } /** * Resolve the user to attribute jsonbre_approval_status_log rows to. * * The admin auto-decision flow merges the acting admin's id into the request * (ConsumerApplicationController::autoDecisionOnWaitingForDecision), so when a * request carrying `user_id` is available we use it. Every other flow * (console commands, queue jobs, pre-eligibility, PSU/SSE) is system driven * and is attributed to the system user 107. */ public static function resolveApprovalLogUserId($request = null) { $userId = $request ? $request->get('user_id') : null; return (is_numeric($userId) && (int)$userId > 0) ? (int)$userId : self::SYSTEM_USER_ID; } public function store_jsonbre_response($response, $req_id, $rejection=false, $reasonDetails=false, $is_pre_eligibility=0, $user_id=self::SYSTEM_USER_ID) { $user_id = (is_numeric($user_id) && (int)$user_id > 0) ? (int)$user_id : self::SYSTEM_USER_ID; if ($reasonDetails) { $this->cwFactory->logger(json_encode(['title' => 'Default check failed', 'reason'=>$reasonDetails['reason'], 'application_id' => $reasonDetails['application_id']])); $logv2 = new JsonBreApprovalStatusLog(); $logv2->rule_id = 0; $logv2->application_id = $reasonDetails['application_id']; $logv2->user_id = $user_id; $logv2->reason = $reasonDetails['reason']; $logv2->request_batch_id = $req_id; $logv2->status = 0; $logv2->created_at = Carbon::now()->toDateTimeString(); $logv2->save(); return; } $serverIP = isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : ''; $data = $response->getData(); $decoded_response = isset($data->res) ? json_decode($data->res) : ""; $bre_api_payload = isset($data->req) ? $data->req : ""; $this->cwFactory->logger(json_encode(['title' => 'Storing BRE Logs', 'ip'=> $serverIP, 'application_id' => $bre_api_payload->identifier, 'req_id'=> $req_id, 'request'=>$data->req, 'response'=>$data->res, 'rules_ignored'=>$this->bre_rules_ignored])); $log = new BreLog(); $log->application_id = $bre_api_payload->identifier; $log->applicant_type = isset($bre_api_payload->applicant_type) ? (int)$bre_api_payload->applicant_type : 1; $log->request = json_encode($data->req); $log->response = json_encode($decoded_response); $log->rule_ignored = json_encode($this->bre_rules_ignored); $log->created_at = Carbon::now()->toDateTimeString(); $log->request_batch_id = $req_id; $log->pre_eligibility = $is_pre_eligibility; $log->save(); if($rejection) { return; } if (!empty($decoded_response)) { foreach ($decoded_response as $key => $value) { // Store the key as a variable if ($key === 'status') { continue; // This skips the rest of the loop for this iteration } $logv2 = new JsonBreApprovalStatusLog(); $pattern = '/(\d+)$/'; $matches = []; $rule_id = preg_match($pattern, $key, $matches) ? (int)$matches[1] : 0; $logv2->rule_id = $rule_id; $logv2->application_id = $bre_api_payload->identifier; $logv2->user_id = $user_id; $logv2->failed_condition = isset($value->failed_condition) ? $value->failed_condition->key : ""; $logv2->reason = isset($value->failed_condition->reason)? $value->failed_condition->reason : ""; $logv2->request_batch_id = $req_id; $logv2->status=(int)$value->policy_status; $logv2->created_at = Carbon::now()->toDateTimeString(); $logv2->save(); } } } public function getJsonApprovalLogs($id) { $approvalCheck = JsonBreApprovalStatusLog::where('application_id', $id)->orderBy('created_at', 'desc')->first(); if (!isset($approvalCheck->id)) { return false; } $result = $this->checkIfBankingPolicy($approvalCheck->rule_id); if ($result) { // Get ALL distinct request_batch_ids ordered by latest first $allRequestIds = JsonBreApprovalStatusLog::select('request_batch_id') ->where('application_id', $id) ->groupBy('request_batch_id') ->orderByRaw('MAX(created_at) DESC') ->pluck('request_batch_id') ->toArray(); // The latest batch is always a banking policy (we already checked above). // Walk through the remaining batches (starting from index 1 = second-last) // and skip any that are also banking policy, until we find one that is not. $latestRequestId = $allRequestIds[0]; $nonBankingRequestId = null; for ($i = 1; $i < count($allRequestIds); $i++) { // Get the representative rule_id for this batch (use its most recent log entry) $batchLatestLog = JsonBreApprovalStatusLog::where('application_id', $id) ->where('request_batch_id', $allRequestIds[$i]) ->orderBy('created_at', 'desc') ->first(); if ($batchLatestLog && !$this->checkIfBankingPolicy($batchLatestLog->rule_id)) { // Found the first non-banking-policy batch $nonBankingRequestId = $allRequestIds[$i]; break; } // This batch is also banking policy — skip it and continue } // Build query using the latest batch + the found non-banking batch (if any) $requestIdsToFetch = [$latestRequestId]; if ($nonBankingRequestId !== null) { $requestIdsToFetch[] = $nonBankingRequestId; } return JsonBreApprovalStatusLog::with('rule') ->where('application_id', $id) ->whereIn('request_batch_id', $requestIdsToFetch) ->orderBy('created_at', 'desc') ->get(); } else { // Use existing logic — latest batch is not a banking policy return JsonBreApprovalStatusLog::with('rule') ->where('application_id', $id) ->where('request_batch_id', $approvalCheck->request_batch_id) ->get(); } return false; } public function checkIfBankingPolicy($rule_id) { // Query the approvalRule table using the rule_id $approvalRule = DB::table('approval_rule')->where('id', $rule_id)->first(); // Check if the rule exists and if the rules column contains emi_multiplier_on_abb key if ($approvalRule && isset($approvalRule->rules)) { $rules = json_decode($approvalRule->rules, true); return isset($rules['emi_multiplier_on_abb']) && !empty($rules['emi_multiplier_on_abb']); } return false; } public function fetchLatestJsonBreResponse($application_id){ // Get the relevant request_batch_ids from getJsonApprovalLogs $approvalLogs = $this->getJsonApprovalLogs($application_id); if ($approvalLogs && $approvalLogs->isNotEmpty()) { // Extract unique request_batch_ids from the approval logs $requestBatchIds = $approvalLogs->pluck('request_batch_id')->unique()->filter()->values()->toArray(); if (!empty($requestBatchIds)) { // Fetch bre_logs that match the request_batch_ids $bre_logs = DB::select( "SELECT id, response, request_batch_id FROM bre_logs WHERE application_id = ? AND applicant_type = 1 AND request_batch_id IN (" . implode(',', array_fill(0, count($requestBatchIds), '?')) . ") ORDER BY created_at DESC", array_merge([$application_id], $requestBatchIds) ); if (count($bre_logs) > 0) { return $bre_logs; } } } // Fallback: original logic — return the first non-rejection log $bre_log = DB::select("SELECT id, response from bre_logs where application_id = ? and applicant_type = 1 order by created_at desc", [$application_id]); if (count($bre_log) > 0) { foreach ($bre_log as $log) { $decoded_response = json_decode($log->response, 1); // display only the approval logs not the rejection logs if (!isset($decoded_response['policy-rejection'])) { return $log; } } } return false; } public function runAutoDecisionBRE($payload) { $curl = curl_init(); curl_setopt_array($curl, array( CURLOPT_URL => _env('BRE_API_UAT_URL'), CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => "", CURLOPT_MAXREDIRS => 10, CURLOPT_TIMEOUT => 120, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => array( "Content-Type: application/json", ), )); $res = curl_exec($curl); $data = json_decode($res, 1); $httpcode = curl_getinfo($curl, CURLINFO_HTTP_CODE); $err = curl_error($curl); curl_close($curl); return response()->json(['req' => $payload, 'res'=> $res, 'code' => $httpcode], 200); } /** * Calculate the eligible amount for a JSON BRE policy. * * The rule_id (taken from the JsonBreApprovalStatusLog / JSON BRE policy key) is used * to fetch the policy from the approval_rule table. The policy type is then derived from * its rules JSON the same way JSON BRE policy keys are generated * (see JsonBREController::runJsonBreApproval): * - emi_multiplier_on_abb set -> banking surrogate (adjusted-abb) * - power_savings set -> power saving * - bureau_surrogate set -> bureau surrogate * * NOTE: the amounts returned here are static placeholders for now. The actual amount * calculation logic for each policy type will be implemented later. * * @param int|string $ruleId rule_id of the policy * @param int $applicationId application id (for upcoming calculation logic) * @return float */ public function calculateEligibleAmount($ruleId, $applicationId) { $application = ConsumerApplication::with('emi', 'application_element', 'offer')->where('id', $applicationId)->first(); $rule = ApprovalRule::find($ruleId); if (!$rule) { return 0; } $ruleConfig = json_decode($rule->rules, true) ?: []; if (!empty($ruleConfig['emi_multiplier_on_abb'])) { // Banking surrogate (adjusted-abb) policy $eligibleAmount = $this->bankingSurrogateEligibleAmount($application); } elseif (isset($ruleConfig['power_savings'])) { // Power saving policy $eligibleAmount = $this->powerSavingEligibleAmount($application); } elseif (isset($ruleConfig['bureau_surrogate'])) { // Bureau surrogate policy $eligibleAmount = $this->bureauSurrogateEligibleAmount($application); } else { // Regular policy $eligibleAmount = $this->regularPolicyEligibleAmount($application); } // add insurance to the eligible amount and then calculate the net loan amount $emiController = new EMICalculation(new Request()); if($emiController->isLoanAmountAddedInLoanAmount($applicationId)){ $proforma = DB::selectOne("select insurance_premium_amount from pro_forma_invoice where application_id = ?", [$applicationId]); $eligibleAmount = $eligibleAmount + (isset($proforma) && !empty($proforma->insurance_premium_amount) ? $proforma->insurance_premium_amount : 0); } $autoBre = new \App\Http\Controllers\Admin\autoBRE(); return $autoBre->calculateNetLoanAmount($eligibleAmount, $application->offer); } /** * Eligible amount for a regular policy. * * If the application's EMI calculation was approved on minimum tenure * (table_of_offers_logic = minimum_tenure), the applicant is eligible for the full * applied loan amount. Otherwise it falls back to the eligible_amount stored against * the application's current offer in the loan_decision_table (0 when not found). * * @param \App\ConsumerApplication $application * @return float */ protected function regularPolicyEligibleAmount($application) { $emiCalculation = ApplicationEMICalculation::where('application_id', $application->id) ->orderByDesc('id') ->first(['table_of_offers_logic']); if ($emiCalculation && $emiCalculation->table_of_offers_logic === 'minimum_tenure') { return $application->loan_amount; } // Fallback: eligible amount stored for the current offer in the decision table. $decision = DecisionTable::where('application_id', $application->id) ->where('offer_id', $application->offer_id) ->whereNull('deleted_at') ->orderByDesc('created_at') ->first(['eligible_amount']); return $decision->eligible_amount ?? 0; } /** * Eligible amount for a power saving policy. * * Looks at the previously stored POWER-SAVING decisions in loan_decision_approved_amount * for this application and picks the calculated_approved_amount closest to (but not * exceeding) the application's applied loan amount. If no such record exists, it falls * back to calculating the power saving amount. * * @param \App\ConsumerApplication $application * @return float */ protected function powerSavingEligibleAmount($application) { $closestRecord = LoanDecisionApprovedAmount::where('application_id', $application->id) ->where('policy_type', 'POWER-SAVING') ->where('calculated_approved_amount', '<=', $application->loan_amount) ->whereNull('deleted_at') ->orderByDesc('calculated_approved_amount') ->first(['calculated_approved_amount', 'emi_amount']); if (!is_null($closestRecord)) { if (isset($closestRecord->emi_amount) && $closestRecord->emi_amount <= $application->emi->emi) { return $closestRecord->calculated_approved_amount; } else { return $application->loan_amount; } } // No stored decision found - calculate the power saving amount. return $this->calculatePowerSavingAmount($application); } public function bankingSurrogateEligibleAmount($application) { $closestRecord = LoanDecisionApprovedAmount::where('application_id', $application->id) ->where('policy_type', 'BANKING-ABB') ->where('calculated_approved_amount', '<=', $application->loan_amount) ->whereNull('deleted_at') ->orderByDesc('calculated_approved_amount') ->first(['calculated_approved_amount', 'emi_amount']); if (!is_null($closestRecord)) { if (isset($closestRecord->emi_amount) && $closestRecord->emi_amount <= $application->emi->emi) { return $closestRecord->calculated_approved_amount; } else { if ($application->emi->table_of_offers_logic === 'banking_abb') { return $application->loan_amount; } return 0; } } return $this->calculateBankingSurrogateAmount($application); } /** * Eligible amount for a bureau surrogate policy. * * Looks up the application's current offer in the loan_decision_table and returns the * bureau_surrogate_limit_amount stored against that offer. Returns 0 when no matching * decision row (or limit) is found. * * @param \App\ConsumerApplication $application * @return float */ protected function bureauSurrogateEligibleAmount($application) { $decision = DecisionTable::where('application_id', $application->id) ->where('offer_id', $application->offer_id) ->whereNull('deleted_at') ->orderByDesc('bureau_surrogate_limit_amount') ->first(['bureau_surrogate_limit_amount']); return $decision->bureau_surrogate_limit_amount ?? 0; } /** * Calculate the power saving eligible amount when no stored decision is available. * * The custom EMI is derived using the same logic as the power saving auto-decision * flow (GradeController::fetchCustomEMIAmount). That EMI is then run through the EMI * calculation to back-calculate the eligible loan amount. * * @param \App\ConsumerApplication $application * @return float */ protected function calculatePowerSavingAmount($application) { $customEMIAmount = (new GradeController(new Request()))->fetchCustomEMIAmount($application); // A 0 custom EMI means the existing EMI already qualifies - eligible for the full applied amount. if (is_numeric($customEMIAmount) && $customEMIAmount == 0) { return $application->loan_amount; } if (empty($customEMIAmount)) { return 0; } return $this->getEligibleLoanAmountForEMI($application, $customEMIAmount); } /** * Back-calculate the eligible loan amount for a given EMI using the EMI calculation. * * The application's own offer (offer_id) is passed as merchant_offer_id so the eligible * amount is computed against the applied offer. * * @param \App\ConsumerApplication $application * @param float $emiAmount * @return float */ protected function getEligibleLoanAmountForEMI($application, $emiAmount) { $request = new Request(); $request->attributes->add([ 'merchant_id' => $application->merchant_id, 'category_id' => $application->purpose, 'emi_amount' => $emiAmount, 'application_id' => $application->id, 'merchant_offer_id' => $application->offer_id, ]); $emiDetails = (new EMICalculation($request))->prepareCalculation()->getData(); return $emiDetails->loan_amount ?? 0; } protected function calculateBankingSurrogateAmount($application) { $customEMIAmount = (new GradeController(new Request()))->fetchCustomEMIAmountForBanking($application); // A 0 custom EMI means the existing EMI already qualifies - eligible for the full applied amount. if (is_numeric($customEMIAmount) && $customEMIAmount == 0) { return $application->loan_amount; } if (empty($customEMIAmount)) { return 0; } return $this->getEligibleLoanAmountForEMI($application, $customEMIAmount); } }