feat(webstatement): tambah AutoSendStatementEmailCommand dan job auto pengiriman email statement

Perubahan yang dilakukan:
- Menambahkan command AutoSendStatementEmailCommand untuk otomatisasi pengiriman email statement.
- Menambahkan job AutoSendStatementEmailJob untuk menangani proses pengiriman email secara asynchronous.
- Menambahkan opsi --force dan --dry-run pada command untuk fleksibilitas eksekusi dan pengujian.
- Mengintegrasikan command baru ke dalam WebstatementServiceProvider dan Console Kernel.
- Mengimplementasikan scheduler untuk menjalankan job setiap menit secara otomatis.
- Menambahkan kondisi auto send: is_available dan is_generated = true, email_sent_at = null.
- Mendukung pengiriman statement multi-period dalam bentuk ZIP attachment.
- Mengoptimalkan proses download dan integrasi file PDF dengan logging yang lebih detail.
- Menambahkan logika prioritas local disk dibandingkan SFTP untuk pengambilan file secara efisien.
- Menambahkan validasi tambahan untuk flow pengiriman email single dan multi period.
- Mengimplementasikan error handling dan logging yang komprehensif.
- Menggunakan database transaction untuk menjamin konsistensi data selama proses kirim email.
- Menambahkan mekanisme prevent overlap dan timeout protection saat job berjalan.
- Menghapus file sementara secara otomatis setelah email berhasil dikirim.
- Membatasi proses pengiriman maksimal 50 statement per run untuk menjaga performa.

Tujuan perubahan:
- Mengotomatiskan pengiriman email statement pelanggan secara periodik dan aman.
- Menyediakan fleksibilitas eksekusi manual dan simulasi pengujian sebelum produksi.
- Menjamin efisiensi, stabilitas, dan monitoring penuh selama proses pengiriman.
This commit is contained in:
Daeng Deni Mardaeni
2025-07-10 19:49:31 +07:00
parent 56665cd77a
commit 5469045b5a
5 changed files with 522 additions and 40 deletions

View File

