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'; } 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(); } } }