feat(webstatement): tambah fungsi generatePdf ke ExportStatementPeriodJob

Perubahan yang dilakukan:
- Menambahkan fungsi generatePdf() untuk proses generate PDF dalam job ExportStatementPeriodJob.
- Mengintegrasikan logika PDF generation dari PrintStatementController ke dalam job.
- Menggunakan data ProcessedStatement yang telah diproses sebagai sumber untuk pembuatan PDF.
- Menambahkan import statement untuk Browsershot, Account, Customer, dan Branch.
- Mengimplementasikan fungsi prepareHeaderTableBackground() untuk mengonversi gambar header menjadi base64.
- Menggunakan database transaction untuk menjaga konsistensi saat generate dan menyimpan PDF.
- Menyimpan PDF ke storage dengan struktur direktori yang terorganisir berdasarkan parameter tertentu.
- Memperbarui PrintStatementLog dengan status akhir dan path file PDF yang dihasilkan.
- Menambahkan error handling dan logging secara menyeluruh untuk memantau proses.
- Menghapus file sementara (temporary) setelah PDF berhasil disimpan ke storage.
- Menambahkan dukungan timeout dan konfigurasi Browsershot yang optimal.
- Melakukan validasi terhadap data account, customer, dan branch sebelum proses generate PDF dilakukan.

Tujuan perubahan:
- Memindahkan logika generate PDF ke dalam background job agar lebih efisien dan terstruktur.
- Menjamin integritas data dan hasil PDF yang valid melalui proses terstandarisasi.
- Mengurangi beban proses di controller serta mendukung proses batch secara asynchronous.
This commit is contained in:
Daeng Deni Mardaeni
2025-07-10 13:15:47 +07:00
parent 5ea8136c13
commit 0aa7d22094
2 changed files with 227 additions and 53 deletions

View File