@@ -128,9 +128,9 @@ ini_set('max_execution_time', 300000);
$this->printStatementRekening($statement);
}
//if($statement->email){
// $this->sendEmail($statement->id);
//}
if($statement->email){
$this->sendEmail($statement->id);
}
DB::commit();
return redirect()->route('statements.index')
@@ -596,8 +596,8 @@ ini_set('max_execution_time', 300000);
return [
'id' => $item->id,
'branch_code' => $item->branch_code,
'branch_name' => $item->account->branch->name ?? 'N/A',
'account_number' => $item->account_number,
'branch_name' => $item->account->branch->name ?? $item->branch->name ?? '',
'account_number' => $item->request_type == 'multi_account' ? $item->stmt_sent_type : ($item->account_number ?? ''),
'period_from' => $item->period_from,
'period_to' => $item->is_period_range ? $item->period_to : null,
'authorization_status' => $item->authorization_status,
@@ -659,9 +659,49 @@ ini_set('max_execution_time', 300000);
}
try {
$disk = Storage::disk('sftpStatement');
// Inisialisasi disk local dan SFTP
$localDisk = Storage::disk('local');
$sftpDisk = Storage::disk('sftpStatement');
$filePath = "{$statement->period_from}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf";
/**
* Fungsi helper untuk mendapatkan file dari disk dengan prioritas local
* @param string $path - Path file yang dicari
* @return array - [disk, exists, content]
*/
$getFileFromDisk = function($path) use ($localDisk, $sftpDisk) {
// Cek di local disk terlebih dahulu
if ($localDisk->exists("statements/{$path}")) {
Log::info('File found in local disk', ['path' => "statements/{$path}"]);
return [
'disk' => $localDisk,
'exists' => true,
'path' => "statements/{$path}",
'source' => 'local'
];
}
// Jika tidak ada di local, cek di SFTP
if ($sftpDisk->exists($path)) {
Log::info('File found in SFTP disk', ['path' => $path]);
return [
'disk' => $sftpDisk,
'exists' => true,
'path' => $path,
'source' => 'sftp'
];
}
Log::warning('File not found in any disk', ['path' => $path]);
return [
'disk' => null,
'exists' => false,
'path' => $path,
'source' => 'none'
];
};
if ($statement->is_period_range && $statement->period_to) {
$periodFrom = Carbon::createFromFormat('Ym', $statement->period_from);
$periodTo = Carbon::createFromFormat('Ym', $statement->period_to);
@@ -669,13 +709,17 @@ ini_set('max_execution_time', 300000);
// Loop through each month in the range
$missingPeriods = [];
$availablePeriods = [];
$periodFiles = []; // Menyimpan info file untuk setiap periode
for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) {
$periodFormatted = $period->format('Ym');
$periodPath = $periodFormatted . "/{$statement->branch_code}/{$statement->account_number}_{$periodFormatted}.pdf";
$periodPath = "{$periodFormatted}/{$statement->branch_code}/{$statement->account_number}_{$periodFormatted}.pdf";
if ($disk->exists($periodPath)) {
$fileInfo = $getFileFromDisk($periodPath);
if ($fileInfo['exists']) {
$availablePeriods[] = $periodFormatted;
$periodFiles[$periodFormatted] = $fileInfo;
} else {
$missingPeriods[] = $periodFormatted;
}
@@ -697,11 +741,17 @@ ini_set('max_execution_time', 300000);
if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) {
// Add each available statement to the zip
foreach ($availablePeriods as $period) {
$filePath = "{$period}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf";
$fileInfo = $periodFiles[$period];
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf");
// Download the file from SFTP to local storage temporarily
file_put_contents($localFilePath, $disk->get($filePath));
// Download/copy the file to local temp storage
file_put_contents($localFilePath, $fileInfo['disk']->get($fileInfo['path']));
Log::info('File retrieved for zip', [
'period' => $period,
'source' => $fileInfo['source'],
'path' => $fileInfo['path']
]);
// Add the file to the zip
$zip->addFile($localFilePath, "{$statement->account_number}_{$period}.pdf");
@@ -725,34 +775,49 @@ ini_set('max_execution_time', 300000);
if (file_exists($zipFilePath)) {
unlink($zipFilePath);
}
Log::info('Multi-period statement email sent successfully', [
'statement_id' => $statement->id,
'periods' => $availablePeriods,
'sources' => array_map(fn($p) => $periodFiles[$p]['source'], $availablePeriods)
]);
} else {
return redirect()->back()->with('error', 'Failed to create zip archive for email.');
}
} else {
return redirect()->back()->with('error', 'No statements available for sending.');
}
} else if ($disk->exists($filePath)) {
// For single period statements
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$statement->period_from}.pdf");
// Ensure the temp directory exists
if (!file_exists(storage_path('app/temp'))) {
mkdir(storage_path('app/temp'), 0755, true);
}
// Download the file from SFTP to local storage temporarily
file_put_contents($localFilePath, $disk->get($filePath));
// Send email with PDF attachment
Mail::to($statement->email)
->send(new StatementEmail($statement, $localFilePath, false));
// Delete the temporary file
if (file_exists($localFilePath)) {
unlink($localFilePath);
}
} else {
return redirect()->back()->with('error', 'Statement file not found.');
// For single period statements
$fileInfo = $getFileFromDisk($filePath);
if ($fileInfo['exists']) {
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$statement->period_from}.pdf");
// Ensure the temp directory exists
if (!file_exists(storage_path('app/temp'))) {
mkdir(storage_path('app/temp'), 0755, true);
}
// Download/copy the file to local temp storage
file_put_contents($localFilePath, $fileInfo['disk']->get($fileInfo['path']));
Log::info('Single period file retrieved', [
'source' => $fileInfo['source'],
'path' => $fileInfo['path']
]);
// Send email with PDF attachment
Mail::to($statement->email)
->send(new StatementEmail($statement, $localFilePath, false));
// Delete the temporary file
if (file_exists($localFilePath)) {
unlink($localFilePath);
}
} else {
return redirect()->back()->with('error', 'Statement file not found.');
}
}
// Update statement record to mark as emailed
@@ -1348,7 +1413,7 @@ ini_set('max_execution_time', 300000);
$accounts = Account::where('branch_code', $statement->branch_code)
->whereIn('stmt_sent_type', $stmtSentTypes)
->with('customer')
->limit(5)
->limit(2)
->get();
if ($accounts->isEmpty()) {