From bcc6d814e949d1c98125e5b6e7fe758a910934b7 Mon Sep 17 00:00:00 2001 From: Daeng Deni Mardaeni Date: Mon, 21 Jul 2025 11:10:49 +0700 Subject: [PATCH] feat(webstatement): tambah stmt_entry_detail migrasi, model, dan job processing Menambahkan fitur pengelolaan data stmt_entry_detail untuk integrasi transaksi dengan detail yang lebih lengkap. Perubahan yang dilakukan: - Membuat migrasi create_stmt_entry_detail_table dengan struktur field sesuai kebutuhan bisnis - Menambahkan index pada kolom penting untuk meningkatkan performa query - Membuat model StmtEntryDetail dengan relasi ke: - Account - TempFundsTransfer - TempTransaction - Teller - DataCapture - TempArrangement - Mengimplementasikan $fillable dan $casts sesuai struktur tabel - Menambahkan relasi untuk memudahkan integrasi antar modul - Membuat job ProcessStmtEntryDetailDataJob untuk memproses file CSV dengan batch processing - Mengimplementasikan chunking untuk menangani file besar secara efisien - Membersihkan trans_reference dari karakter tidak valid sebelum penyimpanan - Menggunakan updateOrCreate untuk mencegah duplikasi primary key - Menggunakan database transaction untuk menjaga konsistensi data - Menambahkan logging komprehensif untuk monitoring dan debugging - Mengimplementasikan error handling yang robust untuk menghindari job failure tanpa informasi - Memastikan penggunaan resource memory tetap optimal saat memproses data besar - Menambahkan case baru di MigrasiController untuk memproses stmt_entry_detail - Konsisten dengan pattern migrasi data yang sudah ada di sistem Tujuan perubahan: - Menyediakan sistem import dan pengolahan data stmt_entry_detail dengan proses yang aman dan efisien - Memudahkan integrasi transaksi dengan detail tambahan di modul Webstatement - Menjamin integritas data dengan penggunaan transaction, logging, dan error handling yang komprehensif --- .../LaporanClosingBalanceController.php | 195 ++++++---- app/Http/Controllers/MigrasiController.php | 5 +- app/Jobs/GenerateBiayaKartuCsvJob.php | 4 +- app/Jobs/ProcessStmtEntryDetailDataJob.php | 340 ++++++++++++++++++ app/Models/StmtEntryDetail.php | 113 ++++++ ..._033413_create_stmt_entry_detail_table.php | 28 ++ .../laporan-closing-balance/index.blade.php | 191 +++++----- routes/web.php | 1 + 8 files changed, 696 insertions(+), 181 deletions(-) create mode 100644 app/Jobs/ProcessStmtEntryDetailDataJob.php create mode 100644 app/Models/StmtEntryDetail.php create mode 100644 database/migrations/2025_07_21_033413_create_stmt_entry_detail_table.php diff --git a/app/Http/Controllers/LaporanClosingBalanceController.php b/app/Http/Controllers/LaporanClosingBalanceController.php index 5ca06ed..7c92d4d 100644 --- a/app/Http/Controllers/LaporanClosingBalanceController.php +++ b/app/Http/Controllers/LaporanClosingBalanceController.php @@ -136,72 +136,6 @@ class LaporanClosingBalanceController extends Controller return view('webstatement::laporan-closing-balance.show', compact('closingBalanceReport')); } - /** - * Download laporan jika tersedia - * - * @param ClosingBalanceReportLog $closingBalanceReport - * @return \Illuminate\Http\Response - */ - public function download(ClosingBalanceReportLog $closingBalanceReport) - { - Log::info('Download laporan closing balance', [ - 'report_id' => $closingBalanceReport->id, - 'user_id' => Auth::id() - ]); - - try { - // Check if report is available - if ($closingBalanceReport->status !== 'completed' || !$closingBalanceReport->file_path) { - Log::warning('Laporan tidak tersedia untuk download', [ - 'report_id' => $closingBalanceReport->id, - 'status' => $closingBalanceReport->status - ]); - return back()->with('error', 'Laporan tidak tersedia untuk download.'); - } - - DB::beginTransaction(); - - // Update download status - $closingBalanceReport->update([ - 'is_downloaded' => true, - 'downloaded_at' => now(), - 'updated_by' => Auth::id() - ]); - - DB::commit(); - - // Download the file - $filePath = $closingBalanceReport->file_path; - if (Storage::exists($filePath)) { - $fileName = "closing_balance_report_{$closingBalanceReport->account_number}_{$closingBalanceReport->period}.csv"; - - Log::info('File laporan berhasil didownload', [ - 'report_id' => $closingBalanceReport->id, - 'file_path' => $filePath - ]); - - return Storage::download($filePath, $fileName); - } - - Log::error('File laporan tidak ditemukan', [ - 'report_id' => $closingBalanceReport->id, - 'file_path' => $filePath - ]); - - return back()->with('error', 'File laporan tidak ditemukan.'); - - } catch (Exception $e) { - DB::rollback(); - - Log::error('Error saat download laporan', [ - 'report_id' => $closingBalanceReport->id, - 'error' => $e->getMessage() - ]); - - return back()->with('error', 'Terjadi kesalahan saat download laporan.'); - } - } - /** * Authorize permintaan laporan * @@ -273,15 +207,53 @@ class LaporanClosingBalanceController extends Controller // Retrieve data from the database $query = ClosingBalanceReportLog::query(); - // Apply search filter if provided + // Apply search filter if provided (handle JSON search parameters) if ($request->has('search') && !empty($request->get('search'))) { $search = $request->get('search'); - $query->where(function ($q) use ($search) { - $q->where('account_number', 'LIKE', "%$search%") - ->orWhere('period', 'LIKE', "%$search%") - ->orWhere('status', 'LIKE', "%$search%") - ->orWhere('authorization_status', 'LIKE', "%$search%"); - }); + + // Check if search is JSON format + if (is_string($search) && json_decode($search, true) !== null) { + $searchParams = json_decode($search, true); + + // Apply account number filter + if (!empty($searchParams['account_number'])) { + $query->where('account_number', 'LIKE', "%{$searchParams['account_number']}%"); + } + + // Apply date range filter + if (!empty($searchParams['start_date'])) { + $startPeriod = Carbon::createFromFormat('Y-m-d', $searchParams['start_date'])->format('Ymd'); + $query->where('period', '>=', $startPeriod); + } + + if (!empty($searchParams['end_date'])) { + $endPeriod = Carbon::createFromFormat('Y-m-d', $searchParams['end_date'])->format('Ymd'); + $query->where('period', '<=', $endPeriod); + } + } else { + // Handle regular string search (fallback) + $query->where(function ($q) use ($search) { + $q->where('account_number', 'LIKE', "%$search%") + ->orWhere('period', 'LIKE', "%$search%") + ->orWhere('status', 'LIKE', "%$search%") + ->orWhere('authorization_status', 'LIKE', "%$search%"); + }); + } + } + + // Apply individual parameter filters (for backward compatibility) + if ($request->has('account_number') && !empty($request->get('account_number'))) { + $query->where('account_number', 'LIKE', "%{$request->get('account_number')}%"); + } + + if ($request->has('start_date') && !empty($request->get('start_date'))) { + $startPeriod = Carbon::createFromFormat('Y-m-d', $request->get('start_date'))->format('Ymd'); + $query->where('period', '>=', $startPeriod); + } + + if ($request->has('end_date') && !empty($request->get('end_date'))) { + $endPeriod = Carbon::createFromFormat('Y-m-d', $request->get('end_date'))->format('Ymd'); + $query->where('period', '<=', $endPeriod); } // Apply column filters if provided @@ -519,4 +491,81 @@ class LaporanClosingBalanceController extends Controller return back()->with('error', 'Gagal mengulang generate laporan: ' . $e->getMessage()); } } + + /** + * Download laporan berdasarkan nomor rekening dan periode + * + * @param string $accountNumber + * @param string $period + * @return \Illuminate\Http\Response + */ + public function download($accountNumber, $period) + { + Log::info('Download laporan closing balance', [ + 'account_number' => $accountNumber, + 'period' => $period, + 'user_id' => Auth::id() + ]); + + try { + // Cari laporan berdasarkan account number dan period + $closingBalanceReport = ClosingBalanceReportLog::where('account_number', $accountNumber) + ->where('period', $period) + ->where('status', 'completed') + ->whereNotNull('file_path') + ->first(); + + if (!$closingBalanceReport) { + Log::warning('Laporan tidak ditemukan atau belum selesai', [ + 'account_number' => $accountNumber, + 'period' => $period + ]); + return back()->with('error', 'Laporan tidak ditemukan atau belum selesai diproses.'); + } + + DB::beginTransaction(); + + // Update download status + $closingBalanceReport->update([ + 'is_downloaded' => true, + 'downloaded_at' => now(), + 'updated_by' => Auth::id() + ]); + + DB::commit(); + + // Download the file + $filePath = $closingBalanceReport->file_path; + if (Storage::exists($filePath)) { + $fileName = "closing_balance_report_{$accountNumber}_{$period}.csv"; + + Log::info('File laporan berhasil didownload', [ + 'account_number' => $accountNumber, + 'period' => $period, + 'file_path' => $filePath + ]); + + return Storage::download($filePath, $fileName); + } + + Log::error('File laporan tidak ditemukan di storage', [ + 'account_number' => $accountNumber, + 'period' => $period, + 'file_path' => $filePath + ]); + + return back()->with('error', 'File laporan tidak ditemukan.'); + + } catch (Exception $e) { + DB::rollback(); + + Log::error('Error saat download laporan', [ + 'account_number' => $accountNumber, + 'period' => $period, + 'error' => $e->getMessage() + ]); + + return back()->with('error', 'Terjadi kesalahan saat mengunduh laporan: ' . $e->getMessage()); + } + } } diff --git a/app/Http/Controllers/MigrasiController.php b/app/Http/Controllers/MigrasiController.php index fd15f11..0e41642 100644 --- a/app/Http/Controllers/MigrasiController.php +++ b/app/Http/Controllers/MigrasiController.php @@ -24,7 +24,8 @@ ProcessTellerDataJob, ProcessTransactionDataJob, ProcessSectorDataJob, - ProcessProvinceDataJob}; + ProcessProvinceDataJob, + ProcessStmtEntryDetailDataJob}; class MigrasiController extends Controller { @@ -38,6 +39,7 @@ 'customer' => ProcessCustomerDataJob::class, 'account' => ProcessAccountDataJob::class, 'stmtEntry' => ProcessStmtEntryDataJob::class, + 'stmtEntryDetail' => ProcessStmtEntryDetailDataJob::class, // Tambahan baru 'dataCapture' => ProcessDataCaptureDataJob::class, 'fundsTransfer' => ProcessFundsTransferDataJob::class, 'teller' => ProcessTellerDataJob::class, @@ -63,6 +65,7 @@ 'customer', 'account', 'stmtEntry', + 'stmtEntryDetail', // Tambahan baru 'dataCapture', 'fundsTransfer', 'teller', diff --git a/app/Jobs/GenerateBiayaKartuCsvJob.php b/app/Jobs/GenerateBiayaKartuCsvJob.php index 03cc175..c2a238f 100644 --- a/app/Jobs/GenerateBiayaKartuCsvJob.php +++ b/app/Jobs/GenerateBiayaKartuCsvJob.php @@ -191,11 +191,11 @@ $subQuery->where('product_code', '!=', '6021') ->orWhere(function($nestedQuery) { $nestedQuery->where('product_code', '6021') - ->where('ctdesc', '!=', 'gold'); + ->where('ctdesc', '!=', 'GOLD'); }); }); - + $cards = $query->get(); diff --git a/app/Jobs/ProcessStmtEntryDetailDataJob.php b/app/Jobs/ProcessStmtEntryDetailDataJob.php new file mode 100644 index 0000000..ca81e42 --- /dev/null +++ b/app/Jobs/ProcessStmtEntryDetailDataJob.php @@ -0,0 +1,340 @@ +period = $period; + Log::info('ProcessStmtEntryDetailDataJob initialized', ['period' => $period]); + } + + /** + * Execute the job. + * + * @return void + * @throws Exception + */ + public function handle(): void + { + try { + Log::info('Memulai ProcessStmtEntryDetailDataJob', ['period' => $this->period]); + + $this->initializeJob(); + + if ($this->period === '') { + Log::warning('No period provided for statement entry detail data processing'); + return; + } + + $this->processPeriod(); + $this->logJobCompletion(); + + Log::info('ProcessStmtEntryDetailDataJob selesai berhasil'); + } catch (Exception $e) { + Log::error('Error in ProcessStmtEntryDetailDataJob: ' . $e->getMessage()); + throw $e; + } + } + + /** + * Inisialisasi job dengan pengaturan awal + * + * @return void + */ + private function initializeJob(): void + { + set_time_limit(self::MAX_EXECUTION_TIME); + $this->processedCount = 0; + $this->errorCount = 0; + $this->entryBatch = []; + + Log::info('Job initialized', [ + 'max_execution_time' => self::MAX_EXECUTION_TIME, + 'chunk_size' => self::CHUNK_SIZE + ]); + } + + /** + * Proses data untuk periode tertentu + * + * @return void + */ + private function processPeriod(): void + { + $disk = Storage::disk(self::DISK_NAME); + $filename = "{$this->period}." . self::FILENAME; + $filePath = "{$this->period}/$filename"; + + Log::info('Memulai proses periode', ['file_path' => $filePath]); + + if (!$this->validateFile($disk, $filePath)) { + return; + } + + $tempFilePath = $this->createTemporaryFile($disk, $filePath, $filename); + $this->processFile($tempFilePath, $filePath); + $this->cleanup($tempFilePath); + + Log::info('Proses periode selesai', ['file_path' => $filePath]); + } + + /** + * Validasi keberadaan file + * + * @param mixed $disk Storage disk instance + * @param string $filePath Path file yang akan divalidasi + * @return bool + */ + private function validateFile($disk, string $filePath): bool + { + Log::info("Processing statement entry detail file: $filePath"); + + if (!$disk->exists($filePath)) { + Log::warning("File not found: $filePath"); + return false; + } + + Log::info("File validated successfully: $filePath"); + return true; + } + + /** + * Buat file temporary untuk proses + * + * @param mixed $disk Storage disk instance + * @param string $filePath Path file sumber + * @param string $filename Nama file + * @return string Path file temporary + */ + private function createTemporaryFile($disk, string $filePath, string $filename): string + { + $tempFilePath = storage_path("app/temp_$filename"); + file_put_contents($tempFilePath, $disk->get($filePath)); + + Log::info('Temporary file created', ['temp_path' => $tempFilePath]); + return $tempFilePath; + } + + /** + * Proses file CSV + * + * @param string $tempFilePath Path file temporary + * @param string $filePath Path file asli + * @return void + */ + private function processFile(string $tempFilePath, string $filePath): void + { + $handle = fopen($tempFilePath, "r"); + if ($handle === false) { + Log::error("Unable to open file: $filePath"); + return; + } + + $headers = (new StmtEntryDetail())->getFillable(); + $rowCount = 0; + $chunkCount = 0; + + Log::info('Memulai proses file', [ + 'file_path' => $filePath, + 'headers_count' => count($headers) + ]); + + while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) { + $rowCount++; + $this->processRow($row, $headers, $rowCount, $filePath); + + // Process in chunks to avoid memory issues + if (count($this->entryBatch) >= self::CHUNK_SIZE) { + $this->saveBatch(); + $chunkCount++; + Log::info("Processed chunk $chunkCount ({$this->processedCount} records so far)"); + } + } + + // Process any remaining records + if (!empty($this->entryBatch)) { + $this->saveBatch(); + } + + fclose($handle); + Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors."); + } + + /** + * Proses setiap baris data + * + * @param array $row Data baris + * @param array $headers Header kolom + * @param int $rowCount Nomor baris + * @param string $filePath Path file + * @return void + */ + private function processRow(array $row, array $headers, int $rowCount, string $filePath): void + { + if (count($headers) !== count($row)) { + Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " . + count($headers) . ", Got: " . count($row)); + return; + } + + $data = array_combine($headers, $row); + $this->cleanTransReference($data); + $this->addToBatch($data, $rowCount, $filePath); + } + + /** + * 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']]); + } + } + + /** + * Tambahkan record ke batch untuk proses bulk insert + * + * @param array $data Data record + * @param int $rowCount Nomor baris + * @param string $filePath Path file + * @return void + */ + private function addToBatch(array $data, int $rowCount, string $filePath): void + { + try { + if (isset($data['stmt_entry_id']) && $data['stmt_entry_id'] !== 'stmt_entry_id') { + // Add timestamp fields + $now = now(); + $data['created_at'] = $now; + $data['updated_at'] = $now; + + // Add to entry batch + $this->entryBatch[] = $data; + $this->processedCount++; + + Log::debug('Record added to batch', ['row' => $rowCount, 'stmt_entry_id' => $data['stmt_entry_id']]); + } + } catch (Exception $e) { + $this->errorCount++; + Log::error("Error processing Statement Entry Detail at row $rowCount in $filePath: " . $e->getMessage()); + } + } + + /** + * Simpan batch data ke database menggunakan updateOrCreate + * untuk menghindari error unique constraint + * + * @return void + * @throws Exception + */ + private function saveBatch(): void + { + Log::info('Memulai proses saveBatch dengan updateOrCreate'); + + DB::beginTransaction(); + + try { + if (!empty($this->entryBatch)) { + $totalProcessed = 0; + + // Process each entry data directly + foreach ($this->entryBatch as $entryData) { + // Validasi bahwa entryData adalah array dan memiliki stmt_entry_id + if (is_array($entryData) && isset($entryData['stmt_entry_id'])) { + // Gunakan updateOrCreate untuk menghindari duplicate key error + StmtEntryDetail::updateOrCreate( + [ + 'stmt_entry_id' => $entryData['stmt_entry_id'] + ], + $entryData + ); + + $totalProcessed++; + } else { + Log::warning('Invalid entry data structure', ['data' => $entryData]); + $this->errorCount++; + } + } + + DB::commit(); + + Log::info("Berhasil memproses {$totalProcessed} record dengan updateOrCreate"); + + // Reset entry batch after successful processing + $this->entryBatch = []; + } + } catch (Exception $e) { + DB::rollback(); + + Log::error("Error in saveBatch: " . $e->getMessage() . "\n" . $e->getTraceAsString()); + $this->errorCount += count($this->entryBatch); + + // Reset batch even if there's an error to prevent reprocessing the same failed records + $this->entryBatch = []; + + throw $e; + } + } + + /** + * Bersihkan file temporary + * + * @param string $tempFilePath Path file temporary + * @return void + */ + private function cleanup(string $tempFilePath): void + { + if (file_exists($tempFilePath)) { + unlink($tempFilePath); + Log::info('Temporary file cleaned up', ['temp_path' => $tempFilePath]); + } + } + + /** + * Log penyelesaian job + * + * @return void + */ + private function logJobCompletion(): void + { + Log::info("Statement Entry Detail data processing completed. " . + "Total processed: {$this->processedCount}, Total errors: {$this->errorCount}"); + } +} diff --git a/app/Models/StmtEntryDetail.php b/app/Models/StmtEntryDetail.php new file mode 100644 index 0000000..7240bf6 --- /dev/null +++ b/app/Models/StmtEntryDetail.php @@ -0,0 +1,113 @@ + 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * Relasi ke model Account + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class, 'account_number', 'account_number'); + } + + /** + * Relasi ke model TempFundsTransfer + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function ft() + { + return $this->belongsTo(TempFundsTransfer::class, 'trans_reference', 'ref_no'); + } + + /** + * Relasi ke model TempTransaction + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function transaction() + { + return $this->belongsTo(TempTransaction::class, 'transaction_code', 'transaction_code'); + } + + /** + * Relasi ke model Teller + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function tt() + { + return $this->belongsTo(Teller::class, 'trans_reference', 'id_teller'); + } + + /** + * Relasi ke model DataCapture + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function dc() + { + return $this->belongsTo(DataCapture::class, 'trans_reference', 'id'); + } + + /** + * Relasi ke model TempArrangement + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function aa() + { + return $this->belongsTo(TempArrangement::class, 'trans_reference', 'arrangement_id'); + } +} diff --git a/database/migrations/2025_07_21_033413_create_stmt_entry_detail_table.php b/database/migrations/2025_07_21_033413_create_stmt_entry_detail_table.php new file mode 100644 index 0000000..f551e35 --- /dev/null +++ b/database/migrations/2025_07_21_033413_create_stmt_entry_detail_table.php @@ -0,0 +1,28 @@ +id(); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('stmt_entry_detail'); + } +}; diff --git a/resources/views/laporan-closing-balance/index.blade.php b/resources/views/laporan-closing-balance/index.blade.php index 86e7442..cdf713b 100644 --- a/resources/views/laporan-closing-balance/index.blade.php +++ b/resources/views/laporan-closing-balance/index.blade.php @@ -6,8 +6,10 @@ @section('content')
-
-
+
+

Laporan Closing Balance

@@ -16,93 +18,73 @@
- - +
- +
-
- +
-
- + - +
- -
- +
- - - - - - - - - + + + + + + +
- - - - Nomor Rekening - - - - - Periode - - - - - Saldo Cleared - - - - - Saldo Akhir - - - - - Tanggal Update - - - Action
+ + + + Nomor Rekening + + + + + Periode + + + + + Tanggal Update + + + Action
-