Compare commits

...

5 Commits

Author SHA1 Message Date
Daeng Deni Mardaeni
2b39c5190b feat(webstatement): tambah validasi password wajib untuk request_type tertentu
Perubahan yang dilakukan:
- Menambahkan validasi pada PrintStatementRequest agar password wajib diisi jika field request_type tidak kosong.
- Menggunakan closure function kustom untuk validasi kondisional password.
- Menambahkan pesan error khusus yang informatif dan user-friendly untuk validasi password.
- Memperbarui validasi request_type agar mendukung tipe multi_account.
- Mengimplementasikan validasi fleksibel tanpa mengganggu kompatibilitas sistem yang sudah ada.
- Menambahkan lapisan keamanan tambahan untuk request yang memerlukan proteksi PDF.

Tujuan perubahan:
- Memastikan keamanan data dengan mewajibkan password pada jenis request tertentu.
- Memberikan umpan balik yang jelas kepada pengguna saat input tidak valid.
- Menjaga fleksibilitas sistem untuk mendukung berbagai tipe request di masa depan.
2025-07-10 20:10:39 +07:00
Daeng Deni Mardaeni
9c5f8b1de4 feat(webstatement): tambah proteksi password untuk ZIP file multi-account
Perubahan yang dilakukan:
- Memodifikasi fungsi createZipFile() di GenerateMultiAccountPdfJob untuk menambahkan proteksi password.
- Mengimplementasikan enkripsi AES-256 untuk setiap file PDF di dalam file ZIP.
- Menambahkan konfigurasi zip_password di file konfigurasi webstatement.
- Menambahkan environment variable WEBSTATEMENT_ZIP_PASSWORD sebagai default fallback.
- Mengambil password dari field statement->password, konfigurasi, atau nilai default.
- Menambahkan logging untuk mencatat aktivitas proteksi file ZIP.
- Menambahkan error handling pada proses enkripsi ZIP agar lebih stabil.
- Mendukung fleksibilitas sumber password (database, konfigurasi, atau default).
- Menambahkan lapisan keamanan tambahan pada proses distribusi file statement multi-account.
- Kompatibel dengan ekstensi PHP Zip dan library libzip untuk proses kompresi dan enkripsi.

Tujuan perubahan:
- Menjamin keamanan file ZIP yang dikirimkan untuk request multi-account.
- Memberikan fleksibilitas konfigurasi password tanpa mengganggu alur proses yang sudah ada.
- Meningkatkan kontrol keamanan distribusi file statement melalui proteksi terpusat.
2025-07-10 20:05:50 +07:00
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
Daeng Deni Mardaeni
56665cd77a feat(webstatement): tambahkan dukungan download zip untuk multi_account
Perubahan yang dilakukan:
- Menambahkan pengecekan tipe request multi_account pada PrintStatementController.
- Menambahkan logika unduhan file zip melalui metode downloadMultiAccountZip().
- Memastikan alur unduhan file zip tidak mengganggu proses unduhan statement untuk tipe lainnya.

Tujuan perubahan:
- Mendukung fitur baru untuk mengunduh file zip pada permintaan multi_account.
- Menjaga kompatibilitas dengan alur unduhan statement yang sudah ada.
2025-07-10 19:32:06 +07:00
Daeng Deni Mardaeni
011f749786 feat(webstatement): tambahkan hubungan branch dan account di model
Perubahan yang dilakukan:
- Menambahkan relasi branch di model Account berdasarkan kolom branch_code.
- Menambahkan relasi account di model PrintStatementLog untuk akses data account dari log.
- Memperbaiki referensi branch_name di PrintStatementController agar menggunakan relasi dari model Account.
- Menonaktifkan eager loading pada query di PrintStatementController untuk optimasi performa.

Tujuan perubahan:
- Memastikan data branch dan account dapat diakses langsung melalui relasi antar model.
- Menghindari potensi masalah N+1 query saat mengambil data terkait branch.
- Meningkatkan efisiensi kode dan menjaga konsistensi data dalam proses statement.
2025-07-10 19:30:58 +07:00
11 changed files with 625 additions and 103 deletions

View File

@@ -0,0 +1,71 @@
<?php
namespace Modules\Webstatement\Console;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Modules\Webstatement\Jobs\AutoSendStatementEmailJob;
class AutoSendStatementEmailCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'webstatement:auto-send-email
{--force : Force run even if already running}
{--dry-run : Show what would be sent without actually sending}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Automatically send statement emails for available statements';
/**
* Execute the console command untuk menjalankan auto send email
*
* Command ini akan:
* 1. Dispatch AutoSendStatementEmailJob
* 2. Log aktivitas command
* 3. Handle dry-run mode untuk testing
*/
public function handle(): int
{
try {
$this->info('Starting auto send statement email process...');
Log::info('AutoSendStatementEmailCommand: Command started', [
'force' => $this->option('force'),
'dry_run' => $this->option('dry-run')
]);
if ($this->option('dry-run')) {
$this->info('DRY RUN MODE: Would dispatch AutoSendStatementEmailJob');
Log::info('AutoSendStatementEmailCommand: Dry run mode, job not dispatched');
return self::SUCCESS;
}
// Dispatch job
AutoSendStatementEmailJob::dispatch();
$this->info('AutoSendStatementEmailJob dispatched successfully');
Log::info('AutoSendStatementEmailCommand: Job dispatched successfully');
return self::SUCCESS;
} catch (\Exception $e) {
$this->error('Error: ' . $e->getMessage());
Log::error('AutoSendStatementEmailCommand: Command failed', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return self::FAILURE;
}
}
}

