Compare commits

...

4 Commits

Author SHA1 Message Date
Daeng Deni Mardaeni
5752427297 refactor(report): konversi query raw SQL ke pure Eloquent ORM
Melakukan refactor besar pada job GenerateClosingBalanceReport untuk mengganti penggunaan raw SQL dan left join dengan implementasi Eloquent ORM penuh, guna meningkatkan maintainability, akurasi data, dan performa sistem.

🧱 Refactor Arsitektur Query:
- Menghilangkan semua penggunaan `leftJoin` yang menyebabkan duplikasi data
- Mengganti query menjadi pure Eloquent dengan relasi dan `with()` (eager loading)
- Menghindari N+1 problem melalui optimasi relasi `ft` (TempFundsTransfer) dan `dc` (DataCapture)

🧩 Integrasi Model:
- Menggunakan model: `StmtEntry`, `StmtEntryDetail`, `TempFundsTransfer`, `DataCapture`
- Seleksi model berdasarkan `groupName`:
  - `QRIS` → `StmtEntry`
  - selainnya → `StmtEntryDetail`
- Relasi dimanfaatkan langsung via properti model, dengan fallback logic

⚙️ Peningkatan Proses Data:
- Menyederhanakan metode `processTransactionData()` untuk memanfaatkan relasi langsung
- Tambahan null safety menggunakan null coalescing operator (`??`)
- Tetap mempertahankan chunk processing untuk efisiensi memori

🔒 Konsistensi & Logging:
- Tambahan DB transaction (`beginTransaction`, `commit`, `rollback`) untuk data integrity
- Logging komprehensif di tiap tahap proses: pemilihan model, query, pemrosesan, fallback
- Error handling dengan informasi error yang lebih informatif

🚀 Optimasi Performa:
- Selective field loading untuk minimalisasi beban memori
- Chunking data untuk skala besar
- Eager loading efisien tanpa join kompleks

 Tujuan & Manfaat:
- Meningkatkan maintainability & keterbacaan kode
- Menghilangkan duplikasi data akibat `leftJoin`
- Meningkatkan akurasi dan konsistensi data laporan
- Mengikuti Laravel best practices dalam penulisan query & relasi
2025-07-26 08:42:42 +07:00
Daeng Deni Mardaeni
eb89916b1c refactor(webstatement): split generateReportData dan dukung closing balance per group akun
- Refactor generateReportData jadi fungsi kecil (query builder, selector, row builder)
- Tambah dukungan multi group akun (DEFAULT, QRIS) pada closing balance job
- Pisahkan akun QRIS dan sesuaikan struktur dispatch dan account list
- Tingkatkan maintainability, scalability, dan clarity proses
2025-07-24 09:30:13 +07:00
Daeng Deni Mardaeni
80c866f646 feat(webstatement): fallback stmt_entry_id menggunakan field id pada CSV
Menambahkan dukungan fallback untuk nilai `stmt_entry_id` yang kosong/null dengan menggunakan field `id` dari CSV (jika tersedia di akhir file).

Perubahan yang dilakukan:
- Menambahkan 'id' sebagai bagian dari expected CSV headers
- Mengimplementasikan handleStmtEntryIdFallback() untuk logika pengganti
- Menggunakan field 'id' sebagai stmt_entry_id jika nilainya kosong atau null
- Menyesuaikan validasi jumlah kolom terhadap struktur CSV terbaru
- Melakukan pembersihan field 'id' sebelum data disimpan ke database
- Memperkuat validasi di addToBatch() agar stmt_entry_id selalu valid
- Menambahkan logging untuk proses fallback dan debugging
- Meningkatkan error handling untuk kasus data tidak valid
- Menjamin kompatibilitas dengan struktur model StmtEntryDetail
- Optimasi batch insert melalui pengecekan dan pembersihan data lebih ketat
2025-07-23 14:45:08 +07:00
Daeng Deni Mardaeni
e5c33bf631 feat(webstatement): tambah parameter period ke ProcessDailyMigration command
Menambahkan parameter --period pada command ProcessDailyMigration untuk fleksibilitas pemrosesan data harian.

