feat(webstatement): tambahkan fitur pengiriman email statement PDF

- Menambahkan Command `SendStatementEmailCommand` untuk mengirim email statement PDF:
  - Mendukung parameter input seperti periode laporan (`YYYY-MM`), nomor rekening, ID batch, queue, dan delay waktu.
  - Menjalankan validasi parameter input, mencatat log eksekusi, dan mendispatch job pengiriman email.
  - Menyediakan feedback status eksekusi serta informasi job kepada user.

- Menambahkan Job `SendStatementEmailJob` untuk pengiriman statement dalam latar belakang:
  - Memfilter account yang memiliki email terkait, baik dari `stmt_email` atau email dari data customer.
  - Melakukan pengiriman email dengan attachment file PDF statement.
  - Mencatat log sukses atau kegagalan pengiriman untuk setiap account.

- Memperbarui Model dan Template Email:
  - Mengubah template email untuk mendukung pengisian nama rekening secara dinamis berdasarkan customer account.
  - Menambahkan pengisian dinamis untuk tahun copyright di footer.

- Memperbarui Provider `WebstatementServiceProvider`:
  - Mendaftarkan Command baru `SendStatementEmailCommand` ke dalam aplikasi.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
This commit is contained in:
Daeng Deni Mardaeni
2025-06-10 13:50:00 +07:00
parent 55313fb0b0
commit f3c649572b
5 changed files with 609 additions and 65 deletions

View File

