Compare commits
99 Commits
a687385017
...
new
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4616137e0c | ||
|
|
19c962307e | ||
|
|
a79b1bd99e | ||
|
|
fd5b8e1dad | ||
|
|
8fb16028d9 | ||
|
|
6035c61cc4 | ||
|
|
2c8f49af20 | ||
|
|
4bfd937490 | ||
|
|
7b32cb8d39 | ||
|
|
4b889da5a5 | ||
|
|
dbdeceb4c0 | ||
|
|
f7a92a5336 | ||
|
|
b717749450 | ||
|
|
e5b8dfc7c4 | ||
|
|
d5482fb824 | ||
|
|
f6df453ddc | ||
|
|
9199a4d748 | ||
|
|
f3c649572b | ||
|
|
55313fb0b0 | ||
|
|
8a7d4f351c | ||
|
|
f800c97a40 | ||
|
|
8fa4b2ea9e | ||
|
|
1f4d37370e | ||
|
|
49f90eef43 | ||
|
|
6eef6e89bf | ||
|
|
8d4accffaf | ||
|
|
903cbd1725 | ||
|
|
3720a24690 | ||
|
|
58a5db7303 | ||
|
|
47bf23302f | ||
|
|
d85954bdf2 | ||
|
|
db99465690 | ||
|
|
df5d0c420b | ||
|
|
836cdfc49d | ||
|
|
dcb6d43026 | ||
|
|
336ef8cf3a | ||
|
|
b71fc1b3f9 | ||
|
|
0d8a4c1ba4 | ||
|
|
e5f3a67374 | ||
|
|
701432a6e7 | ||
|
|
c1c7f03c87 | ||
|
|
d4efa58f1b | ||
|
|
f624a629f5 | ||
|
|
10fcdb5ea2 | ||
|
|
2d07783c46 | ||
|
|
66f84600eb | ||
|
|
cc99883875 | ||
|
|
6b8f44db1d | ||
|
|
d326cce6e0 | ||
|
|
173b229f07 | ||
|
|
1a1fecd0ad | ||
|
|
700c8bbbf6 | ||
|
|
8a728d6c6e | ||
|
|
8ca526e4f2 | ||
|
|
e14ae2ef9c | ||
|
|
7498d14087 | ||
|
|
369f24a8e2 | ||
|
|
d455707dbc | ||
|
|
76ebdce2ea | ||
|
|
eed6c3dbaa | ||
|
|
deb702c68e | ||
|
|
1ae98bcc26 | ||
|
|
cbba58cc50 | ||
|
|
c31f3c0d1f | ||
|
|
762b1457ba | ||
|
|
1b3e0ed30d | ||
|
|
0b607f86cb | ||
|
|
30662b97d5 | ||
|
|
cbfe2c4aa9 | ||
|
|
a8dafb23c5 | ||
|
|
60e60b4fef | ||
|
|
4c1163fa05 | ||
|
|
13344959c4 | ||
|
|
d6bd84c4e5 | ||
|
|
4321150d13 | ||
|
|
299ed0b018 | ||
|
|
9025663954 | ||
|
|
c6363473ac | ||
|
|
41ed7c1ed9 | ||
|
|
1e1120d29b | ||
|
|
3cb3eb449b | ||
|
|
6a7a3418b7 | ||
|
|
8abb8f6901 | ||
|
|
23611ef79b | ||
|
|
429df7035c | ||
|
|
e531193c06 | ||
|
|
38987ce8e3 | ||
|
|
cd447eb019 | ||
|
|
85b8bfa07b | ||
|
|
bf7206f927 | ||
|
|
562cc94822 | ||
|
|
b894a2c9c4 | ||
|
|
29ed72ad8b | ||
|
|
d6915aef1c | ||
|
|
d1962113ed | ||
|
|
566dd1e4e7 | ||
|
|
a3f2244fee | ||
|
|
2e2c8b4b0d | ||
|
|
e3b6e46d83 |
54
app/Console/CheckEmailProgressCommand.php
Normal file
54
app/Console/CheckEmailProgressCommand.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Console;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\Webstatement\Models\PrintStatementLog;
|
||||
|
||||
class CheckEmailProgressCommand extends Command
|
||||
{
|
||||
protected $signature = 'webstatement:check-progress {log-id : ID log untuk dicek progressnya}';
|
||||
protected $description = 'Cek progress pengiriman email statement';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$logId = $this->argument('log-id');
|
||||
|
||||
try {
|
||||
$log = PrintStatementLog::findOrFail($logId);
|
||||
|
||||
$this->info("📊 Progress Pengiriman Email Statement");
|
||||
$this->line("Log ID: {$log->id}");
|
||||
$this->line("Batch ID: {$log->batch_id}");
|
||||
$this->line("Request Type: {$log->request_type}");
|
||||
$this->line("Status: {$log->status}");
|
||||
|
||||
if ($log->total_accounts) {
|
||||
$this->line("Total Accounts: {$log->total_accounts}");
|
||||
$this->line("Processed: {$log->processed_accounts}");
|
||||
$this->line("Success: {$log->success_count}");
|
||||
$this->line("Failed: {$log->failed_count}");
|
||||
$this->line("Progress: {$log->getProgressPercentage()}%");
|
||||
$this->line("Success Rate: {$log->getSuccessRate()}%");
|
||||
}
|
||||
|
||||
if ($log->started_at) {
|
||||
$this->line("Started: {$log->started_at}");
|
||||
}
|
||||
|
||||
if ($log->completed_at) {
|
||||
$this->line("Completed: {$log->completed_at}");
|
||||
}
|
||||
|
||||
if ($log->error_message) {
|
||||
$this->error("Error: {$log->error_message}");
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error("Log dengan ID {$logId} tidak ditemukan.");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
48
app/Console/CombinePdf.php
Normal file
48
app/Console/CombinePdf.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Console;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\Webstatement\Http\Controllers\CombinePdfController;
|
||||
|
||||
class CombinePdf extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'webstatement:combine-pdf {--period= : Period to process migration format Ym contoh. 202506}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Process combine pdf';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Starting combine pdf process...');
|
||||
$period = $this->option('period');
|
||||
|
||||
try {
|
||||
$controller = app(CombinePdfController::class);
|
||||
$response = $controller->combinePdfs($period);
|
||||
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->info($responseData['message'] ?? 'Process completed');
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (Exception $e) {
|
||||
$this->error('Error processing combine pdf: ' . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
48
app/Console/ConvertHtmlToPdf.php
Normal file
48
app/Console/ConvertHtmlToPdf.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Console;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\Webstatement\Jobs\ConvertHtmlToPdfJob;
|
||||
use Exception;
|
||||
|
||||
class ConvertHtmlToPdf extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'webstatement:convert-html-to-pdf {directory}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Convert HTML files to PDF in the specified directory';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
$directory = $this->argument('directory');
|
||||
|
||||
$this->info('Starting HTML to PDF conversion process...');
|
||||
|
||||
// Dispatch the job
|
||||
ConvertHtmlToPdfJob::dispatch($directory);
|
||||
|
||||
$this->info('HTML to PDF conversion job has been queued.');
|
||||
|
||||
return 0;
|
||||
} catch (Exception $e) {
|
||||
$this->error('Error processing HTML to PDF conversion: ' . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
app/Console/ExportDailyStatements.php
Normal file
51
app/Console/ExportDailyStatements.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Console;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\Webstatement\Http\Controllers\WebstatementController;
|
||||
|
||||
class ExportDailyStatements extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'webstatement:export-statements';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Export daily statements for all configured client accounts';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Starting daily statement export process...');
|
||||
|
||||
try {
|
||||
$controller = app(WebstatementController::class);
|
||||
$response = $controller->index();
|
||||
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->info($responseData['message']);
|
||||
|
||||
// Display summary of jobs queued
|
||||
$jobCount = count($responseData['jobs'] ?? []);
|
||||
$this->info("Successfully queued {$jobCount} statement export jobs");
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (Exception $e) {
|
||||
$this->error('Error exporting statements: ' . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
57
app/Console/ExportPeriodStatements.php
Normal file
57
app/Console/ExportPeriodStatements.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Console;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\Webstatement\Http\Controllers\WebstatementController;
|
||||
|
||||
class ExportPeriodStatements extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'webstatement:export-period-statements
|
||||
{--account_number= : Account number to process migration}
|
||||
{--period= : Period to process migration format Ym contoh. 202506}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Export period statements for all configured client accounts';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$accountNumber = $this->option('account_number');
|
||||
$period = $this->option('period');
|
||||
|
||||
$this->info('Starting period statement export process...');
|
||||
|
||||
|
||||
try {
|
||||
$controller = app(WebstatementController::class);
|
||||
$response = $controller->printStatementRekening($accountNumber, $period);
|
||||
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->info($responseData['message']);
|
||||
|
||||
// Display summary of jobs queued
|
||||
$jobCount = count($responseData['jobs'] ?? []);
|
||||
$this->info("Successfully queued {$accountNumber} statement export jobs");
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (Exception $e) {
|
||||
$this->error('Error exporting statements: ' . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
59
app/Console/GenerateAtmTransactionReport.php
Normal file
59
app/Console/GenerateAtmTransactionReport.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Console;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\Webstatement\Jobs\GenerateAtmTransactionReportJob;
|
||||
|
||||
class GenerateAtmTransactionReport extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'webstatement:generate-atm-report {--period= : Period to generate report format Ymd, contoh: 20250512}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Generate ATM Transaction report for specified period';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Starting ATM Transaction report generation...');
|
||||
$period = $this->option('period');
|
||||
|
||||
if (!$period) {
|
||||
$this->error('Period parameter is required. Format: Ymd (example: 20250512)');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// Validate period format
|
||||
if (!preg_match('/^\d{8}$/', $period)) {
|
||||
$this->error('Invalid period format. Use Ymd format (example: 20250512)');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
// Dispatch the job
|
||||
GenerateAtmTransactionReportJob::dispatch($period);
|
||||
|
||||
$this->info("ATM Transaction report generation job queued for period: {$period}");
|
||||
$this->info('The report will be generated in the background.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (Exception $e) {
|
||||
$this->error('Error queuing ATM Transaction report job: ' . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
app/Console/ProcessDailyMigration.php
Normal file
51
app/Console/ProcessDailyMigration.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Console;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\Webstatement\Http\Controllers\MigrasiController;
|
||||
|
||||
class ProcessDailyMigration extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'webstatement:process-daily-migration
|
||||
{--process_parameter= : To process migration parameter true/false}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Process data migration for the previous day\'s period';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$processParameter = $this->option('process_parameter');
|
||||
|
||||
$this->info('Starting daily data migration process...');
|
||||
$this->info('Process Parameter: ' . ($processParameter ?? 'False'));
|
||||
|
||||
try {
|
||||
$controller = app(MigrasiController::class);
|
||||
$response = $controller->index($processParameter);
|
||||
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->info($responseData['message'] ?? 'Process completed');
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (Exception $e) {
|
||||
$this->error('Error processing daily migration: ' . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
249
app/Console/SendStatementEmailCommand.php
Normal file
249
app/Console/SendStatementEmailCommand.php
Normal file
@@ -0,0 +1,249 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Console;
|
||||
|
||||
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
|
||||
{period : Format periode YYYYMM (contoh: 202401)}
|
||||
{--type=single : Tipe pengiriman: single, branch, all}
|
||||
{--account= : Nomor rekening (untuk type=single)}
|
||||
{--branch= : Kode cabang (untuk type=branch)}
|
||||
{--batch-id= : ID batch untuk tracking (opsional)}
|
||||
{--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)';
|
||||
|
||||
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');
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
49
app/Console/UnlockPdf.php
Normal file
49
app/Console/UnlockPdf.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Console;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\Webstatement\Jobs\UnlockPdfJob;
|
||||
use Exception;
|
||||
|
||||
class UnlockPdf extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'webstatement:unlock-pdf {directory} {--password=123456 : Password for PDF files}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Unlock password-protected PDF files in the specified directory';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
$directory = $this->argument('directory');
|
||||
$password = $this->option('password');
|
||||
|
||||
$this->info('Starting PDF unlock process...');
|
||||
|
||||
// Dispatch the job
|
||||
UnlockPdfJob::dispatch($directory, $password);
|
||||
|
||||
$this->info('PDF unlock job has been queued.');
|
||||
|
||||
return 0;
|
||||
} catch (Exception $e) {
|
||||
$this->error('Error processing PDF unlock: ' . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
366
app/Http/Controllers/AtmTransactionReportController.php
Normal file
366
app/Http/Controllers/AtmTransactionReportController.php
Normal file
@@ -0,0 +1,366 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Log;
|
||||
use Modules\Webstatement\Jobs\GenerateAtmTransactionReportJob;
|
||||
use Modules\Webstatement\Models\AtmTransactionReportLog;
|
||||
|
||||
class AtmTransactionReportController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the ATM transaction reports.
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
return view('webstatement::atm-reports.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created ATM transaction report request.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'report_date' => ['required', 'date_format:Y-m-d'],
|
||||
]);
|
||||
|
||||
// Convert date to Ymd format for period
|
||||
$period = Carbon::createFromFormat('Y-m-d', $validated['report_date'])->format('Ymd');
|
||||
|
||||
// Add user tracking data
|
||||
$reportData = [
|
||||
'period' => $period,
|
||||
'report_date' => $validated['report_date'],
|
||||
'user_id' => Auth::id(),
|
||||
'created_by' => Auth::id(),
|
||||
'ip_address' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
'status' => 'pending',
|
||||
];
|
||||
|
||||
// Create the report request log
|
||||
$reportRequest = AtmTransactionReportLog::create($reportData);
|
||||
|
||||
// Dispatch the job to generate the report
|
||||
try {
|
||||
GenerateAtmTransactionReportJob::dispatch($period, $reportRequest->id);
|
||||
|
||||
$reportRequest->update([
|
||||
'status' => 'processing',
|
||||
'updated_by' => Auth::id()
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$reportRequest->update([
|
||||
'status' => 'failed',
|
||||
'error_message' => $e->getMessage(),
|
||||
'updated_by' => Auth::id()
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('atm-reports.index')
|
||||
->with('success', 'ATM Transaction report request has been created successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new report request.
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
return view('webstatement::atm-reports.create', compact('branches'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified report request.
|
||||
*/
|
||||
public function show(AtmTransactionReportLog $atmReport)
|
||||
{
|
||||
$atmReport->load(['user', 'creator', 'authorizer']);
|
||||
return view('webstatement::atm-reports.show', compact('atmReport'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the report if available.
|
||||
*/
|
||||
public function download(AtmTransactionReportLog $atmReport)
|
||||
{
|
||||
// Check if report is available
|
||||
if ($atmReport->status !== 'completed' || !$atmReport->file_path) {
|
||||
return back()->with('error', 'Report is not available for download.');
|
||||
}
|
||||
|
||||
// Update download status
|
||||
$atmReport->update([
|
||||
'is_downloaded' => true,
|
||||
'downloaded_at' => now(),
|
||||
'updated_by' => Auth::id()
|
||||
]);
|
||||
|
||||
// Download the file
|
||||
$filePath = $atmReport->file_path;
|
||||
if (Storage::exists($filePath)) {
|
||||
$fileName = "atm_transaction_report_{$atmReport->period}.csv";
|
||||
return Storage::download($filePath, $fileName);
|
||||
}
|
||||
|
||||
return back()->with('error', 'Report file not found.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorize a report request.
|
||||
*/
|
||||
public function authorize(Request $request, AtmTransactionReportLog $atmReport)
|
||||
{
|
||||
$request->validate([
|
||||
'authorization_status' => ['required', Rule::in(['approved', 'rejected'])],
|
||||
'remarks' => ['nullable', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
// Update authorization status
|
||||
$atmReport->update([
|
||||
'authorization_status' => $request->authorization_status,
|
||||
'authorized_by' => Auth::id(),
|
||||
'authorized_at' => now(),
|
||||
'remarks' => $request->remarks,
|
||||
'updated_by' => Auth::id()
|
||||
]);
|
||||
|
||||
$statusText = $request->authorization_status === 'approved' ? 'approved' : 'rejected';
|
||||
|
||||
return redirect()->route('atm-reports.show', $atmReport->id)
|
||||
->with('success', "ATM Transaction report request has been {$statusText} successfully.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide data for datatables.
|
||||
*/
|
||||
public function dataForDatatables(Request $request)
|
||||
{
|
||||
// Retrieve data from the database
|
||||
$query = AtmTransactionReportLog::query();
|
||||
|
||||
// Apply search filter if provided
|
||||
if ($request->has('search') && !empty($request->get('search'))) {
|
||||
$search = $request->get('search');
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('period', 'LIKE', "%$search%")
|
||||
->orWhere('status', 'LIKE', "%$search%")
|
||||
->orWhere('authorization_status', 'LIKE', "%$search%");
|
||||
});
|
||||
}
|
||||
|
||||
// Apply column filters if provided
|
||||
if ($request->has('filters') && !empty($request->get('filters'))) {
|
||||
$filters = json_decode($request->get('filters'), true);
|
||||
|
||||
foreach ($filters as $filter) {
|
||||
if (!empty($filter['value'])) {
|
||||
if ($filter['column'] === 'status') {
|
||||
$query->where('status', $filter['value']);
|
||||
} else if ($filter['column'] === 'authorization_status') {
|
||||
$query->where('authorization_status', $filter['value']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply sorting if provided
|
||||
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
|
||||
$order = $request->get('sortOrder');
|
||||
$column = $request->get('sortField');
|
||||
|
||||
// Map frontend column names to database column names if needed
|
||||
$columnMap = [
|
||||
'period' => 'period',
|
||||
'status' => 'status',
|
||||
];
|
||||
|
||||
$dbColumn = $columnMap[$column] ?? $column;
|
||||
$query->orderBy($dbColumn, $order);
|
||||
} else {
|
||||
// Default sorting
|
||||
$query->latest('created_at');
|
||||
}
|
||||
|
||||
// Get the total count of records
|
||||
$totalRecords = $query->count();
|
||||
|
||||
// Apply pagination if provided
|
||||
if ($request->has('page') && $request->has('size')) {
|
||||
$page = $request->get('page');
|
||||
$size = $request->get('size');
|
||||
$offset = ($page - 1) * $size;
|
||||
|
||||
$query->skip($offset)->take($size);
|
||||
}
|
||||
|
||||
// Get the filtered count of records
|
||||
$filteredRecords = $query->count();
|
||||
|
||||
// Eager load relationships (remove branch since it's not used anymore)
|
||||
$query->with(['user', 'authorizer']);
|
||||
|
||||
// Get the data for the current page
|
||||
$data = $query->get()->map(function ($item) {
|
||||
$processingHours = $item->status === 'processing' ? $item->updated_at->diffInHours(now()) : 0;
|
||||
$isProcessingTimeout = $item->status === 'processing' && $processingHours >= 1;
|
||||
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'period' => $item->period,
|
||||
'report_date' => Carbon::createFromFormat('Ymd', $item->period)->format('Y-m-d'),
|
||||
'status' => $item->status,
|
||||
'status_display' => $item->status . ($isProcessingTimeout ? ' (Timeout)' : ''),
|
||||
'processing_hours' => $processingHours,
|
||||
'is_processing_timeout' => $isProcessingTimeout,
|
||||
'authorization_status' => $item->authorization_status,
|
||||
'is_downloaded' => $item->is_downloaded,
|
||||
'created_at' => dateFormat($item->created_at, 1, 1),
|
||||
'created_by' => $item->user->name ?? 'N/A',
|
||||
'authorized_by' => $item->authorizer ? $item->authorizer->name : null,
|
||||
'authorized_at' => $item->authorized_at ? $item->authorized_at->format('Y-m-d H:i:s') : null,
|
||||
'file_path' => $item->file_path,
|
||||
'can_retry' => in_array($item->status, ['failed', 'pending']) || $isProcessingTimeout || ($item->status === 'completed' && !$item->file_path),
|
||||
];
|
||||
});
|
||||
|
||||
// Calculate the page count
|
||||
$pageCount = ceil($filteredRecords / ($request->get('size') ?: 1));
|
||||
$currentPage = $request->get('page') ?: 1;
|
||||
|
||||
return response()->json([
|
||||
'draw' => $request->get('draw'),
|
||||
'recordsTotal' => $totalRecords,
|
||||
'recordsFiltered' => $filteredRecords,
|
||||
'pageCount' => $pageCount,
|
||||
'page' => $currentPage,
|
||||
'totalCount' => $totalRecords,
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a report request.
|
||||
*/
|
||||
public function destroy(AtmTransactionReportLog $atmReport)
|
||||
{
|
||||
// Delete the file if exists
|
||||
if ($atmReport->file_path && Storage::exists($atmReport->file_path)) {
|
||||
Storage::delete($atmReport->file_path);
|
||||
}
|
||||
|
||||
// Delete the report request
|
||||
$atmReport->delete();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'ATM Transaction report deleted successfully.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send report to email
|
||||
*/
|
||||
public function sendEmail($id)
|
||||
{
|
||||
$atmReport = AtmTransactionReportLog::findOrFail($id);
|
||||
|
||||
// Check if report has email
|
||||
if (empty($atmReport->email)) {
|
||||
return redirect()->back()->with('error', 'No email address provided for this report.');
|
||||
}
|
||||
|
||||
// Check if report is available
|
||||
if ($atmReport->status !== 'completed' || !$atmReport->file_path) {
|
||||
return redirect()->back()->with('error', 'Report is not available for sending.');
|
||||
}
|
||||
|
||||
try {
|
||||
// Send email with report attachment
|
||||
// Implementation depends on your email system
|
||||
// Mail::to($atmReport->email)->send(new AtmTransactionReportEmail($atmReport));
|
||||
|
||||
$atmReport->update([
|
||||
'email_sent' => true,
|
||||
'email_sent_at' => now(),
|
||||
'updated_by' => Auth::id()
|
||||
]);
|
||||
|
||||
return redirect()->back()->with('success', 'ATM Transaction report sent to email successfully.');
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to send ATM Transaction report email: ' . $e->getMessage());
|
||||
return redirect()->back()->with('error', 'Failed to send email: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry generating the ATM transaction report
|
||||
*/
|
||||
public function retry(AtmTransactionReportLog $atmReport)
|
||||
{
|
||||
// Check if retry is allowed (failed, pending, or processing for more than 1 hour)
|
||||
$allowedStatuses = ['failed', 'pending'];
|
||||
$isProcessingTooLong = $atmReport->status === 'processing' &&
|
||||
$atmReport->updated_at->diffInHours(now()) >= 1;
|
||||
|
||||
if (!in_array($atmReport->status, $allowedStatuses) && !$isProcessingTooLong) {
|
||||
return back()->with('error', 'Report can only be retried if status is failed, pending, or processing for more than 1 hour.');
|
||||
}
|
||||
|
||||
try {
|
||||
// If it was processing for too long, mark it as failed first
|
||||
if ($isProcessingTooLong) {
|
||||
$atmReport->update([
|
||||
'status' => 'failed',
|
||||
'error_message' => 'Processing timeout - exceeded 1 hour limit',
|
||||
'updated_by' => Auth::id()
|
||||
]);
|
||||
}
|
||||
|
||||
// Reset the report status and clear previous data
|
||||
$atmReport->update([
|
||||
'status' => 'processing',
|
||||
'error_message' => null,
|
||||
'file_path' => null,
|
||||
'file_size' => null,
|
||||
'record_count' => null,
|
||||
'updated_by' => Auth::id()
|
||||
]);
|
||||
|
||||
// Dispatch the job again
|
||||
GenerateAtmTransactionReportJob::dispatch($atmReport->period, $atmReport->id);
|
||||
|
||||
return back()->with('success', 'ATM Transaction report job has been retried successfully.');
|
||||
|
||||
} catch (Exception $e) {
|
||||
$atmReport->update([
|
||||
'status' => 'failed',
|
||||
'error_message' => $e->getMessage(),
|
||||
'updated_by' => Auth::id()
|
||||
]);
|
||||
|
||||
return back()->with('error', 'Failed to retry report generation: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if report can be retried
|
||||
*/
|
||||
public function canRetry(AtmTransactionReportLog $atmReport)
|
||||
{
|
||||
$allowedStatuses = ['failed', 'pending'];
|
||||
$isProcessingTooLong = $atmReport->status === 'processing' &&
|
||||
$atmReport->updated_at->diffInHours(now()) >= 1;
|
||||
|
||||
return in_array($atmReport->status, $allowedStatuses) ||
|
||||
$isProcessingTooLong ||
|
||||
($atmReport->status === 'completed' && !$atmReport->file_path);
|
||||
}
|
||||
}
|
||||
215
app/Http/Controllers/CombinePdfController.php
Normal file
215
app/Http/Controllers/CombinePdfController.php
Normal file
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\Webstatement\Jobs\CombinePdfJob;
|
||||
use Modules\Webstatement\Models\Account;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class CombinePdfController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return view('webstatement::index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine PDF files from r14 and r23 folders for all accounts
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function combinePdfs($period)
|
||||
{
|
||||
// Configuration: Set r23 file source - 'local' or 'sftp'
|
||||
$file_r23 = 'local'; // Change this to 'sftp' to use SFTP for r23 files
|
||||
|
||||
// Configuration: Set output destination - 'local' or 'sftp'
|
||||
$output_destination = 'local'; // Change this to 'sftp' to upload combined PDFs to SFTP
|
||||
|
||||
// Get period from request or use current period
|
||||
$period = $period ?? date('Ym');
|
||||
|
||||
// Get all accounts with customer relation
|
||||
$accounts = Account::where('branch_code','ID0010052')->get();
|
||||
$processedCount = 0;
|
||||
$skippedCount = 0;
|
||||
$errorCount = 0;
|
||||
|
||||
foreach ($accounts as $account) {
|
||||
$branchCode = $account->branch_code;
|
||||
$accountNumber = $account->account_number;
|
||||
|
||||
// Define file paths
|
||||
$r14Path = storage_path("app/r14/{$accountNumber}_{$period}.pdf");
|
||||
|
||||
// Define temporary path for r23 files downloaded from SFTP
|
||||
$tempDir = storage_path("app/temp/{$period}");
|
||||
if (!File::exists($tempDir)) {
|
||||
File::makeDirectory($tempDir, 0755, true);
|
||||
}
|
||||
|
||||
$outputDir = storage_path("app/combine/{$period}/{$branchCode}");
|
||||
$outputFilename = "{$accountNumber}_{$period}.pdf";
|
||||
|
||||
// Check if r14 file exists locally
|
||||
$r14Exists = File::exists($r14Path);
|
||||
|
||||
// Check for multiple r23 files based on configuration
|
||||
$r23Files = [];
|
||||
$r23Exists = false;
|
||||
|
||||
if ($file_r23 === 'local') {
|
||||
// Use local r23 files - check for multiple files
|
||||
$r23Pattern = storage_path("app/r23/{$accountNumber}.*.pdf");
|
||||
$foundR23Files = glob($r23Pattern);
|
||||
|
||||
if (!empty($foundR23Files)) {
|
||||
// Sort files numerically by their sequence number
|
||||
usort($foundR23Files, function($a, $b) {
|
||||
preg_match('/\.(\d+)\.pdf$/', $a, $matchesA);
|
||||
preg_match('/\.(\d+)\.pdf$/', $b, $matchesB);
|
||||
return (int)$matchesA[1] - (int)$matchesB[1];
|
||||
});
|
||||
|
||||
$r23Files = $foundR23Files;
|
||||
$r23Exists = true;
|
||||
Log::info("Found " . count($r23Files) . " r23 files locally for account {$accountNumber}");
|
||||
}
|
||||
} elseif ($file_r23 === 'sftp') {
|
||||
// Use SFTP r23 files - check for multiple files
|
||||
try {
|
||||
$sftpFiles = Storage::disk('sftpStatement')->files('r23');
|
||||
$accountR23Files = array_filter($sftpFiles, function($file) use ($accountNumber) {
|
||||
return preg_match("/r23\/{$accountNumber}\.(\d+)\.pdf$/", $file);
|
||||
});
|
||||
|
||||
if (!empty($accountR23Files)) {
|
||||
// Sort files numerically by their sequence number
|
||||
usort($accountR23Files, function($a, $b) {
|
||||
preg_match('/\.(\d+)\.pdf$/', $a, $matchesA);
|
||||
preg_match('/\.(\d+)\.pdf$/', $b, $matchesB);
|
||||
return (int)$matchesA[1] - (int)$matchesB[1];
|
||||
});
|
||||
|
||||
// Download all r23 files
|
||||
foreach ($accountR23Files as $index => $sftpFile) {
|
||||
$r23Content = Storage::disk('sftpStatement')->get($sftpFile);
|
||||
$tempFileName = "{$tempDir}/{$accountNumber}_r23_" . ($index + 1) . ".pdf";
|
||||
File::put($tempFileName, $r23Content);
|
||||
$r23Files[] = $tempFileName;
|
||||
}
|
||||
|
||||
$r23Exists = true;
|
||||
Log::info("Downloaded " . count($r23Files) . " r23 files for account {$accountNumber} from SFTP");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error downloading r23 files from SFTP for account {$accountNumber}: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
// Skip if neither file exists
|
||||
if (!$r14Exists && !$r23Exists) {
|
||||
//Log::warning("No PDF files found for account {$accountNumber}");
|
||||
$skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prepare file list for processing
|
||||
$pdfFiles = [];
|
||||
if ($r14Exists) {
|
||||
$pdfFiles[] = $r14Path;
|
||||
}
|
||||
if ($r23Exists) {
|
||||
// Add all r23 files to the list
|
||||
$pdfFiles = array_merge($pdfFiles, $r23Files);
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate password based on customer relation data
|
||||
$password = $this->generatePassword($account);
|
||||
|
||||
// Dispatch job to combine PDFs or apply password protection
|
||||
CombinePdfJob::dispatch($pdfFiles, $outputDir, $outputFilename, $password, $output_destination, $branchCode, $period);
|
||||
$processedCount++;
|
||||
|
||||
Log::info("Queued PDF processing for account {$accountNumber} - r14: local, r23: {$file_r23}, output: {$output_destination}, password: {$password}");
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error processing PDF for account {$accountNumber}: {$e->getMessage()}");
|
||||
$errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
Log::info("Processed {$processedCount} accounts, skipped {$skippedCount} accounts, and encountered {$errorCount} errors.");
|
||||
|
||||
return response()->json([
|
||||
'message' => "PDF combination process has been queued (r14: local, r23: {$file_r23}, output: {$output_destination})",
|
||||
'processed' => $processedCount,
|
||||
'skipped' => $skippedCount,
|
||||
'errors' => $errorCount,
|
||||
'period' => $period
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate password based on customer relation data
|
||||
* Format: date+end 2 digit account_number
|
||||
* Example: 05Oct202585
|
||||
*
|
||||
* @param Account $account
|
||||
* @return string
|
||||
*/
|
||||
private function generatePassword(Account $account)
|
||||
{
|
||||
$customer = $account->customer;
|
||||
$accountNumber = $account->account_number;
|
||||
|
||||
// Get last 2 digits of account number
|
||||
$lastTwoDigits = substr($accountNumber, -2);
|
||||
|
||||
// Determine which date to use based on sector
|
||||
$dateToUse = null;
|
||||
|
||||
if ($customer && $customer->sector) {
|
||||
$firstDigitSector = substr($customer->sector, 0, 1);
|
||||
|
||||
if ($firstDigitSector === '1') {
|
||||
// Use date_of_birth if available, otherwise birth_incorp_date
|
||||
$dateToUse = $customer->date_of_birth ?: $customer->birth_incorp_date;
|
||||
} else {
|
||||
// Use birth_incorp_date for sector > 1
|
||||
$dateToUse = $customer->birth_incorp_date;
|
||||
}
|
||||
}
|
||||
|
||||
// If no date found, fallback to account number
|
||||
if (!$dateToUse) {
|
||||
Log::warning("No date found for account {$accountNumber}, using account number as password");
|
||||
return $accountNumber;
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse the date and format it
|
||||
$date = Carbon::parse($dateToUse);
|
||||
$day = $date->format('d');
|
||||
$month = $date->format('M'); // 3-letter month abbreviation
|
||||
$year = $date->format('Y');
|
||||
|
||||
// Format: ddMmmyyyyXX (e.g., 05Oct202585)
|
||||
$password = $day . $month . $year . $lastTwoDigits;
|
||||
|
||||
return $password;
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error parsing date for account {$accountNumber}: {$e->getMessage()}");
|
||||
return $accountNumber; // Fallback to account number
|
||||
}
|
||||
}
|
||||
}
|
||||
358
app/Http/Controllers/DebugStatementController.php
Normal file
358
app/Http/Controllers/DebugStatementController.php
Normal file
@@ -0,0 +1,358 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\Webstatement\Models\StmtEntry;
|
||||
use Modules\Webstatement\Models\TempStmtNarrFormat;
|
||||
use Modules\Webstatement\Models\TempStmtNarrParam;
|
||||
|
||||
class DebugStatementController extends Controller
|
||||
{
|
||||
/**
|
||||
* Debug a single statement entry
|
||||
*/
|
||||
public function debugStatement(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'account_number' => 'required|string',
|
||||
'trans_reference' => 'required|string',
|
||||
'period' => 'nullable|string'
|
||||
]);
|
||||
|
||||
try {
|
||||
// Find the statement entry
|
||||
$query = StmtEntry::with(['ft', 'transaction'])
|
||||
->where('account_number', $request->account_number)
|
||||
->where('trans_reference', $request->trans_reference);
|
||||
|
||||
if ($request->period) {
|
||||
$query->where('booking_date', $request->period);
|
||||
}
|
||||
|
||||
$item = $query->first();
|
||||
|
||||
if (!$item) {
|
||||
return response()->json([
|
||||
'error' => 'Statement entry not found',
|
||||
'criteria' => [
|
||||
'account_number' => $request->account_number,
|
||||
'trans_reference' => $request->trans_reference,
|
||||
'period' => $request->period
|
||||
]
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Generate narrative using the same method from ExportStatementJob
|
||||
$narrative = $this->generateNarrative($item);
|
||||
|
||||
// Format dates
|
||||
$transactionDate = $this->formatTransactionDate($item);
|
||||
$actualDate = $this->formatActualDate($item);
|
||||
|
||||
return response()->json([
|
||||
'statement_entry' => [
|
||||
'account_number' => $item->account_number,
|
||||
'trans_reference' => $item->trans_reference,
|
||||
'booking_date' => $item->booking_date,
|
||||
'amount_lcy' => $item->amount_lcy,
|
||||
'narrative' => $item->narrative,
|
||||
'date_time' => $item->date_time
|
||||
],
|
||||
'generated_narrative' => $narrative,
|
||||
'formatted_dates' => [
|
||||
'transaction_date' => $transactionDate,
|
||||
'actual_date' => $actualDate
|
||||
],
|
||||
'related_data' => [
|
||||
'ft' => $item->ft,
|
||||
'transaction' => $item->transaction
|
||||
],
|
||||
'debug_info' => $this->getDebugInfo($item)
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Debug statement error: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'error' => 'An error occurred while debugging the statement',
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List available statement entries for debugging
|
||||
*/
|
||||
public function listStatements(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'account_number' => 'required|string',
|
||||
'period' => 'nullable|string',
|
||||
'limit' => 'nullable|integer|min:1|max:100'
|
||||
]);
|
||||
|
||||
$query = StmtEntry::where('account_number', $request->account_number);
|
||||
|
||||
if ($request->period) {
|
||||
$query->where('booking_date', $request->period);
|
||||
}
|
||||
|
||||
$statements = $query->orderBy('date_time', 'desc')
|
||||
->limit($request->limit ?? 20)
|
||||
->get(['account_number', 'trans_reference', 'booking_date', 'amount_lcy', 'narrative', 'date_time']);
|
||||
|
||||
return response()->json([
|
||||
'statements' => $statements,
|
||||
'count' => $statements->count()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate narrative for a statement entry (copied from ExportStatementJob)
|
||||
*/
|
||||
private 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 based on narrative type (copied from ExportStatementJob)
|
||||
*/
|
||||
private 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format transaction date (copied from ExportStatementJob)
|
||||
*/
|
||||
private function formatTransactionDate($item)
|
||||
{
|
||||
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',
|
||||
$item->booking_date . substr($datetime, 6, 4)
|
||||
)->format('d/m/Y H:i');
|
||||
} catch (Exception $e) {
|
||||
Log::warning("Error formatting transaction date: " . $e->getMessage());
|
||||
return Carbon::now()->format('d/m/Y H:i');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format actual date (copied from ExportStatementJob)
|
||||
*/
|
||||
private function formatActualDate($item)
|
||||
{
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get debug information about the statement entry
|
||||
*/
|
||||
private function getDebugInfo($item)
|
||||
{
|
||||
$prefix = substr($item->trans_reference ?? '', 0, 2);
|
||||
|
||||
$debugInfo = [
|
||||
'transaction_prefix' => $prefix,
|
||||
'has_transaction' => !is_null($item->transaction),
|
||||
'has_ft' => !is_null($item->ft),
|
||||
'narrative_components' => []
|
||||
];
|
||||
|
||||
if ($item->transaction) {
|
||||
$debugInfo['transaction_data'] = [
|
||||
'stmt_narr' => $item->transaction->stmt_narr,
|
||||
'narr_type' => $item->transaction->narr_type
|
||||
];
|
||||
|
||||
if ($item->transaction->narr_type) {
|
||||
$narrParam = TempStmtNarrParam::where('_id', $item->transaction->narr_type)->first();
|
||||
$debugInfo['narr_param'] = $narrParam;
|
||||
|
||||
if ($narrParam) {
|
||||
$fmt = $this->getNarrativeFormat($narrParam->_id);
|
||||
$narrFormat = TempStmtNarrFormat::where('_id', $fmt)->first();
|
||||
$debugInfo['narr_format'] = $narrFormat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $debugInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get narrative format mapping
|
||||
*/
|
||||
private function getNarrativeFormat($narrId)
|
||||
{
|
||||
$mapping = [
|
||||
'FTIN' => 'FT.IN',
|
||||
'FTOUT' => 'FT.OUT',
|
||||
'TTTRFOUT' => 'TT.O.TRF',
|
||||
'TTTRFIN' => 'TT.I.TRF',
|
||||
'APITRX' => 'API.TSEL',
|
||||
'ONUSCR' => 'ONUS.CR',
|
||||
'ONUSDR' => 'ONUS.DR'
|
||||
];
|
||||
|
||||
return $mapping[$narrId] ?? $narrId;
|
||||
}
|
||||
}
|
||||
297
app/Http/Controllers/EmailStatementLogController.php
Normal file
297
app/Http/Controllers/EmailStatementLogController.php
Normal file
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\Webstatement\Models\PrintStatementLog;
|
||||
use Modules\Basicdata\Models\Branch;
|
||||
use Modules\Webstatement\Jobs\SendStatementEmailJob;
|
||||
|
||||
/**
|
||||
* Controller untuk mengelola log pengiriman statement email
|
||||
* Mendukung log untuk pengiriman per rekening, per cabang, atau seluruh cabang
|
||||
*/
|
||||
class EmailStatementLogController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
Log::info('Accessing email statement log index page', [
|
||||
'user_id' => auth()->id(),
|
||||
'ip_address' => $request->ip()
|
||||
]);
|
||||
|
||||
try {
|
||||
$branches = Branch::orderBy('name')->get();
|
||||
return view('webstatement::email-statement-logs.index', compact('branches'));
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to load email statement log index page', [
|
||||
'error' => $e->getMessage(),
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
return back()->with('error', 'Gagal memuat halaman log pengiriman email statement.');
|
||||
}
|
||||
}
|
||||
|
||||
public function dataForDatatables(Request $request)
|
||||
{
|
||||
Log::info('Fetching email statement log data for datatables', [
|
||||
'user_id' => auth()->id(),
|
||||
'filters' => $request->only(['branch_code', 'account_number', 'period_from', 'period_to', 'request_type', 'status'])
|
||||
]);
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
$query = PrintStatementLog::query()
|
||||
->with(['user', 'branch'])
|
||||
->select([
|
||||
'id',
|
||||
'user_id',
|
||||
'branch_code',
|
||||
'account_number',
|
||||
'request_type',
|
||||
'batch_id',
|
||||
'total_accounts',
|
||||
'processed_accounts',
|
||||
'success_count',
|
||||
'failed_count',
|
||||
'status',
|
||||
'period_from',
|
||||
'period_to',
|
||||
'email',
|
||||
'email_sent_at',
|
||||
'is_available',
|
||||
'authorization_status',
|
||||
'started_at',
|
||||
'completed_at',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
]);
|
||||
|
||||
// Filter berdasarkan branch
|
||||
if ($request->filled('branch_code')) {
|
||||
$query->where('branch_code', $request->branch_code);
|
||||
}
|
||||
|
||||
// Filter berdasarkan account number (hanya untuk single account)
|
||||
if ($request->filled('account_number')) {
|
||||
$query->where('account_number', 'like', '%' . $request->account_number . '%');
|
||||
}
|
||||
|
||||
// Filter berdasarkan request type
|
||||
if ($request->filled('request_type')) {
|
||||
$query->where('request_type', $request->request_type);
|
||||
}
|
||||
|
||||
// Filter berdasarkan status
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
|
||||
// Filter berdasarkan periode
|
||||
if ($request->filled('period_from')) {
|
||||
$query->where('period_from', '>=', $request->period_from);
|
||||
}
|
||||
|
||||
if ($request->filled('period_to')) {
|
||||
$query->where('period_to', '<=', $request->period_to);
|
||||
}
|
||||
|
||||
// Filter berdasarkan tanggal
|
||||
if ($request->filled('date_from')) {
|
||||
$query->whereDate('created_at', '>=', $request->date_from);
|
||||
}
|
||||
|
||||
if ($request->filled('date_to')) {
|
||||
$query->whereDate('created_at', '<=', $request->date_to);
|
||||
}
|
||||
|
||||
$query->orderBy('created_at', 'desc');
|
||||
|
||||
$totalRecords = $query->count();
|
||||
|
||||
if ($request->filled('start')) {
|
||||
$query->skip($request->start);
|
||||
}
|
||||
|
||||
if ($request->filled('length') && $request->length != -1) {
|
||||
$query->take($request->length);
|
||||
}
|
||||
|
||||
$logs = $query->get();
|
||||
|
||||
$data = $logs->map(function ($log) {
|
||||
return [
|
||||
'id' => $log->id,
|
||||
'request_type' => $this->formatRequestType($log->request_type),
|
||||
'branch_code' => $log->branch_code,
|
||||
'branch_name' => $log->branch->name ?? 'N/A',
|
||||
'account_number' => $log->account_number ?? '-',
|
||||
'period_display' => $log->period_display,
|
||||
'batch_id' => $log->batch_id,
|
||||
'total_accounts' => $log->total_accounts ?? 1,
|
||||
'processed_accounts' => $log->processed_accounts ?? 0,
|
||||
'success_count' => $log->success_count ?? 0,
|
||||
'failed_count' => $log->failed_count ?? 0,
|
||||
'progress_percentage' => $log->getProgressPercentage(),
|
||||
'success_rate' => $log->getSuccessRate(),
|
||||
'status' => $this->formatStatus($log->status),
|
||||
'email' => $log->email,
|
||||
'email_status' => $log->email_sent_at ? 'Terkirim' : 'Pending',
|
||||
'email_sent_at' => $log->email_sent_at ?? '-',
|
||||
'authorization_status' => ucfirst($log->authorization_status),
|
||||
'user_name' => $log->user->name ?? 'System',
|
||||
'started_at' => $log->started_at ? $log->started_at->format('d/m/Y H:i:s') : '-',
|
||||
'completed_at' => $log->completed_at ? $log->completed_at->format('d/m/Y H:i:s') : '-',
|
||||
'created_at' => $log->created_at->format('d/m/Y H:i:s'),
|
||||
'actions' => $this->generateActionButtons($log)
|
||||
];
|
||||
});
|
||||
|
||||
DB::commit();
|
||||
|
||||
return response()->json([
|
||||
'draw' => intval($request->draw),
|
||||
'recordsTotal' => $totalRecords,
|
||||
'recordsFiltered' => $totalRecords,
|
||||
'data' => $data
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
Log::error('Failed to fetch email statement log data', [
|
||||
'error' => $e->getMessage(),
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'draw' => intval($request->draw),
|
||||
'recordsTotal' => 0,
|
||||
'recordsFiltered' => 0,
|
||||
'data' => [],
|
||||
'error' => 'Gagal memuat data log pengiriman email statement.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
try {
|
||||
$log = PrintStatementLog::with(['user', 'branch'])->findOrFail($id);
|
||||
return view('webstatement::email-statement-logs.show', compact('log'));
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to load email statement log detail', [
|
||||
'log_id' => $id,
|
||||
'error' => $e->getMessage(),
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
return back()->with('error', 'Log pengiriman email statement tidak ditemukan.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mengirim ulang email statement untuk batch atau single account
|
||||
*/
|
||||
public function resendEmail(Request $request, $id)
|
||||
{
|
||||
Log::info('Attempting to resend statement email', [
|
||||
'log_id' => $id,
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
$log = PrintStatementLog::findOrFail($id);
|
||||
|
||||
// Buat batch ID baru untuk resend
|
||||
$newBatchId = 'resend_' . time() . '_' . $log->id;
|
||||
|
||||
// Dispatch job dengan parameter yang sama
|
||||
SendStatementEmailJob::dispatch(
|
||||
$log->period_from,
|
||||
$log->request_type,
|
||||
$log->request_type === 'single_account' ? $log->account_number :
|
||||
($log->request_type === 'branch' ? $log->branch_code : null),
|
||||
$newBatchId,
|
||||
$log->id
|
||||
);
|
||||
|
||||
// Reset status untuk tracking ulang
|
||||
$log->update([
|
||||
'status' => 'pending',
|
||||
'batch_id' => $newBatchId,
|
||||
'processed_accounts' => 0,
|
||||
'success_count' => 0,
|
||||
'failed_count' => 0,
|
||||
'started_at' => null,
|
||||
'completed_at' => null,
|
||||
'error_message' => null
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
Log::info('Statement email resend job dispatched successfully', [
|
||||
'log_id' => $id,
|
||||
'new_batch_id' => $newBatchId,
|
||||
'request_type' => $log->request_type
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Email statement berhasil dijadwalkan untuk dikirim ulang.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
Log::error('Failed to resend statement email', [
|
||||
'log_id' => $id,
|
||||
'error' => $e->getMessage(),
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
return back()->with('error', 'Gagal mengirim ulang email statement.');
|
||||
}
|
||||
}
|
||||
|
||||
private function formatRequestType($requestType)
|
||||
{
|
||||
$types = [
|
||||
'single_account' => 'Single Account',
|
||||
'branch' => 'Per Cabang',
|
||||
'all_branches' => 'Seluruh Cabang'
|
||||
];
|
||||
|
||||
return $types[$requestType] ?? $requestType;
|
||||
}
|
||||
|
||||
private function formatStatus($status)
|
||||
{
|
||||
$statuses = [
|
||||
'pending' => '<span class="badge badge-warning">Pending</span>',
|
||||
'processing' => '<span class="badge badge-info">Processing</span>',
|
||||
'completed' => '<span class="badge badge-success">Completed</span>',
|
||||
'failed' => '<span class="badge badge-danger">Failed</span>'
|
||||
];
|
||||
|
||||
return $statuses[$status] ?? $status;
|
||||
}
|
||||
|
||||
private function generateActionButtons(PrintStatementLog $log)
|
||||
{
|
||||
$buttons = [];
|
||||
|
||||
// Tombol view detail
|
||||
$buttons[] = '<a href="' . route('email-statement-logs.show', $log->id) . '" class="btn btn-sm btn-icon btn-clear btn-light" title="Lihat Detail">' .
|
||||
'<i class="text-base text-gray-500 ki-filled ki-eye"></i>' .
|
||||
'</a>';
|
||||
|
||||
// Tombol resend email
|
||||
if (in_array($log->status, ['completed', 'failed']) && $log->authorization_status === 'approved') {
|
||||
$buttons[] = '<button onclick="resendEmail(' . $log->id . ')" class="btn btn-sm btn-icon btn-clear btn-light" title="Kirim Ulang Email">' .
|
||||
'<i class="text-base ki-filled ki-message-text-2 text-primary"></i>' .
|
||||
'</button>';
|
||||
}
|
||||
|
||||
return implode(' ', $buttons);
|
||||
}
|
||||
}
|
||||
@@ -1,236 +1,123 @@
|
||||
<?php
|
||||
namespace Modules\Webstatement\Http\Controllers;
|
||||
|
||||
namespace Modules\Webstatement\Http\Controllers;
|
||||
use App\Http\Controllers\Controller;
|
||||
use BadMethodCallException;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Log;
|
||||
use Modules\Webstatement\Jobs\{ProcessAccountDataJob,
|
||||
ProcessArrangementDataJob,
|
||||
ProcessAtmTransactionJob,
|
||||
ProcessBillDetailDataJob,
|
||||
ProcessCategoryDataJob,
|
||||
ProcessCompanyDataJob,
|
||||
ProcessCustomerDataJob,
|
||||
ProcessDataCaptureDataJob,
|
||||
ProcessFtTxnTypeConditionJob,
|
||||
ProcessFundsTransferDataJob,
|
||||
ProcessStmtEntryDataJob,
|
||||
ProcessStmtNarrFormatDataJob,
|
||||
ProcessStmtNarrParamDataJob,
|
||||
ProcessTellerDataJob,
|
||||
ProcessTransactionDataJob,
|
||||
ProcessSectorDataJob};
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Log;
|
||||
use Modules\Webstatement\Jobs\ProcessAccountDataJob;
|
||||
use Modules\Webstatement\Jobs\ProcessArrangementDataJob;
|
||||
use Modules\Webstatement\Jobs\ProcessAtmTransactionJob;
|
||||
use Modules\Webstatement\Jobs\ProcessBillDetailDataJob;
|
||||
use Modules\Webstatement\Jobs\ProcessCategoryDataJob;
|
||||
use Modules\Webstatement\Jobs\ProcessCompanyDataJob;
|
||||
use Modules\Webstatement\Jobs\ProcessCustomerDataJob;
|
||||
use Modules\Webstatement\Jobs\ProcessDataCaptureDataJob;
|
||||
use Modules\Webstatement\Jobs\ProcessFtTxnTypeConditionJob;
|
||||
use Modules\Webstatement\Jobs\ProcessFundsTransferDataJob;
|
||||
use Modules\Webstatement\Jobs\ProcessStmtEntryDataJob;
|
||||
use Modules\Webstatement\Jobs\ProcessStmtNarrFormatDataJob;
|
||||
use Modules\Webstatement\Jobs\ProcessStmtNarrParamDataJob;
|
||||
use Modules\Webstatement\Jobs\ProcessTellerDataJob;
|
||||
use Modules\Webstatement\Jobs\ProcessTransactionDataJob;
|
||||
|
||||
class MigrasiController extends Controller
|
||||
{
|
||||
|
||||
public function processArrangementData($periods)
|
||||
class MigrasiController extends Controller
|
||||
{
|
||||
try {
|
||||
ProcessArrangementDataJob::dispatch($periods);
|
||||
return response()->json(['message' => 'Data Arrangement processing job has been successfully']);
|
||||
} catch (Exception $e) {
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function processCustomerData($periods)
|
||||
{
|
||||
try {
|
||||
// Pass the periods to the job for processing
|
||||
ProcessCustomerDataJob::dispatch($periods);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Data Customer processing job has been successfully queued',
|
||||
'periods' => $periods
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in processCustomerData: ' . $e->getMessage());
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function processBillDetailData($periods)
|
||||
{
|
||||
try {
|
||||
ProcessBillDetailDataJob::dispatch($periods);
|
||||
return response()->json(['message' => 'Data Bill Details processing job has been successfully']);
|
||||
} catch (Exception $e) {
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function processAccountData($periods){
|
||||
try{
|
||||
ProcessAccountDataJob::dispatch($periods);
|
||||
return response()->json(['message' => 'Data Account processing job has been successfully']);
|
||||
} catch (Exception $e) {
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function processTransactionData($periods){
|
||||
try{
|
||||
ProcessTransactionDataJob::dispatch($periods);
|
||||
Log::info('Data Transaction processing job has been successfully');
|
||||
return response()->json(['message' => 'Data Transaction processing job has been successfully']);
|
||||
} catch (Exception $e) {
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function processFundsTransferData($periods){
|
||||
try{
|
||||
ProcessFundsTransferDataJob::dispatch($periods);
|
||||
return response()->json(['message' => 'Data Funds Transfer processing job has been successfully']);
|
||||
} catch (Exception $e) {
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function processStmtNarrParamData($periods)
|
||||
{
|
||||
try {
|
||||
ProcessStmtNarrParamDataJob::dispatch($periods);
|
||||
return response()->json(['message' => 'Data TempStmtNarrParam processing job has been successfully']);
|
||||
} catch (Exception $e) {
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function processStmtNarrFormatData($periods){
|
||||
try {
|
||||
ProcessStmtNarrFormatDataJob::dispatch($periods);
|
||||
return response()->json(['message' => 'Data TempStmtNarrFormat processing job has been successfully']);
|
||||
} catch (Exception $e) {
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function ProcessFtTxnTypeConditioData($periods){
|
||||
try {
|
||||
ProcessFtTxnTypeConditionJob::dispatch($periods);
|
||||
return response()->json(['message' => 'FtTxnTypeCondition processing job has been successfully']);
|
||||
} catch (Exception $e) {
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function processStmtEntryData($periods){
|
||||
try {
|
||||
ProcessStmtEntryDataJob::dispatch($periods);
|
||||
return response()->json(['message' => 'Stmt Entry processing job has been successfully']);
|
||||
} catch (Exception $e) {
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function ProcessCompanyData($periods){
|
||||
try {
|
||||
ProcessCompanyDataJob::dispatch($periods);
|
||||
return response()->json(['message' => 'Company processing job has been successfully']);
|
||||
} catch (Exception $e) {
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function ProcessDataCaptureData($periods){
|
||||
try {
|
||||
ProcessDataCaptureDataJob::dispatch($periods);
|
||||
return response()->json(['message' => 'Data Capture processing job has been successfully']);
|
||||
} catch (Exception $e) {
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function ProcessCategoryData($periods){
|
||||
try {
|
||||
ProcessCategoryDataJob::dispatch($periods);
|
||||
return response()->json(['message' => 'Category processing job has been successfully']);
|
||||
} catch (Exception $e) {
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function ProcessTellerData($periods){
|
||||
try {
|
||||
ProcessTellerDataJob::dispatch($periods);
|
||||
return response()->json(['message' => 'Teller processing job has been successfully']);
|
||||
} catch (Exception $e) {
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function ProcessAtmTransaction($periods){
|
||||
try {
|
||||
ProcessAtmTransactionJob::dispatch($periods);
|
||||
return response()->json(['message' => 'AtmTransaction processing job has been successfully']);
|
||||
} catch (Exception $e) {
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function index()
|
||||
{
|
||||
//$disk = Storage::disk('sftpStatement');
|
||||
|
||||
// Get all directories (periods) in the SFTP disk
|
||||
//$allDirectories = $disk->directories();
|
||||
|
||||
//$this->processTransactionData(['_parameter']);
|
||||
//$this->processStmtNarrParamData(['_parameter']);
|
||||
//$this->processStmtNarrFormatData(['_parameter']);
|
||||
//$this->ProcessFtTxnTypeConditioData(['_parameter']);
|
||||
|
||||
// Filter out the _parameter folder
|
||||
/*$periods = array_filter($allDirectories, function($dir) {
|
||||
return $dir !== '_parameter';
|
||||
});*/
|
||||
|
||||
$periods = [
|
||||
/*'20250510',
|
||||
'20250512',
|
||||
'20250513',
|
||||
'20250514',
|
||||
'20250515',
|
||||
'20250516',
|
||||
'20250517',
|
||||
'20250518',
|
||||
'20250519',*/
|
||||
'20250520',
|
||||
'20250521',
|
||||
'20250522'
|
||||
private const PROCESS_TYPES = [
|
||||
'transaction' => ProcessTransactionDataJob::class,
|
||||
'stmtNarrParam' => ProcessStmtNarrParamDataJob::class,
|
||||
'stmtNarrFormat' => ProcessStmtNarrFormatDataJob::class,
|
||||
'ftTxnTypeCondition' => ProcessFtTxnTypeConditionJob::class,
|
||||
'category' => ProcessCategoryDataJob::class,
|
||||
'company' => ProcessCompanyDataJob::class,
|
||||
'customer' => ProcessCustomerDataJob::class,
|
||||
'account' => ProcessAccountDataJob::class,
|
||||
'stmtEntry' => ProcessStmtEntryDataJob::class,
|
||||
'dataCapture' => ProcessDataCaptureDataJob::class,
|
||||
'fundsTransfer' => ProcessFundsTransferDataJob::class,
|
||||
'teller' => ProcessTellerDataJob::class,
|
||||
'atmTransaction' => ProcessAtmTransactionJob::class,
|
||||
'arrangement' => ProcessArrangementDataJob::class,
|
||||
'billDetail' => ProcessBillDetailDataJob::class,
|
||||
'sector' => ProcessSectorDataJob::class
|
||||
];
|
||||
|
||||
// Sort periods by date (descending)
|
||||
usort($periods, function($a, $b) {
|
||||
return strcmp($b, $a); // Reverse comparison for descending order
|
||||
});
|
||||
private const PARAMETER_PROCESSES = [
|
||||
'transaction',
|
||||
'stmtNarrParam',
|
||||
'stmtNarrFormat',
|
||||
'ftTxnTypeCondition',
|
||||
'sector'
|
||||
];
|
||||
|
||||
if (empty($periods)) {
|
||||
return response()->json(['message' => 'No valid period folders found in SFTP storage'], 404);
|
||||
private const DATA_PROCESSES = [
|
||||
'category',
|
||||
'company',
|
||||
'customer',
|
||||
'account',
|
||||
'stmtEntry',
|
||||
'dataCapture',
|
||||
'fundsTransfer',
|
||||
'teller',
|
||||
'atmTransaction',
|
||||
'arrangement',
|
||||
'billDetail'
|
||||
];
|
||||
|
||||
public function __call($method, $parameters)
|
||||
{
|
||||
if (strpos($method, 'process') === 0) {
|
||||
$type = lcfirst(substr($method, 7));
|
||||
if (isset(self::PROCESS_TYPES[$type])) {
|
||||
return $this->processData($type, $parameters[0] ?? '');
|
||||
}
|
||||
}
|
||||
throw new BadMethodCallException("Method {$method} does not exist.");
|
||||
}
|
||||
|
||||
foreach($periods as $period){
|
||||
$this->ProcessCategoryData([$period]);
|
||||
$this->ProcessCompanyData([$period]);
|
||||
private function processData(string $type, string $period)
|
||||
: JsonResponse
|
||||
{
|
||||
try {
|
||||
$jobClass = self::PROCESS_TYPES[$type];
|
||||
$jobClass::dispatch($period);
|
||||
|
||||
$this->processCustomerData([$period]);
|
||||
$this->processAccountData([$period]);
|
||||
$message = sprintf('%s data processing job has been queued successfully', ucfirst($type));
|
||||
Log::info($message);
|
||||
|
||||
$this->processStmtEntryData([$period]);
|
||||
$this->ProcessDataCaptureData([$period]);
|
||||
$this->processFundsTransferData([$period]);
|
||||
$this->ProcessTellerData([$period]);
|
||||
$this->ProcessAtmTransaction([$period]);
|
||||
|
||||
$this->processArrangementData([$period]);
|
||||
$this->processBillDetailData([$period]);
|
||||
return response()->json(['message' => $message]);
|
||||
} catch (Exception $e) {
|
||||
Log::error(sprintf('Error in %s processing: %s', $type, $e->getMessage()));
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
public function index($processParameter = false)
|
||||
{
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
|
||||
return response()->json(['message' => 'Data processing job has been successfully']);
|
||||
if ($processParameter) {
|
||||
foreach (self::PARAMETER_PROCESSES as $process) {
|
||||
$this->processData($process, '_parameter');
|
||||
}
|
||||
return response()->json(['message' => 'Parameter processes completed successfully']);
|
||||
}
|
||||
|
||||
$period = date('Ymd', strtotime('-1 day'));
|
||||
if (!$disk->exists($period)) {
|
||||
return response()->json([
|
||||
"message" => "Period {$period} folder not found in SFTP storage"
|
||||
], 404);
|
||||
}
|
||||
|
||||
foreach (self::DATA_PROCESSES as $process) {
|
||||
$this->processData($process, $period);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => "Data processing for period {$period} has been queued successfully"
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Log;
|
||||
use Modules\Basicdata\Models\Branch;
|
||||
use Modules\Webstatement\Http\Requests\PrintStatementRequest;
|
||||
use Modules\Webstatement\Mail\StatementEmail;
|
||||
use Modules\Webstatement\Models\Account;
|
||||
use Modules\Webstatement\Models\PrintStatementLog;
|
||||
use ZipArchive;
|
||||
|
||||
@@ -23,32 +25,108 @@
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$branches = Branch::orderBy('name')->get();
|
||||
$branches = Branch::whereNotNull('customer_company')
|
||||
->where('code', '!=', 'ID0019999')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('webstatement::statements.index', compact('branches'));
|
||||
$branch = Branch::find(Auth::user()->branch_id);
|
||||
$multiBranch = session('MULTI_BRANCH') ?? false;
|
||||
|
||||
return view('webstatement::statements.index', compact('branches', 'branch', 'multiBranch'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created statement request.
|
||||
* Menangani pembuatan request statement baru dengan logging dan transaksi database
|
||||
*/
|
||||
public function store(PrintStatementRequest $request)
|
||||
{
|
||||
$validated = $request->validated();
|
||||
// Add account verification before storing
|
||||
$accountNumber = $request->input('account_number'); // Assuming this is the field name for account number
|
||||
// First, check if the account exists and get branch information
|
||||
$account = Account::where('account_number', $accountNumber)->first();
|
||||
if ($account) {
|
||||
$branch_code = $account->branch_code;
|
||||
$userBranchId = session('branch_id'); // Assuming branch ID is stored in session
|
||||
$multiBranch = session('MULTI_BRANCH');
|
||||
|
||||
// Add user tracking data
|
||||
$validated['user_id'] = Auth::id();
|
||||
$validated['created_by'] = Auth::id();
|
||||
$validated['ip_address'] = $request->ip();
|
||||
$validated['user_agent'] = $request->userAgent();
|
||||
if (!$multiBranch) {
|
||||
// Check if account branch matches user's branch
|
||||
if ($account->branch_id !== $userBranchId) {
|
||||
return redirect()->route('statements.index')
|
||||
->with('error', 'Nomor rekening tidak sesuai dengan cabang Anda. Transaksi tidak dapat dilanjutkan.');
|
||||
}
|
||||
}
|
||||
|
||||
// Create the statement log
|
||||
$statement = PrintStatementLog::create($validated);
|
||||
// Check if account belongs to restricted branch ID0019999
|
||||
if ($account->branch_id === 'ID0019999') {
|
||||
return redirect()->route('statements.index')
|
||||
->with('error', 'Nomor rekening terdaftar pada cabang khusus. Silakan hubungi bagian HC untuk informasi lebih lanjut.');
|
||||
}
|
||||
|
||||
// Process statement availability check (this would be implemented based on your system)
|
||||
$this->checkStatementAvailability($statement);
|
||||
// If all checks pass, proceed with storing data
|
||||
// Your existing store logic here
|
||||
|
||||
return redirect()->route('statements.index')
|
||||
->with('success', 'Statement request has been created successfully.');
|
||||
} else {
|
||||
// Account not found
|
||||
return redirect()->route('statements.index')
|
||||
->with('error', 'Nomor rekening tidak ditemukan dalam sistem.');
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
$validated = $request->validated();
|
||||
|
||||
// Add user tracking data dan field baru untuk single account request
|
||||
$validated['user_id'] = Auth::id();
|
||||
$validated['created_by'] = Auth::id();
|
||||
$validated['ip_address'] = $request->ip();
|
||||
$validated['user_agent'] = $request->userAgent();
|
||||
$validated['request_type'] = 'single_account'; // Default untuk request manual
|
||||
$validated['status'] = 'pending'; // Status awal
|
||||
$validated['authorization_status'] = 'approved'; // Status otorisasi awal
|
||||
$validated['total_accounts'] = 1; // Untuk single account
|
||||
$validated['processed_accounts'] = 0;
|
||||
$validated['success_count'] = 0;
|
||||
$validated['failed_count'] = 0;
|
||||
$validated['branch_code'] = $branch_code; // Awal tidak tersedia
|
||||
|
||||
// Create the statement log
|
||||
$statement = PrintStatementLog::create($validated);
|
||||
|
||||
// Log aktivitas
|
||||
Log::info('Statement request created', [
|
||||
'statement_id' => $statement->id,
|
||||
'user_id' => Auth::id(),
|
||||
'account_number' => $statement->account_number,
|
||||
'request_type' => $statement->request_type
|
||||
]);
|
||||
|
||||
// Process statement availability check
|
||||
$this->checkStatementAvailability($statement);
|
||||
|
||||
$statement = PrintStatementLog::find($statement->id);
|
||||
if($statement->email){
|
||||
$this->sendEmail($statement->id);
|
||||
}
|
||||
DB::commit();
|
||||
|
||||
return redirect()->route('statements.index')
|
||||
->with('success', 'Statement request has been created successfully.');
|
||||
} catch (Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
Log::error('Failed to create statement request', [
|
||||
'error' => $e->getMessage(),
|
||||
'user_id' => Auth::id()
|
||||
]);
|
||||
|
||||
return redirect()->back()
|
||||
->withInput()
|
||||
->with('error', 'Failed to create statement request: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,68 +140,113 @@
|
||||
|
||||
/**
|
||||
* Check if the statement is available in the system.
|
||||
* This is a placeholder method - implement according to your system.
|
||||
* Memperbarui status availability dengan logging
|
||||
*/
|
||||
protected function checkStatementAvailability(PrintStatementLog $statement)
|
||||
{
|
||||
// This would be implemented based on your system's logic
|
||||
// For example, checking an API or database for statement availability
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
DB::beginTransaction();
|
||||
|
||||
//format folder /periode/Print/branch_code/account_number.pdf
|
||||
$filePath = "{$statement->period_from}/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
|
||||
try {
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
$filePath = "{$statement->period_from}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf";
|
||||
|
||||
// Check if the statement exists in the storage
|
||||
if ($statement->is_period_range && $statement->period_to) {
|
||||
$periodFrom = Carbon::createFromFormat('Ym', $statement->period_from);
|
||||
$periodTo = Carbon::createFromFormat('Ym', $statement->period_to);
|
||||
|
||||
// Loop through each month in the range
|
||||
$missingPeriods = [];
|
||||
$availablePeriods = [];
|
||||
|
||||
for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) {
|
||||
$periodFormatted = $period->format('Ym');
|
||||
$periodPath = $periodFormatted . "/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
|
||||
|
||||
if ($disk->exists($periodPath)) {
|
||||
$availablePeriods[] = $periodFormatted;
|
||||
} else {
|
||||
$missingPeriods[] = $periodFormatted;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If any period is missing, the statement is not available
|
||||
if (count($missingPeriods) > 0) {
|
||||
$notes = "Missing periods: " . implode(', ', $missingPeriods);
|
||||
$statement->update([
|
||||
'is_available' => false,
|
||||
'remarks' => $notes,
|
||||
'updated_by' => Auth::id()
|
||||
]);
|
||||
return;
|
||||
} else {
|
||||
// All periods are available
|
||||
$statement->update([
|
||||
'is_available' => true,
|
||||
'updated_by' => Auth::id()
|
||||
]);
|
||||
return;
|
||||
}
|
||||
} else if ($disk->exists($filePath)) {
|
||||
$statement->update([
|
||||
'is_available' => true,
|
||||
'updated_by' => Auth::id()
|
||||
// Log untuk debugging
|
||||
Log::info('Checking SFTP file path', [
|
||||
'file_path' => $filePath,
|
||||
'sftp_root' => config('filesystems.disks.sftpStatement.root'),
|
||||
'full_path' => config('filesystems.disks.sftpStatement.root') . '/' . $filePath
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$statement->update([
|
||||
'is_available' => false,
|
||||
'updated_by' => Auth::id()
|
||||
]);
|
||||
return;
|
||||
if ($statement->is_period_range && $statement->period_to) {
|
||||
$periodFrom = Carbon::createFromFormat('Ym', $statement->period_from);
|
||||
$periodTo = Carbon::createFromFormat('Ym', $statement->period_to);
|
||||
|
||||
$missingPeriods = [];
|
||||
$availablePeriods = [];
|
||||
|
||||
for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) {
|
||||
$periodFormatted = $period->format('Ym');
|
||||
$periodPath = $periodFormatted . "/{$statement->branch_code}/{$statement->account_number}_{$periodFormatted}.pdf";
|
||||
|
||||
if ($disk->exists($periodPath)) {
|
||||
$availablePeriods[] = $periodFormatted;
|
||||
} else {
|
||||
$missingPeriods[] = $periodFormatted;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($missingPeriods) > 0) {
|
||||
$notes = "Missing periods: " . implode(', ', $missingPeriods);
|
||||
$statement->update([
|
||||
'is_available' => false,
|
||||
'remarks' => $notes,
|
||||
'updated_by' => Auth::id(),
|
||||
'status' => 'failed'
|
||||
]);
|
||||
|
||||
Log::warning('Statement not available - missing periods', [
|
||||
'statement_id' => $statement->id,
|
||||
'missing_periods' => $missingPeriods
|
||||
]);
|
||||
} else {
|
||||
$statement->update([
|
||||
'is_available' => true,
|
||||
'updated_by' => Auth::id(),
|
||||
'status' => 'completed',
|
||||
'processed_accounts' => 1,
|
||||
'success_count' => 1
|
||||
]);
|
||||
|
||||
Log::info('Statement available - all periods found', [
|
||||
'statement_id' => $statement->id,
|
||||
'available_periods' => $availablePeriods
|
||||
]);
|
||||
}
|
||||
} else if ($disk->exists($filePath)) {
|
||||
$statement->update([
|
||||
'is_available' => true,
|
||||
'updated_by' => Auth::id(),
|
||||
'status' => 'completed',
|
||||
'processed_accounts' => 1,
|
||||
'success_count' => 1
|
||||
]);
|
||||
|
||||
Log::info('Statement available', [
|
||||
'statement_id' => $statement->id,
|
||||
'file_path' => $filePath
|
||||
]);
|
||||
} else {
|
||||
$statement->update([
|
||||
'is_available' => false,
|
||||
'updated_by' => Auth::id(),
|
||||
'status' => 'failed',
|
||||
'processed_accounts' => 1,
|
||||
'failed_count' => 1,
|
||||
'error_message' => 'Statement file not found'
|
||||
]);
|
||||
|
||||
Log::warning('Statement not available', [
|
||||
'statement_id' => $statement->id,
|
||||
'file_path' => $filePath
|
||||
]);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
} catch (Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
Log::error('Error checking statement availability', [
|
||||
'statement_id' => $statement->id,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
$statement->update([
|
||||
'is_available' => false,
|
||||
'status' => 'failed',
|
||||
'error_message' => $e->getMessage(),
|
||||
'updated_by' => Auth::id()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,94 +260,179 @@
|
||||
|
||||
/**
|
||||
* Download the statement if available and authorized.
|
||||
* Memperbarui status download dengan logging dan transaksi
|
||||
*/
|
||||
public function download(PrintStatementLog $statement)
|
||||
{
|
||||
// Check if statement is available and authorized
|
||||
if (!$statement->is_available) {
|
||||
return back()->with('error', 'Statement is not available for download.');
|
||||
}
|
||||
|
||||
/* if ($statement->authorization_status !== 'approved') {
|
||||
return back()->with('error', 'Statement download requires authorization.');
|
||||
}*/
|
||||
DB::beginTransaction();
|
||||
|
||||
// Update download status
|
||||
$statement->update([
|
||||
'is_downloaded' => true,
|
||||
'downloaded_at' => now(),
|
||||
'updated_by' => Auth::id()
|
||||
]);
|
||||
try {
|
||||
// Update download status
|
||||
$statement->update([
|
||||
'is_downloaded' => true,
|
||||
'downloaded_at' => now(),
|
||||
'updated_by' => Auth::id()
|
||||
]);
|
||||
|
||||
// Generate or fetch the statement file (implementation depends on your system)
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
$filePath = "{$statement->period_from}/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
|
||||
Log::info('Statement downloaded', [
|
||||
'statement_id' => $statement->id,
|
||||
'user_id' => Auth::id(),
|
||||
'account_number' => $statement->account_number
|
||||
]);
|
||||
|
||||
if ($statement->is_period_range && $statement->period_to) {
|
||||
$periodFrom = Carbon::createFromFormat('Ym', $statement->period_from);
|
||||
$periodTo = Carbon::createFromFormat('Ym', $statement->period_to);
|
||||
DB::commit();
|
||||
|
||||
// Loop through each month in the range
|
||||
$missingPeriods = [];
|
||||
$availablePeriods = [];
|
||||
// Generate or fetch the statement file
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
$filePath = "{$statement->period_from}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf";
|
||||
|
||||
for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) {
|
||||
$periodFormatted = $period->format('Ym');
|
||||
$periodPath = $periodFormatted . "/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
|
||||
if ($statement->is_period_range && $statement->period_to) {
|
||||
// Log: Memulai proses download period range
|
||||
Log::info('Starting period range download', [
|
||||
'statement_id' => $statement->id,
|
||||
'period_from' => $statement->period_from,
|
||||
'period_to' => $statement->period_to
|
||||
]);
|
||||
|
||||
if ($disk->exists($periodPath)) {
|
||||
$availablePeriods[] = $periodFormatted;
|
||||
} else {
|
||||
$missingPeriods[] = $periodFormatted;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handle period range download dengan membuat zip file
|
||||
* yang berisi semua statement dalam rentang periode
|
||||
*/
|
||||
$periodFrom = Carbon::createFromFormat('Ym', $statement->period_from);
|
||||
$periodTo = Carbon::createFromFormat('Ym', $statement->period_to);
|
||||
|
||||
// If any period is missing, the statement is not available
|
||||
if (count($availablePeriods) > 0) {
|
||||
// Create a temporary zip file
|
||||
$zipFileName = "{$statement->account_number}_{$statement->period_from}_to_{$statement->period_to}.zip";
|
||||
$zipFilePath = storage_path("app/temp/{$zipFileName}");
|
||||
// Loop through each month in the range
|
||||
$missingPeriods = [];
|
||||
$availablePeriods = [];
|
||||
|
||||
// Ensure the temp directory exists
|
||||
if (!file_exists(storage_path('app/temp'))) {
|
||||
mkdir(storage_path('app/temp'), 0755, true);
|
||||
for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) {
|
||||
$periodFormatted = $period->format('Ym');
|
||||
$periodPath = $periodFormatted . "/{$statement->branch_code}/{$statement->account_number}_{$periodFormatted}.pdf";
|
||||
|
||||
if ($disk->exists($periodPath)) {
|
||||
$availablePeriods[] = $periodFormatted;
|
||||
Log::info('Period available for download', [
|
||||
'period' => $periodFormatted,
|
||||
'path' => $periodPath
|
||||
]);
|
||||
} else {
|
||||
$missingPeriods[] = $periodFormatted;
|
||||
Log::warning('Period not available for download', [
|
||||
'period' => $periodFormatted,
|
||||
'path' => $periodPath
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new zip archive
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) {
|
||||
// Add each available statement to the zip
|
||||
foreach ($availablePeriods as $period) {
|
||||
$filePath = "{$period}/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
|
||||
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf");
|
||||
// If any period is available, create a zip and download it
|
||||
if (count($availablePeriods) > 0) {
|
||||
/**
|
||||
* Membuat zip file temporary untuk download
|
||||
* dengan semua statement yang tersedia dalam periode
|
||||
*/
|
||||
$zipFileName = "{$statement->account_number}_{$statement->period_from}_to_{$statement->period_to}.zip";
|
||||
$zipFilePath = storage_path("app/temp/{$zipFileName}");
|
||||
|
||||
// Download the file from SFTP to local storage temporarily
|
||||
file_put_contents($localFilePath, $disk->get($filePath));
|
||||
|
||||
// Add the file to the zip
|
||||
$zip->addFile($localFilePath, "{$statement->account_number}_{$period}.pdf");
|
||||
// Ensure the temp directory exists
|
||||
if (!file_exists(storage_path('app/temp'))) {
|
||||
mkdir(storage_path('app/temp'), 0755, true);
|
||||
Log::info('Created temp directory for zip files');
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
// Create a new zip archive
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) {
|
||||
Log::info('Zip archive created successfully', ['zip_path' => $zipFilePath]);
|
||||
|
||||
// Clean up temporary files
|
||||
foreach ($availablePeriods as $period) {
|
||||
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf");
|
||||
if (file_exists($localFilePath)) {
|
||||
unlink($localFilePath);
|
||||
// Add each available statement to the zip
|
||||
foreach ($availablePeriods as $period) {
|
||||
$periodFilePath = "{$period}/{$statement->branch_code}/{$statement->account_number}_{$period}.pdf";
|
||||
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf");
|
||||
|
||||
try {
|
||||
// Download the file from SFTP to local storage temporarily
|
||||
file_put_contents($localFilePath, $disk->get($periodFilePath));
|
||||
|
||||
// Add the file to the zip
|
||||
$zip->addFile($localFilePath, "{$statement->account_number}_{$period}.pdf");
|
||||
|
||||
Log::info('Added file to zip', [
|
||||
'period' => $period,
|
||||
'local_path' => $localFilePath
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to add file to zip', [
|
||||
'period' => $period,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the zip file for download
|
||||
return response()->download($zipFilePath, $zipFileName)->deleteFileAfterSend(true);
|
||||
$zip->close();
|
||||
Log::info('Zip archive closed successfully');
|
||||
|
||||
// Return the zip file for download
|
||||
$response = response()->download($zipFilePath, $zipFileName)->deleteFileAfterSend(true);
|
||||
|
||||
// Clean up temporary PDF files
|
||||
foreach ($availablePeriods as $period) {
|
||||
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf");
|
||||
if (file_exists($localFilePath)) {
|
||||
unlink($localFilePath);
|
||||
Log::info('Cleaned up temporary file', ['file' => $localFilePath]);
|
||||
}
|
||||
}
|
||||
|
||||
Log::info('Period range download completed successfully', [
|
||||
'statement_id' => $statement->id,
|
||||
'available_periods' => count($availablePeriods),
|
||||
'missing_periods' => count($missingPeriods)
|
||||
]);
|
||||
|
||||
return $response;
|
||||
} else {
|
||||
Log::error('Failed to create zip archive', ['zip_path' => $zipFilePath]);
|
||||
return back()->with('error', 'Failed to create zip archive for download.');
|
||||
}
|
||||
} else {
|
||||
return back()->with('error', 'Failed to create zip archive.');
|
||||
Log::warning('No statements available for download in period range', [
|
||||
'statement_id' => $statement->id,
|
||||
'missing_periods' => $missingPeriods
|
||||
]);
|
||||
return back()->with('error', 'No statements available for download in the specified period range.');
|
||||
}
|
||||
} else if ($disk->exists($filePath)) {
|
||||
/**
|
||||
* Handle single period download
|
||||
* Download file PDF tunggal untuk periode tertentu
|
||||
*/
|
||||
Log::info('Single period download', [
|
||||
'statement_id' => $statement->id,
|
||||
'file_path' => $filePath
|
||||
]);
|
||||
|
||||
return $disk->download($filePath, "{$statement->account_number}_{$statement->period_from}.pdf");
|
||||
} else {
|
||||
return back()->with('error', 'No statements available for download.');
|
||||
Log::warning('Statement file not found', [
|
||||
'statement_id' => $statement->id,
|
||||
'file_path' => $filePath
|
||||
]);
|
||||
return back()->with('error', 'Statement file not found.');
|
||||
}
|
||||
} else if ($disk->exists($filePath)) {
|
||||
return $disk->download($filePath, "{$statement->account_number}_{$statement->period_from}.pdf");
|
||||
} catch (Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
Log::error('Failed to download statement', [
|
||||
'statement_id' => $statement->id,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return back()->with('error', 'Failed to download statement: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,6 +475,13 @@
|
||||
// Retrieve data from the database
|
||||
$query = PrintStatementLog::query();
|
||||
|
||||
if (!auth()->user()->hasRole('administrator')) {
|
||||
$query->where(function($q) {
|
||||
$q->where('user_id', Auth::id())
|
||||
->orWhere('branch_code', Auth::user()->branch->code);
|
||||
});
|
||||
}
|
||||
|
||||
// Apply search filter if provided
|
||||
if ($request->has('search') && !empty($request->get('search'))) {
|
||||
$search = $request->get('search');
|
||||
@@ -275,7 +490,9 @@
|
||||
->orWhere('branch_code', 'LIKE', "%$search%")
|
||||
->orWhere('period_from', 'LIKE', "%$search%")
|
||||
->orWhere('period_to', 'LIKE', "%$search%")
|
||||
->orWhere('authorization_status', 'LIKE', "%$search%");
|
||||
->orWhere('authorization_status', 'LIKE', "%$search%")
|
||||
->orWhere('request_type', 'LIKE', "%$search%")
|
||||
->orWhere('status', 'LIKE', "%$search%");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -289,6 +506,10 @@
|
||||
$query->where('branch_code', $filter['value']);
|
||||
} else if ($filter['column'] === 'authorization_status') {
|
||||
$query->where('authorization_status', $filter['value']);
|
||||
} else if ($filter['column'] === 'request_type') {
|
||||
$query->where('request_type', $filter['value']);
|
||||
} else if ($filter['column'] === 'status') {
|
||||
$query->where('status', $filter['value']);
|
||||
} else if ($filter['column'] === 'is_downloaded') {
|
||||
$query->where('is_downloaded', filter_var($filter['value'], FILTER_VALIDATE_BOOLEAN));
|
||||
}
|
||||
@@ -303,12 +524,13 @@
|
||||
|
||||
// Map frontend column names to database column names if needed
|
||||
$columnMap = [
|
||||
'branch' => 'branch_code',
|
||||
'account' => 'account_number',
|
||||
'period' => 'period_from',
|
||||
'status' => 'authorization_status',
|
||||
'remarks' => 'remarks',
|
||||
// Add more mappings as needed
|
||||
'branch' => 'branch_code',
|
||||
'account' => 'account_number',
|
||||
'period' => 'period_from',
|
||||
'auth_status' => 'authorization_status',
|
||||
'request_type' => 'request_type',
|
||||
'status' => 'status',
|
||||
'remarks' => 'remarks',
|
||||
];
|
||||
|
||||
$dbColumn = $columnMap[$column] ?? $column;
|
||||
@@ -391,6 +613,7 @@
|
||||
public function sendEmail($id)
|
||||
{
|
||||
$statement = PrintStatementLog::findOrFail($id);
|
||||
|
||||
// Check if statement has email
|
||||
if (empty($statement->email)) {
|
||||
return redirect()->back()->with('error', 'No email address provided for this statement.');
|
||||
@@ -403,7 +626,7 @@
|
||||
|
||||
try {
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
$filePath = "{$statement->period_from}/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
|
||||
$filePath = "{$statement->period_from}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf";
|
||||
|
||||
if ($statement->is_period_range && $statement->period_to) {
|
||||
$periodFrom = Carbon::createFromFormat('Ym', $statement->period_from);
|
||||
@@ -415,7 +638,7 @@
|
||||
|
||||
for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) {
|
||||
$periodFormatted = $period->format('Ym');
|
||||
$periodPath = $periodFormatted . "/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
|
||||
$periodPath = $periodFormatted . "/{$statement->branch_code}/{$statement->account_number}_{$periodFormatted}.pdf";
|
||||
|
||||
if ($disk->exists($periodPath)) {
|
||||
$availablePeriods[] = $periodFormatted;
|
||||
@@ -440,7 +663,7 @@
|
||||
if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) {
|
||||
// Add each available statement to the zip
|
||||
foreach ($availablePeriods as $period) {
|
||||
$filePath = "{$period}/Print/{$statement->branch_code}/{$statement->account_number}.pdf";
|
||||
$filePath = "{$period}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf";
|
||||
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf");
|
||||
|
||||
// Download the file from SFTP to local storage temporarily
|
||||
@@ -504,6 +727,14 @@
|
||||
'updated_by' => Auth::id()
|
||||
]);
|
||||
|
||||
Log::info('Statement email sent successfully', [
|
||||
'statement_id' => $statement->id,
|
||||
'email' => $statement->email,
|
||||
'user_id' => Auth::id()
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return redirect()->back()->with('success', 'Statement has been sent to ' . $statement->email);
|
||||
} catch (Exception $e) {
|
||||
// Log the error
|
||||
|
||||
@@ -5,14 +5,9 @@
|
||||
use App\Http\Controllers\Controller;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Webstatement\Jobs\ExportStatementJob;
|
||||
use Modules\Webstatement\Models\StmtEntry;
|
||||
use Modules\Webstatement\Models\TempFundsTransfer;
|
||||
use Modules\Webstatement\Models\TempStmtNarrFormat;
|
||||
use Modules\Webstatement\Models\TempStmtNarrParam;
|
||||
use Modules\Webstatement\Models\AccountBalance;
|
||||
use Modules\Webstatement\Jobs\ExportStatementPeriodJob;
|
||||
|
||||
class WebstatementController extends Controller
|
||||
{
|
||||
@@ -21,296 +16,183 @@
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$data = [[
|
||||
'account_number' => '1080425781',
|
||||
'period' => '2025012',
|
||||
'saldo' => '23984352604'
|
||||
],[
|
||||
'account_number' => '1080425781',
|
||||
'period' => '2025013',
|
||||
'saldo' => '13984352604'
|
||||
]];
|
||||
|
||||
$jobIds = [];
|
||||
$data = [];
|
||||
|
||||
// Process each data entry
|
||||
foreach ($data as $entry) {
|
||||
// Dispatch job for each entry
|
||||
$job = new ExportStatementJob(
|
||||
$entry['account_number'],
|
||||
$entry['period'],
|
||||
$entry['saldo']
|
||||
);
|
||||
$jobIds[] = app(Dispatcher::class)->dispatch($job);
|
||||
foreach ($this->listAccount() as $clientName => $accounts) {
|
||||
foreach ($accounts as $accountNumber) {
|
||||
foreach ($this->listPeriod() as $period) {
|
||||
$job = new ExportStatementJob(
|
||||
$accountNumber,
|
||||
$period,
|
||||
$this->getAccountBalance($accountNumber, $period),
|
||||
$clientName // Pass the client name to the job
|
||||
);
|
||||
$jobIds[] = app(Dispatcher::class)->dispatch($job);
|
||||
$data[] = [
|
||||
'client_name' => $clientName,
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $period
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Statement export jobs have been queued',
|
||||
'jobs' => array_map(function($index, $jobId) use ($data) {
|
||||
'jobs' => array_map(function ($index, $jobId) use ($data) {
|
||||
return [
|
||||
'job_id' => $jobId,
|
||||
'job_id' => $jobId,
|
||||
'client_name' => $data[$index]['client_name'],
|
||||
'account_number' => $data[$index]['account_number'],
|
||||
'period' => $data[$index]['period'],
|
||||
'file_name' => "{$data[$index]['account_number']}_{$data[$index]['period']}.csv"
|
||||
'period' => $data[$index]['period'],
|
||||
'file_name' => "{$data[$index]['client_name']}_{$data[$index]['account_number']}_{$data[$index]['period']}.csv"
|
||||
];
|
||||
}, array_keys($jobIds), $jobIds)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a previously exported statement
|
||||
*/
|
||||
public function downloadStatement(Request $request)
|
||||
{
|
||||
$account_number = $request->input('account_number', '1080425781');
|
||||
$period = $request->input('period', '20250512');
|
||||
$fileName = "{$account_number}_{$period}.csv";
|
||||
$filePath = "statements/{$fileName}";
|
||||
|
||||
if (!Storage::disk('local')->exists($filePath)) {
|
||||
return response()->json([
|
||||
'message' => 'Statement file not found. It may still be processing.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
return Storage::disk('local')->download($filePath, $fileName, [
|
||||
"Content-Type" => "text/csv",
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate statement on-demand and return as download
|
||||
*/
|
||||
public function generateAndDownload(Request $request)
|
||||
{
|
||||
$account_number = $request->input('account_number', '1080425781');
|
||||
$period = $request->input('period', '20250512');
|
||||
$saldo = $request->input('saldo', '23984352604');
|
||||
|
||||
$stmt = StmtEntry::with(['ft', 'transaction'])
|
||||
->where('account_number', $account_number)
|
||||
->where('booking_date', $period)
|
||||
->orderBy('date_time', 'ASC')
|
||||
->orderBy('trans_reference', 'ASC')
|
||||
->get();
|
||||
|
||||
if ($stmt->isEmpty()) {
|
||||
return response()->json([
|
||||
'message' => 'No statement data found for the specified account and period.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
$runningBalance = (float) $saldo;
|
||||
// Map the data to transform or format specific fields
|
||||
$mappedData = $stmt->sortBy(['ACTUAL.DATE', 'REFERENCE.NUMBER'])
|
||||
->map(function ($item, $index) use (&$runningBalance) {
|
||||
$runningBalance += (float) $item->amount_lcy;
|
||||
return [
|
||||
'NO' => 0, // Will be updated later
|
||||
'TRANSACTION.DATE' => Carbon::createFromFormat('YmdHi', $item->booking_date . substr($item->ft?->date_time ?? '0000000000', 6, 4))
|
||||
->format('d/m/Y H:i'),
|
||||
'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' => Carbon::createFromFormat('ymdHi', $item->ft?->date_time ?? '2505120000')
|
||||
->format('d/m/Y H:i'),
|
||||
];
|
||||
})
|
||||
->values();
|
||||
|
||||
// Then apply the sequential numbers
|
||||
$mappedData = $mappedData->map(function ($item, $index) {
|
||||
$item['NO'] = $index + 1;
|
||||
return $item;
|
||||
});
|
||||
|
||||
$csvFileName = $account_number . "_" . $period . ".csv";
|
||||
$headers = [
|
||||
"Content-Type" => "text/csv",
|
||||
"Content-Disposition" => "attachment; filename={$csvFileName}"
|
||||
function listAccount(){
|
||||
return [
|
||||
'PLUANG' => [
|
||||
'1080426085',
|
||||
'1080425781',
|
||||
],
|
||||
'OY' => [
|
||||
'1081647484',
|
||||
'1081647485',
|
||||
],
|
||||
'INDORAYA' => [
|
||||
'1083123710',
|
||||
'1083123711',
|
||||
'1083123712',
|
||||
'1083123713',
|
||||
'1083123714',
|
||||
'1083123715',
|
||||
'1083123716',
|
||||
'1083123718',
|
||||
'1083123719',
|
||||
'1083123721',
|
||||
'1083123722',
|
||||
'1083123723',
|
||||
'1083123724',
|
||||
'1083123726',
|
||||
'1083123727',
|
||||
'1083123728',
|
||||
'1083123730',
|
||||
'1083123731',
|
||||
'1083123732',
|
||||
'1083123734',
|
||||
'1083123735',
|
||||
],
|
||||
'TDC' => [
|
||||
'1086677889',
|
||||
'1086677890',
|
||||
'1086677891',
|
||||
'1086677892',
|
||||
'1086677893',
|
||||
'1086677894',
|
||||
'1086677895',
|
||||
'1086677896',
|
||||
'1086677897',
|
||||
],
|
||||
'ASIA_PARKING' => [
|
||||
'1080119298',
|
||||
'1080119361',
|
||||
'1080119425',
|
||||
'1080119387',
|
||||
'1082208069',
|
||||
],
|
||||
'DAU' => [
|
||||
'1085151668',
|
||||
],
|
||||
'EGR' => [
|
||||
'1085368601',
|
||||
],
|
||||
'SARANA_PACTINDO' => [
|
||||
'1078333878',
|
||||
],
|
||||
'SWADAYA_PANDU' => [
|
||||
'0081272689',
|
||||
]
|
||||
];
|
||||
|
||||
$callback = function () use ($mappedData) {
|
||||
$file = fopen('php://output', 'w');
|
||||
// Write headers without quotes, using pipe separator
|
||||
fputs($file, implode('|', array_keys($mappedData[0])) . "\n");
|
||||
// Write data rows without quotes, using pipe separator
|
||||
foreach ($mappedData as $row) {
|
||||
fputs($file, implode('|', $row) . "\n");
|
||||
}
|
||||
fclose($file);
|
||||
};
|
||||
|
||||
return response()->stream($callback, 200, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate narrative for a statement entry
|
||||
*/
|
||||
private function generateNarrative($item)
|
||||
{
|
||||
$narr = '';
|
||||
if ($item->transaction && $item->transaction->narr_type) {
|
||||
$narr .= $item->transaction->stmt_narr . ' ';
|
||||
$narr .= $this->getFormatNarrative($item->transaction->narr_type, $item);
|
||||
} else if ($item->transaction) {
|
||||
$narr .= $item->transaction->stmt_narr . ' ';
|
||||
}
|
||||
|
||||
if ($item->ft && $item->ft->recipt_no) {
|
||||
$narr .= 'Receipt No: ' . $item->ft->recipt_no;
|
||||
}
|
||||
return $narr;
|
||||
function listPeriod(){
|
||||
return [
|
||||
date('Ymd', strtotime('-1 day'))
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted narrative based on narrative type
|
||||
*/
|
||||
private function getFormatNarrative($narr, $item)
|
||||
|
||||
function getAccountBalance($accountNumber, $period)
|
||||
{
|
||||
$narrParam = TempStmtNarrParam::where('_id', $narr)->first();
|
||||
$accountBalance = AccountBalance::where('account_number', $accountNumber)
|
||||
->where('period', '<', $period)
|
||||
->orderBy('period', 'desc')
|
||||
->first();
|
||||
|
||||
if (!$narrParam) {
|
||||
return '';
|
||||
}
|
||||
return $accountBalance->actual_balance ?? 0;
|
||||
}
|
||||
|
||||
$fmt = '';
|
||||
if ($narrParam->_id == 'FTIN') {
|
||||
$fmt = 'FT.IN';
|
||||
} else if ($narrParam->_id == 'FTOUT') {
|
||||
$fmt = 'FT.IN';
|
||||
} else {
|
||||
$fmt = $narrParam->_id;
|
||||
}
|
||||
|
||||
$narrFormat = TempStmtNarrFormat::where('_id', $fmt)->first();
|
||||
function printStatementRekening($accountNumber, $period = null) {
|
||||
$period = $period ?? date('Ym');
|
||||
$balance = AccountBalance::where('account_number', $accountNumber)
|
||||
->when($period === '202505', function($query) {
|
||||
return $query->where('period', '>=', '20250512')
|
||||
->orderBy('period', 'asc');
|
||||
}, function($query) use ($period) {
|
||||
// Get balance from last day of previous month
|
||||
$firstDayOfMonth = Carbon::createFromFormat('Ym', $period)->startOfMonth();
|
||||
$lastDayPrevMonth = $firstDayOfMonth->copy()->subDay()->format('Ymd');
|
||||
return $query->where('period', $lastDayPrevMonth);
|
||||
})
|
||||
->first()
|
||||
->actual_balance ?? '0.00';
|
||||
$clientName = 'client1';
|
||||
|
||||
if (!$narrFormat) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
\Log::info("Starting statement export for account: {$accountNumber}, period: {$period}, client: {$clientName}");
|
||||
|
||||
// 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;
|
||||
// Validate inputs
|
||||
if (empty($accountNumber) || empty($period) || empty($clientName)) {
|
||||
throw new \Exception('Required parameters missing');
|
||||
}
|
||||
|
||||
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));
|
||||
// Dispatch the job
|
||||
$job = ExportStatementPeriodJob::dispatch($accountNumber, $period, $balance, $clientName);
|
||||
|
||||
// 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') {
|
||||
$result .= $this->getTransaction($item->trans_reference, $fieldName) . ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transaction data by reference and field
|
||||
*/
|
||||
private function getTransaction($ref, $field)
|
||||
{
|
||||
$trans = TempFundsTransfer::where('ref_no', $ref)->first();
|
||||
return $trans ? ($trans->$field ?? "") : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a statement export job and return job ID
|
||||
*/
|
||||
public function queueExport(Request $request)
|
||||
{
|
||||
$account_number = $request->input('account_number', '1080425781');
|
||||
$period = $request->input('period', '20250512');
|
||||
$saldo = $request->input('saldo', '23984352604');
|
||||
|
||||
// Dispatch the job and get the job ID
|
||||
$job = new ExportStatementJob($account_number, $period, $saldo);
|
||||
$jobId = app(Dispatcher::class)->dispatch($job);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Statement export job has been queued',
|
||||
'job_id' => $jobId,
|
||||
'account_number' => $account_number,
|
||||
'period' => $period,
|
||||
'file_name' => "{$account_number}_{$period}.csv"
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the status of an export job
|
||||
*/
|
||||
public function checkExportStatus(Request $request, $jobId)
|
||||
{
|
||||
// Get job status from the queue
|
||||
$job = DB::table('jobs')
|
||||
->where('id', $jobId)
|
||||
->first();
|
||||
|
||||
if (!$job) {
|
||||
// Check if job is completed
|
||||
$completedJob = DB::table('job_batches')
|
||||
->where('id', $jobId)
|
||||
->first();
|
||||
|
||||
if ($completedJob) {
|
||||
return response()->json([
|
||||
'status' => 'completed',
|
||||
'message' => 'Export job has been completed'
|
||||
]);
|
||||
}
|
||||
\Log::info("Statement export job dispatched successfully", [
|
||||
'job_id' => $job->job_id ?? null,
|
||||
'account' => $accountNumber,
|
||||
'period' => $period,
|
||||
'client' => $clientName
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'not_found',
|
||||
'message' => 'Export job not found'
|
||||
], 404);
|
||||
}
|
||||
'success' => true,
|
||||
'message' => 'Statement export job queued successfully',
|
||||
'data' => [
|
||||
'job_id' => $job->job_id ?? null,
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $period,
|
||||
'client_name' => $clientName
|
||||
]
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'pending',
|
||||
'message' => 'Export job is still processing'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error("Failed to export statement", [
|
||||
'error' => $e->getMessage(),
|
||||
'account' => $accountNumber,
|
||||
'period' => $period
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to queue statement export job',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,125 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Http\Requests;
|
||||
namespace Modules\Webstatement\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Modules\Webstatement\Models\PrintStatementLog as Statement;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Modules\Webstatement\Models\PrintStatementLog as Statement;
|
||||
|
||||
class PrintStatementRequest extends FormRequest
|
||||
class PrintStatementRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize()
|
||||
: bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules()
|
||||
: array
|
||||
{
|
||||
$rules = [
|
||||
'branch_code' => ['required', 'string', 'exists:branches,code'],
|
||||
'account_number' => ['required', 'string'],
|
||||
'is_period_range' => ['sometimes', 'boolean'],
|
||||
'email' => ['nullable', 'email'],
|
||||
'email_sent_at' => ['nullable', 'timestamp'],
|
||||
'period_from' => [
|
||||
'required',
|
||||
'string',
|
||||
'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);
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'account_number' => ['required', 'string'],
|
||||
'is_period_range' => ['sometimes', 'boolean'],
|
||||
'email' => ['nullable', 'email'],
|
||||
'email_sent_at' => ['nullable', 'timestamp'],
|
||||
'request_type' => ['sometimes', 'string', 'in:single_account,branch,all_branches'],
|
||||
'batch_id' => ['nullable', 'string'],
|
||||
'period_from' => [
|
||||
'required',
|
||||
'string',
|
||||
'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('is_available', true)
|
||||
->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 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 this is an update request, exclude the current record
|
||||
if ($this->route('statement')) {
|
||||
$query->where('id', '!=', $this->route('statement'));
|
||||
}
|
||||
],
|
||||
];
|
||||
|
||||
// If it's a period range, require period_to
|
||||
if ($this->input('period_to')) {
|
||||
$rules['period_to'] = [
|
||||
'required',
|
||||
'string',
|
||||
'regex:/^\d{6}$/', // YYYYMM format
|
||||
'gte:period_from' // period_to must be greater than or equal to period_from
|
||||
];
|
||||
}
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
if ($query->exists()) {
|
||||
$fail('A statement request with this account number and period already exists.');
|
||||
}
|
||||
}
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function messages()
|
||||
: array
|
||||
{
|
||||
return [
|
||||
'branch_code.required' => 'Branch code is required',
|
||||
'branch_code.exists' => 'Selected branch does not exist',
|
||||
'account_number.required' => 'Account number is required',
|
||||
'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',
|
||||
'period_to.regex' => 'End period must be in YYYYMM format',
|
||||
'period_to.gte' => 'End period must be after or equal to start period',
|
||||
// If it's a period range, require period_to
|
||||
if ($this->input('period_to')) {
|
||||
$rules['period_to'] = [
|
||||
'required',
|
||||
'string',
|
||||
'regex:/^\d{6}$/', // YYYYMM format
|
||||
'gte:period_from' // period_to must be greater than or equal to period_from
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function prepareForValidation()
|
||||
: void
|
||||
{
|
||||
if ($this->has('period_from')) {
|
||||
//conver to YYYYMM format
|
||||
$this->merge([
|
||||
'period_from' => substr($this->period_from, 0, 4) . substr($this->period_from, 5, 2),
|
||||
]);
|
||||
}
|
||||
return $rules;
|
||||
}
|
||||
|
||||
if ($this->has('period_to')) {
|
||||
//conver to YYYYMM format
|
||||
$this->merge([
|
||||
'period_to' => substr($this->period_to, 0, 4) . substr($this->period_to, 5, 2),
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'account_number.required' => 'Account number is required',
|
||||
'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',
|
||||
'period_to.regex' => 'End period must be in YYYYMM format',
|
||||
'period_to.gte' => 'End period must be after or equal to start period',
|
||||
'request_type.in' => 'Request type must be single_account, branch, or all_branches',
|
||||
];
|
||||
}
|
||||
|
||||
// Convert is_period_range to boolean if it exists
|
||||
if ($this->has('period_to')) {
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
if ($this->has('period_from')) {
|
||||
// Convert to YYYYMM format
|
||||
$this->merge([
|
||||
'period_from' => substr($this->period_from, 0, 4) . substr($this->period_from, 5, 2),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->has('period_to')) {
|
||||
// Convert to YYYYMM format
|
||||
$this->merge([
|
||||
'period_to' => substr($this->period_to, 0, 4) . substr($this->period_to, 5, 2),
|
||||
]);
|
||||
|
||||
// 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
|
||||
if (!$this->has('request_type')) {
|
||||
$this->merge([
|
||||
'request_type' => 'single_account',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
138
app/Jobs/CombinePdfJob.php
Normal file
138
app/Jobs/CombinePdfJob.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
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\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Owenoj\PDFPasswordProtect\Facade\PDFPasswordProtect;
|
||||
use Webklex\PDFMerger\Facades\PDFMergerFacade as PDFMerger;
|
||||
|
||||
class CombinePdfJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $pdfFiles;
|
||||
protected $outputPath;
|
||||
protected $outputFilename;
|
||||
protected $password;
|
||||
protected $outputDestination;
|
||||
protected $branchCode;
|
||||
protected $period;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @param array $pdfFiles Array of PDF file paths to combine
|
||||
* @param string $outputPath Directory where the combined PDF will be saved
|
||||
* @param string $outputFilename Filename for the combined PDF
|
||||
* @param string $password Password to protect the PDF
|
||||
* @param string $outputDestination Output destination: 'local' or 'sftp'
|
||||
* @param string $branchCode Branch code for SFTP path
|
||||
* @param string $period Period for SFTP path
|
||||
*/
|
||||
public function __construct(array $pdfFiles, string $outputPath, string $outputFilename, string $password, string $outputDestination = 'local', string $branchCode = '', string $period = '')
|
||||
{
|
||||
$this->pdfFiles = $pdfFiles;
|
||||
$this->outputPath = $outputPath;
|
||||
$this->outputFilename = $outputFilename;
|
||||
$this->password = $password;
|
||||
$this->outputDestination = $outputDestination;
|
||||
$this->branchCode = $branchCode;
|
||||
$this->period = $period;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
// Initialize the PDF merger
|
||||
$merger = PDFMerger::init();
|
||||
|
||||
// Add each PDF file to the merger
|
||||
foreach ($this->pdfFiles as $pdfFile) {
|
||||
if (file_exists($pdfFile)) {
|
||||
$merger->addPDF($pdfFile, 'all');
|
||||
} else {
|
||||
Log::warning("PDF file not found: {$pdfFile}");
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the output directory exists
|
||||
if (!file_exists($this->outputPath)) {
|
||||
mkdir($this->outputPath, 0755, true);
|
||||
}
|
||||
|
||||
// Merge the PDFs
|
||||
$merger->merge();
|
||||
|
||||
// Save the merged PDF
|
||||
$fullPath = $this->outputPath . '/' . $this->outputFilename;
|
||||
$merger->save($fullPath);
|
||||
|
||||
// Apply password protection if password is provided
|
||||
if (!empty($this->password)) {
|
||||
$tempPath = $this->outputPath . '/temp_' . $this->outputFilename;
|
||||
|
||||
// Rename the original file to a temporary name
|
||||
rename($fullPath, $tempPath);
|
||||
|
||||
// Apply password protection and save to the original filename
|
||||
PDFPasswordProtect::encrypt($tempPath, $fullPath, $this->password);
|
||||
|
||||
// Remove the temporary file
|
||||
if (file_exists($tempPath)) {
|
||||
unlink($tempPath);
|
||||
}
|
||||
|
||||
Log::info("PDF password protection applied successfully.");
|
||||
}
|
||||
|
||||
// Handle output destination
|
||||
if ($this->outputDestination === 'sftp') {
|
||||
$this->uploadToSftp($fullPath);
|
||||
}
|
||||
|
||||
Log::info("PDFs combined successfully. Output file: {$fullPath}, Destination: {$this->outputDestination}");
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error combining PDFs: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload the combined PDF to SFTP server
|
||||
*
|
||||
* @param string $localFilePath
|
||||
*/
|
||||
private function uploadToSftp(string $localFilePath): void
|
||||
{
|
||||
try {
|
||||
// Define SFTP path: combine/{period}/{branchCode}/{filename}
|
||||
$sftpPath = "combine/{$this->period}/{$this->branchCode}/{$this->outputFilename}";
|
||||
|
||||
// Read the local file content
|
||||
$fileContent = File::get($localFilePath);
|
||||
|
||||
// Upload to SFTP
|
||||
Storage::disk('sftpStatement')->put($sftpPath, $fileContent);
|
||||
|
||||
Log::info("Combined PDF uploaded to SFTP: {$sftpPath}");
|
||||
|
||||
// Optionally, remove the local file after successful upload
|
||||
// File::delete($localFilePath);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error uploading combined PDF to SFTP: {$e->getMessage()}");
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
105
app/Jobs/ConvertHtmlToPdfJob.php
Normal file
105
app/Jobs/ConvertHtmlToPdfJob.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
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\Log;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
|
||||
class ConvertHtmlToPdfJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $baseDirectory;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @param string $baseDirectory Base directory path to scan
|
||||
*/
|
||||
public function __construct(string $baseDirectory)
|
||||
{
|
||||
$this->baseDirectory = $baseDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
Log::info("Starting HTML to PDF conversion in directory: {$this->baseDirectory}");
|
||||
|
||||
// Check if directory exists
|
||||
if (!File::isDirectory($this->baseDirectory)) {
|
||||
Log::error("Directory not found: {$this->baseDirectory}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all subdirectories (ID folders)
|
||||
$idDirectories = File::directories($this->baseDirectory);
|
||||
|
||||
foreach ($idDirectories as $idDirectory) {
|
||||
$this->processDirectory($idDirectory);
|
||||
}
|
||||
|
||||
Log::info("HTML to PDF conversion completed successfully.");
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error converting HTML to PDF: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single ID directory
|
||||
*
|
||||
* @param string $directory Directory path to process
|
||||
*/
|
||||
protected function processDirectory(string $directory): void
|
||||
{
|
||||
try {
|
||||
$htmlFiles = File::glob($directory . '/*.html');
|
||||
|
||||
foreach ($htmlFiles as $htmlFile) {
|
||||
$this->convertHtmlToPdf($htmlFile);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error processing directory {$directory}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a single HTML file to PDF
|
||||
*
|
||||
* @param string $htmlFilePath Path to HTML file
|
||||
*/
|
||||
protected function convertHtmlToPdf(string $htmlFilePath): void
|
||||
{
|
||||
try {
|
||||
$filename = pathinfo($htmlFilePath, PATHINFO_FILENAME);
|
||||
$directory = pathinfo($htmlFilePath, PATHINFO_DIRNAME);
|
||||
$pdfFilePath = $directory . '/' . $filename . '.pdf';
|
||||
|
||||
// Read HTML content
|
||||
$htmlContent = File::get($htmlFilePath);
|
||||
|
||||
// Convert HTML to PDF with A4 size
|
||||
$pdf = PDF::loadHTML($htmlContent);
|
||||
|
||||
// Set paper size to A4
|
||||
$pdf->setPaper('A4', 'portrait');
|
||||
|
||||
// Save PDF file
|
||||
$pdf->save($pdfFilePath);
|
||||
|
||||
Log::info("Converted {$htmlFilePath} to {$pdfFilePath} with A4 size");
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error converting {$htmlFilePath} to PDF: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@
|
||||
protected $period;
|
||||
protected $saldo;
|
||||
protected $disk;
|
||||
protected $client;
|
||||
protected $fileName;
|
||||
protected $chunkSize = 1000; // Proses data dalam chunk untuk mengurangi penggunaan memori
|
||||
|
||||
@@ -37,12 +38,13 @@
|
||||
* @param string $saldo
|
||||
* @param string $disk
|
||||
*/
|
||||
public function __construct(string $account_number, string $period, string $saldo, string $disk = 'local')
|
||||
public function __construct(string $account_number, string $period, string $saldo, string $client = '', string $disk = 'local')
|
||||
{
|
||||
$this->account_number = $account_number;
|
||||
$this->period = $period;
|
||||
$this->saldo = $saldo;
|
||||
$this->disk = $disk;
|
||||
$this->client = $client;
|
||||
$this->fileName = "{$account_number}_{$period}.csv";
|
||||
}
|
||||
|
||||
@@ -166,9 +168,23 @@
|
||||
: 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',
|
||||
$item->booking_date . substr($item->ft?->date_time ?? '0000000000', 6, 4)
|
||||
$item->booking_date . substr($datetime, 6, 4)
|
||||
)->format('d/m/Y H:i');
|
||||
} catch (Exception $e) {
|
||||
Log::warning("Error formatting transaction date: " . $e->getMessage());
|
||||
@@ -180,9 +196,24 @@
|
||||
: 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',
|
||||
$item->ft?->date_time ?? '2505120000'
|
||||
$datetime
|
||||
)->format('d/m/Y H:i');
|
||||
} catch (Exception $e) {
|
||||
Log::warning("Error formatting actual date: " . $e->getMessage());
|
||||
@@ -195,18 +226,27 @@
|
||||
*/
|
||||
private function generateNarrative($item)
|
||||
{
|
||||
$narr = '';
|
||||
if ($item->transaction->narr_type) {
|
||||
$narr .= $item->transaction->stmt_narr . ' ';
|
||||
$narr .= $this->getFormatNarrative($item->transaction->narr_type, $item);
|
||||
} else {
|
||||
$narr .= $item->transaction->stmt_narr . ' ';
|
||||
$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;
|
||||
$narr[] = 'Receipt No: ' . $item->ft->recipt_no;
|
||||
}
|
||||
return trim($narr);
|
||||
|
||||
return implode(' ', array_filter($narr));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -225,7 +265,17 @@
|
||||
$fmt = 'FT.IN';
|
||||
} else if ($narrParam->_id == 'FTOUT') {
|
||||
$fmt = 'FT.OUT';
|
||||
} else {
|
||||
} 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;
|
||||
}
|
||||
|
||||
@@ -254,7 +304,7 @@
|
||||
$splitPart = explode('!', $part);
|
||||
if (count($splitPart) > 0) {
|
||||
// Remove quotes, backslashes, and other escape characters
|
||||
$cleanPart = trim($splitPart[0]);
|
||||
$cleanPart = trim($splitPart[0]).' ';
|
||||
// Remove quotes at the beginning and end
|
||||
$cleanPart = preg_replace('/^["\'\\\\]+|["\'\\\\]+$/', '', $cleanPart);
|
||||
// Remove any remaining backslashes
|
||||
@@ -283,13 +333,24 @@
|
||||
} else {
|
||||
// If no value found, try to use the original field name as a fallback
|
||||
if ($fieldName !== 'recipt_no') {
|
||||
$result .= ($item->ft?->$fieldName ?? '') . ' ';
|
||||
$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);
|
||||
return str_replace('<NL>', ' ', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -298,9 +359,25 @@
|
||||
private function exportToCsv()
|
||||
: void
|
||||
{
|
||||
// Determine the base path based on client
|
||||
$basePath = !empty($this->client)
|
||||
? "statements/{$this->client}"
|
||||
: "statements";
|
||||
|
||||
// Create client directory if it doesn't exist
|
||||
if (!empty($this->client)) {
|
||||
Storage::disk($this->disk)->makeDirectory($basePath);
|
||||
}
|
||||
|
||||
// Create account directory
|
||||
$accountPath = "{$basePath}/{$this->account_number}";
|
||||
Storage::disk($this->disk)->makeDirectory($accountPath);
|
||||
|
||||
$filePath = "{$accountPath}/{$this->fileName}";
|
||||
|
||||
// Delete existing file if it exists
|
||||
if (Storage::disk($this->disk)->exists("statements/{$this->fileName}")) {
|
||||
Storage::disk($this->disk)->delete("statements/{$this->fileName}");
|
||||
if (Storage::disk($this->disk)->exists($filePath)) {
|
||||
Storage::disk($this->disk)->delete($filePath);
|
||||
}
|
||||
|
||||
$csvContent = "NO|TRANSACTION.DATE|REFERENCE.NUMBER|TRANSACTION.AMOUNT|TRANSACTION.TYPE|DESCRIPTION|END.BALANCE|ACTUAL.DATE\n";
|
||||
@@ -309,7 +386,7 @@
|
||||
ProcessedStatement::where('account_number', $this->account_number)
|
||||
->where('period', $this->period)
|
||||
->orderBy('sequence_no')
|
||||
->chunk($this->chunkSize, function ($statements) use (&$csvContent) {
|
||||
->chunk($this->chunkSize, function ($statements) use (&$csvContent, $filePath) {
|
||||
foreach ($statements as $statement) {
|
||||
$csvContent .= implode('|', [
|
||||
$statement->sequence_no,
|
||||
@@ -324,11 +401,11 @@
|
||||
}
|
||||
|
||||
// Tulis ke file secara bertahap untuk mengurangi penggunaan memori
|
||||
Storage::disk($this->disk)->append("statements/{$this->fileName}", $csvContent);
|
||||
Storage::disk($this->disk)->append($filePath, $csvContent);
|
||||
$csvContent = ''; // Reset content setelah ditulis
|
||||
});
|
||||
|
||||
Log::info("Statement exported to {$this->disk} disk: statements/{$this->fileName}");
|
||||
Log::info("Statement exported to {$this->disk} disk: {$filePath}");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
443
app/Jobs/ExportStatementPeriodJob.php
Normal file
443
app/Jobs/ExportStatementPeriodJob.php
Normal file
@@ -0,0 +1,443 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
use Carbon\Carbon;
|
||||
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\Storage;
|
||||
use Modules\Webstatement\Models\ProcessedStatement;
|
||||
use Modules\Webstatement\Models\StmtEntry;
|
||||
use Modules\Webstatement\Models\TempFundsTransfer;
|
||||
use Modules\Webstatement\Models\TempStmtNarrFormat;
|
||||
use Modules\Webstatement\Models\TempStmtNarrParam;
|
||||
|
||||
class ExportStatementPeriodJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $account_number;
|
||||
protected $period; // Format: YYYYMM (e.g., 202505)
|
||||
protected $saldo;
|
||||
protected $disk;
|
||||
protected $client;
|
||||
protected $fileName;
|
||||
protected $chunkSize = 1000;
|
||||
protected $startDate;
|
||||
protected $endDate;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @param string $account_number
|
||||
* @param string $period Format: YYYYMM (e.g., 202505)
|
||||
* @param string $saldo
|
||||
* @param string $client
|
||||
* @param string $disk
|
||||
*/
|
||||
public function __construct(string $account_number, string $period, string $saldo, string $client = '', string $disk = 'local')
|
||||
{
|
||||
$this->account_number = $account_number;
|
||||
$this->period = $period;
|
||||
$this->saldo = $saldo;
|
||||
$this->disk = $disk;
|
||||
$this->client = $client;
|
||||
$this->fileName = "{$account_number}_{$period}.csv";
|
||||
|
||||
// Calculate start and end dates based on period
|
||||
$this->calculatePeriodDates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate start and end dates for the given period
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
Log::info("Starting export statement period job for account: {$this->account_number}, period: {$this->period}");
|
||||
Log::info("Date range: {$this->startDate->format('Y-m-d')} to {$this->endDate->format('Y-m-d')}");
|
||||
|
||||
$this->processStatementData();
|
||||
$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());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function processStatementData(): void
|
||||
{
|
||||
$accountQuery = [
|
||||
'account_number' => $this->account_number,
|
||||
'period' => $this->period
|
||||
];
|
||||
|
||||
$totalCount = $this->getTotalEntryCount();
|
||||
$existingDataCount = $this->getExistingProcessedCount($accountQuery);
|
||||
|
||||
// Only process if data is not fully processed
|
||||
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();
|
||||
}
|
||||
|
||||
private function getExistingProcessedCount(array $criteria): int
|
||||
{
|
||||
return ProcessedStatement::where('account_number', $criteria['account_number'])
|
||||
->where('period', $criteria['period'])
|
||||
->count();
|
||||
}
|
||||
|
||||
private function deleteExistingProcessedData(array $criteria): void
|
||||
{
|
||||
ProcessedStatement::where('account_number', $criteria['account_number'])
|
||||
->where('period', $criteria['period'])
|
||||
->delete();
|
||||
}
|
||||
|
||||
private function processAndSaveStatementEntries(int $totalCount): void
|
||||
{
|
||||
$runningBalance = (float) $this->saldo;
|
||||
$globalSequence = 0;
|
||||
|
||||
Log::info("Processing {$totalCount} statement entries for account: {$this->account_number}");
|
||||
|
||||
StmtEntry::with(['ft', 'transaction'])
|
||||
->where('account_number', $this->account_number)
|
||||
->whereBetween('date_time', [
|
||||
$this->startDate->format('ymdHi'),
|
||||
$this->endDate->format('ymdHi')
|
||||
])
|
||||
->orderBy('date_time', 'ASC')
|
||||
->orderBy('trans_reference', 'ASC')
|
||||
->chunk($this->chunkSize, function ($entries) use (&$runningBalance, &$globalSequence) {
|
||||
$processedData = $this->prepareProcessedData($entries, $runningBalance, $globalSequence);
|
||||
|
||||
if (!empty($processedData)) {
|
||||
DB::table('processed_statements')->insert($processedData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function prepareProcessedData($entries, &$runningBalance, &$globalSequence): array
|
||||
{
|
||||
$processedData = [];
|
||||
|
||||
foreach ($entries as $item) {
|
||||
$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,
|
||||
'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;
|
||||
}
|
||||
|
||||
private function formatTransactionDate($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;
|
||||
}
|
||||
|
||||
// Extract date from datetime (first 6 characters) and time (last 4 characters)
|
||||
$dateStr = substr($datetime, 0, 6); // YYMMDD
|
||||
$timeStr = substr($datetime, 6, 4); // HHMM
|
||||
|
||||
return Carbon::createFromFormat(
|
||||
'ymdHi',
|
||||
$dateStr . $timeStr
|
||||
)->format('d/m/Y H:i');
|
||||
} catch (Exception $e) {
|
||||
Log::warning("Error formatting transaction date: " . $e->getMessage());
|
||||
return Carbon::now()->format('d/m/Y H:i');
|
||||
}
|
||||
}
|
||||
|
||||
private 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 for a statement entry
|
||||
*/
|
||||
private 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 based on narrative type
|
||||
*/
|
||||
private 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export processed data to CSV file
|
||||
*/
|
||||
private function exportToCsv(): void
|
||||
{
|
||||
// Determine the base path based on client
|
||||
$basePath = !empty($this->client)
|
||||
? "statements/{$this->client}"
|
||||
: "statements";
|
||||
|
||||
// Create client directory if it doesn't exist
|
||||
if (!empty($this->client)) {
|
||||
Storage::disk($this->disk)->makeDirectory($basePath);
|
||||
}
|
||||
|
||||
// Create account directory
|
||||
$accountPath = "{$basePath}/{$this->account_number}";
|
||||
Storage::disk($this->disk)->makeDirectory($accountPath);
|
||||
|
||||
$filePath = "{$accountPath}/{$this->fileName}";
|
||||
|
||||
// Delete existing file if it exists
|
||||
if (Storage::disk($this->disk)->exists($filePath)) {
|
||||
Storage::disk($this->disk)->delete($filePath);
|
||||
}
|
||||
|
||||
$csvContent = "NO|TRANSACTION.DATE|REFERENCE.NUMBER|TRANSACTION.AMOUNT|TRANSACTION.TYPE|DESCRIPTION|END.BALANCE|ACTUAL.DATE\n";
|
||||
|
||||
// Retrieve processed data in chunks to reduce memory usage
|
||||
ProcessedStatement::where('account_number', $this->account_number)
|
||||
->where('period', $this->period)
|
||||
->orderBy('sequence_no')
|
||||
->chunk($this->chunkSize, function ($statements) use (&$csvContent, $filePath) {
|
||||
foreach ($statements as $statement) {
|
||||
$csvContent .= implode('|', [
|
||||
$statement->sequence_no,
|
||||
$statement->transaction_date,
|
||||
$statement->reference_number,
|
||||
$statement->transaction_amount,
|
||||
$statement->transaction_type,
|
||||
$statement->description,
|
||||
$statement->end_balance,
|
||||
$statement->actual_date
|
||||
]) . "\n";
|
||||
}
|
||||
|
||||
// Write to file incrementally to reduce memory usage
|
||||
Storage::disk($this->disk)->append($filePath, $csvContent);
|
||||
$csvContent = ''; // Reset content after writing
|
||||
});
|
||||
|
||||
Log::info("Statement exported to {$this->disk} disk: {$filePath}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transaction data by reference and field
|
||||
*/
|
||||
private function getTransaction($ref, $field)
|
||||
{
|
||||
$trans = TempFundsTransfer::where('ref_no', $ref)->first();
|
||||
return $trans->$field ?? "";
|
||||
}
|
||||
}
|
||||
156
app/Jobs/GenerateAtmTransactionReportJob.php
Normal file
156
app/Jobs/GenerateAtmTransactionReportJob.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
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\Storage;
|
||||
use Modules\Webstatement\Models\AtmTransaction;
|
||||
use Modules\Webstatement\Models\AtmTransactionReportLog;
|
||||
|
||||
class GenerateAtmTransactionReportJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
private string $period;
|
||||
private const CHUNK_SIZE = 1000;
|
||||
private const CSV_DELIMITER = ',';
|
||||
private ?int $reportLogId;
|
||||
|
||||
public function __construct(string $period, ?int $reportLogId = null)
|
||||
{
|
||||
$this->period = $period;
|
||||
$this->reportLogId = $reportLogId;
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$reportLog = null;
|
||||
if ($this->reportLogId) {
|
||||
$reportLog = AtmTransactionReportLog::find($this->reportLogId);
|
||||
}
|
||||
|
||||
try {
|
||||
Log::info("Starting ATM Transaction report generation for period: {$this->period} and LogId: {$this->reportLogId}");
|
||||
|
||||
$filename = "atm_transaction_report_{$this->period}.csv";
|
||||
$filePath = "reports/atm_transactions/{$filename}";
|
||||
|
||||
// Create directory if not exists
|
||||
Storage::makeDirectory('reports/atm_transactions');
|
||||
|
||||
// Initialize CSV file with headers
|
||||
$headers = [
|
||||
'reff_no',
|
||||
'pan',
|
||||
'atm_id_terminal_id',
|
||||
'amount',
|
||||
'channel',
|
||||
'account_no',
|
||||
'internal_account',
|
||||
'transaction_type',
|
||||
'trans_ref',
|
||||
'posting_date',
|
||||
'stan',
|
||||
'trans_status'
|
||||
];
|
||||
|
||||
$csvContent = implode(self::CSV_DELIMITER, $headers) . "\n";
|
||||
Storage::put($filePath, $csvContent);
|
||||
|
||||
$totalRecords = 0;
|
||||
|
||||
// Process data in chunks
|
||||
AtmTransaction::select(
|
||||
'retrieval_ref_no as reff_no',
|
||||
'pan_number as pan',
|
||||
'card_acc_id as atm_id_terminal_id',
|
||||
'txn_amount as amount',
|
||||
'merchant_id as channel',
|
||||
'debit_acct_no as account_no',
|
||||
'credit_acct_no as internal_account',
|
||||
'txn_type as transaction_type',
|
||||
'trans_ref',
|
||||
'booking_date as posting_date',
|
||||
'stan_no as stan',
|
||||
'trans_status'
|
||||
)
|
||||
->where('booking_date', $this->period)
|
||||
->chunk(self::CHUNK_SIZE, function ($transactions) use ($filePath, &$totalRecords) {
|
||||
$csvRows = [];
|
||||
|
||||
foreach ($transactions as $transaction) {
|
||||
$csvRows[] = implode(self::CSV_DELIMITER, [
|
||||
$this->escapeCsvValue($transaction->reff_no),
|
||||
$this->escapeCsvValue($transaction->pan),
|
||||
$this->escapeCsvValue($transaction->atm_id_terminal_id),
|
||||
$this->escapeCsvValue($transaction->amount),
|
||||
$this->escapeCsvValue($transaction->channel),
|
||||
$this->escapeCsvValue($transaction->account_no),
|
||||
$this->escapeCsvValue($transaction->internal_account),
|
||||
$this->escapeCsvValue($transaction->transaction_type),
|
||||
$this->escapeCsvValue($transaction->trans_ref),
|
||||
$this->escapeCsvValue($transaction->posting_date),
|
||||
$this->escapeCsvValue($transaction->stan),
|
||||
$this->escapeCsvValue($transaction->trans_status)
|
||||
]);
|
||||
$totalRecords++;
|
||||
}
|
||||
|
||||
if (!empty($csvRows)) {
|
||||
Storage::append($filePath, implode("\n", $csvRows));
|
||||
}
|
||||
});
|
||||
|
||||
// Update report log if exists
|
||||
if ($reportLog) {
|
||||
$reportLog->update([
|
||||
'status' => 'completed',
|
||||
'file_path' => $filePath,
|
||||
'file_size' => Storage::size($filePath),
|
||||
'record_count' => $totalRecords,
|
||||
]);
|
||||
}
|
||||
|
||||
Log::info("ATM Transaction report generated successfully. File: {$filePath}, Total records: {$totalRecords}");
|
||||
|
||||
} catch (Exception $e) {
|
||||
if ($reportLog) {
|
||||
$reportLog->update([
|
||||
'status' => 'failed',
|
||||
'error_message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
Log::error("Error generating ATM Transaction report for period {$this->period}: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape CSV values to handle commas and quotes
|
||||
*/
|
||||
private function escapeCsvValue($value): string
|
||||
{
|
||||
if ($value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$value = (string) $value;
|
||||
|
||||
// If value contains comma, quote, or newline, wrap in quotes and escape internal quotes
|
||||
if (strpos($value, self::CSV_DELIMITER) !== false ||
|
||||
strpos($value, '"') !== false ||
|
||||
strpos($value, "\n") !== false) {
|
||||
$value = '"' . str_replace('"', '""', $value) . '"';
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -175,6 +177,8 @@
|
||||
->whereNotNull('currency')
|
||||
->where('currency', '!=', '')
|
||||
->whereIn('ctdesc', $cardTypes)
|
||||
->whereNotIn('product_code',['6002','6004','6042','6031'])
|
||||
->where('branch','!=','ID0019999')
|
||||
->get();
|
||||
}
|
||||
|
||||
@@ -413,4 +417,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,140 +1,269 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
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\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Webstatement\Models\Account;
|
||||
use Modules\Webstatement\Models\AccountBalance;
|
||||
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\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Webstatement\Models\Account;
|
||||
use Modules\Webstatement\Models\AccountBalance;
|
||||
|
||||
class ProcessAccountDataJob implements ShouldQueue
|
||||
class ProcessAccountDataJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.ACCOUNT.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
|
||||
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
private int $errorCount = 0;
|
||||
|
||||
private $balanceData = [];
|
||||
private $accountBatch = [];
|
||||
private $balanceBatch = [];
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(string $period = '')
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
$this->period = $period;
|
||||
}
|
||||
|
||||
protected $periods;
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle()
|
||||
: void
|
||||
{
|
||||
try {
|
||||
$this->initializeJob();
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(array $periods = [])
|
||||
{
|
||||
$this->periods = $periods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
set_time_limit(24 * 60 * 60);
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
$processedCount = 0;
|
||||
$errorCount = 0;
|
||||
|
||||
if (empty($this->periods)) {
|
||||
Log::warning('No periods provided for account data processing');
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->periods as $period) {
|
||||
// Skip the _parameter folder
|
||||
if ($period === '_parameter') {
|
||||
Log::info("Skipping _parameter folder");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Construct the filename based on the period folder name
|
||||
$filename = "$period.ST.ACCOUNT.csv";
|
||||
$filePath = "$period/$filename";
|
||||
|
||||
Log::info("Processing account file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a temporary local copy of the file
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
|
||||
if ($handle !== false) {
|
||||
$headers = (new Account())->getFillable();
|
||||
Log::info('Headers: ' . implode(", ", $headers));
|
||||
$rowCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, "~")) !== false) {
|
||||
$rowCount++;
|
||||
|
||||
if (count($headers) === count($row)) {
|
||||
$data = array_combine($headers, $row);
|
||||
|
||||
// Check if start_year_bal is empty and set it to 0 if so
|
||||
if (empty($data['start_year_bal']) || $data['start_year_bal'] == "" || $data['start_year_bal'] == null) {
|
||||
$data['start_year_bal'] = 0;
|
||||
}
|
||||
|
||||
if (empty($data['closure_date']) || $data['closure_date'] == "" || $data['closure_date'] == null) {
|
||||
$data['closure_date'] = null;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($data['account_number'] !== 'account_number') {
|
||||
// Use firstOrNew instead of updateOrCreate
|
||||
$account = Account::firstOrNew(['account_number' => $data['account_number']]);
|
||||
$account->fill($data);
|
||||
$account->save();
|
||||
|
||||
// Store the opening balances in the AccountBalance model for this period
|
||||
if (isset($data['open_actual_bal']) || isset($data['open_cleared_bal'])) {
|
||||
$accountBalance = AccountBalance::firstOrNew([
|
||||
'account_number' => $data['account_number'],
|
||||
'period' => $period
|
||||
]);
|
||||
|
||||
// Set the balances
|
||||
$accountBalance->actual_balance = $data['open_actual_bal'] ?? 0;
|
||||
$accountBalance->cleared_balance = $data['open_cleared_bal'] ?? 0;
|
||||
|
||||
$accountBalance->save();
|
||||
Log::info("Saved balance for account {$data['account_number']} for period $period");
|
||||
}
|
||||
|
||||
$processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$errorCount++;
|
||||
Log::error("Error processing Account at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " . count($headers) . ", Got: " . count($row));
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed $processedCount records with $errorCount errors.");
|
||||
|
||||
// Clean up the temporary file
|
||||
unlink($tempFilePath);
|
||||
} else {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
}
|
||||
}
|
||||
|
||||
Log::info("Account data processing completed. Total processed: $processedCount, Total errors: $errorCount");
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessAccountDataJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
if ($this->period === '') {
|
||||
Log::warning('No period provided for account data processing');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processPeriod();
|
||||
$this->logJobCompletion();
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessAccountDataJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function initializeJob()
|
||||
: void
|
||||
{
|
||||
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||
$this->processedCount = 0;
|
||||
$this->errorCount = 0;
|
||||
$this->accountBatch = [];
|
||||
$this->balanceBatch = [];
|
||||
}
|
||||
|
||||
private function processPeriod()
|
||||
: void
|
||||
{
|
||||
$disk = Storage::disk(self::DISK_NAME);
|
||||
$filename = "{$this->period}." . self::FILENAME;
|
||||
$filePath = "{$this->period}/$filename";
|
||||
|
||||
if (!$this->validateFile($disk, $filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tempFilePath = $this->createTemporaryFile($disk, $filePath, $filename);
|
||||
$this->processFile($tempFilePath, $filePath);
|
||||
$this->cleanup($tempFilePath);
|
||||
}
|
||||
|
||||
private function validateFile($disk, string $filePath)
|
||||
: bool
|
||||
{
|
||||
Log::info("Processing account file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function createTemporaryFile($disk, string $filePath, string $filename)
|
||||
: string
|
||||
{
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
return $tempFilePath;
|
||||
}
|
||||
|
||||
private function processFile(string $tempFilePath, string $filePath)
|
||||
: void
|
||||
{
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
if ($handle === false) {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
return;
|
||||
}
|
||||
|
||||
$headers = (new Account())->getFillable();
|
||||
Log::info('Headers: ' . implode(", ", $headers));
|
||||
$rowCount = 0;
|
||||
$chunkCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||
$rowCount++;
|
||||
$this->processRow($row, $headers, $rowCount, $filePath);
|
||||
|
||||
// Process in chunks to avoid memory issues
|
||||
if (count($this->accountBatch) >= self::CHUNK_SIZE) {
|
||||
$this->saveBatch();
|
||||
$chunkCount++;
|
||||
Log::info("Processed chunk $chunkCount ({$this->processedCount} records so far)");
|
||||
}
|
||||
}
|
||||
|
||||
// Process any remaining records
|
||||
if (!empty($this->accountBatch) || !empty($this->balanceBatch)) {
|
||||
$this->saveBatch();
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors.");
|
||||
}
|
||||
|
||||
private function processRow(array $row, array $headers, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
if (count($headers) !== count($row)) {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||
count($headers) . ", Got: " . count($row));
|
||||
return;
|
||||
}
|
||||
|
||||
$data = array_combine($headers, $row);
|
||||
$this->normalizeData($data);
|
||||
$this->addToBatch($data, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
private function normalizeData(array &$data)
|
||||
: void
|
||||
{
|
||||
// Check if start_year_bal is empty and set it to 0 if so
|
||||
if (empty($data['start_year_bal']) || $data['start_year_bal'] == "" || $data['start_year_bal'] == null) {
|
||||
$data['start_year_bal'] = 0;
|
||||
}
|
||||
|
||||
if (empty($data['closure_date']) || $data['closure_date'] == "" || $data['closure_date'] == null) {
|
||||
$data['closure_date'] = null;
|
||||
}
|
||||
|
||||
// Store balance data separately before removing from Account data
|
||||
$this->balanceData = [
|
||||
'open_actual_bal' => empty($data['open_actual_bal']) ? 0 : $data['open_actual_bal'],
|
||||
'open_cleared_bal' => empty($data['open_cleared_bal']) ? 0 : $data['open_cleared_bal'],
|
||||
];
|
||||
|
||||
// Remove balance fields from Account data
|
||||
unset($data['open_actual_bal']);
|
||||
unset($data['open_cleared_bal']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add record to batch instead of saving immediately
|
||||
*/
|
||||
private function addToBatch(array $data, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
try {
|
||||
if ($data['account_number'] !== 'account_number') {
|
||||
// Add timestamp fields
|
||||
$now = now();
|
||||
$data['created_at'] = $now;
|
||||
$data['updated_at'] = $now;
|
||||
|
||||
// Add to account batch
|
||||
$this->accountBatch[] = $data;
|
||||
|
||||
// Add to balance batch
|
||||
if (isset($this->balanceData['open_actual_bal']) || isset($this->balanceData['open_cleared_bal'])) {
|
||||
$this->balanceBatch[] = [
|
||||
'account_number' => $data['account_number'],
|
||||
'period' => $this->period,
|
||||
'actual_balance' => $this->balanceData['open_actual_bal'],
|
||||
'cleared_balance' => $this->balanceData['open_cleared_bal'],
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now
|
||||
];
|
||||
}
|
||||
|
||||
$this->processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->errorCount++;
|
||||
Log::error("Error processing Account at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save batched records to the database
|
||||
*/
|
||||
private function saveBatch()
|
||||
: void
|
||||
{
|
||||
try {
|
||||
if (!empty($this->accountBatch)) {
|
||||
// Bulk insert/update accounts
|
||||
Account::upsert(
|
||||
$this->accountBatch,
|
||||
['account_number'], // Unique key
|
||||
array_diff((new Account())->getFillable(), ['account_number']) // Update columns
|
||||
);
|
||||
|
||||
// Reset account batch after processing
|
||||
$this->accountBatch = [];
|
||||
}
|
||||
|
||||
if (!empty($this->balanceBatch)) {
|
||||
// Bulk insert/update account balances
|
||||
AccountBalance::upsert(
|
||||
$this->balanceBatch,
|
||||
['account_number', 'period'], // Composite unique key
|
||||
['actual_balance', 'cleared_balance', 'updated_at'] // Update columns
|
||||
);
|
||||
|
||||
// Reset balance batch after processing
|
||||
$this->balanceBatch = [];
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error in saveBatch: " . $e->getMessage());
|
||||
$this->errorCount += count($this->accountBatch);
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanup(string $tempFilePath)
|
||||
: void
|
||||
{
|
||||
if (file_exists($tempFilePath)) {
|
||||
unlink($tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private function logJobCompletion()
|
||||
: void
|
||||
{
|
||||
Log::info("Account data processing completed. " .
|
||||
"Total processed: {$this->processedCount}, Total errors: {$this->errorCount}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,97 +16,202 @@
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $periods;
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.AA.ARRANGEMENT.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
|
||||
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
private int $errorCount = 0;
|
||||
private array $arrangementBatch = [];
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(array $periods = [])
|
||||
public function __construct(string $period = '')
|
||||
{
|
||||
$this->periods = $periods;
|
||||
$this->period = $period;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
public function handle()
|
||||
: void
|
||||
{
|
||||
try {
|
||||
set_time_limit(24 * 60 * 60);
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
$processedCount = 0;
|
||||
$errorCount = 0;
|
||||
$this->initializeJob();
|
||||
|
||||
if (empty($this->periods)) {
|
||||
Log::warning('No periods provided for arrangement data processing');
|
||||
if ($this->period === '') {
|
||||
Log::warning('No period provided for arrangement data processing');
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->periods as $period) {
|
||||
// Skip the _parameter folder
|
||||
if ($period === '_parameter') {
|
||||
Log::info("Skipping _parameter folder");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Construct the filename based on the period folder name
|
||||
$filename = "$period.ST.AA.ARRANGEMENT.csv";
|
||||
$filePath = "$period/$filename";
|
||||
|
||||
Log::info("Processing arrangement file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a temporary local copy of the file
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
|
||||
if ($handle !== false) {
|
||||
$headers = (new TempArrangement())->getFillable();
|
||||
$rowCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, "~")) !== false) {
|
||||
$rowCount++;
|
||||
|
||||
if (count($headers) === count($row)) {
|
||||
$data = array_combine($headers, $row);
|
||||
try {
|
||||
if ($data['arrangement_id'] !== 'arrangement_id') {
|
||||
TempArrangement::updateOrCreate(
|
||||
['arrangement_id' => $data['arrangement_id']], // key to find existing record
|
||||
$data // data to update or create
|
||||
);
|
||||
$processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$errorCount++;
|
||||
Log::error("Error processing Arrangement at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " . count($headers) . ", Got: " . count($row));
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed $processedCount records with $errorCount errors.");
|
||||
|
||||
// Clean up the temporary file
|
||||
unlink($tempFilePath);
|
||||
} else {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
}
|
||||
}
|
||||
|
||||
Log::info("Arrangement data processing completed. Total processed: $processedCount, Total errors: $errorCount");
|
||||
|
||||
$this->processPeriod();
|
||||
$this->logJobCompletion();
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessArrangementDataJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function initializeJob()
|
||||
: void
|
||||
{
|
||||
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||
$this->processedCount = 0;
|
||||
$this->errorCount = 0;
|
||||
$this->arrangementBatch = [];
|
||||
}
|
||||
|
||||
private function processPeriod()
|
||||
: void
|
||||
{
|
||||
$disk = Storage::disk(self::DISK_NAME);
|
||||
$filename = "{$this->period}." . self::FILENAME;
|
||||
$filePath = "{$this->period}/$filename";
|
||||
|
||||
if (!$this->validateFile($disk, $filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tempFilePath = $this->createTemporaryFile($disk, $filePath, $filename);
|
||||
$this->processFile($tempFilePath, $filePath);
|
||||
$this->cleanup($tempFilePath);
|
||||
}
|
||||
|
||||
private function validateFile($disk, string $filePath)
|
||||
: bool
|
||||
{
|
||||
Log::info("Processing arrangement file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function createTemporaryFile($disk, string $filePath, string $filename)
|
||||
: string
|
||||
{
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
return $tempFilePath;
|
||||
}
|
||||
|
||||
private function processFile(string $tempFilePath, string $filePath)
|
||||
: void
|
||||
{
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
if ($handle === false) {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
return;
|
||||
}
|
||||
|
||||
$headers = (new TempArrangement())->getFillable();
|
||||
$rowCount = 0;
|
||||
$chunkCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||
$rowCount++;
|
||||
$this->processRow($row, $headers, $rowCount, $filePath);
|
||||
|
||||
// Process in chunks to avoid memory issues
|
||||
if (count($this->arrangementBatch) >= self::CHUNK_SIZE) {
|
||||
$this->saveBatch();
|
||||
$chunkCount++;
|
||||
Log::info("Processed chunk $chunkCount ({$this->processedCount} records so far)");
|
||||
}
|
||||
}
|
||||
|
||||
// Process any remaining records
|
||||
if (!empty($this->arrangementBatch)) {
|
||||
$this->saveBatch();
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors.");
|
||||
}
|
||||
|
||||
private function processRow(array $row, array $headers, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
if (count($headers) !== count($row)) {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||
count($headers) . ", Got: " . count($row));
|
||||
$this->errorCount++;
|
||||
return;
|
||||
}
|
||||
|
||||
$data = array_combine($headers, $row);
|
||||
$this->addToBatch($data, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add record to batch instead of saving immediately
|
||||
*/
|
||||
private function addToBatch(array $data, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
try {
|
||||
if ($data['arrangement_id'] !== 'arrangement_id') {
|
||||
// Add timestamp fields
|
||||
$now = now();
|
||||
$data['created_at'] = $now;
|
||||
$data['updated_at'] = $now;
|
||||
|
||||
// Add to batch
|
||||
$this->arrangementBatch[] = $data;
|
||||
$this->processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->errorCount++;
|
||||
Log::error("Error processing Arrangement at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save batched records to the database
|
||||
*/
|
||||
private function saveBatch()
|
||||
: void
|
||||
{
|
||||
try {
|
||||
if (!empty($this->arrangementBatch)) {
|
||||
// Bulk insert/update arrangements
|
||||
TempArrangement::upsert(
|
||||
$this->arrangementBatch,
|
||||
['arrangement_id'], // Unique key
|
||||
array_diff((new TempArrangement())->getFillable(), ['arrangement_id']) // Update columns
|
||||
);
|
||||
|
||||
// Reset batch after processing
|
||||
$this->arrangementBatch = [];
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error in saveBatch: " . $e->getMessage());
|
||||
$this->errorCount += count($this->arrangementBatch);
|
||||
// Reset batch even if there's an error to prevent reprocessing the same failed records
|
||||
$this->arrangementBatch = [];
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanup(string $tempFilePath)
|
||||
: void
|
||||
{
|
||||
if (file_exists($tempFilePath)) {
|
||||
unlink($tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private function logJobCompletion()
|
||||
: void
|
||||
{
|
||||
Log::info("Arrangement data processing completed. " .
|
||||
"Total processed: {$this->processedCount}, Total errors: {$this->errorCount}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,12 @@
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
private const PARAMETER_FOLDER = '_parameter';
|
||||
|
||||
// Konstanta untuk nilai-nilai statis
|
||||
private const FILE_EXTENSION = '.ST.ATM.csv';
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
private const HEADER_MAP = [
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.ATM.TRANSACTION.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
|
||||
private const HEADER_MAP = [
|
||||
'id' => 'transaction_id',
|
||||
'card_acc_id' => 'card_acc_id',
|
||||
'pan_number' => 'pan_number',
|
||||
@@ -42,163 +41,145 @@
|
||||
'proc_code' => 'proc_code'
|
||||
];
|
||||
|
||||
// Pemetaan bidang header ke kolom model
|
||||
protected array $periods;
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
private int $errorCount = 0;
|
||||
private array $atmTransactionBatch = [];
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(array $periods = [])
|
||||
public function __construct(string $period = '')
|
||||
{
|
||||
$this->periods = $periods;
|
||||
$this->period = $period;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
public function handle()
|
||||
: void
|
||||
{
|
||||
try {
|
||||
set_time_limit(24 * 60 * 60);
|
||||
$this->initializeJob();
|
||||
|
||||
if (empty($this->periods)) {
|
||||
Log::warning('No periods provided for ATM transaction data processing');
|
||||
if ($this->period === '') {
|
||||
Log::warning('No period provided for ATM transaction data processing');
|
||||
return;
|
||||
}
|
||||
|
||||
$stats = $this->processPeriods();
|
||||
|
||||
Log::info("ProcessAtmTransactionJob completed. Total processed: {$stats['processed']}, Total errors: {$stats['errors']}");
|
||||
$this->processPeriod();
|
||||
$this->logJobCompletion();
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error in ProcessAtmTransactionJob: " . $e->getMessage());
|
||||
Log::error('Error in ProcessAtmTransactionJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process all periods and return statistics
|
||||
*/
|
||||
private function processPeriods(): array
|
||||
private function initializeJob()
|
||||
: void
|
||||
{
|
||||
$disk = Storage::disk(self::DISK_NAME);
|
||||
$processedCount = 0;
|
||||
$errorCount = 0;
|
||||
|
||||
foreach ($this->periods as $period) {
|
||||
// Skip the parameter folder
|
||||
if ($period === self::PARAMETER_FOLDER) {
|
||||
Log::info("Skipping " . self::PARAMETER_FOLDER . " folder");
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = $this->processPeriodFile($disk, $period);
|
||||
$processedCount += $result['processed'];
|
||||
$errorCount += $result['errors'];
|
||||
}
|
||||
|
||||
return [
|
||||
'processed' => $processedCount,
|
||||
'errors' => $errorCount
|
||||
];
|
||||
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||
$this->processedCount = 0;
|
||||
$this->errorCount = 0;
|
||||
$this->atmTransactionBatch = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single period file
|
||||
*/
|
||||
private function processPeriodFile($disk, string $period): array
|
||||
private function processPeriod()
|
||||
: void
|
||||
{
|
||||
$filename = $period . self::FILE_EXTENSION;
|
||||
$filePath = "$period/$filename";
|
||||
$processedCount = 0;
|
||||
$errorCount = 0;
|
||||
$disk = Storage::disk(self::DISK_NAME);
|
||||
$filename = "{$this->period}." . self::FILENAME;
|
||||
$filePath = "{$this->period}/$filename";
|
||||
|
||||
if (!$this->validateFile($disk, $filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tempFilePath = $this->createTemporaryFile($disk, $filePath, $filename);
|
||||
$this->processFile($tempFilePath, $filePath);
|
||||
$this->cleanup($tempFilePath);
|
||||
}
|
||||
|
||||
private function validateFile($disk, string $filePath)
|
||||
: bool
|
||||
{
|
||||
Log::info("Processing ATM transaction file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
return ['processed' => 0, 'errors' => 0];
|
||||
return false;
|
||||
}
|
||||
|
||||
$tempFilePath = $this->createTempFile($disk, $filePath, $filename);
|
||||
|
||||
$result = $this->processCSVFile($tempFilePath, $filePath);
|
||||
$processedCount += $result['processed'];
|
||||
$errorCount += $result['errors'];
|
||||
|
||||
// Clean up the temporary file
|
||||
if (file_exists($tempFilePath)) {
|
||||
unlink($tempFilePath);
|
||||
}
|
||||
|
||||
Log::info("Completed processing $filePath. Processed {$result['processed']} records with {$result['errors']} errors.");
|
||||
|
||||
return [
|
||||
'processed' => $processedCount,
|
||||
'errors' => $errorCount
|
||||
];
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a temporary file for processing
|
||||
*/
|
||||
private function createTempFile($disk, string $filePath, string $filename): string
|
||||
private function createTemporaryFile($disk, string $filePath, string $filename)
|
||||
: string
|
||||
{
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
return $tempFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a CSV file and import data
|
||||
*/
|
||||
private function processCSVFile(string $tempFilePath, string $originalFilePath): array
|
||||
private function processFile(string $tempFilePath, string $filePath)
|
||||
: void
|
||||
{
|
||||
$processedCount = 0;
|
||||
$errorCount = 0;
|
||||
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
if ($handle === false) {
|
||||
Log::error("Unable to open file: $originalFilePath");
|
||||
return ['processed' => 0, 'errors' => 0];
|
||||
Log::error("Unable to open file: $filePath");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the headers from the first row
|
||||
$headerRow = fgetcsv($handle, 0, self::CSV_DELIMITER);
|
||||
if (!$headerRow) {
|
||||
fclose($handle);
|
||||
return ['processed' => 0, 'errors' => 0];
|
||||
return;
|
||||
}
|
||||
|
||||
$rowCount = 0;
|
||||
$chunkCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||
$rowCount++;
|
||||
$this->processRow($headerRow, $row, $rowCount, $filePath);
|
||||
|
||||
if (count($headerRow) !== count($row)) {
|
||||
Log::warning("Row $rowCount in $originalFilePath has incorrect column count. Expected: " . count($headerRow) . ", Got: " . count($row));
|
||||
continue;
|
||||
// Process in chunks to avoid memory issues
|
||||
if (count($this->atmTransactionBatch) >= self::CHUNK_SIZE) {
|
||||
$this->saveBatch();
|
||||
$chunkCount++;
|
||||
Log::info("Processed chunk $chunkCount ({$this->processedCount} records so far)");
|
||||
}
|
||||
}
|
||||
|
||||
$result = $this->processRow($headerRow, $row, $rowCount, $originalFilePath);
|
||||
$processedCount += $result['processed'];
|
||||
$errorCount += $result['errors'];
|
||||
// Process any remaining records
|
||||
if (!empty($this->atmTransactionBatch)) {
|
||||
$this->saveBatch();
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
return [
|
||||
'processed' => $processedCount,
|
||||
'errors' => $errorCount
|
||||
];
|
||||
Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single row from the CSV file
|
||||
*/
|
||||
private function processRow(array $headerRow, array $row, int $rowCount, string $filePath): array
|
||||
private function processRow(array $headerRow, array $row, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
if (count($headerRow) !== count($row)) {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||
count($headerRow) . ", Got: " . count($row));
|
||||
$this->errorCount++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Combine the header row with the data row
|
||||
$rawData = array_combine($headerRow, $row);
|
||||
$this->mapAndAddToBatch($rawData, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
private function mapAndAddToBatch(array $rawData, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
// Map the raw data to our model fields
|
||||
$data = [];
|
||||
foreach (self::HEADER_MAP as $csvField => $modelField) {
|
||||
@@ -207,29 +188,76 @@
|
||||
|
||||
// Skip header row if it was included in the data
|
||||
if ($data['transaction_id'] === 'id') {
|
||||
return ['processed' => 0, 'errors' => 0];
|
||||
return;
|
||||
}
|
||||
|
||||
$this->addToBatch($data, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add record to batch instead of saving immediately
|
||||
*/
|
||||
private function addToBatch(array $data, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
try {
|
||||
// Format dates if needed
|
||||
/*if (!empty($data['booking_date'])) {
|
||||
$data['booking_date'] = date('Y-m-d H:i:s', strtotime($data['booking_date']));
|
||||
}
|
||||
// Add timestamp fields
|
||||
$now = now();
|
||||
$data['created_at'] = $now;
|
||||
$data['updated_at'] = $now;
|
||||
|
||||
if (!empty($data['value_date'])) {
|
||||
$data['value_date'] = date('Y-m-d H:i:s', strtotime($data['value_date']));
|
||||
}*/
|
||||
|
||||
// Create or update the record
|
||||
AtmTransaction::updateOrCreate(
|
||||
['transaction_id' => $data['transaction_id']],
|
||||
$data
|
||||
);
|
||||
|
||||
return ['processed' => 1, 'errors' => 0];
|
||||
// Add to batch
|
||||
$this->atmTransactionBatch[] = $data;
|
||||
$this->processedCount++;
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error processing row $rowCount in $filePath: " . $e->getMessage());
|
||||
return ['processed' => 0, 'errors' => 1];
|
||||
$this->errorCount++;
|
||||
Log::error("Error processing ATM Transaction at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save batched records to the database
|
||||
*/
|
||||
private function saveBatch()
|
||||
: void
|
||||
{
|
||||
try {
|
||||
if (!empty($this->atmTransactionBatch)) {
|
||||
// Process in smaller chunks for better memory management
|
||||
foreach ($this->atmTransactionBatch as $entry) {
|
||||
// Extract all stmt_entry_ids from the current chunk
|
||||
$entryIds = array_column($entry, 'transaction_id');
|
||||
|
||||
// Delete existing records with these IDs to avoid conflicts
|
||||
AtmTransaction::whereIn('transaction_id', $entryIds)->delete();
|
||||
|
||||
// Insert all records in the chunk at once
|
||||
AtmTransaction::insert($entry);
|
||||
}
|
||||
|
||||
// Reset entry batch after processing
|
||||
$this->atmTransactionBatch = [];
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error in saveBatch: " . $e->getMessage());
|
||||
$this->errorCount += count($this->atmTransactionBatch);
|
||||
// Reset batch even if there's an error to prevent reprocessing the same failed records
|
||||
$this->atmTransactionBatch = [];
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanup(string $tempFilePath)
|
||||
: void
|
||||
{
|
||||
if (file_exists($tempFilePath)) {
|
||||
unlink($tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private function logJobCompletion()
|
||||
: void
|
||||
{
|
||||
Log::info("ATM transaction data processing completed. " .
|
||||
"Total processed: {$this->processedCount}, Total errors: {$this->errorCount}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,112 +1,222 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Webstatement\Models\TempBillDetail;
|
||||
use Exception;
|
||||
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\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Webstatement\Models\TempBillDetail;
|
||||
|
||||
class ProcessBillDetailDataJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $periods;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(array $periods = [])
|
||||
class ProcessBillDetailDataJob implements ShouldQueue
|
||||
{
|
||||
$this->periods = $periods;
|
||||
}
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
set_time_limit(24 * 60 * 60);
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
$processedCount = 0;
|
||||
$errorCount = 0;
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.AA.BILL.DETAILS.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
|
||||
|
||||
if (empty($this->periods)) {
|
||||
Log::warning('No periods provided for bill detail data processing');
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
private int $errorCount = 0;
|
||||
private array $billDetailBatch = [];
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(string $period = '')
|
||||
{
|
||||
$this->period = $period;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle()
|
||||
: void
|
||||
{
|
||||
try {
|
||||
$this->initializeJob();
|
||||
|
||||
if ($this->period === '') {
|
||||
Log::warning('No period provided for bill detail data processing');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processPeriod();
|
||||
$this->logJobCompletion();
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessBillDetailDataJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function initializeJob()
|
||||
: void
|
||||
{
|
||||
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||
$this->processedCount = 0;
|
||||
$this->errorCount = 0;
|
||||
$this->billDetailBatch = [];
|
||||
}
|
||||
|
||||
private function processPeriod()
|
||||
: void
|
||||
{
|
||||
$disk = Storage::disk(self::DISK_NAME);
|
||||
$filename = "{$this->period}." . self::FILENAME;
|
||||
$filePath = "{$this->period}/$filename";
|
||||
|
||||
if (!$this->validateFile($disk, $filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->periods as $period) {
|
||||
// Skip the _parameter folder
|
||||
if ($period === '_parameter') {
|
||||
Log::info("Skipping _parameter folder");
|
||||
continue;
|
||||
}
|
||||
$tempFilePath = $this->createTemporaryFile($disk, $filePath, $filename);
|
||||
$this->processFile($tempFilePath, $filePath);
|
||||
$this->cleanup($tempFilePath);
|
||||
}
|
||||
|
||||
// Construct the filename based on the period folder name
|
||||
$filename = "$period.ST.AA.BILL.DETAILS.csv";
|
||||
$filePath = "$period/$filename";
|
||||
private function validateFile($disk, string $filePath)
|
||||
: bool
|
||||
{
|
||||
Log::info("Processing bill detail file: $filePath");
|
||||
|
||||
Log::info("Processing bill detail file: $filePath");
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create a temporary local copy of the file
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
private function createTemporaryFile($disk, string $filePath, string $filename)
|
||||
: string
|
||||
{
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
return $tempFilePath;
|
||||
}
|
||||
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
private function processFile(string $tempFilePath, string $filePath)
|
||||
: void
|
||||
{
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
if ($handle === false) {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
return;
|
||||
}
|
||||
|
||||
if ($handle !== false) {
|
||||
$headers = (new TempBillDetail())->getFillable();
|
||||
$rowCount = 0;
|
||||
$headers = (new TempBillDetail())->getFillable();
|
||||
$rowCount = 0;
|
||||
$chunkCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, "~")) !== false) {
|
||||
$rowCount++;
|
||||
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||
$rowCount++;
|
||||
$this->processRow($row, $headers, $rowCount, $filePath);
|
||||
|
||||
if (count($headers) === count($row)) {
|
||||
$data = array_combine($headers, $row);
|
||||
try {
|
||||
if (isset($data['_id']) && $data['_id'] !== '_id') {
|
||||
TempBillDetail::updateOrCreate(
|
||||
['_id' => $data['_id']], // Fixed the syntax error here
|
||||
$data
|
||||
);
|
||||
$processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$errorCount++;
|
||||
Log::error("Error processing Bill Detail at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " . count($headers) . ", Got: " . count($row));
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed $processedCount records with $errorCount errors.");
|
||||
|
||||
// Clean up the temporary file
|
||||
unlink($tempFilePath);
|
||||
} else {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
// Process in chunks to avoid memory issues
|
||||
if (count($this->billDetailBatch) >= self::CHUNK_SIZE) {
|
||||
$this->saveBatch();
|
||||
$chunkCount++;
|
||||
Log::info("Processed chunk $chunkCount ({$this->processedCount} records so far)");
|
||||
}
|
||||
}
|
||||
|
||||
Log::info("Bill Detail data processing completed. Total processed: $processedCount, Total errors: $errorCount");
|
||||
// Process any remaining records
|
||||
if (!empty($this->billDetailBatch)) {
|
||||
$this->saveBatch();
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessBillDetailDataJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors.");
|
||||
}
|
||||
|
||||
private function processRow(array $row, array $headers, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
if (count($headers) !== count($row)) {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||
count($headers) . ", Got: " . count($row));
|
||||
$this->errorCount++;
|
||||
return;
|
||||
}
|
||||
|
||||
$data = array_combine($headers, $row);
|
||||
$this->addToBatch($data, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add record to batch instead of saving immediately
|
||||
*/
|
||||
private function addToBatch(array $data, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
try {
|
||||
if (isset($data['_id']) && $data['_id'] !== '_id') {
|
||||
// Add timestamp fields
|
||||
$now = now();
|
||||
$data['created_at'] = $now;
|
||||
$data['updated_at'] = $now;
|
||||
|
||||
// Add to batch
|
||||
$this->billDetailBatch[] = $data;
|
||||
$this->processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->errorCount++;
|
||||
Log::error("Error processing Bill Detail at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save batched records to the database
|
||||
*/
|
||||
private function saveBatch()
|
||||
: void
|
||||
{
|
||||
try {
|
||||
if (!empty($this->billDetailBatch)) {
|
||||
// Process in smaller chunks for better memory management
|
||||
foreach ($this->billDetailBatch as $entry) {
|
||||
// Extract all stmt_entry_ids from the current chunk
|
||||
$entryIds = array_column($entry, '_id');
|
||||
|
||||
// Delete existing records with these IDs to avoid conflicts
|
||||
TempBillDetail::whereIn('_id', $entryIds)->delete();
|
||||
|
||||
// Insert all records in the chunk at once
|
||||
TempBillDetail::insert($entry);
|
||||
}
|
||||
|
||||
// Reset entry batch after processing
|
||||
$this->billDetailBatch = [];
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error in saveBatch: " . $e->getMessage());
|
||||
$this->errorCount += count($this->billDetailBatch);
|
||||
// Reset batch even if there's an error to prevent reprocessing the same failed records
|
||||
$this->billDetailBatch = [];
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanup(string $tempFilePath)
|
||||
: void
|
||||
{
|
||||
if (file_exists($tempFilePath)) {
|
||||
unlink($tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private function logJobCompletion()
|
||||
: void
|
||||
{
|
||||
Log::info("Bill Detail data processing completed. " .
|
||||
"Total processed: {$this->processedCount}, Total errors: {$this->errorCount}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,14 +16,33 @@
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $periods;
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.CATEGORY.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
private const HEADER_MAP = [
|
||||
'id' => 'id_category',
|
||||
'date_time' => 'date_time',
|
||||
'description' => 'description',
|
||||
'short_name' => 'short_name',
|
||||
'system_ind' => 'system_ind',
|
||||
'record_status' => 'record_status',
|
||||
'co_code' => 'co_code',
|
||||
'curr_no' => 'curr_no',
|
||||
'l_db_cr_ind' => 'l_db_cr_ind',
|
||||
'category_code' => 'category_code'
|
||||
];
|
||||
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
private int $errorCount = 0;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(array $periods = [])
|
||||
public function __construct(string $period = '')
|
||||
{
|
||||
$this->periods = $periods;
|
||||
$this->period = $period;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,106 +52,151 @@
|
||||
: void
|
||||
{
|
||||
try {
|
||||
set_time_limit(24 * 60 * 60);
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
$processedCount = 0;
|
||||
$errorCount = 0;
|
||||
$this->initializeJob();
|
||||
|
||||
if (empty($this->periods)) {
|
||||
Log::warning('No periods provided for category data processing');
|
||||
if ($this->period === '') {
|
||||
Log::warning('No period provided for category data processing');
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->periods as $period) {
|
||||
// Skip the _parameter folder
|
||||
if ($period === '_parameter') {
|
||||
Log::info("Skipping _parameter folder");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Construct the filename based on the period folder name
|
||||
$filename = "$period.ST.CATEGORY.csv";
|
||||
$filePath = "$period/$filename";
|
||||
|
||||
Log::info("Processing category file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a temporary local copy of the file
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
|
||||
if ($handle !== false) {
|
||||
// Get the headers from the first row
|
||||
$headerRow = fgetcsv($handle, 0, "~");
|
||||
|
||||
// Map the headers to our model fields
|
||||
$headerMap = [
|
||||
'id' => 'id_category',
|
||||
'date_time' => 'date_time',
|
||||
'description' => 'description',
|
||||
'short_name' => 'short_name',
|
||||
'system_ind' => 'system_ind',
|
||||
'record_status' => 'record_status',
|
||||
'co_code' => 'co_code',
|
||||
'curr_no' => 'curr_no',
|
||||
'l_db_cr_ind' => 'l_db_cr_ind',
|
||||
'category_code' => 'category_code'
|
||||
];
|
||||
|
||||
$rowCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, "~")) !== false) {
|
||||
$rowCount++;
|
||||
|
||||
if (count($headerRow) === count($row)) {
|
||||
// Combine the header row with the data row
|
||||
$rawData = array_combine($headerRow, $row);
|
||||
|
||||
// Map the raw data to our model fields
|
||||
$data = [];
|
||||
foreach ($headerMap as $csvField => $modelField) {
|
||||
$data[$modelField] = $rawData[$csvField] ?? null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Skip header row if it was included in the data
|
||||
if ($data['id_category'] !== 'id') {
|
||||
// Use firstOrNew instead of updateOrCreate
|
||||
$category = Category::firstOrNew(['id_category' => $data['id_category']]);
|
||||
$category->fill($data);
|
||||
$category->save();
|
||||
$processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$errorCount++;
|
||||
Log::error("Error processing Category at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " . count($headerRow) . ", Got: " . count($row));
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed $processedCount records with $errorCount errors.");
|
||||
|
||||
// Clean up the temporary file
|
||||
unlink($tempFilePath);
|
||||
} else {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
}
|
||||
}
|
||||
|
||||
Log::info("Category data processing completed. Total processed: $processedCount, Total errors: $errorCount");
|
||||
|
||||
$this->processPeriod();
|
||||
$this->logJobCompletion();
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessCategoryDataJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function initializeJob()
|
||||
: void
|
||||
{
|
||||
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||
$this->processedCount = 0;
|
||||
$this->errorCount = 0;
|
||||
}
|
||||
|
||||
private function processPeriod()
|
||||
: void
|
||||
{
|
||||
$disk = Storage::disk(self::DISK_NAME);
|
||||
$filename = "{$this->period}." . self::FILENAME;
|
||||
$filePath = "{$this->period}/$filename";
|
||||
|
||||
if (!$this->validateFile($disk, $filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tempFilePath = $this->createTemporaryFile($disk, $filePath, $filename);
|
||||
$this->processFile($tempFilePath, $filePath);
|
||||
$this->cleanup($tempFilePath);
|
||||
}
|
||||
|
||||
private function validateFile($disk, string $filePath)
|
||||
: bool
|
||||
{
|
||||
Log::info("Processing category file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function createTemporaryFile($disk, string $filePath, string $filename)
|
||||
: string
|
||||
{
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
return $tempFilePath;
|
||||
}
|
||||
|
||||
private function processFile(string $tempFilePath, string $filePath)
|
||||
: void
|
||||
{
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
if ($handle === false) {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the headers from the first row
|
||||
$headerRow = fgetcsv($handle, 0, self::CSV_DELIMITER);
|
||||
if (!$headerRow) {
|
||||
fclose($handle);
|
||||
return;
|
||||
}
|
||||
|
||||
$rowCount = 0;
|
||||
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||
$rowCount++;
|
||||
$this->processRow($headerRow, $row, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors.");
|
||||
}
|
||||
|
||||
private function processRow(array $headerRow, array $row, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
if (count($headerRow) !== count($row)) {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||
count($headerRow) . ", Got: " . count($row));
|
||||
return;
|
||||
}
|
||||
|
||||
// Combine the header row with the data row
|
||||
$rawData = array_combine($headerRow, $row);
|
||||
$this->mapAndSaveRecord($rawData, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
private function mapAndSaveRecord(array $rawData, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
// Map the raw data to our model fields
|
||||
$data = [];
|
||||
foreach (self::HEADER_MAP as $csvField => $modelField) {
|
||||
$data[$modelField] = $rawData[$csvField] ?? null;
|
||||
}
|
||||
|
||||
// Skip header row if it was included in the data
|
||||
if ($data['id_category'] === 'id') {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->saveRecord($data, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
private function saveRecord(array $data, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
try {
|
||||
// Use firstOrNew instead of updateOrCreate
|
||||
$category = Category::firstOrNew(['id_category' => $data['id_category']]);
|
||||
$category->fill($data);
|
||||
$category->save();
|
||||
|
||||
$this->processedCount++;
|
||||
} catch (Exception $e) {
|
||||
$this->errorCount++;
|
||||
Log::error("Error processing Category at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanup(string $tempFilePath)
|
||||
: void
|
||||
{
|
||||
if (file_exists($tempFilePath)) {
|
||||
unlink($tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private function logJobCompletion()
|
||||
: void
|
||||
{
|
||||
Log::info("Category data processing completed. " .
|
||||
"Total processed: {$this->processedCount}, Total errors: {$this->errorCount}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,154 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Basicdata\Models\Branch;
|
||||
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\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Basicdata\Models\Branch;
|
||||
|
||||
class ProcessCompanyDataJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $periods;
|
||||
protected $filename;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(array $periods = [], string $filename = "ST.COMPANY.csv")
|
||||
class ProcessCompanyDataJob implements ShouldQueue
|
||||
{
|
||||
$this->periods = $periods;
|
||||
$this->filename = $filename;
|
||||
}
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
set_time_limit(24 * 60 * 60);
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
$processedCount = 0;
|
||||
$errorCount = 0;
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.COMPANY.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
private const FIELD_MAP = [
|
||||
'id' => null, // Not mapped to model
|
||||
'date_time' => null, // Not mapped to model
|
||||
'company_code' => 'code',
|
||||
'company_name' => 'name',
|
||||
'name_address' => 'address',
|
||||
'mnemonic' => 'mnemonic',
|
||||
'customer_company' => 'customer_company',
|
||||
'customer_mnemonic' => 'customer_mnemonic',
|
||||
'company_group' => 'company_group',
|
||||
'curr_no' => 'curr_no',
|
||||
'co_code' => 'co_code',
|
||||
'l_vendor_atm' => 'l_vendor_atm',
|
||||
'l_vendor_cpc' => 'l_vendor_cpc'
|
||||
];
|
||||
private const BOOLEAN_FIELDS = ['l_vendor_atm', 'l_vendor_cpc'];
|
||||
|
||||
if (empty($this->periods)) {
|
||||
Log::warning('No periods provided for company data processing');
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
private int $errorCount = 0;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(string $period = '')
|
||||
{
|
||||
$this->period = $period;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle()
|
||||
: void
|
||||
{
|
||||
try {
|
||||
$this->initializeJob();
|
||||
|
||||
if ($this->period === '') {
|
||||
Log::warning('No period provided for company data processing');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processPeriod();
|
||||
$this->logJobCompletion();
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessCompanyDataJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function initializeJob()
|
||||
: void
|
||||
{
|
||||
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||
$this->processedCount = 0;
|
||||
$this->errorCount = 0;
|
||||
}
|
||||
|
||||
private function processPeriod()
|
||||
: void
|
||||
{
|
||||
$disk = Storage::disk(self::DISK_NAME);
|
||||
$filename = "{$this->period}." . self::FILENAME;
|
||||
$filePath = "{$this->period}/$filename";
|
||||
|
||||
if (!$this->validateFile($disk, $filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->periods as $period) {
|
||||
// Skip the _parameter folder
|
||||
if ($period === '_parameter') {
|
||||
Log::info("Skipping _parameter folder");
|
||||
$tempFilePath = $this->createTemporaryFile($disk, $filePath, $filename);
|
||||
$this->processFile($tempFilePath, $filePath);
|
||||
$this->cleanup($tempFilePath);
|
||||
}
|
||||
|
||||
private function validateFile($disk, string $filePath)
|
||||
: bool
|
||||
{
|
||||
Log::info("Processing company file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function createTemporaryFile($disk, string $filePath, string $filename = self::FILENAME)
|
||||
: string
|
||||
{
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
return $tempFilePath;
|
||||
}
|
||||
|
||||
private function processFile(string $tempFilePath, string $filePath)
|
||||
: void
|
||||
{
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
if ($handle === false) {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
return;
|
||||
}
|
||||
|
||||
$rowCount = 0;
|
||||
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||
$rowCount++;
|
||||
|
||||
// Skip header row if it exists
|
||||
if ($rowCount === 1 && (strtolower($row[0]) === 'id' || strtolower($row[2]) === 'company_code')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Construct the filepath based on the period folder name
|
||||
$fileName = "$period.$this->filename";
|
||||
$filePath = "$period/$fileName";
|
||||
$this->processRow($row, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
Log::info("Processing company file: $filePath");
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors.");
|
||||
}
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
continue;
|
||||
}
|
||||
private function processRow(array $row, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
$csvHeaders = array_keys(self::FIELD_MAP);
|
||||
|
||||
// Create a temporary local copy of the file
|
||||
$tempFilePath = storage_path("app/temp_{$this->filename}");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
if (count($csvHeaders) !== count($row)) {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||
count($csvHeaders) . ", Got: " . count($row));
|
||||
return;
|
||||
}
|
||||
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
$csvData = array_combine($csvHeaders, $row);
|
||||
$this->mapAndSaveRecord($csvData, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
if ($handle !== false) {
|
||||
// CSV headers from the file
|
||||
$csvHeaders = [
|
||||
'id', 'date_time', 'company_code', 'company_name', 'name_address',
|
||||
'mnemonic', 'customer_company', 'customer_mnemonic', 'company_group',
|
||||
'curr_no', 'co_code', 'l_vendor_atm', 'l_vendor_cpc'
|
||||
];
|
||||
|
||||
// Field mapping from CSV to Branch model
|
||||
$fieldMapping = [
|
||||
'company_code' => 'code',
|
||||
'company_name' => 'name',
|
||||
'name_address' => 'address',
|
||||
'mnemonic' => 'mnemonic',
|
||||
'customer_company' => 'customer_company',
|
||||
'customer_mnemonic' => 'customer_mnemonic',
|
||||
'company_group' => 'company_group',
|
||||
'curr_no' => 'curr_no',
|
||||
'co_code' => 'co_code',
|
||||
'l_vendor_atm' => 'l_vendor_atm',
|
||||
'l_vendor_cpc' => 'l_vendor_cpc'
|
||||
];
|
||||
|
||||
$rowCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, "~")) !== false) {
|
||||
$rowCount++;
|
||||
|
||||
// Skip header row if it exists
|
||||
if ($rowCount === 1 && (strtolower($row[0]) === 'id' || strtolower($row[2]) === 'company_code')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count($csvHeaders) === count($row)) {
|
||||
$csvData = array_combine($csvHeaders, $row);
|
||||
|
||||
// Map CSV data to Branch model fields
|
||||
$branchData = [];
|
||||
foreach ($fieldMapping as $csvField => $modelField) {
|
||||
if (isset($csvData[$csvField])) {
|
||||
// Convert string boolean values to actual booleans for boolean fields
|
||||
if (in_array($modelField, ['l_vendor_atm', 'l_vendor_cpc'])) {
|
||||
$branchData[$modelField] = filter_var($csvData[$csvField], FILTER_VALIDATE_BOOLEAN);
|
||||
} else {
|
||||
$branchData[$modelField] = $csvData[$csvField];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!empty($branchData['code'])) {
|
||||
Branch::updateOrCreate(
|
||||
['code' => $branchData['code']],
|
||||
$branchData
|
||||
);
|
||||
$processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$errorCount++;
|
||||
Log::error("Error processing Company data at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " . count($csvHeaders) . ", Got: " . count($row));
|
||||
}
|
||||
private function mapAndSaveRecord(array $csvData, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
// Map CSV data to Branch model fields
|
||||
$branchData = [];
|
||||
foreach (self::FIELD_MAP as $csvField => $modelField) {
|
||||
if ($modelField !== null && isset($csvData[$csvField])) {
|
||||
// Convert string boolean values to actual booleans for boolean fields
|
||||
if (in_array($modelField, self::BOOLEAN_FIELDS)) {
|
||||
$branchData[$modelField] = filter_var($csvData[$csvField], FILTER_VALIDATE_BOOLEAN);
|
||||
} else {
|
||||
$branchData[$modelField] = $csvData[$csvField];
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed $processedCount records with $errorCount errors.");
|
||||
|
||||
// Clean up the temporary file
|
||||
unlink($tempFilePath);
|
||||
} else {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
}
|
||||
}
|
||||
|
||||
Log::info("Company data processing completed. Total processed: $processedCount, Total errors: $errorCount");
|
||||
$this->saveRecord($branchData, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessCompanyDataJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
private function saveRecord(array $branchData, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
try {
|
||||
if (!empty($branchData['code'])) {
|
||||
Branch::updateOrCreate(
|
||||
['code' => $branchData['code']],
|
||||
$branchData
|
||||
);
|
||||
$this->processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->errorCount++;
|
||||
Log::error("Error processing Company data at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanup(string $tempFilePath)
|
||||
: void
|
||||
{
|
||||
if (file_exists($tempFilePath)) {
|
||||
unlink($tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private function logJobCompletion()
|
||||
: void
|
||||
{
|
||||
Log::info("Company data processing completed. " .
|
||||
"Total processed: {$this->processedCount}, Total errors: {$this->errorCount}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,107 +1,216 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
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 Modules\Webstatement\Models\Customer;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
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\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Webstatement\Models\Customer;
|
||||
|
||||
class ProcessCustomerDataJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $periods;
|
||||
|
||||
public function __construct(array $periods = [])
|
||||
class ProcessCustomerDataJob implements ShouldQueue
|
||||
{
|
||||
$this->periods = $periods;
|
||||
}
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
set_time_limit(24 * 60 * 60);
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
$processedCount = 0;
|
||||
$errorCount = 0;
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.CUSTOMER.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
|
||||
|
||||
if (empty($this->periods)) {
|
||||
Log::warning('No periods provided for customer data processing');
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
private int $errorCount = 0;
|
||||
private array $customerBatch = [];
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(string $period = '')
|
||||
{
|
||||
$this->period = $period;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle()
|
||||
: void
|
||||
{
|
||||
try {
|
||||
$this->initializeJob();
|
||||
|
||||
if ($this->period === '') {
|
||||
Log::warning('No period provided for customer data processing');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processPeriod();
|
||||
$this->logJobCompletion();
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessCustomerDataJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function initializeJob()
|
||||
: void
|
||||
{
|
||||
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||
$this->processedCount = 0;
|
||||
$this->errorCount = 0;
|
||||
$this->customerBatch = [];
|
||||
}
|
||||
|
||||
private function processPeriod()
|
||||
: void
|
||||
{
|
||||
$disk = Storage::disk(self::DISK_NAME);
|
||||
$filename = "{$this->period}." . self::FILENAME;
|
||||
$filePath = "{$this->period}/$filename";
|
||||
|
||||
if (!$this->validateFile($disk, $filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->periods as $period) {
|
||||
$tempFilePath = $this->createTemporaryFile($disk, $filePath, $filename);
|
||||
$this->processFile($tempFilePath, $filePath);
|
||||
$this->cleanup($tempFilePath);
|
||||
}
|
||||
|
||||
// Skip the _parameter folder
|
||||
if ($period === '_parameter') {
|
||||
Log::info("Skipping _parameter folder");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Construct the filename based on the period folder name
|
||||
$filename = "$period.ST.CUSTOMER.csv";
|
||||
$filePath = "$period/$filename";
|
||||
|
||||
Log::info("Processing customer file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a temporary local copy of the file
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
|
||||
if ($handle !== false) {
|
||||
$headers = (new Customer())->getFillable();
|
||||
$rowCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, "~")) !== false) {
|
||||
$rowCount++;
|
||||
|
||||
if (count($headers) === count($row)) {
|
||||
$data = array_combine($headers, $row);
|
||||
try {
|
||||
if ($data['customer_code'] !== 'customer_code') {
|
||||
$customer = Customer::firstOrNew(['customer_code' => $data['customer_code']]);
|
||||
$customer->fill($data);
|
||||
$customer->save();
|
||||
$processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$errorCount++;
|
||||
Log::error("Error processing Customer at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " . count($headers) . ", Got: " . count($row));
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed $processedCount records with $errorCount errors.");
|
||||
|
||||
// Clean up the temporary file
|
||||
unlink($tempFilePath);
|
||||
} else {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
}
|
||||
private function validateFile($disk, string $filePath)
|
||||
: bool
|
||||
{
|
||||
Log::info("Processing customer file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
return false;
|
||||
}
|
||||
|
||||
Log::info("Customer data processing completed. Total processed: $processedCount, Total errors: $errorCount");
|
||||
return true;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessCustomerDataJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
private function createTemporaryFile($disk, string $filePath, string $filename)
|
||||
: string
|
||||
{
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
return $tempFilePath;
|
||||
}
|
||||
|
||||
private function processFile(string $tempFilePath, string $filePath)
|
||||
: void
|
||||
{
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
if ($handle === false) {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
return;
|
||||
}
|
||||
|
||||
$headers = (new Customer())->getFillable();
|
||||
$rowCount = 0;
|
||||
$chunkCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||
$rowCount++;
|
||||
$this->processRow($row, $headers, $rowCount, $filePath);
|
||||
|
||||
// Process in chunks to avoid memory issues
|
||||
if (count($this->customerBatch) >= self::CHUNK_SIZE) {
|
||||
$this->saveBatch();
|
||||
$chunkCount++;
|
||||
Log::info("Processed chunk $chunkCount ({$this->processedCount} records so far)");
|
||||
}
|
||||
}
|
||||
|
||||
// Process any remaining records
|
||||
if (!empty($this->customerBatch)) {
|
||||
$this->saveBatch();
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors.");
|
||||
}
|
||||
|
||||
private function processRow(array $row, array $headers, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
if (count($headers) !== count($row)) {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||
count($headers) . ", Got: " . count($row));
|
||||
return;
|
||||
}
|
||||
|
||||
$data = array_combine($headers, $row);
|
||||
$this->addToBatch($data, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add record to batch instead of saving immediately
|
||||
*/
|
||||
private function addToBatch(array $data, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
try {
|
||||
if (isset($data['customer_code']) && $data['customer_code'] !== 'customer_code') {
|
||||
// Add timestamp fields
|
||||
$now = now();
|
||||
$data['created_at'] = $now;
|
||||
$data['updated_at'] = $now;
|
||||
|
||||
// Add to customer batch
|
||||
$this->customerBatch[] = $data;
|
||||
$this->processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->errorCount++;
|
||||
Log::error("Error processing Customer at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save batched records to the database
|
||||
*/
|
||||
private function saveBatch()
|
||||
: void
|
||||
{
|
||||
try {
|
||||
if (!empty($this->customerBatch)) {
|
||||
// Bulk insert/update customers
|
||||
Customer::upsert(
|
||||
$this->customerBatch,
|
||||
['customer_code'], // Unique key
|
||||
array_diff((new Customer())->getFillable(), ['customer_code']) // Update columns
|
||||
);
|
||||
|
||||
// Reset customer batch after processing
|
||||
$this->customerBatch = [];
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error in saveBatch: " . $e->getMessage());
|
||||
$this->errorCount += count($this->customerBatch);
|
||||
// Reset batch even if there's an error to prevent reprocessing the same failed records
|
||||
$this->customerBatch = [];
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanup(string $tempFilePath)
|
||||
: void
|
||||
{
|
||||
if (file_exists($tempFilePath)) {
|
||||
unlink($tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private function logJobCompletion()
|
||||
: void
|
||||
{
|
||||
Log::info("Customer data processing completed. " .
|
||||
"Total processed: {$this->processedCount}, Total errors: {$this->errorCount}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,16 +16,63 @@
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $periods;
|
||||
protected $filename;
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.DATA.CAPTURE.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
|
||||
private const CSV_HEADERS = [
|
||||
'id',
|
||||
'account_number',
|
||||
'sign',
|
||||
'amount_lcy',
|
||||
'transaction_code',
|
||||
'their_reference',
|
||||
'narrative',
|
||||
'pl_category',
|
||||
'customer_id',
|
||||
'account_officer',
|
||||
'product_category',
|
||||
'value_date',
|
||||
'currency',
|
||||
'amount_fcy',
|
||||
'exchange_rate',
|
||||
'neg_ref_no',
|
||||
'position_type',
|
||||
'our_reference',
|
||||
'reversal_marker',
|
||||
'exposure_date',
|
||||
'currency_market',
|
||||
'iblc_country',
|
||||
'last_version',
|
||||
'otor_version',
|
||||
'department_code',
|
||||
'dealer_desk',
|
||||
'bank_sort_cde',
|
||||
'cheque_number',
|
||||
'accounting_date',
|
||||
'contingent_acct',
|
||||
'cheq_type',
|
||||
'tfs_reference',
|
||||
'accounting_company',
|
||||
'stmt_no',
|
||||
'curr_no',
|
||||
'inputter',
|
||||
'authoriser',
|
||||
'co_code',
|
||||
'date_time'
|
||||
];
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
private int $errorCount = 0;
|
||||
private array $captureBatch = [];
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(array $periods = [], string $filename = "ST.DATA.CAPTURE.csv")
|
||||
public function __construct(string $period = '')
|
||||
{
|
||||
$this->periods = $periods;
|
||||
$this->filename = $filename;
|
||||
$this->period = $period;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,151 +82,180 @@
|
||||
: void
|
||||
{
|
||||
try {
|
||||
set_time_limit(24 * 60 * 60);
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
$processedCount = 0;
|
||||
$errorCount = 0;
|
||||
$this->initializeJob();
|
||||
|
||||
if (empty($this->periods)) {
|
||||
Log::warning('No periods provided for data capture processing');
|
||||
if ($this->period === '') {
|
||||
Log::warning('No period provided for data capture processing');
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->periods as $period) {
|
||||
// Skip the _parameter folder
|
||||
if ($period === '_parameter') {
|
||||
Log::info("Skipping _parameter folder");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Construct the filepath based on the period folder name
|
||||
$fileName = "$period.$this->filename";
|
||||
$filePath = "$period/$fileName";
|
||||
|
||||
Log::info("Processing data capture file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a temporary local copy of the file
|
||||
$tempFilePath = storage_path("app/temp_{$this->filename}");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
|
||||
if ($handle !== false) {
|
||||
// CSV headers from the file
|
||||
$csvHeaders = [
|
||||
'id',
|
||||
'account_number',
|
||||
'sign',
|
||||
'amount_lcy',
|
||||
'transaction_code',
|
||||
'their_reference',
|
||||
'narrative',
|
||||
'pl_category',
|
||||
'customer_id',
|
||||
'account_officer',
|
||||
'product_category',
|
||||
'value_date',
|
||||
'currency',
|
||||
'amount_fcy',
|
||||
'exchange_rate',
|
||||
'neg_ref_no',
|
||||
'position_type',
|
||||
'our_reference',
|
||||
'reversal_marker',
|
||||
'exposure_date',
|
||||
'currency_market',
|
||||
'iblc_country',
|
||||
'last_version',
|
||||
'otor_version',
|
||||
'department_code',
|
||||
'dealer_desk',
|
||||
'bank_sort_cde',
|
||||
'cheque_number',
|
||||
'accounting_date',
|
||||
'contingent_acct',
|
||||
'cheq_type',
|
||||
'tfs_reference',
|
||||
'accounting_company',
|
||||
'stmt_no',
|
||||
'curr_no',
|
||||
'inputter',
|
||||
'authoriser',
|
||||
'co_code',
|
||||
'date_time'
|
||||
];
|
||||
|
||||
$rowCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, "~")) !== false) {
|
||||
$rowCount++;
|
||||
|
||||
// Skip header row if it exists
|
||||
if ($rowCount === 1 && strtolower($row[0]) === 'id') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count($csvHeaders) === count($row)) {
|
||||
$data = array_combine($csvHeaders, $row);
|
||||
|
||||
try {
|
||||
// Format dates if they exist
|
||||
foreach (['value_date', 'exposure_date', 'accounting_date'] as $dateField) {
|
||||
if (!empty($data[$dateField])) {
|
||||
try {
|
||||
$data[$dateField] = date('Y-m-d', strtotime($data[$dateField]));
|
||||
} catch (Exception $e) {
|
||||
// If date parsing fails, keep the original value
|
||||
Log::warning("Failed to parse date for $dateField: {$data[$dateField]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Format datetime if it exists
|
||||
if (!empty($data['date_time'])) {
|
||||
try {
|
||||
$data['date_time'] = date('Y-m-d H:i:s', strtotime($data['date_time']));
|
||||
} catch (Exception $e) {
|
||||
// If datetime parsing fails, keep the original value
|
||||
Log::warning("Failed to parse datetime for date_time: {$data['date_time']}");
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($data['id'])) {
|
||||
DataCapture::updateOrCreate(
|
||||
['id' => $data['id']],
|
||||
$data
|
||||
);
|
||||
$processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$errorCount++;
|
||||
Log::error("Error processing Data Capture at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " . count($csvHeaders) . ", Got: " . count($row));
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed $processedCount records with $errorCount errors.");
|
||||
|
||||
// Clean up the temporary file
|
||||
unlink($tempFilePath);
|
||||
} else {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
}
|
||||
}
|
||||
|
||||
Log::info("Data capture processing completed. Total processed: $processedCount, Total errors: $errorCount");
|
||||
|
||||
$this->processPeriod();
|
||||
$this->logJobCompletion();
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessDataCaptureDataJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function initializeJob()
|
||||
: void
|
||||
{
|
||||
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||
$this->processedCount = 0;
|
||||
$this->errorCount = 0;
|
||||
$this->captureBatch = [];
|
||||
}
|
||||
|
||||
private function processPeriod()
|
||||
: void
|
||||
{
|
||||
$disk = Storage::disk(self::DISK_NAME);
|
||||
$filename = "{$this->period}." . self::FILENAME;
|
||||
$filePath = "{$this->period}/$filename";
|
||||
|
||||
if (!$this->validateFile($disk, $filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tempFilePath = $this->createTemporaryFile($disk, $filePath, $filename);
|
||||
$this->processFile($tempFilePath, $filePath);
|
||||
$this->cleanup($tempFilePath);
|
||||
}
|
||||
|
||||
private function validateFile($disk, string $filePath)
|
||||
: bool
|
||||
{
|
||||
Log::info("Processing data capture file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function createTemporaryFile($disk, string $filePath, string $filename)
|
||||
: string
|
||||
{
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
return $tempFilePath;
|
||||
}
|
||||
|
||||
private function processFile(string $tempFilePath, string $filePath)
|
||||
: void
|
||||
{
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
if ($handle === false) {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
return;
|
||||
}
|
||||
|
||||
$rowCount = 0;
|
||||
$chunkCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||
$rowCount++;
|
||||
|
||||
// Skip header row if it exists
|
||||
if ($rowCount === 1 && strtolower($row[0]) === 'id') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->processRow($row, $rowCount, $filePath);
|
||||
|
||||
// Process in chunks to avoid memory issues
|
||||
if (count($this->captureBatch) >= self::CHUNK_SIZE) {
|
||||
$this->saveBatch();
|
||||
$chunkCount++;
|
||||
Log::info("Processed chunk $chunkCount ({$this->processedCount} records so far)");
|
||||
}
|
||||
}
|
||||
|
||||
// Process any remaining records
|
||||
if (!empty($this->captureBatch)) {
|
||||
$this->saveBatch();
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors.");
|
||||
}
|
||||
|
||||
private function processRow(array $row, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
if (count(self::CSV_HEADERS) !== count($row)) {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||
count(self::CSV_HEADERS) . ", Got: " . count($row));
|
||||
return;
|
||||
}
|
||||
|
||||
$data = array_combine(self::CSV_HEADERS, $row);
|
||||
$this->addToBatch($data, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add record to batch instead of saving immediately
|
||||
*/
|
||||
private function addToBatch(array $data, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
try {
|
||||
if (!empty($data['id'])) {
|
||||
// Add timestamp fields
|
||||
$now = now();
|
||||
$data['created_at'] = $now;
|
||||
$data['updated_at'] = $now;
|
||||
|
||||
// Add to capture batch
|
||||
$this->captureBatch[] = $data;
|
||||
$this->processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->errorCount++;
|
||||
Log::error("Error processing Data Capture at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save batched records to the database
|
||||
*/
|
||||
private function saveBatch()
|
||||
: void
|
||||
{
|
||||
try {
|
||||
if (!empty($this->captureBatch)) {
|
||||
// Bulk insert/update data captures
|
||||
DataCapture::upsert(
|
||||
$this->captureBatch,
|
||||
['id'], // Unique key
|
||||
array_diff(self::CSV_HEADERS, ['id']) // Update columns
|
||||
);
|
||||
|
||||
// Reset capture batch after processing
|
||||
$this->captureBatch = [];
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error in saveBatch: " . $e->getMessage());
|
||||
$this->errorCount += count($this->captureBatch);
|
||||
// Reset batch even if there's an error to prevent reprocessing the same failed records
|
||||
$this->captureBatch = [];
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanup(string $tempFilePath)
|
||||
: void
|
||||
{
|
||||
if (file_exists($tempFilePath)) {
|
||||
unlink($tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private function logJobCompletion()
|
||||
: void
|
||||
{
|
||||
Log::info("Data capture processing completed. " .
|
||||
"Total processed: {$this->processedCount}, Total errors: {$this->errorCount}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,118 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
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\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Webstatement\Models\FtTxnTypeCondition;
|
||||
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\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Webstatement\Models\FtTxnTypeCondition;
|
||||
|
||||
class ProcessFtTxnTypeConditionJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $periods;
|
||||
protected $filename;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(array $periods = [], string $filename = "ST.FT.TXN.TYPE.CONDITION.csv")
|
||||
class ProcessFtTxnTypeConditionJob implements ShouldQueue
|
||||
{
|
||||
$this->periods = $periods;
|
||||
$this->filename = $filename;
|
||||
}
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
set_time_limit(24 * 60 * 60);
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
$processedCount = 0;
|
||||
$errorCount = 0;
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const EXPECTED_HEADERS = [
|
||||
'id',
|
||||
'date_time',
|
||||
'transaction_type',
|
||||
'short_descr',
|
||||
'txn_code_cr',
|
||||
'txn_code_dr'
|
||||
];
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.FT.TXN.TYPE.CONDITION.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
|
||||
if (empty($this->periods)) {
|
||||
Log::warning('No periods provided for transaction type condition data processing');
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
private int $errorCount = 0;
|
||||
|
||||
public function __construct(string $period = '')
|
||||
{
|
||||
$this->period = $period;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
: void
|
||||
{
|
||||
try {
|
||||
$this->initializeJob();
|
||||
|
||||
if ($this->period === '') {
|
||||
Log::warning('No periods provided for transaction type condition data processing');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processPeriod();
|
||||
$this->logJobCompletion();
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessFtTxnTypeConditionJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function initializeJob()
|
||||
: void
|
||||
{
|
||||
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||
$this->processedCount = 0;
|
||||
$this->errorCount = 0;
|
||||
}
|
||||
|
||||
private function processPeriod()
|
||||
: void
|
||||
{
|
||||
$disk = Storage::disk(self::DISK_NAME);
|
||||
$filePath = "$this->period/" . self::FILENAME;
|
||||
|
||||
|
||||
if (!$this->validateFile($disk, $filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->periods as $period) {
|
||||
// Skip the _parameter folder
|
||||
/*if ($period === '_parameter') {
|
||||
Log::info("Skipping _parameter folder");
|
||||
continue;
|
||||
}*/
|
||||
$tempFilePath = $this->createTemporaryFile($disk, $filePath);
|
||||
$this->processFile($tempFilePath, $filePath);
|
||||
$this->cleanup($tempFilePath);
|
||||
}
|
||||
|
||||
// Construct the filepath based on the period folder name
|
||||
$filePath = "$period/{$this->filename}";
|
||||
private function validateFile($disk, string $filePath)
|
||||
: bool
|
||||
{
|
||||
Log::info("Processing transaction type condition file: $filePath");
|
||||
|
||||
Log::info("Processing transaction type condition file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a temporary local copy of the file
|
||||
$tempFilePath = storage_path("app/temp_{$this->filename}");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
|
||||
if ($handle !== false) {
|
||||
$headers = ['id', 'date_time', 'transaction_type', 'short_descr', 'txn_code_cr', 'txn_code_dr'];
|
||||
$rowCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, "~")) !== false) {
|
||||
$rowCount++;
|
||||
|
||||
// Skip header row if it exists
|
||||
if ($rowCount === 1 && strtolower($row[0]) === 'id') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count($headers) === count($row)) {
|
||||
$data = array_combine($headers, $row);
|
||||
try {
|
||||
if (!empty($data['id'])) {
|
||||
FtTxnTypeCondition::updateOrCreate(
|
||||
['id' => $data['id']],
|
||||
$data
|
||||
);
|
||||
$processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$errorCount++;
|
||||
Log::error("Error processing Transaction Type Condition at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " . count($headers) . ", Got: " . count($row));
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed $processedCount records with $errorCount errors.");
|
||||
|
||||
// Clean up the temporary file
|
||||
unlink($tempFilePath);
|
||||
} else {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
}
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
return false;
|
||||
}
|
||||
|
||||
Log::info("Transaction type condition data processing completed. Total processed: $processedCount, Total errors: $errorCount");
|
||||
return true;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessFtTxnTypeConditionJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
private function createTemporaryFile($disk, string $filePath)
|
||||
: string
|
||||
{
|
||||
$tempFilePath = storage_path("app/temp_" . self::FILENAME);
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
return $tempFilePath;
|
||||
}
|
||||
|
||||
private function processFile(string $tempFilePath, string $filePath)
|
||||
: void
|
||||
{
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
if ($handle === false) {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
return;
|
||||
}
|
||||
|
||||
$rowCount = 0;
|
||||
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||
$rowCount++;
|
||||
if ($this->isHeaderRow($rowCount, $row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->processRow($row, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors.");
|
||||
}
|
||||
|
||||
private function isHeaderRow(int $rowCount, array $row)
|
||||
: bool
|
||||
{
|
||||
return $rowCount === 1 && strtolower($row[0]) === 'id';
|
||||
}
|
||||
|
||||
private function processRow(array $row, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
if (count(self::EXPECTED_HEADERS) !== count($row)) {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||
count(self::EXPECTED_HEADERS) . ", Got: " . count($row));
|
||||
return;
|
||||
}
|
||||
|
||||
$data = array_combine(self::EXPECTED_HEADERS, $row);
|
||||
$this->saveRecord($data, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
private function saveRecord(array $data, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
try {
|
||||
if (!empty($data['id'])) {
|
||||
FtTxnTypeCondition::updateOrCreate(['id' => $data['id']], $data);
|
||||
$this->processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->errorCount++;
|
||||
Log::error("Error processing Transaction Type Condition at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanup(string $tempFilePath)
|
||||
: void
|
||||
{
|
||||
if (file_exists($tempFilePath)) {
|
||||
unlink($tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private function logJobCompletion()
|
||||
: void
|
||||
{
|
||||
Log::info("Transaction type condition data processing completed. " .
|
||||
"Total processed: {$this->processedCount}, Total errors: {$this->errorCount}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,102 +16,158 @@
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $periods;
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.FUNDS.TRANSFER.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
private int $errorCount = 0;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(array $periods = [])
|
||||
public function __construct(string $period = '')
|
||||
{
|
||||
$this->periods = $periods;
|
||||
$this->period = $period;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
public function handle()
|
||||
: void
|
||||
{
|
||||
try {
|
||||
set_time_limit(24 * 60 * 60);
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
$processedCount = 0;
|
||||
$errorCount = 0;
|
||||
$this->initializeJob();
|
||||
|
||||
if (empty($this->periods)) {
|
||||
Log::warning('No periods provided for funds transfer data processing');
|
||||
if ($this->period === '') {
|
||||
Log::warning('No period provided for funds transfer data processing');
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->periods as $period) {
|
||||
// Skip the _parameter folder
|
||||
if ($period === '_parameter') {
|
||||
Log::info("Skipping _parameter folder");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Construct the filename based on the period folder name
|
||||
$filename = "$period.ST.FUNDS.TRANSFER.csv";
|
||||
$filePath = "$period/$filename";
|
||||
|
||||
Log::info("Processing funds transfer file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a temporary local copy of the file
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
|
||||
if ($handle !== false) {
|
||||
$headers = (new TempFundsTransfer())->getFillable();
|
||||
$rowCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, "~")) !== false) {
|
||||
$rowCount++;
|
||||
|
||||
// Handle case where row has more columns than headers
|
||||
if (count($row) > count($headers)) {
|
||||
$row = array_slice($row, 0, count($headers));
|
||||
}
|
||||
|
||||
if (count($headers) === count($row)) {
|
||||
$data = array_combine($headers, $row);
|
||||
try {
|
||||
if (isset($data['_id']) && $data['_id'] !== '_id') {
|
||||
TempFundsTransfer::updateOrCreate(
|
||||
['_id' => $data['_id']],
|
||||
$data
|
||||
);
|
||||
$processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$errorCount++;
|
||||
Log::error("Error processing Funds Transfer at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " . count($headers) . ", Got: " . count($row));
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed $processedCount records with $errorCount errors.");
|
||||
|
||||
// Clean up the temporary file
|
||||
unlink($tempFilePath);
|
||||
} else {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
}
|
||||
}
|
||||
|
||||
Log::info("Funds Transfer data processing completed. Total processed: $processedCount, Total errors: $errorCount");
|
||||
|
||||
$this->processPeriod();
|
||||
$this->logJobCompletion();
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessFundsTransferDataJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function initializeJob()
|
||||
: void
|
||||
{
|
||||
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||
$this->processedCount = 0;
|
||||
$this->errorCount = 0;
|
||||
}
|
||||
|
||||
private function processPeriod()
|
||||
: void
|
||||
{
|
||||
$disk = Storage::disk(self::DISK_NAME);
|
||||
$filename = "{$this->period}." . self::FILENAME;
|
||||
$filePath = "{$this->period}/$filename";
|
||||
|
||||
if (!$this->validateFile($disk, $filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tempFilePath = $this->createTemporaryFile($disk, $filePath, $filename);
|
||||
$this->processFile($tempFilePath, $filePath);
|
||||
$this->cleanup($tempFilePath);
|
||||
}
|
||||
|
||||
private function validateFile($disk, string $filePath)
|
||||
: bool
|
||||
{
|
||||
Log::info("Processing funds transfer file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function createTemporaryFile($disk, string $filePath, string $filename)
|
||||
: string
|
||||
{
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
return $tempFilePath;
|
||||
}
|
||||
|
||||
private function processFile(string $tempFilePath, string $filePath)
|
||||
: void
|
||||
{
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
if ($handle === false) {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
return;
|
||||
}
|
||||
|
||||
$headers = (new TempFundsTransfer())->getFillable();
|
||||
$rowCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||
$rowCount++;
|
||||
$this->processRow($row, $headers, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors.");
|
||||
}
|
||||
|
||||
private function processRow(array $row, array $headers, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
// Handle case where row has more columns than headers
|
||||
if (count($row) > count($headers)) {
|
||||
$row = array_slice($row, 0, count($headers));
|
||||
}
|
||||
|
||||
if (count($headers) !== count($row)) {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||
count($headers) . ", Got: " . count($row));
|
||||
return;
|
||||
}
|
||||
|
||||
$data = array_combine($headers, $row);
|
||||
$this->saveRecord($data, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
private function saveRecord(array $data, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
try {
|
||||
if (isset($data['_id']) && $data['_id'] !== '_id') {
|
||||
TempFundsTransfer::updateOrCreate(
|
||||
['_id' => $data['_id']],
|
||||
$data
|
||||
);
|
||||
$this->processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->errorCount++;
|
||||
Log::error("Error processing Funds Transfer at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanup(string $tempFilePath)
|
||||
: void
|
||||
{
|
||||
if (file_exists($tempFilePath)) {
|
||||
unlink($tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private function logJobCompletion()
|
||||
: void
|
||||
{
|
||||
Log::info("Funds Transfer data processing completed. " .
|
||||
"Total processed: {$this->processedCount}, Total errors: {$this->errorCount}");
|
||||
}
|
||||
}
|
||||
|
||||
166
app/Jobs/ProcessSectorDataJob.php
Normal file
166
app/Jobs/ProcessSectorDataJob.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
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\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Webstatement\Models\Sector;
|
||||
|
||||
class ProcessSectorDataJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.SECTOR.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
private int $errorCount = 0;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(string $period = '')
|
||||
{
|
||||
$this->period = $period;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle()
|
||||
: void
|
||||
{
|
||||
try {
|
||||
$this->initializeJob();
|
||||
|
||||
if ($this->period === '') {
|
||||
Log::warning('No periods provided for sector data processing');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processPeriod();
|
||||
$this->logJobCompletion();
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessSectorDataJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function initializeJob()
|
||||
: void
|
||||
{
|
||||
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||
$this->processedCount = 0;
|
||||
$this->errorCount = 0;
|
||||
}
|
||||
|
||||
private function processPeriod()
|
||||
: void
|
||||
{
|
||||
$disk = Storage::disk(self::DISK_NAME);
|
||||
$filePath = "$this->period/" . self::FILENAME;
|
||||
|
||||
if (!$this->validateFile($disk, $filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tempFilePath = $this->createTemporaryFile($disk, $filePath);
|
||||
$this->processFile($tempFilePath, $filePath);
|
||||
$this->cleanup($tempFilePath);
|
||||
}
|
||||
|
||||
private function validateFile($disk, string $filePath)
|
||||
: bool
|
||||
{
|
||||
Log::info("Processing sector file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function createTemporaryFile($disk, string $filePath)
|
||||
: string
|
||||
{
|
||||
$tempFilePath = storage_path("app/temp_" . self::FILENAME);
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
return $tempFilePath;
|
||||
}
|
||||
|
||||
private function processFile(string $tempFilePath, string $filePath)
|
||||
: void
|
||||
{
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
if ($handle === false) {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
return;
|
||||
}
|
||||
|
||||
$headers = array_filter((new Sector())->getFillable(), function($field) {
|
||||
return $field !== 'id';
|
||||
});
|
||||
$rowCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||
$rowCount++;
|
||||
$this->processRow($row, $headers, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors.");
|
||||
}
|
||||
|
||||
private function processRow(array $row, array $headers, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
if (count($headers) !== count($row)) {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||
count($headers) . ", Got: " . count($row));
|
||||
return;
|
||||
}
|
||||
|
||||
$data = array_combine($headers, $row);
|
||||
$this->saveRecord($data, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
private function saveRecord(array $data, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
try {
|
||||
if (isset($data['sector_code']) && !empty($data['sector_code'])) {
|
||||
Sector::updateOrCreate(['sector_code' => $data['sector_code'], 'co_code' => $data['co_code']], $data);
|
||||
$this->processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->errorCount++;
|
||||
Log::error("Error processing Sector at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanup(string $tempFilePath)
|
||||
: void
|
||||
{
|
||||
if (file_exists($tempFilePath)) {
|
||||
unlink($tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private function logJobCompletion()
|
||||
: void
|
||||
{
|
||||
Log::info("Sector data processing completed. " .
|
||||
"Total processed: {$this->processedCount}, Total errors: {$this->errorCount}");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
use Exception;
|
||||
@@ -11,103 +10,246 @@
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Webstatement\Models\StmtEntry;
|
||||
use Modules\Webstatement\Models\TempStmtEntry;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ProcessStmtEntryDataJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $periods;
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.STMT.ENTRY.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
|
||||
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
private int $errorCount = 0;
|
||||
private array $entryBatch = [];
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(array $periods = [])
|
||||
public function __construct(string $period = '')
|
||||
{
|
||||
$this->periods = $periods;
|
||||
$this->period = $period;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
public function handle()
|
||||
: void
|
||||
{
|
||||
try {
|
||||
set_time_limit(24 * 60 * 60);
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
$processedCount = 0;
|
||||
$errorCount = 0;
|
||||
$this->initializeJob();
|
||||
|
||||
if (empty($this->periods)) {
|
||||
Log::warning('No periods provided for statement entry data processing');
|
||||
if ($this->period === '') {
|
||||
Log::warning('No period provided for statement entry data processing');
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->periods as $period) {
|
||||
// Skip the _parameter folder
|
||||
if ($period === '_parameter') {
|
||||
Log::info("Skipping _parameter folder");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Construct the filename based on the period folder name
|
||||
$filename = "$period.ST.STMT.ENTRY.csv";
|
||||
$filePath = "$period/$filename";
|
||||
|
||||
Log::info("Processing statement entry file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a temporary local copy of the file
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
|
||||
if ($handle !== false) {
|
||||
$headers = (new StmtEntry())->getFillable();
|
||||
$rowCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, "~")) !== false) {
|
||||
$rowCount++;
|
||||
|
||||
if (count($headers) === count($row)) {
|
||||
$data = array_combine($headers, $row);
|
||||
try {
|
||||
if ($data['stmt_entry_id'] !== 'stmt_entry_id') {
|
||||
StmtEntry::updateOrCreate(
|
||||
['stmt_entry_id' => $data['stmt_entry_id']],
|
||||
$data
|
||||
);
|
||||
$processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$errorCount++;
|
||||
Log::error("Error processing Statement Entry at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " . count($headers) . ", Got: " . count($row));
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed $processedCount records with $errorCount errors.");
|
||||
|
||||
// Clean up the temporary file
|
||||
unlink($tempFilePath);
|
||||
} else {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
}
|
||||
}
|
||||
|
||||
Log::info("Statement Entry data processing completed. Total processed: $processedCount, Total errors: $errorCount");
|
||||
|
||||
$this->processPeriod();
|
||||
$this->logJobCompletion();
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessStmtEntryDataJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function initializeJob()
|
||||
: void
|
||||
{
|
||||
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||
$this->processedCount = 0;
|
||||
$this->errorCount = 0;
|
||||
$this->entryBatch = [];
|
||||
}
|
||||
|
||||
private function processPeriod()
|
||||
: void
|
||||
{
|
||||
$disk = Storage::disk(self::DISK_NAME);
|
||||
$filename = "{$this->period}." . self::FILENAME;
|
||||
$filePath = "{$this->period}/$filename";
|
||||
|
||||
if (!$this->validateFile($disk, $filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tempFilePath = $this->createTemporaryFile($disk, $filePath, $filename);
|
||||
$this->processFile($tempFilePath, $filePath);
|
||||
$this->cleanup($tempFilePath);
|
||||
}
|
||||
|
||||
private function validateFile($disk, string $filePath)
|
||||
: bool
|
||||
{
|
||||
Log::info("Processing statement entry file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function createTemporaryFile($disk, string $filePath, string $filename)
|
||||
: string
|
||||
{
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
return $tempFilePath;
|
||||
}
|
||||
|
||||
private function processFile(string $tempFilePath, string $filePath)
|
||||
: void
|
||||
{
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
if ($handle === false) {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
return;
|
||||
}
|
||||
|
||||
$headers = (new StmtEntry())->getFillable();
|
||||
$rowCount = 0;
|
||||
$chunkCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||
$rowCount++;
|
||||
$this->processRow($row, $headers, $rowCount, $filePath);
|
||||
|
||||
// Process in chunks to avoid memory issues
|
||||
if (count($this->entryBatch) >= self::CHUNK_SIZE) {
|
||||
$this->saveBatch();
|
||||
$chunkCount++;
|
||||
Log::info("Processed chunk $chunkCount ({$this->processedCount} records so far)");
|
||||
}
|
||||
}
|
||||
|
||||
// Process any remaining records
|
||||
if (!empty($this->entryBatch)) {
|
||||
$this->saveBatch();
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors.");
|
||||
}
|
||||
|
||||
private function processRow(array $row, array $headers, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
if (count($headers) !== count($row)) {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||
count($headers) . ", Got: " . count($row));
|
||||
return;
|
||||
}
|
||||
|
||||
$data = array_combine($headers, $row);
|
||||
$this->cleanTransReference($data);
|
||||
$this->addToBatch($data, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
private function cleanTransReference(array &$data)
|
||||
: void
|
||||
{
|
||||
if (isset($data['trans_reference'])) {
|
||||
// Clean trans_reference from \\BNK if present
|
||||
$data['trans_reference'] = preg_replace('/\\\\.*$/', '', $data['trans_reference']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add record to batch instead of saving immediately
|
||||
*/
|
||||
private function addToBatch(array $data, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
try {
|
||||
if (isset($data['stmt_entry_id']) && $data['stmt_entry_id'] !== 'stmt_entry_id') {
|
||||
// Add timestamp fields
|
||||
$now = now();
|
||||
$data['created_at'] = $now;
|
||||
$data['updated_at'] = $now;
|
||||
|
||||
// Add to entry batch
|
||||
$this->entryBatch[] = $data;
|
||||
$this->processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->errorCount++;
|
||||
Log::error("Error processing Statement Entry at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simpan batch data ke database menggunakan updateOrCreate
|
||||
* untuk menghindari error unique constraint
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function saveBatch(): void
|
||||
{
|
||||
Log::info('Memulai proses saveBatch dengan updateOrCreate');
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
if (!empty($this->entryBatch)) {
|
||||
$totalProcessed = 0;
|
||||
|
||||
// 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
|
||||
);
|
||||
|
||||
$totalProcessed++;
|
||||
} else {
|
||||
Log::warning('Invalid entry data structure', ['data' => $entryData]);
|
||||
$this->errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanup(string $tempFilePath)
|
||||
: void
|
||||
{
|
||||
if (file_exists($tempFilePath)) {
|
||||
unlink($tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private function logJobCompletion()
|
||||
: void
|
||||
{
|
||||
Log::info("Statement Entry data processing completed. " .
|
||||
"Total processed: {$this->processedCount}, Total errors: {$this->errorCount}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,112 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
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\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Webstatement\Models\TempStmtNarrFormat;
|
||||
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\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Webstatement\Models\TempStmtNarrFormat;
|
||||
|
||||
class ProcessStmtNarrFormatDataJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $periods;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(array $periods = [])
|
||||
class ProcessStmtNarrFormatDataJob implements ShouldQueue
|
||||
{
|
||||
$this->periods = $periods;
|
||||
}
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
set_time_limit(24 * 60 * 60);
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
$processedCount = 0;
|
||||
$errorCount = 0;
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.STMT.NARR.FORMAT.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
|
||||
if (empty($this->periods)) {
|
||||
Log::warning('No periods provided for statement narrative format data processing');
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
private int $errorCount = 0;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(string $period = '')
|
||||
{
|
||||
$this->period = $period;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle()
|
||||
: void
|
||||
{
|
||||
try {
|
||||
$this->initializeJob();
|
||||
|
||||
if ($this->period === '') {
|
||||
Log::warning('No periods provided for statement narrative format data processing');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processPeriod();
|
||||
$this->logJobCompletion();
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessStmtNarrFormatDataJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function initializeJob()
|
||||
: void
|
||||
{
|
||||
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||
$this->processedCount = 0;
|
||||
$this->errorCount = 0;
|
||||
}
|
||||
|
||||
private function processPeriod()
|
||||
: void
|
||||
{
|
||||
$disk = Storage::disk(self::DISK_NAME);
|
||||
$filePath = "$this->period/" . self::FILENAME;
|
||||
|
||||
if (!$this->validateFile($disk, $filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->periods as $period) {
|
||||
// Skip the _parameter folder
|
||||
/*if ($period === '_parameter') {
|
||||
Log::info("Skipping _parameter folder");
|
||||
continue;
|
||||
}*/
|
||||
$tempFilePath = $this->createTemporaryFile($disk, $filePath);
|
||||
$this->processFile($tempFilePath, $filePath);
|
||||
$this->cleanup($tempFilePath);
|
||||
}
|
||||
|
||||
// Construct the filename based on the period folder name
|
||||
$filename = "ST.STMT.NARR.FORMAT.csv";
|
||||
$filePath = "$period/$filename";
|
||||
private function validateFile($disk, string $filePath)
|
||||
: bool
|
||||
{
|
||||
Log::info("Processing statement narrative format file: $filePath");
|
||||
|
||||
Log::info("Processing statement narrative format file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a temporary local copy of the file
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
|
||||
if ($handle !== false) {
|
||||
$headers = (new TempStmtNarrFormat())->getFillable();
|
||||
$rowCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, "~")) !== false) {
|
||||
$rowCount++;
|
||||
|
||||
if (count($headers) === count($row)) {
|
||||
$data = array_combine($headers, $row);
|
||||
try {
|
||||
if (isset($data['_id']) && $data['_id'] !== 'id' && $data['_id'] !== '_id') {
|
||||
TempStmtNarrFormat::updateOrCreate(
|
||||
['_id' => $data['_id']],
|
||||
$data
|
||||
);
|
||||
$processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$errorCount++;
|
||||
Log::error("Error processing Statement Narrative Format at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " . count($headers) . ", Got: " . count($row));
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed $processedCount records with $errorCount errors.");
|
||||
|
||||
// Clean up the temporary file
|
||||
unlink($tempFilePath);
|
||||
} else {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
}
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
return false;
|
||||
}
|
||||
|
||||
Log::info("Statement Narrative Format data processing completed. Total processed: $processedCount, Total errors: $errorCount");
|
||||
return true;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessStmtNarrFormatDataJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
private function createTemporaryFile($disk, string $filePath)
|
||||
: string
|
||||
{
|
||||
$tempFilePath = storage_path("app/temp_" . self::FILENAME);
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
return $tempFilePath;
|
||||
}
|
||||
|
||||
private function processFile(string $tempFilePath, string $filePath)
|
||||
: void
|
||||
{
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
if ($handle === false) {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
return;
|
||||
}
|
||||
|
||||
$headers = (new TempStmtNarrFormat())->getFillable();
|
||||
$rowCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||
$rowCount++;
|
||||
$this->processRow($row, $headers, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors.");
|
||||
}
|
||||
|
||||
private function processRow(array $row, array $headers, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
if (count($headers) !== count($row)) {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||
count($headers) . ", Got: " . count($row));
|
||||
return;
|
||||
}
|
||||
|
||||
$data = array_combine($headers, $row);
|
||||
$this->saveRecord($data, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
private function saveRecord(array $data, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
try {
|
||||
if (isset($data['_id']) && $data['_id'] !== 'id' && $data['_id'] !== '_id') {
|
||||
TempStmtNarrFormat::updateOrCreate(['_id' => $data['_id']], $data);
|
||||
$this->processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->errorCount++;
|
||||
Log::error("Error processing Statement Narrative Format at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanup(string $tempFilePath)
|
||||
: void
|
||||
{
|
||||
if (file_exists($tempFilePath)) {
|
||||
unlink($tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private function logJobCompletion()
|
||||
: void
|
||||
{
|
||||
Log::info("Statement Narrative Format data processing completed. " .
|
||||
"Total processed: {$this->processedCount}, Total errors: {$this->errorCount}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,97 +16,149 @@
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $periods;
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.STMT.NARR.PARAM.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
private int $errorCount = 0;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(array $periods = [])
|
||||
public function __construct(string $period = '')
|
||||
{
|
||||
$this->periods = $periods;
|
||||
$this->period = $period;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
public function handle()
|
||||
: void
|
||||
{
|
||||
try {
|
||||
set_time_limit(24 * 60 * 60);
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
$processedCount = 0;
|
||||
$errorCount = 0;
|
||||
$this->initializeJob();
|
||||
|
||||
if (empty($this->periods)) {
|
||||
if ($this->period === '') {
|
||||
Log::warning('No periods provided for statement narrative parameter data processing');
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->periods as $period) {
|
||||
// Skip the _parameter folder
|
||||
/*if ($period === '_parameter') {
|
||||
Log::info("Skipping _parameter folder");
|
||||
continue;
|
||||
}*/
|
||||
|
||||
// Construct the filename based on the period folder name
|
||||
$filename = "ST.STMT.NARR.PARAM.csv";
|
||||
$filePath = "$period/$filename";
|
||||
|
||||
Log::info("Processing statement narrative parameter file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a temporary local copy of the file
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
|
||||
if ($handle !== false) {
|
||||
$headers = (new TempStmtNarrParam())->getFillable();
|
||||
$rowCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, "~")) !== false) {
|
||||
$rowCount++;
|
||||
|
||||
if (count($headers) === count($row)) {
|
||||
$data = array_combine($headers, $row);
|
||||
try {
|
||||
if (isset($data['_id']) && $data['_id'] !== 'id' && $data['_id'] !== '_id') {
|
||||
TempStmtNarrParam::updateOrCreate(
|
||||
['_id' => $data['_id']],
|
||||
$data
|
||||
);
|
||||
$processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$errorCount++;
|
||||
Log::error("Error processing Statement Narrative Parameter at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " . count($headers) . ", Got: " . count($row));
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed $processedCount records with $errorCount errors.");
|
||||
|
||||
// Clean up the temporary file
|
||||
unlink($tempFilePath);
|
||||
} else {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
}
|
||||
}
|
||||
|
||||
Log::info("Statement Narrative Parameter data processing completed. Total processed: $processedCount, Total errors: $errorCount");
|
||||
|
||||
$this->processPeriod();
|
||||
$this->logJobCompletion();
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessStmtNarrParamDataJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function initializeJob()
|
||||
: void
|
||||
{
|
||||
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||
$this->processedCount = 0;
|
||||
$this->errorCount = 0;
|
||||
}
|
||||
|
||||
private function processPeriod()
|
||||
: void
|
||||
{
|
||||
$disk = Storage::disk(self::DISK_NAME);
|
||||
$filePath = "$this->period/" . self::FILENAME;
|
||||
|
||||
if (!$this->validateFile($disk, $filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tempFilePath = $this->createTemporaryFile($disk, $filePath);
|
||||
$this->processFile($tempFilePath, $filePath);
|
||||
$this->cleanup($tempFilePath);
|
||||
}
|
||||
|
||||
private function validateFile($disk, string $filePath)
|
||||
: bool
|
||||
{
|
||||
Log::info("Processing statement narrative parameter file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function createTemporaryFile($disk, string $filePath)
|
||||
: string
|
||||
{
|
||||
$tempFilePath = storage_path("app/temp_" . self::FILENAME);
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
return $tempFilePath;
|
||||
}
|
||||
|
||||
private function processFile(string $tempFilePath, string $filePath)
|
||||
: void
|
||||
{
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
if ($handle === false) {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
return;
|
||||
}
|
||||
|
||||
$headers = (new TempStmtNarrParam())->getFillable();
|
||||
$rowCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||
$rowCount++;
|
||||
$this->processRow($row, $headers, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors.");
|
||||
}
|
||||
|
||||
private function processRow(array $row, array $headers, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
if (count($headers) !== count($row)) {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||
count($headers) . ", Got: " . count($row));
|
||||
return;
|
||||
}
|
||||
|
||||
$data = array_combine($headers, $row);
|
||||
$this->saveRecord($data, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
private function saveRecord(array $data, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
try {
|
||||
if (isset($data['_id']) && $data['_id'] !== 'id' && $data['_id'] !== '_id') {
|
||||
TempStmtNarrParam::updateOrCreate(['_id' => $data['_id']], $data);
|
||||
$this->processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->errorCount++;
|
||||
Log::error("Error processing Statement Narrative Parameter at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanup(string $tempFilePath)
|
||||
: void
|
||||
{
|
||||
if (file_exists($tempFilePath)) {
|
||||
unlink($tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private function logJobCompletion()
|
||||
: void
|
||||
{
|
||||
Log::info("Statement Narrative Parameter data processing completed. " .
|
||||
"Total processed: {$this->processedCount}, Total errors: {$this->errorCount}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,12 @@
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
private const PARAMETER_FOLDER = '_parameter';
|
||||
|
||||
// Konstanta untuk nilai-nilai statis
|
||||
private const FILE_EXTENSION = '.ST.TELLER.csv';
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
private const HEADER_MAP = [
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.TELLER.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
|
||||
private const HEADER_MAP = [
|
||||
'id' => 'id_teller',
|
||||
'account_1' => 'account_1',
|
||||
'currency_1' => 'currency_1',
|
||||
@@ -125,18 +124,21 @@
|
||||
'amount_fcy_2' => 'amount_fcy_2',
|
||||
'rate_2' => 'rate_2',
|
||||
'customer_1' => 'customer_1',
|
||||
'last_version' => 'last_version'
|
||||
'last_version' => 'last_version',
|
||||
'dealer_desk' => 'dealer_desk',
|
||||
];
|
||||
|
||||
// Pemetaan bidang header ke kolom model
|
||||
protected array $periods;
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
private int $errorCount = 0;
|
||||
private array $tellerBatch = [];
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(array $periods = [])
|
||||
public function __construct(string $period = '')
|
||||
{
|
||||
$this->periods = $periods;
|
||||
$this->period = $period;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,148 +148,118 @@
|
||||
: void
|
||||
{
|
||||
try {
|
||||
set_time_limit(24 * 60 * 60);
|
||||
$this->initializeJob();
|
||||
|
||||
if (empty($this->periods)) {
|
||||
Log::warning('No periods provided for teller data processing');
|
||||
if ($this->period === '') {
|
||||
Log::warning('No period provided for teller data processing');
|
||||
return;
|
||||
}
|
||||
|
||||
$stats = $this->processPeriods();
|
||||
|
||||
Log::info("ProcessTellerDataJob completed. Total processed: {$stats['processed']}, Total errors: {$stats['errors']}");
|
||||
$this->processPeriod();
|
||||
$this->logJobCompletion();
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error in ProcessTellerDataJob: " . $e->getMessage());
|
||||
Log::error('Error in ProcessTellerDataJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process all periods and return statistics
|
||||
*/
|
||||
private function processPeriods()
|
||||
: array
|
||||
private function initializeJob()
|
||||
: void
|
||||
{
|
||||
$disk = Storage::disk(self::DISK_NAME);
|
||||
$processedCount = 0;
|
||||
$errorCount = 0;
|
||||
|
||||
foreach ($this->periods as $period) {
|
||||
// Skip the parameter folder
|
||||
if ($period === self::PARAMETER_FOLDER) {
|
||||
Log::info("Skipping " . self::PARAMETER_FOLDER . " folder");
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = $this->processPeriodFile($disk, $period);
|
||||
$processedCount += $result['processed'];
|
||||
$errorCount += $result['errors'];
|
||||
}
|
||||
|
||||
return [
|
||||
'processed' => $processedCount,
|
||||
'errors' => $errorCount
|
||||
];
|
||||
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||
$this->processedCount = 0;
|
||||
$this->errorCount = 0;
|
||||
$this->tellerBatch = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single period file
|
||||
*/
|
||||
private function processPeriodFile($disk, string $period)
|
||||
: array
|
||||
private function processPeriod()
|
||||
: void
|
||||
{
|
||||
$filename = $period . self::FILE_EXTENSION;
|
||||
$filePath = "$period/$filename";
|
||||
$processedCount = 0;
|
||||
$errorCount = 0;
|
||||
$disk = Storage::disk(self::DISK_NAME);
|
||||
$filename = "{$this->period}." . self::FILENAME;
|
||||
$filePath = "{$this->period}/$filename";
|
||||
|
||||
if (!$this->validateFile($disk, $filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tempFilePath = $this->createTemporaryFile($disk, $filePath, $filename);
|
||||
$this->processFile($tempFilePath, $filePath);
|
||||
$this->cleanup($tempFilePath);
|
||||
}
|
||||
|
||||
private function validateFile($disk, string $filePath)
|
||||
: bool
|
||||
{
|
||||
Log::info("Processing teller file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
return ['processed' => 0, 'errors' => 0];
|
||||
return false;
|
||||
}
|
||||
|
||||
$tempFilePath = $this->createTempFile($disk, $filePath, $filename);
|
||||
|
||||
$result = $this->processCSVFile($tempFilePath, $filePath);
|
||||
$processedCount += $result['processed'];
|
||||
$errorCount += $result['errors'];
|
||||
|
||||
// Clean up the temporary file
|
||||
if (file_exists($tempFilePath)) {
|
||||
unlink($tempFilePath);
|
||||
}
|
||||
|
||||
Log::info("Completed processing $filePath. Processed {$result['processed']} records with {$result['errors']} errors.");
|
||||
|
||||
return [
|
||||
'processed' => $processedCount,
|
||||
'errors' => $errorCount
|
||||
];
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a temporary file for processing
|
||||
*/
|
||||
private function createTempFile($disk, string $filePath, string $filename)
|
||||
private function createTemporaryFile($disk, string $filePath, string $fileName)
|
||||
: string
|
||||
{
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
$tempFilePath = storage_path("app/temp_$fileName");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
return $tempFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a CSV file and import data
|
||||
*/
|
||||
private function processCSVFile(string $tempFilePath, string $originalFilePath)
|
||||
: array
|
||||
private function processFile(string $tempFilePath, string $filePath)
|
||||
: void
|
||||
{
|
||||
$processedCount = 0;
|
||||
$errorCount = 0;
|
||||
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
if ($handle === false) {
|
||||
Log::error("Unable to open file: $originalFilePath");
|
||||
return ['processed' => 0, 'errors' => 0];
|
||||
Log::error("Unable to open file: $filePath");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the headers from the first row
|
||||
$headerRow = fgetcsv($handle, 0, self::CSV_DELIMITER);
|
||||
if (!$headerRow) {
|
||||
fclose($handle);
|
||||
return ['processed' => 0, 'errors' => 0];
|
||||
return;
|
||||
}
|
||||
|
||||
$rowCount = 0;
|
||||
$chunkCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||
$rowCount++;
|
||||
$this->processRow($headerRow, $row, $rowCount, $filePath);
|
||||
|
||||
if (count($headerRow) !== count($row)) {
|
||||
Log::warning("Row $rowCount in $originalFilePath has incorrect column count. Expected: " . count($headerRow) . ", Got: " . count($row));
|
||||
continue;
|
||||
// Process in chunks to avoid memory issues
|
||||
if (count($this->tellerBatch) >= self::CHUNK_SIZE) {
|
||||
$this->saveBatch();
|
||||
$chunkCount++;
|
||||
Log::info("Processed chunk $chunkCount ({$this->processedCount} records so far)");
|
||||
}
|
||||
}
|
||||
|
||||
$result = $this->processRow($headerRow, $row, $rowCount, $originalFilePath);
|
||||
$processedCount += $result['processed'];
|
||||
$errorCount += $result['errors'];
|
||||
// Process any remaining records
|
||||
if (!empty($this->tellerBatch)) {
|
||||
$this->saveBatch();
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
return [
|
||||
'processed' => $processedCount,
|
||||
'errors' => $errorCount
|
||||
];
|
||||
Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single row from the CSV file
|
||||
*/
|
||||
private function processRow(array $headerRow, array $row, int $rowCount, string $filePath)
|
||||
: array
|
||||
: void
|
||||
{
|
||||
// Skip if row doesn't have enough columns
|
||||
if (count($headerRow) !== count($row)) {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||
count($headerRow) . ", Got: " . count($row));
|
||||
$this->errorCount++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Combine the header row with the data row
|
||||
$rawData = array_combine($headerRow, $row);
|
||||
|
||||
@@ -299,18 +271,62 @@
|
||||
|
||||
// Skip header row if it was included in the data
|
||||
if ($data['id_teller'] === 'id') {
|
||||
return ['processed' => 0, 'errors' => 0];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$teller = Teller::firstOrNew(['id_teller' => $data['id_teller']]);
|
||||
$teller->fill($data);
|
||||
$teller->save();
|
||||
// Add timestamps
|
||||
$now = now();
|
||||
$data['created_at'] = $now;
|
||||
$data['updated_at'] = $now;
|
||||
|
||||
return ['processed' => 1, 'errors' => 0];
|
||||
// Add to batch for bulk processing
|
||||
$this->tellerBatch[] = $data;
|
||||
$this->processedCount++;
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error processing Teller at row $rowCount in $filePath: " . $e->getMessage());
|
||||
return ['processed' => 0, 'errors' => 1];
|
||||
$this->errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
private function saveBatch(): void
|
||||
{
|
||||
try {
|
||||
if (!empty($this->tellerBatch)) {
|
||||
// Process in smaller chunks for better memory management
|
||||
foreach ($this->tellerBatch as $entry) {
|
||||
// Extract all stmt_entry_ids from the current chunk
|
||||
$entryIds = array_column($entry, 'id_teller');
|
||||
|
||||
// Delete existing records with these IDs to avoid conflicts
|
||||
Teller::whereIn('id_teller', $entryIds)->delete();
|
||||
|
||||
// Insert all records in the chunk at once
|
||||
Teller::insert($entry);
|
||||
}
|
||||
|
||||
// Reset entry batch after processing
|
||||
$this->tellerBatch = [];
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error in saveBatch: " . $e->getMessage());
|
||||
$this->errorCount += count($this->tellerBatch);
|
||||
// Reset batch even if there's an error to prevent reprocessing the same failed records
|
||||
$this->tellerBatch = [];
|
||||
}
|
||||
}
|
||||
|
||||
private function logJobCompletion()
|
||||
: void
|
||||
{
|
||||
Log::info("ProcessTellerDataJob completed. Total processed: {$this->processedCount}, Total errors: {$this->errorCount}");
|
||||
}
|
||||
|
||||
private function cleanup(string $tempFilePath)
|
||||
: void
|
||||
{
|
||||
if (file_exists($tempFilePath)) {
|
||||
unlink($tempFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,112 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
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\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Webstatement\Models\TempTransaction;
|
||||
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\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Webstatement\Models\TempTransaction;
|
||||
|
||||
class ProcessTransactionDataJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $periods;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(array $periods = [])
|
||||
class ProcessTransactionDataJob implements ShouldQueue
|
||||
{
|
||||
$this->periods = $periods;
|
||||
}
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
set_time_limit(24 * 60 * 60);
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
$processedCount = 0;
|
||||
$errorCount = 0;
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.TRANSACTION.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
|
||||
if (empty($this->periods)) {
|
||||
Log::warning('No periods provided for transaction data processing');
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
private int $errorCount = 0;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(string $period = '')
|
||||
{
|
||||
$this->period = $period;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle()
|
||||
: void
|
||||
{
|
||||
try {
|
||||
$this->initializeJob();
|
||||
|
||||
if ($this->period === '') {
|
||||
Log::warning('No periods provided for transaction data processing');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processPeriod();
|
||||
$this->logJobCompletion();
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessTransactionDataJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function initializeJob()
|
||||
: void
|
||||
{
|
||||
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||
$this->processedCount = 0;
|
||||
$this->errorCount = 0;
|
||||
}
|
||||
|
||||
private function processPeriod()
|
||||
: void
|
||||
{
|
||||
$disk = Storage::disk(self::DISK_NAME);
|
||||
$filePath = "$this->period/" . self::FILENAME;
|
||||
|
||||
if (!$this->validateFile($disk, $filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->periods as $period) {
|
||||
// Skip the _parameter folder
|
||||
/*if ($period === '_parameter') {
|
||||
Log::info("Skipping _parameter folder");
|
||||
continue;
|
||||
}*/
|
||||
$tempFilePath = $this->createTemporaryFile($disk, $filePath);
|
||||
$this->processFile($tempFilePath, $filePath);
|
||||
$this->cleanup($tempFilePath);
|
||||
}
|
||||
|
||||
// Construct the filename based on the period folder name
|
||||
$filename = "ST.TRANSACTION.csv";
|
||||
$filePath = "$period/$filename";
|
||||
private function validateFile($disk, string $filePath)
|
||||
: bool
|
||||
{
|
||||
Log::info("Processing transaction file: $filePath");
|
||||
|
||||
Log::info("Processing transaction file: $filePath");
|
||||
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a temporary local copy of the file
|
||||
$tempFilePath = storage_path("app/temp_$filename");
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
|
||||
if ($handle !== false) {
|
||||
$headers = (new TempTransaction())->getFillable();
|
||||
$rowCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, "~")) !== false) {
|
||||
$rowCount++;
|
||||
|
||||
if (count($headers) === count($row)) {
|
||||
$data = array_combine($headers, $row);
|
||||
try {
|
||||
if (isset($data['_id']) && $data['_id'] !== 'id' && $data['_id'] !== '_id') {
|
||||
TempTransaction::updateOrCreate(
|
||||
['_id' => $data['_id']],
|
||||
$data
|
||||
);
|
||||
$processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$errorCount++;
|
||||
Log::error("Error processing Transaction at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " . count($headers) . ", Got: " . count($row));
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed $processedCount records with $errorCount errors.");
|
||||
|
||||
// Clean up the temporary file
|
||||
unlink($tempFilePath);
|
||||
} else {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
}
|
||||
if (!$disk->exists($filePath)) {
|
||||
Log::warning("File not found: $filePath");
|
||||
return false;
|
||||
}
|
||||
|
||||
Log::info("Transaction data processing completed. Total processed: $processedCount, Total errors: $errorCount");
|
||||
return true;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in ProcessTransactionDataJob: ' . $e->getMessage());
|
||||
throw $e;
|
||||
private function createTemporaryFile($disk, string $filePath)
|
||||
: string
|
||||
{
|
||||
$tempFilePath = storage_path("app/temp_" . self::FILENAME);
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
return $tempFilePath;
|
||||
}
|
||||
|
||||
private function processFile(string $tempFilePath, string $filePath)
|
||||
: void
|
||||
{
|
||||
$handle = fopen($tempFilePath, "r");
|
||||
if ($handle === false) {
|
||||
Log::error("Unable to open file: $filePath");
|
||||
return;
|
||||
}
|
||||
|
||||
$headers = (new TempTransaction())->getFillable();
|
||||
$rowCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||
$rowCount++;
|
||||
$this->processRow($row, $headers, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors.");
|
||||
}
|
||||
|
||||
private function processRow(array $row, array $headers, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
if (count($headers) !== count($row)) {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||
count($headers) . ", Got: " . count($row));
|
||||
return;
|
||||
}
|
||||
|
||||
$data = array_combine($headers, $row);
|
||||
$this->saveRecord($data, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
private function saveRecord(array $data, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
try {
|
||||
if (isset($data['_id']) && $data['_id'] !== 'id' && $data['_id'] !== '_id') {
|
||||
TempTransaction::updateOrCreate(['_id' => $data['_id']], $data);
|
||||
$this->processedCount++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->errorCount++;
|
||||
Log::error("Error processing Transaction at row $rowCount in $filePath: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanup(string $tempFilePath)
|
||||
: void
|
||||
{
|
||||
if (file_exists($tempFilePath)) {
|
||||
unlink($tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private function logJobCompletion()
|
||||
: void
|
||||
{
|
||||
Log::info("Transaction data processing completed. " .
|
||||
"Total processed: {$this->processedCount}, Total errors: {$this->errorCount}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
425
app/Jobs/SendStatementEmailJob.php
Normal file
425
app/Jobs/SendStatementEmailJob.php
Normal file
@@ -0,0 +1,425 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Job untuk mengirim email PDF statement ke nasabah
|
||||
* Mendukung pengiriman per rekening, per cabang, atau seluruh cabang
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
Log::info('SendStatementEmailJob created', [
|
||||
'period' => $this->period,
|
||||
'request_type' => $this->requestType,
|
||||
'target_value' => $this->targetValue,
|
||||
'batch_id' => $this->batchId,
|
||||
'log_id' => $this->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
|
||||
]);
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
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()
|
||||
]);
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
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
|
||||
]);
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mengirim email statement untuk account tertentu
|
||||
*
|
||||
* @param Account $account
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function sendStatementEmail(Account $account)
|
||||
{
|
||||
// Dapatkan email untuk pengiriman
|
||||
$emailAddress = $this->getEmailForAccount($account);
|
||||
|
||||
if (!$emailAddress) {
|
||||
throw new Exception("No email address found for account {$account->account_number}");
|
||||
}
|
||||
|
||||
// Cek apakah file PDF ada
|
||||
$pdfPath = $this->getPdfPath($account->account_number, $account->branch_code);
|
||||
|
||||
if (!Storage::exists($pdfPath)) {
|
||||
throw new Exception("PDF file not found: {$pdfPath}");
|
||||
}
|
||||
|
||||
// Buat atau update log statement
|
||||
$statementLog = $this->createOrUpdateStatementLog($account);
|
||||
|
||||
// Dapatkan path absolut file
|
||||
$absolutePdfPath = Storage::path($pdfPath);
|
||||
|
||||
// Kirim email
|
||||
// Add delay between email sends to prevent rate limiting
|
||||
sleep(1); // 2 second delay
|
||||
Mail::to($emailAddress)->send(
|
||||
new StatementEmail($statementLog, $absolutePdfPath, false)
|
||||
);
|
||||
|
||||
// Update status log dengan email yang digunakan
|
||||
$statementLog->update([
|
||||
'email_sent_at' => now(),
|
||||
'email_status' => 'sent',
|
||||
'email_address' => $emailAddress // Simpan email yang digunakan untuk tracking
|
||||
]);
|
||||
|
||||
Log::info('Email sent for account', [
|
||||
'account_number' => $account->account_number,
|
||||
'branch_code' => $account->branch_code,
|
||||
'email' => $emailAddress,
|
||||
'email_source' => !empty($account->stmt_email) ? 'account.stmt_email' : 'customer.email',
|
||||
'pdf_path' => $pdfPath,
|
||||
'batch_id' => $this->batchId
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mendapatkan 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' => $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()
|
||||
]);
|
||||
}
|
||||
}
|
||||
147
app/Jobs/UnlockPdfJob.php
Normal file
147
app/Jobs/UnlockPdfJob.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Jobs;
|
||||
|
||||
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\Log;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Symfony\Component\Process\Process;
|
||||
use Symfony\Component\Process\Exception\ProcessFailedException;
|
||||
|
||||
class UnlockPdfJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $baseDirectory;
|
||||
protected $password;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @param string $baseDirectory Base directory path to scan
|
||||
* @param string $password Password to unlock PDF files
|
||||
*/
|
||||
public function __construct(string $baseDirectory, string $password = '123456')
|
||||
{
|
||||
$this->baseDirectory = $baseDirectory;
|
||||
$this->password = $password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
Log::info("Starting PDF unlock process in directory: {$this->baseDirectory}");
|
||||
|
||||
// Check if directory exists
|
||||
if (!File::isDirectory($this->baseDirectory)) {
|
||||
Log::error("Directory not found: {$this->baseDirectory}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all subdirectories (year folders like 202505)
|
||||
$yearDirectories = File::directories($this->baseDirectory);
|
||||
|
||||
foreach ($yearDirectories as $yearDirectory) {
|
||||
$this->processYearDirectory($yearDirectory);
|
||||
}
|
||||
|
||||
Log::info("PDF unlock process completed successfully.");
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error unlocking PDF files: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a year directory (e.g., 202505)
|
||||
*
|
||||
* @param string $yearDirectory Directory path to process
|
||||
*/
|
||||
protected function processYearDirectory(string $yearDirectory): void
|
||||
{
|
||||
try {
|
||||
// Get all ID directories (e.g., ID0010001)
|
||||
$idDirectories = File::directories($yearDirectory);
|
||||
|
||||
foreach ($idDirectories as $idDirectory) {
|
||||
$this->processIdDirectory($idDirectory);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error processing year directory {$yearDirectory}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an ID directory (e.g., ID0010001)
|
||||
*
|
||||
* @param string $idDirectory Directory path to process
|
||||
*/
|
||||
protected function processIdDirectory(string $idDirectory): void
|
||||
{
|
||||
try {
|
||||
// Get all PDF files in the directory
|
||||
$pdfFiles = File::glob($idDirectory . '/*.pdf');
|
||||
|
||||
foreach ($pdfFiles as $pdfFile) {
|
||||
$this->unlockPdf($pdfFile);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error processing ID directory {$idDirectory}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock a password-protected PDF file
|
||||
*
|
||||
* @param string $pdfFilePath Path to PDF file
|
||||
*/
|
||||
protected function unlockPdf(string $pdfFilePath): void
|
||||
{
|
||||
try {
|
||||
$filename = pathinfo($pdfFilePath, PATHINFO_FILENAME);
|
||||
$directory = pathinfo($pdfFilePath, PATHINFO_DIRNAME);
|
||||
$decryptedPdfPath = $directory . '/' . $filename . '.dec.pdf';
|
||||
$finalPdfPath = $directory . '/' . $filename . '.pdf';
|
||||
|
||||
// Skip if the decrypted file already exists
|
||||
if (File::exists($decryptedPdfPath)) {
|
||||
Log::info("Decrypted file already exists: {$decryptedPdfPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create qpdf command
|
||||
$command = ['qpdf', '--password=' . $this->password, '--decrypt', $pdfFilePath, $decryptedPdfPath];
|
||||
|
||||
// Execute the command
|
||||
$process = new Process($command);
|
||||
$process->run();
|
||||
|
||||
// Check if the command was successful
|
||||
if (!$process->isSuccessful()) {
|
||||
throw new ProcessFailedException($process);
|
||||
}
|
||||
|
||||
Log::info("Unlocked PDF: {$pdfFilePath} to {$decryptedPdfPath}");
|
||||
|
||||
// Remove the original encrypted file after successful decryption
|
||||
if (File::exists($decryptedPdfPath)) {
|
||||
// Delete the encrypted file
|
||||
File::delete($pdfFilePath);
|
||||
Log::info("Removed encrypted file: {$pdfFilePath}");
|
||||
|
||||
// Rename the decrypted file (remove .dec extension)
|
||||
File::move($decryptedPdfPath, $finalPdfPath);
|
||||
Log::info("Renamed decrypted file from {$decryptedPdfPath} to {$finalPdfPath}");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error unlocking PDF {$pdfFilePath}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
@@ -2,10 +2,18 @@
|
||||
|
||||
namespace Modules\Webstatement\Mail;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use 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;
|
||||
|
||||
class StatementEmail extends Mailable
|
||||
{
|
||||
@@ -14,9 +22,11 @@
|
||||
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
|
||||
@@ -29,43 +39,164 @@
|
||||
$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 = 'Your Account Statement';
|
||||
$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 .= " - {$this->statement->period_from}";
|
||||
$subject .= " - " . Carbon::createFromFormat('Ym', $this->statement->period_from)
|
||||
->locale('id')
|
||||
->isoFormat('MMMM Y');
|
||||
}
|
||||
|
||||
$email = $this->subject($subject)
|
||||
->view('webstatement::statements.email')
|
||||
->with([
|
||||
'statement' => $this->statement,
|
||||
'accountNumber' => $this->statement->account_number,
|
||||
'periodFrom' => $this->statement->period_from,
|
||||
'periodTo' => $this->statement->period_to,
|
||||
'isRange' => $this->statement->is_period_range,
|
||||
]);
|
||||
$email = $this->subject($subject);
|
||||
|
||||
if ($this->isZip) {
|
||||
$fileName = "{$this->statement->account_number}_{$this->statement->period_from}_to_{$this->statement->period_to}.zip";
|
||||
$email->attach($this->filePath, [
|
||||
'as' => $fileName,
|
||||
'mime' => 'application/zip',
|
||||
]);
|
||||
} else {
|
||||
$fileName = "{$this->statement->account_number}_{$this->statement->period_from}.pdf";
|
||||
$email->attach($this->filePath, [
|
||||
'as' => $fileName,
|
||||
'mime' => 'application/pdf',
|
||||
]);
|
||||
// Store the email in the message property for later use in toSymfonyEmail()
|
||||
$this->message = $email;
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Laravel message to Symfony Email
|
||||
*/
|
||||
protected function toSymfonyEmail()
|
||||
{
|
||||
// Build the message if it hasn't been built yet
|
||||
$this->build();
|
||||
// Create a new Symfony Email
|
||||
$email = new Email();
|
||||
|
||||
// Set from address using config values instead of trying to call getFrom()
|
||||
$fromAddress = Config::get('mail.from.address');
|
||||
$fromName = Config::get('mail.from.name');
|
||||
$email->from($fromName ? "$fromName <$fromAddress>" : $fromAddress);
|
||||
|
||||
// Set to addresses - use the to addresses from the mailer instead of trying to call getTo()
|
||||
// We'll get the to addresses from the Mail facade when the email is sent
|
||||
// For now, we'll just add a placeholder recipient that will be overridden by the Mail facade
|
||||
$email->to($this->message->to[0]['address']);
|
||||
|
||||
$email->subject($this->message->subject);
|
||||
|
||||
// Set body - use a simple HTML content instead of trying to call getHtmlBody()
|
||||
// In a real implementation, we would need to find a way to access the rendered HTML content
|
||||
$email->html(view('webstatement::statements.email', [
|
||||
'statement' => $this->statement,
|
||||
'accountNumber' => $this->statement->account_number,
|
||||
'periodFrom' => $this->statement->period_from,
|
||||
'periodTo' => $this->statement->period_to,
|
||||
'isRange' => $this->statement->is_period_range,
|
||||
'requestType' => $this->statement->request_type,
|
||||
'batchId' => $this->statement->batch_id,
|
||||
'accounts' => Account::where('account_number', $this->statement->account_number)->first()
|
||||
])->render());
|
||||
//$email->text($this->message->getTextBody());
|
||||
|
||||
// Add attachments - use the file path directly instead of trying to call getAttachments()
|
||||
if ($this->filePath && file_exists($this->filePath)) {
|
||||
if ($this->isZip) {
|
||||
$fileName = "{$this->statement->account_number}_{$this->statement->period_from}_to_{$this->statement->period_to}.zip";
|
||||
$contentType = 'application/zip';
|
||||
} else {
|
||||
$fileName = "{$this->statement->account_number}_{$this->statement->period_from}.pdf";
|
||||
$contentType = 'application/pdf';
|
||||
}
|
||||
$email->attachFromPath($this->filePath, $fileName, $contentType);
|
||||
}
|
||||
|
||||
return $email;
|
||||
|
||||
@@ -30,16 +30,4 @@
|
||||
'trans_status',
|
||||
'proc_code',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'booking_date' => 'datetime',
|
||||
'value_date' => 'datetime',
|
||||
'txn_amount' => 'decimal:2',
|
||||
'chrg_amount' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
|
||||
69
app/Models/AtmTransactionReportLog.php
Normal file
69
app/Models/AtmTransactionReportLog.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Modules\Usermanagement\Models\User;
|
||||
|
||||
class AtmTransactionReportLog extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*/
|
||||
protected $fillable = [
|
||||
'period',
|
||||
'report_date',
|
||||
'status',
|
||||
'authorization_status',
|
||||
'file_path',
|
||||
'file_size',
|
||||
'record_count',
|
||||
'error_message',
|
||||
'is_downloaded',
|
||||
'downloaded_at',
|
||||
'user_id',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'authorized_by',
|
||||
'authorized_at',
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*/
|
||||
protected $casts = [
|
||||
'report_date' => 'date',
|
||||
'downloaded_at' => 'datetime',
|
||||
'authorized_at' => 'datetime',
|
||||
'is_downloaded' => 'boolean',
|
||||
'file_size' => 'integer',
|
||||
'record_count' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the user who created this report request.
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who created this report request.
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who authorized this report request.
|
||||
*/
|
||||
public function authorizer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'authorized_by');
|
||||
}
|
||||
}
|
||||
@@ -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']
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Wildside\Userstamps\Userstamps;
|
||||
use Mattiverse\Userstamps\Traits\Userstamps;
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,10 @@ class Customer extends Model
|
||||
'postal_code',
|
||||
'branch_code',
|
||||
'date_of_birth',
|
||||
'email'
|
||||
'email',
|
||||
'sector',
|
||||
'customer_type',
|
||||
'birth_incorp_date'
|
||||
];
|
||||
|
||||
public function accounts(){
|
||||
|
||||
@@ -61,14 +61,4 @@
|
||||
'co_code',
|
||||
'date_time'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'amount_lcy' => 'decimal:2',
|
||||
'amount_fcy' => 'decimal:2',
|
||||
'exchange_rate' => 'decimal:6',
|
||||
'value_date' => 'date',
|
||||
'exposure_date' => 'date',
|
||||
'accounting_date' => 'date',
|
||||
'date_time' => 'datetime'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -49,13 +49,4 @@
|
||||
'txn_code_cr',
|
||||
'txn_code_dr',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'date_time' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,186 +1,288 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Models;
|
||||
namespace Modules\Webstatement\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Modules\Basicdata\Models\Branch;
|
||||
use Modules\Usermanagement\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Modules\Basicdata\Models\Branch;
|
||||
use Modules\Usermanagement\Models\User;
|
||||
|
||||
class PrintStatementLog extends Model
|
||||
class PrintStatementLog extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'branch_code',
|
||||
'account_number',
|
||||
'request_type',
|
||||
'batch_id',
|
||||
'target_accounts',
|
||||
'total_accounts',
|
||||
'processed_accounts',
|
||||
'success_count',
|
||||
'failed_count',
|
||||
'status',
|
||||
'started_at',
|
||||
'completed_at',
|
||||
'error_message',
|
||||
'period_from',
|
||||
'period_to',
|
||||
'is_period_range',
|
||||
'is_available',
|
||||
'is_downloaded',
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
'downloaded_at',
|
||||
'authorization_status',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
'authorized_by',
|
||||
'authorized_at',
|
||||
'remarks',
|
||||
'email',
|
||||
'email_sent_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_period_range' => 'boolean',
|
||||
'is_available' => 'boolean',
|
||||
'is_downloaded' => 'boolean',
|
||||
'downloaded_at' => 'datetime',
|
||||
'authorized_at' => 'datetime',
|
||||
'started_at' => 'datetime',
|
||||
'completed_at' => 'datetime',
|
||||
'target_accounts' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the formatted period display
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPeriodDisplayAttribute()
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'branch_code',
|
||||
'account_number',
|
||||
'period_from',
|
||||
'period_to',
|
||||
'is_period_range',
|
||||
'is_available',
|
||||
'is_downloaded',
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
'downloaded_at',
|
||||
'authorization_status',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
'authorized_by',
|
||||
'authorized_at',
|
||||
'remarks',
|
||||
'email',
|
||||
'email_sent_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_period_range' => 'boolean',
|
||||
'is_available' => 'boolean',
|
||||
'is_downloaded' => 'boolean',
|
||||
'downloaded_at' => 'datetime',
|
||||
'authorized_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the formatted period display
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPeriodDisplayAttribute()
|
||||
{
|
||||
if ($this->is_period_range) {
|
||||
return $this->formatPeriod($this->period_from) . ' - ' . $this->formatPeriod($this->period_to);
|
||||
}
|
||||
|
||||
return $this->formatPeriod($this->period_from);
|
||||
if ($this->is_period_range) {
|
||||
return $this->formatPeriod($this->period_from) . ' - ' . $this->formatPeriod($this->period_to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format period from YYYYMM to Month Year
|
||||
*
|
||||
* @param string $period
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function formatPeriod($period)
|
||||
{
|
||||
if (strlen($period) !== 6) {
|
||||
return $period;
|
||||
}
|
||||
|
||||
$year = substr($period, 0, 4);
|
||||
$month = substr($period, 4, 2);
|
||||
|
||||
return date('F Y', mktime(0, 0, 0, (int) $month, 1, (int) $year));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who requested the statement
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who created the record
|
||||
*/
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who updated the record
|
||||
*/
|
||||
public function updater()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who authorized the record
|
||||
*/
|
||||
public function authorizer()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'authorized_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include pending authorization records
|
||||
*/
|
||||
public function scopePending($query)
|
||||
{
|
||||
return $query->where('authorization_status', 'pending');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include approved records
|
||||
*/
|
||||
public function scopeApproved($query)
|
||||
{
|
||||
return $query->where('authorization_status', 'approved');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include rejected records
|
||||
*/
|
||||
public function scopeRejected($query)
|
||||
{
|
||||
return $query->where('authorization_status', 'rejected');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include downloaded records
|
||||
*/
|
||||
public function scopeDownloaded($query)
|
||||
{
|
||||
return $query->where('is_downloaded', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include available records
|
||||
*/
|
||||
public function scopeAvailable($query)
|
||||
{
|
||||
return $query->where('is_available', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the statement is for a single period
|
||||
*/
|
||||
public function isSinglePeriod()
|
||||
{
|
||||
return !$this->is_period_range;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the statement is authorized
|
||||
*/
|
||||
public function isAuthorized()
|
||||
{
|
||||
return $this->authorization_status === 'approved';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the statement is rejected
|
||||
*/
|
||||
public function isRejected()
|
||||
{
|
||||
return $this->authorization_status === 'rejected';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the statement is pending authorization
|
||||
*/
|
||||
public function isPending()
|
||||
{
|
||||
return $this->authorization_status === 'pending';
|
||||
}
|
||||
|
||||
public function branch(){
|
||||
return $this->belongsTo(Branch::class, 'branch_code','code');
|
||||
}
|
||||
return $this->formatPeriod($this->period_from);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format period from YYYYMM to Month Year
|
||||
*
|
||||
* @param string $period
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function formatPeriod($period)
|
||||
{
|
||||
if (strlen($period) !== 6) {
|
||||
return $period;
|
||||
}
|
||||
|
||||
$year = substr($period, 0, 4);
|
||||
$month = substr($period, 4, 2);
|
||||
|
||||
return date('F Y', mktime(0, 0, 0, (int) $month, 1, (int) $year));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who requested the statement
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who created the record
|
||||
*/
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who updated the record
|
||||
*/
|
||||
public function updater()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who authorized the record
|
||||
*/
|
||||
public function authorizer()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'authorized_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include pending authorization records
|
||||
*/
|
||||
public function scopePending($query)
|
||||
{
|
||||
return $query->where('authorization_status', 'pending');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include approved records
|
||||
*/
|
||||
public function scopeApproved($query)
|
||||
{
|
||||
return $query->where('authorization_status', 'approved');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include rejected records
|
||||
*/
|
||||
public function scopeRejected($query)
|
||||
{
|
||||
return $query->where('authorization_status', 'rejected');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include downloaded records
|
||||
*/
|
||||
public function scopeDownloaded($query)
|
||||
{
|
||||
return $query->where('is_downloaded', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include available records
|
||||
*/
|
||||
public function scopeAvailable($query)
|
||||
{
|
||||
return $query->where('is_available', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the statement is for a single period
|
||||
*/
|
||||
public function isSinglePeriod()
|
||||
{
|
||||
return !$this->is_period_range;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the statement is authorized
|
||||
*/
|
||||
public function isAuthorized()
|
||||
{
|
||||
return $this->authorization_status === 'approved';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the statement is rejected
|
||||
*/
|
||||
public function isRejected()
|
||||
{
|
||||
return $this->authorization_status === 'rejected';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the statement is pending authorization
|
||||
*/
|
||||
public function isPending()
|
||||
{
|
||||
return $this->authorization_status === 'pending';
|
||||
}
|
||||
|
||||
public function branch(){
|
||||
return $this->belongsTo(Branch::class, 'branch_code','code');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a single account request
|
||||
*/
|
||||
public function isSingleAccountRequest()
|
||||
{
|
||||
return $this->request_type === 'single_account';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a branch request
|
||||
*/
|
||||
public function isBranchRequest()
|
||||
{
|
||||
return $this->request_type === 'branch';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is an all branches request
|
||||
*/
|
||||
public function isAllBranchesRequest()
|
||||
{
|
||||
return $this->request_type === 'all_branches';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if processing is completed
|
||||
*/
|
||||
public function isCompleted()
|
||||
{
|
||||
return $this->status === 'completed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if processing is in progress
|
||||
*/
|
||||
public function isProcessing()
|
||||
{
|
||||
return $this->status === 'processing';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if processing failed
|
||||
*/
|
||||
public function isFailed()
|
||||
{
|
||||
return $this->status === 'failed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get progress percentage
|
||||
*/
|
||||
public function getProgressPercentage()
|
||||
{
|
||||
if (!$this->total_accounts || $this->total_accounts == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return round(($this->processed_accounts / $this->total_accounts) * 100, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get success rate percentage
|
||||
*/
|
||||
public function getSuccessRate()
|
||||
{
|
||||
if (!$this->processed_accounts || $this->processed_accounts == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return round(($this->success_count / $this->processed_accounts) * 100, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for batch requests
|
||||
*/
|
||||
public function scopeBatch($query)
|
||||
{
|
||||
return $query->whereIn('request_type', ['branch', 'all_branches']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for single account requests
|
||||
*/
|
||||
public function scopeSingleAccount($query)
|
||||
{
|
||||
return $query->where('request_type', 'single_account');
|
||||
}
|
||||
}
|
||||
|
||||
29
app/Models/Sector.php
Normal file
29
app/Models/Sector.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Sector extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*/
|
||||
protected $fillable = [
|
||||
'date_time',
|
||||
'description',
|
||||
'curr_no',
|
||||
'co_code',
|
||||
'sector_code'
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*/
|
||||
protected $casts = [
|
||||
'date_time' => 'datetime',
|
||||
];
|
||||
}
|
||||
@@ -64,4 +64,16 @@ class StmtEntry extends Model
|
||||
public function transaction(){
|
||||
return $this->belongsTo(TempTransaction::class, 'transaction_code', 'transaction_code');
|
||||
}
|
||||
|
||||
public function tt(){
|
||||
return $this->belongsTo(Teller::class, 'trans_reference', 'id_teller');
|
||||
}
|
||||
|
||||
public function dc(){
|
||||
return $this->belongsTo(DataCapture::class, 'trans_reference', 'id');
|
||||
}
|
||||
|
||||
public function aa(){
|
||||
return $this->belongsTo(TempArrangement::class, 'trans_reference', 'arrangement_id');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
'amount_fcy_2',
|
||||
'rate_2',
|
||||
'customer_1',
|
||||
'last_version'
|
||||
'last_version',
|
||||
'dealer_desk'
|
||||
];
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,13 +2,23 @@
|
||||
|
||||
namespace Modules\Webstatement\Providers;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\Webstatement\Console\GenerateBiayakartuCommand;
|
||||
use Modules\Webstatement\Console\GenerateBiayaKartuCsvCommand;
|
||||
use Modules\Webstatement\Jobs\UpdateAtmCardBranchCurrencyJob;
|
||||
use Nwidart\Modules\Traits\PathNamespace;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
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;
|
||||
|
||||
class WebstatementServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -52,7 +62,17 @@ class WebstatementServiceProvider extends ServiceProvider
|
||||
{
|
||||
$this->commands([
|
||||
GenerateBiayakartuCommand::class,
|
||||
GenerateBiayaKartuCsvCommand::class
|
||||
GenerateBiayaKartuCsvCommand::class,
|
||||
ProcessDailyMigration::class,
|
||||
ExportDailyStatements::class,
|
||||
CombinePdf::class,
|
||||
ConvertHtmlToPdf::class,
|
||||
UnlockPdf::class,
|
||||
ExportPeriodStatements::class,
|
||||
GenerateAtmTransactionReport::class,
|
||||
SendStatementEmailCommand::class,
|
||||
CheckEmailProgressCommand::class,
|
||||
UpdateAllAtmCardsCommand::class
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -82,6 +102,34 @@ class WebstatementServiceProvider extends ServiceProvider
|
||||
->appendOutputTo(storage_path('logs/biaya-kartu-csv-scheduler.log'));
|
||||
|
||||
|
||||
// Schedule the daily migration process to run at 1:00 AM (from previous task)
|
||||
$schedule->command('webstatement:process-daily-migration')
|
||||
->dailyAt('09:00')
|
||||
->withoutOverlapping()
|
||||
->appendOutputTo(storage_path('logs/daily-migration.log'));
|
||||
|
||||
// Schedule the statement export to run at 2:00 AM (after migration is likely complete)
|
||||
$schedule->command('webstatement:export-statements')
|
||||
->dailyAt('09:30')
|
||||
->withoutOverlapping()
|
||||
->appendOutputTo(storage_path('logs/statement-export.log'));
|
||||
|
||||
// Combine PDf
|
||||
$schedule->command('webstatement:combine-pdf')
|
||||
->dailyAt('09:30')
|
||||
->withoutOverlapping()
|
||||
->appendOutputTo(storage_path('logs/combine-pdf.log'));
|
||||
// Convert HTML to PDF
|
||||
$schedule->command('webstatement:convert-html-to-pdf')
|
||||
->dailyAt('09:30')
|
||||
->withoutOverlapping()
|
||||
->appendOutputTo(storage_path('logs/convert-html-to-pdf.log'));
|
||||
|
||||
// Unlock PDF
|
||||
$schedule->command('webstatement:unlock-pdf')
|
||||
->dailyAt('09:30')
|
||||
->withoutOverlapping()
|
||||
->appendOutputTo(storage_path('logs/unlock-pdf.log'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?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('account_balances', function (Blueprint $table) {
|
||||
// First drop the unique constraint since we'll be making these columns the primary key
|
||||
$table->dropUnique(['account_number', 'period']);
|
||||
|
||||
// Drop the id column and its auto-increment primary key
|
||||
$table->dropColumn('id');
|
||||
|
||||
// Set the composite primary key
|
||||
$table->primary(['account_number', 'period']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('account_balances', function (Blueprint $table) {
|
||||
// Drop the composite primary key
|
||||
$table->dropPrimary(['account_number', 'period']);
|
||||
|
||||
// Add back the id column with auto-increment
|
||||
$table->id()->first();
|
||||
|
||||
// Re-add the unique constraint
|
||||
$table->unique(['account_number', 'period']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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('processed_statements', function (Blueprint $table) {
|
||||
// Drop the id column and its auto-increment primary key
|
||||
$table->dropColumn('id');
|
||||
|
||||
// Set the composite primary key using account_number, period, and sequence_no
|
||||
// This combination should be unique for each record
|
||||
$table->primary(['account_number', 'period', 'sequence_no']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('processed_statements', function (Blueprint $table) {
|
||||
// Drop the composite primary key
|
||||
$table->dropPrimary(['account_number', 'period', 'sequence_no']);
|
||||
|
||||
// Add back the id column with auto-increment
|
||||
$table->id()->first();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
* Change date_of_birth column from date to string type
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('customers', function (Blueprint $table) {
|
||||
// First modify the column to string type
|
||||
$table->string('date_of_birth')->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
* Change date_of_birth column back to date type
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('customers', function (Blueprint $table) {
|
||||
// Convert back to date type
|
||||
$table->date('date_of_birth')->nullable()->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
* Change date fields to string type in accounts table
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('accounts', function (Blueprint $table) {
|
||||
// Change opening_date from date to string
|
||||
$table->string('opening_date')->nullable()->change();
|
||||
|
||||
// Change closure_date from date to string
|
||||
$table->string('closure_date')->nullable()->change();
|
||||
|
||||
// Fix the start_year_bal column which has incorrect parameters
|
||||
// First drop the column
|
||||
$table->string('start_year_bal',255)->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
* Change string fields back to date type
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('accounts', function (Blueprint $table) {
|
||||
// Change opening_date back to date
|
||||
$table->date('opening_date')->nullable()->change();
|
||||
|
||||
// Change closure_date back to date
|
||||
$table->date('closure_date')->nullable()->change();
|
||||
|
||||
// Drop and recreate start_year_bal with original definition
|
||||
$table->string('start_year_bal',15)->nullable()->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
* Change date_time column from dateTime to string type
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('ft_txn_type_condition', function (Blueprint $table) {
|
||||
// Change date_time from dateTime to string
|
||||
$table->string('date_time')->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
* Change date_time column back to dateTime type
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('ft_txn_type_condition', function (Blueprint $table) {
|
||||
// Change date_time back to dateTime
|
||||
$table->dateTime('date_time')->nullable()->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
* Change date and dateTime fields to string type in data_captures table
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('data_captures', function (Blueprint $table) {
|
||||
// Change date fields to string
|
||||
$table->string('value_date')->nullable()->change();
|
||||
$table->string('exposure_date')->nullable()->change();
|
||||
$table->string('accounting_date')->nullable()->change();
|
||||
|
||||
// Change dateTime field to string
|
||||
$table->string('date_time')->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
* Change string fields back to date and dateTime types
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('data_captures', function (Blueprint $table) {
|
||||
// Change string fields back to date
|
||||
$table->date('value_date')->nullable()->change();
|
||||
$table->date('exposure_date')->nullable()->change();
|
||||
$table->date('accounting_date')->nullable()->change();
|
||||
|
||||
// Change string field back to dateTime
|
||||
$table->dateTime('date_time')->nullable()->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
* Change decimal fields to string type in data_captures table
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('data_captures', function (Blueprint $table) {
|
||||
// Change decimal fields to string
|
||||
$table->string('amount_lcy')->nullable()->change();
|
||||
$table->string('amount_fcy')->nullable()->change();
|
||||
$table->string('exchange_rate')->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
* Change string fields back to decimal types
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('data_captures', function (Blueprint $table) {
|
||||
// Change string fields back to decimal with original precision and scale
|
||||
$table->decimal('amount_lcy', 20, 2)->nullable()->change();
|
||||
$table->decimal('amount_fcy', 20, 2)->nullable()->change();
|
||||
$table->decimal('exchange_rate', 20, 6)->nullable()->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
* Change date fields to string type in temp_arrangements table
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('temp_arrangements', function (Blueprint $table) {
|
||||
// Change date fields to string
|
||||
$table->string('orig_contract_date')->nullable()->change();
|
||||
$table->string('start_date')->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
* Change string fields back to date type
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('temp_arrangements', function (Blueprint $table) {
|
||||
// Change string fields back to date
|
||||
$table->date('orig_contract_date')->nullable()->change();
|
||||
$table->date('start_date')->nullable()->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
<?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('temp_funds_transfer', function (Blueprint $table) {
|
||||
$table->text('at_unique_id')->nullable();
|
||||
$table->text('bif_ref_no')->nullable();
|
||||
$table->text('atm_order_id')->nullable();
|
||||
$table->text('api_iss_acct')->nullable();
|
||||
$table->text('api_benff_acct')->nullable();
|
||||
$table->text('remarks')->nullable();
|
||||
$table->text('api_mrchn_id')->nullable();
|
||||
$table->text('bif_rcv_acct')->nullable();
|
||||
$table->text('bif_snd_acct')->nullable();
|
||||
$table->text('bif_rcv_name')->nullable();
|
||||
$table->text('bif_va_no')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('temp_funds_transfer', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'at_unique_id',
|
||||
'bif_ref_no',
|
||||
'atm_order_id',
|
||||
'api_iss_acct',
|
||||
'api_benff_acct',
|
||||
'remarks',
|
||||
'api_mrchn_id',
|
||||
'bif_rcv_acct',
|
||||
'bif_snd_acct',
|
||||
'bif_rcv_name',
|
||||
'bif_va_no'
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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('tellers', function (Blueprint $table) {
|
||||
$table->string('dealer_desk')->nullable()->after('last_version');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('tellers', function (Blueprint $table) {
|
||||
$table->dropColumn('dealer_desk');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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('stmt_entry', function (Blueprint $table) {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('stmt_entry', function (Blueprint $table) {
|
||||
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?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::create('sectors', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->dateTime('date_time');
|
||||
$table->text('description');
|
||||
$table->string('curr_no');
|
||||
$table->string('co_code');
|
||||
$table->string('sector_code');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sectors');
|
||||
}
|
||||
};
|
||||
@@ -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('customers', function (Blueprint $table) {
|
||||
$table->string('sector')->nullable()->after('branch_code');
|
||||
$table->string('customer_type')->nullable()->after('sector');
|
||||
$table->string('birth_incorp_date')->nullable()->after('date_of_birth');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('customers', function (Blueprint $table) {
|
||||
$table->dropColumn(['sector', 'customer_type', 'birth_incorp_date']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
<?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::create('atm_transaction_report_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('period', 8); // Format: Ymd (20250512)
|
||||
$table->date('report_date');
|
||||
$table->enum('status', ['pending', 'processing', 'completed', 'failed'])->default('pending');
|
||||
$table->enum('authorization_status', ['pending', 'approved', 'rejected'])->default('pending');
|
||||
$table->string('file_path')->nullable();
|
||||
$table->bigInteger('file_size')->nullable();
|
||||
$table->integer('record_count')->nullable();
|
||||
$table->text('error_message')->nullable();
|
||||
$table->boolean('is_downloaded')->default(false);
|
||||
$table->timestamp('downloaded_at')->nullable();
|
||||
$table->unsignedBigInteger('user_id');
|
||||
$table->unsignedBigInteger('created_by');
|
||||
$table->unsignedBigInteger('updated_by')->nullable();
|
||||
$table->unsignedBigInteger('authorized_by')->nullable();
|
||||
$table->timestamp('authorized_at')->nullable();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['period']);
|
||||
$table->index(['status']);
|
||||
$table->index(['authorization_status']);
|
||||
$table->index(['created_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('atm_transaction_report_logs');
|
||||
}
|
||||
};
|
||||
@@ -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,95 @@
|
||||
<?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) {
|
||||
// Field untuk mendukung pengiriman dinamis
|
||||
$table->enum('request_type', ['single_account', 'branch', 'all_branches'])
|
||||
->default('single_account')
|
||||
->after('account_number')
|
||||
->comment('Type of statement request');
|
||||
|
||||
$table->string('batch_id')->nullable()
|
||||
->after('request_type')
|
||||
->comment('Batch ID for bulk operations');
|
||||
|
||||
$table->json('target_accounts')->nullable()
|
||||
->after('batch_id')
|
||||
->comment('JSON array of target account numbers for batch processing');
|
||||
|
||||
$table->integer('total_accounts')->nullable()
|
||||
->after('target_accounts')
|
||||
->comment('Total number of accounts in batch');
|
||||
|
||||
$table->integer('processed_accounts')->default(0)
|
||||
->after('total_accounts')
|
||||
->comment('Number of accounts processed');
|
||||
|
||||
$table->integer('success_count')->default(0)
|
||||
->after('processed_accounts')
|
||||
->comment('Number of successful email sends');
|
||||
|
||||
$table->integer('failed_count')->default(0)
|
||||
->after('success_count')
|
||||
->comment('Number of failed email sends');
|
||||
|
||||
$table->enum('status', ['pending', 'processing', 'completed', 'failed'])
|
||||
->default('pending')
|
||||
->after('failed_count')
|
||||
->comment('Overall status of the request');
|
||||
|
||||
$table->timestamp('started_at')->nullable()
|
||||
->after('status')
|
||||
->comment('When processing started');
|
||||
|
||||
$table->timestamp('completed_at')->nullable()
|
||||
->after('started_at')
|
||||
->comment('When processing completed');
|
||||
|
||||
$table->text('error_message')->nullable()
|
||||
->after('completed_at')
|
||||
->comment('Error message if processing failed');
|
||||
|
||||
// Ubah account_number menjadi nullable untuk request batch
|
||||
$table->string('account_number')->nullable()->change();
|
||||
|
||||
// Index untuk performa
|
||||
$table->index(['request_type', 'status']);
|
||||
$table->index(['batch_id']);
|
||||
$table->index(['branch_code', 'request_type']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('print_statement_logs', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'request_type',
|
||||
'batch_id',
|
||||
'target_accounts',
|
||||
'total_accounts',
|
||||
'processed_accounts',
|
||||
'success_count',
|
||||
'failed_count',
|
||||
'status',
|
||||
'started_at',
|
||||
'completed_at',
|
||||
'error_message'
|
||||
]);
|
||||
|
||||
$table->string('account_number')->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
27
module.json
27
module.json
@@ -30,7 +30,8 @@
|
||||
"attributes": [],
|
||||
"permission": "",
|
||||
"roles": [
|
||||
"administrator"
|
||||
"administrator",
|
||||
"customer_service"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -63,6 +64,19 @@
|
||||
"roles": []
|
||||
}
|
||||
],
|
||||
"laporan": [
|
||||
{
|
||||
"title": "Laporan Transaksi ATM",
|
||||
"path": "atm-reports",
|
||||
"icon": "ki-filled ki-printer text-lg text-primary",
|
||||
"classes": "",
|
||||
"attributes": [],
|
||||
"permission": "",
|
||||
"roles": [
|
||||
"administrator"
|
||||
]
|
||||
}
|
||||
],
|
||||
"master": [
|
||||
{
|
||||
"title": "Basic Data",
|
||||
@@ -121,6 +135,17 @@
|
||||
"roles": [
|
||||
"administrator"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Log Email Statement",
|
||||
"path": "email-statement-logs",
|
||||
"icon": "ki-filled ki-message-text-2 text-lg text-primary",
|
||||
"classes": "",
|
||||
"attributes": [],
|
||||
"permission": "",
|
||||
"roles": [
|
||||
"administrator"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
322
resources/views/atm-reports/index.blade.php
Normal file
322
resources/views/atm-reports/index.blade.php
Normal file
@@ -0,0 +1,322 @@
|
||||
@extends('layouts.main')
|
||||
|
||||
@section('title', 'ATM Transaction Reports')
|
||||
|
||||
@section('breadcrumbs')
|
||||
{{ Breadcrumbs::render(request()->route()->getName()) }}
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="grid grid-cols-8 gap-5">
|
||||
<div class="col-span-2 card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Request ATM Transaction Report</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('atm-reports.store') }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="grid grid-cols-1 gap-5">
|
||||
<div class="form-group">
|
||||
<label class="form-label required" for="report_date">Report Date</label>
|
||||
<input type="date" class="input form-control @error('report_date') is-invalid @enderror"
|
||||
id="report_date" name="report_date" value="{{ old('report_date') }}" required>
|
||||
@error('report_date')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="w-full btn btn-primary">
|
||||
<i class="ki-filled ki-plus"></i>
|
||||
Generate Report
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-6">
|
||||
<div class="min-w-full card card-grid" data-datatable="false" data-datatable-page-size="10"
|
||||
data-datatable-state-save="false" id="atm-reports-table"
|
||||
data-api-url="{{ route('atm-reports.datatables') }}">
|
||||
<div class="flex-wrap py-5 card-header">
|
||||
<h3 class="card-title">
|
||||
ATM Transaction Reports
|
||||
</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 Reports" id="search" type="text" value="">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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="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="report_date">
|
||||
<span class="sort"> <span class="sort-label"> Report Date </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="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="authorization_status">
|
||||
<span class="sort"> <span class="sort-label"> Authorization </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">
|
||||
Show
|
||||
<select class="w-16 select select-sm" data-datatable-size="true" name="perpage"> </select> per
|
||||
page
|
||||
</div>
|
||||
<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() }}'
|
||||
}
|
||||
});
|
||||
|
||||
$.ajax(`atm-reports/${data}`, {
|
||||
type: 'DELETE'
|
||||
}).then((response) => {
|
||||
swal.fire('Deleted!', 'ATM Transaction report 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');
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Add the missing retryReport function
|
||||
function retryReport(id) {
|
||||
Swal.fire({
|
||||
title: 'Are you sure?',
|
||||
text: 'This will reset the current job and start a new one.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Yes, retry it!'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$.ajaxSetup({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
}
|
||||
});
|
||||
|
||||
$.ajax(`atm-reports/${id}/retry`, {
|
||||
type: 'POST'
|
||||
}).then((response) => {
|
||||
Swal.fire('Success!', 'Report retry initiated successfully.', 'success').then(
|
||||
() => {
|
||||
window.location.reload();
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.error('Error:', error);
|
||||
Swal.fire('Error!', 'Failed to retry report: ' + (error.responseJSON?.message ||
|
||||
'Unknown error'), 'error');
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
const element = document.querySelector('#atm-reports-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',
|
||||
},
|
||||
period: {
|
||||
title: 'Period',
|
||||
render: (item, data) => {
|
||||
return data.period || '';
|
||||
},
|
||||
},
|
||||
report_date: {
|
||||
title: 'Report Date',
|
||||
render: (item, data) => {
|
||||
return data.report_date || '';
|
||||
},
|
||||
},
|
||||
status: {
|
||||
title: 'Status',
|
||||
render: (item, data) => {
|
||||
let statusClass = 'badge badge-light-primary';
|
||||
let statusText = data.status;
|
||||
|
||||
if (data.status === 'completed') {
|
||||
statusClass = 'badge badge-light-success';
|
||||
} else if (data.status === 'failed') {
|
||||
statusClass = 'badge badge-light-danger';
|
||||
} else if (data.status === 'processing') {
|
||||
if (data.is_processing_timeout) {
|
||||
statusClass = 'badge badge-light-danger';
|
||||
statusText += ` (${data.processing_hours}h)`;
|
||||
} else {
|
||||
statusClass = 'badge badge-light-warning';
|
||||
}
|
||||
} else if (data.status === 'pending') {
|
||||
statusClass = 'badge badge-light-info';
|
||||
}
|
||||
|
||||
return `<span class="${statusClass}">${statusText}</span>`;
|
||||
},
|
||||
},
|
||||
authorization_status: {
|
||||
title: 'Authorization',
|
||||
render: (item, data) => {
|
||||
let statusClass = 'badge badge-light-primary';
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
return `<span class="${statusClass}">${data.authorization_status || 'pending'}</span>`;
|
||||
},
|
||||
},
|
||||
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="atm-reports/${data.id}">
|
||||
<i class="ki-outline ki-eye"></i>
|
||||
</a>`;
|
||||
|
||||
// Show download button if report is completed
|
||||
if (data.status === 'completed' && data.file_path) {
|
||||
buttons += `<a class="btn btn-sm btn-icon btn-clear btn-success" href="atm-reports/${data.id}/download">
|
||||
<i class="ki-outline ki-cloud-download"></i>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
// Retry button
|
||||
if (data.can_retry) {
|
||||
let retryClass = 'btn-warning';
|
||||
if (data.is_processing_timeout) {
|
||||
retryClass = 'btn-danger';
|
||||
}
|
||||
buttons += `<button class="btn btn-sm btn-icon btn-clear ${retryClass}" onclick="retryReport(${data.id})">
|
||||
<i class="ki-outline ki-arrows-circle"></i>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
// Only show delete button if status is pending or failed
|
||||
if (data.status === 'pending' || data.status === 'failed') {
|
||||
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;
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let dataTable = new KTDataTable(element, dataTableOptions);
|
||||
// Custom search functionality
|
||||
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"]');
|
||||
|
||||
rowCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = isChecked;
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
291
resources/views/atm-reports/show.blade.php
Normal file
291
resources/views/atm-reports/show.blade.php
Normal file
@@ -0,0 +1,291 @@
|
||||
@extends('layouts.main')
|
||||
|
||||
@section('content')
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">ATM Transaction Report Details</h3>
|
||||
<div class="card-toolbar">
|
||||
<a href="{{ route('atm-reports.index') }}" class="btn btn-sm btn-info me-2">
|
||||
<i class="ki-duotone ki-arrow-left fs-2"></i>Back to List
|
||||
</a>
|
||||
|
||||
@if ($atmReport->status === 'completed' && $atmReport->file_path)
|
||||
<a href="{{ route('atm-reports.download', $atmReport->id) }}" class="btn btn-sm btn-primary">
|
||||
<i class="ki-duotone ki-document fs-2"></i>Download Report
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$canRetry = in_array($atmReport->status, ['failed', 'pending']) ||
|
||||
($atmReport->status === 'processing' && $atmReport->updated_at->diffInHours(now()) >= 1) ||
|
||||
($atmReport->status === 'completed' && !$atmReport->file_path);
|
||||
@endphp
|
||||
|
||||
@if ($canRetry)
|
||||
<form action="{{ route('atm-reports.retry', $atmReport->id) }}" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to retry generating this report?')">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-sm btn-warning me-2">
|
||||
<i class="ki-duotone ki-arrows-circle fs-2"></i>
|
||||
@if($atmReport->status === 'processing' && $atmReport->updated_at->diffInHours(now()) >= 1)
|
||||
Retry (Timeout)
|
||||
@else
|
||||
Retry Job
|
||||
@endif
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
@if (session('success'))
|
||||
<div class="alert alert-success">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (session('error'))
|
||||
<div class="alert alert-danger">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="grid grid-cols-2 gap-5 g-5">
|
||||
<!-- Left Column - Report Information -->
|
||||
<div class="shadow-sm card card-flush h-xl-100">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<h2>Report Information</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-5 card-body">
|
||||
<div class="gap-5 d-flex flex-column">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="text-gray-500 fw-semibold">Period</div>
|
||||
<div class="fw-bold fs-5">
|
||||
@php
|
||||
// Convert format YYYYMMDD to readable date
|
||||
$date = \Carbon\Carbon::createFromFormat('Ymd', $atmReport->period);
|
||||
$periodText = $date->format('d F Y');
|
||||
@endphp
|
||||
{{ $periodText }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
<div class="text-gray-500 fw-semibold">Report Date</div>
|
||||
<div class="fw-bold fs-5">{{ $atmReport->report_date }}</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
<div class="text-gray-500 fw-semibold">Status</div>
|
||||
<div>
|
||||
@if ($atmReport->status === 'pending')
|
||||
<span class="badge badge-info">Pending</span>
|
||||
@elseif($atmReport->status === 'processing')
|
||||
@php
|
||||
$processingHours = $atmReport->updated_at->diffInHours(now());
|
||||
@endphp
|
||||
<span class="badge {{ $processingHours >= 1 ? 'badge-danger' : 'badge-warning' }}">
|
||||
Processing
|
||||
@if($processingHours >= 1)
|
||||
({{ $processingHours }}h - Timeout)
|
||||
@endif
|
||||
</span>
|
||||
@if($processingHours >= 1)
|
||||
<div class="mt-1 text-danger small">
|
||||
Processing for more than 1 hour. You can retry this job.
|
||||
</div>
|
||||
@endif
|
||||
@elseif($atmReport->status === 'completed')
|
||||
<span class="badge badge-success">Completed</span>
|
||||
@elseif($atmReport->status === 'failed')
|
||||
<span class="badge badge-danger">Failed</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
<div class="text-gray-500 fw-semibold">Authorization Status</div>
|
||||
<div>
|
||||
@if ($atmReport->authorization_status === 'pending')
|
||||
<span class="badge badge-warning">Pending Authorization</span>
|
||||
@elseif($atmReport->authorization_status === 'approved')
|
||||
<span class="badge badge-success">Approved</span>
|
||||
@elseif($atmReport->authorization_status === 'rejected')
|
||||
<span class="badge badge-danger">Rejected</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($atmReport->status === 'completed')
|
||||
<div class="d-flex flex-column">
|
||||
<div class="text-gray-500 fw-semibold">File Information</div>
|
||||
<div class="fw-bold fs-6">
|
||||
@if ($atmReport->file_path)
|
||||
<div>Path: {{ $atmReport->file_path }}</div>
|
||||
@else
|
||||
<div class="text-warning">File not available -
|
||||
<form action="{{ route('atm-reports.retry', $atmReport->id) }}" method="POST" class="d-inline">
|
||||
@csrf
|
||||
<button type="submit" class="p-0 btn btn-link text-warning" onclick="return confirm('File is missing. Retry generating the report?')">
|
||||
Click here to retry
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
@if ($atmReport->file_size)
|
||||
<div>Size: {{ number_format($atmReport->file_size / 1024, 2) }} KB</div>
|
||||
@endif
|
||||
@if ($atmReport->record_count)
|
||||
<div>Records: {{ number_format($atmReport->record_count) }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($atmReport->status === 'failed' && $atmReport->error_message)
|
||||
<div class="d-flex flex-column">
|
||||
<div class="text-gray-500 fw-semibold">Error Message</div>
|
||||
<div class="text-danger fw-bold fs-6">{{ $atmReport->error_message }}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
<div class="text-gray-500 fw-semibold">Downloaded</div>
|
||||
<div>
|
||||
@if ($atmReport->is_downloaded)
|
||||
<span class="badge badge-success">Yes</span>
|
||||
<div class="mt-1 text-muted">
|
||||
Downloaded at:
|
||||
{{ $atmReport->downloaded_at ? $atmReport->downloaded_at->format('d M Y H:i:s') : 'N/A' }}
|
||||
</div>
|
||||
@else
|
||||
<span class="badge badge-light-primary">No</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column - Request Information -->
|
||||
<div class="shadow-sm card card-flush h-xl-100">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<h2>Request Information</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-5 card-body">
|
||||
<div class="gap-5 d-flex flex-column">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="text-gray-500 fw-semibold">Requested By</div>
|
||||
<div class="fw-bold fs-5">{{ $atmReport->user->name ?? 'N/A' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
<div class="text-gray-500 fw-semibold">Requested At</div>
|
||||
<div class="fw-bold fs-5">{{ dateFormat($atmReport->created_at, 1, 1) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
<div class="text-gray-500 fw-semibold">IP Address</div>
|
||||
<div class="fw-bold fs-5">{{ $atmReport->ip_address }}</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
<div class="text-gray-500 fw-semibold">User Agent</div>
|
||||
<div class="text-muted small">{{ $atmReport->user_agent }}</div>
|
||||
</div>
|
||||
|
||||
@if ($atmReport->authorization_status !== 'pending')
|
||||
<div class="d-flex flex-column">
|
||||
<div class="text-gray-500 fw-semibold">Authorized By</div>
|
||||
<div class="fw-bold fs-5">{{ $atmReport->authorizer->name ?? 'N/A' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
<div class="text-gray-500 fw-semibold">Authorized At</div>
|
||||
<div class="fw-bold fs-5">
|
||||
{{ $atmReport->authorized_at ? $atmReport->authorized_at->format('d M Y H:i:s') : 'N/A' }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($atmReport->created_by && $atmReport->created_by !== $atmReport->user_id)
|
||||
<div class="d-flex flex-column">
|
||||
<div class="text-gray-500 fw-semibold">Created By</div>
|
||||
<div class="fw-bold fs-5">{{ $atmReport->creator->name ?? 'N/A' }}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($atmReport->updated_by)
|
||||
<div class="d-flex flex-column">
|
||||
<div class="text-gray-500 fw-semibold">Last Updated By</div>
|
||||
<div class="fw-bold fs-5">{{ $atmReport->updater->name ?? 'N/A' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
<div class="text-gray-500 fw-semibold">Last Updated At</div>
|
||||
<div class="fw-bold fs-5">{{ dateFormat($atmReport->updated_at, 1, 1) }}</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($atmReport->authorization_status === 'pending' && auth()->user()->can('authorize_atm_reports'))
|
||||
<div class="mt-7 shadow-sm card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Authorization</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('atm-reports.authorize', $atmReport->id) }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="mb-5">
|
||||
<label class="form-label required">Authorization Decision</label>
|
||||
<div class="d-flex">
|
||||
<div class="form-check form-check-custom form-check-solid me-5">
|
||||
<input class="form-check-input" type="radio" name="authorization_status"
|
||||
value="approved" id="status_approved" required />
|
||||
<label class="form-check-label" for="status_approved">
|
||||
Approve
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check form-check-custom form-check-solid">
|
||||
<input class="form-check-input" type="radio" name="authorization_status"
|
||||
value="rejected" id="status_rejected" required />
|
||||
<label class="form-check-label" for="status_rejected">
|
||||
Reject
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-5">
|
||||
<label class="form-label">Remarks</label>
|
||||
<textarea class="form-control" name="remarks" rows="3"
|
||||
placeholder="Enter any remarks or reasons for your decision"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="text-end">
|
||||
<button type="submit" class="btn btn-primary">Submit Authorization</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Any additional JavaScript for this page
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize any components if needed
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
393
resources/views/email-statement-logs/index.blade.php
Normal file
393
resources/views/email-statement-logs/index.blade.php
Normal file
@@ -0,0 +1,393 @@
|
||||
@extends('layouts.main')
|
||||
|
||||
@section('breadcrumbs')
|
||||
{{ Breadcrumbs::render(request()->route()->getName()) }}
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="grid">
|
||||
<div class="min-w-full card card-grid" data-datatable="false" data-datatable-page-size="10"
|
||||
data-datatable-state-save="false" id="email-statement-logs-table"
|
||||
data-api-url="{{ route('email-statement-logs.datatables') }}">
|
||||
<div class="flex-wrap py-5 card-header">
|
||||
<h3 class="card-title">
|
||||
Log Pengiriman Email Statement
|
||||
</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="Cari Log Email Statement" id="search" type="text" value="">
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<select class="select select-sm" id="filter-branch">
|
||||
<option value="">Cabang (Semua)</option>
|
||||
@foreach ($branches as $branch)
|
||||
<option value="{{ $branch }}">{{ $branch }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<select class="select select-sm" id="filter-email-status">
|
||||
<option value="">Status Email (Semua)</option>
|
||||
<option value="sent">Terkirim</option>
|
||||
<option value="failed">Gagal</option>
|
||||
<option value="pending">Pending</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<select class="select select-sm" id="filter-email-source">
|
||||
<option value="">Sumber Email (Semua)</option>
|
||||
<option value="account">Email Akun</option>
|
||||
<option value="customer">Email Nasabah</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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="min-w-[120px]" data-datatable-column="branch_code">
|
||||
<span class="sort"> <span class="sort-label">Cabang</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">No. Rekening</span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[120px]" data-datatable-column="period_from">
|
||||
<span class="sort"> <span class="sort-label">Periode Dari</span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[120px]" data-datatable-column="period_to">
|
||||
<span class="sort"> <span class="sort-label">Periode Sampai</span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[200px]" data-datatable-column="email_address">
|
||||
<span class="sort"> <span class="sort-label">Email</span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[120px]" data-datatable-column="email_source">
|
||||
<span class="sort"> <span class="sort-label">Sumber Email</span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[120px]" data-datatable-column="email_status">
|
||||
<span class="sort"> <span class="sort-label">Status Email</span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="email_sent_at">
|
||||
<span class="sort"> <span class="sort-label">Waktu Kirim</span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[100px]" data-datatable-column="actions">
|
||||
<span>Aksi</span>
|
||||
</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">
|
||||
Show
|
||||
<select class="w-16 select select-sm" data-datatable-size="true" name="perpage"> </select> per page
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<!-- Modal for detail view -->
|
||||
<div class="modal modal-open:!flex" data-modal="true" id="detail-modal">
|
||||
<div class="overflow-hidden px-5 w-full modal-content pt-7.5 container-fixed">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Detail Log Email Statement</h2>
|
||||
<button class="btn btn-sm btn-icon btn-active-color-danger" data-modal-dismiss="true">
|
||||
<i class="ki-outline ki-cross fs-1"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-5">
|
||||
<h3 class="mb-2 font-bold">Informasi Umum</h3>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>Cabang:</div>
|
||||
<div id="detail-branch-code"></div>
|
||||
<div>No. Rekening:</div>
|
||||
<div id="detail-account-number"></div>
|
||||
<div>Periode:</div>
|
||||
<div id="detail-period"></div>
|
||||
<div>Email:</div>
|
||||
<div id="detail-email-address"></div>
|
||||
<div>Sumber Email:</div>
|
||||
<div id="detail-email-source"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-5">
|
||||
<h3 class="mb-2 font-bold">Status Pengiriman Email</h3>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>Status:</div>
|
||||
<div id="detail-email-status"></div>
|
||||
<div>Waktu Kirim:</div>
|
||||
<div id="detail-email-sent-at"></div>
|
||||
<div>Error Message:</div>
|
||||
<div id="detail-error-message"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-5">
|
||||
<h3 class="mb-2 font-bold">Informasi Tambahan</h3>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>Dibuat Oleh:</div>
|
||||
<div id="detail-user-id"></div>
|
||||
<div>Waktu Dibuat:</div>
|
||||
<div id="detail-created-at"></div>
|
||||
<div>Waktu Update:</div>
|
||||
<div id="detail-updated-at"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="btn-resend-email" class="btn btn-primary btn-sm me-3">
|
||||
<i class="ki-outline ki-send me-1"></i> Kirim Ulang Email
|
||||
</button>
|
||||
<button type="button" class="btn btn-light" data-modal-dismiss="true">Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="module">
|
||||
const element = document.querySelector('#email-statement-logs-table');
|
||||
const searchInput = document.getElementById('search');
|
||||
const filterBranch = document.getElementById('filter-branch');
|
||||
const filterEmailStatus = document.getElementById('filter-email-status');
|
||||
const filterEmailSource = document.getElementById('filter-email-source');
|
||||
const detailModal = document.getElementById('detail-modal');
|
||||
const btnResendEmail = document.getElementById('btn-resend-email');
|
||||
|
||||
const apiUrl = element.getAttribute('data-api-url');
|
||||
const dataTableOptions = {
|
||||
apiEndpoint: apiUrl,
|
||||
pageSize: 10,
|
||||
columns: {
|
||||
branch_code: {
|
||||
title: 'Cabang',
|
||||
},
|
||||
account_number: {
|
||||
title: 'No. Rekening',
|
||||
},
|
||||
period_from: {
|
||||
title: 'Periode Dari',
|
||||
render: (item, data) => {
|
||||
return data.period_from ? new Date(data.period_from).toLocaleDateString('id-ID') : '-';
|
||||
},
|
||||
},
|
||||
period_to: {
|
||||
title: 'Periode Sampai',
|
||||
render: (item, data) => {
|
||||
return data.period_to ? new Date(data.period_to).toLocaleDateString('id-ID') : '-';
|
||||
},
|
||||
},
|
||||
email_address: {
|
||||
title: 'Email',
|
||||
},
|
||||
email_source: {
|
||||
title: 'Sumber Email',
|
||||
render: (item, data) => {
|
||||
if (data.email_source === 'account') {
|
||||
return '<span class="badge badge-info">Email Akun</span>';
|
||||
} else if (data.email_source === 'customer') {
|
||||
return '<span class="badge badge-primary">Email Nasabah</span>';
|
||||
}
|
||||
return '-';
|
||||
},
|
||||
},
|
||||
email_status: {
|
||||
title: 'Status Email',
|
||||
render: (item, data) => {
|
||||
if (data.email_status === 'sent') {
|
||||
return '<span class="badge badge-success">Terkirim</span>';
|
||||
} else if (data.email_status === 'failed') {
|
||||
return '<span class="badge badge-danger">Gagal</span>';
|
||||
} else if (data.email_status === 'pending') {
|
||||
return '<span class="badge badge-warning">Pending</span>';
|
||||
}
|
||||
return '<span class="badge badge-secondary">Unknown</span>';
|
||||
},
|
||||
},
|
||||
email_sent_at: {
|
||||
title: 'Waktu Kirim',
|
||||
render: (item, data) => {
|
||||
return data.email_sent_at ? new Date(data.email_sent_at).toLocaleString('id-ID') : '-';
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
title: 'Aksi',
|
||||
render: (item, data) => {
|
||||
let actions = `<div class="flex gap-2">`;
|
||||
|
||||
// Detail button
|
||||
actions += `<button class="btn btn-sm btn-icon btn-light btn-detail" data-id="${data.id}">
|
||||
<i class="ki-outline ki-eye fs-3"></i>
|
||||
</button>`;
|
||||
|
||||
// Resend button for failed emails
|
||||
if (data.email_status === 'failed') {
|
||||
actions += `<button class="btn btn-sm btn-icon btn-warning btn-resend" data-id="${data.id}" title="Kirim Ulang Email">
|
||||
<i class="ki-outline ki-send fs-3"></i>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
actions += `</div>`;
|
||||
return actions;
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let dataTable = new KTDataTable(element, dataTableOptions);
|
||||
|
||||
// Custom search functionality
|
||||
searchInput.addEventListener('change', function() {
|
||||
const searchValue = this.value.trim();
|
||||
dataTable.goPage(1);
|
||||
dataTable.search(searchValue, true);
|
||||
});
|
||||
|
||||
// Filter functionality
|
||||
const applyFilters = () => {
|
||||
const branchValue = filterBranch.value;
|
||||
const emailStatusValue = filterEmailStatus.value;
|
||||
const emailSourceValue = filterEmailSource.value;
|
||||
|
||||
const params = {};
|
||||
if (searchInput.value) {
|
||||
params.search = searchInput.value;
|
||||
}
|
||||
if (branchValue !== '') params.branch_code = branchValue;
|
||||
if (emailStatusValue !== '') params.email_status = emailStatusValue;
|
||||
if (emailSourceValue !== '') params.email_source = emailSourceValue;
|
||||
|
||||
dataTable.goPage(1);
|
||||
dataTable.search(params);
|
||||
dataTable.reload();
|
||||
};
|
||||
|
||||
filterBranch.addEventListener('change', applyFilters);
|
||||
filterEmailStatus.addEventListener('change', applyFilters);
|
||||
filterEmailSource.addEventListener('change', applyFilters);
|
||||
|
||||
// Detail modal functionality
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.btn-detail')) {
|
||||
const id = e.target.closest('.btn-detail').getAttribute('data-id');
|
||||
|
||||
// Fetch log details by ID
|
||||
fetch(`{{ url('email-statement-logs') }}/${id}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Store current log ID for resend functionality
|
||||
btnResendEmail.setAttribute('data-id', data.id);
|
||||
|
||||
// Show/hide resend button based on email status
|
||||
if (data.email_status === 'failed') {
|
||||
btnResendEmail.classList.remove('hidden');
|
||||
} else {
|
||||
btnResendEmail.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Populate modal with data
|
||||
document.getElementById('detail-branch-code').textContent = data.branch_code;
|
||||
document.getElementById('detail-account-number').textContent = data.account_number;
|
||||
document.getElementById('detail-period').textContent =
|
||||
`${new Date(data.period_from).toLocaleDateString('id-ID')} - ${new Date(data.period_to).toLocaleDateString('id-ID')}`;
|
||||
document.getElementById('detail-email-address').textContent = data.email_address;
|
||||
|
||||
const emailSourceText = data.email_source === 'account' ? 'Email Akun' : (data
|
||||
.email_source === 'customer' ? 'Email Nasabah' : '-');
|
||||
document.getElementById('detail-email-source').textContent = emailSourceText;
|
||||
|
||||
let emailStatusBadge = '';
|
||||
if (data.email_status === 'sent') {
|
||||
emailStatusBadge = '<span class="badge badge-success">Terkirim</span>';
|
||||
} else if (data.email_status === 'failed') {
|
||||
emailStatusBadge = '<span class="badge badge-danger">Gagal</span>';
|
||||
} else if (data.email_status === 'pending') {
|
||||
emailStatusBadge = '<span class="badge badge-warning">Pending</span>';
|
||||
}
|
||||
document.getElementById('detail-email-status').innerHTML = emailStatusBadge;
|
||||
|
||||
document.getElementById('detail-email-sent-at').textContent = data.email_sent_at ?
|
||||
new Date(data.email_sent_at).toLocaleString('id-ID') :
|
||||
'-';
|
||||
document.getElementById('detail-error-message').textContent = data.error_message || '-';
|
||||
document.getElementById('detail-user-id').textContent = data.user_id || '-';
|
||||
document.getElementById('detail-created-at').textContent = data.created_at ?
|
||||
new Date(data.created_at).toLocaleString('id-ID') :
|
||||
'-';
|
||||
document.getElementById('detail-updated-at').textContent = data.updated_at ?
|
||||
new Date(data.updated_at).toLocaleString('id-ID') :
|
||||
'-';
|
||||
|
||||
// Show modal
|
||||
const modalEl = KTDom.getElement('#detail-modal');
|
||||
const modal = KTModal.getInstance(modalEl);
|
||||
modal.show();
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching log details:', error);
|
||||
alert('Gagal mengambil detail log');
|
||||
});
|
||||
}
|
||||
|
||||
// Resend email functionality
|
||||
if (e.target.closest('.btn-resend') || e.target.closest('#btn-resend-email')) {
|
||||
const id = e.target.closest('.btn-resend, #btn-resend-email').getAttribute('data-id');
|
||||
|
||||
if (confirm('Apakah Anda yakin ingin mengirim ulang email ini?')) {
|
||||
fetch(`{{ url('email-statement-logs') }}/${id}/resend`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
|
||||
'content')
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Email berhasil dikirim ulang');
|
||||
dataTable.reload();
|
||||
|
||||
// Close modal if open
|
||||
const modalEl = KTDom.getElement('#detail-modal');
|
||||
const modal = KTModal.getInstance(modalEl);
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
} else {
|
||||
alert('Gagal mengirim ulang email: ' + (data.message || 'Unknown error'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error resending email:', error);
|
||||
alert('Gagal mengirim ulang email');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.dataTable = dataTable;
|
||||
</script>
|
||||
@endpush
|
||||
176
resources/views/email-statement-logs/show.blade.php
Normal file
176
resources/views/email-statement-logs/show.blade.php
Normal file
@@ -0,0 +1,176 @@
|
||||
@extends('layouts.main')
|
||||
|
||||
@section('title', 'Detail Log Pengiriman Email Statement')
|
||||
|
||||
@section('breadcrumbs')
|
||||
{{ Breadcrumbs::render(request()->route()->getName(), $log) }}
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Detail Log Pengiriman Email Statement</h3>
|
||||
<div class="card-toolbar">
|
||||
<a href="{{ route('email-statement-logs.index') }}" class="btn btn-sm btn-light">
|
||||
<i class="text-base ki-filled ki-black-left"></i>
|
||||
Kembali
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<table class="table table-borderless">
|
||||
<tr>
|
||||
<td class="fw-bold">ID Log:</td>
|
||||
<td>{{ $log->id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Branch:</td>
|
||||
<td>{{ $log->branch->name ?? 'N/A' }} ({{ $log->branch_code }})</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">No. Rekening:</td>
|
||||
<td>{{ $log->account_number }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Periode:</td>
|
||||
<td>{{ $log->period_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Email Tujuan:</td>
|
||||
<td>{{ $log->email }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Status Email:</td>
|
||||
<td>
|
||||
@if ($log->email_sent_at)
|
||||
<span class="badge badge-success">Terkirim</span>
|
||||
@else
|
||||
<span class="badge badge-warning">Pending</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<table class="table table-borderless">
|
||||
<tr>
|
||||
<td class="fw-bold">Tanggal Kirim:</td>
|
||||
<td>{{ $log->email_sent_at ? $log->email_sent_at->format('d/m/Y H:i:s') : '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Status Otorisasi:</td>
|
||||
<td>
|
||||
@php
|
||||
$badgeClass = 'badge-secondary';
|
||||
if ($log->authorization_status === 'approved') {
|
||||
$badgeClass = 'badge-success';
|
||||
} elseif ($log->authorization_status === 'rejected') {
|
||||
$badgeClass = 'badge-danger';
|
||||
} elseif ($log->authorization_status === 'pending') {
|
||||
$badgeClass = 'badge-warning';
|
||||
}
|
||||
@endphp
|
||||
<span
|
||||
class="badge {{ $badgeClass }}">{{ ucfirst($log->authorization_status) }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Statement Tersedia:</td>
|
||||
<td>
|
||||
@if ($log->is_available)
|
||||
<span class="badge badge-success">Ya</span>
|
||||
@else
|
||||
<span class="badge badge-danger">Tidak</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">User Pembuat:</td>
|
||||
<td>{{ $log->user->name ?? 'N/A' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Tanggal Dibuat:</td>
|
||||
<td>{{ $log->created_at->format('d/m/Y H:i:s') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Terakhir Update:</td>
|
||||
<td>{{ $log->updated_at->format('d/m/Y H:i:s') }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($log->remarks)
|
||||
<div class="mt-4 row">
|
||||
<div class="col-12">
|
||||
<h5>Catatan:</h5>
|
||||
<div class="alert alert-info">
|
||||
{{ $log->remarks }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Aksi</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if ($log->is_available && $log->authorization_status === 'approved')
|
||||
<button onclick="resendEmail({{ $log->id }})" class="mb-3 btn btn-primary w-100">
|
||||
<i class="text-base ki-filled ki-message-text-2"></i>
|
||||
Kirim Ulang Email
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@if ($log->is_available)
|
||||
<a href="{{ route('statements.download', $log->id) }}" class="mb-3 btn btn-success w-100">
|
||||
<i class="text-base ki-filled ki-file-down"></i>
|
||||
Download Statement
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<a href="{{ route('statements.show', $log->id) }}" class="btn btn-info w-100">
|
||||
<i class="text-base ki-filled ki-eye"></i>
|
||||
Lihat Statement Log
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Function untuk resend email
|
||||
function resendEmail(logId) {
|
||||
if (confirm('Apakah Anda yakin ingin mengirim ulang email statement ini?')) {
|
||||
$.ajax({
|
||||
url: '{{ route('email-statement-logs.resend-email', ':id') }}'.replace(':id', logId),
|
||||
type: 'POST',
|
||||
data: {
|
||||
_token: '{{ csrf_token() }}'
|
||||
},
|
||||
success: function(response) {
|
||||
alert('Email statement berhasil dijadwalkan untuk dikirim ulang.');
|
||||
location.reload();
|
||||
},
|
||||
error: function(xhr) {
|
||||
alert('Gagal mengirim ulang email statement.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -15,8 +16,8 @@
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 90%;
|
||||
margin: 20px auto;
|
||||
max-width: 100%;
|
||||
margin: 0px auto;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
@@ -36,7 +37,7 @@
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 30px;
|
||||
padding: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -59,85 +60,84 @@
|
||||
}
|
||||
|
||||
ul.dashed-list {
|
||||
list-style-type: none; /* Remove default bullet */
|
||||
padding-left: 1em; /* Add some left padding for spacing */
|
||||
list-style-type: none;
|
||||
/* Remove default bullet */
|
||||
padding-left: 1em;
|
||||
/* Add some left padding for spacing */
|
||||
}
|
||||
|
||||
ul.dashed-list li::before {
|
||||
content: "– "; /* Use an en dash (U+2013) or a hyphen "-" */
|
||||
display: inline-block; /* Ensure proper spacing */
|
||||
width: 1em; /* Adjust width as needed */
|
||||
margin-left: 0.5em; /* Align the dash properly */
|
||||
content: "– ";
|
||||
/* Use an en dash (U+2013) or a hyphen "-" */
|
||||
display: inline-block;
|
||||
/* Ensure proper spacing */
|
||||
width: 1em;
|
||||
/* Adjust width as needed */
|
||||
margin-left: 0.5em;
|
||||
/* Align the dash properly */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<div>
|
||||
Yang Terhormat <strong>Bapak/Ibu Daeng Deni Mardaeni</strong>,<br><br>
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<div>
|
||||
Yang Terhormat <strong>Bapak/Ibu {{ $accounts->customer->name }}</strong>,<br><br>
|
||||
|
||||
Terlampir adalah Electronic Statement Rekening Anda.<br>
|
||||
Silahkan gunakan password Electronic Statement Anda untuk membukanya.<br><br>
|
||||
Password standar Elektronic Statement ini adalah <strong>ddMonyyyyxx</strong> (contoh: 01Aug1970xx) dimana :
|
||||
<ul class="dashed-list">
|
||||
<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 huruf besar dan selanjutnya huruf kecil, contoh : Aug
|
||||
</li>
|
||||
<li>yyyy : <strong>4 digit</strong> tahun kelahiran anda, contoh : 1970</li>
|
||||
<li>xx : <strong>2 digit terakhir</strong> dari nomer rekening anda, contoh : 12</li>
|
||||
</ul>
|
||||
<br>
|
||||
Terlampir adalah Electronic Statement Rekening Anda.<br>
|
||||
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;">
|
||||
<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
|
||||
huruf besar dan selanjutnya huruf kecil, contoh : Aug
|
||||
</li>
|
||||
<li>- yyyy : <strong>4 digit</strong> tahun kelahiran anda, contoh : 1970</li>
|
||||
<li>- xx : <strong>2 digit terakhir</strong> dari nomer rekening anda, contoh : 12</li>
|
||||
</ul>
|
||||
<br>
|
||||
|
||||
Terima Kasih,<br><br>
|
||||
Terima Kasih,<br><br>
|
||||
|
||||
<strong>Bank Artha Graha Internasional</strong><br>
|
||||
------------------------------
|
||||
<wbr>
|
||||
------------------------------
|
||||
<wbr>
|
||||
--------<br>
|
||||
Kami sangat menghargai masukan dan saran Anda untuk meningkatkan layanan dan produk kami.<br>
|
||||
Untuk memberikan masukan, silakan hubungi <strong>GrahaCall 24 Jam</strong> kami di
|
||||
<strong>0-800-191-8880</strong>.<br><br><br>
|
||||
<strong>Bank Artha Graha Internasional</strong><br>
|
||||
------------------------------------------------------------<br>
|
||||
Kami sangat menghargai masukan dan saran Anda untuk meningkatkan layanan dan produk kami.<br>
|
||||
Untuk memberikan masukan, silakan hubungi <strong>GrahaCall 24 Jam</strong> kami di
|
||||
<strong>0-800-191-8880</strong>.<br><br><br>
|
||||
|
||||
Dear <strong>Mr/Mrs/Ms Daeng Deni Mardaeni</strong>,<br><br>
|
||||
Dear <strong>Mr/Mrs/Ms {{ $accounts->customer->name }}</strong>,<br><br>
|
||||
|
||||
Attached is your Electronic Account Statement.<br>
|
||||
Please use your Electronic Statement password to open it.<br><br>
|
||||
Attached is your Electronic Account Statement.<br>
|
||||
Please use your Electronic Statement password to open it.<br><br>
|
||||
|
||||
The Electronic Statement standard password is <strong>ddMonyyyyxx</strong> (example: 01Aug1970xx) where:
|
||||
<ul class="dashed-list">
|
||||
<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 uppercase and the rest are lowercase, example: Aug
|
||||
</li>
|
||||
<li>yyyy : <strong>4 digit</strong> of your birth year, example: 1970</li>
|
||||
<li>xx : <strong>The last 2 digits</strong> of your account number, example: 12.</li>
|
||||
</ul>
|
||||
<br>
|
||||
The Electronic Statement standard password is <strong>ddMonyyyyxx</strong> (example: 01Aug1970xx) where:
|
||||
<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
|
||||
uppercase and the rest are lowercase, example: Aug
|
||||
</li>
|
||||
<li>- yyyy : <strong>4 digit</strong> of your birth year, example: 1970</li>
|
||||
<li>- xx : <strong>The last 2 digits</strong> of your account number, example: 12.</li>
|
||||
</ul>
|
||||
<br>
|
||||
|
||||
Regards,<br><br>
|
||||
Regards,<br><br>
|
||||
|
||||
<strong>Bank Artha Graha Internasional</strong><br>
|
||||
------------------------------
|
||||
<wbr>
|
||||
------------------------------
|
||||
<wbr>
|
||||
--------<br>
|
||||
We welcome any feedback or suggestions to improve our product and services.<br>
|
||||
If you have any feedback, please contact our <strong>GrahaCall 24 Hours</strong> at
|
||||
<strong>0-800-191-8880</strong>.
|
||||
<div class="yj6qo"></div>
|
||||
<div class="adL"><br>
|
||||
<strong>Bank Artha Graha Internasional</strong><br>
|
||||
------------------------------------------------------------<br>
|
||||
We welcome any feedback or suggestions to improve our product and services.<br>
|
||||
If you have any feedback, please contact our <strong>GrahaCall 24 Hours</strong> at
|
||||
<strong>0-800-191-8880</strong>.
|
||||
<div class="yj6qo"></div>
|
||||
<div class="adL"><br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>© 2023 Bank Artha Graha Internasional. All rights reserved.</p>
|
||||
<p>Jika Anda memiliki pertanyaan, silakan hubungi customer service kami.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -11,41 +11,59 @@
|
||||
<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">
|
||||
<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
|
||||
</select>
|
||||
@error('branch_code')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
@if ($multiBranch)
|
||||
<div class="form-group">
|
||||
<label class="form-label required" for="branch_id">Branch/Cabang</label>
|
||||
<select class="input form-control tomselect @error('branch_id') is-invalid @enderror"
|
||||
id="branch_id" name="branch_id" required>
|
||||
<option value="">Pilih Branch/Cabang</option>
|
||||
@foreach ($branches as $branchOption)
|
||||
<option value="{{ $branchOption->code }}"
|
||||
{{ old('branch_id', $statement->branch_id ?? ($branch->code ?? '')) == $branchOption->code ? 'selected' : '' }}>
|
||||
{{ $branchOption->code }} - {{ $branchOption->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('branch_id')
|
||||
<div class="invalid-feedback">{{ $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_id" value="{{ $branch->code ?? '' }}">
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<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>
|
||||
<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>
|
||||
@error('account_number')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
<div class="invalid-feedback">{{ $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') is-invalid @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="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
@@ -53,24 +71,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,8 +100,9 @@
|
||||
</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">
|
||||
<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>
|
||||
@@ -100,55 +116,54 @@
|
||||
</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">
|
||||
<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="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>
|
||||
<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="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">
|
||||
<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="select select-sm w-16" 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 items-center gap-4">
|
||||
<div class="flex gap-4 items-center">
|
||||
<span data-datatable-info="true"> </span>
|
||||
<div class="pagination" data-datatable-pagination="true">
|
||||
</div>
|
||||
@@ -162,6 +177,10 @@
|
||||
|
||||
@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?',
|
||||
@@ -175,7 +194,7 @@
|
||||
if (result.isConfirmed) {
|
||||
$.ajaxSetup({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -192,6 +211,56 @@
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
|
||||
// Log: Inisialisasi event listener untuk konfirmasi email
|
||||
console.log('Email confirmation listener initialized');
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
const emailValue = emailInput.value.trim();
|
||||
|
||||
// Jika email diisi, tampilkan konfirmasi
|
||||
if (emailValue) {
|
||||
e.preventDefault(); // Hentikan submit form sementara
|
||||
|
||||
// Log: Email terdeteksi, menampilkan konfirmasi
|
||||
console.log('Email detected:', emailValue);
|
||||
|
||||
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');
|
||||
|
||||
// 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">
|
||||
@@ -243,31 +312,16 @@
|
||||
return fromPeriod + toPeriod;
|
||||
},
|
||||
},
|
||||
authorization_status: {
|
||||
title: 'Status',
|
||||
render: (item, data) => {
|
||||
let statusClass = 'badge badge-light-primary';
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
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 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 : {
|
||||
remarks: {
|
||||
title: 'Notes',
|
||||
},
|
||||
created_at: {
|
||||
@@ -315,7 +369,7 @@
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -114,3 +114,14 @@
|
||||
$trail->parent('home');
|
||||
$trail->push('Print Stetement', route('statements.index'));
|
||||
});
|
||||
|
||||
|
||||
Breadcrumbs::for('atm-reports.index', function (BreadcrumbTrail $trail) {
|
||||
$trail->parent('home');
|
||||
$trail->push('Laporan Transaksi ATM', route('atm-reports.index'));
|
||||
});
|
||||
|
||||
Breadcrumbs::for('email-statement-logs.index', function (BreadcrumbTrail $trail) {
|
||||
$trail->parent('home');
|
||||
$trail->push('Statement Email Logs', route('email-statement-logs.index'));
|
||||
});
|
||||
|
||||
@@ -7,9 +7,13 @@ use Illuminate\Support\Facades\Route;
|
||||
use Modules\Webstatement\Http\Controllers\JenisKartuController;
|
||||
use Modules\Webstatement\Http\Controllers\KartuAtmController;
|
||||
use Modules\Webstatement\Http\Controllers\MigrasiController;
|
||||
use Modules\Webstatement\Http\Controllers\WebstatementController;
|
||||
use Modules\Webstatement\Http\Controllers\CustomerController;
|
||||
use Modules\Webstatement\Http\Controllers\EmailBlastController;
|
||||
use Modules\Webstatement\Http\Controllers\WebstatementController;
|
||||
use Modules\Webstatement\Http\Controllers\DebugStatementController;
|
||||
use Modules\Webstatement\Http\Controllers\EmailStatementLogController;
|
||||
use Modules\Webstatement\Http\Controllers\AtmTransactionReportController;
|
||||
|
||||
|
||||
|
||||
/*
|
||||
@@ -87,10 +91,36 @@ 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 () {
|
||||
Route::get('/datatables', [AtmTransactionReportController::class, 'dataForDatatables'])->name('datatables');
|
||||
Route::get('/{atmReport}/download', [AtmTransactionReportController::class, 'download'])->name('download');
|
||||
Route::post('/{atmReport}/authorize', [AtmTransactionReportController::class, 'authorize'])->name('authorize');
|
||||
Route::get('/{atmReport}/send-email', [AtmTransactionReportController::class, 'sendEmail'])->name('send-email');
|
||||
Route::post('/{atmReport}/retry', [AtmTransactionReportController::class, 'retry'])->name('retry');
|
||||
});
|
||||
|
||||
Route::resource('atm-reports', AtmTransactionReportController::class);
|
||||
|
||||
// Email Statement Log Routes
|
||||
Route::group(['prefix' => 'email-statement-logs', 'as' => 'email-statement-logs.', 'middleware' => ['auth']], function () {
|
||||
Route::get('/datatables', [EmailStatementLogController::class, 'dataForDatatables'])->name('datatables');
|
||||
Route::post('/{id}/resend-email', [EmailStatementLogController::class, 'resendEmail'])->name('resend-email');
|
||||
});
|
||||
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('/', [WebstatementController::class, 'index'])->name('webstatement.index');
|
||||
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');
|
||||
Route::get('/statements', [DebugStatementController::class, 'listStatements'])->name('debug.statements.list');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user