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 = s3ListContents($s3Temp, $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())));
}
}
}