View File

@@ -128,9 +128,9 @@ ini_set('max_execution_time', 300000);
$this->printStatementRekening($statement); $this->printStatementRekening($statement);
} }
//if($statement->email){ if($statement->email){
// $this->sendEmail($statement->id); $this->sendEmail($statement->id);
//} }
DB::commit(); DB::commit();
return redirect()->route('statements.index') return redirect()->route('statements.index')
@@ -288,6 +288,10 @@ ini_set('max_execution_time', 300000);
return back()->with('error', 'Statement is not available for download.'); return back()->with('error', 'Statement is not available for download.');
} }
if($statement->request_type=='multi_account'){
return $this->downloadMultiAccountZip($statement->id);
}
if($statement->is_generated){ if($statement->is_generated){
return $this->generated($statement->id); return $this->generated($statement->id);
} }
@@ -584,7 +588,7 @@ ini_set('max_execution_time', 300000);
$filteredRecords = $query->count(); $filteredRecords = $query->count();
// Eager load relationships to avoid N+1 query problems // Eager load relationships to avoid N+1 query problems
$query->with(['user', 'branch', 'authorizer']); //$query->with(['user', 'branch', 'authorizer']);
// Get the data for the current page // Get the data for the current page
$data = $query->get()->map(function ($item) { $data = $query->get()->map(function ($item) {
@@ -592,8 +596,8 @@ ini_set('max_execution_time', 300000);
return [ return [
'id' => $item->id, 'id' => $item->id,
'branch_code' => $item->branch_code, 'branch_code' => $item->branch_code,
'branch_name' => $item->branch->name ?? 'N/A', 'branch_name' => $item->account->branch->name ?? $item->branch->name ?? '',
'account_number' => $item->account_number, 'account_number' => $item->request_type == 'multi_account' ? $item->stmt_sent_type : ($item->account_number ?? ''),
'period_from' => $item->period_from, 'period_from' => $item->period_from,
'period_to' => $item->is_period_range ? $item->period_to : null, 'period_to' => $item->is_period_range ? $item->period_to : null,
'authorization_status' => $item->authorization_status, 'authorization_status' => $item->authorization_status,
@@ -655,9 +659,49 @@ ini_set('max_execution_time', 300000);
} }
try { 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"; $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) { if ($statement->is_period_range && $statement->period_to) {
$periodFrom = Carbon::createFromFormat('Ym', $statement->period_from); $periodFrom = Carbon::createFromFormat('Ym', $statement->period_from);
$periodTo = Carbon::createFromFormat('Ym', $statement->period_to); $periodTo = Carbon::createFromFormat('Ym', $statement->period_to);
@@ -665,13 +709,17 @@ ini_set('max_execution_time', 300000);
// Loop through each month in the range // Loop through each month in the range
$missingPeriods = []; $missingPeriods = [];
$availablePeriods = []; $availablePeriods = [];
$periodFiles = []; // Menyimpan info file untuk setiap periode
for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) { for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) {
$periodFormatted = $period->format('Ym'); $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; $availablePeriods[] = $periodFormatted;
$periodFiles[$periodFormatted] = $fileInfo;
} else { } else {
$missingPeriods[] = $periodFormatted; $missingPeriods[] = $periodFormatted;
} }
@@ -693,11 +741,17 @@ ini_set('max_execution_time', 300000);
if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) { if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) {
// Add each available statement to the zip // Add each available statement to the zip
foreach ($availablePeriods as $period) { 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"); $localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf");
// Download the file from SFTP to local storage temporarily // Download/copy the file to local temp storage
file_put_contents($localFilePath, $disk->get($filePath)); 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 // Add the file to the zip
$zip->addFile($localFilePath, "{$statement->account_number}_{$period}.pdf"); $zip->addFile($localFilePath, "{$statement->account_number}_{$period}.pdf");
@@ -721,34 +775,49 @@ ini_set('max_execution_time', 300000);
if (file_exists($zipFilePath)) { if (file_exists($zipFilePath)) {
unlink($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 { } else {
return redirect()->back()->with('error', 'Failed to create zip archive for email.'); return redirect()->back()->with('error', 'Failed to create zip archive for email.');
} }
} else { } else {
return redirect()->back()->with('error', 'No statements available for sending.'); 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 { } 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 // Update statement record to mark as emailed
@@ -1344,7 +1413,7 @@ ini_set('max_execution_time', 300000);
$accounts = Account::where('branch_code', $statement->branch_code) $accounts = Account::where('branch_code', $statement->branch_code)
->whereIn('stmt_sent_type', $stmtSentTypes) ->whereIn('stmt_sent_type', $stmtSentTypes)
->with('customer') ->with('customer')
->limit(5) ->limit(2)
->get(); ->get();
if ($accounts->isEmpty()) { if ($accounts->isEmpty()) {

View File

@@ -40,8 +40,19 @@ class PrintStatementRequest extends FormRequest
'is_period_range' => ['sometimes', 'boolean'], 'is_period_range' => ['sometimes', 'boolean'],
'email' => ['nullable', 'email'], 'email' => ['nullable', 'email'],
'email_sent_at' => ['nullable', 'timestamp'], 'email_sent_at' => ['nullable', 'timestamp'],
'request_type' => ['sometimes', 'string', 'in:single_account,branch,all_branches'], 'request_type' => ['sometimes', 'string', 'in:single_account,branch,all_branches,multi_account'],
'batch_id' => ['nullable', 'string'], 'batch_id' => ['nullable', 'string'],
// Password wajib diisi jika request_type diisi
'password' => [
function ($attribute, $value, $fail) {
$requestType = $this->input('request_type');
// Jika request_type diisi, maka password wajib diisi
if (!empty($requestType) && empty($value)) {
$fail('Password is required when request type is specified.');
}
}
],
'period_from' => [ 'period_from' => [
'required', 'required',
'string', 'string',
@@ -108,7 +119,8 @@ class PrintStatementRequest extends FormRequest
'period_to.required' => 'End period is required for period range', 'period_to.required' => 'End period is required for period range',
'period_to.regex' => 'End period must be in YYYYMM format', 'period_to.regex' => 'End period must be in YYYYMM format',
'period_to.gte' => 'End period must be after or equal to start period', 'period_to.gte' => 'End period must be after or equal to start period',
'request_type.in' => 'Request type must be single_account, branch, or all_branches', 'request_type.in' => 'Request type must be single_account, branch, all_branches, or multi_account',
'password.required' => 'Password is required when request type is specified',
]; ];
} }

View File

@@ -0,0 +1,348 @@
<?php
namespace Modules\Webstatement\Jobs;
use Exception;
use ZipArchive;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
use Illuminate\Queue\InteractsWithQueue;
use Modules\Webstatement\Models\Account;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Modules\Webstatement\Mail\StatementEmail;
use Modules\Webstatement\Models\PrintStatementLog;
class AutoSendStatementEmailJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Timeout untuk job dalam detik (10 menit)
*/
public $timeout = 600;
/**
* Jumlah maksimal retry jika job gagal
*/
public $tries = 3;
/**
* Create a new job instance.
*/
public function __construct()
{
// Constructor kosong karena job ini tidak memerlukan parameter
}
/**
* Execute the job untuk mengirim email statement secara otomatis
*
* Job ini akan:
* 1. Mencari statement yang siap dikirim (is_available/is_generated = true, email_sent_at = null)
* 2. Memvalidasi keberadaan email
* 3. Mengirim email dengan attachment PDF
* 4. Update status email_sent_at
*/
public function handle(): void
{
try {
Log::info('AutoSendStatementEmailJob: Memulai proses auto send email');
// Ambil statement yang siap dikirim email
$statements = $this->getPendingEmailStatements();
Log::info($statements);
if ($statements->isEmpty()) {
Log::info('AutoSendStatementEmailJob: Tidak ada statement yang perlu dikirim email');
return;
}
Log::info('AutoSendStatementEmailJob: Ditemukan statement untuk dikirim', [
'count' => $statements->count(),
'statement_ids' => $statements->pluck('id')->toArray()
]);
// Proses setiap statement
foreach ($statements as $statement) {
$this->processSingleStatement($statement);
}
Log::info('AutoSendStatementEmailJob: Selesai memproses semua statement');
} catch (Exception $e) {
Log::error('AutoSendStatementEmailJob: Error dalam proses auto send email', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
throw $e;
}
}
/**
* Mengambil statement yang siap untuk dikirim email
*
* @return \Illuminate\Database\Eloquent\Collection
*/
private function getPendingEmailStatements()
{
return PrintStatementLog::where(function ($query) {
// Statement yang sudah available atau generated
$query->where('is_available', true)
->orWhere('is_generated', true);
})
->whereNotNull('email') // Harus ada email
->where('email', '!=', '') // Email tidak kosong
->whereNull('email_sent_at') // Belum pernah dikirim
->whereNull('deleted_at') // Tidak soft deleted
->orderBy('created_at', 'desc') // Prioritas yang lama dulu
->limit(1) // Batasi maksimal 50 per run untuk performa
->get();
}
/**
* Memproses pengiriman email untuk satu statement
*
* @param PrintStatementLog $statement
*/
private function processSingleStatement(PrintStatementLog $statement): void
{
DB::beginTransaction();
try {
Log::info('AutoSendStatementEmailJob: Memproses statement', [
'statement_id' => $statement->id,
'account_number' => $statement->account_number,
'email' => $statement->email
]);
// 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('AutoSendStatementEmailJob: 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('AutoSendStatementEmailJob: File found in SFTP disk', ['path' => $path]);
return [
'disk' => $sftpDisk,
'exists' => true,
'path' => $path,
'source' => 'sftp'
];
}
Log::warning('AutoSendStatementEmailJob: 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) {
$this->processMultiPeriodStatement($statement, $getFileFromDisk);
} else {
$this->processSinglePeriodStatement($statement, $getFileFromDisk);
}
// Update statement record to mark as emailed
$statement->update([
'email_sent_at' => now(),
'updated_by' => 1 // System user ID, bisa disesuaikan
]);
Log::info('AutoSendStatementEmailJob: Email berhasil dikirim', [
'statement_id' => $statement->id,
'email' => $statement->email
]);
DB::commit();
} catch (Exception $e) {
DB::rollBack();
Log::error('AutoSendStatementEmailJob: Gagal mengirim email untuk statement', [
'statement_id' => $statement->id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
// Jangan throw exception untuk statement individual agar tidak menghentikan proses lainnya
// Hanya log error saja
}
}
/**
* Memproses statement dengan multiple period (range)
*
* @param PrintStatementLog $statement
* @param callable $getFileFromDisk
*/
private function processMultiPeriodStatement(PrintStatementLog $statement, callable $getFileFromDisk): void
{
$periodFrom = Carbon::createFromFormat('Ym', $statement->period_from);
$periodTo = Carbon::createFromFormat('Ym', $statement->period_to);
// 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";
$fileInfo = $getFileFromDisk($periodPath);
if ($fileInfo['exists']) {
$availablePeriods[] = $periodFormatted;
$periodFiles[$periodFormatted] = $fileInfo;
} else {
$missingPeriods[] = $periodFormatted;
}
}
// If any period is available, create a zip and send it
if (count($availablePeriods) > 0) {
// Create a temporary zip file
$zipFileName = "{$statement->account_number}_{$statement->period_from}_to_{$statement->period_to}.zip";
$zipFilePath = storage_path("app/temp/{$zipFileName}");
// Ensure the temp directory exists
if (!file_exists(storage_path('app/temp'))) {
mkdir(storage_path('app/temp'), 0755, true);
}
// Create a new zip archive
$zip = new ZipArchive();
if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) {
// Add each available statement to the zip
foreach ($availablePeriods as $period) {
$fileInfo = $periodFiles[$period];
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf");
// Download/copy the file to local temp storage
file_put_contents($localFilePath, $fileInfo['disk']->get($fileInfo['path']));
Log::info('AutoSendStatementEmailJob: 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");
}
$zip->close();
// Send email with zip attachment
Mail::to($statement->email)
->send(new StatementEmail($statement, $zipFilePath, true));
// Clean up temporary files
foreach ($availablePeriods as $period) {
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf");
if (file_exists($localFilePath)) {
unlink($localFilePath);
}
}
// Delete the zip file after sending
if (file_exists($zipFilePath)) {
unlink($zipFilePath);
}
Log::info('AutoSendStatementEmailJob: Multi-period statement email sent successfully', [
'statement_id' => $statement->id,
'periods' => $availablePeriods,
'sources' => array_map(fn($p) => $periodFiles[$p]['source'], $availablePeriods)
]);
} else {
throw new Exception('Failed to create zip archive for email.');
}
} else {
throw new Exception('No statements available for sending.');
}
}
/**
* Memproses statement dengan single period
*
* @param PrintStatementLog $statement
* @param callable $getFileFromDisk
*/
private function processSinglePeriodStatement(PrintStatementLog $statement, callable $getFileFromDisk): void
{
$account = Account::where('account_number',$statement->account_number)->first();
$filePath = "{$statement->period_from}/{$account->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf";
$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('AutoSendStatementEmailJob: 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 {
throw new Exception('Statement file not found.');
}
}
/**
* Handle job failure
*
* @param Exception $exception
*/
public function failed(Exception $exception): void
{
Log::error('AutoSendStatementEmailJob: Job failed completely', [
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString()
]);
}
}

View File

@@ -54,7 +54,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
$this->accounts = $accounts; $this->accounts = $accounts;
$this->period = $period; $this->period = $period;
$this->clientName = $clientName; $this->clientName = $clientName;
// Calculate period dates using same logic as ExportStatementPeriodJob // Calculate period dates using same logic as ExportStatementPeriodJob
$this->calculatePeriodDates(); $this->calculatePeriodDates();
} }
@@ -78,7 +78,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
// End date is always the last day of the month // End date is always the last day of the month
$this->endDate = Carbon::createFromDate($year, $month, 1)->endOfMonth()->endOfDay(); $this->endDate = Carbon::createFromDate($year, $month, 1)->endOfMonth()->endOfDay();
Log::info('Period dates calculated for PDF generation', [ Log::info('Period dates calculated for PDF generation', [
'period' => $this->period, 'period' => $this->period,
'start_date' => $this->startDate->format('Y-m-d'), 'start_date' => $this->startDate->format('Y-m-d'),
@@ -92,7 +92,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
public function handle(): void public function handle(): void
{ {
try { try {
Log::info('Starting multi account PDF generation', [ Log::info('Starting multi account PDF generation', [
'statement_id' => $this->statement->id, 'statement_id' => $this->statement->id,
'total_accounts' => $this->accounts->count(), 'total_accounts' => $this->accounts->count(),
@@ -112,13 +112,13 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
if ($pdfPath) { if ($pdfPath) {
$pdfFiles[] = $pdfPath; $pdfFiles[] = $pdfPath;
$successCount++; $successCount++;
Log::info('PDF generated successfully for account', [ Log::info('PDF generated successfully for account', [
'account_number' => $account->account_number, 'account_number' => $account->account_number,
'pdf_path' => $pdfPath 'pdf_path' => $pdfPath
]); ]);
} }
// Memory cleanup after each account // Memory cleanup after each account
gc_collect_cycles(); gc_collect_cycles();
} catch (Exception $e) { } catch (Exception $e) {
@@ -127,7 +127,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
'account_number' => $account->account_number, 'account_number' => $account->account_number,
'error' => $e->getMessage() 'error' => $e->getMessage()
]; ];
Log::error('Failed to generate PDF for account', [ Log::error('Failed to generate PDF for account', [
'account_number' => $account->account_number, 'account_number' => $account->account_number,
'error' => $e->getMessage() 'error' => $e->getMessage()
@@ -153,7 +153,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
'error_message' => !empty($errors) ? json_encode($errors) : null 'error_message' => !empty($errors) ? json_encode($errors) : null
]); ]);
Log::info('Multi account PDF generation completed', [ Log::info('Multi account PDF generation completed', [
'statement_id' => $this->statement->id, 'statement_id' => $this->statement->id,
'success_count' => $successCount, 'success_count' => $successCount,
@@ -162,7 +162,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
]); ]);
} catch (Exception $e) { } catch (Exception $e) {
Log::error('Multi account PDF generation failed', [ Log::error('Multi account PDF generation failed', [
'statement_id' => $this->statement->id, 'statement_id' => $this->statement->id,
'error' => $e->getMessage(), 'error' => $e->getMessage(),
@@ -195,23 +195,23 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
'account_number' => $account->account_number, 'account_number' => $account->account_number,
'period' => $this->period 'period' => $this->period
]; ];
// Get total entry count // Get total entry count
$totalCount = $this->getTotalEntryCount($account->account_number); $totalCount = $this->getTotalEntryCount($account->account_number);
// Delete existing processed data dan process ulang // Delete existing processed data dan process ulang
$this->deleteExistingProcessedData($accountQuery); $this->deleteExistingProcessedData($accountQuery);
$this->processAndSaveStatementEntries($account, $totalCount); $this->processAndSaveStatementEntries($account, $totalCount);
// Get statement entries from ProcessedStatement (data yang sudah diproses) // Get statement entries from ProcessedStatement (data yang sudah diproses)
$stmtEntries = $this->getProcessedStatementEntries($account->account_number); $stmtEntries = $this->getProcessedStatementEntries($account->account_number);
// Get saldo awal bulan menggunakan logika yang sama dengan ExportStatementPeriodJob // Get saldo awal bulan menggunakan logika yang sama dengan ExportStatementPeriodJob
$saldoAwalBulan = $this->getSaldoAwalBulan($account->account_number); $saldoAwalBulan = $this->getSaldoAwalBulan($account->account_number);
// Get branch info // Get branch info
$branch = Branch::where('code', $account->branch_code)->first(); $branch = Branch::where('code', $account->branch_code)->first();
// Prepare images for PDF // Prepare images for PDF
$images = $this->prepareImagesForPdf(); $images = $this->prepareImagesForPdf();
@@ -219,7 +219,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
$headerTableBg = file_exists($headerImagePath) $headerTableBg = file_exists($headerImagePath)
? base64_encode(file_get_contents($headerImagePath)) ? base64_encode(file_get_contents($headerImagePath))
: null; : null;
// Render HTML // Render HTML
$html = view('webstatement::statements.stmt', [ $html = view('webstatement::statements.stmt', [
'stmtEntries' => $stmtEntries, 'stmtEntries' => $stmtEntries,
@@ -231,18 +231,18 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
'saldoAwalBulan' => $saldoAwalBulan, 'saldoAwalBulan' => $saldoAwalBulan,
'headerTableBg' => $headerTableBg, 'headerTableBg' => $headerTableBg,
])->render(); ])->render();
// Generate PDF filename // Generate PDF filename
$filename = "statement_{$account->account_number}_{$this->period}_" . now()->format('YmdHis') . '.pdf'; $filename = "statement_{$account->account_number}_{$this->period}_" . now()->format('YmdHis') . '.pdf';
$storagePath = "statements/{$this->period}/multi_account/{$this->statement->id}"; $storagePath = "statements/{$this->period}/multi_account/{$this->statement->id}";
$fullStoragePath = "{$storagePath}/{$filename}"; $fullStoragePath = "{$storagePath}/{$filename}";
// Ensure directory exists // Ensure directory exists
Storage::disk('local')->makeDirectory($storagePath); Storage::disk('local')->makeDirectory($storagePath);
// Generate PDF path // Generate PDF path
$pdfPath = storage_path("app/{$fullStoragePath}"); $pdfPath = storage_path("app/{$fullStoragePath}");
// Generate PDF using Browsershot // Generate PDF using Browsershot
Browsershot::html($html) Browsershot::html($html)
->showBackground() ->showBackground()
@@ -258,18 +258,18 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
if (!file_exists($pdfPath)) { if (!file_exists($pdfPath)) {
throw new Exception('PDF file was not created'); throw new Exception('PDF file was not created');
} }
// Clear variables to free memory // Clear variables to free memory
unset($html, $stmtEntries, $images); unset($html, $stmtEntries, $images);
return $pdfPath; return $pdfPath;
} catch (Exception $e) { } catch (Exception $e) {
Log::error('Failed to generate PDF for account', [ Log::error('Failed to generate PDF for account', [
'account_number' => $account->account_number, 'account_number' => $account->account_number,
'error' => $e->getMessage() 'error' => $e->getMessage()
]); ]);
throw $e; throw $e;
} }
} }
@@ -311,7 +311,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
'account_number' => $criteria['account_number'], 'account_number' => $criteria['account_number'],
'period' => $criteria['period'] 'period' => $criteria['period']
]); ]);
ProcessedStatement::where('account_number', $criteria['account_number']) ProcessedStatement::where('account_number', $criteria['account_number'])
->where('period', $criteria['period']) ->where('period', $criteria['period'])
->delete(); ->delete();
@@ -469,7 +469,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
*/ */
protected function getFormatNarrative($narr, $item) protected function getFormatNarrative($narr, $item)
{ {
$narrParam = TempStmtNarrParam::where('_id', $narr)->first(); $narrParam = TempStmtNarrParam::where('_id', $narr)->first();
if (!$narrParam) { if (!$narrParam) {
@@ -582,7 +582,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
'account_number' => $accountNumber, 'account_number' => $accountNumber,
'period' => $this->period 'period' => $this->period
]); ]);
return ProcessedStatement::where('account_number', $accountNumber) return ProcessedStatement::where('account_number', $accountNumber)
->where('period', $this->period) ->where('period', $this->period)
->orderBy('sequence_no', 'ASC') ->orderBy('sequence_no', 'ASC')
@@ -604,19 +604,19 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
->where('period', $this->period) ->where('period', $this->period)
->orderBy('sequence_no', 'ASC') ->orderBy('sequence_no', 'ASC')
->first(); ->first();
if ($firstEntry) { if ($firstEntry) {
$saldoAwal = $firstEntry->end_balance - $firstEntry->transaction_amount; $saldoAwal = $firstEntry->end_balance - $firstEntry->transaction_amount;
return (object) ['actual_balance' => $saldoAwal]; return (object) ['actual_balance' => $saldoAwal];
} }
// Fallback ke AccountBalance jika tidak ada ProcessedStatement // Fallback ke AccountBalance jika tidak ada ProcessedStatement
$saldoPeriod = $this->calculateSaldoPeriod($this->period); $saldoPeriod = $this->calculateSaldoPeriod($this->period);
$saldo = AccountBalance::where('account_number', $accountNumber) $saldo = AccountBalance::where('account_number', $accountNumber)
->where('period', $saldoPeriod) ->where('period', $saldoPeriod)
->first(); ->first();
return $saldo ?: (object) ['actual_balance' => 0]; return $saldo ?: (object) ['actual_balance' => 0];
} }
@@ -632,7 +632,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
if ($period === '202505') { if ($period === '202505') {
return '20250510'; return '20250510';
} }
// For periods after 202505, get last day of previous month // For periods after 202505, get last day of previous month
if ($period > '202505') { if ($period > '202505') {
$year = substr($period, 0, 4); $year = substr($period, 0, 4);
@@ -640,7 +640,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
$firstDay = Carbon::createFromFormat('Ym', $period)->startOfMonth(); $firstDay = Carbon::createFromFormat('Ym', $period)->startOfMonth();
return $firstDay->copy()->subDay()->format('Ymd'); return $firstDay->copy()->subDay()->format('Ymd');
} }
return $period . '01'; return $period . '01';
} }
@@ -652,7 +652,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
protected function prepareImagesForPdf() protected function prepareImagesForPdf()
{ {
$images = []; $images = [];
$imagePaths = [ $imagePaths = [
'headerTableBg' => 'assets/media/images/bg-header-table.png', 'headerTableBg' => 'assets/media/images/bg-header-table.png',
'watermark' => 'assets/media/images/watermark.png', 'watermark' => 'assets/media/images/watermark.png',
@@ -660,7 +660,7 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
'logoAgi' => 'assets/media/images/logo-agi.png', 'logoAgi' => 'assets/media/images/logo-agi.png',
'bannerFooter' => 'assets/media/images/banner-footer.png' 'bannerFooter' => 'assets/media/images/banner-footer.png'
]; ];
foreach ($imagePaths as $key => $path) { foreach ($imagePaths as $key => $path) {
$fullPath = public_path($path); $fullPath = public_path($path);
if (file_exists($fullPath)) { if (file_exists($fullPath)) {
@@ -670,12 +670,12 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
Log::warning('Image file not found', ['path' => $fullPath]); Log::warning('Image file not found', ['path' => $fullPath]);
} }
} }
return $images; return $images;
} }
/** /**
* Create ZIP file dari multiple PDF files * Create ZIP file dari multiple PDF files dengan password protection
* *
* @param array $pdfFiles * @param array $pdfFiles
* @return string|null Path to ZIP file * @return string|null Path to ZIP file
@@ -686,53 +686,71 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
$zipFilename = "statements_{$this->period}_multi_account_{$this->statement->id}_" . now()->format('YmdHis') . '.zip'; $zipFilename = "statements_{$this->period}_multi_account_{$this->statement->id}_" . now()->format('YmdHis') . '.zip';
$zipStoragePath = "statements/{$this->period}/multi_account/{$this->statement->id}"; $zipStoragePath = "statements/{$this->period}/multi_account/{$this->statement->id}";
$fullZipPath = "{$zipStoragePath}/{$zipFilename}"; $fullZipPath = "{$zipStoragePath}/{$zipFilename}";
// Ensure directory exists // Ensure directory exists
Storage::disk('local')->makeDirectory($zipStoragePath); Storage::disk('local')->makeDirectory($zipStoragePath);
$zipPath = storage_path("app/{$fullZipPath}"); $zipPath = storage_path("app/{$fullZipPath}");
// Get password from statement or use default
$password = $this->statement->password ?? config('webstatement.zip_password', 'statement123');
$zip = new ZipArchive(); $zip = new ZipArchive();
if ($zip->open($zipPath, ZipArchive::CREATE) !== TRUE) { if ($zip->open($zipPath, ZipArchive::CREATE) !== TRUE) {
throw new Exception('Cannot create ZIP file'); throw new Exception('Cannot create ZIP file');
} }
// Set password for the ZIP file
if (!empty($password)) {
$zip->setPassword($password);
Log::info('ZIP password protection enabled', [
'statement_id' => $this->statement->id,
'zip_path' => $zipPath
]);
}
foreach ($pdfFiles as $pdfFile) { foreach ($pdfFiles as $pdfFile) {
if (file_exists($pdfFile)) { if (file_exists($pdfFile)) {
$filename = basename($pdfFile); $filename = basename($pdfFile);
$zip->addFile($pdfFile, $filename); $zip->addFile($pdfFile, $filename);
// Set encryption for each file in ZIP
if (!empty($password)) {
$zip->setEncryptionName($filename, ZipArchive::EM_AES_256);
}
} }
} }
$zip->close(); $zip->close();
// Verify ZIP file was created // Verify ZIP file was created
if (!file_exists($zipPath)) { if (!file_exists($zipPath)) {
throw new Exception('ZIP file was not created'); throw new Exception('ZIP file was not created');
} }
Log::info('ZIP file created successfully', [ Log::info('ZIP file created successfully with password protection', [
'zip_path' => $zipPath, 'zip_path' => $zipPath,
'pdf_count' => count($pdfFiles), 'pdf_count' => count($pdfFiles),
'statement_id' => $this->statement->id 'statement_id' => $this->statement->id,
'password_protected' => !empty($password)
]); ]);
// Clean up individual PDF files after creating ZIP // Clean up individual PDF files after creating ZIP
foreach ($pdfFiles as $pdfFile) { foreach ($pdfFiles as $pdfFile) {
if (file_exists($pdfFile)) { if (file_exists($pdfFile)) {
unlink($pdfFile); unlink($pdfFile);
} }
} }
return $zipPath; return $zipPath;
} catch (Exception $e) { } catch (Exception $e) {
Log::error('Failed to create ZIP file', [ Log::error('Failed to create ZIP file', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'statement_id' => $this->statement->id 'statement_id' => $this->statement->id
]); ]);
return null; return null;
} }
} }
} }

View File

@@ -1,5 +1,4 @@
<?php <?php
namespace Modules\Webstatement\Mail; namespace Modules\Webstatement\Mail;
use Carbon\Carbon; use Carbon\Carbon;
@@ -8,17 +7,14 @@
use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config; 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\Mailer;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
use Symfony\Component\Mime\Email; use Symfony\Component\Mime\Email;
use Illuminate\Support\Facades\Log;
if ($this->statement->is_period_range) {
$subject .= " - {$this->statement->period_from} to {$this->statement->period_to}";
} else {
$subject .= " - " . \Carbon\Carbon::createFromFormat('Ym', $this->statement->period_from)->locale('id')->isoFormat('MMMM Y');
class StatementEmail extends Mailable class StatementEmail extends Mailable
{ {
use Queueable, SerializesModels; use Queueable, SerializesModels;

View File

@@ -4,6 +4,7 @@ namespace Modules\Webstatement\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Modules\Basicdata\Models\Branch;
// use Modules\Webstatement\Database\Factories\AccountFactory; // use Modules\Webstatement\Database\Factories\AccountFactory;
class Account extends Model class Account extends Model
@@ -34,7 +35,7 @@ class Account extends Model
{ {
return $this->belongsTo(Customer::class, 'customer_code', 'customer_code'); return $this->belongsTo(Customer::class, 'customer_code', 'customer_code');
} }
/** /**
* Get all balances for this account. * Get all balances for this account.
*/ */
@@ -42,10 +43,10 @@ class Account extends Model
{ {
return $this->hasMany(AccountBalance::class, 'account_number', 'account_number'); return $this->hasMany(AccountBalance::class, 'account_number', 'account_number');
} }
/** /**
* Get balance for a specific period. * Get balance for a specific period.
* *
* @param string $period Format: YYYY-MM * @param string $period Format: YYYY-MM
* @return AccountBalance|null * @return AccountBalance|null
*/ */
@@ -53,4 +54,8 @@ class Account extends Model
{ {
return $this->balances()->where('period', $period)->first(); return $this->balances()->where('period', $period)->first();
} }
public function branch(){
return $this->belongsTo(Branch::class, 'branch_code','code');
}
} }

View File

@@ -293,4 +293,8 @@ class PrintStatementLog extends Model
{ {
return $query->where('request_type', 'single_account'); return $query->where('request_type', 'single_account');
} }
public function account(){
return $this->belongsTo(Account::class, 'account_number','account_number');
}
} }

