Compare commits
5 Commits
5b235def37
...
2b39c5190b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b39c5190b | ||
|
|
9c5f8b1de4 | ||
|
|
5469045b5a | ||
|
|
56665cd77a | ||
|
|
011f749786 |
71
app/Console/AutoSendStatementEmailCommand.php
Normal file
71
app/Console/AutoSendStatementEmailCommand.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
348
app/Jobs/AutoSendStatementEmailJob.php
Normal file
348
app/Jobs/AutoSendStatementEmailJob.php
Normal 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()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,7 @@
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'name' => 'Webstatement',
|
'name' => 'Webstatement',
|
||||||
|
|
||||||
|
// ZIP file password configuration
|
||||||
|
'zip_password' => env('WEBSTATEMENT_ZIP_PASSWORD', 'statement123'),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user