Perubahan yang dilakukan:

- Menambahkan parameter --period dengan default '-1 day' pada command ProcessDailyMigration
- Memungkinkan input period dalam berbagai format:
  - Format Ymd (contoh: 20250120)
  - Format relative date (contoh: '-2 days', '-1 week')
  - Default fallback ke '-1 day' jika parameter kosong atau format tidak valid

- Update method index di MigrasiController untuk menerima dan memproses parameter period
- Menambahkan method determinePeriod untuk konversi dan validasi parameter period
- Menggunakan Carbon untuk parsing dan konversi tanggal
- Menambahkan logging detail untuk tracking parameter input dan hasil konversi period
- Menambahkan validasi dan error handling jika format periode tidak sesuai
- Mempertahankan backward compatibility agar command lama tetap berjalan seperti sebelumnya
- Update deskripsi command dan signature agar dokumentasi CLI lebih jelas

Tujuan perubahan:

- Memberikan fleksibilitas bagi tim operasional untuk menjalankan migrasi data dengan periode yang spesifik
- Memudahkan eksekusi ulang data harian atau data backdate tanpa modifikasi kode
- Memastikan proses migrasi lebih aman, transparan, dan dapat dipantau melalui logging
2025-07-21 11:30:55 +07:00
7 changed files with 543 additions and 170 deletions

View File

@@ -64,16 +64,6 @@ class GenerateClosingBalanceReportBulkCommand extends Command
'IDR1354400010001',
'IDR1728500010001',
'IDR1728600010001',
'IDR1354500010001',
'IDR1354500020001',
'IDR1354500030001',
'IDR1354500040001',
'IDR1354500050001',
'IDR1354500060001',
'IDR1354500070001',
'IDR1354500080001',
'IDR1354500090001',
'IDR1354500100001',
'IDR1720500010001',
'1078333878',
'1081647484',
@@ -89,6 +79,19 @@ class GenerateClosingBalanceReportBulkCommand extends Command
'IDR1354200010001'
];
private $qrisAccount = [
'IDR1354500010001',
'IDR1354500020001',
'IDR1354500030001',
'IDR1354500040001',
'IDR1354500050001',
'IDR1354500060001',
'IDR1354500070001',
'IDR1354500080001',
'IDR1354500090001',
'IDR1354500100001',
];
/**
* Execute the console command.
* Menjalankan proses generate laporan closing balance untuk banyak rekening dengan periode range
@@ -281,7 +284,7 @@ class GenerateClosingBalanceReportBulkCommand extends Command
}
// Jika tidak ada parameter accounts, gunakan default list
$accountList = ['DEFAULT' => $this->defaultAccounts];
$accountList = ['DEFAULT' => $this->defaultAccounts, 'QRIS' => $this->qrisAccount];
// Filter by client jika ada (untuk backward compatibility)
if ($clientFilter) {
@@ -378,7 +381,7 @@ class GenerateClosingBalanceReportBulkCommand extends Command
}
// Dispatch the job
GenerateClosingBalanceReportJob::dispatch($accountNumber, $period, $reportLog->id);
GenerateClosingBalanceReportJob::dispatch($accountNumber, $period, $reportLog->id, $groupName);
DB::commit();

View File

@@ -1,51 +1,67 @@
<?php
namespace Modules\Webstatement\Console;
namespace Modules\Webstatement\Console;
use Exception;
use Illuminate\Console\Command;
use Modules\Webstatement\Http\Controllers\MigrasiController;
use Exception;
use Illuminate\Console\Command;
use Modules\Webstatement\Http\Controllers\MigrasiController;
use Illuminate\Support\Facades\Log;
class ProcessDailyMigration extends Command
class ProcessDailyMigration extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'webstatement:process-daily-migration
{--process_parameter= : To process migration parameter true/false}
{--period= : Period to process (default: -1 day, format: Ymd or relative date)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Process data migration for the specified period (default: previous day)';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'webstatement:process-daily-migration
{--process_parameter= : To process migration parameter true/false}';
$processParameter = $this->option('process_parameter');
$period = $this->option('period');
/**
* The console command description.
*
* @var string
*/
protected $description = 'Process data migration for the previous day\'s period';
// Log start of process
Log::info('Starting daily data migration process', [
'process_parameter' => $processParameter ?? 'false',
'period' => $period ?? '-1 day'
]);
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$processParameter = $this->option('process_parameter');
$this->info('Starting daily data migration process...');
$this->info('Process Parameter: ' . ($processParameter ?? 'False'));
$this->info('Period: ' . ($period ?? '-1 day (default)'));
$this->info('Starting daily data migration process...');
$this->info('Process Parameter: ' . ($processParameter ?? 'False'));
try {
$controller = app(MigrasiController::class);
$response = $controller->index($processParameter, $period);
try {
$controller = app(MigrasiController::class);
$response = $controller->index($processParameter);
$responseData = json_decode($response->getContent(), true);
$message = $responseData['message'] ?? 'Process completed';
$responseData = json_decode($response->getContent(), true);
$this->info($responseData['message'] ?? 'Process completed');
$this->info($message);
Log::info('Daily migration process completed successfully', ['message' => $message]);
return Command::SUCCESS;
} catch (Exception $e) {
$this->error('Error processing daily migration: ' . $e->getMessage());
return Command::FAILURE;
}
return Command::SUCCESS;
} catch (Exception $e) {
$errorMessage = 'Error processing daily migration: ' . $e->getMessage();
$this->error($errorMessage);
Log::error($errorMessage, ['exception' => $e->getTraceAsString()]);
return Command::FAILURE;
}
}
}