@@ -0,0 +1,191 @@
<?php
namespace Modules\Webstatement\Console;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Modules\Webstatement\Jobs\SendStatementEmailJob;
use Modules\Webstatement\Models\Account;
/**
* Command untuk mengirim email statement PDF ke nasabah
*
* Command ini akan:
* 1. Memvalidasi parameter input
* 2. Menjalankan job pengiriman email statement
* 3. Memberikan feedback ke user tentang status eksekusi
*/
class SendStatementEmailCommand extends Command
{
/**
* Signature dan parameter command
*
* @var string
*/
protected $signature = 'webstatement:send-email
{period : Format periode YYYY-MM (contoh: 2024-01)}
{--account= : Nomor rekening spesifik (opsional)}
{--batch-id= : ID batch untuk tracking (opsional)}
{--queue=emails : Nama queue untuk job (default: emails)}
{--delay=0 : Delay dalam menit sebelum job dijalankan}';
/**
* Deskripsi command
*
* @var string
*/
protected $description = 'Mengirim email statement PDF ke nasabah berdasarkan periode';
/**
* Menjalankan command
*
* @return int
*/
public function handle()
{
$this->info('🚀 Memulai proses pengiriman email statement...');
try {
// Ambil parameter
$period = $this->argument('period');
$accountNumber = $this->option('account');
$batchId = $this->option('batch-id');
$queueName = $this->option('queue');
$delay = (int) $this->option('delay');
// Validasi parameter
if (!$this->validateParameters($period, $accountNumber)) {
return Command::FAILURE;
}
// Log command execution
Log::info('SendStatementEmailCommand started', [
'period' => $period,
'account_number' => $accountNumber,
'batch_id' => $batchId,
'queue' => $queueName,
'delay' => $delay
]);
// Dispatch job
$job = SendStatementEmailJob::dispatch($period, $accountNumber, $batchId)
->onQueue($queueName);
if ($delay > 0) {
$job->delay(now()->addMinutes($delay));
$this->info("⏰ Job dijadwalkan untuk dijalankan dalam {$delay} menit");
}
// Tampilkan informasi
$this->displayJobInfo($period, $accountNumber, $batchId, $queueName);
$this->info('✅ Job pengiriman email statement berhasil didispatch!');
$this->info('📊 Gunakan command berikut untuk monitoring:');
$this->line(" php artisan queue:work {$queueName}");
$this->line(' php artisan telescope:work (jika menggunakan Telescope)');
return Command::SUCCESS;
} catch (Exception $e) {
$this->error('❌ Error saat mendispatch job: ' . $e->getMessage());
Log::error('SendStatementEmailCommand failed', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return Command::FAILURE;
}
}
/**
* Validasi parameter input
*
* @param string $period
* @param string|null $accountNumber
* @return bool
*/
private function validateParameters($period, $accountNumber = null)
{
// Validasi format periode
if (!preg_match('/^\d{4}\d{2}$/', $period)) {
$this->error('❌ Format periode tidak valid. Gunakan format YYYYMM (contoh: 202401)');
return false;
}
// Validasi account number jika diberikan
if ($accountNumber) {
$account = Account::with('customer')
->where('account_number', $accountNumber)
->first();
if (!$account) {
$this->error("❌ Account {$accountNumber} tidak ditemukan");
return false;
}
// Cek apakah ada email (dari stmt_email atau customer email)
$hasEmail = !empty($account->stmt_email) ||
($account->customer && !empty($account->customer->email));
if (!$hasEmail) {
$this->error("❌ Account {$accountNumber} tidak memiliki email (baik di stmt_email maupun customer email)");
return false;
}
$emailSource = !empty($account->stmt_email) ? 'stmt_email' : 'customer email';
$emailAddress = !empty($account->stmt_email) ? $account->stmt_email : $account->customer->email;
$this->info("✅ Account {$accountNumber} ditemukan dengan email: {$emailAddress} (dari {$emailSource}) - Cabang: {$account->branch_code}");
} else {
// Cek apakah ada account dengan email (dari stmt_email atau customer email)
$accountCount = Account::with('customer')
->where('stmt_sent_type', 'BY.EMAIL')
->get()
->filter(function ($account) {
return !empty($account->stmt_email) ||
($account->customer && !empty($account->customer->email));
})
->count();
if ($accountCount === 0) {
$this->error('❌ Tidak ada account dengan email ditemukan (baik di stmt_email maupun customer email)');
return false;
}
$this->info("✅ Ditemukan {$accountCount} account dengan email");
}
return true;
}
/**
* Menampilkan informasi job yang akan dijalankan
*
* @param string $period
* @param string|null $accountNumber
* @param string|null $batchId
* @param string $queueName
* @return void
*/
private function displayJobInfo($period, $accountNumber, $batchId, $queueName)
{
$this->info('📋 Detail Job:');
$this->line(" Periode: {$period}");
$this->line(" Account: " . ($accountNumber ?: 'Semua account dengan email'));
$this->line(" Batch ID: " . ($batchId ?: 'Auto-generated'));
$this->line(" Queue: {$queueName}");
// Estimasi path file
if ($accountNumber) {
$account = Account::where('account_number', $accountNumber)->first();
if ($account) {
$pdfPath = "storage/app/combine/{$period}/{$account->branch_code}/{$accountNumber}_{$period}.pdf";
$this->line(" File PDF: {$pdfPath}");
}
} else {
$this->line(" File PDF: storage/app/combine/{$period}/[branch_code]/[account_number]_{$period}.pdf");
}
}
}

View File

@@ -0,0 +1,337 @@
<?php
namespace Modules\Webstatement\Jobs;
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 Modules\Webstatement\Models\Account;
use Modules\Webstatement\Models\PrintStatementLog;
use Modules\Webstatement\Mail\StatementEmail;
/**
* Job untuk mengirim email PDF statement ke nasabah
*
* Job ini akan:
* 1. Mengambil data account yang memiliki email
* 2. Mencari file PDF statement di storage
* 3. Mengirim email dengan attachment PDF
* 4. Mencatat log pengiriman
*/
class SendStatementEmailJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $period;
protected $accountNumber;
protected $batchId;
/**
* Membuat instance job baru
*
* @param string $period Format: YYYY-MM
* @param string|null $accountNumber Nomor rekening spesifik (opsional)
* @param string|null $batchId ID batch untuk tracking
*/
public function __construct($period, $accountNumber = null, $batchId = null)
{
$this->period = $period;
$this->accountNumber = $accountNumber;
$this->batchId = $batchId ?? uniqid('batch_');
Log::info('SendStatementEmailJob created', [
'period' => $this->period,
'account_number' => $this->accountNumber,
'batch_id' => $this->batchId
]);
}
/**
* Menjalankan job pengiriman email statement
*
* @return void
*/
public function handle(): void
{
Log::info('Starting SendStatementEmailJob execution', [
'batch_id' => $this->batchId,
'period' => $this->period,
'account_number' => $this->accountNumber
]);
DB::beginTransaction();
try {
// Ambil accounts yang memiliki email
$accounts = $this->getAccountsWithEmail();
if ($accounts->isEmpty()) {
Log::warning('No accounts with email found', [
'period' => $this->period,
'account_number' => $this->accountNumber,
'batch_id' => $this->batchId
]);
DB::commit();
return;
}
$successCount = 0;
$failedCount = 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' => $account->stmt_email,
'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' => $account->stmt_email,
'error' => $e->getMessage(),
'batch_id' => $this->batchId
]);
}
}
DB::commit();
Log::info('SendStatementEmailJob completed', [
'batch_id' => $this->batchId,
'total_accounts' => $accounts->count(),
'success_count' => $successCount,
'failed_count' => $failedCount
]);
} catch (\Exception $e) {
DB::rollBack();
Log::error('SendStatementEmailJob failed', [
'batch_id' => $this->batchId,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
throw $e;
}
}
/**
* Mengambil accounts yang memiliki email dan sesuai kriteria
*
* @return \Illuminate\Database\Eloquent\Collection
*/
private function getAccountsWithEmail()
{
Log::info('Fetching accounts with email', [
'period' => $this->period,
'account_number' => $this->accountNumber
]);
$query = Account::with('customer')
->where('stmt_sent_type', 'BY.EMAIL');
// Jika account number spesifik diberikan
if ($this->accountNumber) {
$query->where('account_number', $this->accountNumber);
}
// Ambil semua accounts yang memenuhi kriteria
$accounts = $query->get();
// Filter accounts yang memiliki email (dari stmt_email atau customer email)
$accountsWithEmail = $accounts->filter(function ($account) {
// Cek apakah stmt_email ada dan tidak kosong
if (!empty($account->stmt_email)) {
return true;
}
// Jika stmt_email kosong, cek email di customer
if ($account->customer && !empty($account->customer->email)) {
return true;
}
return false;
});
Log::info('Accounts with email retrieved', [
'total_accounts' => $accounts->count(),
'accounts_with_email' => $accountsWithEmail->count(),
'batch_id' => $this->batchId
]);
return $accountsWithEmail;
}
/**
* 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;
}
/**
* 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
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 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
*
* @param \Throwable $exception
* @return void
*/
public function failed(\Throwable $exception)
{
Log::error('SendStatementEmailJob failed permanently', [
'batch_id' => $this->batchId,
'period' => $this->period,
'account_number' => $this->accountNumber,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString()
]);
}
}

