offer_id, e.g. "1234_b", "5678_r" * @param int $applicationId * @return array */ public function reviseOffer(Request $request, $applicationId) { // 2. Application must be in an editable state $appState = DB::table('consumer_application') ->where('id', $applicationId) ->value('state'); $validStates = ['waiting for decision', 'pre approved', 'cf approved', 'approved']; if (!in_array($appState, $validStates, true)) { return $this->errorResponse( '201004', 'CF300', "Auto update cannot be done because status is {$appState} and offer cannot be updated." ); } // 4. Snapshot current ("previous") offer state $appPrevState = DB::table('consumer_application') ->where('id', $applicationId) ->select('offer_id as prev_offer_id', 'loan_amount as prev_loan_amount', 'approved_amount as prev_approved_amount') ->first(); $prevCalcState = DB::table('emi_calculation') ->where('application_id', $applicationId) ->select('table_of_offers_logic as prev_logic') ->first(); $prevOfferDetails = DB::table('merchant_offers') ->where('id', $appPrevState->prev_offer_id ?? null) ->select('slug as prev_slug', 'interest_rate as prev_roi', 'advance_emi as prev_adv') ->first(); // 5. Decode incoming offer_id suffix -> tof_type / logic_type $requestOfferId = (string) $request->offer_id; if ($this->endsWith($requestOfferId, '_b')) { $tofType = 'bureau_surrogate_limit'; $logicType = 'bureau_surrogate_limit'; } elseif ($this->endsWith($requestOfferId, '_r')) { $tofType = 'residual_emi'; $logicType = 'residual_emi'; } elseif ($this->endsWith($requestOfferId, '_a')) { $tofType = 'BANKING-ABB'; $logicType = 'residual_emi'; } elseif ($this->endsWith($requestOfferId, '_p')) { $tofType = 'POWER-SAVING'; $logicType = 'residual_emi'; } else { $tofType = 'minimum_tenure'; $logicType = 'minimum_tenure'; } // 6. Strip suffix to get the real merchant_offers.id (mirrors // SUBSTRING_INDEX(offer_id, '_', 1)) $rawOfferId = (int) explode('_', $requestOfferId)[0]; // 7. Resolve eligible amount/EMI for the new offer. // priority 1: loan_decision_approved_amount (policy-type specific) // priority 2: loan_decision_table (standard tof types only) $decisionRow = DB::table('loan_decision_approved_amount') ->where('application_id', $applicationId) ->where('offer_id', $rawOfferId) ->where('policy_type', $tofType) ->whereNull('deleted_at') ->select('calculated_approved_amount as final_loan_amount', 'emi_amount as final_emi') ->first(); if (!$decisionRow && in_array($tofType, ['minimum_tenure', 'residual_emi', 'bureau_surrogate_limit'], true)) { $decisionTable = DB::table('loan_decision_table') ->where('application_id', $applicationId) ->where('offer_id', $rawOfferId) ->whereNull('deleted_at') ->first(); if ($decisionTable) { $decisionRow = (object) [ 'final_loan_amount' => $tofType === 'bureau_surrogate_limit' ? $decisionTable->bureau_surrogate_limit_amount : $decisionTable->eligible_amount, 'final_emi' => $tofType === 'bureau_surrogate_limit' ? $decisionTable->bureau_surrogate_limit_emi : $decisionTable->emi, ]; } } if (!$decisionRow) { return $this->errorResponse( '201006', 'CF300', 'Selected offer is invalid or not applicable for this application' ); } // 8. Reject duplicate hits (same offer id AND same approved amount) $isDuplicate = ((int) $rawOfferId === (int) $appPrevState->prev_offer_id) && ((float) $decisionRow->final_loan_amount === (float) $appPrevState->prev_approved_amount); if ($isDuplicate) { return $this->errorResponse( '201007', 'CF300', 'Duplicate hit. This offer is already applied to the application.' ); } // 9. New offer details, needed for the audit comment $newOfferDetails = DB::table('merchant_offers') ->where('id', $rawOfferId) ->select('slug as new_slug', 'interest_rate as new_roi', 'advance_emi as new_adv') ->first(); // 10. Build the audit-trail comment (mirrors the CONCAT step) $comment = $this->buildComment($prevCalcState, $tofType, $appPrevState, $prevOfferDetails, $decisionRow, $newOfferDetails); // Payload for the document-revision job (webhook notification omitted) $reviseJobPayload = json_encode([ 'list' => [ 'cam' => '1', 'proforma' => '1', 'sanction' => '1', 'loanagreement' => '0', 'emi_schedule' => '1', 'payment_links' => '1', ], 'auth_user_id' => 107, 'app_id' => (string) $applicationId, 'source' => 'update-table-of-offers', ]); // --- Below this line: logical completion, not in the source JSON --- // 11/12. Persist the new offer + log comment + queue the revise job DB::transaction(function () use ($applicationId, $rawOfferId, $decisionRow, $logicType, $comment, $reviseJobPayload) { DB::table('consumer_application') ->where('id', $applicationId) ->update([ 'offer_id' => $rawOfferId, 'approved_amount' => $decisionRow->final_loan_amount, 'updated_at' => Carbon::now(), ]); DB::table('emi_calculation') ->where('application_id', $applicationId) ->update([ 'table_of_offers_logic' => $logicType, 'updated_at' => Carbon::now(), ]); DB::table('comments')->insert([ 'application_id' => $applicationId, 'comment' => $comment, 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), 'user_id' => 107, ]); DB::table('jobs')->insert([ 'application_id' => $applicationId, 'from' => 'reviseDocsViaMconnect', 'payload' => $reviseJobPayload, 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), 'available_at' => Carbon::now(), ]); }); return [ 'return_code' => '200000', 'CF_code' => 'CF100', 'message' => 'Offer updated successfully.', ]; } /** * Builds the HTML audit comment, mirroring the source CONCAT logic. */ private function buildComment($prevCalcState, $tofType, $appPrevState, $prevOfferDetails, $decisionRow, $newOfferDetails) { $comment = 'Offer changed/updated by system via mconnect flow.

'; if (($prevCalcState->prev_logic ?? null) !== $tofType) { $comment .= "Offer type changed from {$prevCalcState->prev_logic} to {$tofType}.

"; } $comment .= 'Applied Amount - Rs. ' . $appPrevState->prev_loan_amount . ' Applied Tenure - ' . $prevOfferDetails->prev_slug . ', ' . $prevOfferDetails->prev_roi . '% interest, ' . $prevOfferDetails->prev_adv . ' Advance EMIs
' . 'Eligible Amount - Rs. ' . $decisionRow->final_loan_amount . ' Eligible Tenure - ' . $newOfferDetails->new_slug . ', ' . $newOfferDetails->new_roi . '% interest, ' . $newOfferDetails->new_adv . ' Advance EMIs

' . 'Please find below the list of documents required to process your original loan request:
' . 'Please submit 12 months bank statement & Income proof to check for Eligibility on applied amount'; return $comment; } private function endsWith($haystack, $needle) { return substr($haystack, -strlen($needle)) === $needle; } private function errorResponse($returnCode, $cfCode, $message) { return [ 'return_code' => $returnCode, 'CF_code' => $cfCode, 'message' => $message, ]; } /** * Move all documents from the temp S3 bucket (folders: ) * to the main S3 bucket (folder: ). * * For each unique ID in $uniqueIds, every file stored at * s3_temp:/// * is fetched and written to: * s3:/// * * @param int|string $id The application ID (destination folder name) * @param array|string $uniqueIds A single unique ID or an array of unique IDs * (source folder names on s3_temp) * @return array Summary of migrated / failed files keyed by unique ID */ public function migrateS3Documents($id, $uniqueIds) { $cwFactory = new CloudWatchLoggerFactory(); try { $s3Temp = Storage::disk('s3_temp'); $s3Main = Storage::disk('s3'); $destFolder = ltrim((string) $id, '/'); // Normalise: accept both a single ID and an array of IDs $uniqueIdList = is_array($uniqueIds) ? $uniqueIds : [$uniqueIds]; $results = []; foreach ($uniqueIdList as $uniqueId) { $sourceFolder = ltrim((string) $uniqueId, '/'); $migrated = []; $failed = []; // List every file inside the unique_id folder on the temp bucket $allItems = $s3Temp->listContents($sourceFolder, true); foreach ($allItems as $item) { // Skip directory entries if (isset($item['type']) && $item['type'] === 'dir') { continue; } $sourcePath = $item['path'] ?? ($item['key'] ?? null); if (!$sourcePath) { $cwFactory->logger(json_encode([ 'title' => 'whatsapp s3 source file empty', 'application_id' => $id ?? '', 'unique_id' => $uniqueId, 'file_item' => $item ])); continue; } try { // Extract bare file name by stripping the unique_id prefix // so the destination is: application_id/file_name $fileName = ltrim(substr($sourcePath, strlen($sourceFolder)), '/'); $destPath = $destFolder . '/' . $fileName; $fileContents = $s3Temp->get($sourcePath); $s3Main->put($destPath, $fileContents); // Insert a new record in consumer_application_files // pointing to the migrated destination path // ConsumerApplicationFiles::create([ // 'consumer_application_id' => $id, // 'file_path' => $destPath, // 'file_type' => pathinfo($fileName, PATHINFO_FILENAME), // ]); $file_id = $this->createConsumerApplicationFile($id, $destPath, pathinfo($fileName, PATHINFO_FILENAME)); if($file_id){ // Remove from temp bucket after a successful copy $s3Temp->delete($sourcePath); }else{ $cwFactory->logger(json_encode([ 'title' => 'whatsapp s3 file createion error', 'application_id' => $id ?? '', 'unique_id' => $uniqueId, 'source' => $sourcePath ])); } $migrated[] = ['source' => $sourcePath, 'dest' => $destPath]; } catch (\Exception $fileEx) { $failed[] = ['source' => $sourcePath, 'error' => $fileEx->getMessage()]; $cwFactory->logger(json_encode([ 'title' => 'whatsapp s3 migration file error', 'application_id' => $id ?? '', 'unique_id' => $uniqueId, 'source' => $sourcePath, 'message' => $fileEx->getMessage(), 'line' => $fileEx->getLine(), ])); } } $results[$uniqueId] = ['migrated' => $migrated, 'failed' => $failed]; } return true; } catch (\Exception $e) { $cwFactory->logger(json_encode([ 'title' => 'whatsapp s3 migration error', 'application_id' => $id ?? '', 'message' => $e->getMessage(), 'line' => $e->getLine(), 'trace' => $e->getTraceAsString(), ])); } } public function createConsumerApplicationFile($id, $file_path, $file_type){ $consumer_application_file = new ConsumerApplicationFiles(); $consumer_application_file->consumer_application_id = $id; $consumer_application_file->file_type = $file_type; $consumer_application_file->bank_name = ""; $consumer_application_file->password = ""; $consumer_application_file->accounttype = ""; $consumer_application_file->statementtype = ""; $consumer_application_file->statemenfilettype = ""; $consumer_application_file->customer_id = ""; $consumer_application_file->provider = ""; $consumer_application_file->discom_district = ""; $consumer_application_file->discom_mobile = ""; $consumer_application_file->doc_ref_id = ""; $consumer_application_file->finbit_analysis_date = ""; $consumer_application_file->bp_number = ""; $consumer_application_file->accountUID = ""; $consumer_application_file->file_path = $file_path; $consumer_application_file->created_by = 107; $consumer_application_file->image_score = ""; $consumer_application_file->m_generated = 0; $consumer_application_file->applicant_type = 1; $consumer_application_file->bureau_report_type = ""; $consumer_application_file->bureau_report_score = ""; $consumer_application_file->is_masked = 0; $consumer_application_file->save(); return $consumer_application_file->id; } public function autoRejectJSONBRE(Request $request) { try { $id = $request->input('application_id'); $applicationDetails = ConsumerApplication::with('merchant.grade_v1')->where('id', $id)->first(); $grade = $applicationDetails->merchant->grade_v1; $source = $request->input('source', 'bot'); $tracking_id = $request->input('tracking_id', ''); $account_aggregator_id = $request->input('account_aggregator_id', ''); $autoRejection = new \App\Http\Controllers\Grade\AutoRejection($request); $result = $autoRejection->autoRejectJSONBRE($grade, $id, $source, $tracking_id, $account_aggregator_id); if ($result) { $consumer_application = ConsumerApplication::find($id); $consumer_application->state = "rejected"; $consumer_application->save(); StatusLogs::insert([ 'application_id' => $id, 'user_id' => 107, 'status' => 'rejected', 'created_at' => Carbon::now()->toDateTimeString() ]); return response()->json(['status' => 'success', 'message' => 'Application auto rejected successfully']); } else { return response()->json(['status' => 'failure', 'message' => 'Auto rejection failed or not applicable']); } } catch (\Exception $e) { $cwFactory->logger(json_encode(array('title' => 'auto rejection json bre', 'application_id' => $id ?? '', 'message' => $e->getMessage(), 'line' => $e->getLine(), 'trace' => $e->getTraceAsString()))); return response()->json(['status' => 'success', 'message' => 'Application auto rejected successfully']); } } public function autoApprovalJSONBRE(Request $request) { try{ $cwFactory = new CloudWatchLoggerFactory(); $id = $request->input('application_id'); $request->merge(['source' => 'whatsapp_agent']); $consumerUpdateData = []; if ($request->filled('loan_amount')) $consumerUpdateData['loan_amount'] = $request->input('loan_amount'); if ($request->filled('tenure')) $consumerUpdateData['tenure'] = $request->input('tenure'); if ($request->filled('profession')) $consumerUpdateData['employee_status'] = $request->input('profession'); if ($request->filled('company_name')) $consumerUpdateData['company_name'] = $request->input('company_name'); if ($request->filled('permanent_address')) $consumerUpdateData['permanent_address_line_1'] = $request->input('permanent_address'); if ($request->filled('permanent_pincode')) $consumerUpdateData['permanent_address_pincode'] = $request->input('permanent_pincode'); if ($request->filled('permanent_city')) $consumerUpdateData['permanent_address_city'] = $request->input('permanent_city'); if ($request->filled('permanent_state')) $consumerUpdateData['permanent_address_state'] = $request->input('permanent_state'); if ($request->filled('permanent_address_status')) $consumerUpdateData['permanent_address_status'] = $request->input('permanent_address_status'); if (!empty($consumerUpdateData)) { ConsumerApplication::where('id', $id)->update($consumerUpdateData); } $elementUpdateData = []; if ($request->filled('gross_project_amount')) $elementUpdateData['inverter_gross_project_amount'] = $request->input('gross_project_amount'); if ($request->filled('product_name')) $elementUpdateData['product_name'] = $request->input('product_name'); if ($request->filled('solar_finance_for')) $elementUpdateData['type_of_customer'] = $request->input('solar_finance_for'); if ($request->filled('solar_power_type')) $elementUpdateData['solar_power_type'] = $request->input('solar_power_type'); if ($request->filled('kw_installation')) $elementUpdateData['inverter_kw_installation'] = $request->input('kw_installation'); if (!empty($elementUpdateData)) { ConsumerApplicationElement::where('application_id', $id)->update($elementUpdateData); } $application = ConsumerApplication::with('category', 'merchant', 'emi', 'creditReport', 'approval','application_element', 'activeOffer')->where('id', $id)->first(); if (!$application->approved_amount && $application->loan_amount){ ConsumerApplication::where('id', $id)->update(['approved_amount' => $application->loan_amount]); $application->approved_amount = $application->loan_amount; } $consumerApplicationController = new ConsumerApplicationController(); $gradeController = new GradeController($request); $gradeController->generateEMIForDecision($consumerApplicationController, $application, $request, $id); $ltv = $gradeController->calculateLTV($application, $application->offer); $ltv_column_value = [ 'ltv_gross_project_amount' => $ltv['ltv_gross_project_amount'] ?? 0, 'ltv_net_loan_amount' => $ltv['ltv_net_loan_amount'] ?? 0, ]; ConsumerApplicationAdditional::where('application_id', $id)->update(['loan_to_value_limit' => json_encode($ltv_column_value)]); $testImport = new TestImport(new Request()); $req_id = $testImport->random_strings(8); $tableOfOffers = true; $gradeController = new \App\Http\Controllers\Grade\GradeController($request); $result = $gradeController->MultiApprovalCheckJSONBRE($id, $req_id, $tableOfOffers); if ($result) { $consumerAppController = new \App\Http\Controllers\Admin\ConsumerApplicationController(); $offers = $consumerAppController->getTableOfOffersMConnectPayload($id); if ($consumerAppController->isMultiLender($application->merchant_id) > 1) { $status = 'cf approved'; } else { $status = 'approved'; } \request()->merge(['state' => $status, 'source' => 'whatsapp_agent']); $consumerApplicationController->updateStatus($id, true); return response()->json(['status' => 'approved', 'offers' => $offers]); } else { return response()->json(['status' => 'manual_review', 'offers' => []]); } } catch (\Exception $e) { $cwFactory->logger(json_encode(array('title' => 'auto approval json bre', 'application_id' => $id ?? '', 'message' => $e->getMessage(), 'line' => $e->getLine(), 'trace' => $e->getTraceAsString()))); return response()->json(['status' => 'manual_review', 'offers' => []]); } } public function saveOffer(Request $request) { $cwFactory = new CloudWatchLoggerFactory(); try { $applicationId = $request->input('application_id'); $result = $this->reviseOffer($request, $applicationId); return response()->json($result); } catch (\Exception $e) { $cwFactory->logger(json_encode(array('title' => 'save offer json bre', 'application_id' => $applicationId ?? '', 'message' => $e->getMessage(), 'line' => $e->getLine(), 'trace' => $e->getTraceAsString()))); } } }