statementId = $statementId; $this->account_number = $account_number; $this->period = $period; $this->endPeriod = $endPeriod; $this->saldo = $saldo; $this->disk = $disk; $this->client = $client; $this->fileName = "{$account_number}_{$period}.csv"; $this->toCsv = $toCsv; // Calculate start and end dates based on period $this->formatPeriodForFolder(); } /** * Calculate start and end dates for the given period */ private function formatPeriodForFolder(): void { $year = substr($this->period, 0, 4); $month = substr($this->period, 4, 2); // Special case for May 2025 - start from 12th if ($this->period === '202505') { $this->startDate = Carbon::createFromDate($year, $month, 9)->startOfDay(); } else { // For all other periods, start from 1st of the month $this->startDate = Carbon::createFromDate($year, $month, 1)->startOfDay(); } // End date is always the last day of the month $this->endDate = Carbon::createFromDate($year, $month, 1)->endOfMonth()->endOfDay(); // If endPeriod is provided, use it instead of endDate if($this->endPeriod){ $year = substr($this->endPeriod, 0, 4); $month = substr($this->endPeriod, 4, 2); $this->endDate = Carbon::createFromDate($year, $month, 1)->endOfMonth()->endOfDay(); } } /** * Execute the job. */ public function handle(): void { try { Log::info("Starting export statement period job for account: {$this->account_number}, period: {$this->period}"); Log::info("Date range: {$this->startDate->format('Y-m-d')} to {$this->endDate->format('Y-m-d')}"); $this->processStatementData(); if($this->toCsv){ $this->exportToCsv(); } // Generate PDF setelah data diproses $this->generatePdf(); Log::info("Export statement period job completed successfully for account: {$this->account_number}, period: {$this->period}"); } catch (Exception $e) { Log::error("Error in ExportStatementPeriodJob: " . $e->getMessage()); throw $e; } } private function processStatementData(): void { $accountQuery = [ 'account_number' => $this->account_number, 'period' => $this->period ]; $totalCount = $this->getTotalEntryCount(); $existingDataCount = $this->getExistingProcessedCount($accountQuery); // Only process if data is not fully processed if ($existingDataCount !== $totalCount) { $this->deleteExistingProcessedData($accountQuery); $this->processAndSaveStatementEntries($totalCount); } } private function getTotalEntryCount(): int { $query = StmtEntry::where('account_number', $this->account_number) ->whereBetween('booking_date', [ $this->startDate->format('Ymd'), $this->endDate->format('Ymd') ]); Log::info("Getting total entry count with query: " . $query->toSql(), [ 'bindings' => $query->getBindings(), 'account' => $this->account_number, 'start_date' => $this->startDate->format('Ymd'), 'end_date' => $this->endDate->format('Ymd') ]); return $query->count(); } private function getExistingProcessedCount(array $criteria): int { return ProcessedStatement::where('account_number', $criteria['account_number']) ->where('period', $criteria['period']) ->count(); } private function deleteExistingProcessedData(array $criteria): void { ProcessedStatement::where('account_number', $criteria['account_number']) ->where('period', $criteria['period']) ->delete(); } private function processAndSaveStatementEntries(int $totalCount): void { $runningBalance = (float) $this->saldo; $globalSequence = 0; Log::info("Processing {$totalCount} statement entries for account: {$this->account_number}"); $entry = StmtEntry::with(['ft', 'transaction']) ->where('account_number', $this->account_number) ->whereBetween('booking_date', [ $this->startDate->format('Ymd'), $this->endDate->format('Ymd') ]) ->orderBy('date_time', 'ASC') ->orderBy('trans_reference', 'ASC') ->chunk($this->chunkSize, function ($entries) use (&$runningBalance, &$globalSequence) { $processedData = $this->prepareProcessedData($entries, $runningBalance, $globalSequence); if (!empty($processedData)) { DB::table('processed_statements')->insert($processedData); } }); if($entry){ $printLog = PrintStatementLog::find($this->statementId); if($printLog){ $printLog->update(['is_generated' => true]); } } } private function prepareProcessedData($entries, &$runningBalance, &$globalSequence): array { $processedData = []; foreach ($entries as $item) { $globalSequence++; $actualDate = $this->formatActualDate($item); $amount = $item->amount_fcy; if($item->currency=='IDR'){ $amount = $item->amount_lcy; } $runningBalance += (float) $amount; $processedData[] = [ 'account_number' => $this->account_number, 'period' => $this->period, 'sequence_no' => $globalSequence, 'transaction_date' => $item->booking_date, 'reference_number' => $item->trans_reference, 'transaction_amount' => $amount, 'transaction_type' => $amount < 0 ? 'D' : 'C', 'description' => $this->generateNarrative($item), 'end_balance' => $runningBalance, 'actual_date' => $actualDate, 'recipt_no' => $item->ft?->recipt_no ?? '-', 'created_at' => now(), 'updated_at' => now(), ]; } return $processedData; } private function formatTransactionDate($item): string { try { $prefix = substr($item->trans_reference ?? '', 0, 2); $relationMap = [ 'FT' => 'ft', 'TT' => 'tt', 'DC' => 'dc', 'AA' => 'aa' ]; $datetime = $item->date_time; if (isset($relationMap[$prefix])) { $relation = $relationMap[$prefix]; $datetime = $item->$relation?->date_time ?? $datetime; } // Extract date from datetime (first 6 characters) and time (last 4 characters) $dateStr = substr($datetime, 0, 6); // YYMMDD $timeStr = substr($datetime, 6, 4); // HHMM return Carbon::createFromFormat( 'ymdHi', $dateStr . $timeStr )->format('d/m/Y H:i'); } catch (Exception $e) { Log::warning("Error formatting transaction date: " . $e->getMessage()); return Carbon::now()->format('d/m/Y H:i'); } } private function formatActualDate($item): string { try { $prefix = substr($item->trans_reference ?? '', 0, 2); $relationMap = [ 'FT' => 'ft', 'TT' => 'tt', 'DC' => 'dc', 'AA' => 'aa' ]; $datetime = $item->date_time; if (isset($relationMap[$prefix])) { $relation = $relationMap[$prefix]; $datetime = $item->$relation?->date_time ?? $datetime; } return Carbon::createFromFormat( 'ymdHi', $datetime )->format('d/m/Y H:i'); } catch (Exception $e) { Log::warning("Error formatting actual date: " . $e->getMessage()); return Carbon::now()->format('d/m/Y H:i'); } } /** * Generate narrative for a statement entry */ private function generateNarrative($item) { $narr = []; if ($item->transaction) { if ($item->transaction->stmt_narr) { $narr[] = $item->transaction->stmt_narr; } if ($item->narrative) { $narr[] = $item->narrative; } if ($item->transaction->narr_type) { $narr[] = $this->getFormatNarrative($item->transaction->narr_type, $item); } } else if ($item->narrative) { $narr[] = $item->narrative; } if ($item->ft?->recipt_no) { $narr[] = 'Receipt No: ' . $item->ft->recipt_no; } return implode(' ', array_filter($narr)); } /** * Get formatted narrative based on narrative type */ private function getFormatNarrative($narr, $item) { $narrParam = TempStmtNarrParam::where('_id', $narr)->first(); if (!$narrParam) { return ''; } $fmt = ''; if ($narrParam->_id == 'FTIN') { $fmt = 'FT.IN'; } else if ($narrParam->_id == 'FTOUT') { $fmt = 'FT.OUT'; } else if ($narrParam->_id == 'TTTRFOUT') { $fmt = 'TT.O.TRF'; } else if ($narrParam->_id == 'TTTRFIN') { $fmt = 'TT.I.TRF'; } else if ($narrParam->_id == 'APITRX'){ $fmt = 'API.TSEL'; } else if ($narrParam->_id == 'ONUSCR'){ $fmt = 'ONUS.CR'; } else if ($narrParam->_id == 'ONUSDR'){ $fmt = 'ONUS.DR'; }else { $fmt = $narrParam->_id; } $narrFormat = TempStmtNarrFormat::where('_id', $fmt)->first(); if (!$narrFormat) { return ''; } // Get the format string from the database $formatString = $narrFormat->text_data ?? ''; // Parse the format string // Split by the separator ']' $parts = explode(']', $formatString); $result = ''; foreach ($parts as $index => $part) { if (empty($part)) { continue; } if ($index === 0) { // For the first part, take only what's before the '!' $splitPart = explode('!', $part); if (count($splitPart) > 0) { // Remove quotes, backslashes, and other escape characters $cleanPart = trim($splitPart[0]).' '; // Remove quotes at the beginning and end $cleanPart = preg_replace('/^["\'\\\\]+|["\'\\\\]+$/', '', $cleanPart); // Remove any remaining backslashes $cleanPart = str_replace('\\', '', $cleanPart); // Remove any remaining quotes $cleanPart = str_replace('"', '', $cleanPart); $result .= $cleanPart; } } else { // For other parts, these are field placeholders $fieldName = strtolower(str_replace('.', '_', $part)); // Get the corresponding parameter value from narrParam $paramValue = null; // Check if the field exists as a property in narrParam if (property_exists($narrParam, $fieldName)) { $paramValue = $narrParam->$fieldName; } else if (isset($narrParam->$fieldName)) { $paramValue = $narrParam->$fieldName; } // If we found a value, add it to the result if ($paramValue !== null) { $result .= $paramValue; } else { // If no value found, try to use the original field name as a fallback if ($fieldName !== 'recipt_no') { $prefix = substr($item->trans_reference ?? '', 0, 2); $relationMap = [ 'FT' => 'ft', 'TT' => 'tt', 'DC' => 'dc', 'AA' => 'aa' ]; if (isset($relationMap[$prefix])) { $relation = $relationMap[$prefix]; $result .= ($item->$relation?->$fieldName ?? '') . ' '; } } } } } return str_replace('', ' ', $result); } /** * Generate PDF statement untuk account yang diproses * Menggunakan data yang sudah diproses dari ProcessedStatement * * @return void * @throws Exception */ private function generatePdf(): void { try { DB::beginTransaction(); Log::info('ExportStatementPeriodJob: Memulai generate PDF', [ 'account_number' => $this->account_number, 'period' => $this->period, 'statement_id' => $this->statementId ]); // Ambil data account dan customer $account = Account::where('account_number', $this->account_number)->first(); if (!$account) { throw new Exception("Account tidak ditemukan: {$this->account_number}"); } $customer = Customer::where('customer_code', $account->customer_code)->first(); if (!$customer) { throw new Exception("Customer tidak ditemukan untuk account: {$this->account_number}"); } // Ambil data branch $branch = Branch::where('code', $account->branch_code)->first(); if (!$branch) { throw new Exception("Branch tidak ditemukan: {$account->branch_code}"); } // Ambil statement entries yang sudah diproses $stmtEntries = ProcessedStatement::where('account_number', $this->account_number) ->where('period', $this->period) ->orderBy('sequence_no') ->get(); if ($stmtEntries->isEmpty()) { throw new Exception("Tidak ada data statement yang diproses untuk account: {$this->account_number}"); } // Prepare header table background (convert to base64 if needed) $headerImagePath = public_path('assets/media/images/bg-header-table.png'); $headerTableBg = file_exists($headerImagePath) ? base64_encode(file_get_contents($headerImagePath)) : null; // Hitung saldo awal bulan $saldoAwalBulan = (object) ['actual_balance' => (float) $this->saldo]; // Generate filename $filename = "{$this->account_number}_{$this->period}.pdf"; // Tentukan path storage dengan format folder baru $periodPath = formatPeriodForFolder($this->period); $storagePath = "statements/{$periodPath}/{$account->branch_code}"; $tempPath = storage_path("app/temp/{$filename}"); $fullStoragePath = "{$storagePath}/{$filename}"; // Buat direktori temp jika belum ada if (!is_dir(dirname($tempPath))) { mkdir(dirname($tempPath), 0777, true); } // Pastikan direktori storage ada Storage::makeDirectory($storagePath); $period = $this->period; $endPeriod = $this->endPeriod; // Render HTML view $html = view('webstatement::statements.stmt', compact( 'stmtEntries', 'account', 'customer', 'headerTableBg', 'branch', 'period', 'endPeriod', 'saldoAwalBulan' ))->render(); Log::info('ExportStatementPeriodJob: HTML view berhasil di-render', [ 'account_number' => $this->account_number, 'html_length' => strlen($html) ]); // Di dalam fungsi generatePdf(), setelah Browsershot::html()->save($tempPath) // Generate PDF menggunakan Browsershot Browsershot::html($html) ->showBackground() ->setOption('addStyleTag', json_encode(['content' => '@page { margin: 0; }'])) ->setOption('protocolTimeout', 2147483) // 2 menit timeout ->setOption('headless', true) ->noSandbox() ->format('A4') ->margins(0, 0, 0, 0) ->waitUntil('load') ->waitUntilNetworkIdle() ->timeout(2147483) ->save($tempPath); // Verifikasi file berhasil dibuat if (!file_exists($tempPath)) { throw new Exception('PDF file gagal dibuat'); } $printLog = PrintStatementLog::find($this->statementId); // Apply password protection jika diperlukan $password = $printLog->password ?? generatePassword($account); // Ambil dari config atau set default if (!empty($password)) { $tempProtectedPath = storage_path("app/temp/protected_{$filename}"); // Encrypt PDF dengan password PDFPasswordProtect::encrypt($tempPath, $tempProtectedPath, $password); // Ganti file original dengan yang sudah diproteksi if (file_exists($tempProtectedPath)) { unlink($tempPath); // Hapus file original rename($tempProtectedPath, $tempPath); // Rename protected file ke original path Log::info('ExportStatementPeriodJob: PDF password protection applied', [ 'account_number' => $this->account_number, 'period' => $this->period ]); } } $fileSize = filesize($tempPath); // Pindahkan file ke storage permanen $pdfContent = file_get_contents($tempPath); Storage::put($fullStoragePath, $pdfContent); // Update print statement log if ($printLog) { $printLog->update([ 'is_available' => true, 'is_generated' => true, 'pdf_path' => $fullStoragePath, 'file_size' => $fileSize ]); } // Hapus file temporary if (file_exists($tempPath)) { unlink($tempPath); } Log::info('ExportStatementPeriodJob: PDF berhasil dibuat dan disimpan', [ 'account_number' => $this->account_number, 'period' => $this->period, 'storage_path' => $fullStoragePath, 'file_size' => $fileSize, 'statement_id' => $this->statementId ]); DB::commit(); } catch (Exception $e) { DB::rollBack(); Log::error('ExportStatementPeriodJob: Gagal generate PDF', [ 'error' => $e->getMessage(), 'account_number' => $this->account_number, 'period' => $this->period, 'statement_id' => $this->statementId, 'trace' => $e->getTraceAsString() ]); // Update print statement log dengan status error $printLog = PrintStatementLog::find($this->statementId); if ($printLog) { $printLog->update([ 'is_available' => false, 'error_message' => $e->getMessage() ]); } throw new Exception('Gagal generate PDF: ' . $e->getMessage()); } } /** * Export processed data to CSV file */ private function exportToCsv(): void { // Determine the base path based on client $account = Account::where('account_number', $this->account_number)->first(); $periodPath = formatPeriodForFolder($this->period); $storagePath = "statements/{$periodPath}/{$account->branch_code}"; Storage::disk($this->disk)->makeDirectory($storagePath); $filePath = "{$storagePath}/{$this->fileName}"; // Delete existing file if it exists if (Storage::disk($this->disk)->exists($filePath)) { Storage::disk($this->disk)->delete($filePath); } $csvContent = "NO|TRANSACTION.DATE|REFERENCE.NUMBER|TRANSACTION.AMOUNT|TRANSACTION.TYPE|DESCRIPTION|END.BALANCE|ACTUAL.DATE\n"; // Retrieve processed data in chunks to reduce memory usage ProcessedStatement::where('account_number', $this->account_number) ->where('period', $this->period) ->orderBy('sequence_no') ->chunk($this->chunkSize, function ($statements) use (&$csvContent, $filePath) { foreach ($statements as $statement) { $csvContent .= implode('|', [ $statement->sequence_no, $statement->transaction_date, $statement->reference_number, $statement->transaction_amount, $statement->transaction_type, $statement->description, $statement->end_balance, $statement->actual_date ]) . "\n"; } // Write to file incrementally to reduce memory usage Storage::disk($this->disk)->append($filePath, $csvContent); }); Log::info("Statement exported to {$this->disk} disk: {$filePath}"); } /** * Get transaction data by reference and field */ private function getTransaction($ref, $field) { $trans = TempFundsTransfer::where('ref_no', $ref)->first(); return $trans->$field ?? ""; } }