Compare commits

...

4 Commits

Author SHA1 Message Date
Daeng Deni Mardaeni
b717749450 refactor(webstatement): perbaikan tampilan dan penyesuaian template email pada statement
- **Penyesuaian Layout dan Tampilan:**
  - Mengubah properti CSS pada `.container`:
    - `max-width` diubah dari `90%` menjadi `100%`.
    - `margin` diubah dari `20px auto` menjadi `0px auto`.
  - Mengurangi padding pada elemen `.content` dari `30px` menjadi `5px` untuk meningkatkan efisiensi ruang tampilan.

- **Perbaikan Format Signature:**
  - Menggabungkan pemisah signature menjadi satu baris panjang untuk meningkatkan estetika dan keterbacaan.
  - Menghapus elemen `<wbr>` yang berlebihan.

- **Penghapusan Footer:**
  - Menghilangkan bagian footer yang terdapat elemen copyright dan informasi customer service.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
2025-06-12 09:19:07 +07:00
Daeng Deni Mardaeni
e5b8dfc7c4 feat(webstatement): tambahkan pengiriman email dengan fallback konfigurasi
- **Implementasi Custom Email Sender:**
  - Menambahkan metode `send()` dengan EsmtpTransport untuk mendukung pengiriman email menggunakan fallback konfigurasi yang lebih fleksibel.
  - Penanganan port `STARTTLS` dengan koneksi manual serta pengaturan SSL.

- **Refaktor Method Build:**
  - Memperbaiki tampilan struktur email:
    - Menyertakan template email, attachment, dan penempatan properti email secara lebih dinamis.
    - Mendukung file PDF atau ZIP sebagai lampiran.

- **Implementasi Konversi Laravel to Symfony Mailer:**
  - Metode `toSymfonyEmail()` diimplementasikan untuk mengonversi email Laravel ke Symfony Mailer.
  - Penanganan dari email `from`, `to`, `subject`, hingga body HTML secara langsung.
  - Penambahan mekanisme attachment dengan validasi eksistensi file.

- **Peningkatan Logging:**
  - Menambahkan logging detail untuk setiap tahap proses pengiriman, termasuk metode fallback yang berhasil atau gagal.
  - Log peringatan dan error ditambahkan saat terjadi kegagalan di setiap metode yang dicoba.

- **Penanganan Error:**
  - Menangani pengecualian dan pencatatan error terakhir dari koneksi email manual serta fallback ke Laravel mailer jika cara custom gagal.

- **Konsistensi Format:**
  - Melakukan perapihan atribut class serta alignment pada kode untuk meningkatkan keterbacaan dan konsistensi.

- **Optimalisasi Email Statement:**
  - Memastikan format periode (bulanan/range) tampil dinamis pada subjek email.
  - Menambahkan validasi serta pengelolaan untuk lampiran file sebelum pengiriman.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
2025-06-12 09:18:42 +07:00
Daeng Deni Mardaeni
d5482fb824 refactor(webstatement): restrukturisasi kode pada SendStatementEmailJob
- **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>
2025-06-12 09:18:16 +07:00
Daeng Deni Mardaeni
f6df453ddc refactor(webstatement): perbaiki pembentukan logika, struktur kode, dan validasi parameter pada SendStatementEmailCommand
- **Perbaikan Struktur Kode:**
  - Melakukan perapihan kode dengan konsistensi indentasi dan penyusunan namespace.
  - Memisahkan logika kompleks dan mengorganisasi ulang kode untuk meningkatkan keterbacaan.
  - Menambahkan namespace `InvalidArgumentException`.

- **Peningkatan Validasi:**
  - Menambahkan validasi komprehensif untuk parameter `period`, `type`, `--account`, dan `--branch`.
  - Validasi lebih spesifik untuk memastikan account atau branch terkait sesuai kebutuhan.
  - Memberikan pesan error informatif ketika validasi gagal.

- **Peningkatan Metode Utility:**
  - Menambahkan metode `validateParameters` untuk menangani berbagai skenario validasi input.
  - Menambahkan metode `determineRequestTypeAndTarget` untuk memisahkan logika penentuan tipe request.
  - Memperbarui metode `createLogEntry` untuk menyesuaikan atribut log dengan lebih baik berdasarkan request type.