View File

@@ -5,7 +5,8 @@
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Modules\Webstatement\Models\PrintStatementLog;
use Modules\Webstatement\Models\Account;
use Modules\Webstatement\Models\PrintStatementLog;
class StatementEmail extends Mailable
{
@@ -36,12 +37,12 @@
*/
public function build()
{
$subject = 'Your Account Statement';
$subject = 'Statement Rekening Bank Artha Graha Internasional';
if ($this->statement->is_period_range) {
$subject .= " - {$this->statement->period_from} to {$this->statement->period_to}";
} else {
$subject .= " - {$this->statement->period_from}";
$subject .= " - " . \Carbon\Carbon::createFromFormat('Ym', $this->statement->period_from)->locale('id')->isoFormat('MMMM Y');
}
$email = $this->subject($subject)
@@ -52,6 +53,7 @@
'periodFrom' => $this->statement->period_from,
'periodTo' => $this->statement->period_to,
'isRange' => $this->statement->is_period_range,
'accounts' => Account::where('account_number',$this->statement->account_number)->first()
]);
if ($this->isZip) {

View File

@@ -16,6 +16,7 @@ use Modules\Webstatement\Console\GenerateBiayakartuCommand;
use Modules\Webstatement\Jobs\UpdateAtmCardBranchCurrencyJob;
use Modules\Webstatement\Console\GenerateAtmTransactionReport;
use Modules\Webstatement\Console\GenerateBiayaKartuCsvCommand;
use Modules\Webstatement\Console\SendStatementEmailCommand;
class WebstatementServiceProvider extends ServiceProvider
{
@@ -67,6 +68,7 @@ class WebstatementServiceProvider extends ServiceProvider
UnlockPdf::class,
ExportPeriodStatements::class,
GenerateAtmTransactionReport::class,
SendStatementEmailCommand::class
]);
}