- **Perbaikan Struktural:** - Melakukan perapihan kode dengan konsistensi indentasi dan penyusunan namespace seluruh file. - Menambahkan dan mengimpor namespace baru seperti `Throwable`, `InvalidArgumentException`, dan `Exception`. - **Peningkatan Readability:** - Menambahkan format dan penyesuaian pada komentar, khususnya penjelasan method dan atribut. - Menggunakan alignment untuk parameter pada log dan constructor untuk meningkatkan keterbacaan. - **Pengelolaan Job:** - Menambahkan logging detail saat memulai, menjalankan, hingga menyelesaikan job. - Menambahkan penanganan proses tiap akun dalam batch, termasuk logging sukses/gagal dan pembaruan status log. - **Penanganan Error:** - Menambahkan rollback database jika terjadi exception pada saat proses pengiriman email. - Melakukan logging error dengan detail tambahan termasuk pesan dan trace. - **Penambahan Utility:** - Menambahkan metode reusable seperti `updateLogStatus` untuk update status log dengan parameter dinamis. - Menambahkan validasi seperti pengecekan eksistensi file PDF dan email terkait sebelum pengiriman. - **Peningkatan Proses Batch:** - Menambahkan pengelolaan batch berbasis properti `batchId` untuk tracking. - Memperbaiki handle retries dan status akhir batch secara komprehensif (completed, failed). - Menambahkan logging agregat untuk jumlah akun yang diproses dan tingkat keberhasilan. - **Peningkatan Validasi Email:** - Menambahkan skenario untuk pengambilan email dari `stmt_email` atau fallback ke data customer jika tersedia. - Menambahkan peringatan saat akun tertentu tidak memiliki email yang valid. - **Pemeliharaan File PDF:** - Mengecek keberadaan file PDF terkait sebelum proses pengiriman. - Menampilkan log error jika file tidak ditemukan. - **Peningkatan Retry dan Logging Gagal:** - Implementasi metode `failed()` untuk logging pada job yang gagal permanen. - Menangani detail error dan rollback pada level tiap akun dan batch. Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
426 lines
16 KiB
PHP
426 lines
16 KiB
PHP
<?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\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use InvalidArgumentException;
|
|
use Modules\Webstatement\Mail\StatementEmail;
|
|
use Modules\Webstatement\Models\Account;
|
|
use Modules\Webstatement\Models\PrintStatementLog;
|
|
use Throwable;
|
|
|
|
/**
|
|
* Job untuk mengirim email PDF statement ke nasabah
|
|
* Mendukung pengiriman per rekening, per cabang, atau seluruh cabang
|
|
*/
|
|
class SendStatementEmailJob implements ShouldQueue
|
|
{
|
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
|
protected $period;
|
|
protected $requestType;
|
|
protected $targetValue; // account_number, branch_code, atau null untuk all
|
|
protected $batchId;
|
|
protected $logId;
|
|
|
|
/**
|
|
* Membuat instance job baru
|
|
*
|
|
* @param string $period Format: YYYYMM
|
|
* @param string $requestType 'single_account', 'branch', 'all_branches'
|
|
* @param string|null $targetValue account_number untuk single, branch_code untuk branch, null untuk all
|
|
* @param string|null $batchId ID batch untuk tracking
|
|
* @param int|null $logId ID log untuk update progress
|
|
*/
|
|
public function __construct($period, $requestType = 'single_account', $targetValue = null, $batchId = null, $logId = null)
|
|
{
|
|
$this->period = $period;
|
|
$this->requestType = $requestType;
|
|
$this->targetValue = $targetValue;
|
|
$this->batchId = $batchId ?? uniqid('batch_');
|
|
$this->logId = $logId;
|
|
|
|
Log::info('SendStatementEmailJob created', [
|
|
'period' => $this->period,
|
|
'request_type' => $this->requestType,
|
|
'target_value' => $this->targetValue,
|
|
'batch_id' => $this->batchId,
|
|
'log_id' => $this->logId
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Menjalankan job pengiriman email statement
|
|
*/
|
|
public function handle()
|
|
: void
|
|
{
|
|
Log::info('Starting SendStatementEmailJob execution', [
|
|
'batch_id' => $this->batchId,
|
|
'period' => $this->period,
|
|
'request_type' => $this->requestType,
|
|
'target_value' => $this->targetValue
|
|
]);
|
|
|
|
DB::beginTransaction();
|
|
|
|
try {
|
|
// Update log status menjadi processing
|
|
$this->updateLogStatus('processing', ['started_at' => now()]);
|
|
|
|
// Ambil accounts berdasarkan request type
|
|
$accounts = $this->getAccountsByRequestType();
|
|
|
|
if ($accounts->isEmpty()) {
|
|
Log::warning('No accounts with email found', [
|
|
'period' => $this->period,
|
|
'request_type' => $this->requestType,
|
|
'target_value' => $this->targetValue,
|
|
'batch_id' => $this->batchId
|
|
]);
|
|
|
|
$this->updateLogStatus('completed', [
|
|
'completed_at' => now(),
|
|
'total_accounts' => 0,
|
|
'processed_accounts' => 0,
|
|
'success_count' => 0,
|
|
'failed_count' => 0
|
|
]);
|
|
|
|
DB::commit();
|
|
return;
|
|
}
|
|
|
|
// Update total accounts
|
|
$this->updateLogStatus('processing', [
|
|
'total_accounts' => $accounts->count(),
|
|
'target_accounts' => $accounts->pluck('account_number')->toArray()
|
|
]);
|
|
|
|
$successCount = 0;
|
|
$failedCount = 0;
|
|
$processedCount = 0;
|
|
|
|
foreach ($accounts as $account) {
|
|
try {
|
|
$this->sendStatementEmail($account);
|
|
$successCount++;
|
|
|
|
Log::info('Statement email sent successfully', [
|
|
'account_number' => $account->account_number,
|
|
'branch_code' => $account->branch_code,
|
|
'email' => $this->getEmailForAccount($account),
|
|
'batch_id' => $this->batchId
|
|
]);
|
|
} catch (Exception $e) {
|
|
$failedCount++;
|
|
|
|
Log::error('Failed to send statement email', [
|
|
'account_number' => $account->account_number,
|
|
'branch_code' => $account->branch_code,
|
|
'email' => $this->getEmailForAccount($account),
|
|
'error' => $e->getMessage(),
|
|
'batch_id' => $this->batchId
|
|
]);
|
|
}
|
|
|
|
$processedCount++;
|
|
|
|
// Update progress setiap 10 account atau di akhir
|
|
if ($processedCount % 10 === 0 || $processedCount === $accounts->count()) {
|
|
$this->updateLogStatus('processing', [
|
|
'processed_accounts' => $processedCount,
|
|
'success_count' => $successCount,
|
|
'failed_count' => $failedCount
|
|
]);
|
|
}
|
|
}
|
|
|
|
// Update status final
|
|
$finalStatus = $failedCount === 0 ? 'completed' : ($successCount === 0 ? 'failed' : 'completed');
|
|
$this->updateLogStatus($finalStatus, [
|
|
'completed_at' => now(),
|
|
'processed_accounts' => $processedCount,
|
|
'success_count' => $successCount,
|
|
'failed_count' => $failedCount
|
|
]);
|
|
|
|
DB::commit();
|
|
|
|
Log::info('SendStatementEmailJob completed', [
|
|
'batch_id' => $this->batchId,
|
|
'total_accounts' => $accounts->count(),
|
|
'success_count' => $successCount,
|
|
'failed_count' => $failedCount,
|
|
'final_status' => $finalStatus
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
DB::rollBack();
|
|
|
|
$this->updateLogStatus('failed', [
|
|
'completed_at' => now(),
|
|
'error_message' => $e->getMessage()
|
|
]);
|
|
|
|
Log::error('SendStatementEmailJob failed', [
|
|
'batch_id' => $this->batchId,
|
|
'error' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString()
|
|
]);
|
|
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update status log
|
|
*/
|
|
private function updateLogStatus($status, $additionalData = [])
|
|
{
|
|
if (!$this->logId) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$updateData = array_merge(['status' => $status], $additionalData);
|
|
PrintStatementLog::where('id', $this->logId)->update($updateData);
|
|
} catch (Exception $e) {
|
|
Log::error('Failed to update log status', [
|
|
'log_id' => $this->logId,
|
|
'status' => $status,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mengambil accounts berdasarkan request type
|
|
*/
|
|
private function getAccountsByRequestType()
|
|
{
|
|
Log::info('Fetching accounts by request type', [
|
|
'period' => $this->period,
|
|
'request_type' => $this->requestType,
|
|
'target_value' => $this->targetValue
|
|
]);
|
|
|
|
$query = Account::with('customer')
|
|
->where('stmt_sent_type', 'BY.EMAIL');
|
|
|
|
switch ($this->requestType) {
|
|
case 'single_account':
|
|
if ($this->targetValue) {
|
|
$query->where('account_number', $this->targetValue);
|
|
}
|
|
break;
|
|
|
|
case 'branch':
|
|
if ($this->targetValue) {
|
|
$query->where('branch_code', $this->targetValue);
|
|
}
|
|
break;
|
|
|
|
case 'all_branches':
|
|
// Tidak ada filter tambahan, ambil semua
|
|
break;
|
|
|
|
default:
|
|
throw new InvalidArgumentException("Invalid request type: {$this->requestType}");
|
|
}
|
|
|
|
$accounts = $query->get();
|
|
|
|
// Filter accounts yang memiliki email
|
|
$accountsWithEmail = $accounts->filter(function ($account) {
|
|
return !empty($account->stmt_email) ||
|
|
($account->customer && !empty($account->customer->email));
|
|
});
|
|
|
|
Log::info('Accounts with email retrieved', [
|
|
'total_accounts' => $accounts->count(),
|
|
'accounts_with_email' => $accountsWithEmail->count(),
|
|
'request_type' => $this->requestType,
|
|
'batch_id' => $this->batchId
|
|
]);
|
|
|
|
return $accountsWithEmail;
|
|
}
|
|
|
|
/**
|
|
* Mengirim email statement untuk account tertentu
|
|
*
|
|
* @param Account $account
|
|
*
|
|
* @return void
|
|
* @throws \Exception
|
|
*/
|
|
private function sendStatementEmail(Account $account)
|
|
{
|
|
// Dapatkan email untuk pengiriman
|
|
$emailAddress = $this->getEmailForAccount($account);
|
|
|
|
if (!$emailAddress) {
|
|
throw new Exception("No email address found for account {$account->account_number}");
|
|
}
|
|
|
|
// Cek apakah file PDF ada
|
|
$pdfPath = $this->getPdfPath($account->account_number, $account->branch_code);
|
|
|
|
if (!Storage::exists($pdfPath)) {
|
|
throw new Exception("PDF file not found: {$pdfPath}");
|
|
}
|
|
|
|
// Buat atau update log statement
|
|
$statementLog = $this->createOrUpdateStatementLog($account);
|
|
|
|
// Dapatkan path absolut file
|
|
$absolutePdfPath = Storage::path($pdfPath);
|
|
|
|
// Kirim email
|
|
// Add delay between email sends to prevent rate limiting
|
|
sleep(1); // 2 second delay
|
|
Mail::to($emailAddress)->send(
|
|
new StatementEmail($statementLog, $absolutePdfPath, false)
|
|
);
|
|
|
|
// Update status log dengan email yang digunakan
|
|
$statementLog->update([
|
|
'email_sent_at' => now(),
|
|
'email_status' => 'sent',
|
|
'email_address' => $emailAddress // Simpan email yang digunakan untuk tracking
|
|
]);
|
|
|
|
Log::info('Email sent for account', [
|
|
'account_number' => $account->account_number,
|
|
'branch_code' => $account->branch_code,
|
|
'email' => $emailAddress,
|
|
'email_source' => !empty($account->stmt_email) ? 'account.stmt_email' : 'customer.email',
|
|
'pdf_path' => $pdfPath,
|
|
'batch_id' => $this->batchId
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Mendapatkan email untuk pengiriman statement
|
|
*
|
|
* @param Account $account
|
|
*
|
|
* @return string|null
|
|
*/
|
|
private function getEmailForAccount(Account $account)
|
|
{
|
|
// Prioritas pertama: stmt_email dari account
|
|
if (!empty($account->stmt_email)) {
|
|
Log::info('Using stmt_email from account', [
|
|
'account_number' => $account->account_number,
|
|
'email' => $account->stmt_email,
|
|
'batch_id' => $this->batchId
|
|
]);
|
|
return $account->stmt_email;
|
|
}
|
|
|
|
// Prioritas kedua: email dari customer
|
|
if ($account->customer && !empty($account->customer->email)) {
|
|
Log::info('Using email from customer', [
|
|
'account_number' => $account->account_number,
|
|
'customer_code' => $account->customer_code,
|
|
'email' => $account->customer->email,
|
|
'batch_id' => $this->batchId
|
|
]);
|
|
return $account->customer->email;
|
|
}
|
|
|
|
Log::warning('No email found for account', [
|
|
'account_number' => $account->account_number,
|
|
'customer_code' => $account->customer_code,
|
|
'batch_id' => $this->batchId
|
|
]);
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Mendapatkan path file PDF statement
|
|
*
|
|
* @param string $accountNumber
|
|
* @param string $branchCode
|
|
*
|
|
* @return string
|
|
*/
|
|
private function getPdfPath($accountNumber, $branchCode)
|
|
{
|
|
return "combine/{$this->period}/{$branchCode}/{$accountNumber}_{$this->period}.pdf";
|
|
}
|
|
|
|
/**
|
|
* Membuat atau update log statement
|
|
*
|
|
* @param Account $account
|
|
*
|
|
* @return PrintStatementLog
|
|
*/
|
|
private function createOrUpdateStatementLog(Account $account)
|
|
{
|
|
$emailAddress = $this->getEmailForAccount($account);
|
|
|
|
$logData = [
|
|
'account_number' => $account->account_number,
|
|
'customer_code' => $account->customer_code,
|
|
'branch_code' => $account->branch_code,
|
|
'period' => $this->period,
|
|
'print_date' => now(),
|
|
'batch_id' => $this->batchId,
|
|
'email_address' => $emailAddress,
|
|
'email_source' => !empty($account->stmt_email) ? 'account' : 'customer'
|
|
];
|
|
|
|
$statementLog = PrintStatementLog::updateOrCreate(
|
|
[
|
|
'account_number' => $account->account_number,
|
|
'period_from' => $this->period,
|
|
'period_to' => $this->period
|
|
],
|
|
$logData
|
|
);
|
|
|
|
Log::info('Statement log created/updated', [
|
|
'log_id' => $statementLog->id,
|
|
'account_number' => $account->account_number,
|
|
'email_address' => $emailAddress,
|
|
'batch_id' => $this->batchId
|
|
]);
|
|
|
|
return $statementLog;
|
|
}
|
|
|
|
/**
|
|
* Handle job failure
|
|
*/
|
|
public function failed(Throwable $exception)
|
|
{
|
|
$this->updateLogStatus('failed', [
|
|
'completed_at' => now(),
|
|
'error_message' => $exception->getMessage()
|
|
]);
|
|
|
|
Log::error('SendStatementEmailJob failed permanently', [
|
|
'batch_id' => $this->batchId,
|
|
'period' => $this->period,
|
|
'request_type' => $this->requestType,
|
|
'target_value' => $this->targetValue,
|
|
'error' => $exception->getMessage(),
|
|
'trace' => $exception->getTraceAsString()
|
|
]);
|
|
}
|
|
}
|