Compare commits
30 Commits
0cbb7c9a3c
...
1f140af94a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f140af94a | ||
|
|
c1a173c8f7 | ||
|
|
974bf1cc35 | ||
|
|
0ace1d5c70 | ||
|
|
595ab89390 | ||
|
|
34571483eb | ||
|
|
062bac2138 | ||
|
|
8ee0dd2218 | ||
|
|
51697f017e | ||
|
|
e2c9f3480d | ||
|
|
40f552cb66 | ||
|
|
65b846f0c7 | ||
|
|
a3060322f9 | ||
|
|
428792ed1b | ||
|
|
4616137e0c | ||
|
|
19c962307e | ||
|
|
a79b1bd99e | ||
|
|
fd5b8e1dad | ||
|
|
8fb16028d9 | ||
|
|
6035c61cc4 | ||
|
|
2c8f49af20 | ||
|
|
4bfd937490 | ||
|
|
7b32cb8d39 | ||
|
|
4b889da5a5 | ||
|
|
dbdeceb4c0 | ||
|
|
f7a92a5336 | ||
|
|
b717749450 | ||
|
|
e5b8dfc7c4 | ||
|
|
d5482fb824 | ||
|
|
f6df453ddc |
@@ -1,22 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Console;
|
||||
namespace Modules\Webstatement\Console;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\Webstatement\Jobs\SendStatementEmailJob;
|
||||
use Modules\Webstatement\Models\Account;
|
||||
use Modules\Webstatement\Models\PrintStatementLog;
|
||||
use Modules\Basicdata\Models\Branch;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\Basicdata\Models\Branch;
|
||||
use Modules\Webstatement\Jobs\SendStatementEmailJob;
|
||||
use Modules\Webstatement\Models\Account;
|
||||
use Modules\Webstatement\Models\PrintStatementLog;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Command untuk mengirim email statement PDF ke nasabah
|
||||
* Mendukung pengiriman per rekening, per cabang, atau seluruh cabang
|
||||
*/
|
||||
class SendStatementEmailCommand extends Command
|
||||
{
|
||||
protected $signature = 'webstatement:send-email
|
||||
/**
|
||||
* Command untuk mengirim email statement PDF ke nasabah
|
||||
* Mendukung pengiriman per rekening, per cabang, atau seluruh cabang
|
||||
*/
|
||||
class SendStatementEmailCommand extends Command
|
||||
{
|
||||
protected $signature = 'webstatement:send-email
|
||||
{period : Format periode YYYYMM (contoh: 202401)}
|
||||
{--type=single : Tipe pengiriman: single, branch, all}
|
||||
{--account= : Nomor rekening (untuk type=single)}
|
||||
@@ -25,224 +26,224 @@ class SendStatementEmailCommand extends Command
|
||||
{--queue=emails : Nama queue untuk job (default: emails)}
|
||||
{--delay=0 : Delay dalam menit sebelum job dijalankan}';
|
||||
|
||||
protected $description = 'Mengirim email statement PDF ke nasabah (per rekening, per cabang, atau seluruh cabang)';
|
||||
protected $description = 'Mengirim email statement PDF ke nasabah (per rekening, per cabang, atau seluruh cabang)';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->info('🚀 Memulai proses pengiriman email statement...');
|
||||
public function handle()
|
||||
{
|
||||
$this->info('🚀 Memulai proses pengiriman email statement...');
|
||||
|
||||
try {
|
||||
$period = $this->argument('period');
|
||||
$type = $this->option('type');
|
||||
$accountNumber = $this->option('account');
|
||||
$branchCode = $this->option('branch');
|
||||
$batchId = $this->option('batch-id');
|
||||
$queueName = $this->option('queue');
|
||||
$delay = (int) $this->option('delay');
|
||||
try {
|
||||
$period = $this->argument('period');
|
||||
$type = $this->option('type');
|
||||
$accountNumber = $this->option('account');
|
||||
$branchCode = $this->option('branch');
|
||||
$batchId = $this->option('batch-id');
|
||||
$queueName = $this->option('queue');
|
||||
$delay = (int) $this->option('delay');
|
||||
|
||||
// Validasi parameter
|
||||
if (!$this->validateParameters($period, $type, $accountNumber, $branchCode)) {
|
||||
// Validasi parameter
|
||||
if (!$this->validateParameters($period, $type, $accountNumber, $branchCode)) {
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// Tentukan request type dan target value
|
||||
[$requestType, $targetValue] = $this->determineRequestTypeAndTarget($type, $accountNumber, $branchCode);
|
||||
|
||||
// Buat log entry
|
||||
$log = $this->createLogEntry($period, $requestType, $targetValue, $batchId);
|
||||
|
||||
// Dispatch job
|
||||
$job = SendStatementEmailJob::dispatch($period, $requestType, $targetValue, $batchId, $log->id)
|
||||
->onQueue($queueName);
|
||||
|
||||
if ($delay > 0) {
|
||||
$job->delay(now()->addMinutes($delay));
|
||||
$this->info("⏰ Job dijadwalkan untuk dijalankan dalam {$delay} menit");
|
||||
}
|
||||
|
||||
$this->displayJobInfo($period, $requestType, $targetValue, $queueName, $log);
|
||||
$this->info('✅ Job pengiriman email statement berhasil didispatch!');
|
||||
$this->info('📊 Gunakan command berikut untuk monitoring:');
|
||||
$this->line(" php artisan queue:work {$queueName}");
|
||||
$this->line(' php artisan webstatement:check-progress ' . $log->id);
|
||||
|
||||
return Command::SUCCESS;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->error('❌ Error saat mendispatch job: ' . $e->getMessage());
|
||||
Log::error('SendStatementEmailCommand failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
// Tentukan request type dan target value
|
||||
[$requestType, $targetValue] = $this->determineRequestTypeAndTarget($type, $accountNumber, $branchCode);
|
||||
|
||||
// Buat log entry
|
||||
$log = $this->createLogEntry($period, $requestType, $targetValue, $batchId);
|
||||
|
||||
// Dispatch job
|
||||
$job = SendStatementEmailJob::dispatch($period, $requestType, $targetValue, $batchId, $log->id)
|
||||
->onQueue($queueName);
|
||||
|
||||
if ($delay > 0) {
|
||||
$job->delay(now()->addMinutes($delay));
|
||||
$this->info("⏰ Job dijadwalkan untuk dijalankan dalam {$delay} menit");
|
||||
private function validateParameters($period, $type, $accountNumber, $branchCode)
|
||||
{
|
||||
// Validasi format periode
|
||||
if (!preg_match('/^\d{6}$/', $period)) {
|
||||
$this->error('❌ Format periode tidak valid. Gunakan format YYYYMM (contoh: 202401)');
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->displayJobInfo($period, $requestType, $targetValue, $queueName, $log);
|
||||
$this->info('✅ Job pengiriman email statement berhasil didispatch!');
|
||||
$this->info('📊 Gunakan command berikut untuk monitoring:');
|
||||
$this->line(" php artisan queue:work {$queueName}");
|
||||
$this->line(' php artisan webstatement:check-progress ' . $log->id);
|
||||
// Validasi type
|
||||
if (!in_array($type, ['single', 'branch', 'all'])) {
|
||||
$this->error('❌ Type tidak valid. Gunakan: single, branch, atau all');
|
||||
return false;
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
// Validasi parameter berdasarkan type
|
||||
switch ($type) {
|
||||
case 'single':
|
||||
if (!$accountNumber) {
|
||||
$this->error('❌ Parameter --account diperlukan untuk type=single');
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->error('❌ Error saat mendispatch job: ' . $e->getMessage());
|
||||
Log::error('SendStatementEmailCommand failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return Command::FAILURE;
|
||||
$account = Account::with('customer')
|
||||
->where('account_number', $accountNumber)
|
||||
->first();
|
||||
|
||||
if (!$account) {
|
||||
$this->error("❌ Account {$accountNumber} tidak ditemukan");
|
||||
return false;
|
||||
}
|
||||
|
||||
$hasEmail = !empty($account->stmt_email) ||
|
||||
($account->customer && !empty($account->customer->email));
|
||||
|
||||
if (!$hasEmail) {
|
||||
$this->error("❌ Account {$accountNumber} tidak memiliki email");
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->info("✅ Account {$accountNumber} ditemukan dengan email");
|
||||
break;
|
||||
|
||||
case 'branch':
|
||||
if (!$branchCode) {
|
||||
$this->error('❌ Parameter --branch diperlukan untuk type=branch');
|
||||
return false;
|
||||
}
|
||||
|
||||
$branch = Branch::where('code', $branchCode)->first();
|
||||
if (!$branch) {
|
||||
$this->error("❌ Branch {$branchCode} tidak ditemukan");
|
||||
return false;
|
||||
}
|
||||
|
||||
$accountCount = Account::with('customer')
|
||||
->where('branch_code', $branchCode)
|
||||
->where('stmt_sent_type', 'BY.EMAIL')
|
||||
->get()
|
||||
->filter(function ($account) {
|
||||
return !empty($account->stmt_email) ||
|
||||
($account->customer && !empty($account->customer->email));
|
||||
})
|
||||
->count();
|
||||
|
||||
if ($accountCount === 0) {
|
||||
$this->error("❌ Tidak ada account dengan email di branch {$branchCode}");
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->info("✅ Ditemukan {$accountCount} account dengan email di branch {$branch->name}");
|
||||
break;
|
||||
|
||||
case 'all':
|
||||
$accountCount = Account::with('customer')
|
||||
->where('stmt_sent_type', 'BY.EMAIL')
|
||||
->get()
|
||||
->filter(function ($account) {
|
||||
return !empty($account->stmt_email) ||
|
||||
($account->customer && !empty($account->customer->email));
|
||||
})
|
||||
->count();
|
||||
|
||||
if ($accountCount === 0) {
|
||||
$this->error('❌ Tidak ada account dengan email ditemukan');
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->info("✅ Ditemukan {$accountCount} account dengan email di seluruh cabang");
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function determineRequestTypeAndTarget($type, $accountNumber, $branchCode)
|
||||
{
|
||||
switch ($type) {
|
||||
case 'single':
|
||||
return ['single_account', $accountNumber];
|
||||
case 'branch':
|
||||
return ['branch', $branchCode];
|
||||
case 'all':
|
||||
return ['all_branches', null];
|
||||
default:
|
||||
throw new InvalidArgumentException("Invalid type: {$type}");
|
||||
}
|
||||
}
|
||||
|
||||
private function createLogEntry($period, $requestType, $targetValue, $batchId)
|
||||
{
|
||||
$logData = [
|
||||
'user_id' => null, // Command line execution
|
||||
'period_from' => $period,
|
||||
'period_to' => $period,
|
||||
'is_period_range' => false,
|
||||
'request_type' => $requestType,
|
||||
'batch_id' => $batchId ?? uniqid('cmd_'),
|
||||
'status' => 'pending',
|
||||
'authorization_status' => 'approved', // Auto-approved untuk command line
|
||||
'created_by' => null,
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'Command Line'
|
||||
];
|
||||
|
||||
// Set branch_code dan account_number berdasarkan request type
|
||||
switch ($requestType) {
|
||||
case 'single_account':
|
||||
$account = Account::where('account_number', $targetValue)->first();
|
||||
$logData['branch_code'] = $account->branch_code;
|
||||
$logData['account_number'] = $targetValue;
|
||||
break;
|
||||
case 'branch':
|
||||
$logData['branch_code'] = $targetValue;
|
||||
$logData['account_number'] = null;
|
||||
break;
|
||||
case 'all_branches':
|
||||
$logData['branch_code'] = 'ALL';
|
||||
$logData['account_number'] = null;
|
||||
break;
|
||||
}
|
||||
|
||||
return PrintStatementLog::create($logData);
|
||||
}
|
||||
|
||||
private function displayJobInfo($period, $requestType, $targetValue, $queueName, $log)
|
||||
{
|
||||
$this->info('📋 Detail Job:');
|
||||
$this->line(" Log ID: {$log->id}");
|
||||
$this->line(" Periode: {$period}");
|
||||
$this->line(" Request Type: {$requestType}");
|
||||
|
||||
switch ($requestType) {
|
||||
case 'single_account':
|
||||
$this->line(" Account: {$targetValue}");
|
||||
break;
|
||||
case 'branch':
|
||||
$branch = Branch::where('code', $targetValue)->first();
|
||||
$this->line(" Branch: {$targetValue} ({$branch->name})");
|
||||
break;
|
||||
case 'all_branches':
|
||||
$this->line(" Target: Seluruh cabang");
|
||||
break;
|
||||
}
|
||||
|
||||
$this->line(" Batch ID: {$log->batch_id}");
|
||||
$this->line(" Queue: {$queueName}");
|
||||
}
|
||||
}
|
||||
|
||||
private function validateParameters($period, $type, $accountNumber, $branchCode)
|
||||
{
|
||||
// Validasi format periode
|
||||
if (!preg_match('/^\d{6}$/', $period)) {
|
||||
$this->error('❌ Format periode tidak valid. Gunakan format YYYYMM (contoh: 202401)');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validasi type
|
||||
if (!in_array($type, ['single', 'branch', 'all'])) {
|
||||
$this->error('❌ Type tidak valid. Gunakan: single, branch, atau all');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validasi parameter berdasarkan type
|
||||
switch ($type) {
|
||||
case 'single':
|
||||
if (!$accountNumber) {
|
||||
$this->error('❌ Parameter --account diperlukan untuk type=single');
|
||||
return false;
|
||||
}
|
||||
|
||||
$account = Account::with('customer')
|
||||
->where('account_number', $accountNumber)
|
||||
->first();
|
||||
|
||||
if (!$account) {
|
||||
$this->error("❌ Account {$accountNumber} tidak ditemukan");
|
||||
return false;
|
||||
}
|
||||
|
||||
$hasEmail = !empty($account->stmt_email) ||
|
||||
($account->customer && !empty($account->customer->email));
|
||||
|
||||
if (!$hasEmail) {
|
||||
$this->error("❌ Account {$accountNumber} tidak memiliki email");
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->info("✅ Account {$accountNumber} ditemukan dengan email");
|
||||
break;
|
||||
|
||||
case 'branch':
|
||||
if (!$branchCode) {
|
||||
$this->error('❌ Parameter --branch diperlukan untuk type=branch');
|
||||
return false;
|
||||
}
|
||||
|
||||
$branch = Branch::where('code', $branchCode)->first();
|
||||
if (!$branch) {
|
||||
$this->error("❌ Branch {$branchCode} tidak ditemukan");
|
||||
return false;
|
||||
}
|
||||
|
||||
$accountCount = Account::with('customer')
|
||||
->where('branch_code', $branchCode)
|
||||
->where('stmt_sent_type', 'BY.EMAIL')
|
||||
->get()
|
||||
->filter(function ($account) {
|
||||
return !empty($account->stmt_email) ||
|
||||
($account->customer && !empty($account->customer->email));
|
||||
})
|
||||
->count();
|
||||
|
||||
if ($accountCount === 0) {
|
||||
$this->error("❌ Tidak ada account dengan email di branch {$branchCode}");
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->info("✅ Ditemukan {$accountCount} account dengan email di branch {$branch->name}");
|
||||
break;
|
||||
|
||||
case 'all':
|
||||
$accountCount = Account::with('customer')
|
||||
->where('stmt_sent_type', 'BY.EMAIL')
|
||||
->get()
|
||||
->filter(function ($account) {
|
||||
return !empty($account->stmt_email) ||
|
||||
($account->customer && !empty($account->customer->email));
|
||||
})
|
||||
->count();
|
||||
|
||||
if ($accountCount === 0) {
|
||||
$this->error('❌ Tidak ada account dengan email ditemukan');
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->info("✅ Ditemukan {$accountCount} account dengan email di seluruh cabang");
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function determineRequestTypeAndTarget($type, $accountNumber, $branchCode)
|
||||
{
|
||||
switch ($type) {
|
||||
case 'single':
|
||||
return ['single_account', $accountNumber];
|
||||
case 'branch':
|
||||
return ['branch', $branchCode];
|
||||
case 'all':
|
||||
return ['all_branches', null];
|
||||
default:
|
||||
throw new \InvalidArgumentException("Invalid type: {$type}");
|
||||
}
|
||||
}
|
||||
|
||||
private function createLogEntry($period, $requestType, $targetValue, $batchId)
|
||||
{
|
||||
$logData = [
|
||||
'user_id' => null, // Command line execution
|
||||
'period_from' => $period,
|
||||
'period_to' => $period,
|
||||
'is_period_range' => false,
|
||||
'request_type' => $requestType,
|
||||
'batch_id' => $batchId ?? uniqid('cmd_'),
|
||||
'status' => 'pending',
|
||||
'authorization_status' => 'approved', // Auto-approved untuk command line
|
||||
'created_by' => null,
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'Command Line'
|
||||
];
|
||||
|
||||
// Set branch_code dan account_number berdasarkan request type
|
||||
switch ($requestType) {
|
||||
case 'single_account':
|
||||
$account = Account::where('account_number', $targetValue)->first();
|
||||
$logData['branch_code'] = $account->branch_code;
|
||||
$logData['account_number'] = $targetValue;
|
||||
break;
|
||||
case 'branch':
|
||||
$logData['branch_code'] = $targetValue;
|
||||
$logData['account_number'] = null;
|
||||
break;
|
||||
case 'all_branches':
|
||||
$logData['branch_code'] = 'ALL';
|
||||
$logData['account_number'] = null;
|
||||
break;
|
||||
}
|
||||
|
||||
return PrintStatementLog::create($logData);
|
||||
}
|
||||
|
||||
private function displayJobInfo($period, $requestType, $targetValue, $queueName, $log)
|
||||
{
|
||||
$this->info('📋 Detail Job:');
|
||||
$this->line(" Log ID: {$log->id}");
|
||||
$this->line(" Periode: {$period}");
|
||||
$this->line(" Request Type: {$requestType}");
|
||||
|
||||
switch ($requestType) {
|
||||
case 'single_account':
|
||||
$this->line(" Account: {$targetValue}");
|
||||
break;
|
||||
case 'branch':
|
||||
$branch = Branch::where('code', $targetValue)->first();
|
||||
$this->line(" Branch: {$targetValue} ({$branch->name})");
|
||||
break;
|
||||
case 'all_branches':
|
||||
$this->line(" Target: Seluruh cabang");
|
||||
break;
|
||||
}
|
||||
|
||||
$this->line(" Batch ID: {$log->batch_id}");
|
||||
$this->line(" Queue: {$queueName}");
|
||||
}
|
||||
}
|
||||
|
||||
110
app/Console/UpdateAllAtmCardsCommand.php
Normal file
110
app/Console/UpdateAllAtmCardsCommand.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Console;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\Webstatement\Jobs\UpdateAllAtmCardsBatchJob;
|
||||
|
||||
class UpdateAllAtmCardsCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'atmcard:update-all
|
||||
{--sync-log-id= : ID sync log yang akan digunakan}
|
||||
{--batch-size=100 : Ukuran batch untuk processing}
|
||||
{--queue=atmcard-update : Nama queue untuk job}
|
||||
{--filters= : Filter JSON untuk kondisi kartu}
|
||||
{--dry-run : Preview tanpa eksekusi aktual}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Jalankan job untuk update seluruh kartu ATM secara batch';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
Log::info('Memulai command update seluruh kartu ATM');
|
||||
|
||||
try {
|
||||
$syncLogId = $this->option('sync-log-id');
|
||||
$batchSize = (int) $this->option('batch-size');
|
||||
$queueName = $this->option('queue');
|
||||
$filtersJson = $this->option('filters');
|
||||
$isDryRun = $this->option('dry-run');
|
||||
|
||||
// Parse filters jika ada
|
||||
$filters = [];
|
||||
if ($filtersJson) {
|
||||
$filters = json_decode($filtersJson, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$this->error('Format JSON filters tidak valid');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
// Validasi input
|
||||
if ($batchSize <= 0) {
|
||||
$this->error('Batch size harus lebih besar dari 0');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$this->info('Konfigurasi job:');
|
||||
$this->info("- Sync Log ID: " . ($syncLogId ?: 'Akan dibuat baru'));
|
||||
$this->info("- Batch Size: {$batchSize}");
|
||||
$this->info("- Queue: {$queueName}");
|
||||
$this->info("- Filters: " . ($filtersJson ?: 'Tidak ada'));
|
||||
$this->info("- Dry Run: " . ($isDryRun ? 'Ya' : 'Tidak'));
|
||||
|
||||
if ($isDryRun) {
|
||||
$this->warn('Mode DRY RUN - Job tidak akan dijalankan');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
// Konfirmasi sebelum menjalankan
|
||||
if (!$this->confirm('Apakah Anda yakin ingin menjalankan job update seluruh kartu ATM?')) {
|
||||
$this->info('Operasi dibatalkan');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
// Dispatch job
|
||||
$job = new UpdateAllAtmCardsBatchJob($syncLogId, $batchSize, $filters);
|
||||
$job->onQueue($queueName);
|
||||
dispatch($job);
|
||||
|
||||
$this->info('Job berhasil dijadwalkan!');
|
||||
$this->info("Queue: {$queueName}");
|
||||
$this->info('Gunakan command berikut untuk memonitor:');
|
||||
$this->info('php artisan queue:work --queue=' . $queueName);
|
||||
|
||||
Log::info('Command update seluruh kartu ATM selesai', [
|
||||
'sync_log_id' => $syncLogId,
|
||||
'batch_size' => $batchSize,
|
||||
'queue' => $queueName,
|
||||
'filters' => $filters
|
||||
]);
|
||||
|
||||
return Command::SUCCESS;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->error('Terjadi error: ' . $e->getMessage());
|
||||
Log::error('Error dalam command update seluruh kartu ATM: ' . $e->getMessage(), [
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine()
|
||||
]);
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
app/Helpers/helpers.php
Normal file
39
app/Helpers/helpers.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
if(!function_exists('calculatePeriodDates')) {
|
||||
/**
|
||||
* Fungsi untuk menghitung tanggal periode berdasarkan periode yang diberikan
|
||||
* Jika periode 202505, mulai dari tanggal 9 sampai akhir bulan
|
||||
* Jika periode lain, mulai dari tanggal 1 sampai akhir bulan
|
||||
*/
|
||||
function calculatePeriodDates($period)
|
||||
{
|
||||
$year = substr($period, 0, 4);
|
||||
$month = substr($period, 4, 2);
|
||||
|
||||
// Log untuk debugging
|
||||
Log::info('Calculating period dates', [
|
||||
'period' => $period,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
]);
|
||||
|
||||
if ($period === '202505') {
|
||||
// Untuk periode 202505, mulai dari tanggal 9
|
||||
$startDate = \Carbon\Carbon::createFromDate($year, $month, 9,'Asia/Jakarta');
|
||||
} else {
|
||||
// Untuk periode lain, mulai dari tanggal 1
|
||||
$startDate = \Carbon\Carbon::createFromDate($year, $month, 1,'Asia/Jakarta');
|
||||
}
|
||||
|
||||
// Tanggal akhir selalu akhir bulan
|
||||
$endDate = \Carbon\Carbon::createFromDate($year, $month, 1)->endOfMonth();
|
||||
|
||||
return [
|
||||
'start' => $startDate,
|
||||
'end' => $endDate,
|
||||
];
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,8 +21,22 @@ class PrintStatementRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'branch_code' => ['required', 'string', 'exists:branches,code'],
|
||||
'account_number' => ['required', 'string'],
|
||||
'branch_code' => ['required', 'string'],
|
||||
// account_number required jika stmt_sent_type tidak diisi atau kosong
|
||||
'account_number' => [
|
||||
function ($attribute, $value, $fail) {
|
||||
$stmtSentType = $this->input('stmt_sent_type');
|
||||
|
||||
// Jika stmt_sent_type kosong atau tidak ada, maka account_number wajib diisi
|
||||
if (empty($stmtSentType) || (is_array($stmtSentType) && count(array_filter($stmtSentType)) === 0)) {
|
||||
if (empty($value)) {
|
||||
$fail('Account number is required when statement type is not specified.');
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
'stmt_sent_type' => ['nullable', 'array'],
|
||||
'stmt_sent_type.*' => ['string', 'in:ALL,BY.EMAIL,BY.MAIL.TO.DOM.ADDR,BY.MAIL.TO.KTP.ADDR,NO.PRINT,PRINT'],
|
||||
'is_period_range' => ['sometimes', 'boolean'],
|
||||
'email' => ['nullable', 'email'],
|
||||
'email_sent_at' => ['nullable', 'timestamp'],
|
||||
@@ -34,25 +48,33 @@ class PrintStatementRequest extends FormRequest
|
||||
'regex:/^\d{6}$/', // YYYYMM format
|
||||
// Prevent duplicate requests with same account number and period
|
||||
function ($attribute, $value, $fail) {
|
||||
$query = Statement::where('account_number', $this->input('account_number'))
|
||||
->where('authorization_status', '!=', 'rejected')
|
||||
->where('period_from', $value);
|
||||
// Hanya cek duplikasi jika account_number ada
|
||||
if (!empty($this->input('account_number'))) {
|
||||
$query = Statement::where('account_number', $this->input('account_number'))
|
||||
->where('authorization_status', '!=', 'rejected')
|
||||
->where(function($query) {
|
||||
$query->where('is_available', true)
|
||||
->orWhere('is_generated', true);
|
||||
})
|
||||
->where('user_id', $this->user()->id)
|
||||
->where('period_from', $value);
|
||||
|
||||
// If this is an update request, exclude the current record
|
||||
if ($this->route('statement')) {
|
||||
$query->where('id', '!=', $this->route('statement'));
|
||||
}
|
||||
// If this is an update request, exclude the current record
|
||||
if ($this->route('statement')) {
|
||||
$query->where('id', '!=', $this->route('statement'));
|
||||
}
|
||||
|
||||
// If period_to is provided, check for overlapping periods
|
||||
if ($this->input('period_to')) {
|
||||
$query->where(function ($q) use ($value) {
|
||||
$q->where('period_from', '<=', $this->input('period_to'))
|
||||
->where('period_to', '>=', $value);
|
||||
});
|
||||
}
|
||||
// If period_to is provided, check for overlapping periods
|
||||
if ($this->input('period_to')) {
|
||||
$query->where(function ($q) use ($value) {
|
||||
$q->where('period_from', '<=', $this->input('period_to'))
|
||||
->where('period_to', '>=', $value);
|
||||
});
|
||||
}
|
||||
|
||||
if ($query->exists()) {
|
||||
$fail('A statement request with this account number and period already exists.');
|
||||
if ($query->exists()) {
|
||||
$fail('A statement request with this account number and period already exists.');
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -78,8 +100,9 @@ class PrintStatementRequest extends FormRequest
|
||||
{
|
||||
return [
|
||||
'branch_code.required' => 'Branch code is required',
|
||||
'branch_code.exists' => 'Selected branch does not exist',
|
||||
'account_number.required' => 'Account number is required',
|
||||
'branch_code.string' => 'Branch code must be a string',
|
||||
'account_number.required' => 'Account number is required when statement type is not specified',
|
||||
'stmt_sent_type.*.in' => 'Invalid statement type selected',
|
||||
'period_from.required' => 'Period is required',
|
||||
'period_from.regex' => 'Period must be in YYYYMM format',
|
||||
'period_to.required' => 'End period is required for period range',
|
||||
@@ -106,13 +129,13 @@ class PrintStatementRequest extends FormRequest
|
||||
$this->merge([
|
||||
'period_to' => substr($this->period_to, 0, 4) . substr($this->period_to, 5, 2),
|
||||
]);
|
||||
}
|
||||
|
||||
// Convert is_period_range to boolean if it exists
|
||||
if ($this->has('period_to')) {
|
||||
$this->merge([
|
||||
'is_period_range' => true,
|
||||
]);
|
||||
// Only set is_period_range to true if period_to is different from period_from
|
||||
if ($this->period_to !== $this->period_from) {
|
||||
$this->merge([
|
||||
'is_period_range' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Set default request_type if not provided
|
||||
|
||||
@@ -12,6 +12,7 @@ use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Webstatement\Models\PrintStatementLog;
|
||||
use Modules\Webstatement\Models\ProcessedStatement;
|
||||
use Modules\Webstatement\Models\StmtEntry;
|
||||
use Modules\Webstatement\Models\TempFundsTransfer;
|
||||
@@ -31,6 +32,8 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
protected $chunkSize = 1000;
|
||||
protected $startDate;
|
||||
protected $endDate;
|
||||
protected $toCsv;
|
||||
protected $statementId;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
@@ -41,14 +44,16 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
* @param string $client
|
||||
* @param string $disk
|
||||
*/
|
||||
public function __construct(string $account_number, string $period, string $saldo, string $client = '', string $disk = 'local')
|
||||
public function __construct(int $statementId, string $account_number, string $period, string $saldo, string $client = '', string $disk = 'local', bool $toCsv = true)
|
||||
{
|
||||
$this->statementId = $statementId;
|
||||
$this->account_number = $account_number;
|
||||
$this->period = $period;
|
||||
$this->saldo = $saldo;
|
||||
$this->disk = $disk;
|
||||
$this->client = $client;
|
||||
$this->fileName = "{$account_number}_{$period}.csv";
|
||||
$this->toCsv = $toCsv;
|
||||
|
||||
// Calculate start and end dates based on period
|
||||
$this->calculatePeriodDates();
|
||||
@@ -84,8 +89,9 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
Log::info("Date range: {$this->startDate->format('Y-m-d')} to {$this->endDate->format('Y-m-d')}");
|
||||
|
||||
$this->processStatementData();
|
||||
$this->exportToCsv();
|
||||
|
||||
if($this->toCsv){
|
||||
$this->exportToCsv();
|
||||
}
|
||||
Log::info("Export statement period job completed successfully for account: {$this->account_number}, period: {$this->period}");
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error in ExportStatementPeriodJob: " . $e->getMessage());
|
||||
@@ -104,20 +110,28 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
$existingDataCount = $this->getExistingProcessedCount($accountQuery);
|
||||
|
||||
// Only process if data is not fully processed
|
||||
if ($existingDataCount !== $totalCount) {
|
||||
//if ($existingDataCount !== $totalCount) {
|
||||
$this->deleteExistingProcessedData($accountQuery);
|
||||
$this->processAndSaveStatementEntries($totalCount);
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
||||
private function getTotalEntryCount(): int
|
||||
{
|
||||
return StmtEntry::where('account_number', $this->account_number)
|
||||
->whereBetween('date_time', [
|
||||
$this->startDate->format('ymdHi'),
|
||||
$this->endDate->format('ymdHi')
|
||||
])
|
||||
->count();
|
||||
$query = StmtEntry::where('account_number', $this->account_number)
|
||||
->whereBetween('booking_date', [
|
||||
$this->startDate->format('Ymd'),
|
||||
$this->endDate->format('Ymd')
|
||||
]);
|
||||
|
||||
Log::info("Getting total entry count with query: " . $query->toSql(), [
|
||||
'bindings' => $query->getBindings(),
|
||||
'account' => $this->account_number,
|
||||
'start_date' => $this->startDate->format('Ymd'),
|
||||
'end_date' => $this->endDate->format('Ymd')
|
||||
]);
|
||||
|
||||
return $query->count();
|
||||
}
|
||||
|
||||
private function getExistingProcessedCount(array $criteria): int
|
||||
@@ -141,11 +155,11 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
|
||||
Log::info("Processing {$totalCount} statement entries for account: {$this->account_number}");
|
||||
|
||||
StmtEntry::with(['ft', 'transaction'])
|
||||
$entry = StmtEntry::with(['ft', 'transaction'])
|
||||
->where('account_number', $this->account_number)
|
||||
->whereBetween('date_time', [
|
||||
$this->startDate->format('ymdHi'),
|
||||
$this->endDate->format('ymdHi')
|
||||
->whereBetween('booking_date', [
|
||||
$this->startDate->format('Ymd'),
|
||||
$this->endDate->format('Ymd')
|
||||
])
|
||||
->orderBy('date_time', 'ASC')
|
||||
->orderBy('trans_reference', 'ASC')
|
||||
@@ -156,6 +170,13 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
DB::table('processed_statements')->insert($processedData);
|
||||
}
|
||||
});
|
||||
|
||||
if($entry){
|
||||
$printLog = PrintStatementLog::find($this->statementId);
|
||||
if($printLog){
|
||||
$printLog->update(['is_generated' => true]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function prepareProcessedData($entries, &$runningBalance, &$globalSequence): array
|
||||
@@ -166,14 +187,13 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
$globalSequence++;
|
||||
$runningBalance += (float) $item->amount_lcy;
|
||||
|
||||
$transactionDate = $this->formatTransactionDate($item);
|
||||
$actualDate = $this->formatActualDate($item);
|
||||
|
||||
$processedData[] = [
|
||||
'account_number' => $this->account_number,
|
||||
'period' => $this->period,
|
||||
'sequence_no' => $globalSequence,
|
||||
'transaction_date' => $transactionDate,
|
||||
'transaction_date' => $item->booking_date,
|
||||
'reference_number' => $item->trans_reference,
|
||||
'transaction_amount' => $item->amount_lcy,
|
||||
'transaction_type' => $item->amount_lcy < 0 ? 'D' : 'C',
|
||||
|
||||
@@ -63,7 +63,9 @@
|
||||
$this->updateCsvLogStart();
|
||||
|
||||
// Generate CSV file
|
||||
$result = $this->generateAtmCardCsv();
|
||||
// $result = $this->generateAtmCardCsv();
|
||||
|
||||
$result = $this->generateSingleAtmCardCsv();
|
||||
|
||||
// Update status CSV generation berhasil
|
||||
$this->updateCsvLogSuccess($result);
|
||||
@@ -159,23 +161,53 @@
|
||||
|
||||
/**
|
||||
* Get eligible ATM cards from database
|
||||
* Mengambil data kartu ATM yang memenuhi syarat untuk dikenakan biaya admin
|
||||
* dengan filter khusus untuk mengecualikan product_code 6021 yang ctdesc nya gold
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
private function getEligibleAtmCards()
|
||||
{
|
||||
// Log: Memulai proses pengambilan data kartu ATM yang eligible
|
||||
Log::info('Starting to fetch eligible ATM cards', [
|
||||
'periode' => $this->periode
|
||||
]);
|
||||
|
||||
$cardTypes = array_keys($this->getDefaultFees());
|
||||
|
||||
return Atmcard::where('crsts', 1)
|
||||
->whereNotNull('accflag')
|
||||
->where('accflag', '!=', '')
|
||||
->where('flag','')
|
||||
->whereNotNull('branch')
|
||||
->where('branch', '!=', '')
|
||||
->whereNotNull('currency')
|
||||
->where('currency', '!=', '')
|
||||
->whereIn('ctdesc', $cardTypes)
|
||||
->get();
|
||||
$query = Atmcard::where('crsts', 1)
|
||||
->whereNotNull('accflag')
|
||||
->where('accflag', '!=', '')
|
||||
->where('flag','')
|
||||
->whereNotNull('branch')
|
||||
->where('branch', '!=', '')
|
||||
->whereNotNull('currency')
|
||||
->where('currency', '!=', '')
|
||||
->whereIn('ctdesc', $cardTypes)
|
||||
->whereNotIn('product_code',['6002','6004','6042','6031']) // Hapus 6021 dari sini
|
||||
->where('branch','!=','ID0019999')
|
||||
// Filter khusus: Kecualikan product_code 6021 yang ctdesc nya gold
|
||||
->where(function($subQuery) {
|
||||
$subQuery->where('product_code', '!=', '6021')
|
||||
->orWhere(function($nestedQuery) {
|
||||
$nestedQuery->where('product_code', '6021')
|
||||
->where('ctdesc', '!=', 'gold');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
$cards = $query->get();
|
||||
|
||||
// Log: Hasil pengambilan data kartu ATM
|
||||
Log::info('Eligible ATM cards fetched successfully', [
|
||||
'total_cards' => $cards->count(),
|
||||
'periode' => $this->periode,
|
||||
'excluded_product_codes' => ['6002','6004','6042','6031'],
|
||||
'special_filter' => 'product_code 6021 dengan ctdesc gold dikecualikan'
|
||||
]);
|
||||
|
||||
return $cards;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -413,4 +445,155 @@
|
||||
|
||||
Log::error('Pembuatan file CSV gagal: ' . $errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate single CSV file with all ATM card data without branch separation
|
||||
*
|
||||
* @return array Information about the generated file and upload status
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
private function generateSingleAtmCardCsv(): array
|
||||
{
|
||||
Log::info('Memulai pembuatan file CSV tunggal untuk semua kartu ATM');
|
||||
|
||||
try {
|
||||
// Ambil semua kartu yang memenuhi syarat
|
||||
$cards = $this->getEligibleAtmCards();
|
||||
|
||||
if ($cards->isEmpty()) {
|
||||
Log::warning('Tidak ada kartu ATM yang memenuhi syarat untuk periode ini');
|
||||
throw new RuntimeException('Tidak ada kartu ATM yang memenuhi syarat untuk diproses');
|
||||
}
|
||||
|
||||
// Buat nama file dengan timestamp
|
||||
$dateTime = now()->format('Ymd_Hi');
|
||||
$singleFilename = pathinfo($this->csvFilename, PATHINFO_FILENAME)
|
||||
. '_ALL_BRANCHES_'
|
||||
. $dateTime . '.'
|
||||
. pathinfo($this->csvFilename, PATHINFO_EXTENSION);
|
||||
|
||||
$filename = storage_path('app/' . $singleFilename);
|
||||
|
||||
Log::info('Membuat file CSV: ' . $filename);
|
||||
|
||||
// Buka file untuk menulis
|
||||
$handle = fopen($filename, 'w+');
|
||||
if (!$handle) {
|
||||
throw new RuntimeException("Tidak dapat membuat file CSV: $filename");
|
||||
}
|
||||
|
||||
$recordCount = 0;
|
||||
|
||||
try {
|
||||
// Tulis semua kartu ke dalam satu file
|
||||
foreach ($cards as $card) {
|
||||
$fee = $this->determineCardFee($card);
|
||||
$csvRow = $this->createCsvRow($card, $fee);
|
||||
|
||||
if (fputcsv($handle, $csvRow, '|') === false) {
|
||||
throw new RuntimeException("Gagal menulis data kartu ke file CSV: {$card->crdno}");
|
||||
}
|
||||
|
||||
$recordCount++;
|
||||
|
||||
// Log progress setiap 1000 record
|
||||
if ($recordCount % 1000 === 0) {
|
||||
Log::info("Progress: {$recordCount} kartu telah diproses");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
fclose($handle);
|
||||
}
|
||||
|
||||
Log::info("Selesai menulis {$recordCount} kartu ke file CSV");
|
||||
|
||||
// Bersihkan file CSV (hapus double quotes)
|
||||
$this->cleanupCsvFile($filename);
|
||||
|
||||
Log::info('File CSV berhasil dibersihkan dari double quotes');
|
||||
|
||||
// Upload file ke SFTP (tanpa branch specific directory)
|
||||
$uploadSuccess = true; // $this->uploadSingleFileToSftp($filename);
|
||||
|
||||
$result = [
|
||||
'localFilePath' => $filename,
|
||||
'recordCount' => $recordCount,
|
||||
'uploadToSftp' => $uploadSuccess,
|
||||
'timestamp' => now()->format('Y-m-d H:i:s'),
|
||||
'fileName' => $singleFilename
|
||||
];
|
||||
|
||||
Log::info('Pembuatan file CSV tunggal selesai', $result);
|
||||
|
||||
return $result;
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error dalam generateSingleAtmCardCsv: ' . $e->getMessage(), [
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload single CSV file to SFTP server without branch directory
|
||||
*
|
||||
* @param string $localFilePath Path to the local CSV file
|
||||
* @return bool True if upload successful, false otherwise
|
||||
*/
|
||||
private function uploadSingleFileToSftp(string $localFilePath): bool
|
||||
{
|
||||
try {
|
||||
Log::info('Memulai upload file tunggal ke SFTP: ' . $localFilePath);
|
||||
|
||||
// Update status SFTP upload dimulai
|
||||
$this->updateSftpLogStart();
|
||||
|
||||
// Ambil nama file dari path
|
||||
$filename = basename($localFilePath);
|
||||
|
||||
// Ambil konten file
|
||||
$fileContent = file_get_contents($localFilePath);
|
||||
if ($fileContent === false) {
|
||||
Log::error("Tidak dapat membaca file untuk upload: {$localFilePath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Dapatkan disk SFTP
|
||||
$disk = Storage::disk('sftpKartu');
|
||||
|
||||
// Tentukan path tujuan di server SFTP (root directory)
|
||||
$remotePath = env('BIAYA_KARTU_REMOTE_PATH', '/');
|
||||
$remoteFilePath = rtrim($remotePath, '/') . '/' . $filename;
|
||||
|
||||
Log::info('Mengunggah ke path remote: ' . $remoteFilePath);
|
||||
|
||||
// Upload file ke server SFTP
|
||||
$result = $disk->put($remoteFilePath, $fileContent);
|
||||
|
||||
if ($result) {
|
||||
$this->updateSftpLogSuccess();
|
||||
Log::info("File CSV tunggal berhasil diunggah ke SFTP: {$remoteFilePath}");
|
||||
return true;
|
||||
} else {
|
||||
$errorMsg = "Gagal mengunggah file CSV tunggal ke SFTP: {$remoteFilePath}";
|
||||
$this->updateSftpLogFailed($errorMsg);
|
||||
Log::error($errorMsg);
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$errorMsg = "Error saat mengunggah file tunggal ke SFTP: " . $e->getMessage();
|
||||
$this->updateSftpLogFailed($errorMsg);
|
||||
|
||||
Log::error($errorMsg, [
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'periode' => $this->periode
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
737
app/Jobs/GenerateMultiAccountPdfJob.php
Normal file
737
app/Jobs/GenerateMultiAccountPdfJob.php
Normal file
@@ -0,0 +1,737 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
use Exception;
|
||||
use ZipArchive;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Support\Facades\{
|
||||
DB,
|
||||
Log,
|
||||
Storage
|
||||
};
|
||||
use Spatie\Browsershot\Browsershot;
|
||||
use Modules\Basicdata\Models\Branch;
|
||||
use Illuminate\Queue\{
|
||||
SerializesModels,
|
||||
InteractsWithQueue
|
||||
};
|
||||
use Modules\Webstatement\Models\{
|
||||
StmtEntry,
|
||||
AccountBalance,
|
||||
PrintStatementLog,
|
||||
ProcessedStatement,
|
||||
TempStmtNarrParam,
|
||||
TempStmtNarrFormat
|
||||
};
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class GenerateMultiAccountPdfJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $statement;
|
||||
protected $accounts;
|
||||
protected $period;
|
||||
protected $clientName;
|
||||
protected $chunkSize = 10; // Process 10 accounts at a time
|
||||
protected $startDate;
|
||||
protected $endDate;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @param PrintStatementLog $statement
|
||||
* @param \Illuminate\Database\Eloquent\Collection $accounts
|
||||
* @param string $period
|
||||
* @param string $clientName
|
||||
*/
|
||||
public function __construct($statement, $accounts, $period, $clientName)
|
||||
{
|
||||
$this->statement = $statement;
|
||||
$this->accounts = $accounts;
|
||||
$this->period = $period;
|
||||
$this->clientName = $clientName;
|
||||
|
||||
// Calculate period dates using same logic as ExportStatementPeriodJob
|
||||
$this->calculatePeriodDates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate start and end dates for the given period
|
||||
* Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||
*/
|
||||
private function calculatePeriodDates(): void
|
||||
{
|
||||
$year = substr($this->period, 0, 4);
|
||||
$month = substr($this->period, 4, 2);
|
||||
|
||||
// Special case for May 2025 - start from 12th
|
||||
if ($this->period === '202505') {
|
||||
$this->startDate = Carbon::createFromDate($year, $month, 12)->startOfDay();
|
||||
} else {
|
||||
// For all other periods, start from 1st of the month
|
||||
$this->startDate = Carbon::createFromDate($year, $month, 1)->startOfDay();
|
||||
}
|
||||
|
||||
// End date is always the last day of the month
|
||||
$this->endDate = Carbon::createFromDate($year, $month, 1)->endOfMonth()->endOfDay();
|
||||
|
||||
Log::info('Period dates calculated for PDF generation', [
|
||||
'period' => $this->period,
|
||||
'start_date' => $this->startDate->format('Y-m-d'),
|
||||
'end_date' => $this->endDate->format('Y-m-d')
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
|
||||
Log::info('Starting multi account PDF generation', [
|
||||
'statement_id' => $this->statement->id,
|
||||
'total_accounts' => $this->accounts->count(),
|
||||
'period' => $this->period,
|
||||
'date_range' => $this->startDate->format('Y-m-d') . ' to ' . $this->endDate->format('Y-m-d')
|
||||
]);
|
||||
|
||||
$pdfFiles = [];
|
||||
$successCount = 0;
|
||||
$failedCount = 0;
|
||||
$errors = [];
|
||||
|
||||
// Process each account
|
||||
foreach ($this->accounts as $account) {
|
||||
try {
|
||||
$pdfPath = $this->generateAccountPdf($account);
|
||||
if ($pdfPath) {
|
||||
$pdfFiles[] = $pdfPath;
|
||||
$successCount++;
|
||||
|
||||
Log::info('PDF generated successfully for account', [
|
||||
'account_number' => $account->account_number,
|
||||
'pdf_path' => $pdfPath
|
||||
]);
|
||||
}
|
||||
|
||||
// Memory cleanup after each account
|
||||
gc_collect_cycles();
|
||||
} catch (Exception $e) {
|
||||
$failedCount++;
|
||||
$errors[] = [
|
||||
'account_number' => $account->account_number,
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
|
||||
Log::error('Failed to generate PDF for account', [
|
||||
'account_number' => $account->account_number,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Create ZIP file if there are PDFs
|
||||
$zipPath = null;
|
||||
if (!empty($pdfFiles)) {
|
||||
$zipPath = $this->createZipFile($pdfFiles);
|
||||
}
|
||||
|
||||
// Update statement log
|
||||
$this->statement->update([
|
||||
'processed_accounts' => $this->accounts->count(),
|
||||
'success_count' => $successCount,
|
||||
'failed_count' => $failedCount,
|
||||
'status' => $failedCount > 0 ? 'completed_with_errors' : 'completed',
|
||||
'completed_at' => now(),
|
||||
'is_available' => $zipPath ? true : false,
|
||||
'is_generated' => $zipPath ? true : false,
|
||||
'error_message' => !empty($errors) ? json_encode($errors) : null
|
||||
]);
|
||||
|
||||
|
||||
Log::info('Multi account PDF generation completed', [
|
||||
'statement_id' => $this->statement->id,
|
||||
'success_count' => $successCount,
|
||||
'failed_count' => $failedCount,
|
||||
'zip_path' => $zipPath
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
|
||||
Log::error('Multi account PDF generation failed', [
|
||||
'statement_id' => $this->statement->id,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
// Update statement with error status
|
||||
$this->statement->update([
|
||||
'status' => 'failed',
|
||||
'completed_at' => now(),
|
||||
'error_message' => $e->getMessage()
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate PDF untuk satu account
|
||||
* Menggunakan data dari ProcessedStatement yang sudah diproses oleh ExportStatementPeriodJob
|
||||
*
|
||||
* @param Account $account
|
||||
* @return string|null Path to generated PDF
|
||||
*/
|
||||
protected function generateAccountPdf($account)
|
||||
{
|
||||
try {
|
||||
// Prepare account query untuk processing
|
||||
$accountQuery = [
|
||||
'account_number' => $account->account_number,
|
||||
'period' => $this->period
|
||||
];
|
||||
|
||||
// Get total entry count
|
||||
$totalCount = $this->getTotalEntryCount($account->account_number);
|
||||
|
||||
// Delete existing processed data dan process ulang
|
||||
$this->deleteExistingProcessedData($accountQuery);
|
||||
$this->processAndSaveStatementEntries($account, $totalCount);
|
||||
|
||||
// Get statement entries from ProcessedStatement (data yang sudah diproses)
|
||||
$stmtEntries = $this->getProcessedStatementEntries($account->account_number);
|
||||
|
||||
// Get saldo awal bulan menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||
$saldoAwalBulan = $this->getSaldoAwalBulan($account->account_number);
|
||||
|
||||
// Get branch info
|
||||
$branch = Branch::where('code', $account->branch_code)->first();
|
||||
|
||||
// Prepare images for PDF
|
||||
$images = $this->prepareImagesForPdf();
|
||||
|
||||
$headerImagePath = public_path('assets/media/images/bg-header-table.png');
|
||||
$headerTableBg = file_exists($headerImagePath)
|
||||
? base64_encode(file_get_contents($headerImagePath))
|
||||
: null;
|
||||
|
||||
// Render HTML
|
||||
$html = view('webstatement::statements.stmt', [
|
||||
'stmtEntries' => $stmtEntries,
|
||||
'account' => $account,
|
||||
'customer' => $account->customer,
|
||||
'images' => $images,
|
||||
'branch' => $branch,
|
||||
'period' => $this->period,
|
||||
'saldoAwalBulan' => $saldoAwalBulan,
|
||||
'headerTableBg' => $headerTableBg,
|
||||
])->render();
|
||||
|
||||
// Generate PDF filename
|
||||
$filename = "statement_{$account->account_number}_{$this->period}_" . now()->format('YmdHis') . '.pdf';
|
||||
$storagePath = "statements/{$this->period}/multi_account/{$this->statement->id}";
|
||||
$fullStoragePath = "{$storagePath}/{$filename}";
|
||||
|
||||
// Ensure directory exists
|
||||
Storage::disk('local')->makeDirectory($storagePath);
|
||||
|
||||
// Generate PDF path
|
||||
$pdfPath = storage_path("app/{$fullStoragePath}");
|
||||
|
||||
// Generate PDF using Browsershot
|
||||
Browsershot::html($html)
|
||||
->showBackground()
|
||||
->setOption('addStyleTag', json_encode(['content' => '@page { margin: 0; }']))
|
||||
->format('A4')
|
||||
->margins(0, 0, 0, 0)
|
||||
->waitUntilNetworkIdle()
|
||||
->timeout(60000)
|
||||
->save($pdfPath);
|
||||
|
||||
// Verify file was created
|
||||
if (!file_exists($pdfPath)) {
|
||||
throw new Exception('PDF file was not created');
|
||||
}
|
||||
|
||||
// Clear variables to free memory
|
||||
unset($html, $stmtEntries, $images);
|
||||
|
||||
return $pdfPath;
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to generate PDF for account', [
|
||||
'account_number' => $account->account_number,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total entry count untuk account
|
||||
* Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||
*
|
||||
* @param string $accountNumber
|
||||
* @return int
|
||||
*/
|
||||
protected function getTotalEntryCount($accountNumber): int
|
||||
{
|
||||
$query = StmtEntry::where('account_number', $accountNumber)
|
||||
->whereBetween('booking_date', [
|
||||
$this->startDate->format('Ymd'),
|
||||
$this->endDate->format('Ymd')
|
||||
]);
|
||||
|
||||
Log::info("Getting total entry count for PDF generation", [
|
||||
'account' => $accountNumber,
|
||||
'start_date' => $this->startDate->format('Ymd'),
|
||||
'end_date' => $this->endDate->format('Ymd')
|
||||
]);
|
||||
|
||||
return $query->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete existing processed data untuk account
|
||||
* Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||
*
|
||||
* @param array $criteria
|
||||
* @return void
|
||||
*/
|
||||
protected function deleteExistingProcessedData(array $criteria): void
|
||||
{
|
||||
Log::info('Deleting existing processed data for PDF generation', [
|
||||
'account_number' => $criteria['account_number'],
|
||||
'period' => $criteria['period']
|
||||
]);
|
||||
|
||||
ProcessedStatement::where('account_number', $criteria['account_number'])
|
||||
->where('period', $criteria['period'])
|
||||
->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process dan save statement entries untuk account
|
||||
* Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||
*
|
||||
* @param Account $account
|
||||
* @param int $totalCount
|
||||
* @return void
|
||||
*/
|
||||
protected function processAndSaveStatementEntries($account, int $totalCount): void
|
||||
{
|
||||
// Get saldo awal dari AccountBalance
|
||||
$saldoAwalBulan = $this->getSaldoAwalBulan($account->account_number);
|
||||
$runningBalance = (float) $saldoAwalBulan->actual_balance;
|
||||
$globalSequence = 0;
|
||||
|
||||
Log::info("Processing {$totalCount} statement entries for PDF generation", [
|
||||
'account_number' => $account->account_number,
|
||||
'starting_balance' => $runningBalance
|
||||
]);
|
||||
|
||||
StmtEntry::with(['ft', 'transaction'])
|
||||
->where('account_number', $account->account_number)
|
||||
->whereBetween('booking_date', [
|
||||
$this->startDate->format('Ymd'),
|
||||
$this->endDate->format('Ymd')
|
||||
])
|
||||
->orderBy('date_time', 'ASC')
|
||||
->orderBy('trans_reference', 'ASC')
|
||||
->chunk(1000, function ($entries) use (&$runningBalance, &$globalSequence, $account) {
|
||||
$processedData = $this->prepareProcessedData($entries, $runningBalance, $globalSequence, $account->account_number);
|
||||
|
||||
if (!empty($processedData)) {
|
||||
DB::table('processed_statements')->insert($processedData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare processed data untuk batch insert
|
||||
* Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||
*
|
||||
* @param $entries
|
||||
* @param float $runningBalance
|
||||
* @param int $globalSequence
|
||||
* @param string $accountNumber
|
||||
* @return array
|
||||
*/
|
||||
protected function prepareProcessedData($entries, &$runningBalance, &$globalSequence, $accountNumber): array
|
||||
{
|
||||
$processedData = [];
|
||||
|
||||
foreach ($entries as $item) {
|
||||
$globalSequence++;
|
||||
$runningBalance += (float) $item->amount_lcy;
|
||||
|
||||
$actualDate = $this->formatActualDate($item);
|
||||
|
||||
$processedData[] = [
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $this->period,
|
||||
'sequence_no' => $globalSequence,
|
||||
'transaction_date' => $item->booking_date,
|
||||
'reference_number' => $item->trans_reference,
|
||||
'transaction_amount' => $item->amount_lcy,
|
||||
'transaction_type' => $item->amount_lcy < 0 ? 'D' : 'C',
|
||||
'description' => $this->generateNarrative($item),
|
||||
'end_balance' => $runningBalance,
|
||||
'actual_date' => $actualDate,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
return $processedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format actual date dari item
|
||||
* Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||
*
|
||||
* @param $item
|
||||
* @return string
|
||||
*/
|
||||
protected function formatActualDate($item): string
|
||||
{
|
||||
try {
|
||||
$prefix = substr($item->trans_reference ?? '', 0, 2);
|
||||
$relationMap = [
|
||||
'FT' => 'ft',
|
||||
'TT' => 'tt',
|
||||
'DC' => 'dc',
|
||||
'AA' => 'aa'
|
||||
];
|
||||
|
||||
$datetime = $item->date_time;
|
||||
if (isset($relationMap[$prefix])) {
|
||||
$relation = $relationMap[$prefix];
|
||||
$datetime = $item->$relation?->date_time ?? $datetime;
|
||||
}
|
||||
|
||||
return Carbon::createFromFormat(
|
||||
'ymdHi',
|
||||
$datetime
|
||||
)->format('d/m/Y H:i');
|
||||
} catch (Exception $e) {
|
||||
Log::warning("Error formatting actual date: " . $e->getMessage());
|
||||
return Carbon::now()->format('d/m/Y H:i');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate narrative untuk statement entry
|
||||
* Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||
*
|
||||
* @param $item
|
||||
* @return string
|
||||
*/
|
||||
protected function generateNarrative($item)
|
||||
{
|
||||
$narr = [];
|
||||
|
||||
if ($item->transaction) {
|
||||
if ($item->transaction->stmt_narr) {
|
||||
$narr[] = $item->transaction->stmt_narr;
|
||||
}
|
||||
if ($item->narrative) {
|
||||
$narr[] = $item->narrative;
|
||||
}
|
||||
if ($item->transaction->narr_type) {
|
||||
$narr[] = $this->getFormatNarrative($item->transaction->narr_type, $item);
|
||||
}
|
||||
} else if ($item->narrative) {
|
||||
$narr[] = $item->narrative;
|
||||
}
|
||||
|
||||
if ($item->ft?->recipt_no) {
|
||||
$narr[] = 'Receipt No: ' . $item->ft->recipt_no;
|
||||
}
|
||||
|
||||
return implode(' ', array_filter($narr));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted narrative berdasarkan narrative type
|
||||
* Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||
*
|
||||
* @param $narr
|
||||
* @param $item
|
||||
* @return string
|
||||
*/
|
||||
protected function getFormatNarrative($narr, $item)
|
||||
{
|
||||
|
||||
$narrParam = TempStmtNarrParam::where('_id', $narr)->first();
|
||||
|
||||
if (!$narrParam) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$fmt = '';
|
||||
if ($narrParam->_id == 'FTIN') {
|
||||
$fmt = 'FT.IN';
|
||||
} else if ($narrParam->_id == 'FTOUT') {
|
||||
$fmt = 'FT.OUT';
|
||||
} else if ($narrParam->_id == 'TTTRFOUT') {
|
||||
$fmt = 'TT.O.TRF';
|
||||
} else if ($narrParam->_id == 'TTTRFIN') {
|
||||
$fmt = 'TT.I.TRF';
|
||||
} else if ($narrParam->_id == 'APITRX'){
|
||||
$fmt = 'API.TSEL';
|
||||
} else if ($narrParam->_id == 'ONUSCR'){
|
||||
$fmt = 'ONUS.CR';
|
||||
} else if ($narrParam->_id == 'ONUSDR'){
|
||||
$fmt = 'ONUS.DR';
|
||||
}else {
|
||||
$fmt = $narrParam->_id;
|
||||
}
|
||||
|
||||
$narrFormat = TempStmtNarrFormat::where('_id', $fmt)->first();
|
||||
|
||||
if (!$narrFormat) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Get the format string from the database
|
||||
$formatString = $narrFormat->text_data ?? '';
|
||||
|
||||
// Parse the format string
|
||||
// Split by the separator ']'
|
||||
$parts = explode(']', $formatString);
|
||||
|
||||
$result = '';
|
||||
|
||||
foreach ($parts as $index => $part) {
|
||||
if (empty($part)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($index === 0) {
|
||||
// For the first part, take only what's before the '!'
|
||||
$splitPart = explode('!', $part);
|
||||
if (count($splitPart) > 0) {
|
||||
// Remove quotes, backslashes, and other escape characters
|
||||
$cleanPart = trim($splitPart[0]).' ';
|
||||
// Remove quotes at the beginning and end
|
||||
$cleanPart = preg_replace('/^["\'\\\\]+|["\'\\\\]+$/', '', $cleanPart);
|
||||
// Remove any remaining backslashes
|
||||
$cleanPart = str_replace('\\', '', $cleanPart);
|
||||
// Remove any remaining quotes
|
||||
$cleanPart = str_replace('"', '', $cleanPart);
|
||||
$result .= $cleanPart;
|
||||
}
|
||||
} else {
|
||||
// For other parts, these are field placeholders
|
||||
$fieldName = strtolower(str_replace('.', '_', $part));
|
||||
|
||||
// Get the corresponding parameter value from narrParam
|
||||
$paramValue = null;
|
||||
|
||||
// Check if the field exists as a property in narrParam
|
||||
if (property_exists($narrParam, $fieldName)) {
|
||||
$paramValue = $narrParam->$fieldName;
|
||||
} else if (isset($narrParam->$fieldName)) {
|
||||
$paramValue = $narrParam->$fieldName;
|
||||
}
|
||||
|
||||
// If we found a value, add it to the result
|
||||
if ($paramValue !== null) {
|
||||
$result .= $paramValue;
|
||||
} else {
|
||||
// If no value found, try to use the original field name as a fallback
|
||||
if ($fieldName !== 'recipt_no') {
|
||||
$prefix = substr($item->trans_reference ?? '', 0, 2);
|
||||
$relationMap = [
|
||||
'FT' => 'ft',
|
||||
'TT' => 'tt',
|
||||
'DC' => 'dc',
|
||||
'AA' => 'aa'
|
||||
];
|
||||
|
||||
if (isset($relationMap[$prefix])) {
|
||||
$relation = $relationMap[$prefix];
|
||||
$result .= ($item->$relation?->$fieldName ?? '') . ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return str_replace('<NL>', ' ', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get processed statement entries untuk account
|
||||
* Menggunakan data dari tabel ProcessedStatement yang sudah diproses
|
||||
*
|
||||
* @param string $accountNumber
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
protected function getProcessedStatementEntries($accountNumber)
|
||||
{
|
||||
Log::info('Getting processed statement entries', [
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $this->period
|
||||
]);
|
||||
|
||||
return ProcessedStatement::where('account_number', $accountNumber)
|
||||
->where('period', $this->period)
|
||||
->orderBy('sequence_no', 'ASC')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get saldo awal bulan untuk account
|
||||
* Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||
*
|
||||
* @param string $accountNumber
|
||||
* @return object
|
||||
*/
|
||||
protected function getSaldoAwalBulan($accountNumber)
|
||||
{
|
||||
// Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||
// Ambil saldo dari ProcessedStatement entry pertama dikurangi transaction_amount
|
||||
$firstEntry = ProcessedStatement::where('account_number', $accountNumber)
|
||||
->where('period', $this->period)
|
||||
->orderBy('sequence_no', 'ASC')
|
||||
->first();
|
||||
|
||||
if ($firstEntry) {
|
||||
$saldoAwal = $firstEntry->end_balance - $firstEntry->transaction_amount;
|
||||
return (object) ['actual_balance' => $saldoAwal];
|
||||
}
|
||||
|
||||
// Fallback ke AccountBalance jika tidak ada ProcessedStatement
|
||||
$saldoPeriod = $this->calculateSaldoPeriod($this->period);
|
||||
|
||||
$saldo = AccountBalance::where('account_number', $accountNumber)
|
||||
->where('period', $saldoPeriod)
|
||||
->first();
|
||||
|
||||
return $saldo ?: (object) ['actual_balance' => 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate saldo period berdasarkan aturan bisnis
|
||||
* Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||
*
|
||||
* @param string $period
|
||||
* @return string
|
||||
*/
|
||||
protected function calculateSaldoPeriod($period)
|
||||
{
|
||||
if ($period === '202505') {
|
||||
return '20250510';
|
||||
}
|
||||
|
||||
// For periods after 202505, get last day of previous month
|
||||
if ($period > '202505') {
|
||||
$year = substr($period, 0, 4);
|
||||
$month = substr($period, 4, 2);
|
||||
$firstDay = Carbon::createFromFormat('Ym', $period)->startOfMonth();
|
||||
return $firstDay->copy()->subDay()->format('Ymd');
|
||||
}
|
||||
|
||||
return $period . '01';
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare images as base64 for PDF
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function prepareImagesForPdf()
|
||||
{
|
||||
$images = [];
|
||||
|
||||
$imagePaths = [
|
||||
'headerTableBg' => 'assets/media/images/bg-header-table.png',
|
||||
'watermark' => 'assets/media/images/watermark.png',
|
||||
'logoArthagraha' => 'assets/media/images/logo-arthagraha.png',
|
||||
'logoAgi' => 'assets/media/images/logo-agi.png',
|
||||
'bannerFooter' => 'assets/media/images/banner-footer.png'
|
||||
];
|
||||
|
||||
foreach ($imagePaths as $key => $path) {
|
||||
$fullPath = public_path($path);
|
||||
if (file_exists($fullPath)) {
|
||||
$images[$key] = base64_encode(file_get_contents($fullPath));
|
||||
} else {
|
||||
$images[$key] = null;
|
||||
Log::warning('Image file not found', ['path' => $fullPath]);
|
||||
}
|
||||
}
|
||||
|
||||
return $images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ZIP file dari multiple PDF files
|
||||
*
|
||||
* @param array $pdfFiles
|
||||
* @return string|null Path to ZIP file
|
||||
*/
|
||||
protected function createZipFile($pdfFiles)
|
||||
{
|
||||
try {
|
||||
$zipFilename = "statements_{$this->period}_multi_account_{$this->statement->id}_" . now()->format('YmdHis') . '.zip';
|
||||
$zipStoragePath = "statements/{$this->period}/multi_account/{$this->statement->id}";
|
||||
$fullZipPath = "{$zipStoragePath}/{$zipFilename}";
|
||||
|
||||
// Ensure directory exists
|
||||
Storage::disk('local')->makeDirectory($zipStoragePath);
|
||||
|
||||
$zipPath = storage_path("app/{$fullZipPath}");
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($zipPath, ZipArchive::CREATE) !== TRUE) {
|
||||
throw new Exception('Cannot create ZIP file');
|
||||
}
|
||||
|
||||
foreach ($pdfFiles as $pdfFile) {
|
||||
if (file_exists($pdfFile)) {
|
||||
$filename = basename($pdfFile);
|
||||
$zip->addFile($pdfFile, $filename);
|
||||
}
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
// Verify ZIP file was created
|
||||
if (!file_exists($zipPath)) {
|
||||
throw new Exception('ZIP file was not created');
|
||||
}
|
||||
|
||||
Log::info('ZIP file created successfully', [
|
||||
'zip_path' => $zipPath,
|
||||
'pdf_count' => count($pdfFiles),
|
||||
'statement_id' => $this->statement->id
|
||||
]);
|
||||
|
||||
// Clean up individual PDF files after creating ZIP
|
||||
foreach ($pdfFiles as $pdfFile) {
|
||||
if (file_exists($pdfFile)) {
|
||||
unlink($pdfFile);
|
||||
}
|
||||
}
|
||||
|
||||
return $zipPath;
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to create ZIP file', [
|
||||
'error' => $e->getMessage(),
|
||||
'statement_id' => $this->statement->id
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
use Exception;
|
||||
@@ -11,6 +10,7 @@
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Webstatement\Models\StmtEntry;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ProcessStmtEntryDataJob implements ShouldQueue
|
||||
{
|
||||
@@ -184,33 +184,57 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* Save batched records to the database
|
||||
* Simpan batch data ke database menggunakan updateOrCreate
|
||||
* untuk menghindari error unique constraint
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function saveBatch()
|
||||
: void
|
||||
private function saveBatch(): void
|
||||
{
|
||||
Log::info('Memulai proses saveBatch dengan updateOrCreate');
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
if (!empty($this->entryBatch)) {
|
||||
// Process in smaller chunks for better memory management
|
||||
foreach ($this->entryBatch as $entry) {
|
||||
// Extract all stmt_entry_ids from the current chunk
|
||||
$entryIds = array_column($entry, 'stmt_entry_id');
|
||||
$totalProcessed = 0;
|
||||
|
||||
// Delete existing records with these IDs to avoid conflicts
|
||||
StmtEntry::whereIn('stmt_entry_id', $entryIds)->delete();
|
||||
// Process each entry data directly (tidak ada nested array)
|
||||
foreach ($this->entryBatch as $entryData) {
|
||||
// Validasi bahwa entryData adalah array dan memiliki stmt_entry_id
|
||||
if (is_array($entryData) && isset($entryData['stmt_entry_id'])) {
|
||||
// Gunakan updateOrCreate untuk menghindari duplicate key error
|
||||
StmtEntry::updateOrCreate(
|
||||
[
|
||||
'stmt_entry_id' => $entryData['stmt_entry_id']
|
||||
],
|
||||
$entryData
|
||||
);
|
||||
|
||||
// Insert all records in the chunk at once
|
||||
StmtEntry::insert($entry);
|
||||
$totalProcessed++;
|
||||
} else {
|
||||
Log::warning('Invalid entry data structure', ['data' => $entryData]);
|
||||
$this->errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset entry batch after processing
|
||||
DB::commit();
|
||||
|
||||
Log::info("Berhasil memproses {$totalProcessed} record dengan updateOrCreate");
|
||||
|
||||
// Reset entry batch after successful processing
|
||||
$this->entryBatch = [];
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
DB::rollback();
|
||||
|
||||
Log::error("Error in saveBatch: " . $e->getMessage() . "\n" . $e->getTraceAsString());
|
||||
$this->errorCount += count($this->entryBatch);
|
||||
|
||||
// Reset batch even if there's an error to prevent reprocessing the same failed records
|
||||
$this->entryBatch = [];
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,424 +1,425 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Webstatement\Models\Account;
|
||||
use Modules\Webstatement\Models\PrintStatementLog;
|
||||
use Modules\Webstatement\Mail\StatementEmail;
|
||||
use Modules\Basicdata\Models\Branch;
|
||||
|
||||
/**
|
||||
* Job untuk mengirim email PDF statement ke nasabah
|
||||
* Mendukung pengiriman per rekening, per cabang, atau seluruh cabang
|
||||
* Menggunakan PHPMailer dengan dukungan NTLM/GSSAPI
|
||||
*/
|
||||
class SendStatementEmailJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $period;
|
||||
protected $requestType;
|
||||
protected $targetValue; // account_number, branch_code, atau null untuk all
|
||||
protected $batchId;
|
||||
protected $logId;
|
||||
use Exception;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use InvalidArgumentException;
|
||||
use Modules\Webstatement\Mail\StatementEmail;
|
||||
use Modules\Webstatement\Models\Account;
|
||||
use Modules\Webstatement\Models\PrintStatementLog;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Membuat instance job baru
|
||||
*
|
||||
* @param string $period Format: YYYYMM
|
||||
* @param string $requestType 'single_account', 'branch', 'all_branches'
|
||||
* @param string|null $targetValue account_number untuk single, branch_code untuk branch, null untuk all
|
||||
* @param string|null $batchId ID batch untuk tracking
|
||||
* @param int|null $logId ID log untuk update progress
|
||||
* Job untuk mengirim email PDF statement ke nasabah
|
||||
* Mendukung pengiriman per rekening, per cabang, atau seluruh cabang
|
||||
*/
|
||||
public function __construct($period, $requestType = 'single_account', $targetValue = null, $batchId = null, $logId = null)
|
||||
class SendStatementEmailJob implements ShouldQueue
|
||||
{
|
||||
$this->period = $period;
|
||||
$this->requestType = $requestType;
|
||||
$this->targetValue = $targetValue;
|
||||
$this->batchId = $batchId ?? uniqid('batch_');
|
||||
$this->logId = $logId;
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
Log::info('SendStatementEmailJob created with PHPMailer', [
|
||||
'period' => $this->period,
|
||||
'request_type' => $this->requestType,
|
||||
'target_value' => $this->targetValue,
|
||||
'batch_id' => $this->batchId,
|
||||
'log_id' => $this->logId
|
||||
]);
|
||||
}
|
||||
protected $period;
|
||||
protected $requestType;
|
||||
protected $targetValue; // account_number, branch_code, atau null untuk all
|
||||
protected $batchId;
|
||||
protected $logId;
|
||||
|
||||
/**
|
||||
* Menjalankan job pengiriman email statement
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
Log::info('Starting SendStatementEmailJob execution', [
|
||||
'batch_id' => $this->batchId,
|
||||
'period' => $this->period,
|
||||
'request_type' => $this->requestType,
|
||||
'target_value' => $this->targetValue
|
||||
]);
|
||||
/**
|
||||
* Membuat instance job baru
|
||||
*
|
||||
* @param string $period Format: YYYYMM
|
||||
* @param string $requestType 'single_account', 'branch', 'all_branches'
|
||||
* @param string|null $targetValue account_number untuk single, branch_code untuk branch, null untuk all
|
||||
* @param string|null $batchId ID batch untuk tracking
|
||||
* @param int|null $logId ID log untuk update progress
|
||||
*/
|
||||
public function __construct($period, $requestType = 'single_account', $targetValue = null, $batchId = null, $logId = null)
|
||||
{
|
||||
$this->period = $period;
|
||||
$this->requestType = $requestType;
|
||||
$this->targetValue = $targetValue;
|
||||
$this->batchId = $batchId ?? uniqid('batch_');
|
||||
$this->logId = $logId;
|
||||
|
||||
DB::beginTransaction();
|
||||
Log::info('SendStatementEmailJob created', [
|
||||
'period' => $this->period,
|
||||
'request_type' => $this->requestType,
|
||||
'target_value' => $this->targetValue,
|
||||
'batch_id' => $this->batchId,
|
||||
'log_id' => $this->logId
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
// Update log status menjadi processing
|
||||
$this->updateLogStatus('processing', ['started_at' => now()]);
|
||||
/**
|
||||
* Menjalankan job pengiriman email statement
|
||||
*/
|
||||
public function handle()
|
||||
: void
|
||||
{
|
||||
Log::info('Starting SendStatementEmailJob execution', [
|
||||
'batch_id' => $this->batchId,
|
||||
'period' => $this->period,
|
||||
'request_type' => $this->requestType,
|
||||
'target_value' => $this->targetValue
|
||||
]);
|
||||
|
||||
// Ambil accounts berdasarkan request type
|
||||
$accounts = $this->getAccountsByRequestType();
|
||||
DB::beginTransaction();
|
||||
|
||||
if ($accounts->isEmpty()) {
|
||||
Log::warning('No accounts with email found', [
|
||||
'period' => $this->period,
|
||||
'request_type' => $this->requestType,
|
||||
'target_value' => $this->targetValue,
|
||||
'batch_id' => $this->batchId
|
||||
try {
|
||||
// Update log status menjadi processing
|
||||
$this->updateLogStatus('processing', ['started_at' => now()]);
|
||||
|
||||
// Ambil accounts berdasarkan request type
|
||||
$accounts = $this->getAccountsByRequestType();
|
||||
|
||||
if ($accounts->isEmpty()) {
|
||||
Log::warning('No accounts with email found', [
|
||||
'period' => $this->period,
|
||||
'request_type' => $this->requestType,
|
||||
'target_value' => $this->targetValue,
|
||||
'batch_id' => $this->batchId
|
||||
]);
|
||||
|
||||
$this->updateLogStatus('completed', [
|
||||
'completed_at' => now(),
|
||||
'total_accounts' => 0,
|
||||
'processed_accounts' => 0,
|
||||
'success_count' => 0,
|
||||
'failed_count' => 0
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update total accounts
|
||||
$this->updateLogStatus('processing', [
|
||||
'total_accounts' => $accounts->count(),
|
||||
'target_accounts' => $accounts->pluck('account_number')->toArray()
|
||||
]);
|
||||
|
||||
$this->updateLogStatus('completed', [
|
||||
'completed_at' => now(),
|
||||
'total_accounts' => 0,
|
||||
'processed_accounts' => 0,
|
||||
'success_count' => 0,
|
||||
'failed_count' => 0
|
||||
$successCount = 0;
|
||||
$failedCount = 0;
|
||||
$processedCount = 0;
|
||||
|
||||
foreach ($accounts as $account) {
|
||||
try {
|
||||
$this->sendStatementEmail($account);
|
||||
$successCount++;
|
||||
|
||||
Log::info('Statement email sent successfully', [
|
||||
'account_number' => $account->account_number,
|
||||
'branch_code' => $account->branch_code,
|
||||
'email' => $this->getEmailForAccount($account),
|
||||
'batch_id' => $this->batchId
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
$failedCount++;
|
||||
|
||||
Log::error('Failed to send statement email', [
|
||||
'account_number' => $account->account_number,
|
||||
'branch_code' => $account->branch_code,
|
||||
'email' => $this->getEmailForAccount($account),
|
||||
'error' => $e->getMessage(),
|
||||
'batch_id' => $this->batchId
|
||||
]);
|
||||
}
|
||||
|
||||
$processedCount++;
|
||||
|
||||
// Update progress setiap 10 account atau di akhir
|
||||
if ($processedCount % 10 === 0 || $processedCount === $accounts->count()) {
|
||||
$this->updateLogStatus('processing', [
|
||||
'processed_accounts' => $processedCount,
|
||||
'success_count' => $successCount,
|
||||
'failed_count' => $failedCount
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Update status final
|
||||
$finalStatus = $failedCount === 0 ? 'completed' : ($successCount === 0 ? 'failed' : 'completed');
|
||||
$this->updateLogStatus($finalStatus, [
|
||||
'completed_at' => now(),
|
||||
'processed_accounts' => $processedCount,
|
||||
'success_count' => $successCount,
|
||||
'failed_count' => $failedCount
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
Log::info('SendStatementEmailJob completed', [
|
||||
'batch_id' => $this->batchId,
|
||||
'total_accounts' => $accounts->count(),
|
||||
'success_count' => $successCount,
|
||||
'failed_count' => $failedCount,
|
||||
'final_status' => $finalStatus
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
$this->updateLogStatus('failed', [
|
||||
'completed_at' => now(),
|
||||
'error_message' => $e->getMessage()
|
||||
]);
|
||||
|
||||
Log::error('SendStatementEmailJob failed', [
|
||||
'batch_id' => $this->batchId,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status log
|
||||
*/
|
||||
private function updateLogStatus($status, $additionalData = [])
|
||||
{
|
||||
if (!$this->logId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update total accounts
|
||||
$this->updateLogStatus('processing', [
|
||||
'total_accounts' => $accounts->count(),
|
||||
'target_accounts' => $accounts->pluck('account_number')->toArray()
|
||||
try {
|
||||
$updateData = array_merge(['status' => $status], $additionalData);
|
||||
PrintStatementLog::where('id', $this->logId)->update($updateData);
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to update log status', [
|
||||
'log_id' => $this->logId,
|
||||
'status' => $status,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mengambil accounts berdasarkan request type
|
||||
*/
|
||||
private function getAccountsByRequestType()
|
||||
{
|
||||
Log::info('Fetching accounts by request type', [
|
||||
'period' => $this->period,
|
||||
'request_type' => $this->requestType,
|
||||
'target_value' => $this->targetValue
|
||||
]);
|
||||
|
||||
$successCount = 0;
|
||||
$failedCount = 0;
|
||||
$processedCount = 0;
|
||||
$query = Account::with('customer')
|
||||
->where('stmt_sent_type', 'BY.EMAIL');
|
||||
|
||||
foreach ($accounts as $account) {
|
||||
try {
|
||||
$this->sendStatementEmail($account);
|
||||
$successCount++;
|
||||
switch ($this->requestType) {
|
||||
case 'single_account':
|
||||
if ($this->targetValue) {
|
||||
$query->where('account_number', $this->targetValue);
|
||||
}
|
||||
break;
|
||||
|
||||
Log::info('Statement email sent successfully', [
|
||||
'account_number' => $account->account_number,
|
||||
'branch_code' => $account->branch_code,
|
||||
'email' => $this->getEmailForAccount($account),
|
||||
'batch_id' => $this->batchId
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$failedCount++;
|
||||
case 'branch':
|
||||
if ($this->targetValue) {
|
||||
$query->where('branch_code', $this->targetValue);
|
||||
}
|
||||
break;
|
||||
|
||||
Log::error('Failed to send statement email', [
|
||||
'account_number' => $account->account_number,
|
||||
'branch_code' => $account->branch_code,
|
||||
'email' => $this->getEmailForAccount($account),
|
||||
'error' => $e->getMessage(),
|
||||
'batch_id' => $this->batchId
|
||||
]);
|
||||
}
|
||||
case 'all_branches':
|
||||
// Tidak ada filter tambahan, ambil semua
|
||||
break;
|
||||
|
||||
$processedCount++;
|
||||
|
||||
// Update progress setiap 10 account atau di akhir
|
||||
if ($processedCount % 10 === 0 || $processedCount === $accounts->count()) {
|
||||
$this->updateLogStatus('processing', [
|
||||
'processed_accounts' => $processedCount,
|
||||
'success_count' => $successCount,
|
||||
'failed_count' => $failedCount
|
||||
]);
|
||||
}
|
||||
default:
|
||||
throw new InvalidArgumentException("Invalid request type: {$this->requestType}");
|
||||
}
|
||||
|
||||
// Update status final
|
||||
$finalStatus = $failedCount === 0 ? 'completed' : ($successCount === 0 ? 'failed' : 'completed');
|
||||
$this->updateLogStatus($finalStatus, [
|
||||
'completed_at' => now(),
|
||||
'processed_accounts' => $processedCount,
|
||||
'success_count' => $successCount,
|
||||
'failed_count' => $failedCount
|
||||
$accounts = $query->get();
|
||||
|
||||
// Filter accounts yang memiliki email
|
||||
$accountsWithEmail = $accounts->filter(function ($account) {
|
||||
return !empty($account->stmt_email) ||
|
||||
($account->customer && !empty($account->customer->email));
|
||||
});
|
||||
|
||||
Log::info('Accounts with email retrieved', [
|
||||
'total_accounts' => $accounts->count(),
|
||||
'accounts_with_email' => $accountsWithEmail->count(),
|
||||
'request_type' => $this->requestType,
|
||||
'batch_id' => $this->batchId
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
return $accountsWithEmail;
|
||||
}
|
||||
|
||||
Log::info('SendStatementEmailJob completed', [
|
||||
'batch_id' => $this->batchId,
|
||||
'total_accounts' => $accounts->count(),
|
||||
'success_count' => $successCount,
|
||||
'failed_count' => $failedCount,
|
||||
'final_status' => $finalStatus
|
||||
/**
|
||||
* Mengirim email statement untuk account tertentu
|
||||
*
|
||||
* @param Account $account
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function sendStatementEmail(Account $account)
|
||||
{
|
||||
// Dapatkan email untuk pengiriman
|
||||
$emailAddress = $this->getEmailForAccount($account);
|
||||
|
||||
if (!$emailAddress) {
|
||||
throw new Exception("No email address found for account {$account->account_number}");
|
||||
}
|
||||
|
||||
// Cek apakah file PDF ada
|
||||
$pdfPath = $this->getPdfPath($account->account_number, $account->branch_code);
|
||||
|
||||
if (!Storage::exists($pdfPath)) {
|
||||
throw new Exception("PDF file not found: {$pdfPath}");
|
||||
}
|
||||
|
||||
// Buat atau update log statement
|
||||
$statementLog = $this->createOrUpdateStatementLog($account);
|
||||
|
||||
// Dapatkan path absolut file
|
||||
$absolutePdfPath = Storage::path($pdfPath);
|
||||
|
||||
// Kirim email
|
||||
// Add delay between email sends to prevent rate limiting
|
||||
sleep(1); // 2 second delay
|
||||
Mail::to($emailAddress)->send(
|
||||
new StatementEmail($statementLog, $absolutePdfPath, false)
|
||||
);
|
||||
|
||||
// Update status log dengan email yang digunakan
|
||||
$statementLog->update([
|
||||
'email_sent_at' => now(),
|
||||
'email_status' => 'sent',
|
||||
'email_address' => $emailAddress // Simpan email yang digunakan untuk tracking
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
Log::info('Email sent for account', [
|
||||
'account_number' => $account->account_number,
|
||||
'branch_code' => $account->branch_code,
|
||||
'email' => $emailAddress,
|
||||
'email_source' => !empty($account->stmt_email) ? 'account.stmt_email' : 'customer.email',
|
||||
'pdf_path' => $pdfPath,
|
||||
'batch_id' => $this->batchId
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mendapatkan email untuk pengiriman statement
|
||||
*
|
||||
* @param Account $account
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private function getEmailForAccount(Account $account)
|
||||
{
|
||||
// Prioritas pertama: stmt_email dari account
|
||||
if (!empty($account->stmt_email)) {
|
||||
Log::info('Using stmt_email from account', [
|
||||
'account_number' => $account->account_number,
|
||||
'email' => $account->stmt_email,
|
||||
'batch_id' => $this->batchId
|
||||
]);
|
||||
return $account->stmt_email;
|
||||
}
|
||||
|
||||
// Prioritas kedua: email dari customer
|
||||
if ($account->customer && !empty($account->customer->email)) {
|
||||
Log::info('Using email from customer', [
|
||||
'account_number' => $account->account_number,
|
||||
'customer_code' => $account->customer_code,
|
||||
'email' => $account->customer->email,
|
||||
'batch_id' => $this->batchId
|
||||
]);
|
||||
return $account->customer->email;
|
||||
}
|
||||
|
||||
Log::warning('No email found for account', [
|
||||
'account_number' => $account->account_number,
|
||||
'customer_code' => $account->customer_code,
|
||||
'batch_id' => $this->batchId
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mendapatkan path file PDF statement
|
||||
*
|
||||
* @param string $accountNumber
|
||||
* @param string $branchCode
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getPdfPath($accountNumber, $branchCode)
|
||||
{
|
||||
return "combine/{$this->period}/{$branchCode}/{$accountNumber}_{$this->period}.pdf";
|
||||
}
|
||||
|
||||
/**
|
||||
* Membuat atau update log statement
|
||||
*
|
||||
* @param Account $account
|
||||
*
|
||||
* @return PrintStatementLog
|
||||
*/
|
||||
private function createOrUpdateStatementLog(Account $account)
|
||||
{
|
||||
$emailAddress = $this->getEmailForAccount($account);
|
||||
|
||||
$logData = [
|
||||
'account_number' => $account->account_number,
|
||||
'customer_code' => $account->customer_code,
|
||||
'branch_code' => $account->branch_code,
|
||||
'period' => $this->period,
|
||||
'print_date' => now(),
|
||||
'batch_id' => $this->batchId,
|
||||
'email_address' => $emailAddress,
|
||||
'email_source' => !empty($account->stmt_email) ? 'account' : 'customer'
|
||||
];
|
||||
|
||||
$statementLog = PrintStatementLog::updateOrCreate(
|
||||
[
|
||||
'account_number' => $account->account_number,
|
||||
'period_from' => $this->period,
|
||||
'period_to' => $this->period
|
||||
],
|
||||
$logData
|
||||
);
|
||||
|
||||
Log::info('Statement log created/updated', [
|
||||
'log_id' => $statementLog->id,
|
||||
'account_number' => $account->account_number,
|
||||
'email_address' => $emailAddress,
|
||||
'batch_id' => $this->batchId
|
||||
]);
|
||||
|
||||
return $statementLog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle job failure
|
||||
*/
|
||||
public function failed(Throwable $exception)
|
||||
{
|
||||
$this->updateLogStatus('failed', [
|
||||
'completed_at' => now(),
|
||||
'error_message' => $e->getMessage()
|
||||
'completed_at' => now(),
|
||||
'error_message' => $exception->getMessage()
|
||||
]);
|
||||
|
||||
Log::error('SendStatementEmailJob failed', [
|
||||
'batch_id' => $this->batchId,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mengambil accounts berdasarkan request type
|
||||
*/
|
||||
private function getAccountsByRequestType()
|
||||
{
|
||||
Log::info('Fetching accounts by request type', [
|
||||
'period' => $this->period,
|
||||
'request_type' => $this->requestType,
|
||||
'target_value' => $this->targetValue
|
||||
]);
|
||||
|
||||
$query = Account::with('customer')
|
||||
->where('stmt_sent_type', 'BY.EMAIL');
|
||||
|
||||
switch ($this->requestType) {
|
||||
case 'single_account':
|
||||
if ($this->targetValue) {
|
||||
$query->where('account_number', $this->targetValue);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'branch':
|
||||
if ($this->targetValue) {
|
||||
$query->where('branch_code', $this->targetValue);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'all_branches':
|
||||
// Tidak ada filter tambahan, ambil semua
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \InvalidArgumentException("Invalid request type: {$this->requestType}");
|
||||
}
|
||||
|
||||
$accounts = $query->get();
|
||||
|
||||
// Filter accounts yang memiliki email
|
||||
$accountsWithEmail = $accounts->filter(function ($account) {
|
||||
return !empty($account->stmt_email) ||
|
||||
($account->customer && !empty($account->customer->email));
|
||||
});
|
||||
|
||||
Log::info('Accounts with email retrieved', [
|
||||
'total_accounts' => $accounts->count(),
|
||||
'accounts_with_email' => $accountsWithEmail->count(),
|
||||
'request_type' => $this->requestType,
|
||||
'batch_id' => $this->batchId
|
||||
]);
|
||||
|
||||
return $accountsWithEmail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status log
|
||||
*/
|
||||
private function updateLogStatus($status, $additionalData = [])
|
||||
{
|
||||
if (!$this->logId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$updateData = array_merge(['status' => $status], $additionalData);
|
||||
PrintStatementLog::where('id', $this->logId)->update($updateData);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to update log status', [
|
||||
'log_id' => $this->logId,
|
||||
'status' => $status,
|
||||
'error' => $e->getMessage()
|
||||
Log::error('SendStatementEmailJob failed permanently', [
|
||||
'batch_id' => $this->batchId,
|
||||
'period' => $this->period,
|
||||
'request_type' => $this->requestType,
|
||||
'target_value' => $this->targetValue,
|
||||
'error' => $exception->getMessage(),
|
||||
'trace' => $exception->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mendapatkan email untuk pengiriman statement
|
||||
*
|
||||
* @param Account $account
|
||||
* @return string|null
|
||||
*/
|
||||
private function getEmailForAccount(Account $account)
|
||||
{
|
||||
// Prioritas pertama: stmt_email dari account
|
||||
if (!empty($account->stmt_email)) {
|
||||
Log::info('Using stmt_email from account', [
|
||||
'account_number' => $account->account_number,
|
||||
'email' => $account->stmt_email,
|
||||
'batch_id' => $this->batchId
|
||||
]);
|
||||
return $account->stmt_email;
|
||||
}
|
||||
|
||||
// Prioritas kedua: email dari customer
|
||||
if ($account->customer && !empty($account->customer->email)) {
|
||||
Log::info('Using email from customer', [
|
||||
'account_number' => $account->account_number,
|
||||
'customer_code' => $account->customer_code,
|
||||
'email' => $account->customer->email,
|
||||
'batch_id' => $this->batchId
|
||||
]);
|
||||
return $account->customer->email;
|
||||
}
|
||||
|
||||
Log::warning('No email found for account', [
|
||||
'account_number' => $account->account_number,
|
||||
'customer_code' => $account->customer_code,
|
||||
'batch_id' => $this->batchId
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mengirim email statement untuk account tertentu
|
||||
*
|
||||
* @param Account $account
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function sendStatementEmail(Account $account)
|
||||
{
|
||||
// Dapatkan email untuk pengiriman
|
||||
$emailAddress = $this->getEmailForAccount($account);
|
||||
|
||||
if (!$emailAddress) {
|
||||
throw new \Exception("No email address found for account {$account->account_number}");
|
||||
}
|
||||
|
||||
// Cek apakah file PDF ada
|
||||
$pdfPath = $this->getPdfPath($account->account_number, $account->branch_code);
|
||||
|
||||
if (!Storage::exists($pdfPath)) {
|
||||
throw new \Exception("PDF file not found: {$pdfPath}");
|
||||
}
|
||||
|
||||
// Buat atau update log statement
|
||||
$statementLog = $this->createOrUpdateStatementLog($account);
|
||||
|
||||
// Dapatkan path absolut file
|
||||
$absolutePdfPath = Storage::path($pdfPath);
|
||||
|
||||
// Buat instance StatementEmail dengan PHPMailer
|
||||
$statementEmail = new StatementEmail($statementLog, $absolutePdfPath, false);
|
||||
|
||||
// Kirim email menggunakan PHPMailer
|
||||
$emailSent = $statementEmail->send($emailAddress);
|
||||
|
||||
if (!$emailSent) {
|
||||
throw new \Exception("Failed to send email to {$emailAddress} for account {$account->account_number}");
|
||||
}
|
||||
|
||||
// Update status log dengan email yang digunakan
|
||||
$statementLog->update([
|
||||
'email_sent_at' => now(),
|
||||
'email_status' => 'sent',
|
||||
'email_address' => $emailAddress // Simpan email yang digunakan untuk tracking
|
||||
]);
|
||||
|
||||
Log::info('Email sent via PHPMailer for account', [
|
||||
'account_number' => $account->account_number,
|
||||
'branch_code' => $account->branch_code,
|
||||
'email' => $emailAddress,
|
||||
'email_source' => !empty($account->stmt_email) ? 'account.stmt_email' : 'customer.email',
|
||||
'pdf_path' => $pdfPath,
|
||||
'batch_id' => $this->batchId
|
||||
]);
|
||||
|
||||
// Add delay between email sends to prevent rate limiting
|
||||
sleep(2); // 2 second delay for NTLM/GSSAPI connections
|
||||
}
|
||||
|
||||
/**
|
||||
* Mendapatkan path file PDF statement
|
||||
*
|
||||
* @param string $accountNumber
|
||||
* @param string $branchCode
|
||||
* @return string
|
||||
*/
|
||||
private function getPdfPath($accountNumber, $branchCode)
|
||||
{
|
||||
return "combine/{$this->period}/{$branchCode}/{$accountNumber}_{$this->period}.pdf";
|
||||
}
|
||||
|
||||
/**
|
||||
* Membuat atau update log statement
|
||||
*
|
||||
* @param Account $account
|
||||
* @return PrintStatementLog
|
||||
*/
|
||||
private function createOrUpdateStatementLog(Account $account)
|
||||
{
|
||||
$emailAddress = $this->getEmailForAccount($account);
|
||||
|
||||
$logData = [
|
||||
'account_number' => $account->account_number,
|
||||
'customer_code' => $account->customer_code,
|
||||
'branch_code' => $account->branch_code,
|
||||
'period' => $this->period,
|
||||
'print_date' => now(),
|
||||
'batch_id' => $this->batchId,
|
||||
'email_address' => $emailAddress,
|
||||
'email_source' => !empty($account->stmt_email) ? 'account' : 'customer'
|
||||
];
|
||||
|
||||
$statementLog = PrintStatementLog::updateOrCreate(
|
||||
[
|
||||
'account_number' => $account->account_number,
|
||||
'period_from' => $this->period,
|
||||
'period_to' => $this->period
|
||||
],
|
||||
$logData
|
||||
);
|
||||
|
||||
Log::info('Statement log created/updated', [
|
||||
'log_id' => $statementLog->id,
|
||||
'account_number' => $account->account_number,
|
||||
'email_address' => $emailAddress,
|
||||
'batch_id' => $this->batchId
|
||||
]);
|
||||
|
||||
return $statementLog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle job failure
|
||||
*/
|
||||
public function failed(\Throwable $exception)
|
||||
{
|
||||
$this->updateLogStatus('failed', [
|
||||
'completed_at' => now(),
|
||||
'error_message' => $exception->getMessage()
|
||||
]);
|
||||
|
||||
Log::error('SendStatementEmailJob failed permanently', [
|
||||
'batch_id' => $this->batchId,
|
||||
'period' => $this->period,
|
||||
'request_type' => $this->requestType,
|
||||
'target_value' => $this->targetValue,
|
||||
'error' => $exception->getMessage(),
|
||||
'trace' => $exception->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
379
app/Jobs/UpdateAllAtmCardsBatchJob.php
Normal file
379
app/Jobs/UpdateAllAtmCardsBatchJob.php
Normal file
@@ -0,0 +1,379 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Modules\Webstatement\Models\Atmcard;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Modules\Webstatement\Models\KartuSyncLog;
|
||||
|
||||
class UpdateAllAtmCardsBatchJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Konstanta untuk konfigurasi batch processing
|
||||
*/
|
||||
private const BATCH_SIZE = 100;
|
||||
private const MAX_EXECUTION_TIME = 7200; // 2 jam dalam detik
|
||||
private const DELAY_BETWEEN_JOBS = 2; // 2 detik delay antar job
|
||||
private const MAX_DELAY_SPREAD = 300; // Spread maksimal 5 menit
|
||||
|
||||
/**
|
||||
* ID log sinkronisasi
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $syncLogId;
|
||||
|
||||
/**
|
||||
* Model log sinkronisasi
|
||||
*
|
||||
* @var KartuSyncLog
|
||||
*/
|
||||
protected $syncLog;
|
||||
|
||||
/**
|
||||
* Batch size untuk processing
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $batchSize;
|
||||
|
||||
/**
|
||||
* Filter kondisi kartu yang akan diupdate
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $filters;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @param int|null $syncLogId ID log sinkronisasi
|
||||
* @param int $batchSize Ukuran batch untuk processing
|
||||
* @param array $filters Filter kondisi kartu
|
||||
*/
|
||||
public function __construct(?int $syncLogId = null, int $batchSize = self::BATCH_SIZE, array $filters = [])
|
||||
{
|
||||
$this->syncLogId = $syncLogId;
|
||||
$this->batchSize = $batchSize > 0 ? $batchSize : self::BATCH_SIZE;
|
||||
$this->filters = $filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job untuk update seluruh kartu ATM
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||
|
||||
Log::info('Memulai job update seluruh kartu ATM', [
|
||||
'sync_log_id' => $this->syncLogId,
|
||||
'batch_size' => $this->batchSize,
|
||||
'filters' => $this->filters
|
||||
]);
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// Load atau buat log sinkronisasi
|
||||
$this->loadOrCreateSyncLog();
|
||||
|
||||
// Update status job dimulai
|
||||
$this->updateJobStartStatus();
|
||||
|
||||
// Ambil total kartu yang akan diproses
|
||||
$totalCards = $this->getTotalCardsCount();
|
||||
|
||||
if ($totalCards === 0) {
|
||||
Log::info('Tidak ada kartu ATM yang perlu diupdate');
|
||||
$this->updateJobCompletedStatus(0, 0);
|
||||
DB::commit();
|
||||
return;
|
||||
}
|
||||
|
||||
Log::info("Ditemukan {$totalCards} kartu ATM yang akan diproses");
|
||||
|
||||
// Proses kartu dalam batch
|
||||
$processedCount = $this->processCardsInBatches($totalCards);
|
||||
|
||||
// Update status job selesai
|
||||
$this->updateJobCompletedStatus($totalCards, $processedCount);
|
||||
|
||||
Log::info('Job update seluruh kartu ATM selesai', [
|
||||
'total_cards' => $totalCards,
|
||||
'processed_count' => $processedCount,
|
||||
'sync_log_id' => $this->syncLog->id
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
$this->updateJobFailedStatus($e->getMessage());
|
||||
|
||||
Log::error('Gagal menjalankan job update seluruh kartu ATM: ' . $e->getMessage(), [
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'sync_log_id' => $this->syncLogId,
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load atau buat log sinkronisasi baru
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
private function loadOrCreateSyncLog(): void
|
||||
{
|
||||
Log::info('Loading atau membuat sync log', ['sync_log_id' => $this->syncLogId]);
|
||||
|
||||
if ($this->syncLogId) {
|
||||
$this->syncLog = KartuSyncLog::find($this->syncLogId);
|
||||
if (!$this->syncLog) {
|
||||
throw new Exception("Sync log dengan ID {$this->syncLogId} tidak ditemukan");
|
||||
}
|
||||
} else {
|
||||
// Buat log sinkronisasi baru
|
||||
$this->syncLog = KartuSyncLog::create([
|
||||
'periode' => now()->format('Y-m'),
|
||||
'sync_notes' => 'Batch update seluruh kartu ATM dimulai',
|
||||
'is_sync' => false,
|
||||
'sync_at' => null,
|
||||
'is_csv' => false,
|
||||
'csv_at' => null,
|
||||
'is_ftp' => false,
|
||||
'ftp_at' => null
|
||||
]);
|
||||
}
|
||||
|
||||
Log::info('Sync log berhasil dimuat/dibuat', ['sync_log_id' => $this->syncLog->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status saat job dimulai
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function updateJobStartStatus(): void
|
||||
{
|
||||
Log::info('Memperbarui status job dimulai');
|
||||
|
||||
$this->syncLog->update([
|
||||
'sync_notes' => $this->syncLog->sync_notes . "\nBatch update seluruh kartu ATM dimulai pada " . now()->format('Y-m-d H:i:s'),
|
||||
'is_sync' => false,
|
||||
'sync_at' => null
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil total jumlah kartu yang akan diproses
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function getTotalCardsCount(): int
|
||||
{
|
||||
Log::info('Menghitung total kartu yang akan diproses', ['filters' => $this->filters]);
|
||||
|
||||
$query = $this->buildCardQuery();
|
||||
$count = $query->count();
|
||||
|
||||
Log::info("Total kartu ditemukan: {$count}");
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build query untuk mengambil kartu berdasarkan filter
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
private function buildCardQuery()
|
||||
{
|
||||
$query = Atmcard::where('crsts', 1) // Kartu aktif
|
||||
->whereNotNull('accflag')
|
||||
->where('accflag', '!=', '');
|
||||
|
||||
// Terapkan filter default untuk kartu yang perlu update branch/currency
|
||||
if (empty($this->filters) || !isset($this->filters['skip_branch_currency_filter'])) {
|
||||
$query->where(function ($q) {
|
||||
$q->whereNull('branch')
|
||||
->orWhere('branch', '')
|
||||
->orWhereNull('currency')
|
||||
->orWhere('currency', '');
|
||||
});
|
||||
}
|
||||
|
||||
// Terapkan filter tambahan jika ada
|
||||
if (!empty($this->filters)) {
|
||||
foreach ($this->filters as $field => $value) {
|
||||
if ($field === 'skip_branch_currency_filter') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$query->whereIn($field, $value);
|
||||
} else {
|
||||
$query->where($field, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proses kartu dalam batch
|
||||
*
|
||||
* @param int $totalCards
|
||||
* @return int Jumlah kartu yang berhasil diproses
|
||||
*/
|
||||
private function processCardsInBatches(int $totalCards): int
|
||||
{
|
||||
Log::info('Memulai pemrosesan kartu dalam batch', [
|
||||
'total_cards' => $totalCards,
|
||||
'batch_size' => $this->batchSize
|
||||
]);
|
||||
|
||||
$processedCount = 0;
|
||||
$batchNumber = 1;
|
||||
$totalBatches = ceil($totalCards / $this->batchSize);
|
||||
|
||||
// Proses kartu dalam chunk/batch
|
||||
$this->buildCardQuery()->chunk($this->batchSize, function ($cards) use (&$processedCount, &$batchNumber, $totalBatches, $totalCards) {
|
||||
Log::info("Memproses batch {$batchNumber}/{$totalBatches}", [
|
||||
'cards_in_batch' => $cards->count(),
|
||||
'processed_so_far' => $processedCount
|
||||
]);
|
||||
|
||||
try {
|
||||
// Dispatch job untuk setiap kartu dalam batch dengan delay
|
||||
foreach ($cards as $index => $card) {
|
||||
// Hitung delay berdasarkan nomor batch dan index untuk menyebar eksekusi job
|
||||
$delay = (($batchNumber - 1) * $this->batchSize + $index) % self::MAX_DELAY_SPREAD;
|
||||
$delay += self::DELAY_BETWEEN_JOBS; // Tambah delay minimum
|
||||
|
||||
// Dispatch job UpdateAtmCardBranchCurrencyJob
|
||||
UpdateAtmCardBranchCurrencyJob::dispatch($card, $this->syncLog->id)
|
||||
->delay(now()->addSeconds($delay))
|
||||
->onQueue('default');
|
||||
|
||||
$processedCount++;
|
||||
}
|
||||
|
||||
// Update progress di log setiap 10 batch
|
||||
if ($batchNumber % 10 === 0) {
|
||||
$this->updateProgressStatus($processedCount, $totalCards, $batchNumber, $totalBatches);
|
||||
}
|
||||
|
||||
Log::info("Batch {$batchNumber} berhasil dijadwalkan", [
|
||||
'cards_scheduled' => $cards->count(),
|
||||
'total_processed' => $processedCount
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error saat memproses batch {$batchNumber}: " . $e->getMessage(), [
|
||||
'batch_number' => $batchNumber,
|
||||
'cards_count' => $cards->count(),
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$batchNumber++;
|
||||
});
|
||||
|
||||
Log::info('Selesai memproses semua batch', [
|
||||
'total_processed' => $processedCount,
|
||||
'total_batches' => $batchNumber - 1
|
||||
]);
|
||||
|
||||
return $processedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status progress pemrosesan
|
||||
*
|
||||
* @param int $processedCount
|
||||
* @param int $totalCards
|
||||
* @param int $batchNumber
|
||||
* @param int $totalBatches
|
||||
* @return void
|
||||
*/
|
||||
private function updateProgressStatus(int $processedCount, int $totalCards, int $batchNumber, int $totalBatches): void
|
||||
{
|
||||
Log::info('Memperbarui status progress', [
|
||||
'processed' => $processedCount,
|
||||
'total' => $totalCards,
|
||||
'batch' => $batchNumber,
|
||||
'total_batches' => $totalBatches
|
||||
]);
|
||||
|
||||
$percentage = round(($processedCount / $totalCards) * 100, 2);
|
||||
$progressNote = "\nProgress: {$processedCount}/{$totalCards} kartu dijadwalkan ({$percentage}%) - Batch {$batchNumber}/{$totalBatches}";
|
||||
|
||||
$this->syncLog->update([
|
||||
'sync_notes' => $this->syncLog->sync_notes . $progressNote
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status saat job selesai
|
||||
*
|
||||
* @param int $totalCards
|
||||
* @param int $processedCount
|
||||
* @return void
|
||||
*/
|
||||
private function updateJobCompletedStatus(int $totalCards, int $processedCount): void
|
||||
{
|
||||
Log::info('Memperbarui status job selesai', [
|
||||
'total_cards' => $totalCards,
|
||||
'processed_count' => $processedCount
|
||||
]);
|
||||
|
||||
$completionNote = "\nBatch update selesai pada " . now()->format('Y-m-d H:i:s') .
|
||||
" - Total {$processedCount} kartu dari {$totalCards} berhasil dijadwalkan untuk update";
|
||||
|
||||
$this->syncLog->update([
|
||||
'is_sync' => true,
|
||||
'sync_at' => now(),
|
||||
'sync_notes' => $this->syncLog->sync_notes . $completionNote
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status saat job gagal
|
||||
*
|
||||
* @param string $errorMessage
|
||||
* @return void
|
||||
*/
|
||||
private function updateJobFailedStatus(string $errorMessage): void
|
||||
{
|
||||
Log::error('Memperbarui status job gagal', ['error' => $errorMessage]);
|
||||
|
||||
if ($this->syncLog) {
|
||||
$failureNote = "\nBatch update gagal pada " . now()->format('Y-m-d H:i:s') .
|
||||
" - Error: {$errorMessage}";
|
||||
|
||||
$this->syncLog->update([
|
||||
'is_sync' => false,
|
||||
'sync_notes' => $this->syncLog->sync_notes . $failureNote
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,14 @@ namespace Modules\Webstatement\Jobs;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Modules\Webstatement\Models\Account;
|
||||
use Modules\Webstatement\Models\Atmcard;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\Webstatement\Models\Atmcard;
|
||||
|
||||
class UpdateAtmCardBranchCurrencyJob implements ShouldQueue
|
||||
{
|
||||
@@ -77,7 +78,7 @@ class UpdateAtmCardBranchCurrencyJob implements ShouldQueue
|
||||
}
|
||||
|
||||
/**
|
||||
* Get account information from the API
|
||||
* Get account information from Account model or API
|
||||
*
|
||||
* @param string $accountNumber
|
||||
* @return array|null
|
||||
@@ -85,10 +86,26 @@ class UpdateAtmCardBranchCurrencyJob implements ShouldQueue
|
||||
private function getAccountInfo(string $accountNumber): ?array
|
||||
{
|
||||
try {
|
||||
// Coba dapatkan data dari model Account terlebih dahulu
|
||||
$account = Account::where('account_number', $accountNumber)->first();
|
||||
|
||||
if ($account) {
|
||||
// Jika account ditemukan, format data sesuai dengan format response dari API
|
||||
return [
|
||||
'responseCode' => '00',
|
||||
'acctCompany' => $account->branch_code,
|
||||
'acctCurrency' => $account->currency,
|
||||
'acctType' => $account->open_category
|
||||
// Tambahkan field lain yang mungkin diperlukan
|
||||
];
|
||||
}
|
||||
|
||||
// Jika tidak ditemukan di database, ambil dari Fiorano API
|
||||
$url = env('FIORANO_URL') . self::API_BASE_PATH;
|
||||
$path = self::API_INQUIRY_PATH;
|
||||
$data = [
|
||||
'accountNo' => $accountNumber
|
||||
'accountNo' => $accountNumber,
|
||||
|
||||
];
|
||||
|
||||
$response = Http::post($url . $path, $data);
|
||||
@@ -110,6 +127,7 @@ class UpdateAtmCardBranchCurrencyJob implements ShouldQueue
|
||||
$cardData = [
|
||||
'branch' => !empty($accountInfo['acctCompany']) ? $accountInfo['acctCompany'] : null,
|
||||
'currency' => !empty($accountInfo['acctCurrency']) ? $accountInfo['acctCurrency'] : null,
|
||||
'product_code' => !empty($accountInfo['acctType']) ? $accountInfo['acctType'] : null,
|
||||
];
|
||||
|
||||
$this->card->update($cardData);
|
||||
|
||||
@@ -1,195 +1,208 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Mail;
|
||||
namespace Modules\Webstatement\Mail;
|
||||
|
||||
use Modules\Webstatement\Models\Account;
|
||||
use Modules\Webstatement\Models\PrintStatementLog;
|
||||
use Modules\Webstatement\Services\PHPMailerService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\View;
|
||||
|
||||
/**
|
||||
* Service untuk mengirim email statement menggunakan PHPMailer
|
||||
* dengan dukungan autentikasi NTLM/GSSAPI
|
||||
*/
|
||||
class StatementEmail
|
||||
{
|
||||
protected $statement;
|
||||
protected $filePath;
|
||||
protected $isZip;
|
||||
protected $phpMailerService;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
* Membuat instance email baru untuk pengiriman statement
|
||||
*
|
||||
* @param PrintStatementLog $statement
|
||||
* @param string $filePath
|
||||
* @param bool $isZip
|
||||
*/
|
||||
public function __construct(PrintStatementLog $statement, $filePath, $isZip = false)
|
||||
{
|
||||
$this->statement = $statement;
|
||||
$this->filePath = $filePath;
|
||||
$this->isZip = $isZip;
|
||||
$this->phpMailerService = new PHPMailerService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Kirim email statement
|
||||
*
|
||||
* @param string $emailAddress
|
||||
* @return bool
|
||||
*/
|
||||
public function send(string $emailAddress): bool
|
||||
{
|
||||
try {
|
||||
// Generate subject
|
||||
$subject = $this->generateSubject();
|
||||
|
||||
// Generate email body
|
||||
$body = $this->generateEmailBody();
|
||||
|
||||
// Generate attachment name
|
||||
$attachmentName = $this->generateAttachmentName();
|
||||
|
||||
// Determine MIME type
|
||||
$mimeType = $this->isZip ? 'application/zip' : 'application/pdf';
|
||||
|
||||
// Send email using PHPMailer
|
||||
$result = $this->phpMailerService->sendEmail(
|
||||
$emailAddress,
|
||||
$subject,
|
||||
$body,
|
||||
$this->filePath,
|
||||
$attachmentName,
|
||||
$mimeType
|
||||
);
|
||||
|
||||
Log::info('Statement email sent via PHPMailer', [
|
||||
'to' => $emailAddress,
|
||||
'subject' => $subject,
|
||||
'attachment' => $attachmentName,
|
||||
'account_number' => $this->statement->account_number,
|
||||
'period' => $this->statement->period_from,
|
||||
'success' => $result
|
||||
]);
|
||||
|
||||
return $result;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to send statement email via PHPMailer', [
|
||||
'to' => $emailAddress,
|
||||
'account_number' => $this->statement->account_number,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate email subject
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function generateSubject(): string
|
||||
{
|
||||
$subject = 'Statement Rekening Bank Artha Graha Internasional';
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Log;
|
||||
use Modules\Webstatement\Models\Account;
|
||||
use Modules\Webstatement\Models\PrintStatementLog;
|
||||
use Symfony\Component\Mailer\Mailer;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
// Add batch info for batch requests
|
||||
if ($this->statement->request_type && $this->statement->request_type !== 'single_account') {
|
||||
$subject .= " [{$this->statement->request_type}]";
|
||||
}
|
||||
|
||||
if ($this->statement->batch_id) {
|
||||
$subject .= " [Batch: {$this->statement->batch_id}]";
|
||||
}
|
||||
|
||||
return $subject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate email body HTML
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function generateEmailBody(): string
|
||||
class StatementEmail extends Mailable
|
||||
{
|
||||
try {
|
||||
// Get account data
|
||||
$account = Account::where('account_number', $this->statement->account_number)->first();
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
// Prepare data for view
|
||||
$data = [
|
||||
'statement' => $this->statement,
|
||||
protected $statement;
|
||||
protected $filePath;
|
||||
protected $isZip;
|
||||
protected $message;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
* Membuat instance email baru untuk pengiriman statement
|
||||
*
|
||||
* @param PrintStatementLog $statement
|
||||
* @param string $filePath
|
||||
* @param bool $isZip
|
||||
*/
|
||||
public function __construct(PrintStatementLog $statement, $filePath, $isZip = false)
|
||||
{
|
||||
$this->statement = $statement;
|
||||
$this->filePath = $filePath;
|
||||
$this->isZip = $isZip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the send method to use EsmtpTransport directly
|
||||
* Using the working configuration from Python script with multiple fallback methods
|
||||
*/
|
||||
public function send($mailer)
|
||||
{
|
||||
// Get mail configuration
|
||||
$host = Config::get('mail.mailers.smtp.host');
|
||||
$port = Config::get('mail.mailers.smtp.port');
|
||||
$username = Config::get('mail.mailers.smtp.username');
|
||||
$password = Config::get('mail.mailers.smtp.password');
|
||||
|
||||
Log::info('StatementEmail: Attempting to send email with multiple fallback methods');
|
||||
|
||||
// Define connection methods like in Python script
|
||||
$method =
|
||||
// Method 3: STARTTLS with original port
|
||||
[
|
||||
'port' => $port,
|
||||
'ssl' => false,
|
||||
'name' => 'STARTTLS (Port $port)'
|
||||
];
|
||||
|
||||
$lastException = null;
|
||||
|
||||
// Try each connection method until one succeeds
|
||||
try {
|
||||
Log::info('StatementEmail: Trying ' . $method['name']);
|
||||
|
||||
// Create EsmtpTransport with current method
|
||||
$transport = new EsmtpTransport($host, $method['port'], $method['ssl']);
|
||||
|
||||
// Set username and password
|
||||
if ($username) {
|
||||
$transport->setUsername($username);
|
||||
}
|
||||
if ($password) {
|
||||
$transport->setPassword($password);
|
||||
}
|
||||
|
||||
// Disable SSL verification for development
|
||||
$streamOptions = [
|
||||
'ssl' => [
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
'allow_self_signed' => true
|
||||
]
|
||||
];
|
||||
$transport->getStream()->setStreamOptions($streamOptions);
|
||||
|
||||
// Build the email content
|
||||
$this->build();
|
||||
|
||||
// Start transport connection
|
||||
$transport->start();
|
||||
|
||||
// Create Symfony mailer
|
||||
$symfonyMailer = new Mailer($transport);
|
||||
|
||||
// Convert Laravel message to Symfony Email
|
||||
$email = $this->toSymfonyEmail();
|
||||
|
||||
// Send the email
|
||||
$symfonyMailer->send($email);
|
||||
|
||||
// Close connection
|
||||
$transport->stop();
|
||||
|
||||
Log::info('StatementEmail: Successfully sent email using ' . $method['name']);
|
||||
return $this;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$lastException = $e;
|
||||
Log::warning('StatementEmail: Failed to send with ' . $method['name'] . ': ' . $e->getMessage());
|
||||
// Continue to next method
|
||||
}
|
||||
|
||||
try {
|
||||
return parent::send($mailer);
|
||||
} catch (Exception $e) {
|
||||
Log::error('StatementEmail: Laravel mailer also failed: ' . $e->getMessage());
|
||||
// If we got here, throw the last exception from our custom methods
|
||||
throw $lastException;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
* Membangun struktur email dengan attachment statement
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
$subject = 'Statement Rekening Bank Artha Graha Internasional';
|
||||
|
||||
if ($this->statement->is_period_range) {
|
||||
$subject .= " - {$this->statement->period_from} to {$this->statement->period_to}";
|
||||
} else {
|
||||
$subject .= " - " . Carbon::createFromFormat('Ym', $this->statement->period_from)
|
||||
->locale('id')
|
||||
->isoFormat('MMMM Y');
|
||||
}
|
||||
|
||||
$email = $this->subject($subject);
|
||||
|
||||
// Store the email in the message property for later use in toSymfonyEmail()
|
||||
$this->message = $email;
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Laravel message to Symfony Email
|
||||
*/
|
||||
protected function toSymfonyEmail()
|
||||
{
|
||||
// Build the message if it hasn't been built yet
|
||||
$this->build();
|
||||
// Create a new Symfony Email
|
||||
$email = new Email();
|
||||
|
||||
// Set from address using config values instead of trying to call getFrom()
|
||||
$fromAddress = Config::get('mail.from.address');
|
||||
$fromName = Config::get('mail.from.name');
|
||||
$email->from($fromName ? "$fromName <$fromAddress>" : $fromAddress);
|
||||
|
||||
// Set to addresses - use the to addresses from the mailer instead of trying to call getTo()
|
||||
// We'll get the to addresses from the Mail facade when the email is sent
|
||||
// For now, we'll just add a placeholder recipient that will be overridden by the Mail facade
|
||||
$email->to($this->message->to[0]['address']);
|
||||
|
||||
$email->subject($this->message->subject);
|
||||
|
||||
// Set body - use a simple HTML content instead of trying to call getHtmlBody()
|
||||
// In a real implementation, we would need to find a way to access the rendered HTML content
|
||||
$email->html(view('webstatement::statements.email', [
|
||||
'statement' => $this->statement,
|
||||
'accountNumber' => $this->statement->account_number,
|
||||
'periodFrom' => $this->statement->period_from,
|
||||
'periodTo' => $this->statement->period_to,
|
||||
'isRange' => $this->statement->is_period_range,
|
||||
'requestType' => $this->statement->request_type,
|
||||
'batchId' => $this->statement->batch_id,
|
||||
'accounts' => $account
|
||||
];
|
||||
'periodFrom' => $this->statement->period_from,
|
||||
'periodTo' => $this->statement->period_to,
|
||||
'isRange' => $this->statement->is_period_range,
|
||||
'requestType' => $this->statement->request_type,
|
||||
'batchId' => $this->statement->batch_id,
|
||||
'accounts' => Account::where('account_number', $this->statement->account_number)->first()
|
||||
])->render());
|
||||
//$email->text($this->message->getTextBody());
|
||||
|
||||
// Render view to HTML
|
||||
return View::make('webstatement::statements.email', $data)->render();
|
||||
// Add attachments - use the file path directly instead of trying to call getAttachments()
|
||||
if ($this->filePath && file_exists($this->filePath)) {
|
||||
if ($this->isZip) {
|
||||
$fileName = "{$this->statement->account_number}_{$this->statement->period_from}_to_{$this->statement->period_to}.zip";
|
||||
$contentType = 'application/zip';
|
||||
} else {
|
||||
$fileName = "{$this->statement->account_number}_{$this->statement->period_from}.pdf";
|
||||
$contentType = 'application/pdf';
|
||||
}
|
||||
$email->attachFromPath($this->filePath, $fileName, $contentType);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to generate email body', [
|
||||
'account_number' => $this->statement->account_number,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
// Fallback to simple HTML
|
||||
return $this->generateFallbackEmailBody();
|
||||
return $email;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate fallback email body
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function generateFallbackEmailBody(): string
|
||||
{
|
||||
$periodText = $this->statement->is_period_range
|
||||
? "periode {$this->statement->period_from} sampai {$this->statement->period_to}"
|
||||
: "periode " . \Carbon\Carbon::createFromFormat('Ym', $this->statement->period_from)->locale('id')->isoFormat('MMMM Y');
|
||||
|
||||
return "
|
||||
<html>
|
||||
<body>
|
||||
<h2>Statement Rekening Bank Artha Graha Internasional</h2>
|
||||
<p>Yth. Nasabah,</p>
|
||||
<p>Terlampir adalah statement rekening Anda untuk {$periodText}.</p>
|
||||
<p>Nomor Rekening: {$this->statement->account_number}</p>
|
||||
<p>Terima kasih atas kepercayaan Anda.</p>
|
||||
<br>
|
||||
<p>Salam,<br>Bank Artha Graha Internasional</p>
|
||||
</body>
|
||||
</html>
|
||||
";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate attachment filename
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function generateAttachmentName(): string
|
||||
{
|
||||
if ($this->isZip) {
|
||||
return "{$this->statement->account_number}_{$this->statement->period_from}_to_{$this->statement->period_to}.zip";
|
||||
} else {
|
||||
return "{$this->statement->account_number}_{$this->statement->period_from}.pdf";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Modules\Webstatement\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
// use Modules\Webstatement\Database\Factories\AtmcardFactory;
|
||||
|
||||
class Atmcard extends Model
|
||||
@@ -15,7 +16,64 @@ class Atmcard extends Model
|
||||
*/
|
||||
protected $guarded = ['id'];
|
||||
|
||||
/**
|
||||
* Relasi ke tabel JenisKartu untuk mendapatkan informasi biaya kartu
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function biaya(){
|
||||
Log::info('Mengakses relasi biaya untuk ATM card', ['card_id' => $this->id]);
|
||||
return $this->belongsTo(JenisKartu::class,'ctdesc','code');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk mendapatkan kartu ATM yang aktif
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
Log::info('Menggunakan scope active untuk filter kartu ATM aktif');
|
||||
return $query->where('crsts', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk mendapatkan kartu berdasarkan product_code
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $productCode
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeByProductCode($query, $productCode)
|
||||
{
|
||||
Log::info('Menggunakan scope byProductCode', ['product_code' => $productCode]);
|
||||
return $query->where('product_code', $productCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor untuk mendapatkan product_code dengan format yang konsisten
|
||||
*
|
||||
* @param string $value
|
||||
* @return string|null
|
||||
*/
|
||||
public function getProductCodeAttribute($value)
|
||||
{
|
||||
return $value ? strtoupper(trim($value)) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutator untuk menyimpan product_code dengan format yang konsisten
|
||||
*
|
||||
* @param string $value
|
||||
* @return void
|
||||
*/
|
||||
public function setProductCodeAttribute($value)
|
||||
{
|
||||
$this->attributes['product_code'] = $value ? strtoupper(trim($value)) : null;
|
||||
Log::info('Product code diset untuk ATM card', [
|
||||
'card_id' => $this->id ?? 'new',
|
||||
'product_code' => $this->attributes['product_code']
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,13 @@ class Customer extends Model
|
||||
'email',
|
||||
'sector',
|
||||
'customer_type',
|
||||
'birth_incorp_date'
|
||||
'birth_incorp_date',
|
||||
'home_rt',
|
||||
'home_rw',
|
||||
'ktp_rt',
|
||||
'ktp_rw',
|
||||
'local_ref'
|
||||
];
|
||||
|
||||
public function accounts(){
|
||||
return $this->hasMany(Account::class, 'customer_code', 'customer_code');
|
||||
}
|
||||
|
||||
@@ -44,11 +44,14 @@ class PrintStatementLog extends Model
|
||||
'remarks',
|
||||
'email',
|
||||
'email_sent_at',
|
||||
'stmt_sent_type',
|
||||
'is_generated',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_period_range' => 'boolean',
|
||||
'is_available' => 'boolean',
|
||||
'is_generated' => 'boolean',
|
||||
'is_downloaded' => 'boolean',
|
||||
'downloaded_at' => 'datetime',
|
||||
'authorized_at' => 'datetime',
|
||||
|
||||
@@ -6,18 +6,19 @@ use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Nwidart\Modules\Traits\PathNamespace;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Modules\Webstatement\Console\CheckEmailProgressCommand;
|
||||
use Modules\Webstatement\Console\UnlockPdf;
|
||||
use Modules\Webstatement\Console\CombinePdf;
|
||||
use Modules\Webstatement\Console\ConvertHtmlToPdf;
|
||||
use Modules\Webstatement\Console\ExportDailyStatements;
|
||||
use Modules\Webstatement\Console\ProcessDailyMigration;
|
||||
use Modules\Webstatement\Console\ExportPeriodStatements;
|
||||
use Modules\Webstatement\Console\UpdateAllAtmCardsCommand;
|
||||
use Modules\Webstatement\Console\CheckEmailProgressCommand;
|
||||
use Modules\Webstatement\Console\GenerateBiayakartuCommand;
|
||||
use Modules\Webstatement\Console\SendStatementEmailCommand;
|
||||
use Modules\Webstatement\Jobs\UpdateAtmCardBranchCurrencyJob;
|
||||
use Modules\Webstatement\Console\GenerateAtmTransactionReport;
|
||||
use Modules\Webstatement\Console\GenerateBiayaKartuCsvCommand;
|
||||
use Modules\Webstatement\Console\SendStatementEmailCommand;
|
||||
|
||||
class WebstatementServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -70,7 +71,8 @@ class WebstatementServiceProvider extends ServiceProvider
|
||||
ExportPeriodStatements::class,
|
||||
GenerateAtmTransactionReport::class,
|
||||
SendStatementEmailCommand::class,
|
||||
CheckEmailProgressCommand::class
|
||||
CheckEmailProgressCommand::class,
|
||||
UpdateAllAtmCardsCommand::class
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Menjalankan migration untuk menambahkan field product_code pada tabel atmcards
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Log::info('Memulai migration: menambahkan field product_code ke tabel atmcards');
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
Schema::table('atmcards', function (Blueprint $table) {
|
||||
// Menambahkan field product_code setelah field ctdesc
|
||||
$table->string('product_code')->nullable()->after('ctdesc')->comment('Kode produk kartu ATM');
|
||||
});
|
||||
|
||||
DB::commit();
|
||||
Log::info('Migration berhasil: field product_code telah ditambahkan ke tabel atmcards');
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollback();
|
||||
Log::error('Migration gagal: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Membalikkan migration dengan menghapus field product_code dari tabel atmcards
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Log::info('Memulai rollback migration: menghapus field product_code dari tabel atmcards');
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
Schema::table('atmcards', function (Blueprint $table) {
|
||||
$table->dropColumn('product_code');
|
||||
});
|
||||
|
||||
DB::commit();
|
||||
Log::info('Rollback migration berhasil: field product_code telah dihapus dari tabel atmcards');
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollback();
|
||||
Log::error('Rollback migration gagal: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('print_statement_logs', function (Blueprint $table) {
|
||||
$table->string('stmt_sent_type')->after('status')->nullable();
|
||||
$table->boolean('is_generated')->after('is_available')->nullable()->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('print_statement_logs', function (Blueprint $table) {
|
||||
$table->dropColumn('stmt_sent_type');
|
||||
$table->dropColumn('is_generated');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* Menambahkan field tambahan ke tabel customers:
|
||||
* - home_rt: RT alamat rumah
|
||||
* - home_rw: RW alamat rumah
|
||||
* - ktp_rt: RT alamat KTP
|
||||
* - ktp_rw: RW alamat KTP
|
||||
* - local_ref: Referensi lokal dengan data panjang
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('customers', function (Blueprint $table) {
|
||||
// Field RT dan RW untuk alamat rumah
|
||||
$table->string('home_rt', 10)->nullable()->comment('RT alamat rumah');
|
||||
$table->string('home_rw', 10)->nullable()->comment('RW alamat rumah');
|
||||
|
||||
// Field RT dan RW untuk alamat KTP
|
||||
$table->string('ktp_rt', 10)->nullable()->comment('RT alamat KTP');
|
||||
$table->string('ktp_rw', 10)->nullable()->comment('RW alamat KTP');
|
||||
|
||||
// Field untuk referensi lokal dengan tipe data TEXT untuk menampung data panjang
|
||||
$table->text('local_ref')->nullable()->comment('Referensi lokal dengan data panjang');
|
||||
|
||||
// Menambahkan index untuk performa query jika diperlukan
|
||||
$table->index(['home_rt', 'home_rw'], 'idx_customers_home_rt_rw');
|
||||
$table->index(['ktp_rt', 'ktp_rw'], 'idx_customers_ktp_rt_rw');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* Menghapus field yang ditambahkan jika migration di-rollback
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('customers', function (Blueprint $table) {
|
||||
// Hapus index terlebih dahulu
|
||||
$table->dropIndex('idx_customers_home_rt_rw');
|
||||
$table->dropIndex('idx_customers_ktp_rt_rw');
|
||||
|
||||
// Hapus kolom yang ditambahkan
|
||||
$table->dropColumn([
|
||||
'home_rt',
|
||||
'home_rw',
|
||||
'ktp_rt',
|
||||
'ktp_rw',
|
||||
'local_ref'
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Menjalankan migrasi untuk menambahkan support multi_account
|
||||
* Menggunakan constraint check sebagai alternatif enum
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
// Hapus constraint enum yang lama jika ada
|
||||
DB::statement("ALTER TABLE print_statement_logs DROP CONSTRAINT IF EXISTS print_statement_logs_request_type_check");
|
||||
|
||||
// Ubah kolom menjadi varchar
|
||||
Schema::table('print_statement_logs', function (Blueprint $table) {
|
||||
$table->string('request_type', 50)->change();
|
||||
});
|
||||
|
||||
// Tambahkan constraint check baru dengan multi_account
|
||||
DB::statement("
|
||||
ALTER TABLE print_statement_logs
|
||||
ADD CONSTRAINT print_statement_logs_request_type_check
|
||||
CHECK (request_type IN ('single_account', 'branch', 'all_branches', 'multi_account'))
|
||||
");
|
||||
|
||||
DB::commit();
|
||||
Log::info('Migration berhasil: request_type sekarang mendukung multi_account');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollback();
|
||||
Log::error('Migration gagal: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Membalikkan migrasi
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
// Hapus constraint yang baru
|
||||
DB::statement("ALTER TABLE print_statement_logs DROP CONSTRAINT IF EXISTS print_statement_logs_request_type_check");
|
||||
|
||||
// Kembalikan constraint lama tanpa multi_account
|
||||
DB::statement("
|
||||
ALTER TABLE print_statement_logs
|
||||
ADD CONSTRAINT print_statement_logs_request_type_check
|
||||
CHECK (request_type IN ('single_account', 'branch', 'all_branches'))
|
||||
");
|
||||
|
||||
DB::commit();
|
||||
Log::info('Migration rollback berhasil: multi_account dihapus dari request_type');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollback();
|
||||
Log::error('Migration rollback gagal: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -8,7 +8,9 @@
|
||||
"providers": [
|
||||
"Modules\\Webstatement\\Providers\\WebstatementServiceProvider"
|
||||
],
|
||||
"files": [],
|
||||
"files": [
|
||||
"app/Helpers/helpers.php"
|
||||
],
|
||||
"menu": {
|
||||
"main": [
|
||||
{
|
||||
@@ -30,7 +32,8 @@
|
||||
"attributes": [],
|
||||
"permission": "",
|
||||
"roles": [
|
||||
"administrator"
|
||||
"administrator",
|
||||
"customer_service"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 90%;
|
||||
margin: 20px auto;
|
||||
max-width: 100%;
|
||||
margin: 0px auto;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
@@ -37,7 +37,7 @@
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 30px;
|
||||
padding: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
Silahkan gunakan password Electronic Statement Anda untuk membukanya.<br><br>
|
||||
Password standar Elektronic Statement ini adalah <strong>ddMonyyyyxx</strong> (contoh: 01Aug1970xx)
|
||||
dimana :
|
||||
<ul style="list-style-type: none">
|
||||
<ul style="list-style-type: none;">
|
||||
<li>- dd : <strong>2 digit</strong> tanggal lahir anda, contoh: 01</li>
|
||||
<li>- Mon :
|
||||
<strong>3 huruf pertama</strong> bulan lahir anda dalam bahasa Ingris. Huruf pertama adalah
|
||||
@@ -114,7 +114,7 @@
|
||||
Please use your Electronic Statement password to open it.<br><br>
|
||||
|
||||
The Electronic Statement standard password is <strong>ddMonyyyyxx</strong> (example: 01Aug1970xx) where:
|
||||
<ul style="list-style-type: none">
|
||||
<ul style="list-style-type: none;">
|
||||
<li>- dd : <strong>The first 2 digits</strong> of your birthdate, example: 01</li>
|
||||
<li>- Mon :
|
||||
<strong>The first 3 letters</strong> of your birth month in English. The first letter is
|
||||
@@ -137,10 +137,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>© {{ date('Y') }} Bank Artha Graha Internasional. All rights reserved.</p>
|
||||
<p>Jika Anda memiliki pertanyaan, silakan hubungi customer service kami.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -11,41 +11,99 @@
|
||||
<h3 class="card-title">Request Print Stetement</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ isset($statement) ? route('statements.update', $statement->id) : route('statements.store') }}" method="POST">
|
||||
<form
|
||||
action="{{ isset($statement) ? route('statements.update', $statement->id) : route('statements.store') }}"
|
||||
method="POST">
|
||||
@csrf
|
||||
@if(isset($statement))
|
||||
@if (isset($statement))
|
||||
@method('PUT')
|
||||
@endif
|
||||
|
||||
<div class="grid grid-cols-1 gap-5">
|
||||
@if ($multiBranch)
|
||||
<div class="form-group">
|
||||
<label class="form-label required" for="branch_code">Branch/Cabang</label>
|
||||
<select
|
||||
class="input form-control tomselect @error('branch_code') border-danger bg-danger-light @enderror"
|
||||
id="branch_code" name="branch_code" required>
|
||||
<option value="">Pilih Branch/Cabang</option>
|
||||
@foreach ($branches as $branchOption)
|
||||
<option value="{{ $branchOption->code }}"
|
||||
{{ old('branch_code', $statement->branch_code ?? ($branch->code ?? '')) == $branchOption->code ? 'selected' : '' }}>
|
||||
{{ $branchOption->code }} - {{ $branchOption->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('branch_code')
|
||||
<div class="text-sm alert text-danger">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
@else
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="branch_display">Branch/Cabang</label>
|
||||
<input type="text" class="input form-control" id="branch_display"
|
||||
value="{{ $branch->code ?? '' }} - {{ $branch->name ?? '' }}" readonly>
|
||||
<input type="hidden" name="branch_code" value="{{ $branch->code ?? '' }}">
|
||||
@error('branch_code')
|
||||
<div class="text-sm alert text-danger">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label required" for="branch_code">Branch</label>
|
||||
<select class="select tomselect @error('branch_code') is-invalid @enderror" id="branch_code" name="branch_code" required>
|
||||
<option value="">Select Branch</option>
|
||||
@foreach($branches as $branch)
|
||||
<option value="{{ $branch->code }}" {{ (old('branch_code', $statement->branch_code ?? '') == $branch->code) ? 'selected' : '' }}>
|
||||
{{ $branch->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
<label class="form-label" for="stmt_sent_type">Statement Type</label>
|
||||
<select
|
||||
class="select tomselect @error('stmt_sent_type') border-danger bg-danger-light @enderror"
|
||||
id="stmt_sent_type" name="stmt_sent_type[]" multiple>
|
||||
<option value="ALL"
|
||||
{{ in_array('ALL', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}>
|
||||
ALL
|
||||
</option>
|
||||
<option value="BY.EMAIL"
|
||||
{{ in_array('BY.EMAIL', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}>
|
||||
BY EMAIL
|
||||
</option>
|
||||
<option value="BY.MAIL.TO.DOM.ADDR"
|
||||
{{ in_array('BY.MAIL.TO.DOM.ADDR', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}>
|
||||
BY MAIL TO DOM ADDR
|
||||
</option>
|
||||
<option value="BY.MAIL.TO.KTP.ADDR"
|
||||
{{ in_array('BY.MAIL.TO.KTP.ADDR', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}>
|
||||
BY MAIL TO KTP ADDR
|
||||
</option>
|
||||
<option value="NO.PRINT"
|
||||
{{ in_array('NO.PRINT', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}>
|
||||
NO PRINT
|
||||
</option>
|
||||
<option value="PRINT"
|
||||
{{ in_array('PRINT', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}>
|
||||
PRINT
|
||||
</option>
|
||||
</select>
|
||||
@error('branch_code')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@error('stmt_sent_type')
|
||||
<div class="text-sm alert text-danger">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label required" for="account_number">Account Number</label>
|
||||
<input type="text" class="input form-control @error('account_number') is-invalid @enderror" id="account_number" name="account_number" value="{{ old('account_number', $statement->account_number ?? '') }}" required>
|
||||
<label class="form-label" for="account_number">Account Number</label>
|
||||
<input type="text"
|
||||
class="input form-control @error('account_number') border-danger bg-danger-light @enderror"
|
||||
id="account_number" name="account_number"
|
||||
value="{{ old('account_number', $statement->account_number ?? '') }}">
|
||||
@error('account_number')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
<div class="text-sm alert text-danger">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="email">Email</label>
|
||||
<input type="email" class="input form-control @error('email') is-invalid @enderror" id="email" name="email" value="{{ old('email', $statement->email ?? '') }}" placeholder="Optional email for send statement">
|
||||
<input type="email"
|
||||
class="input form-control @error('email') border-danger bg-danger-light @enderror"
|
||||
id="email" name="email" value="{{ old('email', $statement->email ?? '') }}"
|
||||
placeholder="Optional email for send statement">
|
||||
@error('email')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
<div class="text-sm alert text-danger">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
@@ -53,24 +111,21 @@
|
||||
<label class="form-label required" for="start_date">Start Date</label>
|
||||
|
||||
<input class="input @error('period_from') border-danger bg-danger-light @enderror"
|
||||
type="month"
|
||||
name="period_from"
|
||||
value="{{ $statement->period_from ?? old('period_from') }}"
|
||||
max="{{ date('Y-m', strtotime('-1 month')) }}">
|
||||
type="month" name="period_from"
|
||||
value="{{ $statement->period_from ?? old('period_from') }}"
|
||||
max="{{ date('Y-m', strtotime('-1 month')) }}">
|
||||
@error('period_from')
|
||||
<em class="alert text-danger text-sm">{{ $message }}</em>
|
||||
<em class="text-sm alert text-danger">{{ $message }}</em>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label required" for="end_date">End Date</label>
|
||||
<input class="input @error('period_to') border-danger bg-danger-light @enderror"
|
||||
type="month"
|
||||
name="period_to"
|
||||
value="{{ $statement->period_to ?? old('period_to') }}"
|
||||
max="{{ date('Y-m', strtotime('-1 month')) }}">
|
||||
<input class="input @error('period_to') border-danger bg-danger-light @enderror" type="month"
|
||||
name="period_to" value="{{ $statement->period_to ?? old('period_to') }}"
|
||||
max="{{ date('Y-m', strtotime('-1 month')) }}">
|
||||
@error('period_to')
|
||||
<em class="alert text-danger text-sm">{{ $message }}</em>
|
||||
<em class="text-sm alert text-danger">{{ $message }}</em>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,279 +140,357 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-6">
|
||||
<div class="card card-grid min-w-full" data-datatable="false" data-datatable-page-size="10" data-datatable-state-save="false" id="statement-table" data-api-url="{{ route('statements.datatables') }}">
|
||||
<div class="card-header py-5 flex-wrap">
|
||||
<h3 class="card-title">
|
||||
Daftar Statement Request
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-2 lg:gap-5">
|
||||
<div class="flex">
|
||||
<label class="input input-sm"> <i class="ki-filled ki-magnifier"> </i>
|
||||
<input placeholder="Search Statement" id="search" type="text" value="">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="scrollable-x-auto">
|
||||
<table class="table table-auto table-border align-middle text-gray-700 font-medium text-sm" data-datatable-table="true">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-14">
|
||||
<input class="checkbox checkbox-sm" data-datatable-check="true" type="checkbox"/>
|
||||
</th>
|
||||
<th class="min-w-[100px]" data-datatable-column="id">
|
||||
<span class="sort"> <span class="sort-label"> ID </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="branch_name">
|
||||
<span class="sort"> <span class="sort-label"> Branch </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="account_number">
|
||||
<span class="sort"> <span class="sort-label"> Account Number </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="period">
|
||||
<span class="sort"> <span class="sort-label"> Period </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="authorization_status">
|
||||
<span class="sort"> <span class="sort-label"> Status </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="is_available">
|
||||
<span class="sort"> <span class="sort-label"> Available </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="remarks">
|
||||
<span class="sort"> <span class="sort-label"> Notes </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[180px]" data-datatable-column="created_at">
|
||||
<span class="sort"> <span class="sort-label"> Created At </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[50px] text-center" data-datatable-column="actions">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer justify-center md:justify-between flex-col md:flex-row gap-3 text-gray-600 text-2sm font-medium">
|
||||
<div class="flex items-center gap-2">
|
||||
Show
|
||||
<select class="select select-sm w-16" data-datatable-size="true" name="perpage"> </select> per page
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span data-datatable-info="true"> </span>
|
||||
<div class="pagination" data-datatable-pagination="true">
|
||||
<div class="min-w-full card card-grid" data-datatable="false" data-datatable-page-size="10"
|
||||
data-datatable-state-save="false" id="statement-table" data-api-url="{{ route('statements.datatables') }}">
|
||||
<div class="flex-wrap py-5 card-header">
|
||||
<div class="min-w-full card card-grid" data-datatable="false" data-datatable-page-size="10"
|
||||
data-datatable-state-save="false" id="statement-table"
|
||||
data-api-url="{{ route('statements.datatables') }}">
|
||||
<div class="flex-wrap py-5 card-header">
|
||||
<h3 class="card-title">
|
||||
Daftar Statement Request
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-2 lg:gap-5">
|
||||
<div class="flex">
|
||||
<label class="input input-sm"> <i class="ki-filled ki-magnifier"> </i>
|
||||
<input placeholder="Search Statement" id="search" type="text"
|
||||
value="">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
<div class="card-body">
|
||||
<div class="scrollable-x-auto">
|
||||
<table class="table text-sm font-medium text-gray-700 align-middle table-auto table-border"
|
||||
data-datatable-table="true">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-14">
|
||||
<input class="checkbox checkbox-sm" data-datatable-check="true"
|
||||
type="checkbox" />
|
||||
</th>
|
||||
<th class="min-w-[100px]" data-datatable-column="id">
|
||||
<span class="sort"> <span class="sort-label"> ID </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="branch_name">
|
||||
<span class="sort"> <span class="sort-label"> Branch </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="account_number">
|
||||
<span class="sort"> <span class="sort-label"> Account Number </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="period">
|
||||
<span class="sort"> <span class="sort-label"> Period </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="is_available">
|
||||
<span class="sort"> <span class="sort-label"> Available </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="is_generated">
|
||||
<span class="sort"> <span class="sort-label"> Generated </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="remarks">
|
||||
<span class="sort"> <span class="sort-label"> Notes </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[180px]" data-datatable-column="created_at">
|
||||
<span class="sort"> <span class="sort-label"> Created At </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[50px] text-center" data-datatable-column="actions">
|
||||
Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
<div
|
||||
class="flex-col gap-3 justify-center font-medium text-gray-600 card-footer md:justify-between md:flex-row text-2sm">
|
||||
<div class="flex gap-2 items-center">
|
||||
<div
|
||||
class="flex-col gap-3 justify-center font-medium text-gray-600 card-footer md:justify-between md:flex-row text-2sm">
|
||||
<div class="flex gap-2 items-center">
|
||||
Show
|
||||
<select class="w-16 select select-sm" data-datatable-size="true"
|
||||
name="perpage"> </select>
|
||||
per page
|
||||
<select class="w-16 select select-sm" data-datatable-size="true"
|
||||
name="perpage"> </select>
|
||||
per page
|
||||
</div>
|
||||
<div class="flex gap-4 items-center">
|
||||
<div class="flex gap-4 items-center">
|
||||
<span data-datatable-info="true"> </span>
|
||||
<div class="pagination" data-datatable-pagination="true">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
function deleteData(data) {
|
||||
Swal.fire({
|
||||
title: 'Are you sure?',
|
||||
text: "You won't be able to revert this!",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Yes, delete it!'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$.ajaxSetup({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
}
|
||||
});
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
/**
|
||||
* Fungsi untuk menghapus data statement
|
||||
* @param {number} data - ID statement yang akan dihapus
|
||||
*/
|
||||
function deleteData(data) {
|
||||
Swal.fire({
|
||||
title: 'Are you sure?',
|
||||
text: "You won't be able to revert this!",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Yes, delete it!'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$.ajaxSetup({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
}
|
||||
});
|
||||
|
||||
$.ajax(`statements/${data}`, {
|
||||
type: 'DELETE'
|
||||
}).then((response) => {
|
||||
swal.fire('Deleted!', 'Statement request has been deleted.', 'success').then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.error('Error:', error);
|
||||
Swal.fire('Error!', 'An error occurred while deleting the record.', 'error');
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
$.ajax(`statements/${data}`, {
|
||||
type: 'DELETE'
|
||||
}).then((response) => {
|
||||
swal.fire('Deleted!', 'Statement request has been deleted.', 'success').then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.error('Error:', error);
|
||||
Swal.fire('Error!', 'An error occurred while deleting the record.', 'error');
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
<script type="module">
|
||||
const element = document.querySelector('#statement-table');
|
||||
const searchInput = document.getElementById('search');
|
||||
/**
|
||||
* Konfirmasi email sebelum submit form
|
||||
* Menampilkan SweetAlert jika email diisi untuk konfirmasi pengiriman
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.querySelector('form');
|
||||
const emailInput = document.getElementById('email');
|
||||
|
||||
const apiUrl = element.getAttribute('data-api-url');
|
||||
const dataTableOptions = {
|
||||
apiEndpoint: apiUrl,
|
||||
pageSize: 10,
|
||||
columns: {
|
||||
select: {
|
||||
render: (item, data, context) => {
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.className = 'checkbox checkbox-sm';
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.value = data.id.toString();
|
||||
checkbox.setAttribute('data-datatable-row-check', 'true');
|
||||
return checkbox.outerHTML.trim();
|
||||
},
|
||||
},
|
||||
id: {
|
||||
title: 'ID',
|
||||
},
|
||||
branch_name: {
|
||||
title: 'Branch',
|
||||
},
|
||||
account_number: {
|
||||
title: 'Account Number',
|
||||
},
|
||||
period: {
|
||||
title: 'Period',
|
||||
render: (item, data) => {
|
||||
const monthNames = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'
|
||||
];
|
||||
// Log: Inisialisasi event listener untuk konfirmasi email
|
||||
console.log('Email confirmation listener initialized');
|
||||
|
||||
const formatPeriod = (period) => {
|
||||
if (!period) return '';
|
||||
const year = period.substring(0, 4);
|
||||
const month = parseInt(period.substring(4, 6));
|
||||
return `${monthNames[month - 1]} ${year}`;
|
||||
};
|
||||
form.addEventListener('submit', function(e) {
|
||||
const emailValue = emailInput.value.trim();
|
||||
|
||||
const fromPeriod = formatPeriod(data.period_from);
|
||||
const toPeriod = data.period_to ? ` - ${formatPeriod(data.period_to)}` : '';
|
||||
// Jika email diisi, tampilkan konfirmasi
|
||||
if (emailValue) {
|
||||
e.preventDefault(); // Hentikan submit form sementara
|
||||
|
||||
return fromPeriod + toPeriod;
|
||||
},
|
||||
},
|
||||
authorization_status: {
|
||||
title: 'Status',
|
||||
render: (item, data) => {
|
||||
let statusClass = 'badge badge-light-primary';
|
||||
// Log: Email terdeteksi, menampilkan konfirmasi
|
||||
console.log('Email detected:', emailValue);
|
||||
|
||||
if (data.authorization_status === 'approved') {
|
||||
statusClass = 'badge badge-light-success';
|
||||
} else if (data.authorization_status === 'rejected') {
|
||||
statusClass = 'badge badge-light-danger';
|
||||
} else if (data.authorization_status === 'pending') {
|
||||
statusClass = 'badge badge-light-warning';
|
||||
}
|
||||
Swal.fire({
|
||||
title: 'Konfirmasi Pengiriman Email',
|
||||
text: `Apakah Anda yakin ingin mengirimkan statement ke email: ${emailValue}?`,
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Ya, Kirim Email',
|
||||
cancelButtonText: 'Batal',
|
||||
reverseButtons: true
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
// Log: User konfirmasi pengiriman email
|
||||
console.log('User confirmed email sending');
|
||||
|
||||
return `<span class="${statusClass}">${data.authorization_status}</span>`;
|
||||
},
|
||||
},
|
||||
is_available: {
|
||||
title: 'Available',
|
||||
render: (item, data) => {
|
||||
let statusClass = data.is_available ? 'badge badge-light-success' : 'badge badge-light-danger';
|
||||
let statusText = data.is_available ? 'Yes' : 'No';
|
||||
return `<span class="${statusClass}">${statusText}</span>`;
|
||||
},
|
||||
},
|
||||
remarks : {
|
||||
title: 'Notes',
|
||||
},
|
||||
created_at: {
|
||||
title: 'Created At',
|
||||
render: (item, data) => {
|
||||
return data.created_at ?? '';
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
title: 'Actions',
|
||||
render: (item, data) => {
|
||||
let buttons = `<div class="flex flex-nowrap justify-center">
|
||||
// Submit form setelah konfirmasi
|
||||
form.submit();
|
||||
} else {
|
||||
// Log: User membatalkan pengiriman email
|
||||
console.log('User cancelled email sending');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Log: Tidak ada email, submit form normal
|
||||
console.log('No email provided, submitting form normally');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
const element = document.querySelector('#statement-table');
|
||||
const searchInput = document.getElementById('search');
|
||||
|
||||
const apiUrl = element.getAttribute('data-api-url');
|
||||
const dataTableOptions = {
|
||||
apiEndpoint: apiUrl,
|
||||
pageSize: 10,
|
||||
columns: {
|
||||
select: {
|
||||
render: (item, data, context) => {
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.className = 'checkbox checkbox-sm';
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.value = data.id.toString();
|
||||
checkbox.setAttribute('data-datatable-row-check', 'true');
|
||||
return checkbox.outerHTML.trim();
|
||||
},
|
||||
},
|
||||
id: {
|
||||
title: 'ID',
|
||||
},
|
||||
branch_name: {
|
||||
title: 'Branch',
|
||||
},
|
||||
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: {
|
||||
title: 'Period',
|
||||
render: (item, data) => {
|
||||
const monthNames = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'
|
||||
];
|
||||
|
||||
const formatPeriod = (period) => {
|
||||
if (!period) return '';
|
||||
const year = period.substring(0, 4);
|
||||
const month = parseInt(period.substring(4, 6));
|
||||
return `${monthNames[month - 1]} ${year}`;
|
||||
};
|
||||
|
||||
const fromPeriod = formatPeriod(data.period_from);
|
||||
const toPeriod = data.period_to ? ` - ${formatPeriod(data.period_to)}` : '';
|
||||
|
||||
return fromPeriod + toPeriod;
|
||||
},
|
||||
},
|
||||
is_available: {
|
||||
title: 'Available',
|
||||
render: (item, data) => {
|
||||
let statusClass = data.is_available ? 'badge badge-light-success' :
|
||||
|
||||
'badge badge-light-danger';
|
||||
let statusText = data.is_available ? 'Yes' : 'No';
|
||||
return `<span class="${statusClass}">${statusText}</span>`;
|
||||
},
|
||||
},
|
||||
is_generated: {
|
||||
title: 'Generated',
|
||||
render: (item, data) => {
|
||||
let statusClass = data.is_generated ? 'badge badge-light-success' :
|
||||
'badge badge-light-danger';
|
||||
let statusText = data.is_generated ? 'Yes' : 'No';
|
||||
return `<span class="${statusClass}">${statusText}</span>`;
|
||||
},
|
||||
},
|
||||
remarks: {
|
||||
title: 'Notes',
|
||||
},
|
||||
created_at: {
|
||||
title: 'Created At',
|
||||
render: (item, data) => {
|
||||
return data.created_at ?? '';
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
title: 'Actions',
|
||||
render: (item, data) => {
|
||||
let buttons = `<div class="flex flex-nowrap justify-center">
|
||||
<a class="btn btn-sm btn-icon btn-clear btn-info" href="statements/${data.id}">
|
||||
<i class="ki-outline ki-eye"></i>
|
||||
</a>`;
|
||||
|
||||
// Show download button if statement is approved and available but not downloaded
|
||||
//if (data.authorization_status === 'approved' && data.is_available && !data.is_downloaded) {
|
||||
if (data.is_available) {
|
||||
buttons += `<a class="btn btn-sm btn-icon btn-clear btn-success" href="statements/${data.id}/download">
|
||||
// Show download button if statement is approved and available but not downloaded
|
||||
//if (data.authorization_status === 'approved' && data.is_available && !data.is_downloaded) {
|
||||
if (data.is_available) {
|
||||
buttons += `<a class="btn btn-sm btn-icon btn-clear btn-success" href="statements/${data.id}/download">
|
||||
<i class="ki-outline ki-cloud-download"></i>
|
||||
</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Show send email button if email is not empty and statement is available
|
||||
if (data.is_available && data.email) {
|
||||
buttons += `<a class="btn btn-sm btn-icon btn-clear btn-primary" href="statements/${data.id}/send-email" title="Send to Email">
|
||||
// Show send email button if email is not empty and statement is available
|
||||
if (data.is_available && data.email) {
|
||||
buttons += `<a class="btn btn-sm btn-icon btn-clear btn-primary" href="statements/${data.id}/send-email" title="Send to Email">
|
||||
<i class="ki-outline ki-paper-plane"></i>
|
||||
</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Only show delete button if status is pending
|
||||
if (data.authorization_status === 'pending') {
|
||||
buttons += `<a onclick="deleteData(${data.id})" class="delete btn btn-sm btn-icon btn-clear btn-danger">
|
||||
// Only show delete button if status is pending
|
||||
if (data.authorization_status === 'pending') {
|
||||
buttons += `<a onclick="deleteData(${data.id})" class="delete btn btn-sm btn-icon btn-clear btn-danger">
|
||||
<i class="ki-outline ki-trash"></i>
|
||||
</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
buttons += `</div>`;
|
||||
return buttons;
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
buttons += `</div>`;
|
||||
return buttons;
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let dataTable = new KTDataTable(element, dataTableOptions);
|
||||
// Custom search functionality
|
||||
searchInput.addEventListener('input', function () {
|
||||
const searchValue = this.value.trim();
|
||||
dataTable.search(searchValue, true);
|
||||
});
|
||||
let dataTable = new KTDataTable(element, dataTableOptions);
|
||||
// Custom search functionality
|
||||
searchInput.addEventListener('input', function() {
|
||||
searchInput.addEventListener('input', function() {
|
||||
const searchValue = this.value.trim();
|
||||
dataTable.search(searchValue, true);
|
||||
});
|
||||
|
||||
// Handle the "select all" checkbox
|
||||
const selectAllCheckbox = document.querySelector('input[data-datatable-check="true"]');
|
||||
if (selectAllCheckbox) {
|
||||
selectAllCheckbox.addEventListener('change', function() {
|
||||
const isChecked = this.checked;
|
||||
const rowCheckboxes = document.querySelectorAll('input[data-datatable-row-check="true"]');
|
||||
// Handle the "select all" checkbox
|
||||
const selectAllCheckbox = document.querySelector('input[data-datatable-check="true"]');
|
||||
if (selectAllCheckbox) {
|
||||
selectAllCheckbox.addEventListener('change', function() {
|
||||
const isChecked = this.checked;
|
||||
const rowCheckboxes = document.querySelectorAll(
|
||||
'input[data-datatable-row-check="true"]');
|
||||
|
||||
rowCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = isChecked;
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
rowCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = isChecked;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Validate that end date is after start date
|
||||
const startDateInput = document.getElementById('start_date');
|
||||
const endDateInput = document.getElementById('end_date');
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Validate that end date is after start date
|
||||
const startDateInput = document.getElementById('start_date');
|
||||
const endDateInput = document.getElementById('end_date');
|
||||
|
||||
function validateDates() {
|
||||
const startDate = new Date(startDateInput.value);
|
||||
const endDate = new Date(endDateInput.value);
|
||||
function validateDates() {
|
||||
const startDate = new Date(startDateInput.value);
|
||||
const endDate = new Date(endDateInput.value);
|
||||
|
||||
if (startDate > endDate) {
|
||||
endDateInput.setCustomValidity('End date must be after start date');
|
||||
} else {
|
||||
endDateInput.setCustomValidity('');
|
||||
}
|
||||
}
|
||||
if (startDate > endDate) {
|
||||
endDateInput.setCustomValidity('End date must be after start date');
|
||||
} else {
|
||||
endDateInput.setCustomValidity('');
|
||||
}
|
||||
}
|
||||
|
||||
startDateInput.addEventListener('change', validateDates);
|
||||
endDateInput.addEventListener('change', validateDates);
|
||||
startDateInput.addEventListener('change', validateDates);
|
||||
endDateInput.addEventListener('change', validateDates);
|
||||
|
||||
// Set max date for date inputs to today
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
startDateInput.setAttribute('max', today);
|
||||
endDateInput.setAttribute('max', today);
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
// Set max date for date inputs to today
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
startDateInput.setAttribute('max', today);
|
||||
endDateInput.setAttribute('max', today);
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@@ -73,19 +73,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
<div class="text-gray-500 fw-semibold">Status</div>
|
||||
<div>
|
||||
@if($statement->authorization_status === 'pending')
|
||||
<span class="badge badge-warning">Pending Authorization</span>
|
||||
@elseif($statement->authorization_status === 'approved')
|
||||
<span class="badge badge-success">Approved</span>
|
||||
@elseif($statement->authorization_status === 'rejected')
|
||||
<span class="badge badge-danger">Rejected</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
<div class="text-gray-500 fw-semibold">Availability</div>
|
||||
<div>
|
||||
|
||||
569
resources/views/statements/stmt.blade.php
Normal file
569
resources/views/statements/stmt.blade.php
Normal file
@@ -0,0 +1,569 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Rekening Tabungan</title>
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #fff;
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 190mm;
|
||||
min-height: 277mm;
|
||||
margin: 10mm auto;
|
||||
background: #ffffff;
|
||||
border: 1px solid #ccc;
|
||||
padding: 10mm;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.watermark {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 1;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.watermark img {
|
||||
margin: 0px 50px;
|
||||
width: 85%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
/* Ensure content is above watermark */
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
padding-bottom: 10px;
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
.header .title {
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
color: #0056b3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.header .title h1 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header .logo {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.header .logo img {
|
||||
max-height: 50px;
|
||||
margin-right: 10px
|
||||
}
|
||||
|
||||
.info-section {
|
||||
text-transform: uppercase;
|
||||
padding: 10px 0;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.info-section .column {
|
||||
width: 48%;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.info-section .column p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
padding-top: 15px;
|
||||
flex: 1;
|
||||
/* Allow table section to grow */
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
table th,
|
||||
table td {
|
||||
padding: 5px;
|
||||
text-align: left;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
table th {
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
table td.text-right {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
table td.text-center {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
table td.text-left {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
margin-top: auto;
|
||||
position: relative;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
/* Ensure footer is above watermark */
|
||||
}
|
||||
|
||||
.footer p {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.footer .highlight {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.left-25 {
|
||||
margin-left: 25px !important;
|
||||
}
|
||||
|
||||
.same-size {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.sponsor {
|
||||
border-top: 1.5px solid black;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
margin: 10px 0px 0px 0px;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
tbody td {
|
||||
border-bottom: none;
|
||||
border-top: none;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* Menghilangkan padding dan margin untuk baris narrative tambahan */
|
||||
tbody tr td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
/* Khusus untuk baris narrative tambahan - tanpa padding/margin */
|
||||
tbody tr.narrative-line td {
|
||||
padding: 2px 5px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Column width classes */
|
||||
.col-date {
|
||||
width: 10%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.col-desc {
|
||||
width: 25%;
|
||||
min-width: 150px;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.col-valuta {
|
||||
width: 10%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.col-referensi {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.col-debet,
|
||||
.col-kredit {
|
||||
width: 12.5%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.col-saldo {
|
||||
width: 15%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.page-number {
|
||||
position: absolute;
|
||||
bottom: 5mm;
|
||||
right: 10mm;
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0;
|
||||
border: initial;
|
||||
border-radius: initial;
|
||||
width: initial;
|
||||
min-height: initial;
|
||||
box-shadow: initial;
|
||||
background: initial;
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
.container:last-of-type {
|
||||
page-break-after: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@php
|
||||
$saldo = $saldoAwalBulan->actual_balance ?? 0;
|
||||
$totalDebit = 0;
|
||||
$totalKredit = 0;
|
||||
$line = 1;
|
||||
$linePerPage = 26;
|
||||
@endphp
|
||||
@php
|
||||
// Hitung tanggal periode berdasarkan $period
|
||||
$periodDates = calculatePeriodDates($period);
|
||||
$startDate = $periodDates['start'];
|
||||
$endDate = $periodDates['end'];
|
||||
|
||||
// Log hasil perhitungan
|
||||
\Log::info('Period dates calculated', [
|
||||
'period' => $period,
|
||||
'start_date' => $startDate->format('d/m/Y'),
|
||||
'end_date' => $endDate->format('d/m/Y'),
|
||||
]);
|
||||
@endphp
|
||||
|
||||
@php
|
||||
// Calculate total pages based on actual line count
|
||||
$totalLines = 0;
|
||||
foreach ($stmtEntries as $entry) {
|
||||
// Split narrative into multiple lines of approximately 35 characters, breaking at word boundaries
|
||||
$narrative = $entry->description ?? '';
|
||||
$words = explode(' ', $narrative);
|
||||
|
||||
$narrativeLineCount = 0;
|
||||
$currentLine = '';
|
||||
|
||||
foreach ($words as $word) {
|
||||
if (strlen($currentLine . ' ' . $word) > 30) {
|
||||
$narrativeLineCount++;
|
||||
$currentLine = $word;
|
||||
} else {
|
||||
$currentLine .= ($currentLine ? ' ' : '') . $word;
|
||||
}
|
||||
}
|
||||
if ($currentLine) {
|
||||
$narrativeLineCount++;
|
||||
}
|
||||
|
||||
// Each entry takes at least one line for the main data + narrative lines + gap row
|
||||
$totalLines += $narrativeLineCount; // +1 for gap row
|
||||
}
|
||||
|
||||
// Add 1 for the "Saldo Awal Bulan" row
|
||||
$totalLines += 1;
|
||||
|
||||
// Calculate total pages ($linePerPage lines per page)
|
||||
$totalPages = ceil($totalLines / $linePerPage);
|
||||
$pageNumber = 0;
|
||||
|
||||
$footerContent =
|
||||
'
|
||||
<div class="footer">
|
||||
<p class="sponsor">Belanja puas di Electronic City! Dapatkan cashback hingga 250 ribu dan nikmati makan enak dengan cashback hingga 25 ribu. Bayar pakai QRIS AGI. S&K berlaku. Info lengkap: www.arthagraha.com</p>
|
||||
|
||||
<p class="sponsor">Waspada dalam bertransaksi QRIS! Periksa kembali identitas penjual & nominal pembayaran sebelum melanjutkan transaksi. Info terkait Bank Artha Graha Internasional, kunjungi website www.arthagraha.com</p>
|
||||
|
||||
<div class="highlight">
|
||||
<img src="' .
|
||||
public_path('assets/media/images/banner-footer.png') .
|
||||
'" alt="Logo Bank" style="width: 100%; height: auto;">
|
||||
</div>
|
||||
</div>
|
||||
';
|
||||
@endphp
|
||||
|
||||
<div class="container">
|
||||
<div class="watermark">
|
||||
<img src="{{ public_path('assets/media/images/watermark.png') }}" alt="Watermark">
|
||||
</div>
|
||||
<div class="content-wrapper">
|
||||
<!-- Header Section -->
|
||||
<div class="header">
|
||||
<div class="logo">
|
||||
<img src="{{ public_path('assets/media/images/logo-arthagraha.png') }}" alt="Logo Bank">
|
||||
<img src="{{ public_path('assets/media/images/logo-agi.png') }}" alt="Logo Bank">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bank Information Section -->
|
||||
<div class="info-section">
|
||||
<div class="column">
|
||||
<p>{{ $branch->name }}</p>
|
||||
<p style="text-transform: capitalize">Kepada</p>
|
||||
<p>{{ $account->customer->name }}</p>
|
||||
<p>{{ $account->customer->address }}</p>
|
||||
<p>{{ $account->customer->district }}</p>
|
||||
<p>{{ ($account->customer->city ? $account->customer->city . ' ' : '') . ($account->customer->province ? $account->customer->province . ' ' : '') . ($account->customer->postal_code ?? '') }}
|
||||
</p>
|
||||
</div>
|
||||
<div style="text-transform: capitalize;" class="column">
|
||||
<p style="padding-left:50px"><span class="same-size">Periode Statement </span>:
|
||||
{{ dateFormat($startDate) }} <span style="text-transform:lowercase !important">s/d</span>
|
||||
{{ dateFormat($endDate) }}</p>
|
||||
<p style="padding-left:50px"><span class="same-size">Nomor Rekening</span>:
|
||||
{{ $account->account_number }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table Section -->
|
||||
<div class="table-section">
|
||||
<table>
|
||||
<thead>
|
||||
<tr
|
||||
style="@if ($headerTableBg) background-image: url('data:image/png;base64,{{ $headerTableBg }}'); background-repeat: no-repeat; background-size: cover; background-position: center; @else background-color: #0056b3; @endif height: 30px;">
|
||||
<th class="col-date">Tanggal</th>
|
||||
<th class="col-valuta">Tanggal<br>Valuta</th>
|
||||
<th class="text-left col-desc">Keterangan</th>
|
||||
<th class="text-left col-referensi">Referensi</th>
|
||||
<th class="col-debet">Debet</th>
|
||||
<th class="col-kredit">Kredit</th>
|
||||
<th class="col-saldo">Saldo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center"></td>
|
||||
<td class="text-center"> </td>
|
||||
<td><strong>Saldo Awal Bulan</strong></td>
|
||||
<td class="text-center"> </td>
|
||||
<td class="text-right"> </td>
|
||||
<td class="text-right"> </td>
|
||||
<td class="text-right">
|
||||
<strong>{{ number_format($saldoAwalBulan->actual_balance, 2, ',', '.') }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@foreach ($stmtEntries as $row)
|
||||
@php
|
||||
$debit = $row->transaction_amount < 0 ? abs($row->transaction_amount) : 0;
|
||||
$kredit = $row->transaction_amount > 0 ? $row->transaction_amount : 0;
|
||||
$saldo += $kredit - $debit;
|
||||
$totalDebit += $debit;
|
||||
$totalKredit += $kredit;
|
||||
|
||||
// Split narrative into multiple lines of approximately 35 characters, breaking at word boundaries
|
||||
$narrative = $row->description ?? '';
|
||||
$words = explode(' ', $narrative);
|
||||
$narrativeLines = [];
|
||||
$currentLine = '';
|
||||
foreach ($words as $word) {
|
||||
if (strlen($currentLine . ' ' . $word) > 30) {
|
||||
$narrativeLines[] = trim($currentLine);
|
||||
$currentLine = $word;
|
||||
} else {
|
||||
$currentLine .= ($currentLine ? ' ' : '') . $word;
|
||||
}
|
||||
}
|
||||
if ($currentLine) {
|
||||
$narrativeLines[] = trim($currentLine);
|
||||
}
|
||||
|
||||
@endphp
|
||||
<tr>
|
||||
<td class="text-center">{{ date('d/m/Y', strtotime($row->transaction_date)) }}</td>
|
||||
<td class="text-center">{{ substr($row->actual_date, 0, 10) }}</td>
|
||||
<td>{{ str_replace(['[', ']'], ' ', $narrativeLines[0] ?? '') }}</td>
|
||||
<td>{{ $row->reference_number }}</td>
|
||||
<td class="text-right">{{ $debit > 0 ? number_format($debit, 2, ',', '.') : '' }}</td>
|
||||
<td class="text-right">{{ $kredit > 0 ? number_format($kredit, 2, ',', '.') : '' }}
|
||||
</td>
|
||||
<td class="text-right">{{ number_format($saldo, 2, ',', '.') }}</td>
|
||||
</tr>
|
||||
@for ($i = 1; $i < count($narrativeLines); $i++)
|
||||
<tr class="narrative-line">
|
||||
<td class="text-center"></td>
|
||||
<td class="text-center"></td>
|
||||
<td>{{ str_replace(['[', ']'], ' ', $narrativeLines[$i] ?? '') }}</td>
|
||||
<td class="text-center"></td>
|
||||
<td class="text-right"></td>
|
||||
<td class="text-right"></td>
|
||||
<td class="text-right"></td>
|
||||
</tr>
|
||||
@endfor
|
||||
@php $line += count($narrativeLines); @endphp
|
||||
<!-- Add a gap row -->
|
||||
<tr class="gap-row">
|
||||
<td class="text-center"></td>
|
||||
<td class="text-center"></td>
|
||||
<td class="text-center"></td>
|
||||
<td></td>
|
||||
<td class="text-right"></td>
|
||||
<td class="text-right"></td>
|
||||
<td class="text-right"></td>
|
||||
</tr>
|
||||
|
||||
@if ($line >= $linePerPage && !$loop->last)
|
||||
@php
|
||||
$line = 0;
|
||||
$pageNumber++;
|
||||
@endphp
|
||||
<tr>
|
||||
<td class="text-center"></td>
|
||||
<td class="text-center"></td>
|
||||
<td><strong>Pindah ke Halaman Berikutnya</strong></td>
|
||||
<td class="text-center"></td>
|
||||
<td class="text-right"> </td>
|
||||
<td class="text-right"> </td>
|
||||
<td class="text-right"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{!! $footerContent !!}
|
||||
</div>
|
||||
<div class="page-number">Halaman {{ $pageNumber }} dari {{ $totalPages }}</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="watermark">
|
||||
<img src="{{ public_path('assets/media/images/watermark.png') }}" alt="Watermark">
|
||||
</div>
|
||||
<div class="content-wrapper">
|
||||
<!-- Header Section for continuation page -->
|
||||
<div class="header">
|
||||
<div class="logo">
|
||||
<img src="{{ public_path('assets/media/images/logo-arthagraha.png') }}" alt="Logo Bank">
|
||||
<img src="{{ public_path('assets/media/images/logo-agi.png') }}" alt="Logo Bank">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bank Information Section for continuation page -->
|
||||
<div class="info-section">
|
||||
<div class="column">
|
||||
<p>{{ $branch->name }}</p>
|
||||
<p style="text-transform: capitalize">Kepada</p>
|
||||
<p>{{ $account->customer->name }}</p>
|
||||
<p>{{ $account->customer->address }}</p>
|
||||
<p>{{ $account->customer->district }}</p>
|
||||
<p>{{ ($account->customer->city ? $account->customer->city . ' ' : '') . ($account->customer->province ? $account->customer->province . ' ' : '') . ($account->customer->postal_code ?? '') }}
|
||||
</p>
|
||||
</div>
|
||||
<div style="text-transform: capitalize;" class="column">
|
||||
<p style="padding-left:50px"><span class="same-size">Periode Statement </span>:
|
||||
{{ dateFormat($startDate) }} <span style="text-transform:lowercase !important">s/d</span>
|
||||
{{ dateFormat($endDate) }}</p>
|
||||
<p style="padding-left:50px"><span class="same-size">Nomor Rekening</span>:
|
||||
{{ $account->account_number }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-section">
|
||||
<table>
|
||||
<thead>
|
||||
<tr
|
||||
style="@if ($headerTableBg) background-image: url('data:image/png;base64,{{ $headerTableBg }}'); background-repeat: no-repeat; background-size: cover; background-position: center; @else background-color: #0056b3; @endif height: 30px;">
|
||||
<th class="col-date">Tanggal</th>
|
||||
<th class="col-valuta">Tanggal<br>Valuta</th>
|
||||
<th class="text-left col-desc">Keterangan</th>
|
||||
<th class="col-referensi">Referensi</th>
|
||||
<th class="col-debet">Debet</th>
|
||||
<th class="col-kredit">Kredit</th>
|
||||
<th class="col-saldo">Saldo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@endif
|
||||
@endforeach
|
||||
@for ($i = 0; $i < $linePerPage - $line; $i++)
|
||||
<tr>
|
||||
<td class="text-center"></td>
|
||||
<td class="text-center"></td>
|
||||
<td></td>
|
||||
<td class="text-center"></td>
|
||||
<td class="text-right"></td>
|
||||
<td class="text-right"></td>
|
||||
<td class="text-right"></td>
|
||||
</tr>
|
||||
@endfor
|
||||
<tr>
|
||||
<td class="text-center"></td>
|
||||
<td class="text-center"></td>
|
||||
<td><strong>Total Akhir</strong></td>
|
||||
<td class="text-center"></td>
|
||||
<td class="text-right"><strong>{{ number_format($totalDebit, 2, ',', '.') }}</strong></td>
|
||||
<td class="text-right"><strong>{{ number_format($totalKredit, 2, ',', '.') }}</strong>
|
||||
</td>
|
||||
<td class="text-right"><strong>{{ number_format($saldo, 2, ',', '.') }}</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Footer Section -->
|
||||
{!! $footerContent !!}
|
||||
</div>
|
||||
<div class="page-number">Halaman {{ $pageNumber + 1 }} dari {{ $totalPages }}</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -91,7 +91,7 @@ Route::middleware(['auth'])->group(function () {
|
||||
});
|
||||
|
||||
Route::resource('statements', PrintStatementController::class);
|
||||
|
||||
|
||||
|
||||
// ATM Transaction Report Routes
|
||||
Route::group(['prefix' => 'atm-reports', 'as' => 'atm-reports.', 'middleware' => ['auth']], function () {
|
||||
@@ -112,13 +112,8 @@ Route::middleware(['auth'])->group(function () {
|
||||
Route::resource('email-statement-logs', EmailStatementLogController::class)->only(['index', 'show']);
|
||||
});
|
||||
|
||||
Route::get('migrasi', [MigrasiController::class, 'index'])->name('migrasi.index');
|
||||
Route::get('biaya-kartu', [SyncLogsController::class, 'index'])->name('biaya-kartu.index');
|
||||
|
||||
Route::get('/stmt-entries/{accountNumber}', [MigrasiController::class, 'getStmtEntryByAccount']);
|
||||
Route::get('/stmt-export-csv', [WebstatementController::class, 'index'])->name('webstatement.index');
|
||||
|
||||
|
||||
Route::prefix('debug')->group(function () {
|
||||
Route::get('/test-statement',[WebstatementController::class,'printStatementRekening'])->name('webstatement.test');
|
||||
Route::post('/statement', [DebugStatementController::class, 'debugStatement'])->name('debug.statement');
|
||||
|
||||
Reference in New Issue
Block a user