feat(webstatement): implementasi job processing untuk laporan closing balance
Menambahkan fitur job processing untuk memproses laporan closing balance secara asynchronous dengan dukungan data besar. Perubahan yang dilakukan: - Membuat model `ClosingBalanceReportLog` untuk mencatat permintaan laporan dan status proses - Membuat job `GenerateClosingBalanceReportJob` untuk memproses laporan closing balance di background queue - Memodifikasi `LaporanClosingBalanceController` untuk mengintegrasikan job processing saat generate laporan - Menambahkan migration `closing_balance_report_logs` untuk menyimpan log permintaan, path file, dan status - Menggunakan query custom dari input user untuk pengambilan data transaksi - Menambahkan field `closing_balance` yang dihitung otomatis (saldo awal + amount_lcy) - Mengimplementasikan chunking data untuk memproses transaksi dalam jumlah besar secara efisien - Menambahkan logging detail untuk memudahkan monitoring, debugging, dan audit trail - Menggunakan database transaction untuk menjaga konsistensi data selama proses job - Menambahkan fitur retry otomatis pada job jika terjadi kegagalan atau timeout - Mengekspor hasil laporan ke file CSV dengan delimiter pipe `|` untuk kebutuhan integrasi sistem lain - Menambahkan workflow approval untuk validasi laporan sebelum download - Implementasi download tracking dan manajemen file untuk memudahkan kontrol akses Tujuan perubahan: - Memungkinkan pemrosesan laporan closing balance dengan jumlah data besar secara efisien dan aman - Mengurangi beban proses synchronous pada server dengan pemanfaatan queue - Menyediakan audit trail lengkap untuk setiap proses generate laporan - Meningkatkan pengalaman pengguna dengan proses generate yang lebih responsif dan terkontrol
This commit is contained in:
386
app/Jobs/GenerateClosingBalanceReportJob.php
Normal file
386
app/Jobs/GenerateClosingBalanceReportJob.php
Normal file
@@ -0,0 +1,386 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
use Carbon\Carbon;
|
||||
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\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Webstatement\Models\AccountBalance;
|
||||
use Modules\Webstatement\Models\ClosingBalanceReportLog;
|
||||
use Modules\Webstatement\Models\StmtEntry;
|
||||
|
||||
/**
|
||||
* Job untuk generate laporan closing balance
|
||||
* Mengambil data transaksi dan menghitung closing balance
|
||||
*/
|
||||
class GenerateClosingBalanceReportJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $accountNumber;
|
||||
protected $period;
|
||||
protected $reportLogId;
|
||||
protected $chunkSize = 1000;
|
||||
protected $disk = 'local';
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @param string $accountNumber
|
||||
* @param string $period
|
||||
* @param int $reportLogId
|
||||
*/
|
||||
public function __construct(string $accountNumber, string $period, int $reportLogId)
|
||||
{
|
||||
$this->accountNumber = $accountNumber;
|
||||
$this->period = $period;
|
||||
$this->reportLogId = $reportLogId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
* Memproses data transaksi dan generate laporan closing balance
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$reportLog = ClosingBalanceReportLog::find($this->reportLogId);
|
||||
|
||||
if (!$reportLog) {
|
||||
Log::error('Closing balance report log not found', ['id' => $this->reportLogId]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Log::info('Starting closing balance report generation', [
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period,
|
||||
'report_log_id' => $this->reportLogId
|
||||
]);
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
// Update status to processing
|
||||
$reportLog->update([
|
||||
'status' => 'processing',
|
||||
'updated_at' => now()
|
||||
]);
|
||||
|
||||
// Get opening balance
|
||||
$openingBalance = $this->getOpeningBalance();
|
||||
|
||||
// Generate report data
|
||||
$reportData = $this->generateReportData($openingBalance);
|
||||
|
||||
// Export to CSV
|
||||
$filePath = $this->exportToCsv($reportData);
|
||||
|
||||
// Update report log with success
|
||||
$reportLog->update([
|
||||
'status' => 'completed',
|
||||
'file_path' => $filePath,
|
||||
'file_size' => Storage::disk($this->disk)->size($filePath),
|
||||
'record_count' => count($reportData),
|
||||
'updated_at' => now()
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
Log::info('Closing balance report generation completed successfully', [
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period,
|
||||
'file_path' => $filePath,
|
||||
'record_count' => count($reportData)
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollback();
|
||||
|
||||
Log::error('Error generating closing balance report', [
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
$reportLog->update([
|
||||
'status' => 'failed',
|
||||
'error_message' => $e->getMessage(),
|
||||
'updated_at' => now()
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get opening balance from account balance table
|
||||
* Mengambil saldo awal dari tabel account balance
|
||||
*/
|
||||
private function getOpeningBalance(): float
|
||||
{
|
||||
Log::info('Getting opening balance', [
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period
|
||||
]);
|
||||
|
||||
$accountBalance = AccountBalance::where('account_number', $this->accountNumber)
|
||||
->where('period', $this->period)
|
||||
->first();
|
||||
|
||||
if (!$accountBalance) {
|
||||
Log::warning('Account balance not found, using 0 as opening balance', [
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period
|
||||
]);
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$openingBalance = (float) $accountBalance->actual_balance;
|
||||
|
||||
Log::info('Opening balance retrieved', [
|
||||
'account_number' => $this->accountNumber,
|
||||
'opening_balance' => $openingBalance
|
||||
]);
|
||||
|
||||
return $openingBalance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate report data based on the provided SQL query
|
||||
* Menggenerate data laporan berdasarkan query yang diberikan
|
||||
*/
|
||||
private function generateReportData(float $openingBalance): array
|
||||
{
|
||||
Log::info('Generating closing balance report data', [
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period,
|
||||
'opening_balance' => $openingBalance
|
||||
]);
|
||||
|
||||
$reportData = [];
|
||||
$runningBalance = $openingBalance;
|
||||
$sequenceNo = 0;
|
||||
|
||||
// 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');
|
||||
|
||||
// Process data in chunks
|
||||
$query->chunk($this->chunkSize, function ($transactions) use (&$reportData, &$runningBalance, &$sequenceNo) {
|
||||
foreach ($transactions as $transaction) {
|
||||
$sequenceNo++;
|
||||
|
||||
// Calculate running balance
|
||||
$runningBalance += (float) $transaction->amount_lcy;
|
||||
|
||||
// Format transaction date
|
||||
$transactionDate = $this->formatDateTime($transaction->date_time);
|
||||
|
||||
$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
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
Log::info('Report data generated', [
|
||||
'total_records' => count($reportData),
|
||||
'final_balance' => $runningBalance
|
||||
]);
|
||||
|
||||
return $reportData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format datetime string
|
||||
* Memformat string datetime
|
||||
*/
|
||||
private function formatDateTime(?string $datetime): string
|
||||
{
|
||||
if (!$datetime) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
return Carbon::createFromFormat('ymdHi', $datetime)->format('d/m/Y H:i');
|
||||
} catch (Exception $e) {
|
||||
Log::warning('Error formatting datetime', [
|
||||
'datetime' => $datetime,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
return $datetime;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export report data to CSV file
|
||||
* Export data laporan ke file CSV
|
||||
*/
|
||||
private function exportToCsv(array $reportData): string
|
||||
{
|
||||
Log::info('Starting CSV export for closing balance report', [
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period,
|
||||
'record_count' => count($reportData)
|
||||
]);
|
||||
|
||||
// Create directory structure
|
||||
$basePath = "closing_balance_reports";
|
||||
$accountPath = "{$basePath}/{$this->accountNumber}";
|
||||
|
||||
Storage::disk($this->disk)->makeDirectory($basePath);
|
||||
Storage::disk($this->disk)->makeDirectory($accountPath);
|
||||
|
||||
// Generate filename
|
||||
$fileName = "closing_balance_{$this->accountNumber}_{$this->period}.csv";
|
||||
$filePath = "{$accountPath}/{$fileName}";
|
||||
|
||||
// Delete existing file if exists
|
||||
if (Storage::disk($this->disk)->exists($filePath)) {
|
||||
Storage::disk($this->disk)->delete($filePath);
|
||||
}
|
||||
|
||||
// Create CSV header
|
||||
$csvHeader = [
|
||||
'NO',
|
||||
'TRANS_REFERENCE',
|
||||
'BOOKING_DATE',
|
||||
'TRANSACTION_DATE',
|
||||
'AMOUNT_LCY',
|
||||
'DEBIT_ACCT_NO',
|
||||
'DEBIT_VALUE_DATE',
|
||||
'DEBIT_AMOUNT',
|
||||
'CREDIT_ACCT_NO',
|
||||
'BIF_RCV_ACCT',
|
||||
'BIF_RCV_NAME',
|
||||
'CREDIT_VALUE_DATE',
|
||||
'CREDIT_AMOUNT',
|
||||
'AT_UNIQUE_ID',
|
||||
'BIF_REF_NO',
|
||||
'ATM_ORDER_ID',
|
||||
'RECIPT_NO',
|
||||
'API_ISS_ACCT',
|
||||
'API_BENFF_ACCT',
|
||||
'AUTHORISER',
|
||||
'REMARKS',
|
||||
'PAYMENT_DETAILS',
|
||||
'REF_NO',
|
||||
'MERCHANT_ID',
|
||||
'TERM_ID',
|
||||
'CLOSING_BALANCE'
|
||||
];
|
||||
|
||||
$csvContent = implode('|', $csvHeader) . "\n";
|
||||
|
||||
// Add data rows
|
||||
foreach ($reportData as $row) {
|
||||
$csvRow = [
|
||||
$row['sequence_no'],
|
||||
$row['trans_reference'] ?? '',
|
||||
$row['booking_date'] ?? '',
|
||||
$row['transaction_date'] ?? '',
|
||||
$row['amount_lcy'] ?? '',
|
||||
$row['debit_acct_no'] ?? '',
|
||||
$row['debit_value_date'] ?? '',
|
||||
$row['debit_amount'] ?? '',
|
||||
$row['credit_acct_no'] ?? '',
|
||||
$row['bif_rcv_acct'] ?? '',
|
||||
$row['bif_rcv_name'] ?? '',
|
||||
$row['credit_value_date'] ?? '',
|
||||
$row['credit_amount'] ?? '',
|
||||
$row['at_unique_id'] ?? '',
|
||||
$row['bif_ref_no'] ?? '',
|
||||
$row['atm_order_id'] ?? '',
|
||||
$row['recipt_no'] ?? '',
|
||||
$row['api_iss_acct'] ?? '',
|
||||
$row['api_benff_acct'] ?? '',
|
||||
$row['authoriser'] ?? '',
|
||||
$row['remarks'] ?? '',
|
||||
$row['payment_details'] ?? '',
|
||||
$row['ref_no'] ?? '',
|
||||
$row['merchant_id'] ?? '',
|
||||
$row['term_id'] ?? '',
|
||||
$row['closing_balance'] ?? ''
|
||||
];
|
||||
|
||||
$csvContent .= implode('|', $csvRow) . "\n";
|
||||
}
|
||||
|
||||
// Save file
|
||||
Storage::disk($this->disk)->put($filePath, $csvContent);
|
||||
|
||||
// Verify file creation
|
||||
if (!Storage::disk($this->disk)->exists($filePath)) {
|
||||
throw new Exception("Failed to create CSV file: {$filePath}");
|
||||
}
|
||||
|
||||
Log::info('CSV export completed successfully', [
|
||||
'file_path' => $filePath,
|
||||
'file_size' => Storage::disk($this->disk)->size($filePath)
|
||||
]);
|
||||
|
||||
return $filePath;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user