Files
webstatement/app/Mail/StatementEmail.php
Daeng Deni Mardaeni 5469045b5a 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.
2025-07-10 19:49:31 +07:00

205 lines
7.8 KiB
PHP

<?php
namespace Modules\Webstatement\Mail;
use Carbon\Carbon;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use Modules\Webstatement\Models\Account;
use Modules\Webstatement\Models\PrintStatementLog;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
use Symfony\Component\Mime\Email;
use Illuminate\Support\Facades\Log;
class StatementEmail extends Mailable
{
use Queueable, SerializesModels;
protected $statement;
protected $filePath;
protected $isZip;
protected $message;
/**
* Create a new message instance.
* Membuat instance email baru untuk pengiriman statement
*
* @param PrintStatementLog $statement
* @param string $filePath
* @param bool $isZip
*/
public function __construct(PrintStatementLog $statement, $filePath, $isZip = false)
{
$this->statement = $statement;
$this->filePath = $filePath;
$this->isZip = $isZip;
}
/**
* Override the send method to use EsmtpTransport directly
* Using the working configuration from Python script with multiple fallback methods
*/
public function send($mailer)
{
// Get mail configuration
$host = Config::get('mail.mailers.smtp.host');
$port = Config::get('mail.mailers.smtp.port');
$username = Config::get('mail.mailers.smtp.username');
$password = Config::get('mail.mailers.smtp.password');
Log::info('StatementEmail: Attempting to send email with multiple fallback methods');
// Define connection methods like in Python script
$method =
// Method 3: STARTTLS with original port
[
'port' => $port,
'ssl' => false,
'name' => 'STARTTLS (Port $port)'
];
$lastException = null;
// Try each connection method until one succeeds
try {
Log::info('StatementEmail: Trying ' . $method['name']);
// Create EsmtpTransport with current method
$transport = new EsmtpTransport($host, $method['port'], $method['ssl']);
// Set username and password
if ($username) {
$transport->setUsername($username);
}
if ($password) {
$transport->setPassword($password);
}
// Disable SSL verification for development
$streamOptions = [
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true
]
];
$transport->getStream()->setStreamOptions($streamOptions);
// Build the email content
$this->build();
// Start transport connection
$transport->start();
// Create Symfony mailer
$symfonyMailer = new Mailer($transport);
// Convert Laravel message to Symfony Email
$email = $this->toSymfonyEmail();
// Send the email
$symfonyMailer->send($email);
// Close connection
$transport->stop();
Log::info('StatementEmail: Successfully sent email using ' . $method['name']);
return $this;
} catch (Exception $e) {
$lastException = $e;
Log::warning('StatementEmail: Failed to send with ' . $method['name'] . ': ' . $e->getMessage());
// Continue to next method
}
try {
return parent::send($mailer);
} catch (Exception $e) {
Log::error('StatementEmail: Laravel mailer also failed: ' . $e->getMessage());
// If we got here, throw the last exception from our custom methods
throw $lastException;
}
}
/**
* Build the message.
* Membangun struktur email dengan attachment statement
*
* @return $this
*/
public function build()
{
$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 .= " - " . Carbon::createFromFormat('Ym', $this->statement->period_from)
->locale('id')
->isoFormat('MMMM Y');
}
$email = $this->subject($subject);
// Store the email in the message property for later use in toSymfonyEmail()
$this->message = $email;
return $email;
}
/**
* Convert Laravel message to Symfony Email
*/
protected function toSymfonyEmail()
{
// Build the message if it hasn't been built yet
$this->build();
// Create a new Symfony Email
$email = new Email();
// Set from address using config values instead of trying to call getFrom()
$fromAddress = Config::get('mail.from.address');
$fromName = Config::get('mail.from.name');
$email->from($fromName ? "$fromName <$fromAddress>" : $fromAddress);
// Set to addresses - use the to addresses from the mailer instead of trying to call getTo()
// We'll get the to addresses from the Mail facade when the email is sent
// For now, we'll just add a placeholder recipient that will be overridden by the Mail facade
$email->to($this->message->to[0]['address']);
$email->subject($this->message->subject);
// Set body - use a simple HTML content instead of trying to call getHtmlBody()
// In a real implementation, we would need to find a way to access the rendered HTML content
$email->html(view('webstatement::statements.email', [
'statement' => $this->statement,
'accountNumber' => $this->statement->account_number,
'periodFrom' => $this->statement->period_from,
'periodTo' => $this->statement->period_to,
'isRange' => $this->statement->is_period_range,
'requestType' => $this->statement->request_type,
'batchId' => $this->statement->batch_id,
'accounts' => Account::where('account_number', $this->statement->account_number)->first()
])->render());
//$email->text($this->message->getTextBody());
// Add attachments - use the file path directly instead of trying to call getAttachments()
if ($this->filePath && file_exists($this->filePath)) {
if ($this->isZip) {
$fileName = "{$this->statement->account_number}_{$this->statement->period_from}_to_{$this->statement->period_to}.zip";
$contentType = 'application/zip';
} else {
$fileName = "{$this->statement->account_number}_{$this->statement->period_from}.pdf";
$contentType = 'application/pdf';
}
$email->attachFromPath($this->filePath, $fileName, $contentType);
}
return $email;
}
}