accountNumber = $accountNumber; $this->period = $period; $this->reportLogId = $reportLogId; $this->groupName = $groupName ?? 'DEFAULT'; } /** * Execute the job. * Memproses data transaksi dan generate laporan closing balance */ public function handle(): void { $reportLog = ClosingBalanceReportLog::find($this->reportLogId); if (!$reportLog) { Log::error('Closing balance report log not found', ['id' => $this->reportLogId]); return; } try { Log::info('Starting closing balance report generation', [ 'account_number' => $this->accountNumber, 'period' => $this->period, 'report_log_id' => $this->reportLogId ]); DB::beginTransaction(); // Update status to processing $reportLog->update([ 'status' => 'processing', 'updated_at' => now() ]); // Get opening balance $openingBalance = $this->getOpeningBalance(); // Generate report data $reportData = $this->generateReportData($openingBalance); // Export to CSV $filePath = $this->exportToCsv($reportData); // Update report log with success $reportLog->update([ 'status' => 'completed', 'file_path' => $filePath, 'file_size' => Storage::disk($this->disk)->size($filePath), 'record_count' => count($reportData), 'updated_at' => now() ]); DB::commit(); Log::info('Closing balance report generation completed successfully', [ 'account_number' => $this->accountNumber, 'period' => $this->period, 'file_path' => $filePath, 'record_count' => count($reportData) ]); } catch (Exception $e) { DB::rollback(); Log::error('Error generating closing balance report', [ 'account_number' => $this->accountNumber, 'period' => $this->period, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); $reportLog->update([ 'status' => 'failed', 'error_message' => $e->getMessage(), 'updated_at' => now() ]); throw $e; } } /** * Get opening balance from account balance table * Mengambil saldo awal dari tabel account balance */ private function getOpeningBalance(): float { Log::info('Getting opening balance', [ 'account_number' => $this->accountNumber, 'period' => $this->period ]); // Get previous period based on current period $previousPeriod = $this->period === '20250512' ? Carbon::createFromFormat('Ymd', $this->period)->subDays(2)->format('Ymd') : Carbon::createFromFormat('Ymd', $this->period)->subDay()->format('Ymd'); $accountBalance = AccountBalance::where('account_number', $this->accountNumber) ->where('period', $previousPeriod) ->first(); if (!$accountBalance) { Log::warning('Account balance not found, using 0 as opening balance', [ 'account_number' => $this->accountNumber, 'period' => $this->period ]); return 0.0; } $openingBalance = (float) $accountBalance->actual_balance; Log::info('Opening balance retrieved', [ 'account_number' => $this->accountNumber, 'opening_balance' => $openingBalance ]); return $openingBalance; } /** * Generate report data based on the provided SQL query * Menggenerate data laporan berdasarkan query yang diberikan */ private function generateReportData(float $openingBalance): array { Log::info('Generating closing balance report data', [ 'account_number' => $this->accountNumber, 'period' => $this->period, 'opening_balance' => $openingBalance ]); $reportData = []; $runningBalance = $openingBalance; $sequenceNo = 0; // Build query berdasarkan group name $query = $this->buildTransactionQuery(); // Process data in chunks untuk memory efficiency $query->chunk($this->chunkSize, function ($transactions) use (&$reportData, &$runningBalance, &$sequenceNo) { foreach ($transactions as $transaction) { $sequenceNo++; // Calculate running balance $runningBalance += (float) $transaction->amount_lcy; // Format transaction date $transactionDate = $this->formatDateTime($transaction->date_time); $reportData[] = $this->buildReportDataRow($transaction, $sequenceNo, $transactionDate, $runningBalance); } }); Log::info('Report data generated', [ 'total_records' => count($reportData), 'final_balance' => $runningBalance ]); return $reportData; } /** * Build transaction query based on group name * Membangun query transaksi berdasarkan group name */ private function buildTransactionQuery() { // Tentukan tabel berdasarkan group name $tableName = $this->getTableNameByGroup(); $query = DB::table($tableName . ' as s') ->leftJoin('temp_funds_transfer as ft', 'ft._id', '=', 's.trans_reference') ->leftJoin('data_captures as dc', 'dc.id', '=', 's.trans_reference') ->select($this->getSelectFields()) ->where('s.account_number', $this->accountNumber) ->where('s.booking_date', $this->period) ->orderBy('s.booking_date') ->orderBy('date_time'); return $query; } /** * Get table name based on group name * Mendapatkan nama tabel berdasarkan group name */ private function getTableNameByGroup(): string { return $this->groupName === 'QRIS' ? 'stmt_entry' : 'stmt_entry_details'; } /** * Get select fields for the query * Mendapatkan field select untuk query */ private function getSelectFields(): array { return [ 's.trans_reference', 's.booking_date', 's.amount_lcy', 'ft.debit_acct_no', 'ft.debit_value_date', DB::raw('CASE WHEN s.amount_lcy::numeric < 0 THEN s.amount_lcy::numeric ELSE NULL END AS debit_amount'), 'ft.credit_acct_no', 'ft.bif_rcv_acct', 'ft.bif_rcv_name', 'ft.credit_value_date', DB::raw('CASE WHEN s.amount_lcy::numeric > 0 THEN s.amount_lcy::numeric ELSE NULL END AS credit_amount'), 'ft.at_unique_id', 'ft.bif_ref_no', 'ft.atm_order_id', 'ft.recipt_no', 'ft.api_iss_acct', 'ft.api_benff_acct', DB::raw('COALESCE(ft.date_time, dc.date_time, s.date_time) AS date_time'), 'ft.authoriser', 'ft.remarks', 'ft.payment_details', 'ft.ref_no', 'ft.merchant_id', 'ft.term_id' ]; } /** * Build report data row from transaction * Membangun baris data laporan dari transaksi */ private function buildReportDataRow($transaction, int $sequenceNo, string $transactionDate, float $runningBalance): array { return [ 'sequence_no' => $sequenceNo, 'trans_reference' => $transaction->trans_reference, 'booking_date' => $transaction->booking_date, 'transaction_date' => $transactionDate, 'amount_lcy' => $transaction->amount_lcy, 'debit_acct_no' => $transaction->debit_acct_no, 'debit_value_date' => $transaction->debit_value_date, 'debit_amount' => $transaction->debit_amount, 'credit_acct_no' => $transaction->credit_acct_no, 'bif_rcv_acct' => $transaction->bif_rcv_acct, 'bif_rcv_name' => $transaction->bif_rcv_name, 'credit_value_date' => $transaction->credit_value_date, 'credit_amount' => $transaction->credit_amount, 'at_unique_id' => $transaction->at_unique_id, 'bif_ref_no' => $transaction->bif_ref_no, 'atm_order_id' => $transaction->atm_order_id, 'recipt_no' => $transaction->recipt_no, 'api_iss_acct' => $transaction->api_iss_acct, 'api_benff_acct' => $transaction->api_benff_acct, 'authoriser' => $transaction->authoriser, 'remarks' => $transaction->remarks, 'payment_details' => $transaction->payment_details, 'ref_no' => $transaction->ref_no, 'merchant_id' => $transaction->merchant_id, 'term_id' => $transaction->term_id, 'closing_balance' => $runningBalance ]; } /** * Format datetime string * Memformat string datetime */ private function formatDateTime(?string $datetime): string { if (!$datetime) { return ''; } try { return Carbon::createFromFormat('ymdHi', $datetime)->format('d/m/Y H:i'); } catch (Exception $e) { Log::warning('Error formatting datetime', [ 'datetime' => $datetime, 'error' => $e->getMessage() ]); return $datetime; } } /** * Export report data to CSV file * Export data laporan ke file CSV */ private function exportToCsv(array $reportData): string { Log::info('Starting CSV export for closing balance report', [ 'account_number' => $this->accountNumber, 'period' => $this->period, 'record_count' => count($reportData) ]); // Create directory structure $basePath = "closing_balance_reports"; $accountPath = "{$basePath}/{$this->accountNumber}"; Storage::disk($this->disk)->makeDirectory($basePath); Storage::disk($this->disk)->makeDirectory($accountPath); // Generate filename $fileName = "closing_balance_{$this->accountNumber}_{$this->period}.csv"; $filePath = "{$accountPath}/{$fileName}"; // Delete existing file if exists if (Storage::disk($this->disk)->exists($filePath)) { Storage::disk($this->disk)->delete($filePath); } // Create CSV header $csvHeader = [ 'NO', 'TRANS_REFERENCE', 'BOOKING_DATE', 'TRANSACTION_DATE', 'AMOUNT_LCY', 'DEBIT_ACCT_NO', 'DEBIT_VALUE_DATE', 'DEBIT_AMOUNT', 'CREDIT_ACCT_NO', 'BIF_RCV_ACCT', 'BIF_RCV_NAME', 'CREDIT_VALUE_DATE', 'CREDIT_AMOUNT', 'AT_UNIQUE_ID', 'BIF_REF_NO', 'ATM_ORDER_ID', 'RECIPT_NO', 'API_ISS_ACCT', 'API_BENFF_ACCT', 'AUTHORISER', 'REMARKS', 'PAYMENT_DETAILS', 'REF_NO', 'MERCHANT_ID', 'TERM_ID', 'CLOSING_BALANCE' ]; $csvContent = implode('|', $csvHeader) . "\n"; // Add data rows foreach ($reportData as $row) { $csvRow = [ $row['sequence_no'], $row['trans_reference'] ?? '', $row['booking_date'] ?? '', $row['transaction_date'] ?? '', $row['amount_lcy'] ?? '', $row['debit_acct_no'] ?? '', $row['debit_value_date'] ?? '', $row['debit_amount'] ?? '', $row['credit_acct_no'] ?? '', $row['bif_rcv_acct'] ?? '', $row['bif_rcv_name'] ?? '', $row['credit_value_date'] ?? '', $row['credit_amount'] ?? '', $row['at_unique_id'] ?? '', $row['bif_ref_no'] ?? '', $row['atm_order_id'] ?? '', $row['recipt_no'] ?? '', $row['api_iss_acct'] ?? '', $row['api_benff_acct'] ?? '', $row['authoriser'] ?? '', $row['remarks'] ?? '', $row['payment_details'] ?? '', $row['ref_no'] ?? '', $row['merchant_id'] ?? '', $row['term_id'] ?? '', $row['closing_balance'] ?? '' ]; $csvContent .= implode('|', $csvRow) . "\n"; } // Save file Storage::disk($this->disk)->put($filePath, $csvContent); // Verify file creation if (!Storage::disk($this->disk)->exists($filePath)) { throw new Exception("Failed to create CSV file: {$filePath}"); } Log::info('CSV export completed successfully', [ 'file_path' => $filePath, 'file_size' => Storage::disk($this->disk)->size($filePath) ]); return $filePath; } }