View File

@@ -101,30 +101,99 @@
return response()->json(['error' => $e->getMessage()], 500);
}
}
public function index($processParameter = false)
/**
* Proses migrasi data dengan parameter dan periode yang dapat dikustomisasi
*
* @param bool|string $processParameter Flag untuk memproses parameter
* @param string|null $period Periode yang akan diproses (default: -1 day)
* @return JsonResponse
*/
public function index($processParameter = false, $period = null)
{
$disk = Storage::disk('sftpStatement');
try {
Log::info('Starting migration process', [
'process_parameter' => $processParameter,
'period' => $period
]);
if ($processParameter) {
foreach (self::PARAMETER_PROCESSES as $process) {
$this->processData($process, '_parameter');
$disk = Storage::disk('sftpStatement');
if ($processParameter) {
Log::info('Processing parameter data');
foreach (self::PARAMETER_PROCESSES as $process) {
$this->processData($process, '_parameter');
}
Log::info('Parameter processes completed successfully');
return response()->json(['message' => 'Parameter processes completed successfully']);
}
return response()->json(['message' => 'Parameter processes completed successfully']);
}
$period = date('Ymd', strtotime('-1 day'));
if (!$disk->exists($period)) {
// Tentukan periode yang akan diproses
$targetPeriod = $this->determinePeriod($period);
Log::info('Processing data for period', ['period' => $targetPeriod]);
if (!$disk->exists($targetPeriod)) {
$errorMessage = "Period {$targetPeriod} folder not found in SFTP storage";
Log::warning($errorMessage);
return response()->json([
"message" => $errorMessage
], 404);
}
foreach (self::DATA_PROCESSES as $process) {
$this->processData($process, $targetPeriod);
}
$successMessage = "Data processing for period {$targetPeriod} has been queued successfully";
Log::info($successMessage);
return response()->json([
"message" => "Period {$period} folder not found in SFTP storage"
], 404);
'message' => $successMessage
]);
} catch (Exception $e) {
Log::error('Error in migration index method: ' . $e->getMessage());
return response()->json(['error' => $e->getMessage()], 500);
}
}
/**
* Tentukan periode berdasarkan input atau gunakan default
*
* @param string|null $period Input periode
* @return string Periode dalam format Ymd
*/
private function determinePeriod($period = null): string
{
if ($period === null) {
// Default: -1 day
$calculatedPeriod = date('Ymd', strtotime('-1 day'));
Log::info('Using default period', ['period' => $calculatedPeriod]);
return $calculatedPeriod;
}
foreach (self::DATA_PROCESSES as $process) {
$this->processData($process, $period);
// Jika periode sudah dalam format Ymd (8 digit)
if (preg_match('/^\d{8}$/', $period)) {
Log::info('Using provided period in Ymd format', ['period' => $period]);
return $period;
}
return response()->json([
'message' => "Data processing for period {$period} has been queued successfully"
]);
// Jika periode dalam format relative date (contoh: -2 days, -1 week, etc.)
try {
$calculatedPeriod = date('Ymd', strtotime($period));
Log::info('Calculated period from relative date', [
'input' => $period,
'calculated' => $calculatedPeriod
]);
return $calculatedPeriod;
} catch (Exception $e) {
Log::warning('Invalid period format, using default', [
'input' => $period,
'error' => $e->getMessage()
]);
return date('Ymd', strtotime('-1 day'));
}
}
}