- **Perbaikan Feedback Pengguna:**
  - Menampilkan informasi yang lebih rinci terkait status pengiriman email, seperti parameter validasi, log ID, dan batch ID.
  - Memberikan panduan untuk monitoring queue melalui command.

- **Penanganan Error dan Logging:**
  - Menambahkan logging detail untuk error yang terjadi dalam proses pengiriman email.
  - Memastikan rollback jika terjadi kegagalan selama proses dispatch job.

Signed-off-by: Daeng Deni Mardaeni <ddeni05@gmail.com>
2025-06-12 09:15:38 +07:00
4 changed files with 805 additions and 684 deletions

View File

@@ -5,10 +5,11 @@ namespace Modules\Webstatement\Console;
use Exception; use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Modules\Basicdata\Models\Branch;
use Modules\Webstatement\Jobs\SendStatementEmailJob; use Modules\Webstatement\Jobs\SendStatementEmailJob;
use Modules\Webstatement\Models\Account; use Modules\Webstatement\Models\Account;
use Modules\Webstatement\Models\PrintStatementLog; use Modules\Webstatement\Models\PrintStatementLog;
use Modules\Basicdata\Models\Branch; use InvalidArgumentException;
/** /**
* Command untuk mengirim email statement PDF ke nasabah * Command untuk mengirim email statement PDF ke nasabah
@@ -182,7 +183,7 @@ class SendStatementEmailCommand extends Command
case 'all': case 'all':
return ['all_branches', null]; return ['all_branches', null];
default: default:
throw new \InvalidArgumentException("Invalid type: {$type}"); throw new InvalidArgumentException("Invalid type: {$type}");
} }
} }

View File

@@ -2,6 +2,7 @@
namespace Modules\Webstatement\Jobs; namespace Modules\Webstatement\Jobs;
use Exception;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
@@ -11,10 +12,11 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use InvalidArgumentException;
use Modules\Webstatement\Mail\StatementEmail;
use Modules\Webstatement\Models\Account; use Modules\Webstatement\Models\Account;
use Modules\Webstatement\Models\PrintStatementLog; use Modules\Webstatement\Models\PrintStatementLog;
use Modules\Webstatement\Mail\StatementEmail; use Throwable;
use Modules\Basicdata\Models\Branch;
/** /**
* Job untuk mengirim email PDF statement ke nasabah * Job untuk mengirim email PDF statement ke nasabah
@@ -59,7 +61,8 @@ class SendStatementEmailJob implements ShouldQueue
/** /**
* Menjalankan job pengiriman email statement * Menjalankan job pengiriman email statement
*/ */
public function handle(): void public function handle()
: void
{ {
Log::info('Starting SendStatementEmailJob execution', [ Log::info('Starting SendStatementEmailJob execution', [
'batch_id' => $this->batchId, 'batch_id' => $this->batchId,
@@ -118,7 +121,7 @@ class SendStatementEmailJob implements ShouldQueue
'email' => $this->getEmailForAccount($account), 'email' => $this->getEmailForAccount($account),
'batch_id' => $this->batchId 'batch_id' => $this->batchId
]); ]);
} catch (\Exception $e) { } catch (Exception $e) {
$failedCount++; $failedCount++;
Log::error('Failed to send statement email', [ Log::error('Failed to send statement email', [
@@ -161,7 +164,7 @@ class SendStatementEmailJob implements ShouldQueue
'final_status' => $finalStatus 'final_status' => $finalStatus
]); ]);
} catch (\Exception $e) { } catch (Exception $e) {
DB::rollBack(); DB::rollBack();
$this->updateLogStatus('failed', [ $this->updateLogStatus('failed', [
@@ -179,6 +182,27 @@ class SendStatementEmailJob implements ShouldQueue
} }
} }
/**
* 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 * Mengambil accounts berdasarkan request type
*/ */
@@ -211,7 +235,7 @@ class SendStatementEmailJob implements ShouldQueue
break; break;
default: default:
throw new \InvalidArgumentException("Invalid request type: {$this->requestType}"); throw new InvalidArgumentException("Invalid request type: {$this->requestType}");
} }
$accounts = $query->get(); $accounts = $query->get();
@@ -233,30 +257,64 @@ class SendStatementEmailJob implements ShouldQueue
} }
/** /**
* Update status log * Mengirim email statement untuk account tertentu
*
* @param Account $account
*
* @return void
* @throws \Exception
*/ */
private function updateLogStatus($status, $additionalData = []) private function sendStatementEmail(Account $account)
{ {
if (!$this->logId) { // Dapatkan email untuk pengiriman
return; $emailAddress = $this->getEmailForAccount($account);
if (!$emailAddress) {
throw new Exception("No email address found for account {$account->account_number}");
} }
try { // Cek apakah file PDF ada
$updateData = array_merge(['status' => $status], $additionalData); $pdfPath = $this->getPdfPath($account->account_number, $account->branch_code);
PrintStatementLog::where('id', $this->logId)->update($updateData);
} catch (\Exception $e) { if (!Storage::exists($pdfPath)) {
Log::error('Failed to update log status', [ throw new Exception("PDF file not found: {$pdfPath}");
'log_id' => $this->logId,
'status' => $status,
'error' => $e->getMessage()
]);
} }
// 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 * Mendapatkan email untuk pengiriman statement
* *
* @param Account $account * @param Account $account
*
* @return string|null * @return string|null
*/ */
private function getEmailForAccount(Account $account) private function getEmailForAccount(Account $account)
@@ -291,64 +349,12 @@ class SendStatementEmailJob implements ShouldQueue
return null; 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
// 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 path file PDF statement * Mendapatkan path file PDF statement
* *
* @param string $accountNumber * @param string $accountNumber
* @param string $branchCode * @param string $branchCode
*
* @return string * @return string
*/ */
private function getPdfPath($accountNumber, $branchCode) private function getPdfPath($accountNumber, $branchCode)
@@ -360,6 +366,7 @@ class SendStatementEmailJob implements ShouldQueue
* Membuat atau update log statement * Membuat atau update log statement
* *
* @param Account $account * @param Account $account
*
* @return PrintStatementLog * @return PrintStatementLog
*/ */
private function createOrUpdateStatementLog(Account $account) private function createOrUpdateStatementLog(Account $account)
@@ -399,7 +406,7 @@ class SendStatementEmailJob implements ShouldQueue
/** /**
* Handle job failure * Handle job failure
*/ */
public function failed(\Throwable $exception) public function failed(Throwable $exception)
{ {
$this->updateLogStatus('failed', [ $this->updateLogStatus('failed', [
'completed_at' => now(), 'completed_at' => now(),

View File

@@ -2,11 +2,18 @@
namespace Modules\Webstatement\Mail; namespace Modules\Webstatement\Mail;
use Carbon\Carbon;
use Exception;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use Log;
use Modules\Webstatement\Models\Account; use Modules\Webstatement\Models\Account;
use Modules\Webstatement\Models\PrintStatementLog; use Modules\Webstatement\Models\PrintStatementLog;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
use Symfony\Component\Mime\Email;
class StatementEmail extends Mailable class StatementEmail extends Mailable
{ {
@@ -15,6 +22,7 @@ class StatementEmail extends Mailable
protected $statement; protected $statement;
protected $filePath; protected $filePath;
protected $isZip; protected $isZip;
protected $message;
/** /**
* Create a new message instance. * Create a new message instance.
@@ -31,6 +39,92 @@ class StatementEmail extends Mailable
$this->isZip = $isZip; $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. * Build the message.
* Membangun struktur email dengan attachment statement * Membangun struktur email dengan attachment statement
@@ -44,12 +138,44 @@ class StatementEmail extends Mailable
if ($this->statement->is_period_range) { if ($this->statement->is_period_range) {
$subject .= " - {$this->statement->period_from} to {$this->statement->period_to}"; $subject .= " - {$this->statement->period_from} to {$this->statement->period_to}";
} else { } else {
$subject .= " - " . \Carbon\Carbon::createFromFormat('Ym', $this->statement->period_from)->locale('id')->isoFormat('MMMM Y'); $subject .= " - " . Carbon::createFromFormat('Ym', $this->statement->period_from)
->locale('id')
->isoFormat('MMMM Y');
} }
$email = $this->subject($subject) $email = $this->subject($subject);
->view('webstatement::statements.email')
->with([ // 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, 'statement' => $this->statement,
'accountNumber' => $this->statement->account_number, 'accountNumber' => $this->statement->account_number,
'periodFrom' => $this->statement->period_from, 'periodFrom' => $this->statement->period_from,
@@ -58,20 +184,19 @@ class StatementEmail extends Mailable
'requestType' => $this->statement->request_type, 'requestType' => $this->statement->request_type,
'batchId' => $this->statement->batch_id, 'batchId' => $this->statement->batch_id,
'accounts' => Account::where('account_number', $this->statement->account_number)->first() '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) { if ($this->isZip) {
$fileName = "{$this->statement->account_number}_{$this->statement->period_from}_to_{$this->statement->period_to}.zip"; $fileName = "{$this->statement->account_number}_{$this->statement->period_from}_to_{$this->statement->period_to}.zip";
$email->attach($this->filePath, [ $contentType = 'application/zip';
'as' => $fileName,
'mime' => 'application/zip',
]);
} else { } else {
$fileName = "{$this->statement->account_number}_{$this->statement->period_from}.pdf"; $fileName = "{$this->statement->account_number}_{$this->statement->period_from}.pdf";
$email->attach($this->filePath, [ $contentType = 'application/pdf';
'as' => $fileName, }
'mime' => 'application/pdf', $email->attachFromPath($this->filePath, $fileName, $contentType);
]);
} }
return $email; return $email;

View File

@@ -16,8 +16,8 @@
} }
.container { .container {
max-width: 90%; max-width: 100%;
margin: 20px auto; margin: 0px auto;
background-color: #ffffff; background-color: #ffffff;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
@@ -37,7 +37,7 @@
} }
.content { .content {
padding: 30px; padding: 5px;
font-size: 14px; font-size: 14px;
} }
@@ -103,11 +103,7 @@
Terima Kasih,<br><br> Terima Kasih,<br><br>
<strong>Bank Artha Graha Internasional</strong><br> <strong>Bank Artha Graha Internasional</strong><br>
------------------------------ ------------------------------------------------------------<br>
<wbr>
------------------------------
<wbr>
--------<br>
Kami sangat menghargai masukan dan saran Anda untuk meningkatkan layanan dan produk kami.<br> Kami sangat menghargai masukan dan saran Anda untuk meningkatkan layanan dan produk kami.<br>
Untuk memberikan masukan, silakan hubungi <strong>GrahaCall 24 Jam</strong> kami di Untuk memberikan masukan, silakan hubungi <strong>GrahaCall 24 Jam</strong> kami di
<strong>0-800-191-8880</strong>.<br><br><br> <strong>0-800-191-8880</strong>.<br><br><br>
@@ -132,11 +128,7 @@
Regards,<br><br> Regards,<br><br>
<strong>Bank Artha Graha Internasional</strong><br> <strong>Bank Artha Graha Internasional</strong><br>
------------------------------ ------------------------------------------------------------<br>
<wbr>
------------------------------
<wbr>
--------<br>
We welcome any feedback or suggestions to improve our product and services.<br> We welcome any feedback or suggestions to improve our product and services.<br>
If you have any feedback, please contact our <strong>GrahaCall 24 Hours</strong> at If you have any feedback, please contact our <strong>GrahaCall 24 Hours</strong> at
<strong>0-800-191-8880</strong>. <strong>0-800-191-8880</strong>.
@@ -145,10 +137,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="footer">
<p>© {{ date('Y') }} Bank Artha Graha Internasional. All rights reserved.</p>
<p>Jika Anda memiliki pertanyaan, silakan hubungi customer service kami.</p>
</div>
</div> </div>
</body> </body>