# BRE Flow This document describes the pre-BRE and BRE execution flow for the Laravel admin application. It starts where application/consent updates become eligible for AutoDecision, follows the queue and bureau gates, then documents the rejection and approval JSON BRE paths, BRE API payload construction, response logging, and post-BRE status handling. The broader AutoDecision overview belongs in `documentation/auto_decision_flow.md`. This file is intentionally more implementation-focused around the pre-BRE and BRE engine flow. Table of Offers is only covered as a handoff point at the end. ## 1. Scope And Boundaries Covered here: - AutoDecision trigger eligibility before the BRE can run. - Queue creation and queue consumer execution. - `command:AutoDecision` setup and top-level guards. - Consent and bureau generation gates. - Primary applicant and co-applicant split. - Auto-rejection JSON BRE. - Approval JSON BRE. - JSON BRE request payload shape and policy construction. - BRE API invocation. - BRE logs and approval-status logs. - Post-BRE application status handling. Not expanded here: - Full Table of Offers internals. - Full EMI calculation internals. - Full scorecard calculation internals. - Business meaning of every credit rule. This document records how the code selects and sends rules to BRE. Where the code exposes a reachable path but does not prove whether that path is active in production, this document states that explicitly instead of assuming. ## 2. High-Level Flow The normal primary-applicant path is: 1. A controller receives a consent/application update. 2. Pre-queue eligibility gates are evaluated. 3. If eligible, `EventAutoDecision` is dispatched with application ID and applicant type. 4. `ListenerAutoDecision` writes a `queue_log` row with `cmd = command:AutoDecision`. 5. `RQConsumerListener` polls `queue_log` and sends the row to `RabbitMQ::callMeBack()`. 6. `RabbitMQ::callMeBack()` resolves the job handler and calls `CmdHelper::runCommand()`. 7. `CmdHelper::runCommand()` calls Artisan command `command:AutoDecision`. 8. `AutoDecision::handle()` validates merchant/BRE eligibility and enriches the request context. 9. `generateSoftBureauReport()` validates consent and ensures bureau data exists. 10. For primary applicant, `runAutoDecision()` calls scorecard refresh and then `runAutoDecisionAlgorithm()`. 11. `GradeController::autoApproveInit()` runs pre-approval checks and auto-rejection first. 12. If rejection does not happen, `ApprovalCheckV1()` routes into `MultiApprovalCheck()`. 13. When `JSON_BRE_ACTIVE` is enabled, `MultiApprovalCheck()` immediately calls `MultiApprovalCheckJSONBRE()`. 14. `JsonBREController::runJsonBreApproval()` builds the JSON BRE payload and calls the BRE API. 15. `JsonBREController::store_jsonbre_response()` stores raw BRE logs and approval status rows. 16. `MultiApprovalCheckJSONBRE()` interprets policy results. 17. `AutoDecision::handle()` updates final application state to approved / cf approved / waiting for decision / waiting for documents according to the response and SSE branch logic. The co-applicant path is different: 1. `AutoDecision::handle()` calls `generateSoftBureauReport($id, 'autoBRE', 2, false)`. 2. Bureau/consent is validated for co-applicant. 3. If bureau succeeds, `GradeController::autoApproveInitCoapplicant()` runs co-applicant rejection BRE. 4. The command returns after co-applicant processing and does not run primary approval BRE. ## 3. Trigger Sources Before BRE The main trigger code is in: - `app/Http/Controllers/ConsentController.php` - `app/Http/Controllers/Admin/ConsumerApplicationController.php` - `app/Http/Controllers/ApiController.php` - `app/Http/Controllers/MerchantApiController.php` - `app/Http/Controllers/Admin/autoBRE.php` The actively verified trigger blocks are: - Consent submission flow in `ConsentController`. - Admin application save/update flow in `ConsumerApplicationController`. - A reachable event dispatch in `ApiController`. Production usage for `ApiController` and `MerchantApiController` trigger paths is not established from code alone in this document. They should be treated as reachable code paths unless route/runtime usage is confirmed separately. ## 4. Pre-Queue Eligibility Gates Before AutoDecision is queued, the trigger code evaluates several gates. The admin application update flow and consent flow use the same broad pattern. ### 4.1 Application State Gate The trigger path checks that the application state is in the allowed set used by the relevant code block: - `in-process` - `incomplete form` - `form completed` - `waiting for documents` If the application is not in the allowed state set, the queue event is not dispatched from that trigger block. ### 4.2 Source Gate The trigger path checks `consumer_application.updated_by` against allowed source values used in the code: - `admin` - `MERCHANT API` - `consumer_app` - `merchant admin panel` - `Whatsapp` If `updated_by` is outside the allowed set in that trigger block, AutoDecision is not queued from that path. ### 4.3 Net Loan Amount Gate The trigger path calculates net loan amount through: `app/Http/Controllers/Admin/autoBRE.php::calculateNetLoanAmount($loanAmount, $offer)` That helper: - creates an `EMICalculation`, - sets loan amount and net loan amount from `$loanAmount`, - sets advance EMI, advance type, and tenure from the offer, - calls `netLoanCalculation(true)`, - returns the calculated net loan amount. The trigger code compares that calculated value to `500000`. If it is greater than `500000`, this no-touch/AutoDecision queue path is not entered. The code does not contain an inline business label for this threshold. In this document it is described only as the hard-coded pre-queue net-loan eligibility threshold. ### 4.4 Solar/Application Element Gate The trigger code checks application element fields: - `type_of_customer` must be present. - `solar_power_type` must be present. - `type_of_customer == 1`. - `solar_power_type == 1`. The code also logs: - application ID, - calculated net loan amount, - `type_of_customer`, - `solar_power_type`. This document does not assign a business name beyond the coded gate. If the business label is required, confirm it separately. ### 4.5 Request Data Gate In the admin save/update trigger, the code requires at least one of these request fields to be non-empty before calling `checkDataPoints()`: - `employee_status` - `monthly_income` - `power_savings` If all are empty, the trigger block does not continue to queue AutoDecision. ### 4.6 Required Data Points Gate The required field validation is implemented in: `app/Http/Controllers/Admin/autoBRE.php::checkDataPoints($id, $request)` It checks two groups of fields. From `consumer_application`: - `name` - `gender` - `pan_card` - `loan_amount` - `dob` - `permanent_address_line_1` - `permanent_address_city` - `permanent_address_state` - `permanent_address_pincode` - `current_address_line_1` - `current_address_city` - `current_address_state` - `current_address_pincode` - `mobile_number` - `monthly_income` - `merchant_id` - `offer_id` - `employee_status` From `consumer_application_element`: - `inverter_KW_installation` - `inverter_power_savings` For each field read from the database, the method treats the field as missing/invalid when: - it is `null`, - it is an empty string, - it contains only non-alphanumeric characters. If missing fields exist and the current request does not fill them, the method logs the null fields to CloudWatch and returns `false`. For each missing field that the request does include, it validates the request value again. If the request value is empty or only non-alphanumeric characters, it logs the invalid field and returns `false`. Only when all required database/request field checks pass does `checkDataPoints()` return `true`. ### 4.7 Merchant BRE Eligibility Gate The trigger code loads merchant data through a query joining `consumer_application` and `merchant`. The merchant must have: - a merchant row, - `grade_id`, - at least one enabled flag among: - `is_auto_approve` - `is_auto_doc` - `pre_approval` If these are not true, the event is not dispatched. ### 4.8 Applicant Type Selection At Trigger Time In the admin update flow, applicant types are collected from consent flags: - if `consumer_application_element.is_consent == 1`, applicant type `1` is added; - if `consumer_application_element.is_co_app_consent == 1`, applicant type `2` is added. The trigger then loops through the selected applicant types and applies the AutoDecision queue/bypass logic per applicant type. The consent flow uses the submitted/current applicant type and applies the broad same gate pattern around that applicant type. ### 4.9 Waiting For Documents Bypass Gate If the application state is `waiting for documents`, the trigger code inspects: `consumer_application_element.mandatory_document_list` It splits the value by comma and compares it against consent-only document keys: - `consent_logs` - `co_app_consent_logs` If any pending document is not one of those two consent keys, `$triggerAutoDecision` is set to `false`. When `$triggerAutoDecision` is `false`: - full AutoDecision is not queued; - soft bureau is generated with `$runBre = false`; - for applicant type `1`, `AutoRejection::autoReject(false, $id, 'bot')` is executed; - if auto-rejection returns true, the surrounding controller sets `consumer_application.state = rejected`; - a `status_logs` row is inserted with user ID `107` and status `rejected`. The co-applicant branch in this bypass does not update the primary application state in the inspected code. ### 4.10 Duplicate Queue Prevention Before dispatching the event, the trigger checks `queue_log` through `QueueLog`: - `application_id = {application id}` - `cmd = command:AutoDecision` - `input LIKE %"applicant_type":{applicant_type}%` If no matching `job_id` exists, the event is dispatched: ```php event(new EventAutoDecision(['id' => $application->id, 'applicant_type' => (int) $applicant_type])); ``` If a matching job already exists, the event is not dispatched again for that application/applicant type. ## 5. Queue Event And Queue Log The event class is: `app/Events/V1/EventAutoDecision.php` It stores the incoming payload in public property `$param`. The listener is: `app/Listeners/V1/ListenerAutoDecision.php` `ListenerAutoDecision::handle()` builds: ```php $request_data = [ 'cmd' => 'command:AutoDecision', 'con' => 'jobs', 'param' => $event->param, ]; ``` It also sets: ```php $request_data['job_handler'] = Config::get('constants.rabbitmq.es_handler') . '@' . Config::get('constants.rabbitmq.method_run_cmd'); ``` `constants.rabbitmq.method_run_cmd` is `runCommand` in `config/constants.php`. The listener writes a `queue_log` row through `QueueLog::create()` with: - `exchange` - `route_key` - `job_handler` - JSON `input` `QueueLog::create()` also parses and stores derived fields: - `application_id` from `input.param.id` or `input.param.application_id`, - `cmd` from `input.cmd`. The model table is: `queue_log` The model primary key is: `job_id` ## 6. Queue Consumer Execution The queue consumer command is: `app/Console/Commands/RQConsumerListener.php` `RQConsumerListener::handle()` does two modes: - if an ID is passed, it loads that single `queue_log` job; - otherwise, it loads up to 20 jobs where: - `status != com`, - `input NOT LIKE %TableOfOffer%`, - `attempt_count = 0`, - `job_status IS NULL`, - newest first. For each selected job: - it updates `queue_log.job_status = 1`; - wraps the job as `$bodyObj->body = $job`; - calls `RabbitMQ::callMeBack($bodyObj, $this, $automation)`. `RabbitMQ::callMeBack()`: - decodes message body data; - if the data has `input`, `CmdHelper::runCommand()` later decodes it; - resolves `job_handler` either from data or from `queue_log.job_handler`; - splits the handler string into class and method; - calls the handler method; - calls `QueueLog::markCompleted()` after handler output is returned. For AutoDecision, the handler is: `App\Helpers\CmdHelper@runCommand` `CmdHelper::runCommand()`: - decodes `input` when present; - reads `cmd`; - for `command:AutoDecision`, builds Artisan payload: - `--con` from `con`; - `--param` from `param`; - calls `Artisan::call($cmd, $payload)`. Therefore, the durable execution chain is: `EventAutoDecision` -> `ListenerAutoDecision` -> `queue_log` -> `RQConsumerListener` -> `RabbitMQ::callMeBack()` -> `CmdHelper::runCommand()` -> `Artisan::call('command:AutoDecision')`. ## 7. AutoDecision Command Guards The command class is: `app/Console/Commands/AutoDecision.php` The command signature is: ```php command:AutoDecision {--con=} {--param=*} ``` The command handles only: `--con=jobs` If `--con` is missing, it returns failure with message `Context is missing`. If `--con` has an unsupported value, it returns failure with message `No case found to handle in ExecuteJob`. For `--con=jobs`, it reads: - `id` from `--param`; - `applicant_type` from `--param`. `applicant_type` is normalized: - if it is `1` or `2`, it is kept; - otherwise it defaults to `1`. If `id` is missing, the command returns failure with message `Missing parameter ExecuteJob`. The command loads: ```php ConsumerApplication::with('merchant') ->where('id', $id) ->first(['id','merchant_id', 'state', 'co_applicant_name', 'updated_by']); ``` It also reads `electricity_name_change` from `consumer_application_element`. The command continues only when: - application has merchant, - merchant has at least one of: - `is_auto_approve` - `is_auto_doc` - `pre_approval` - merchant has `grade_id`. If this merchant/application guard fails, the inspected command body does not run bureau/BRE. ## 8. AutoDecision Context Enrichment Inside `AutoDecision::handle()`, before bureau/BRE: ### 8.1 Co-Applicant Flag The command adds: ```php $param['co_applicant'] = !empty($application->co_applicant_name); ``` This is later used by the SSE status branch. ### 8.2 Electricity Validation Flag The command checks if any row exists in `utility_validation` for the application. If a row exists, it sets: ```php $param['electricity_bill_validation'] = 'Success'; ``` ### 8.3 Request Batch ID The command generates an 8-character random string and merges into the global request: ```php request()->merge([ 'request_ap_id' => $requestID, 'log_decision' => true, ]); ``` `request_ap_id` becomes the request batch ID used by approval logging if it reaches `MultiApprovalCheckJSONBRE()`. ### 8.4 runBRE Flag The command starts with: ```php $runBRE = true; ``` It checks whether an `electricity_bill` file exists in `consumer_application_files`. It sets `$runBRE = false` only when all these are true: - `$param['electricity_bill_validation'] == 'Fail'`; - no electricity bill is uploaded; - `updated_by == 'MERCHANT API'`. In the inspected code, the command itself sets electricity validation to `Success` when a utility validation row exists. The `Fail` value can only affect this branch if it is already present in `$param` from the queued payload or another caller. ## 9. MERCHANT API LTV Bootstrap For applications with: ```php $application->updated_by == 'MERCHANT API' ``` the command performs LTV bootstrap before bureau/BRE. It loads the application with `offer`. If both application and offer exist: - creates `GradeController(new Request())`; - calls `GradeController::calculateLTV($ltvApplication, $ltvApplication->offer)`; - creates or updates `ConsumerApplicationAdditional` for the application; - saves `loan_to_value_limit` with: - `ltv_gross_project_amount` - `ltv_net_loan_amount` - adds a comment through `GradeController::addComment()`; - inserts a `ContentLogger` row for model `consumer_application` with old/new LTV values. `GradeController::addComment()` writes into the `comments` table through the `Comments` model. This LTV work happens before `generateSoftBureauReport()`, so later approval BRE payload construction can use the updated application/additional state. ## 10. Applicant Type Split In AutoDecision ### 10.1 Co-Applicant Path If normalized `applicant_type == 2`, the command calls: ```php $applicationController->generateSoftBureauReport($id, 'autoBRE', 2, false); ``` Key points: - `runBre` is explicitly `false`. - If bureau response status is not `OK`, the command returns failure using the bureau response message. - If bureau response status is `OK`, the command returns immediately. - The primary applicant approval BRE is not run in this command execution. Inside `generateSoftBureauReport()`, applicant type `2` can call: ```php (new GradeController(new Request()))->autoApproveInitCoapplicant($id); ``` That method runs co-applicant rejection JSON BRE if a co-applicant credit report exists. ### 10.2 Primary Applicant Path For applicant type `1`, the command first forces: ```php Input::merge(['applicant_type' => 1]); ``` Then it calls: ```php $applicationController->generateSoftBureauReport($id, 'autoBRE', 1, $runBRE); ``` The returned JSON includes: - `status` - `message` - `is_approved` - `is_rejected` - `is_pre_approved` If `status == OK` and both `is_rejected` and `is_pre_approved` are false, the command continues into post-BRE status resolution. If the response is not OK or indicates rejected/pre-approved, the command returns failure with the response message in the inspected branch. ## 11. Soft Bureau And Consent Gate The method is: `app/Http/Controllers/Admin/ConsumerApplicationController.php::generateSoftBureauReport($id, $type = 'customer', $applicantType = 1, $runBre = true)` ### 11.1 Applicant Type Source The method resolves applicant type from: - `Input::get('applicant_type')` if present; - otherwise the `$applicantType` argument. ### 11.2 Consent Type And Flag For applicant type `1`: - file type: `consent_logs` - element flag: `is_consent` For applicant type `2`: - file type: `co_app_consent_logs` - element flag: `is_co_app_consent` The method looks for a `ConsumerApplicationFiles` row matching: - `consumer_application_id = $id` - `file_type = $consent_type` - `applicant_type = $applicant_type` It also loads the matching consent flag from `ConsumerApplicationElement`. Consent is considered received when either: - a matching consent file row exists; or - the matching element flag is `1`. This means the flow can continue when MConnect has set the flag before the consent PDF job finishes. If consent is missing: - applicant type `1` returns `FAIL` with message `Please get applicant consent.`; - applicant type `2` returns `FAIL` with message `Please get Co-Applicant consent.`; - bureau and BRE do not run. ### 11.3 Existing Credit Report Gate The method checks `credit_report` for: - same application ID, - same applicant type, - `deleted_at IS NULL`. If a row exists, it does not call the soft bureau API and sets: - `status = OK` - `message = Bureau Fetched Successfully` If no row exists, it builds a bureau payload and calls the soft bureau API. ### 11.4 Primary Applicant Bureau Payload For applicant type `1`, it requires application name and mobile number. The payload includes: - `application_id` - `applicant_type` - `request_type` - `pan_card` - `first_name` - `last_name` - `mobile_number` - `document_type = PAN_DETAILED` - `vendor = admin` - `skip_pan_validation` If application name or mobile number is missing, it returns: `Either application name or mobile number is missing` ### 11.5 Co-Applicant Bureau Payload For applicant type `2`, it requires co-applicant name and co-applicant mobile number. The payload includes: - `application_id` - `applicant_type` - `pan_card` - `request_type` - `first_name` - `last_name` - `mobile_number` - `document_type = PAN_DETAILED` - `vendor = admin` If co-applicant name or co-applicant mobile number is missing, it returns: `Either co application name or co applicant mobile number is missing` ### 11.6 Soft Bureau API The soft pull call posts JSON to: `EXPERIAN_SOFT_PULL_HOST` It sends headers: - `Content-Type: application/json` - `X-Client-Id: EXPERIAN_SOFT_PULL_ID` - `X-Client-Secret: EXPERIAN_SOFT_PULL_SECRET` It captures: - raw response, - decoded JSON response, - HTTP code, - curl error. It logs the bureau payload and response to Slack. Failure handling sets `status = FAIL` when: - response is empty, - decoded response is invalid/null, - response contains `error`, - curl error exists, - API returns `status = FAIL`, - HTTP code is not `200`. ### 11.7 Equifax/Hard Pull Fallback If soft bureau status is `FAIL`, the method can try `generateEqifaxReport($id)` when `$this->isEquifaxEnabled` is true. Before doing that, it merges the current `applicant_type` into input. If Equifax returns a failure message containing `consent missing`, the message is rewritten to: `Soft bureau failed system is trying to do hard pull for that Consent is required` ### 11.8 Handoff After Bureau Success The method proceeds only when: - `status == OK`, and - message is `Bureau Fetched Successfully`, or Equifax/Experian fallback generated the report successfully. Then: - if `log_decision` is present and true and `$type != 'autoBRE'`, it updates current status to `waiting for decision`; - if applicant type is `2`, it calls `GradeController::autoApproveInitCoapplicant($id)`; - if `$runBre` is true and applicant type is `1`, it calls `runAutoDecision($id, $type)`. It returns: ```php [ 'status' => $status, 'message' => $message, 'is_approved' => $isApproved, 'is_rejected' => $isRejected, 'is_pre_approved' => $isPreApproved, ] ``` ## 12. runAutoDecision Wrapper The method is: `ConsumerApplicationController::runAutoDecision($id, $type = null, $account_aggregator_id = null)` It creates a new request and adds: ```php application_id = $id ``` For banking decision webhook: ```php if ($type === 'banking_decision_webhook') { skip_auto_reject = true; rule_filter = emi_multiplier_on_abb; } ``` If `$account_aggregator_id` is passed, it is added to request attributes. Then: - `getScoreCard($request)` runs; - `runAutoDecisionAlgorithm($request, $id)` runs; - response flags are derived from the returned grade status. `getScoreCard()`: - sets `ConsumerApplication::$isCFFoirNotInclude = true`; - creates `ScoreCard($request)`; - calls `ScoreCard::updateLoanStatus($application_id)`; - loads application with credit report, FOIR, merchant offer, application element, and EMI; - validates the request/application but does not return the error in this flow; - sets the application on the scorecard; - calls `ScoreCard::getScore()`. This document treats `getScoreCard()` as a pre-BRE scorecard/loan-status refresh. It does not document the full scorecard calculation internals. If the application is approved and `$type != 'autoBRE'`, `runAutoDecision()` updates the application status immediately to: - `approved`, or - `cf approved` when `isMultiLender($merchant_id) > 1`. For the normal `autoBRE` command path, status is handled later by `AutoDecision::handle()`. ## 13. runAutoDecisionAlgorithm The method is: `ConsumerApplicationController::runAutoDecisionAlgorithm(Request $request, $id)` It loads the application with: - `application_element` - `proFormaInvoice` The method only continues when application state is not: - `aborted` - `rejected` If state is `aborted` or `rejected`, the inspected method does not run the grade controller flow. When allowed: - it creates `GradeController($request)`; - initializes `$gradeStatus = false`; - checks if current application state maps to `$this->statusreversearray['approved']`. If the state is in the approved-status group: - it logs `Auto Decision: autoApproveInit` to Slack; - resolves `$isLog` from request `log_decision`; - calls `GradeController::autoApproveInit($id, $isLog)`; - if the result is an HTTP response, it decodes the JSON body. Then: - if `rejected_case` is present, it returns the grade status; - if `auto_approved` is present, it returns the grade status. If not approved and state maps to pre-approved and `pre_approvalCheck()` passes: - `initAutoDocumentation()` runs; - pre-approval communications are sent; - application state is set to `pre approved`; - a `status_logs` row is inserted with user ID `107`; - response `pre_approved = true` is returned. Otherwise: - if the application has an element and no `auto_document_rule_id`, `initAutoDocumentation()` runs; - the method returns `false`. The method has an empty catch block in the inspected code, so exceptions inside it are swallowed. ## 14. autoApproveInit Pre-BRE Work The method is: `GradeController::autoApproveInit($id, $isLog = true)` It first checks that a primary applicant credit report exists: ```sql SELECT id FROM credit_report WHERE application_id = $id AND applicant_type = 1 ``` If no credit report exists, it logs `Credit Report not found` and returns `false`. When the credit report exists: - it loads application with category, merchant, EMI, credit report, approval, application element, and active offer; - creates `ConsumerApplicationController`; - if `approved_amount` is empty and `loan_amount` exists, it updates approved amount to loan amount; - if EMI does not exist, it calls `generateEMIForDecision()`; - if credit report relation is missing, it attempts `generateSoftBureauReport($id)`. Then it checks request attribute: ```php skip_auto_reject ``` If `skip_auto_reject` is not true: - it calls `AutoRejection::autoReject(false, $id)`; - logs the auto-reject result; - if auto-reject returns true: - sets application state to `rejected`; - inserts `status_logs` with user ID `107`; - returns response `rejected_case = true`. If auto-rejection does not reject: - Slack logs Auto Decision V1 start; - `ApprovalCheckV1($id, $req_id)` runs; - Slack logs Auto Decision V1 end. If `ApprovalCheckV1()` returns truthy `$grade`, `autoApproveInit()` returns: ```php response(['auto_approved' => false], 200) ``` Despite the key name, the value set in this method is `false`. If no grade result is returned and communication is enabled, it calls `initAutoDocumentation()`. ## 15. ApprovalCheckV1 And JSON BRE Branch The method is: `GradeController::ApprovalCheckV1($application_id, $req_id = '')` It: - checks whether `EPFUANLookup` exists for the application; - calls `CallEPFKarza($application_id)` when no EPF record exists; - loads application with EMI, application element, credit report, FOIR, merchant, merchant grade, and merchant offer; - calls `CalculateFOIRbeforeAutoDecision($application_id, $application)`; - immediately returns `MultiApprovalCheck($application_id, $req_id)`. The JSON BRE branch is in: `GradeController::MultiApprovalCheck($application_id, $req_id = '', $tableOfOffers = false)` At the top of the method: ```php if (_env('JSON_BRE_ACTIVE')) { return $this->MultiApprovalCheckJSONBRE($application_id, '', true); } ``` Therefore, when `JSON_BRE_ACTIVE` is enabled: - legacy approval logic inside `MultiApprovalCheck()` is bypassed; - the passed `$req_id` is not forwarded; - `MultiApprovalCheckJSONBRE()` receives empty request ID and `tableOfOffers = true`. Inside `MultiApprovalCheckJSONBRE()`, the request ID is then resolved from: - explicit argument if non-empty, - request value `request_ap_id`, - generated random string if still empty/zero. Because `AutoDecision::handle()` merges `request_ap_id` before bureau/BRE, the queued AutoDecision path still carries the generated batch ID into JSON BRE. ## 16. Grade-Level Checks Before Approval BRE The method is: `GradeController::gradeChecks($application, $application_id, $tableOfOffers)` It returns: ```php [ 'status' => $status, 'failed_condition' => $failed_condition, 'reason' => $reason, ] ``` `MultiApprovalCheckJSONBRE()` calls this before fetching/running approval rules. If `status == failed` and `reason` is non-empty: - `saveApprovalStatus(0, $application_id, $req_id, $gradeChecks['status'], $gradeChecks)` runs; - `MultiApprovalCheckJSONBRE()` returns `false`; - approval BRE is not executed. The checks are: ### 16.1 Required Relations/Data Check Fails with reason: `Failed to fetch data from tables and relations` when any of these are missing: - application object; - EMI when not in Table of Offers mode; - application element; - credit report relation in the odd coded condition `(isset($application->creditReport[0]) && !$application->creditReport[0])`; - FOIR relation; - merchant offer relation; - products relation. ### 16.2 Bureau Score Check If no credit report row exists or no score exists, it fails with: - reason: `Bureau not pulled` - failed condition: `{"score":"no_equifax"}` ### 16.3 Merchant Grade Check If merchant is missing, it fails with: `Merchant not set` If merchant exists but has no `grade_v1`, it fails with: `Merchant grade not set` If merchant exists and has no `custom_grade_id`, the code can set: `Custom grade is not set properly` This last assignment occurs inside the same merchant/grade failure block and can overwrite the previous reason when the condition matches. ### 16.4 Merchant Active Status Check If merchant status is `inactive`, it fails with: `Merchant is not eligible for auto approval` ### 16.5 Merchant Auto-Approval Flag Check If merchant does not have `is_auto_approve`, it fails with: `Merchant does not have auto-approval enabled` ### 16.6 RBI Blacklist Check It calls: `ConsumerApplicationController::getBlacklistData($application)` If returned `status` is true, it fails with: - reason: `The RBI has blacklisted this applicant` - failed condition: `{"black_list":true}` ### 16.7 Table Of Offers Event Trigger In Non-TOF Mode If `$tableOfOffers` is false and merchant has `table_of_offers`, it dispatches: ```php event(new EventTableOfOffer(['id' => $application_id])); ``` In the JSON BRE path called by `MultiApprovalCheck()` when `JSON_BRE_ACTIVE` is true, `$tableOfOffers` is passed as true, so this branch is not used in that entry. ### 16.8 Merchant Max Approval Limits It calculates existing approved exposure for the merchant from `consumer_application` rows in states: - `approved` - `disbursement checklist done` - `disbursement query` - `ready for disbursement` - `disbursed` - `nach registered` - `closed` It computes: ```php $total_remaining_amount = $application->merchant->max_approval_value - $total->total_approved_amount; ``` It fails when: - current loan amount is greater than total remaining amount; - total approved amount is greater than merchant max approval value; - application count is greater than or equal to merchant max number of cases. Failure reasons and failed conditions: - `Application loan amount greater than total remaining amount`, with `total_remaining_amount`; - `Total approved amount greater than max approval limit`, with `max_approval_value`; - `Total application count limit reached`, with `max_no_cases`. ### 16.9 Product Amount Check If `application_element.product_name` is non-empty and products exist: - it splits product IDs by comma; - loads `Products` by those IDs; - sums product `amount`; - if sum is non-zero and less than application `loan_amount`, it fails with: - reason: `Loan amount is greater than the product amount`; - failed condition containing `product_loan_amount`, calculated as application loan amount minus summed product amount. ## 17. Auto-Rejection JSON BRE Auto-rejection happens before approval BRE unless skipped by request attribute `skip_auto_reject`. The entry method is: `AutoRejection::autoReject($grade, $id, $source = 'bot', $tracking_id = '', $account_aggregator_id = '')` If `JSON_BRE_ACTIVE` is enabled, this delegates to: `AutoRejection::autoRejectJSONBRE(...)` ### 17.1 Primary Applicant Rejection Gates `autoRejectJSONBRE()` initializes: - reject rules array; - fail logs; - approval checks; - credit score and total past due from `credit_report`; - application data. It checks if a system rejection status log already exists: - `application_id = $id` - `status = rejected` - `user_id = 107` If it exists, the method returns `false`. It computes: - score, defaulting to `-2` if missing; - total past due, defaulting to `0`; - age as created year minus DOB year; - DPD count from FOIR records older than two years, counting non-normal DPD keys and flagging when count exceeds `6`. It calls: `GradeController::getFOIRByID($application->id)` If score, monthly income, or age is missing and source is not `consumer`, it logs `Basic Check` and returns `false`. If merchant exists but has no `grade_v1`, older hard-coded rejection checks can still reject based on score/past dues/DPD/age. ### 17.2 Primary Applicant JSON Rejection Rule Fetch For JSON rejection, it fetches: ```sql SELECT modifications FROM auto_reject_rule WHERE grade_id = ? AND applicant_type = 1 ORDER BY created_at DESC ``` If no `modifications` exist, it returns `false`. If modifications exist: - generates a random `req_id`; - calls `JsonBREController::runRejection($id, $req_id, $autoRejectRule, false, $tracking_id, 1, $account_aggregator_id)`. If the response is not a `JsonResponse`, it returns `false`. It decodes the response `res`, checks `policy-rejection`, and if response top-level `status` is false: - iterates failed rules; - ignores failed rules whose reason is `Value not found in s3`; - formats each failed rule through `formatRejectionLogs()`; - stores reject rule names in `$this->rejectRules`; - stores formatted failure logs in `$failLogs`. If reject rules are not empty: - sends auto-reject communication; - calls `autoRejectLog($id, json_encode($failLogs))`; - returns `true`. ### 17.3 Co-Applicant JSON Rejection The co-applicant method is: `AutoRejection::autoRejectCoApplicantJSONBRE($grade, $id, $tracking_id = '')` It fetches: ```sql SELECT modifications FROM auto_reject_rule WHERE grade_id = ? AND applicant_type = 2 ORDER BY created_at DESC ``` If modifications exist: - generates a random `req_id`; - calls `JsonBREController::runRejection($id, $req_id, $autoRejectRule, false, $tracking_id, 2)`. The co-applicant BRE response can contain multiple policies. The code processes all keys that start with: `policy-rejection` When a policy has `policy_status == false`, it: - derives a role key from the policy name; - groups fail logs under that role; - formats failed rules; - stores unique reject rules; - merges duplicate rule logs. If reject rules exist: - calls `autoRejectLog($id, json_encode($failLogs), 2)`; - returns `true`. The inspected co-applicant path logs rejection but does not update primary application state by itself. ## 18. Rejection BRE Payload Construction The method is: `JsonBREController::runRejection($application_id, $req_id, $auto_reject_rule, $json_bre_for_log = false, $pirimid_tracking_id = '', $applicant_type = 1, $account_aggregator_id = '')` It starts with: ```php $bre_api_payload = $this->createBrePayload($application_id); $bre_api_payload['applicant_type'] = $applicant_type; ``` `createBrePayload()` returns: ```php [ 'identifier' => $id, 'grade' => [ 'rules' => null, ], ] ``` Then rejection flow: - reads exhaustive rule JSON from S3 path `JSON_BRE_EXHAUSTIVE_JSON`; - loads `credit_report` for the application and applicant type; - decodes `auto_reject_rule.modifications`. For applicant type `2`, it may create these policies: - `policy-rejection-income`, when `co_applicant_for_income` exists; - `policy-rejection-signing`, when `co_applicant_for_signing` exists; - fallback `policy-rejection`, when neither role-specific group exists. For applicant type `1`, it creates: `policy-rejection` Each rejection policy payload includes: - `identifier` - `applicant_type` - `rule_name = auto-rejection-rule` - `age_range` - `monthly_income` - `credit_score_range` - `bureau_type` - `dpd_rejection_rules` - `keyword_rejection_rules` - `account_status_rejection_rules` - `principal_write_off_rules` - `total_past_dues_rules` If a Pirimid tracking ID is present or found from latest `pirimid_bank_analysis`, it sets: - top-level `pirimid_tracking_id`; - policy payload `nach_bounce_rule`. If no Pirimid tracking ID exists and `account_aggregator_id` is passed, it sets top-level: `account_aggregator_id` Then it calls: `create_rejection_rule_list($application_id, (object)$reject_rules, $exhaustive_criteria_json, $applicant_type, $pirimid_tracking_id)` That maps configured rejection keys into exhaustive JSON BRE rule definitions. ## 19. Rejection Rule List Mapping The method is: `JsonBREController::create_rejection_rule_list(...)` For each configured `new_rejection_rules` key: - `MAX_SCORE` / `MIN_SCORE` map to: - `rejection_score` for applicant type `1`; - `co_applicant_rejection_score` for applicant type `2`. - `MAX_AGE` / `MIN_AGE` map to: - `rejection_age` for applicant type `1`; - `co_applicant_rejection_age` for applicant type `2`. - `NEGATIVE_AREA` maps to: - `negative_area` for applicant type `1`; - `co_applicant_negative_area` for applicant type `2`; - only if the configured reject rule is truthy. - `APPROVED_PINCODE` maps to: - `is_pincode_approved` for applicant type `1`; - `co_applicant_is_pincode_approved` for applicant type `2`; - only if the configured reject rule is truthy. - `MONTHLY_INCOME` maps to `co_applicant_rejection_monthly_income` only for applicant type `2`. - Other keys map to `rejection_` plus lowercase key. Special skip: - `rejection_nach_bounce` is skipped when no tracking/NACH context is present. For each mapped rule: - the exhaustive rule definition is appended; - if that definition has a fallback rule, the fallback exhaustive rule definition is appended. Missing rule/exceptions are added to `bre_rules_ignored` and logged to Slack as skipped rules. ## 20. Approval BRE Setup The main approval JSON BRE method is: `GradeController::MultiApprovalCheckJSONBRE($application_id, $req_id = '', $tableOfOffers = false)` It: - creates `CloudWatchLoggerFactory`; - creates `JsonBREController`; - logs `Auto Decision: MultiApprovalCheck`; - resolves request batch ID; - stores application ID on `$this->appId`; - loads application relations; - recalculates `$tableOfOffers` from `merchant.table_of_offers`; - runs `gradeChecks()`; - applies custom grade override when request has `custom_grade_id`; - loads active approval rules; - calls `JsonBREController::runJsonBreApproval()`. Application relations loaded: - `emi` - `application_element` - `creditReport` - `foir` - `merchant` - `merchant.grade_v1` - `merchant.custom_grade` - `merchant.offer` - `products` - `bank_details` Active approval rules are: ```php ApprovalRule::where('grade_id', $application->merchant->grade_v1->id) ->where('status', 1) ->get(); ``` The JSON BRE call is: ```php $jsonBRE->runJsonBreApproval( $application->id, $req_id, $application->merchant->grade_v1->id, $tableOfOffers, $json_bre_for_log, $this->request ); ``` If request has `json_bre_for_log = 1`, the method returns the raw JSON BRE response after the BRE call and does not continue into response interpretation. ## 21. Approval BRE Payload Base The method is: `JsonBREController::runJsonBreApproval($application_id, $req_id, $gradeID, $tableOfOffers, $json_bre_for_log = false, $request = null)` It selects active approval rules: ```sql SELECT id, rule_name, rules FROM approval_rule WHERE grade_id = ? AND status = 1 ``` If request attribute `rule_filter` exists, it adds: ```sql AND rules LIKE ? ``` This is used by banking decision webhook to run only rules containing `emi_multiplier_on_abb`. It creates the base payload: ```php $bre_api_payload = [ 'identifier' => $application_id, 'grade' => [ 'rules' => null, ], ]; $bre_api_payload['applicant_type'] = 1; ``` It loads application with: - `emi` - `application_element` - `creditReport` It calculates: ```php $netLoanAmount = $application->approved_amount - $application->emi->upfront_from_customer; ``` It reads latest Pirimid bank analysis: - `id` - `tracking_id` - `response` If `tracking_id` column exists, it uses that. Otherwise it decodes `response` and reads `trackingId`. If a tracking ID exists, it sets top-level: `pirimid_tracking_id` If no tracking ID exists and request has attribute `account_aggregator_id`, it sets top-level: `account_aggregator_id` Then it reads exhaustive criteria JSON from S3: `JSON_BRE_EXHAUSTIVE_JSON` ## 22. Approval Policy Naming For each active approval rule, `runJsonBreApproval()` decodes `approval_rule.rules`. Policy name is selected as follows: ### 22.1 ABB / Banking Policy If rule JSON has non-empty, non-zero `emi_multiplier_on_abb`: - if no Pirimid tracking ID and no account aggregator ID exists, the policy is skipped; - otherwise: - `adjustedABBMultiplier` is set to the configured multiplier; - policy name becomes `policy-adjusted-abb-{approval_rule_id}`. ### 22.2 Power Savings Policy If rule JSON has `power_savings`, policy name becomes: `policy-power-savings-{approval_rule_id}` ### 22.3 Standard Policy All other approval rules use: `policy-{approval_rule_id}` ## 23. Approval Rule List Construction The method is: `JsonBREController::create_rule_list($application_id, $grade_rule, $req_id, $tableOfOffers, $exhaustive_criteria_json)` It: - decodes `approval_rule.rules`; - casts it to array; - removes `score` when `thin_file` exists; - sorts keys; - loads application employee status; - loads `electricity_name_change`. For each configured rule key: - it gets the corresponding exhaustive JSON rule definition by key; - it avoids duplicate rule definitions; - it can skip rules based on runtime context; - it adds fallback rule definitions when configured. Skip rules: - `age_salaried` is skipped unless `employee_status == SALARIED`. - `age_self_employed` is skipped unless employee status is: - `Self Employed - Business` - `Self Employed - Professional` - `foir` and `cf_foir` are skipped when Table of Offers mode is active. - `e_bill_fuzzy_match` is skipped when `electricity_name_change == 1`. Fallback behavior: - if the exhaustive criteria entry has `fallback.rule`, that fallback rule is also added. After the loop, it always appends: `loan_to_value_limit` If any rule mapping fails, it adds the rule key to `bre_rules_ignored` and sends a Slack log titled `Skipped a rule`. ## 24. Approval Policy Payload Fields Each approval policy receives: ```php $policy['rule'] = $rule_list; $policy['payload'] = [...] ``` The payload fields are: | Field | Source | | --- | --- | | `identifier` | application ID | | `applicant_type` | hard-coded `1` in approval flow | | `rule_name` | `approval_rule.rule_name` | | `loan_amount` | `consumer_application.approved_amount` | | `employee_status` | approval rule JSON | | `past_dues` | approval rule JSON | | `credit_card_percentage` | approval rule JSON key `credit_card_amount_paid` | | `age` | approval rule JSON | | `ageAtLastEMI` | approval rule JSON | | `monthly_income` | approval rule JSON | | `score` | approval rule JSON unless affected by thin-file logic | | `case_type` | approval rule JSON key `caseType` | | `dpd_years_above_0` | approval rule JSON key `dpd_years_above_zero` | | `dpd_count_above_0` | approval rule JSON key `dpd_count_above_zero` | | `pre_dpd_years` | approval rule JSON | | `pre_dpd_count` | approval rule JSON | | `pre_dpd_keywords` | approval rule JSON | | `post_dpd_year` | approval rule JSON | | `post_dpd_count` | approval rule JSON | | `post_dpd_keywords` | approval rule JSON | | `term_loan_amount_paid_percentage` | approval rule JSON key `term_loan_amount_paid` | | `power_savings` | `consumer_application_element.inverter_power_savings` | | `emi` | `consumer_application.emi.emi` | | `zero_downpayment` | hard-coded `0` | | `bank_validation` | approval rule JSON | | `solar_financing_for` | approval rule JSON | | `solar_power_type` | approval rule JSON | | `kw_capping` | approval rule JSON, default `false` | | `bureau_type` | first credit report bureau | | `keyword_rules` | approval rule JSON | | `account_status_rules` | approval rule JSON | | `dpd_rules` | approval rule JSON | | `past_dues_rules` | approval rule JSON | | `principal_written_off_rules` | approval rule JSON | | `bureau_surrogate` | approval rule JSON | | `age_self_employed` | approval rule JSON cast to integer array | | `age_salaried` | approval rule JSON cast to integer array | | `max_net_tenure` | approval rule JSON | | `min_net_tenure` | approval rule JSON | | `max_net_loan_amount` | approval rule JSON | | `min_net_loan_amount` | approval rule JSON | | `track_record` | approval rule JSON | | `e_bill_fuzzy_match` | approval rule JSON cast to integer, default `0` | | `net_loan_amount` | calculated approved amount minus upfront from customer | | `declared_monthly_income` | application monthly income, only when speculative transaction cap rule exists | | `applicant_name` | application name, only when name match threshold exists | | `min_banking_months` | approval rule JSON | | `name_match_threshold_pct` | approval rule JSON | | `emi_bounce_rule` | approval rule JSON key `emi_bounce_rule.max_emi_bounces` | | `emi_bounce_lookback_months` | approval rule JSON key `emi_bounce_rule.emi_bounce_lookback_months`, default `3` | | `speculative_txn_cap_pct` | approval rule JSON | | `loan_to_value_limit` | `merchant.loan_to_value_limit` | When rule JSON has `thin_file`, the payload additionally sets: ```php thin_file_score = [-1, 10] ``` The method logs each payload addition attempt to CloudWatch with title: `BRE_API payload addition attempted` ## 25. Optional Rejection Policy Inside Approval Payload If `$json_bre_for_log` is true, `runJsonBreApproval()` also appends a rejection policy to the approval BRE payload. It fetches latest `auto_reject_rule.modifications` by grade ID and builds: `policy-rejection` with payload fields: - `identifier` - `applicant_type = 1` - `rule_name = auto-rejection-rule` - `age_range` - `monthly_income` - `credit_score_range` - `bureau_type` - `dpd_rejection_rules` - `keyword_rejection_rules` - `account_status_rejection_rules` - `principal_write_off_rules` - `total_past_dues_rules` - `nach_bounce_rule` This is for JSON BRE logging mode and is not the normal approval decision path when `$json_bre_for_log` is false. ## 26. BRE API Execution Both approval and rejection flows call: `JsonBREController::runAutoDecisionBRE($payload)` It: - initializes curl; - posts to `BRE_API_UAT_URL`; - sends content type `application/json`; - sends `json_encode($payload)` as body; - sets timeout to `120` seconds; - decodes the response into `$data`; - captures HTTP code and curl error; - closes curl. It returns a Laravel JSON response: ```php [ 'req' => $payload, 'res' => $res, 'code' => $httpcode, ] ``` The inspected method does not branch on non-200 HTTP status and does not return curl error in the wrapper. Callers receive the response wrapper and must interpret `res`/`code`. ## 27. BRE Response Storage The storage method is: `JsonBREController::store_jsonbre_response($response, $req_id, $rejection = false, $reasonDetails = false)` ### 27.1 Default Failure Reason Mode If `$reasonDetails` is passed: - it logs `Default check failed`; - creates `JsonBreApprovalStatusLog`; - sets `rule_id = 0`; - stores application ID, reason, request batch ID, status `0`, and timestamp; - returns. This mode is used for grade/default checks rather than normal BRE API response storage. ### 27.2 Raw BRE Log For normal BRE API responses: - it reads `$response->getData()`; - decodes `res`; - reads request payload from `req`; - logs title `Storing BRE Logs` with request/response/rules ignored. It creates a `BreLog` row: - `application_id` - `applicant_type` - `request` - `response` - `rule_ignored` - `created_at` - `request_batch_id` The model is `App\BreLog`. The table is represented by that model. ### 27.3 Rejection Storage Stops After Raw Log If `$rejection` is true, the method returns immediately after writing `BreLog`. Therefore rejection BRE writes raw request/response to `bre_logs`, but it does not write per-policy rows to `jsonbre_approval_status_log` through this method. ### 27.4 Approval Status Rows For approval responses: - it iterates every decoded response key except `status`; - extracts rule ID from the numeric suffix of the policy key; - creates `JsonBreApprovalStatusLog`. Stored fields: - `rule_id` - `application_id` - `failed_condition` - `reason` - `request_batch_id` - `status` - `created_at` `status` is cast from `policy_status`. This table is later used by `AutoDecision::getApprovedRules()`. ## 28. Approval BRE Response Interpretation After `runJsonBreApproval()` returns, `MultiApprovalCheckJSONBRE()`: - gets response data; - decodes `res`; - initializes result flags: - `$allFailed = true` - `$allSuccess = true` - `$success = false` It calculates solar/TOF guards: - `$is_sub_category_allowed` from `application_element.type_of_customer` against `SOLAR_SUB_CATEGORIES`; - `$is_solar_on_grid` from `application_element.solar_power_type == 1`; - `$is_solar_case` from `application.purpose == SOLAR_CATEGORY_ID`. If JSON BRE result exists, it separates policies into: - power-saving policies: key contains `power-savings`; - banking policies: key contains `adjusted-abb`; - other policies: excludes `power-savings`, `adjusted-abb`, and `status`. ### 28.1 Standard Policy Passes For other policies, it selects policies where: - policy object exists; - `policy_status` is true; - policy key matches `policy-{id}`. If any standard policies pass: - `$this->policyPassed` is set; - `$allFailed = false`; - `$success = true`; - `$filteredGradeRules` contains approval rules matching passed IDs. If none pass: - `$allSuccess = false`. It extracts bureau surrogate limits from passed policies through: `extractBureauSurrogateLimits($passedPolicies)` ### 28.2 Power-Saving Policies If power-saving policies exist: - the method loops through them; - the first policy with `policy_status = true` is mapped back to its approval rule ID; - the matching rule is stored in `$this->powerSavingPolicyPassed`; - `$allFailed = false`; - `$success = true`; - loop breaks. If a power-saving policy fails, `$allSuccess = false`. ### 28.3 Banking Policies If banking policies exist: - the method loops through them; - each policy with `policy_status = true` is mapped back to approval rule ID; - matching rules are stored in `$this->bankingPolicyPassed`; - `$allFailed = false`; - `$success = true`. If a banking policy fails, `$allSuccess = false`. ### 28.4 Return Without TOF If TOF does not run, the method returns: ```php return !$allFailed; ``` So at least one successful policy means approval result true. ## 29. Post-BRE Status Handling In AutoDecision After primary applicant `generateSoftBureauReport()` returns to `AutoDecision::handle()`: The command continues post-BRE status handling only when: - bureau response status is `OK`; - response is not rejected; - response is not pre-approved. Then it: - creates a new `ConsumerApplicationController`; - calls `updateProforma($id)`; - calls `$this->getApprovedRules($id)`. `getApprovedRules()` checks: - `jsonbre_approval_status_log` when `JSON_BRE_ACTIVE` is enabled; - legacy `approval_status_log` otherwise. For JSON BRE, it checks whether any row exists for: - the application; - the latest request batch ID for that application; - `status = 1`. ### 29.1 Approved Response If: - response has `is_approved = true`; and - `getApprovedRules($id)->status` is truthy; then standard status is: `approved` If the merchant is multi-lender: `cf approved` The multi-lender check is: `$applicationController->isMultiLender($application->merchant_id) > 1` For SSE merchant, status may be replaced by `sseSetStatus()`. ### 29.2 Not Approved Response If response has `is_approved = false`, standard status becomes: `waiting for decision` For SSE merchant, status may be replaced by `sseSetStatus()`. ### 29.3 Persisting Status If calculated `$status` differs from current application state: - it merges request values: - `state = $status` - `autoDecisionQueue = true` - calls: ```php $consumerApplicationController->updateStatus($id, true); ``` ## 30. SSE Merchant Special Status Branch The method is: `AutoDecision::sseSetStatus($id, $bre_status, $applicationController, $param, $electricity_bill_upload)` It is used only when: - merchant ID equals `SSE_MERCHANT_ID`; and - `electricity_name_change` is not set/truthy. The method starts with: - default status: `waiting for documents`; - default reason: `Co-applicant needed for Ebill Match`; - webhook name: `Add-Co-applicant`. It reads latest electricity bill validation report/electricity bill file where: - file type is `electricity_bill_validation_report` or `electricity_bill`; - `ebill_name_verify` is not null. ### 30.1 Electricity Validation Failed With Uploaded Bill If electricity validation is `Fail` and electricity bill is uploaded: - status becomes `waiting for decision`; - comment depends on BRE status: - approved: `BRE approved. Electricity bill document verification is pending.` - not approved: `BRE not approved. Electricity bill document verification is pending.` ### 30.2 Electricity Validation Failed Without Uploaded Bill If electricity validation is `Fail` and no electricity bill is uploaded: - status remains default `waiting for documents`; - comment: `Electricity bill validation failed. Electricity bill document required.` ### 30.3 Electricity Validation Success And BRE Approved If validation is `Success` and BRE approved: - it decodes `ebill_name_verify`; - if applicant match is below `80`, it branches on co-applicant presence and co-applicant match. Without co-applicant: - applicant match `51` to `79`: status `waiting for decision`; comment asks team to check owner name; - applicant match `<= 50`: status remains `waiting for documents`; comment asks to add owner in co-applicant details; `Add-Co-applicant` webhook is fired. With co-applicant: - if co-applicant match `<= 50` and applicant match `< 50`, webhook is fired and comment asks to add co-applicant using e-bill name; - if co-applicant match `>= 80`, status becomes `approved` or `cf approved`; - otherwise status becomes `waiting for decision` with owner-name review comment. If applicant match is `>= 80`, status becomes `approved` or `cf approved`. ### 30.4 Electricity Validation Success And BRE Not Approved If validation is `Success` and BRE not approved: - applicant match `< 50` and no acceptable co-applicant match can trigger `Add-Co-applicant` webhook and comment requiring bank statement; - applicant match `< 80` creates manual validation/bank statement comments; - otherwise comment says bank statement is required and status becomes `waiting for decision`. ### 30.5 Mandatory Document Append If final status is `waiting for documents` and a comment exists: - if comment contains `Electricity bill validation failed`, it appends `electricity_bill`; - if comment contains `Bank Statement required`, it appends `bank_statement`. Append target: `consumer_application_element.mandatory_document_list` It avoids duplicating the document key when already present. ## 31. Data Writes And Logs ### 31.1 `queue_log` Written by: - `ListenerAutoDecision::handle()` through `QueueLog::create()`. Used by: - duplicate prevention in trigger paths; - queue polling in `RQConsumerListener`; - command completion marking in `QueueLog::markCompleted()`. Important fields: - `job_id` - `application_id` - `cmd` - `exchange` - `route_key` - `job_handler` - `input` - `status` - `attempt_count` - `job_status` - `output` ### 31.2 `credit_report` Used by: - `generateSoftBureauReport()` to avoid duplicate bureau pulls; - `autoApproveInit()` to require primary applicant bureau; - `autoApproveInitCoapplicant()` to require co-applicant bureau; - JSON BRE payload fields such as `bureau_type`; - grade checks requiring bureau score. ### 31.3 `bre_logs` Written by: - `JsonBREController::store_jsonbre_response()`. Stores: - raw BRE request; - raw decoded BRE response; - ignored/skipped rules; - applicant type; - request batch ID. ### 31.4 `jsonbre_approval_status_log` Written by: - `store_jsonbre_response()` for approval responses; - default failure logging paths when `reasonDetails` is passed. Used by: - `AutoDecision::getApprovedRules()` to determine whether latest BRE batch has any approved policy. ### 31.5 `auto_reject_logs` Written by: - `AutoRejection::autoRejectLog()`. Used for: - primary and co-applicant rejection outcomes and formatted failure logs. ### 31.6 `status_logs` Written explicitly when: - trigger bypass auto-rejection sets primary state to rejected; - `autoApproveInit()` rejects primary applicant; - pre-approval branch sets `pre approved`. Final status updates through `updateStatus()` may also create status log records depending on that method's internals, which are outside this BRE-focused document. ### 31.7 `consumer_application` Read/written for: - current state; - merchant ID; - source `updated_by`; - applicant/co-applicant identity data; - loan amount and approved amount; - selected offer; - final status transitions. ### 31.8 `consumer_application_element` Read/written for: - consent flags; - mandatory document list; - type of customer; - solar power type; - inverter KW installation; - inverter power savings; - product names; - electricity name change; - previous applied offer in TOF handoff. ### 31.9 `consumer_application_additional` Written in MERCHANT API LTV bootstrap before BRE: - `loan_to_value_limit.ltv_gross_project_amount` - `loan_to_value_limit.ltv_net_loan_amount` ### 31.10 `comments` Written by: - `GradeController::addComment()`. Used for: - MERCHANT API LTV bootstrap comments; - SSE status comments; - TOF offer-change comments when that flow applies. ### 31.11 `content_logger` Written by: - MERCHANT API LTV bootstrap. Stores: - old/new LTV values; - application ID; - status; - user ID `107`; - model `consumer_application`. ## 32. Early Return And Failure Cases ### 32.1 Pre-Queue The event is not dispatched when: - application state is outside allowed trigger states; - `updated_by` source is outside allowed source list; - net loan amount exceeds `500000`; - `type_of_customer` / `solar_power_type` gates fail; - required request fields are empty in the admin trigger; - `checkDataPoints()` fails; - merchant has no grade or no auto/pre-approval flags; - duplicate queue job exists for same application/applicant type. ### 32.2 Waiting For Documents Bypass When `mandatory_document_list` has non-consent documents: - full AutoDecision queue is skipped; - bureau may still be generated with `runBre = false`; - primary applicant rejection BRE can run directly; - state is set to `rejected` only if primary applicant auto-rejection returns true. ### 32.3 Queue/Command `AutoDecision` returns failure when: - `--con` is missing; - `--con` is unsupported; - application ID is missing; - co-applicant/primary bureau call returns non-OK response in the command branch. ### 32.4 Consent/Bureau `generateSoftBureauReport()` returns failure when: - applicant consent is missing; - co-applicant consent is missing; - applicant name/mobile is missing; - co-applicant name/mobile is missing; - soft bureau returns empty/invalid response; - API returns error/failure; - HTTP code is not `200`; - Equifax fallback fails or is unavailable after soft bureau failure. ### 32.5 Algorithm `runAutoDecisionAlgorithm()` does not run grade logic for: - `aborted` - `rejected` `autoApproveInit()` returns false when primary credit report is missing. ### 32.6 Auto-Rejection Approval BRE is skipped when: - auto-rejection returns true; - application state is set to `rejected`; - `rejected_case` response returns. Auto-rejection returns false without rejecting when: - a system rejected status log already exists; - basic score/income/age check fails for non-consumer source; - no applicable rejection rule modifications exist; - BRE response is not a `JsonResponse`; - no failed rejection rules are collected. ### 32.7 Grade Checks Approval BRE is skipped when `gradeChecks()` fails for: - missing required relations/data; - bureau not pulled; - merchant missing/grade missing/custom grade issue; - inactive merchant; - merchant auto approval disabled; - RBI blacklist; - merchant max approval exposure/count limits; - loan amount greater than product amount. ### 32.8 Approval BRE An approval policy can be skipped when: - it is an ABB rule and neither Pirimid tracking ID nor account aggregator ID exists. `runJsonBreApproval()` logs and exits catch blocks when: - payload construction fails; - BRE API execution throws. The inspected code catches these exceptions and logs CloudWatch entries; not all catch blocks rethrow. ### 32.9 Post-BRE If no approval policy passes: - `MultiApprovalCheckJSONBRE()` returns false unless TOF branch changes the return. If `getApprovedRules()` finds no approved status in latest batch: - `AutoDecision::handle()` does not take the approved-status branch even when response has `is_approved = true`. ## 33. Configuration And Environment Keys The flow uses: | Key | Usage | | --- | --- | | `JSON_BRE_ACTIVE` | switches auto-rejection and approval into JSON BRE paths | | `JSON_BRE_EXHAUSTIVE_JSON` | S3 path for exhaustive rule JSON | | `BRE_API_UAT_URL` | JSON BRE API URL used by `runAutoDecisionBRE()` | | `EXPERIAN_SOFT_PULL_HOST` | soft bureau endpoint | | `EXPERIAN_SOFT_PULL_ID` | soft bureau client ID header | | `EXPERIAN_SOFT_PULL_SECRET` | soft bureau client secret header | | `SKIP_PAN_VALIDATION` | primary applicant soft bureau payload flag | | `SSE_MERCHANT_ID` | enables SSE special status branch | | `SOLAR_CATEGORY_ID` | solar case detection in `MultiApprovalCheckJSONBRE()` | | `SOLAR_SUB_CATEGORIES` | allowed subcategory/type list for TOF branch | | `constants.rabbitmq.es_handler` | queue job handler class | | `constants.rabbitmq.method_run_cmd` | queue job handler method, `runCommand` | | `constants.rabbitmq.job_exch` | queue exchange written to `queue_log` | | `constants.rabbitmq.exch_rout_key_job` | queue route key written to `queue_log` | Do not document secret values in this file. ## 34. Table Of Offers Handoff Table of Offers begins after approval BRE response interpretation inside: `GradeController::MultiApprovalCheckJSONBRE()` The TOF branch is entered when: - merchant has `table_of_offers`; - the solar/category guards allow it: - not a solar case, or - solar case with allowed subcategory and on-grid solar power type. At that point: - passed approval rules are used to derive max tenure, FOIR, and CF-FOIR limits; - fallback values are used when no policy passes: - tenure `60` - FOIR `75` - CF-FOIR `50` - `setTOFAttributes()` prepares offer-level FOIR, CF-FOIR, tenure, EMI, payable, interest, processing fee, and upfront-from-customer data; - `saveDecisionInTable()` builds and persists decision rows and applies offer-selection logic; - `triggerTableOfOffersWebhook()` sends the `table-of-offers-webhook`. This document stops at that handoff. Detailed TOF behavior belongs in the TOF-specific documentation.