View File

@@ -114,6 +114,12 @@
],
'SWADAYA_PANDU' => [
'0081272689',
],
"AWAN_LINTANG_SOLUSI"=> [
"1084269430"
],
"MONETA"=> [
"1085667890"
]
];
}

View File

@@ -2,8 +2,6 @@
namespace Modules\Webstatement\Jobs;
use Carbon\Carbon;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@@ -12,9 +10,14 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Carbon\Carbon;
use Exception;
use Modules\Webstatement\Models\AccountBalance;
use Modules\Webstatement\Models\ClosingBalanceReportLog;
use Modules\Webstatement\Models\StmtEntry;
use Modules\Webstatement\Models\StmtEntryDetail;
use Modules\Webstatement\Models\TempFundsTransfer;
use Modules\Webstatement\Models\DataCapture;
/**
* Job untuk generate laporan closing balance
@@ -27,6 +30,7 @@ class GenerateClosingBalanceReportJob implements ShouldQueue
protected $accountNumber;
protected $period;
protected $reportLogId;
protected $groupName;
protected $chunkSize = 1000;
protected $disk = 'local';
@@ -37,11 +41,12 @@ class GenerateClosingBalanceReportJob implements ShouldQueue
* @param string $period
* @param int $reportLogId
*/
public function __construct(string $accountNumber, string $period, int $reportLogId)
public function __construct(string $accountNumber, string $period, int $reportLogId, string $groupName='DEFAULT')
{
$this->accountNumber = $accountNumber;
$this->period = $period;
$this->reportLogId = $reportLogId;
$this->groupName = $groupName ?? 'DEFAULT';
}
/**
@@ -158,106 +163,316 @@ class GenerateClosingBalanceReportJob implements ShouldQueue
}
/**
* Generate report data based on the provided SQL query
* Menggenerate data laporan berdasarkan query yang diberikan
* Build transaction query using pure Eloquent relationships
* Membangun query transaksi menggunakan relasi Eloquent murni
*/
private function generateReportData(float $openingBalance): array
private function buildTransactionQuery()
{
Log::info('Generating closing balance report data', [
Log::info('Building transaction query using pure Eloquent relationships', [
'group_name' => $this->groupName,
'account_number' => $this->accountNumber,
'period' => $this->period
]);
// Tentukan model berdasarkan group name
$modelClass = $this->getModelByGroup();
// Build query menggunakan pure Eloquent dengan eager loading
$query = $modelClass::with([
'ft' => function($query) {
$query->select([
'_id',
'ref_no',
'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',
'merchant_id',
'term_id',
'date_time'
]);
},
'dc' => function($query) {
$query->select([
'id',
'date_time'
]);
}
])
->select([
'id',
'trans_reference',
'booking_date',
'amount_lcy',
'date_time'
])
->where('account_number', $this->accountNumber)
->where('booking_date', $this->period)
->orderBy('booking_date')
->orderBy('date_time');
Log::info('Transaction query built successfully using pure Eloquent', [
'model_class' => $modelClass,
'account_number' => $this->accountNumber,
'period' => $this->period
]);
return $query;
}
/**
* Get model class based on group name
* Mendapatkan class model berdasarkan group name
*/
private function getModelByGroup()
{
Log::info('Determining model by group', [
'group_name' => $this->groupName
]);
$model = $this->groupName === 'QRIS' ? StmtEntryDetail::class : StmtEntry::class;
Log::info('Model determined', [
'group_name' => $this->groupName,
'model_class' => $model
]);
return $model;
}
/**
* Process transaction data from ORM result
* Memproses data transaksi dari hasil ORM
*/
private function processTransactionData($transaction): array
{
Log::info('Processing transaction data', [
'trans_reference' => $transaction->trans_reference,
'has_ft_relation' => !is_null($transaction->ft),
'has_dc_relation' => !is_null($transaction->dc)
]);
// Hitung debit dan credit amount
$debitAmount = $transaction->amount_lcy < 0 ? abs($transaction->amount_lcy) : null;
$creditAmount = $transaction->amount_lcy > 0 ? $transaction->amount_lcy : null;
// Ambil date_time dari prioritas: ft -> dc -> stmt
$dateTime = $transaction->ft?->date_time ??
$transaction->dc?->date_time ??
$transaction->date_time;
$processedData = [
'trans_reference' => $transaction->trans_reference,
'booking_date' => $transaction->booking_date,
'amount_lcy' => $transaction->amount_lcy,
'debit_amount' => $debitAmount,
'credit_amount' => $creditAmount,
'date_time' => $dateTime,
// Data dari TempFundsTransfer melalui relasi
'debit_acct_no' => $transaction->ft?->debit_acct_no,
'debit_value_date' => $transaction->ft?->debit_value_date,
'credit_acct_no' => $transaction->ft?->credit_acct_no,
'bif_rcv_acct' => $transaction->ft?->bif_rcv_acct,
'bif_rcv_name' => $transaction->ft?->bif_rcv_name,
'credit_value_date' => $transaction->ft?->credit_value_date,
'at_unique_id' => $transaction->ft?->at_unique_id,
'bif_ref_no' => $transaction->ft?->bif_ref_no,
'atm_order_id' => $transaction->ft?->atm_order_id,
'recipt_no' => $transaction->ft?->recipt_no,
'api_iss_acct' => $transaction->ft?->api_iss_acct,
'api_benff_acct' => $transaction->ft?->api_benff_acct,
'authoriser' => $transaction->ft?->authoriser,
'remarks' => $transaction->ft?->remarks,
'payment_details' => $transaction->ft?->payment_details,
'ref_no' => $transaction->ft?->ref_no,
'merchant_id' => $transaction->ft?->merchant_id,
'term_id' => $transaction->ft?->term_id,
];
Log::info('Transaction data processed successfully', [
'trans_reference' => $transaction->trans_reference,
'final_date_time' => $dateTime,
'debit_amount' => $debitAmount,
'credit_amount' => $creditAmount
]);
return $processedData;
}
/**
* Updated generateReportData method using pure ORM
* Method generateReportData yang diperbarui menggunakan ORM murni
*/
private function generateReportData(): array
{
Log::info('Starting report data generation using pure ORM', [
'account_number' => $this->accountNumber,
'period' => $this->period,
'opening_balance' => $openingBalance
'group_name' => $this->groupName,
'chunk_size' => $this->chunkSize
]);
$reportData = [];
$runningBalance = $openingBalance;
$sequenceNo = 0;
$runningBalance = $this->getOpeningBalance();
$sequenceNo = 1;
// Query berdasarkan SQL yang diberikan user
$query = DB::table('stmt_entry as s')
->leftJoin('temp_funds_transfer as ft', 'ft._id', '=', 's.trans_reference')
->leftJoin('data_captures as dc', 'dc.id', '=', 's.trans_reference')
->select([
'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'
])
->where('s.account_number', $this->accountNumber)
->where('s.booking_date', $this->period)
->orderBy('s.booking_date')
->orderBy('date_time');
try {
DB::beginTransaction();
// Process data in chunks
$query->chunk($this->chunkSize, function ($transactions) use (&$reportData, &$runningBalance, &$sequenceNo) {
foreach ($transactions as $transaction) {
$sequenceNo++;
// Build query menggunakan pure ORM
$query = $this->buildTransactionQuery();
// Calculate running balance
$runningBalance += (float) $transaction->amount_lcy;
// Process data dalam chunks untuk efisiensi memory
$query->chunk($this->chunkSize, function ($transactions) use (&$reportData, &$runningBalance, &$sequenceNo) {
Log::info('Processing transaction chunk', [
'chunk_size' => $transactions->count(),
'current_sequence' => $sequenceNo,
'current_balance' => $runningBalance
]);
// Format transaction date
$transactionDate = $this->formatDateTime($transaction->date_time);
foreach ($transactions as $transaction) {
// Process transaction data
$processedData = $this->processTransactionData($transaction);
$reportData[] = [
'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
];
}
});
// Update running balance
$amount = (float) $transaction->amount_lcy;
$runningBalance += $amount;
Log::info('Report data generated', [
'total_records' => count($reportData),
'final_balance' => $runningBalance
]);
// Format transaction date
$transactionDate = $this->formatDateTime($processedData['date_time']);
// Build report data row
$reportData[] = $this->buildReportDataRow(
(object) $processedData,
$sequenceNo,
$transactionDate,
$runningBalance
);
$sequenceNo++;
}
Log::info('Chunk processed successfully', [
'processed_count' => $transactions->count(),
'total_records_so_far' => count($reportData),
'current_balance' => $runningBalance
]);
});
DB::commit();
Log::info('Report data generation completed using pure ORM', [
'total_records' => count($reportData),
'final_balance' => $runningBalance,
'final_sequence' => $sequenceNo - 1
]);
} catch (Exception $e) {
DB::rollback();
Log::error('Error generating report data using pure ORM', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'account_number' => $this->accountNumber,
'period' => $this->period
]);
throw $e;
}
return $reportData;
}
/**
* 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

View File

@@ -162,17 +162,20 @@ class ProcessStmtEntryDetailDataJob implements ShouldQueue
}
$headers = (new StmtEntryDetail())->getFillable();
// Tambahkan field 'id' ke headers untuk menangani kolom tambahan di akhir CSV
$expectedHeaders = array_merge($headers, ['id']);
$rowCount = 0;
$chunkCount = 0;
Log::info('Memulai proses file', [
'file_path' => $filePath,
'headers_count' => count($headers)
'headers_count' => count($headers),
'expected_headers_count' => count($expectedHeaders)
]);
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
$rowCount++;
$this->processRow($row, $headers, $rowCount, $filePath);
$this->processRow($row, $expectedHeaders, $rowCount, $filePath);
// Process in chunks to avoid memory issues
if (count($this->entryBatch) >= self::CHUNK_SIZE) {
@@ -192,39 +195,67 @@ class ProcessStmtEntryDetailDataJob implements ShouldQueue
}
/**
* Proses setiap baris data
* Proses setiap baris data dengan penanganan field id tambahan
*
* @param array $row Data baris
* @param array $headers Header kolom
* @param array $expectedHeaders Header kolom yang diharapkan (termasuk id)
* @param int $rowCount Nomor baris
* @param string $filePath Path file
* @return void
*/
private function processRow(array $row, array $headers, int $rowCount, string $filePath): void
private function processRow(array $row, array $expectedHeaders, int $rowCount, string $filePath): void
{
if (count($headers) !== count($row)) {
// Validasi jumlah kolom - sekarang menggunakan expectedHeaders yang sudah include field 'id'
if (count($expectedHeaders) !== count($row)) {
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
count($headers) . ", Got: " . count($row));
count($expectedHeaders) . ", Got: " . count($row));
$this->errorCount++;
return;
}
$data = array_combine($headers, $row);
// Kombinasikan data dengan headers
$data = array_combine($expectedHeaders, $row);
// Log untuk debugging struktur data
Log::debug('Processing row data', [
'row_count' => $rowCount,
'stmt_entry_id' => $data['stmt_entry_id'] ?? 'not_set',
'id' => $data['id'] ?? 'not_set'
]);
// Logika untuk menggunakan field 'id' sebagai fallback jika stmt_entry_id kosong
$this->handleStmtEntryIdFallback($data);
// Hapus field 'id' dari data sebelum disimpan karena tidak ada di fillable model
unset($data['id']);
$this->cleanTransReference($data);
$this->addToBatch($data, $rowCount, $filePath);
}
/**
* Bersihkan trans_reference dari karakter tidak diinginkan
* Menangani logika fallback untuk stmt_entry_id menggunakan field id
*
* @param array $data Data yang akan dibersihkan
* @param array $data Data yang akan diproses
* @return void
*/
private function cleanTransReference(array &$data): void
private function handleStmtEntryIdFallback(array &$data): void
{
if (isset($data['trans_reference'])) {
// Clean trans_reference from \\BNK if present
$data['trans_reference'] = preg_replace('/\\\\.*$/', '', $data['trans_reference']);
Log::debug('Trans reference cleaned', ['original' => $data['trans_reference']]);
// Jika stmt_entry_id kosong atau null, gunakan value dari field 'id'
if (empty($data['stmt_entry_id']) || $data['stmt_entry_id'] === '' || $data['stmt_entry_id'] === null) {
if (isset($data['id']) && !empty($data['id'])) {
$data['stmt_entry_id'] = $data['id'];
Log::info('Using id as stmt_entry_id fallback', [
'original_stmt_entry_id' => $data['stmt_entry_id'] ?? 'empty',
'fallback_id' => $data['id']
]);
} else {
Log::warning('Both stmt_entry_id and id are empty', [
'stmt_entry_id' => $data['stmt_entry_id'] ?? 'not_set',
'id' => $data['id'] ?? 'not_set'
]);
}
}
}
@@ -239,7 +270,11 @@ class ProcessStmtEntryDetailDataJob implements ShouldQueue
private function addToBatch(array $data, int $rowCount, string $filePath): void
{
try {
if (isset($data['stmt_entry_id']) && $data['stmt_entry_id'] !== 'stmt_entry_id') {
// Validasi bahwa stmt_entry_id tidak kosong dan bukan header
if (isset($data['stmt_entry_id']) &&
$data['stmt_entry_id'] !== 'stmt_entry_id' &&
!empty($data['stmt_entry_id'])) {
// Add timestamp fields
$now = now();
$data['created_at'] = $now;
@@ -249,7 +284,16 @@ class ProcessStmtEntryDetailDataJob implements ShouldQueue
$this->entryBatch[] = $data;
$this->processedCount++;
Log::debug('Record added to batch', ['row' => $rowCount, 'stmt_entry_id' => $data['stmt_entry_id']]);
Log::debug('Record added to batch', [
'row' => $rowCount,
'stmt_entry_id' => $data['stmt_entry_id']
]);
} else {
Log::warning('Skipping row due to invalid stmt_entry_id', [
'row' => $rowCount,
'stmt_entry_id' => $data['stmt_entry_id'] ?? 'not_set'
]);
$this->errorCount++;
}
} catch (Exception $e) {
$this->errorCount++;
@@ -257,6 +301,21 @@ class ProcessStmtEntryDetailDataJob implements ShouldQueue
}
}
/**
* Bersihkan trans_reference dari karakter tidak diinginkan
*
* @param array $data Data yang akan dibersihkan
* @return void
*/
private function cleanTransReference(array &$data): void
{
if (isset($data['trans_reference'])) {
// Clean trans_reference from \\BNK if present
$data['trans_reference'] = preg_replace('/\\\\.*$/', '', $data['trans_reference']);
Log::debug('Trans reference cleaned', ['original' => $data['trans_reference']]);
}
}
/**
* Simpan batch data ke database menggunakan updateOrCreate
* untuk menghindari error unique constraint

View File

@@ -130,6 +130,11 @@
padding: 5px;
text-align: left;
font-size: 10px;
word-wrap: break-word;
word-break: break-word;
white-space: normal;
overflow-wrap: break-word;
hyphens: auto;
}
table th {
@@ -278,7 +283,7 @@
$totalDebit = 0;
$totalKredit = 0;
$line = 1;
$linePerPage = 26;
$linePerPage = 23;
@endphp
@php
// Hitung tanggal periode berdasarkan $period