diff --git a/app/Jobs/GenerateClosingBalanceReportJob.php b/app/Jobs/GenerateClosingBalanceReportJob.php index bfeae88..700dc5d 100644 --- a/app/Jobs/GenerateClosingBalanceReportJob.php +++ b/app/Jobs/GenerateClosingBalanceReportJob.php @@ -165,76 +165,6 @@ class GenerateClosingBalanceReportJob implements ShouldQueue ]); } - /** - * Delete existing processed data - */ - private function deleteExistingProcessedData(array $criteria): void - { - ProcessedClosingBalance::where('account_number', $criteria['account_number']) - ->where('period', $criteria['period']) - ->where('group_name', $criteria['group_name']) - ->delete(); - } - - /** - * Prepare processed closing balance data for batch insert - */ - private function prepareProcessedClosingBalanceData($transactions, &$runningBalance, &$sequenceNo): array - { - $processedData = []; - - foreach ($transactions as $transaction) { - $sequenceNo++; - - // Process transaction data - $processedTransactionData = $this->processTransactionData($transaction); - - // Update running balance - $amount = (float) $transaction->amount_lcy; - $runningBalance += $amount; - - // Format transaction date - $transactionDate = $this->formatDateTime($processedTransactionData['date_time']); - - // Prepare data for database insert - $processedData[] = [ - 'account_number' => $this->accountNumber, - 'period' => $this->period, - 'group_name' => $this->groupName, - 'sequence_no' => $sequenceNo, - 'trans_reference' => $processedTransactionData['trans_reference'], - 'booking_date' => $processedTransactionData['booking_date'], - 'transaction_date' => $transactionDate, - 'amount_lcy' => $processedTransactionData['amount_lcy'], - 'debit_acct_no' => $processedTransactionData['debit_acct_no'], - 'debit_value_date' => $processedTransactionData['debit_value_date'], - 'debit_amount' => $processedTransactionData['debit_amount'], - 'credit_acct_no' => $processedTransactionData['credit_acct_no'], - 'bif_rcv_acct' => $processedTransactionData['bif_rcv_acct'], - 'bif_rcv_name' => $processedTransactionData['bif_rcv_name'], - 'credit_value_date' => $processedTransactionData['credit_value_date'], - 'credit_amount' => $processedTransactionData['credit_amount'], - 'at_unique_id' => $processedTransactionData['at_unique_id'], - 'bif_ref_no' => $processedTransactionData['bif_ref_no'], - 'atm_order_id' => $processedTransactionData['atm_order_id'], - 'recipt_no' => $processedTransactionData['recipt_no'], - 'api_iss_acct' => $processedTransactionData['api_iss_acct'], - 'api_benff_acct' => $processedTransactionData['api_benff_acct'], - 'authoriser' => $processedTransactionData['authoriser'], - 'remarks' => $processedTransactionData['remarks'], - 'payment_details' => $processedTransactionData['payment_details'], - 'ref_no' => $processedTransactionData['ref_no'], - 'merchant_id' => $processedTransactionData['merchant_id'], - 'term_id' => $processedTransactionData['term_id'], - 'closing_balance' => $runningBalance, - 'created_at' => now(), - 'updated_at' => now() - ]; - } - - return $processedData; - } - /** * Export from database to CSV (very fast) */ @@ -407,42 +337,159 @@ class GenerateClosingBalanceReportJob implements ShouldQueue * Build transaction query using pure Eloquent relationships * Membangun query transaksi menggunakan relasi Eloquent murni */ + /** + * Build transaction query dengan eliminasi duplicate yang efektif + * Membangun query transaksi dengan menghilangkan duplicate trans_reference dan amount_lcy + */ private function buildTransactionQuery() { - Log::info('Building optimized transaction query', [ + Log::info('Building transaction query with duplicate elimination', [ 'group_name' => $this->groupName, 'account_number' => $this->accountNumber, 'period' => $this->period ]); $modelClass = $this->getModelByGroup(); + $tableName = (new $modelClass)->getTable(); - // OPTIMASI: Eager loading untuk mencegah N+1 queries + // PERBAIKAN: Gunakan groupBy untuk benar-benar menghilangkan duplicate $query = $modelClass::select([ - 'id', + DB::raw('MIN(id) as id'), // Ambil ID terkecil untuk setiap group 'trans_reference', 'booking_date', 'amount_lcy', - 'date_time' + DB::raw('MIN(date_time) as date_time') // Ambil date_time terkecil untuk konsistensi + ]) + ->with([ + 'ft' => function($query) { + $query->select('ref_no', 'date_time', 'debit_acct_no', 'debit_value_date', + 'credit_acct_no', 'bif_rcv_acct', 'bif_rcv_name', 'credit_value_date', + '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'); + }, + 'dc' => function($query) { + $query->select('id', 'date_time'); + } ]) - ->with(['ft:ref_no,date_time,debit_acct_no,debit_value_date,credit_acct_no,bif_rcv_acct,bif_rcv_name,credit_value_date,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', - 'dc:id,date_time']) // Eager load hanya kolom yang diperlukan ->where('account_number', $this->accountNumber) ->where('booking_date', $this->period) - // OPTIMASI: Gunakan raw SQL untuk distinct yang lebih efisien - ->whereRaw('(trans_reference, amount_lcy) IN ( - SELECT DISTINCT trans_reference, amount_lcy - FROM ' . (new $modelClass)->getTable() . ' - WHERE account_number = ? AND booking_date = ? - )', [$this->accountNumber, $this->period]) - // OPTIMASI: Simplifikasi ordering + // KUNCI: GroupBy untuk menghilangkan duplicate berdasarkan trans_reference dan amount_lcy + ->groupBy('trans_reference', 'amount_lcy', 'booking_date') ->orderBy('booking_date') ->orderBy('date_time'); - Log::info('Optimized transaction query built successfully'); + Log::info('Transaction query with duplicate elimination built successfully', [ + 'model_class' => $modelClass, + 'table_name' => $tableName + ]); + return $query; } + /** + * Prepare processed closing balance data dengan validasi duplicate + * Mempersiapkan data closing balance dengan validasi untuk mencegah duplicate + */ + private function prepareProcessedClosingBalanceData($transactions, &$runningBalance, &$sequenceNo): array + { + $processedData = []; + $seenTransactions = []; // Track untuk mencegah duplicate di level aplikasi + + foreach ($transactions as $transaction) { + // VALIDASI: Cek duplicate di level aplikasi sebagai safety net + $duplicateKey = $transaction->trans_reference . '|' . $transaction->amount_lcy; + + if (isset($seenTransactions[$duplicateKey])) { + Log::warning('Duplicate transaction detected and skipped', [ + 'trans_reference' => $transaction->trans_reference, + 'amount_lcy' => $transaction->amount_lcy, + 'duplicate_key' => $duplicateKey + ]); + continue; // Skip duplicate + } + + $seenTransactions[$duplicateKey] = true; + $sequenceNo++; + + // Process transaction data + $processedTransactionData = $this->processTransactionData($transaction); + + // Update running balance + $amount = (float) $transaction->amount_lcy; + $runningBalance += $amount; + + // Format transaction date + $transactionDate = $this->formatDateTime($processedTransactionData['date_time']); + + // Prepare data for database insert + $processedData[] = [ + 'account_number' => $this->accountNumber, + 'period' => $this->period, + 'group_name' => $this->groupName, + 'sequence_no' => $sequenceNo, + 'trans_reference' => $processedTransactionData['trans_reference'], + 'booking_date' => $processedTransactionData['booking_date'], + 'transaction_date' => $transactionDate, + 'amount_lcy' => $processedTransactionData['amount_lcy'], + 'debit_acct_no' => $processedTransactionData['debit_acct_no'], + 'debit_value_date' => $processedTransactionData['debit_value_date'], + 'debit_amount' => $processedTransactionData['debit_amount'], + 'credit_acct_no' => $processedTransactionData['credit_acct_no'], + 'bif_rcv_acct' => $processedTransactionData['bif_rcv_acct'], + 'bif_rcv_name' => $processedTransactionData['bif_rcv_name'], + 'credit_value_date' => $processedTransactionData['credit_value_date'], + 'credit_amount' => $processedTransactionData['credit_amount'], + 'at_unique_id' => $processedTransactionData['at_unique_id'], + 'bif_ref_no' => $processedTransactionData['bif_ref_no'], + 'atm_order_id' => $processedTransactionData['atm_order_id'], + 'recipt_no' => $processedTransactionData['recipt_no'], + 'api_iss_acct' => $processedTransactionData['api_iss_acct'], + 'api_benff_acct' => $processedTransactionData['api_benff_acct'], + 'authoriser' => $processedTransactionData['authoriser'], + 'remarks' => $processedTransactionData['remarks'], + 'payment_details' => $processedTransactionData['payment_details'], + 'ref_no' => $processedTransactionData['ref_no'], + 'merchant_id' => $processedTransactionData['merchant_id'], + 'term_id' => $processedTransactionData['term_id'], + 'closing_balance' => $runningBalance, + 'created_at' => now(), + 'updated_at' => now() + ]; + } + + Log::info('Processed closing balance data prepared', [ + 'total_records' => count($processedData), + 'duplicates_skipped' => count($seenTransactions) - count($processedData) + ]); + + return $processedData; + } + + /** + * Delete existing processed data dengan logging + * Menghapus data processed yang sudah ada dengan logging untuk audit + */ + private function deleteExistingProcessedData(array $criteria): void + { + Log::info('Deleting existing processed data', $criteria); + + $deletedCount = ProcessedClosingBalance::where('account_number', $criteria['account_number']) + ->where('period', $criteria['period']) + ->where('group_name', $criteria['group_name']) + ->count(); + + ProcessedClosingBalance::where('account_number', $criteria['account_number']) + ->where('period', $criteria['period']) + ->where('group_name', $criteria['group_name']) + ->delete(); + + Log::info('Existing processed data deleted', [ + 'deleted_count' => $deletedCount, + 'criteria' => $criteria + ]); + } + /** * Get model class based on group name * Mendapatkan class model berdasarkan group name