Compare commits

...

7 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
Daeng Deni Mardaeni
f37707b2f6 Merge remote-tracking branch 'composer/master'
# Conflicts:
#	database/migrations/2025_07_21_033413_create_stmt_entry_detail_table.php
2025-07-21 11:22:41 +07:00
Daeng Deni Mardaeni
ad9780ccd6 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
2025-07-21 11:21:42 +07:00
Daeng Deni Mardaeni
bcc6d814e9 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
2025-07-21 11:10:49 +07:00
13 changed files with 1247 additions and 334 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

@@ -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());
}
}
}

View File

@@ -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',
@@ -98,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

@@ -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();

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

@@ -0,0 +1,399 @@
<?php
namespace Modules\Webstatement\Jobs;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Modules\Webstatement\Models\StmtEntryDetail;
use Illuminate\Support\Facades\DB;
class ProcessStmtEntryDetailDataJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private const CSV_DELIMITER = '~';
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
private const FILENAME = 'ST.STMT.ENTRY.DETAIL.csv';
private const DISK_NAME = 'sftpStatement';
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
private string $period = '';
private int $processedCount = 0;
private int $errorCount = 0;
private array $entryBatch = [];
/**
* Create a new job instance.
*
* @param string $period Periode data yang akan diproses
*/
public function __construct(string $period = '')
{
$this->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();
// 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),
'expected_headers_count' => count($expectedHeaders)
]);
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
$rowCount++;
$this->processRow($row, $expectedHeaders, $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 dengan penanganan field id tambahan
*
* @param array $row Data baris
* @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 $expectedHeaders, int $rowCount, string $filePath): void
{
// 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($expectedHeaders) . ", Got: " . count($row));
$this->errorCount++;
return;
}
// 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);
}
/**
* Menangani logika fallback untuk stmt_entry_id menggunakan field id
*
* @param array $data Data yang akan diproses
* @return void
*/
private function handleStmtEntryIdFallback(array &$data): void
{
// 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'
]);
}
}
}
/**
* 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 {
// 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;
$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']
]);
} 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++;
Log::error("Error processing Statement Entry Detail at row $rowCount in $filePath: " . $e->getMessage());
}
}
/**
* 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
*
* @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}");
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace Modules\Webstatement\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class StmtEntryDetail extends Model
{
use HasFactory;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'stmt_entry_detail';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'stmt_entry_id',
'account_number',
'company_code',
'amount_lcy',
'transaction_code',
'narrative',
'product_category',
'value_date',
'amount_fcy',
'exchange_rate',
'trans_reference',
'booking_date',
'stmt_no',
'date_time',
'currency',
'crf_type',
'consol_key',
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'created_at' => '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');
}
}

View File

@@ -0,0 +1,53 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateStmtEntryDetailTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('stmt_entry_detail', function (Blueprint $table) {
$table->id();
$table->string('stmt_entry_id')->nullable();
$table->string('account_number')->nullable();
$table->string('company_code')->nullable();
$table->string('amount_lcy')->nullable();
$table->string('transaction_code')->nullable();
$table->string('narrative')->nullable();
$table->string('product_category')->nullable();
$table->string('value_date')->nullable();
$table->string('amount_fcy')->nullable();
$table->string('exchange_rate')->nullable();
$table->string('trans_reference')->nullable();
$table->string('booking_date')->nullable();
$table->string('stmt_no')->nullable();
$table->string('date_time')->nullable();
$table->string('currency')->nullable();
$table->string('crf_type')->nullable();
$table->string('consol_key')->nullable();
$table->timestamps();
// Index untuk performa query
$table->index('stmt_entry_id');
$table->index('account_number');
$table->index('trans_reference');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('stmt_entry_detail');
}
}

View File

@@ -6,8 +6,10 @@
@section('content')
<div class="grid">
<div class="card card-grid min-w-full" data-datatable="false" data-datatable-page-size="10" data-datatable-state-save="false" id="laporan-closing-balance-table" data-api-url="{{ route('laporan-closing-balance.datatables') }}">
<div class="card-header py-5 flex-wrap">
<div class="min-w-full card card-grid" data-datatable="false" data-datatable-page-size="10"
data-datatable-state-save="false" id="laporan-closing-balance-table"
data-api-url="{{ route('laporan-closing-balance.datatables') }}">
<div class="flex-wrap py-5 card-header">
<h3 class="card-title">
Laporan Closing Balance
</h3>
@@ -16,93 +18,73 @@
<div class="flex flex-wrap gap-2.5 items-end">
<!-- Nomor Rekening Filter -->
<div class="flex flex-col">
<label class="text-sm font-medium text-gray-700 mb-1">Nomor Rekening</label>
<input type="text" id="account-number-filter" class="input w-[200px]" placeholder="Masukkan nomor rekening">
<input type="text" id="account-number-filter" class="input w-[200px]"
placeholder="Masukkan nomor rekening">
</div>
<!-- Tanggal Mulai Filter -->
<div class="flex flex-col">
<label class="text-sm font-medium text-gray-700 mb-1">Tanggal Mulai</label>
<input type="date" id="start-date-filter" class="input w-[150px]">
</div>
<!-- Tanggal Akhir Filter -->
<div class="flex flex-col">
<label class="text-sm font-medium text-gray-700 mb-1">Tanggal Akhir</label>
<input type="date" id="end-date-filter" class="input w-[150px]">
</div>
<!-- Tombol Filter -->
<button type="button" id="apply-filter" class="btn btn-primary">
<i class="ki-filled ki-magnifier"></i>
Filter
</button>
<!-- Tombol Reset -->
<button type="button" id="reset-filter" class="btn btn-light">
<i class="ki-filled ki-arrows-circle"></i>
Reset
</button>
</div>
<div class="flex flex-wrap gap-2.5">
<div class="h-[24px] border border-r-gray-200"></div>
<a class="btn btn-light" href="{{ route('laporan-closing-balance.export') }}" id="export-btn">
<i class="ki-filled ki-file-down"></i>
Export to CSV
</a>
</div>
</div>
</div>
<div class="card-body">
<div class="scrollable-x-auto">
<table class="table table-auto table-border align-middle text-gray-700 font-medium text-sm" data-datatable-table="true">
<table class="table text-sm font-medium text-gray-700 align-middle table-auto table-border"
data-datatable-table="true">
<thead>
<tr>
<th class="w-14">
<input class="checkbox checkbox-sm" data-datatable-check="true" type="checkbox"/>
</th>
<th class="min-w-[200px]" data-datatable-column="account_number">
<span class="sort">
<span class="sort-label">Nomor Rekening</span>
<span class="sort-icon"></span>
</span>
</th>
<th class="min-w-[150px]" data-datatable-column="period">
<span class="sort">
<span class="sort-label">Periode</span>
<span class="sort-icon"></span>
</span>
</th>
<th class="min-w-[150px]" data-datatable-column="cleared_balance">
<span class="sort">
<span class="sort-label">Saldo Cleared</span>
<span class="sort-icon"></span>
</span>
</th>
<th class="min-w-[150px]" data-datatable-column="actual_balance">
<span class="sort">
<span class="sort-label">Saldo Akhir</span>
<span class="sort-icon"></span>
</span>
</th>
<th class="min-w-[150px]" data-datatable-column="updated_at">
<span class="sort">
<span class="sort-label">Tanggal Update</span>
<span class="sort-icon"></span>
</span>
</th>
<th class="min-w-[100px] text-center" data-datatable-column="actions">Action</th>
</tr>
<tr>
<th class="w-14">
<input class="checkbox checkbox-sm" data-datatable-check="true" type="checkbox" />
</th>
<th class="min-w-[200px]" data-datatable-column="account_number">
<span class="sort">
<span class="sort-label">Nomor Rekening</span>
<span class="sort-icon"></span>
</span>
</th>
<th class="min-w-[150px]" data-datatable-column="period">
<span class="sort">
<span class="sort-label">Periode</span>
<span class="sort-icon"></span>
</span>
</th>
<th class="min-w-[150px]" data-datatable-column="updated_at">
<span class="sort">
<span class="sort-label">Tanggal Update</span>
<span class="sort-icon"></span>
</span>
</th>
<th class="min-w-[100px] text-center" data-datatable-column="actions">Action</th>
</tr>
</thead>
</table>
</div>
<div class="card-footer justify-center md:justify-between flex-col md:flex-row gap-3 text-gray-600 text-2sm font-medium">
<div class="flex items-center gap-2">
<div
class="flex-col gap-3 justify-center font-medium text-gray-600 card-footer md:justify-between md:flex-row text-2sm">
<div class="flex gap-2 items-center">
Show
<select class="select select-sm w-16" data-datatable-size="true" name="perpage"></select> per page
<select class="w-16 select select-sm" data-datatable-size="true" name="perpage"></select> per page
</div>
<div class="flex items-center gap-4">
<div class="flex gap-4 items-center">
<span data-datatable-info="true"></span>
<div class="pagination" data-datatable-pagination="true">
</div>
@@ -135,11 +117,11 @@
*/
function formatPeriod(period) {
if (!period || period.length !== 8) return period;
const year = period.substring(0, 4);
const month = period.substring(4, 6);
const day = period.substring(6, 8);
return `${day}/${month}/${year}`;
}
@@ -150,7 +132,7 @@
*/
function formatDate(dateString) {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleDateString('id-ID', {
year: 'numeric',
@@ -171,11 +153,25 @@
const exportBtn = document.getElementById('export-btn');
const apiUrl = element.getAttribute('data-api-url');
// Konfigurasi DataTable
// Set default date range (last 30 days) SEBELUM inisialisasi DataTable
const today = new Date();
const thirtyDaysAgo = new Date(today.getTime() - (30 * 24 * 60 * 60 * 1000));
endDateFilter.value = today.toISOString().split('T')[0];
startDateFilter.value = thirtyDaysAgo.toISOString().split('T')[0];
// Prepare initial filters
let initialFilters = {
start_date: startDateFilter.value,
end_date: endDateFilter.value
};
// Konfigurasi DataTable dengan filter awal
const dataTableOptions = {
apiEndpoint: apiUrl,
pageSize: 10,
searchParams: initialFilters, // Set filter awal di sini
columns: {
select: {
render: (item, data, context) => {
@@ -199,36 +195,26 @@
return `<span class="text-gray-700">${formatPeriod(data.period)}</span>`;
}
},
cleared_balance: {
title: 'Saldo Cleared',
render: (item, data) => {
const amount = parseFloat(data.cleared_balance) || 0;
return `<span class="text-blue-600 font-medium">${formatCurrency(amount)}</span>`;
}
},
actual_balance: {
title: 'Saldo Akhir',
render: (item, data) => {
const amount = parseFloat(data.actual_balance) || 0;
return `<span class="text-green-600 font-medium">${formatCurrency(amount)}</span>`;
}
},
updated_at: {
title: 'Tanggal Update',
render: (item, data) => {
return `<span class="text-gray-600 text-sm">${formatDate(data.updated_at)}</span>`;
return `<span class="text-sm text-gray-600">${formatDate(data.created_at)}</span>`;
}
},
actions: {
title: 'Action',
render: (item, data) => {
const downloadUrl =
`{{ route('laporan-closing-balance.download', ['accountNumber' => '__ACCOUNT__', 'period' => '__PERIOD__']) }}`
.replace('__ACCOUNT__', data.account_number)
.replace('__PERIOD__', data.period);
return `<div class="flex flex-nowrap justify-center">
<a class="btn btn-sm btn-icon btn-clear btn-info"
href="{{ route('laporan-closing-balance.show', ['accountNumber' => '__ACCOUNT__', 'period' => '__PERIOD__']) }}"
.replace('__ACCOUNT__', data.account_number)
.replace('__PERIOD__', data.period)
title="Lihat Detail">
<i class="ki-outline ki-eye"></i>
<a class="btn btn-sm btn-icon btn-clear btn-success"
href="${downloadUrl}"
title="Download Laporan"
download>
<i class="ki-outline ki-file-down"></i>
</a>
</div>`;
},
@@ -236,9 +222,11 @@
},
};
// Inisialisasi DataTable
// Inisialisasi DataTable dengan filter awal sudah terset
let dataTable = new KTDataTable(element, dataTableOptions);
dataTable.showSpinner();
// Update export URL dengan filter awal
updateExportUrl(initialFilters);
/**
* Fungsi untuk menerapkan filter
@@ -260,7 +248,7 @@
console.log('Applying filters:', filters);
dataTable.search(filters);
// Update export URL dengan filter
updateExportUrl(filters);
}
@@ -272,7 +260,7 @@
accountNumberFilter.value = '';
startDateFilter.value = '';
endDateFilter.value = '';
dataTable.search({});
updateExportUrl({});
}
@@ -281,15 +269,15 @@
* Fungsi untuk update URL export dengan parameter filter
*/
function updateExportUrl(filters) {
const baseUrl = '{{ route("laporan-closing-balance.export") }}';
const baseUrl = '{{ route('laporan-closing-balance.export') }}';
const params = new URLSearchParams();
Object.keys(filters).forEach(key => {
if (filters[key]) {
params.append(key, filters[key]);
}
});
const newUrl = params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl;
exportBtn.href = newUrl;
}
@@ -297,7 +285,7 @@
// Event listeners
applyFilterBtn.addEventListener('click', applyFilters);
resetFilterBtn.addEventListener('click', resetFilters);
// Auto apply filter saat enter di input
[accountNumberFilter, startDateFilter, endDateFilter].forEach(input => {
input.addEventListener('keypress', function(e) {
@@ -307,16 +295,9 @@
});
});
// Set default date range (last 30 days)
const today = new Date();
const thirtyDaysAgo = new Date(today.getTime() - (30 * 24 * 60 * 60 * 1000));
endDateFilter.value = today.toISOString().split('T')[0];
startDateFilter.value = thirtyDaysAgo.toISOString().split('T')[0];
// Apply default filter
setTimeout(() => {
applyFilters();
}, 100);
// HAPUS bagian ini yang menyebabkan double API call:
// setTimeout(() => {
// applyFilters();
// }, 100);
</script>
@endpush
@endpush

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

View File

@@ -117,6 +117,7 @@ Route::middleware(['auth'])->group(function () {
Route::group(['prefix' => 'laporan-closing-balance', 'as' => 'laporan-closing-balance.', 'middleware' => ['auth']], function () {
Route::get('/datatables', [LaporanClosingBalanceController::class, 'dataForDatatables'])->name('datatables');
Route::get('/export', [LaporanClosingBalanceController::class, 'export'])->name('export');
Route::get('/{accountNumber}/{period}/download', [LaporanClosingBalanceController::class, 'download'])->name('download');
Route::get('/{accountNumber}/{period}', [LaporanClosingBalanceController::class, 'show'])->name('show');
});
Route::resource('laporan-closing-balance', LaporanClosingBalanceController::class)->only(['index']);