@@ -16,7 +16,7 @@ use Modules\Webstatement\Models\{Account, AccountBalance, PrintStatementLog, Pro
use Spatie\Browsershot\Browsershot; use Spatie\Browsershot\Browsershot;
use ZipArchive; use ZipArchive;
ini_set('memory_limit', '2G'); // Atau '1G' untuk data yang sangat besar ini_set('memory_limit', '2G'); // Atau '1G' untuk data yang sangat besar
ini_set('max_execution_time', 300000); ini_set('max_execution_time', 300000);
class PrintStatementController extends Controller class PrintStatementController extends Controller
{ {
@@ -49,7 +49,7 @@ ini_set('max_execution_time', 300000);
if($request->input('branch_code') && !empty($request->input('stmt_sent_type'))){ if($request->input('branch_code') && !empty($request->input('stmt_sent_type'))){
$request_type = 'multi_account'; // Default untuk request manual $request_type = 'multi_account'; // Default untuk request manual
} }
if($request_type=='single_account'){ if($request_type=='single_account'){
$account = Account::where('account_number', $accountNumber)->first(); $account = Account::where('account_number', $accountNumber)->first();
if ($account) { if ($account) {
@@ -101,7 +101,7 @@ ini_set('max_execution_time', 300000);
$validated['created_by'] = Auth::id(); $validated['created_by'] = Auth::id();
$validated['ip_address'] = $request->ip(); $validated['ip_address'] = $request->ip();
$validated['user_agent'] = $request->userAgent(); $validated['user_agent'] = $request->userAgent();
$validated['status'] = 'pending'; // Status awal $validated['status'] = 'pending'; // Status awal
$validated['authorization_status'] = 'approved'; // Status otorisasi awal $validated['authorization_status'] = 'approved'; // Status otorisasi awal
$validated['total_accounts'] = 1; // Untuk single account $validated['total_accounts'] = 1; // Untuk single account
@@ -110,7 +110,7 @@ ini_set('max_execution_time', 300000);
$validated['failed_count'] = 0; $validated['failed_count'] = 0;
$validated['stmt_sent_type'] = $request->input('stmt_sent_type') ? implode(",",$request->input('stmt_sent_type')) : ''; $validated['stmt_sent_type'] = $request->input('stmt_sent_type') ? implode(",",$request->input('stmt_sent_type')) : '';
$validated['branch_code'] = $validated['branch_code'] ?? $branch_code; // Awal tidak tersedia $validated['branch_code'] = $validated['branch_code'] ?? $branch_code; // Awal tidak tersedia
// Create the statement log // Create the statement log
$statement = PrintStatementLog::create($validated); $statement = PrintStatementLog::create($validated);
@@ -825,9 +825,6 @@ ini_set('max_execution_time', 300000);
$period = $statement->period_from; $period = $statement->period_from;
$format='pdf'; $format='pdf';
// Generate nama file PDF // Generate nama file PDF
$filename = $this->generatePdfFileName($norek, $period); $filename = $this->generatePdfFileName($norek, $period);
@@ -967,10 +964,11 @@ ini_set('max_execution_time', 300000);
Browsershot::html($html) Browsershot::html($html)
->showBackground() ->showBackground()
->setOption('addStyleTag', json_encode(['content' => '@page { margin: 0; }'])) ->setOption('addStyleTag', json_encode(['content' => '@page { margin: 0; }']))
->setOption('protocolTimeout', 2147483) // 120000 ms = 2 menit
->format('A4') ->format('A4')
->margins(0, 0, 0, 0) ->margins(0, 0, 0, 0)
->waitUntilNetworkIdle() ->waitUntil('load')
->timeout(6000) ->timeout(2147483)
->save($tempPath); ->save($tempPath);
// Verifikasi file berhasil dibuat // Verifikasi file berhasil dibuat
@@ -1281,30 +1279,30 @@ ini_set('max_execution_time', 300000);
try { try {
// DB::beginTransaction(); // DB::beginTransaction();
Log::info('Starting statement processing', [ Log::info('Starting statement processing', [
'statement_id' => $statement->id, 'statement_id' => $statement->id,
'request_type' => $statement->request_type, 'request_type' => $statement->request_type,
'stmt_sent_type' => $statement->stmt_sent_type, 'stmt_sent_type' => $statement->stmt_sent_type,
'branch_code' => $statement->branch_code 'branch_code' => $statement->branch_code
]); ]);
if ($statement->request_type === 'multi_account') { if ($statement->request_type === 'multi_account') {
return $this->processMultiAccountStatement($statement); return $this->processMultiAccountStatement($statement);
} else { } else {
return $this->processSingleAccountStatement($statement); return $this->processSingleAccountStatement($statement);
} }
} catch (\Exception $e) { } catch (\Exception $e) {
//DB::rollBack(); //DB::rollBack();
Log::error('Failed to process statement', [ Log::error('Failed to process statement', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'statement_id' => $statement->id, 'statement_id' => $statement->id,
'trace' => $e->getTraceAsString() 'trace' => $e->getTraceAsString()
]); ]);
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'Failed to process statement', 'message' => 'Failed to process statement',
@@ -1323,22 +1321,22 @@ ini_set('max_execution_time', 300000);
{ {
try { try {
$period = $statement->period_from ?? date('Ym'); $period = $statement->period_from ?? date('Ym');
// Validasi stmt_sent_type // Validasi stmt_sent_type
if (empty($statement->stmt_sent_type)) { if (empty($statement->stmt_sent_type)) {
throw new \Exception('stmt_sent_type is required for multi account processing'); throw new \Exception('stmt_sent_type is required for multi account processing');
} }
// Decode stmt_sent_type jika dalam format JSON array // Decode stmt_sent_type jika dalam format JSON array
$stmtSentTypes = explode(',', $statement->stmt_sent_type); $stmtSentTypes = explode(',', $statement->stmt_sent_type);
Log::info('Processing multi account statement', [ Log::info('Processing multi account statement', [
'statement_id' => $statement->id, 'statement_id' => $statement->id,
'branch_code' => $statement->branch_code, 'branch_code' => $statement->branch_code,
'stmt_sent_types' => $stmtSentTypes, 'stmt_sent_types' => $stmtSentTypes,
'period' => $period 'period' => $period
]); ]);
$clientName = $statement->branch_code.'_'.$period.'_';//.implode('_'.$stmtSentTypes); $clientName = $statement->branch_code.'_'.$period.'_';//.implode('_'.$stmtSentTypes);
@@ -1352,13 +1350,13 @@ ini_set('max_execution_time', 300000);
if ($accounts->isEmpty()) { if ($accounts->isEmpty()) {
throw new \Exception('No accounts found for the specified criteria'); throw new \Exception('No accounts found for the specified criteria');
} }
Log::info('Found accounts for processing', [ Log::info('Found accounts for processing', [
'total_accounts' => $accounts->count(), 'total_accounts' => $accounts->count(),
'branch_code' => $statement->branch_code, 'branch_code' => $statement->branch_code,
'stmt_sent_types' => $stmtSentTypes 'stmt_sent_types' => $stmtSentTypes
]); ]);
// Update statement log dengan informasi accounts // Update statement log dengan informasi accounts
$accountNumbers = $accounts->pluck('account_number')->toArray(); $accountNumbers = $accounts->pluck('account_number')->toArray();
@@ -1368,7 +1366,7 @@ ini_set('max_execution_time', 300000);
'status' => 'processing', 'status' => 'processing',
'started_at' => now() 'started_at' => now()
]); ]);
// Dispatch job untuk generate PDF multi account // Dispatch job untuk generate PDF multi account
$job = GenerateMultiAccountPdfJob::dispatch( $job = GenerateMultiAccountPdfJob::dispatch(
$statement, $statement,
@@ -1376,16 +1374,16 @@ ini_set('max_execution_time', 300000);
$period, $period,
$clientName $clientName
); );
DB::commit(); DB::commit();
Log::info('Multi account PDF generation job dispatched', [ Log::info('Multi account PDF generation job dispatched', [
'job_id' => $job->job_id ?? null, 'job_id' => $job->job_id ?? null,
'statement_id' => $statement->id, 'statement_id' => $statement->id,
'total_accounts' => $accounts->count(), 'total_accounts' => $accounts->count(),
'period' => $period 'period' => $period
]); ]);
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'message' => 'Multi account statement processing queued successfully', 'message' => 'Multi account statement processing queued successfully',
@@ -1398,16 +1396,16 @@ ini_set('max_execution_time', 300000);
'client_name' => $clientName 'client_name' => $clientName
] ]
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
DB::rollBack(); DB::rollBack();
Log::error('Failed to process multi account statement', [ Log::error('Failed to process multi account statement', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'statement_id' => $statement->id, 'statement_id' => $statement->id,
'trace' => $e->getTraceAsString() 'trace' => $e->getTraceAsString()
]); ]);
throw $e; throw $e;
} }
} }
@@ -1491,24 +1489,24 @@ ini_set('max_execution_time', 300000);
{ {
try { try {
$statement = PrintStatementLog::findOrFail($statementId); $statement = PrintStatementLog::findOrFail($statementId);
if ($statement->request_type !== 'multi_account') { if ($statement->request_type !== 'multi_account') {
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'This statement is not a multi account request' 'message' => 'This statement is not a multi account request'
], 400); ], 400);
} }
if (!$statement->is_available) { if (!$statement->is_available) {
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'Statement files are not available for download' 'message' => 'Statement files are not available for download'
], 404); ], 404);
} }
// Find ZIP file // Find ZIP file
$zipFiles = Storage::disk('local')->files("statements/{$statement->period_from}/multi_account/{$statementId}"); $zipFiles = Storage::disk('local')->files("statements/{$statement->period_from}/multi_account/{$statementId}");
$zipFile = null; $zipFile = null;
foreach ($zipFiles as $file) { foreach ($zipFiles as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) === 'zip') { if (pathinfo($file, PATHINFO_EXTENSION) === 'zip') {
@@ -1516,39 +1514,39 @@ ini_set('max_execution_time', 300000);
break; break;
} }
} }
if (!$zipFile || !Storage::disk('local')->exists($zipFile)) { if (!$zipFile || !Storage::disk('local')->exists($zipFile)) {
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'ZIP file not found' 'message' => 'ZIP file not found'
], 404); ], 404);
} }
$zipPath = Storage::disk('local')->path($zipFile); $zipPath = Storage::disk('local')->path($zipFile);
$filename = basename($zipFile); $filename = basename($zipFile);
// Update download status // Update download status
$statement->update([ $statement->update([
'is_downloaded' => true, 'is_downloaded' => true,
'downloaded_at' => now() 'downloaded_at' => now()
]); ]);
Log::info('Multi account ZIP downloaded', [ Log::info('Multi account ZIP downloaded', [
'statement_id' => $statementId, 'statement_id' => $statementId,
'zip_file' => $zipFile, 'zip_file' => $zipFile,
'user_id' => Auth::id() 'user_id' => Auth::id()
]); ]);
return response()->download($zipPath, $filename, [ return response()->download($zipPath, $filename, [
'Content-Type' => 'application/zip' 'Content-Type' => 'application/zip'
]); ]);
} catch (Exception $e) { } catch (Exception $e) {
Log::error('Failed to download multi account ZIP', [ Log::error('Failed to download multi account ZIP', [
'statement_id' => $statementId, 'statement_id' => $statementId,
'error' => $e->getMessage() 'error' => $e->getMessage()
]); ]);
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'Failed to download ZIP file', 'message' => 'Failed to download ZIP file',

View File

@@ -4,20 +4,32 @@ namespace Modules\Webstatement\Jobs;
use Carbon\Carbon; use Carbon\Carbon;
use Exception; use Exception;
use Illuminate\Bus\Queueable; use Illuminate\Bus\{
Queueable
};
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\{
InteractsWithQueue,
SerializesModels
};
use Illuminate\Support\Facades\{
DB,
Log,
Storage
};
use Spatie\Browsershot\Browsershot;
use Modules\Webstatement\Models\{
PrintStatementLog,
ProcessedStatement,
StmtEntry,
TempFundsTransfer,
TempStmtNarrFormat,
TempStmtNarrParam,
Account,
Customer
};
use Modules\Basicdata\Models\Branch;
use Illuminate\Foundation\Bus\Dispatchable; 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\PrintStatementLog;
use Modules\Webstatement\Models\ProcessedStatement;
use Modules\Webstatement\Models\StmtEntry;
use Modules\Webstatement\Models\TempFundsTransfer;
use Modules\Webstatement\Models\TempStmtNarrFormat;
use Modules\Webstatement\Models\TempStmtNarrParam;
class ExportStatementPeriodJob implements ShouldQueue class ExportStatementPeriodJob implements ShouldQueue
{ {
@@ -92,6 +104,10 @@ class ExportStatementPeriodJob implements ShouldQueue
if($this->toCsv){ if($this->toCsv){
$this->exportToCsv(); $this->exportToCsv();
} }
// Generate PDF setelah data diproses
$this->generatePdf();
Log::info("Export statement period job completed successfully for account: {$this->account_number}, period: {$this->period}"); Log::info("Export statement period job completed successfully for account: {$this->account_number}, period: {$this->period}");
} catch (Exception $e) { } catch (Exception $e) {
Log::error("Error in ExportStatementPeriodJob: " . $e->getMessage()); Log::error("Error in ExportStatementPeriodJob: " . $e->getMessage());
@@ -110,10 +126,10 @@ class ExportStatementPeriodJob implements ShouldQueue
$existingDataCount = $this->getExistingProcessedCount($accountQuery); $existingDataCount = $this->getExistingProcessedCount($accountQuery);
// Only process if data is not fully processed // Only process if data is not fully processed
//if ($existingDataCount !== $totalCount) { if ($existingDataCount !== $totalCount) {
$this->deleteExistingProcessedData($accountQuery); $this->deleteExistingProcessedData($accountQuery);
$this->processAndSaveStatementEntries($totalCount); $this->processAndSaveStatementEntries($totalCount);
//} }
} }
private function getTotalEntryCount(): int private function getTotalEntryCount(): int
@@ -398,6 +414,166 @@ class ExportStatementPeriodJob implements ShouldQueue
return str_replace('<NL>', ' ', $result); return str_replace('<NL>', ' ', $result);
} }
/**
* Generate PDF statement untuk account yang diproses
* Menggunakan data yang sudah diproses dari ProcessedStatement
*
* @return void
* @throws Exception
*/
private function generatePdf(): void
{
try {
DB::beginTransaction();
Log::info('ExportStatementPeriodJob: Memulai generate PDF', [
'account_number' => $this->account_number,
'period' => $this->period,
'statement_id' => $this->statementId
]);
// Ambil data account dan customer
$account = Account::where('account_number', $this->account_number)->first();
if (!$account) {
throw new Exception("Account tidak ditemukan: {$this->account_number}");
}
$customer = Customer::where('customer_code', $account->customer_code)->first();
if (!$customer) {
throw new Exception("Customer tidak ditemukan untuk account: {$this->account_number}");
}
// Ambil data branch
$branch = Branch::where('code', $account->branch_code)->first();
if (!$branch) {
throw new Exception("Branch tidak ditemukan: {$account->branch_code}");
}
// Ambil statement entries yang sudah diproses
$stmtEntries = ProcessedStatement::where('account_number', $this->account_number)
->where('period', $this->period)
->orderBy('sequence_no')
->get();
if ($stmtEntries->isEmpty()) {
throw new Exception("Tidak ada data statement yang diproses untuk account: {$this->account_number}");
}
// Prepare header table background (convert to base64 if needed)
$headerImagePath = public_path('assets/media/images/bg-header-table.png');
$headerTableBg = file_exists($headerImagePath)
? base64_encode(file_get_contents($headerImagePath))
: null;
// Hitung saldo awal bulan
$saldoAwalBulan = (object) ['actual_balance' => (float) $this->saldo];
// Generate filename
$filename = "statement_{$this->account_number}_{$this->period}.pdf";
// Tentukan path storage
$storagePath = "statements/{$this->period}/{$this->account_number}";
$tempPath = storage_path("app/temp/{$filename}");
$fullStoragePath = "{$storagePath}/{$filename}";
// Pastikan direktori temp ada
if (!file_exists(dirname($tempPath))) {
mkdir(dirname($tempPath), 0755, true);
}
// Pastikan direktori storage ada
Storage::makeDirectory($storagePath);
$period = $this->period;
// Render HTML view
$html = view('webstatement::statements.stmt', compact(
'stmtEntries',
'account',
'customer',
'headerTableBg',
'branch',
'period',
'saldoAwalBulan'
))->render();
Log::info('ExportStatementPeriodJob: HTML view berhasil di-render', [
'account_number' => $this->account_number,
'html_length' => strlen($html)
]);
// Generate PDF menggunakan Browsershot
Browsershot::html($html)
->showBackground()
->setOption('addStyleTag', json_encode(['content' => '@page { margin: 0; }']))
->setOption('protocolTimeout', 2147483) // 2 menit timeout
->format('A4')
->margins(0, 0, 0, 0)
->waitUntil('load')
->timeout(2147483)
->save($tempPath);
// Verifikasi file berhasil dibuat
if (!file_exists($tempPath)) {
throw new Exception('PDF file gagal dibuat');
}
$fileSize = filesize($tempPath);
// Pindahkan file ke storage permanen
$pdfContent = file_get_contents($tempPath);
Storage::put($fullStoragePath, $pdfContent);
// Update print statement log
$printLog = PrintStatementLog::find($this->statementId);
if ($printLog) {
$printLog->update([
'is_available' => true,
'is_generated' => true,
'pdf_path' => $fullStoragePath,
'file_size' => $fileSize
]);
}
// Hapus file temporary
if (file_exists($tempPath)) {
unlink($tempPath);
}
Log::info('ExportStatementPeriodJob: PDF berhasil dibuat dan disimpan', [
'account_number' => $this->account_number,
'period' => $this->period,
'storage_path' => $fullStoragePath,
'file_size' => $fileSize,
'statement_id' => $this->statementId
]);
DB::commit();
} catch (Exception $e) {
DB::rollBack();
Log::error('ExportStatementPeriodJob: Gagal generate PDF', [
'error' => $e->getMessage(),
'account_number' => $this->account_number,
'period' => $this->period,
'statement_id' => $this->statementId,
'trace' => $e->getTraceAsString()
]);
// Update print statement log dengan status error
$printLog = PrintStatementLog::find($this->statementId);
if ($printLog) {
$printLog->update([
'is_available' => false,
'error_message' => $e->getMessage()
]);
}
throw new Exception('Gagal generate PDF: ' . $e->getMessage());
}
}
/** /**
* Export processed data to CSV file * Export processed data to CSV file
*/ */