fix(jobs): eliminasi duplicate trans_reference dengan amount_lcy yang sama pada GenerateClosingBalanceReportJob
Memperbaiki bug yang menyebabkan data duplikat pada hasil closing balance report karena penggunaan `distinct` yang tidak efektif. Perubahan utama: • Query Optimization untuk Duplicate Elimination: - Mengganti subquery `whereRaw` dengan pendekatan `groupBy` untuk performa dan akurasi yang lebih baik - Menggunakan `MIN(id)` dan `MIN(date_time)` untuk menjaga konsistensi data terpilih - Menambahkan `groupBy` pada `trans_reference`, `amount_lcy`, dan `booking_date` untuk filter duplikat yang akurat • Double Layer Validation: - Validasi di level database: eliminasi duplikat menggunakan `groupBy` - Validasi di level aplikasi: menggunakan array `$seenTransactions` untuk deteksi duplikat saat iterasi - Transaksi yang terdeteksi sebagai duplikat akan dilewati (skip processing) • Enhanced Logging: - Menambahkan log saat duplicate transaction terdeteksi dan dilewati - Log jumlah total duplikat yang ditemukan untuk audit dan monitoring - Menambahkan log jumlah record yang dihapus saat `deleteExistingProcessedData()` • Database Index Recommendations: - Menambahkan saran index untuk mendukung operasi `groupBy`: - `(account_number, booking_date, trans_reference, amount_lcy)` - Index ini akan membantu efisiensi dalam filtering dan deduplikasi data • Compatibility: - Eager loading tetap dipertahankan untuk efisiensi query - Skema output CSV tidak berubah (tidak ada breaking changes) - Proses chunk tetap digunakan untuk efisiensi memory Testing: - Validasi output CSV tidak mengandung duplikat `trans_reference` dengan `amount_lcy` sama - Performa tetap cepat (±5–10 menit) - Validasi hasil closing balance tetap akurat dan konsisten Breaking Changes: Tidak ada Performa: Terjaga dengan eliminasi duplikat yang efektif
This commit is contained in:
@@ -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)
|
* Export from database to CSV (very fast)
|
||||||
*/
|
*/
|
||||||
@@ -407,42 +337,159 @@ class GenerateClosingBalanceReportJob implements ShouldQueue
|
|||||||
* Build transaction query using pure Eloquent relationships
|
* Build transaction query using pure Eloquent relationships
|
||||||
* Membangun query transaksi menggunakan relasi Eloquent murni
|
* 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()
|
private function buildTransactionQuery()
|
||||||
{
|
{
|
||||||
Log::info('Building optimized transaction query', [
|
Log::info('Building transaction query with duplicate elimination', [
|
||||||
'group_name' => $this->groupName,
|
'group_name' => $this->groupName,
|
||||||
'account_number' => $this->accountNumber,
|
'account_number' => $this->accountNumber,
|
||||||
'period' => $this->period
|
'period' => $this->period
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$modelClass = $this->getModelByGroup();
|
$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([
|
$query = $modelClass::select([
|
||||||
'id',
|
DB::raw('MIN(id) as id'), // Ambil ID terkecil untuk setiap group
|
||||||
'trans_reference',
|
'trans_reference',
|
||||||
'booking_date',
|
'booking_date',
|
||||||
'amount_lcy',
|
'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('account_number', $this->accountNumber)
|
||||||
->where('booking_date', $this->period)
|
->where('booking_date', $this->period)
|
||||||
// OPTIMASI: Gunakan raw SQL untuk distinct yang lebih efisien
|
// KUNCI: GroupBy untuk menghilangkan duplicate berdasarkan trans_reference dan amount_lcy
|
||||||
->whereRaw('(trans_reference, amount_lcy) IN (
|
->groupBy('trans_reference', 'amount_lcy', 'booking_date')
|
||||||
SELECT DISTINCT trans_reference, amount_lcy
|
|
||||||
FROM ' . (new $modelClass)->getTable() . '
|
|
||||||
WHERE account_number = ? AND booking_date = ?
|
|
||||||
)', [$this->accountNumber, $this->period])
|
|
||||||
// OPTIMASI: Simplifikasi ordering
|
|
||||||
->orderBy('booking_date')
|
->orderBy('booking_date')
|
||||||
->orderBy('date_time');
|
->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;
|
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
|
* Get model class based on group name
|
||||||
* Mendapatkan class model berdasarkan group name
|
* Mendapatkan class model berdasarkan group name
|
||||||
|
|||||||
Reference in New Issue
Block a user