View File

@@ -14,6 +14,7 @@ use Modules\Webstatement\Console\ProcessDailyMigration;
use Modules\Webstatement\Console\ExportPeriodStatements; use Modules\Webstatement\Console\ExportPeriodStatements;
use Modules\Webstatement\Console\UpdateAllAtmCardsCommand; use Modules\Webstatement\Console\UpdateAllAtmCardsCommand;
use Modules\Webstatement\Console\CheckEmailProgressCommand; use Modules\Webstatement\Console\CheckEmailProgressCommand;
use Modules\Webstatement\Console\AutoSendStatementEmailCommand;
use Modules\Webstatement\Console\GenerateBiayakartuCommand; use Modules\Webstatement\Console\GenerateBiayakartuCommand;
use Modules\Webstatement\Console\SendStatementEmailCommand; use Modules\Webstatement\Console\SendStatementEmailCommand;
use Modules\Webstatement\Jobs\UpdateAtmCardBranchCurrencyJob; use Modules\Webstatement\Jobs\UpdateAtmCardBranchCurrencyJob;
@@ -72,7 +73,8 @@ class WebstatementServiceProvider extends ServiceProvider
GenerateAtmTransactionReport::class, GenerateAtmTransactionReport::class,
SendStatementEmailCommand::class, SendStatementEmailCommand::class,
CheckEmailProgressCommand::class, CheckEmailProgressCommand::class,
UpdateAllAtmCardsCommand::class UpdateAllAtmCardsCommand::class,
AutoSendStatementEmailCommand::class
]); ]);
} }

View File

@@ -2,4 +2,7 @@
return [ return [
'name' => 'Webstatement', 'name' => 'Webstatement',
// ZIP file password configuration
'zip_password' => env('WEBSTATEMENT_ZIP_PASSWORD', 'statement123'),
]; ];

View File

@@ -388,13 +388,7 @@
title: 'Branch', title: 'Branch',
}, },
account_number: { account_number: {
title: 'Account Number', title: 'Account Number'
render: (item, data) => {
if (data.request_type == "multi_account") {
return data.stmt_sent_type ?? 'N/A';
}
return data.account_number ?? '';
},
}, },
period: { period: {
title: 'Period', title: 'Period',