request = $request; $this->baseUrl = rtrim(_env('DIGIO_BASE_URL', 'https://api.digio.in'), '/'); } /** * Initiate (or re-use) a hypothecation signing request via Digio. * Returns the first active signer URL on success, false on failure. */ public function initiateHypothecation(array $config = []) { // Honour Vivriti-lender override if present if ($this->request->get('application') && $this->request->get('application')->lender_id == Vivriti::$lender_id) { $this->entity = Vivriti::$lender_id; } return $this->createSignRequest(); } /** * Re-send signing notifications to pending signers. */ public function resendNotification() { $loanHypothecationModel = LoanHypothecationModel::where('application_id', $this->request->get('id'))->whereNull('deleted_at') ->orderBy('created_at', 'desc') ->where('status', 'pending') ->first(); if (!$loanHypothecationModel) { return false; } $response = json_decode($loanHypothecationModel->response); $document_id = $loanHypothecationModel->documentId; // Digio resend: POST /v2/client/document/resend { id, identifier } // We iterate over each pending signer and trigger resend per identifier. $signers = $response->signers ?? []; foreach ($signers as $signer) { if (($signer->status ?? '') !== 'signed') { $payload = [ 'id' => $document_id, 'identifier' => $signer->identifier ?? ($signer->email ?? ''), ]; $this->makeRequest('POST', $this->baseUrl . self::ENDPOINT_RESEND, $payload); } } return true; } /** * Send (or re-send) signing-link emails to each pending signer. * * This is the Digio-aware counterpart of LoanHypothecation::triggerHypothecationCommunication(). * The critical difference: link extraction uses getHypothecationLink() which reads Digio's * response shape { signers: [ { identifier, signing_url } ] } instead of Leegality's * { data: { invitees: [ { phone, email, signUrl } ] } }. * * Return values mirror the Leegality version: * 0 – emails sent successfully * 1 – no LoanHypothecationModel record found (not yet initiated) * 2 – already fully signed * 3 – mail exception */ public function triggerHypothecationCommunication(int $id) { $apiController = new ApiController(); $isHypothecationActive = $apiController->canHypothecated($id); if (!$isHypothecationActive) { return 1; } $consumer_details = ConsumerApplication::with('merchant', 'category') ->where('id', $id) ->first(); $exceptional_ids = explode(',', _env('STAMP_SERIES_SPECIFIC_CASES')); if (isset($consumer_details->approved_amount) && $consumer_details->approved_amount <= 500000 && !in_array($this->request->get('id'), $exceptional_ids)) { $first_party_email = _env('LEEGALITY_CF_PARTY_EMAIL'); $first_party_phone = _env('LEEGALITY_CF_PARTY_PHONE'); } else { $first_party_email = _env('LEEGALITY_FIRST_PARTY_EMAIL'); $first_party_phone = _env('LEEGALITY_FIRST_PARTY_PHONE'); } $applicant_name = $consumer_details['name']; $co_applicant_name = $consumer_details['co_applicant_name']; $merchant_name = $consumer_details->merchant->merchant_name; $purpose_of_the_loan = $consumer_details->category->name; $applicant_email = (strlen($consumer_details->email) > 0) ? wall_decrypt($consumer_details->email) : ''; $co_applicant_email = (strlen($consumer_details->co_applicant_email) > 0) ? wall_decrypt($consumer_details->co_applicant_email) : ''; $subject = 'Signing of Loan Agreement'; $user = [ 'customer_name' => $applicant_name, 'co_applicant_name' => $co_applicant_name, 'merchant_name' => $merchant_name, 'purpose_of_the_loan' => $purpose_of_the_loan, ]; $loanHypothecationModel = LoanHypothecationModel::where('application_id', $id)->whereNull('deleted_at') ->orderBy('created_at', 'desc') ->first(); if (!isset($loanHypothecationModel->id)) { return 1; } if ($loanHypothecationModel->status === 'success') { return 2; } // Parse the stored Digio response – signers[].signing_url $data = json_decode($loanHypothecationModel->response); $applicant_link = $co_applicant_link = $first_party_link = ''; if (!$loanHypothecationModel->applicant_signing) { $applicant_link = (string)$this->getHypothecationLink( $data, $consumer_details->mobile_number, $consumer_details->email ); } elseif (!$loanHypothecationModel->co_applicant_signing) { $co_applicant_link = (string)$this->getHypothecationLink( $data, $consumer_details->co_applicant_mobile_number, $consumer_details->co_applicant_email ); } elseif (!$loanHypothecationModel->first_party_signing) { $first_party_link = (string)$this->getHypothecationLink( $data, $first_party_phone, $first_party_email ); } // ---- Send applicant email ------------------------------------------- if ($applicant_link !== '') { $user['esignlink'] = $applicant_link; $dataview['user'] = $user; $body = View::make('emails.consumer_esign_mail', $dataview)->render(); try { if ($applicant_email !== '') { Mail::send('emails.blank', ['body' => $body], function ($message) use ($applicant_email, $subject) { $message->to($applicant_email)->subject($subject); }); } } catch (\Exception $e) { (new Slack(new Request(['title' => 'Digio Email exception', 'fields' => [ 'Message' => $e->getMessage(), 'Line' => $e->getLine(), 'File' => $e->getFile(), 'Trace' => $e->getTraceAsString(), ]])))->send(); return 3; } } // ---- Send co-applicant email ---------------------------------------- if ($co_applicant_link !== '') { $user['esignlink'] = $co_applicant_link; $dataview['user'] = $user; $body = View::make('emails.consumer_esign_mail', $dataview)->render(); try { if ($co_applicant_email !== '') { Mail::send('emails.blank', ['body' => $body], function ($message) use ($co_applicant_email, $subject) { $message->to($co_applicant_email)->subject($subject); }); } } catch (\Exception $e) { (new Slack(new Request(['title' => 'Digio Email exception', 'fields' => [ 'Message' => $e->getMessage(), 'Line' => $e->getLine(), 'File' => $e->getFile(), 'Trace' => $e->getTraceAsString(), ]])))->send(); return 3; } } // ---- Send first-party (lender) email -------------------------------- if ($first_party_link !== '') { $user['esignlink'] = $first_party_link; $dataview['user'] = $user; $body = View::make('emails.consumer_esign_mail', $dataview)->render(); try { if ($first_party_email !== '') { Mail::send('emails.blank', ['body' => $body], function ($message) use ($first_party_email, $subject) { $message->to($first_party_email)->subject($subject); }); } } catch (\Exception $e) { (new Slack(new Request(['title' => 'Digio Email exception', 'fields' => [ 'Message' => $e->getMessage(), 'Line' => $e->getLine(), 'File' => $e->getFile(), 'Trace' => $e->getTraceAsString(), ]])))->send(); return 3; } } return 0; } /** * Fetch and store the signed document + audit trail from Digio. * * @param int $id Application ID * @param int $lender_id Lender ID * @param string $documentId Digio document ID */ public function fetchDocuments(int $id, int $lender_id, string $documentId) { if ($lender_id == Vivriti::$lender_id) { $this->entity = Vivriti::$lender_id; } $this->fetch = true; $this->application_id = $id; // Download the signed document $signedPdfUrl = $this->baseUrl . self::ENDPOINT_DOWNLOAD . '?id=' . $documentId; $signedResponse = $this->makeRequest('GET', $signedPdfUrl); // Download the audit trail (Digio appends ?type=audit_trail) $auditUrl = $this->baseUrl . self::ENDPOINT_DOWNLOAD . '?id=' . $documentId . '&type=audit_trail'; $auditResponse = $this->makeRequest('GET', $auditUrl); $this->saveDocuments($signedResponse, $auditResponse, $documentId, $id); } public function updateHypothecation(int $application_id, int $id = 0, string $signer = '') { $loanHypothecationModel = LoanHypothecationModel::where('application_id', $application_id)->whereNull('deleted_at') ->orderBy('created_at', 'desc') ->first(['id', 'applicant_signing', 'co_applicant_signing', 'first_party_signing']); if (!$loanHypothecationModel) { return false; } if ($signer === 'applicant') { LoanHypothecationModel::where('id', $loanHypothecationModel->id) ->update(['applicant_signing' => 1]); $this->createDigitalSigned($application_id, 1); return true; } if ($signer === 'co_applicant') { LoanHypothecationModel::where('id', $loanHypothecationModel->id) ->update(['co_applicant_signing' => 1]); $this->createDigitalSigned($application_id, 2); return true; } if ($signer === 'first_party') { LoanHypothecationModel::where('id', $loanHypothecationModel->id) ->update(['first_party_signing' => 1, 'status' => 'success']); return true; } return false; } // ------------------------------------------------------------------------- // Private – Digio API calls // ------------------------------------------------------------------------- /** * Build the sign-request payload and POST it to Digio. * Maps Leegality-style data (invitees, stamped PDF) to Digio's format. * * @return string|false First active signer URL, or false on error. */ private function createSignRequest() { $data = $this->getData(); $application = $data['application']; // ---- Determine stamp series (same logic as Leegality) --------------- $exceptional_ids = explode(',', _env('STAMP_SERIES_SPECIFIC_CASES')); $stamp_series = (isset($application->approved_amount) && $application->approved_amount <= 500000 && !in_array($this->request->get('id'), $exceptional_ids)) ? _env('DIGIO_STAMP_SERIES', 'MH-500-Homeville') : 'DL-300-undefined'; if ($application->lender_id == \App\BlackSoil::$lender_id) { $stamp_series = 'DL-300-'; } // ---- Generate and base64-encode the PDF (same helper as Leegality) -- $pdfBase64 = $this->getHypothecationAgreement($stamp_series); $expireDays = (int)_env('DIGIO_EXPIRE_IN_DAYS', 30); // ---- Map invitees → Digio signers ----------------------------------- $signers = $this->buildSigners($application, $exceptional_ids); // ---- Build Digio request body --------------------------------------- $payload = [ 'file_name' => 'LoanAgreement_' . $this->request->get('id') . '.pdf', 'file_data' => $pdfBase64, // Base64 PDF 'signers' => $signers, 'expire_in_days' => $expireDays, 'notify_signers' => (bool)_env('DIGIO_NOTIFY_SIGNERS', true), 'send_sign_link' => (bool)_env('DIGIO_SEND_SIGN_LINK', true), 'include_authentication_url' => true, 'sequential' => false, // All signers can sign in parallel 'display_on_page' => 'all', "estamp_request" =>["tags" =>[$stamp_series => 1]] ]; $url = $this->baseUrl . self::ENDPOINT_CREATE; $response = $this->makeRequest('POST', $url, $payload); if (!$response) { return false; } return $this->handleCreateResponse($response); } /** * Build the `signers` array for Digio from application data. * Mirrors LoanHypothecation::getInvitees(). */ private function buildSigners($application, array $exceptional_ids): array { $signers = []; // Primary applicant $signers[] = [ 'identifier' => $application->mobile_number ?: $application->email, 'name' => $application->name, 'reason' => 'Loan Agreement Signing – Applicant', 'sign_type' => 'aadhaar', ]; // Co-applicant (if present) if (!empty($application->co_applicant_name) && ($application->co_applicant_mobile_number || $application->co_applicant_email)) { $signers[] = [ 'identifier' => $application->co_applicant_mobile_number ?: $application->co_applicant_email, 'name' => $application->co_applicant_name, 'reason' => 'Loan Agreement Signing – Co-Applicant', 'sign_type' => 'aadhaar', ]; } // First party (lender / CreditFair) – only for non-Vivriti lenders if ($this->entity != Vivriti::$lender_id) { $isSmallLoan = isset($application->approved_amount) && $application->approved_amount <= 500000 && !in_array($this->request->get('id'), $exceptional_ids); if ($application->lender_id != \App\BlackSoil::$lender_id) { if ($isSmallLoan) { $signers[] = [ 'identifier' => _env('LEEGALITY_CF_PARTY_EMAIL'), // reuse same config 'name' => _env('LEEGALITY_CF_PARTY_NAME'), 'reason' => 'Loan Agreement Signing – First Party', 'sign_type' => 'aadhaar', ]; } else { $signers[] = [ 'identifier' => _env('LEEGALITY_FIRST_PARTY_EMAIL'), 'name' => _env('LEEGALITY_FIRST_PARTY_NAME'), 'reason' => 'Loan Agreement Signing – First Party', 'sign_type' => 'aadhaar', ]; } }else{ $signers[] = [ 'identifier' => 'esign@creditfair.in', // reuse same config 'name' => 'Blacksoil Capital Private Limited', 'reason' => 'Loan Agreement Signing – First Party', 'sign_type' => 'aadhaar', ]; } } return $signers; } /** * Parse Digio's response to a create-sign-request call. * Stores the record in LoanHypothecationModel and returns the first signer URL. * * Digio response shape: * { * "id": "DID...", * "status": "requested", * "signing_url": "https://app.digio.in/...", <- first-signer URL (may not exist) * "signers": [ * { "identifier": "...", "name": "...", "status": "requested", * "sign_url": "https://app.digio.in/..." }, * ... * ] * } */ private function handleCreateResponse($response) { $id = $response->id ?? null; if (!$id) { return false; } // Persist to LoanHypothecationModel (same table as Leegality) $loanHypothecationModel = new LoanHypothecationModel(); $loanHypothecationModel->fill([ 'application_id' => $this->request->get('id'), 'documentId' => $id, 'response' => json_encode($response), 'status' => 'pending', 'first_party_signing' => 0, 'applicant_signing' => 0, 'co_applicant_signing' => 0, 'created' => Carbon::now()->toDateTimeString(), ]); $loanHypothecationModel->save(); // Return the first active signer's URL $signers = $response->signers ?? []; foreach ($signers as $signer) { if (($signer->status ?? '') !== 'signed' && !empty($signer->signing_url ?? $signer->sign_url ?? '')) { return $signer->signing_url ?? $signer->sign_url; } } // Fallback to top-level signing_url return $response->signing_url ?? false; } // ------------------------------------------------------------------------- // Private – HTTP helper // ------------------------------------------------------------------------- /** * Execute a cURL request against the Digio API. * Authentication: HTTP Basic Auth (client_id:client_secret). * * @param string $method GET | POST * @param string $url Full URL * @param array|null $payload JSON body (for POST) * @return object|string|false Parsed JSON response object, raw string for binary, * or false on error. */ private function makeRequest(string $method, string $url, ?array $payload = null) { $clientId = _env('DIGIO_CLIENT_ID'); $clientSecret = _env('DIGIO_CLIENT_SECRET'); $curl = curl_init(); $headers = [ 'Content-Type: application/json', ]; $options = [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => '', CURLOPT_MAXREDIRS => 10, CURLOPT_TIMEOUT => 60, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => strtoupper($method), CURLOPT_USERPWD => $clientId . ':' . $clientSecret, // Basic Auth CURLOPT_HTTPAUTH => CURLAUTH_BASIC, ]; if ($payload !== null && strtoupper($method) === 'POST') { $options[CURLOPT_POSTFIELDS] = json_encode($payload); } $options[CURLOPT_HTTPHEADER] = $headers; curl_setopt_array($curl, $options); $response = curl_exec($curl); $err = curl_error($curl); curl_close($curl); // Log to CloudWatch $cwFactory = new CloudWatchLoggerFactory(); $logData = [ 'application_id' => $this->request->get('id'), 'user_id' => Auth::user()->id ?? '', 'vendor' => 'digio', 'api_url' => $url, 'Response' => is_string($response) ? substr($response, 0, 2000) : $response, 'error_msg' => $err ?? '', ]; $cwFactory->logger(json_encode($logData)); $payloadForLog = $payload; unset($payloadForLog['file_data']); // Slack notification (mirrors Leegality logging) (new Slack(new Request([ 'title' => 'Digio', 'fields' => ['url' => $url, 'res' => is_string($response) ? substr($response, 0, 500) : '', 'req' => json_encode($payloadForLog)], 'group' => Slack::$STAGE_TESTING, ])))->send(); if ($err) { return false; } // Attempt JSON decode; return raw string if it fails (e.g. binary PDF) $decoded = json_decode($response); return ($decoded === null && json_last_error() !== JSON_ERROR_NONE) ? $response : $decoded; } // ------------------------------------------------------------------------- // Private – Document storage // ------------------------------------------------------------------------- /** * Save the signed PDF and audit trail to S3, then record in consumer_application_files. * * @param mixed $signedResponse Raw PDF bytes (or JSON with base64) from Digio download * @param mixed $auditResponse Raw PDF bytes for audit trail * @param string $documentId Digio document ID * @param int $applicationId Our application ID */ private function saveDocuments($signedResponse, $auditResponse, string $documentId, int $applicationId) { $s3 = Storage::disk('s3'); // Digio returns raw PDF bytes for download; store directly $fileName = 'loan_agreement_esign'; $path = $applicationId . '/' . $fileName . '.pdf'; // Handle both raw binary and base64-encoded responses $pdfContent = is_object($signedResponse) && isset($signedResponse->file) ? base64_decode($signedResponse->file) : (string)$signedResponse; $s3->put($path, $pdfContent); $this->saveToFile($fileName, $path, $applicationId); // Audit trail $auditFileName = 'auditTrail'; $auditPath = $applicationId . '/' . $auditFileName . '.pdf'; $auditContent = is_object($auditResponse) && isset($auditResponse->file) ? base64_decode($auditResponse->file) : (string)$auditResponse; $s3->put($auditPath, $auditContent); $this->saveToFile($auditFileName, $auditPath, $applicationId); // Raw response JSON $jsonPath = $applicationId . '/digio_hypothecation_response.json'; $s3->put($jsonPath, json_encode(['documentId' => $documentId])); } /** * Insert a record into consumer_application_files (mirrors LoanHypothecation::saveToFile). */ private function saveToFile(string $fileName, string $path, int $applicationId) { $consumerApplicationFile = new ConsumerApplicationFiles(); $consumerApplicationFile->consumer_application_id = $applicationId; $consumerApplicationFile->file_type = $fileName; $consumerApplicationFile->bank_name = ''; $consumerApplicationFile->password = ''; $consumerApplicationFile->accounttype = ''; $consumerApplicationFile->statementtype = ''; $consumerApplicationFile->statemenfilettype = ''; $consumerApplicationFile->customer_id = ''; $consumerApplicationFile->provider = ''; $consumerApplicationFile->finbit_analysis_date = ''; $consumerApplicationFile->bp_number = ''; $consumerApplicationFile->accountUID = ''; $consumerApplicationFile->file_path = $path; $consumerApplicationFile->created_by = Auth::user()->id ?? 107; $consumerApplicationFile->image_score = ''; $consumerApplicationFile->m_generated = 1; $consumerApplicationFile->applicant_type = 1; $consumerApplicationFile->bureau_report_type = ''; $consumerApplicationFile->bureau_report_score = ''; $consumerApplicationFile->is_masked = 0; $consumerApplicationFile->save(); } // ------------------------------------------------------------------------- // Private – Signing link helpers (mirrors LoanHypothecation) // ------------------------------------------------------------------------- /** * Find the signing URL for a specific signer (matched by phone or email). * Works against the Digio response stored in LoanHypothecationModel::response. * * Digio response stores signers as: * { "signers": [ { "identifier": "mobile|email", "signing_url": "..." } ] } */ public function getHypothecationLink($response, ?string $phone, ?string $email): ?string { $signers = $response->signers ?? []; foreach ($signers as $signer) { if ($signer->identifier == $phone || $signer->identifier == $email) { return $signer->signing_url ?? $signer->sign_url ?? null; } } return null; } // ------------------------------------------------------------------------- // Private – Shared helpers (copied from LoanHypothecation) // ------------------------------------------------------------------------- private function getData(): array { $data = []; if (!$this->request->has('application')) { $data['application'] = ConsumerApplication::where('id', $this->request->get('id'))->first([ 'name', 'mobile_number', 'email', 'co_applicant_name', 'co_applicant_mobile_number', 'co_applicant_email', 'approved_amount', ]); } else { $data['application'] = $this->request->get('application'); } return $data; } /** * Render the hypothecation agreement PDF (identical to LoanHypothecation::getHypothecationAgreement). * * @param string $stamp_series * @return string Base64-encoded PDF */ public function getHypothecationAgreement(string $stamp_series): string { $type = 'Loan Agreement Esign'; $request = new Request(); $request->request->add(['type' => $type]); $consumerDocument = new ConsumerDocument($request); $viewParameter = $consumerDocument->getDocument($this->request->get('id')); $pdf = App::make('snappy.pdf.wrapper'); $apiController = new ApiController(); $isHypothecationActive = $apiController->canHypothecated($this->request->get('id')); $save_agreement_document_details = [ 'isInsuranceTaken' => 1, 'user' => $viewParameter['user'], 'id' => $this->request->get('id'), 'data' => $viewParameter['data'], 'pdf' => 0, 'lender' => $viewParameter['lender'], 'combined_sl_la' => $viewParameter['combined_sl_la'], 'stampSeries' => $stamp_series, 'isHypothecationActive' => $isHypothecationActive, ]; $loanAmount = $save_agreement_document_details['user']['loan_amount'] ?? $save_agreement_document_details['user']['loan_details']['loan_amount'] ?? 'NA'; $coApplicantName = $save_agreement_document_details['user']['co_applicant_name'] ?? 'blank'; $lender_id = $save_agreement_document_details['user']['lender_id'] ?? 'blank'; $applicant_name = $save_agreement_document_details['user']['customer_name'] ?? 'NA'; $exceptional_ids = explode(',', _env('STAMP_SERIES_SPECIFIC_CASES')); $isSmallLoan = isset($loanAmount) && $loanAmount <= 500000 && !in_array($this->request->get('id'), $exceptional_ids); $invitee_name = $isSmallLoan ? _env('LEEGALITY_CF_PARTY_NAME') : _env('LEEGALITY_FIRST_PARTY_NAME'); if ($viewParameter['user']['lender_id'] == Vivriti::$lender_id) { $view = 'admin.documents.vivriti.loan_agreement_v2'; } elseif ($viewParameter['user']['lender_id'] == \App\BlackSoil::$lender_id) { $view = 'admin.documents.blacksoil.loan_agreement_v1'; // Non-solar BlackSoil cases have their own agreement. This payload has no // 'application' key, so appliesTo() resolves from the id instead. if (NonSolarSanctionLetterData::appliesTo($this->request->get('id'))) { $view = NonSolarSanctionLetterData::LOAN_AGREEMENT_VIEW; $save_agreement_document_details['nonsolar_sanction_letter'] = NonSolarSanctionLetterData::build($this->request->get('id')); } } else { $view = 'admin.documents.hypothecation_borrower_loan_agreement_esign_v_two_v1'; } $footerData = [ 'applicant_name' => $applicant_name, 'co_applicant_name' => $coApplicantName, 'invitee_name' => $invitee_name, ]; $footerHtml = view('admin.documents.vivriti.pdf_footer', $footerData)->render(); $file = $pdf->loadView($view, $save_agreement_document_details) ->setOption('encoding', 'UTF-8') ->setPaper('a4') ->setOrientation('portrait') ->setOption('margin-bottom', '35mm') ->setOption('footer-html', $footerHtml) ->setOption('footer-spacing', 28) ->output(); return base64_encode($file); } /** * Record that a party has digitally signed (mirrors LoanHypothecation::createDigitalSigned). */ private function createDigitalSigned(int $application_id, int $applicant_type) { $date = Carbon::now()->toDateTimeString(); $application = DigitalSignDetails::where('application_id', $application_id) ->where('applicant', $applicant_type) ->first(['id']); if (!isset($application->id)) { $digitalSignDetails = new DigitalSignDetails(); $digitalSignDetails->fill([ 'application_id' => $application_id, 'applicant' => $applicant_type, 'load_time' => $date, 'click_time' => $date, 'language' => 'ENGLISH', 'isagreementsigned' => 1, 'type' => 'consumer', 'esign_vendor' => 'digio', // ← "digio" instead of "leegality" 'created_at' => $date, ]); $digitalSignDetails->save(); } } /** * Cancel an active/pending loan agreement signing request via Digio API. * * Business rules: * - Fetches the latest non-deleted record from loan_hypothecation_model for this application. * - If status != 'pending' (e.g. success, completed, cancelled): DO NOT call Digio API. * - If status == 'pending': Call Digio cancel API using documentId from the table. * - Logs to AWS CloudWatch with application_id, document_id, and Digio API response. * * @param int|null $applicationId * @return array */ public function cancelDocument($applicationId = null) { $appId = $applicationId ?: ($this->request ? $this->request->get('id') : null); if (!$appId) { return [ 'status' => 'FAIL', 'message' => 'Application ID is missing.' ]; } $loanHypo = LoanHypothecationModel::where('application_id', $appId) ->whereNull('deleted_at') ->orderBy('created_at', 'desc') ->first(); // Fallback to recently soft-deleted record if active record not found if (!$loanHypo) { $loanHypo = LoanHypothecationModel::withTrashed() ->where('application_id', $appId) ->orderBy('created_at', 'desc') ->first(); } if (!$loanHypo) { return [ 'status' => 'FAIL', 'message' => 'No loan hypothecation record found for application #' . $appId ]; } $status = strtolower(trim($loanHypo->status)); // Rule: Only pending documents can be cancelled on Digio if ($status !== 'pending') { $cwFactory = new CloudWatchLoggerFactory(); $cwFactory->logger(json_encode([ 'application_id' => $appId, 'user_id' => Auth::check() ? Auth::id() : '', 'vendor' => 'digio', 'action' => 'cancel_loan_agreement_skipped', 'document_id' => $loanHypo->documentId, 'status' => $loanHypo->status, 'message' => 'Document status is ' . $status . ', skipping Digio cancel API call.' ])); return [ 'status' => 'SKIPPED', 'message' => 'Document status is ' . $status . ', Digio cancel API was not called.' ]; } $documentId = $loanHypo->documentId; if (empty($documentId)) { return [ 'status' => 'FAIL', 'message' => 'Document ID is missing for application #' . $appId ]; } // Digio cancel API endpoint: POST https://ext.digio.in:444/v2/client/document/{documentId}/cancel $urlTemplate = _env('DIGIO_DELETE_AGREEMENT_URL'); if ($urlTemplate) { $url = str_replace(['{document_id}', '{documentId}'], $documentId, $urlTemplate); } else { $url = rtrim(_env('DIGIO_URL', 'https://ext.digio.in:444/v2'), '/') . '/client/document/' . $documentId . '/cancel'; } $clientId = _env('DIGIO_DELETE_AGREEMENT_CLIENT_ID'); $clientSecret = _env('DIGIO_DELETE_AGREEMENT_CLIENT_SECRET'); $curl = curl_init(); $headers = [ 'Accept: application/json', 'Content-Type: application/json', ]; $options = [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => '', CURLOPT_MAXREDIRS => 10, CURLOPT_TIMEOUT => 60, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_USERPWD => $clientId . ':' . $clientSecret, CURLOPT_HTTPAUTH => CURLAUTH_BASIC, CURLOPT_HTTPHEADER => $headers, ]; curl_setopt_array($curl, $options); $response = curl_exec($curl); $err = curl_error($curl); $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); curl_close($curl); // AWS CloudWatch logging with application_id and Digio API response $cwFactory = new CloudWatchLoggerFactory(); $logData = [ 'application_id' => $appId, 'user_id' => Auth::check() ? Auth::id() : '', 'vendor' => 'digio', 'action' => 'cancel_loan_agreement', 'document_id' => $documentId, 'api_url' => $url, 'http_code' => $httpCode, 'Response' => is_string($response) ? substr($response, 0, 3000) : json_encode($response), 'error_msg' => $err, ]; $cwFactory->logger(json_encode($logData)); $decoded = is_string($response) ? json_decode($response) : $response; if ($err) { return [ 'status' => 'FAIL', 'message' => 'cURL Error: ' . $err, 'response' => $response ]; } if ($httpCode == 200) { // Update loan_hypothecation_model record status to cancelled $loanHypo->status = 'cancelled'; $loanHypo->response = is_string($response) ? $response : json_encode($response); $loanHypo->save(); return [ 'status' => 'OK', 'message' => 'Document cancelled successfully on Digio.', 'data' => $decoded, 'raw' => $response ]; } $errMsg = (isset($decoded->message) && $decoded->message) ? $decoded->message : 'Failed to cancel document on Digio.'; return [ 'status' => 'FAIL', 'code' => $decoded->code ?? $httpCode, 'message' => $errMsg, 'details' => $decoded->details ?? null, 'response' => $decoded ]; } }