Compare commits
173 Commits
713c2b1e12
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5752427297 | ||
|
|
eb89916b1c | ||
|
|
80c866f646 | ||
|
|
e5c33bf631 | ||
|
|
f37707b2f6 | ||
|
|
ad9780ccd6 | ||
|
|
bcc6d814e9 | ||
|
|
5de1c19d09 | ||
|
|
3c01c1728c | ||
|
|
3beaf78872 | ||
|
|
23a0679f74 | ||
|
|
1564ce2efa | ||
|
|
e6c46701ce | ||
|
|
35bb173056 | ||
|
|
2dd8024586 | ||
|
|
36abab1280 | ||
|
|
7818d1677b | ||
|
|
92afe58e66 | ||
|
|
c264d63fa6 | ||
|
|
6bd8b77d87 | ||
|
|
efabba4c39 | ||
|
|
2b39c5190b | ||
|
|
9c5f8b1de4 | ||
|
|
5469045b5a | ||
|
|
56665cd77a | ||
|
|
011f749786 | ||
|
|
5b235def37 | ||
|
|
593a4f0d9c | ||
|
|
d4e6a3d73d | ||
|
|
0aa7d22094 | ||
|
|
5ea8136c13 | ||
|
|
4b7e6c983b | ||
|
|
8d84c0a1ba | ||
|
|
1f140af94a | ||
|
|
c1a173c8f7 | ||
|
|
974bf1cc35 | ||
|
|
0ace1d5c70 | ||
|
|
595ab89390 | ||
|
|
34571483eb | ||
|
|
062bac2138 | ||
|
|
8ee0dd2218 | ||
|
|
51697f017e | ||
|
|
e2c9f3480d | ||
|
|
40f552cb66 | ||
|
|
65b846f0c7 | ||
|
|
a3060322f9 | ||
|
|
428792ed1b | ||
|
|
4616137e0c | ||
|
|
19c962307e | ||
|
|
a79b1bd99e | ||
|
|
fd5b8e1dad | ||
|
|
8fb16028d9 | ||
|
|
6035c61cc4 | ||
|
|
2c8f49af20 | ||
|
|
4bfd937490 | ||
|
|
7b32cb8d39 | ||
|
|
4b889da5a5 | ||
|
|
dbdeceb4c0 | ||
|
|
f7a92a5336 | ||
|
|
b717749450 | ||
|
|
e5b8dfc7c4 | ||
|
|
d5482fb824 | ||
|
|
f6df453ddc | ||
|
|
0cbb7c9a3c | ||
|
|
fabc35e729 | ||
|
|
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 | ||
|
|
a687385017 | ||
|
|
3414fd9414 | ||
|
|
cb0a248ce5 | ||
|
|
57463f2429 | ||
|
|
9f0ee812a9 | ||
|
|
ed4ffb4254 | ||
|
|
ac6d139b4a | ||
|
|
59d186e3b5 | ||
|
|
a7a55a92a1 | ||
|
|
b54fabd416 | ||
|
|
d66afb36c7 | ||
|
|
79d8ea2eee | ||
|
|
b98408a8d2 | ||
|
|
eab14ff1e1 | ||
|
|
04f6f02702 | ||
|
|
d5495d721e | ||
|
|
e511025307 | ||
|
|
285c2409ea | ||
|
|
3c061f40f7 | ||
|
|
def0b037a8 | ||
|
|
39e356e2ff | ||
|
|
fb6fd534d5 | ||
|
|
359cfea905 | ||
|
|
1c8d9a4a8a | ||
|
|
24700c5bd8 |
71
app/Console/AutoSendStatementEmailCommand.php
Normal file
71
app/Console/AutoSendStatementEmailCommand.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Console;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Modules\Webstatement\Jobs\AutoSendStatementEmailJob;
|
||||||
|
|
||||||
|
class AutoSendStatementEmailCommand extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'webstatement:auto-send-email
|
||||||
|
{--force : Force run even if already running}
|
||||||
|
{--dry-run : Show what would be sent without actually sending}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Automatically send statement emails for available statements';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command untuk menjalankan auto send email
|
||||||
|
*
|
||||||
|
* Command ini akan:
|
||||||
|
* 1. Dispatch AutoSendStatementEmailJob
|
||||||
|
* 2. Log aktivitas command
|
||||||
|
* 3. Handle dry-run mode untuk testing
|
||||||
|
*/
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->info('Starting auto send statement email process...');
|
||||||
|
|
||||||
|
Log::info('AutoSendStatementEmailCommand: Command started', [
|
||||||
|
'force' => $this->option('force'),
|
||||||
|
'dry_run' => $this->option('dry-run')
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($this->option('dry-run')) {
|
||||||
|
$this->info('DRY RUN MODE: Would dispatch AutoSendStatementEmailJob');
|
||||||
|
Log::info('AutoSendStatementEmailCommand: Dry run mode, job not dispatched');
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch job
|
||||||
|
AutoSendStatementEmailJob::dispatch();
|
||||||
|
|
||||||
|
$this->info('AutoSendStatementEmailJob dispatched successfully');
|
||||||
|
|
||||||
|
Log::info('AutoSendStatementEmailCommand: Job dispatched successfully');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error('Error: ' . $e->getMessage());
|
||||||
|
|
||||||
|
Log::error('AutoSendStatementEmailCommand: Command failed', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
535
app/Console/GenerateClosingBalanceReportBulkCommand.php
Normal file
535
app/Console/GenerateClosingBalanceReportBulkCommand.php
Normal file
@@ -0,0 +1,535 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Console;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Modules\Usermanagement\Models\User;
|
||||||
|
use Modules\Webstatement\Jobs\GenerateClosingBalanceReportJob;
|
||||||
|
use Modules\Webstatement\Models\ClosingBalanceReportLog;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Console command untuk generate laporan closing balance untuk banyak rekening sekaligus
|
||||||
|
* Command ini dapat dijalankan secara manual atau dijadwalkan
|
||||||
|
* Mendukung periode range dan daftar rekening custom
|
||||||
|
*/
|
||||||
|
class GenerateClosingBalanceReportBulkCommand extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'webstatement:generate-closing-balance-bulk
|
||||||
|
{start_date : Tanggal mulai periode format YYYYMMDD, contoh: 20250512}
|
||||||
|
{end_date : Tanggal akhir periode format YYYYMMDD, contoh: 20250712}
|
||||||
|
{--accounts= : Daftar rekening dipisahkan koma (opsional, jika tidak ada akan gunakan default list)}
|
||||||
|
{--client= : Filter berdasarkan client tertentu (opsional)}
|
||||||
|
{--user_id=1 : ID user yang menjalankan command (default: 1)}
|
||||||
|
{--dry-run : Tampilkan daftar rekening yang akan diproses tanpa menjalankan job}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Generate Closing Balance report untuk banyak rekening sekaligus dengan periode range';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Daftar rekening default yang akan diproses
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $defaultAccounts = [
|
||||||
|
'IDR1723200010001',
|
||||||
|
'IDR1728100010001',
|
||||||
|
'IDR1728200010001',
|
||||||
|
'IDR1733100010001',
|
||||||
|
'IDR1728300010001',
|
||||||
|
'IDR1733100030001',
|
||||||
|
'IDR1723300010001',
|
||||||
|
'IDR1733100020001',
|
||||||
|
'IDR1733100040001',
|
||||||
|
'IDR1733200010001',
|
||||||
|
'IDR1733200020001',
|
||||||
|
'IDR1733500010001',
|
||||||
|
'IDR1733600010001',
|
||||||
|
'IDR1733300010001',
|
||||||
|
'IDR1733400010001',
|
||||||
|
'IDR1354100010001',
|
||||||
|
'IDR1354300010001',
|
||||||
|
'IDR1354400010001',
|
||||||
|
'IDR1728500010001',
|
||||||
|
'IDR1728600010001',
|
||||||
|
'IDR1720500010001',
|
||||||
|
'1078333878',
|
||||||
|
'1081647484',
|
||||||
|
'1085552121',
|
||||||
|
'1085677889',
|
||||||
|
'1086677889',
|
||||||
|
'IDR1744200010001',
|
||||||
|
'IDR1744300010001',
|
||||||
|
'IDR1744100010001',
|
||||||
|
'IDR1744400010001',
|
||||||
|
'IDR1364100010001',
|
||||||
|
'IDR1723100010001',
|
||||||
|
'IDR1354200010001'
|
||||||
|
];
|
||||||
|
|
||||||
|
private $qrisAccount = [
|
||||||
|
'IDR1354500010001',
|
||||||
|
'IDR1354500020001',
|
||||||
|
'IDR1354500030001',
|
||||||
|
'IDR1354500040001',
|
||||||
|
'IDR1354500050001',
|
||||||
|
'IDR1354500060001',
|
||||||
|
'IDR1354500070001',
|
||||||
|
'IDR1354500080001',
|
||||||
|
'IDR1354500090001',
|
||||||
|
'IDR1354500100001',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
* Menjalankan proses generate laporan closing balance untuk banyak rekening dengan periode range
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->info('Starting Bulk Closing Balance report generation with date range...');
|
||||||
|
|
||||||
|
// Get parameters
|
||||||
|
$startDate = $this->argument('start_date');
|
||||||
|
$endDate = $this->argument('end_date');
|
||||||
|
$accountsOption = $this->option('accounts');
|
||||||
|
$clientFilter = $this->option('client');
|
||||||
|
$userId = $this->option('user_id');
|
||||||
|
$isDryRun = $this->option('dry-run');
|
||||||
|
|
||||||
|
// Validate parameters
|
||||||
|
if (!$this->validateParameters($startDate, $endDate, $userId)) {
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get account list
|
||||||
|
$accountList = $this->getAccountList($accountsOption, $clientFilter);
|
||||||
|
|
||||||
|
if (empty($accountList)) {
|
||||||
|
$this->warn('No accounts found for processing.');
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate date range
|
||||||
|
$dateRange = $this->generateDateRange($startDate, $endDate);
|
||||||
|
|
||||||
|
// Show summary
|
||||||
|
$this->showSummary($accountList, $dateRange, $isDryRun);
|
||||||
|
|
||||||
|
if ($isDryRun) {
|
||||||
|
$this->info('Dry run completed. No jobs were dispatched.');
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm execution
|
||||||
|
if (!$this->confirm('Do you want to proceed with generating reports for all accounts and periods?')) {
|
||||||
|
$this->info('Operation cancelled.');
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process accounts for all dates in range
|
||||||
|
$results = $this->processAccountsWithDateRange($accountList, $dateRange, $userId);
|
||||||
|
|
||||||
|
// Show results
|
||||||
|
$this->showResults($results);
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->error('Error in bulk closing balance report generation: ' . $e->getMessage());
|
||||||
|
|
||||||
|
Log::error('Console command: Error in bulk closing balance report generation', [
|
||||||
|
'start_date' => $startDate,
|
||||||
|
'end_date' => $endDate,
|
||||||
|
'client_filter' => $clientFilter,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate command parameters
|
||||||
|
* Validasi parameter command termasuk validasi range tanggal
|
||||||
|
*
|
||||||
|
* @param string $startDate
|
||||||
|
* @param string $endDate
|
||||||
|
* @param int $userId
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function validateParameters(string $startDate, string $endDate, int $userId): bool
|
||||||
|
{
|
||||||
|
// Validate date format (YYYYMMDD)
|
||||||
|
if (!preg_match('/^\\d{8}$/', $startDate)) {
|
||||||
|
$this->error('Invalid start_date format. Use YYYYMMDD format (example: 20250512)');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match('/^\\d{8}$/', $endDate)) {
|
||||||
|
$this->error('Invalid end_date format. Use YYYYMMDD format (example: 20250712)');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate start date
|
||||||
|
$startYear = substr($startDate, 0, 4);
|
||||||
|
$startMonth = substr($startDate, 4, 2);
|
||||||
|
$startDay = substr($startDate, 6, 2);
|
||||||
|
|
||||||
|
if (!checkdate($startMonth, $startDay, $startYear)) {
|
||||||
|
$this->error('Invalid start_date.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate end date
|
||||||
|
$endYear = substr($endDate, 0, 4);
|
||||||
|
$endMonth = substr($endDate, 4, 2);
|
||||||
|
$endDay = substr($endDate, 6, 2);
|
||||||
|
|
||||||
|
if (!checkdate($endMonth, $endDay, $endYear)) {
|
||||||
|
$this->error('Invalid end_date.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate date range
|
||||||
|
$startCarbon = Carbon::createFromFormat('Ymd', $startDate);
|
||||||
|
$endCarbon = Carbon::createFromFormat('Ymd', $endDate);
|
||||||
|
|
||||||
|
if ($startCarbon->gt($endCarbon)) {
|
||||||
|
$this->error('Start date cannot be greater than end date.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate range not too long (max 3 months)
|
||||||
|
if ($startCarbon->diffInDays($endCarbon) > 90) {
|
||||||
|
$this->error('Date range cannot exceed 90 days.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate user exists
|
||||||
|
$user = User::find($userId);
|
||||||
|
if (!$user) {
|
||||||
|
$this->error("User with ID {$userId} not found.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate date range array from start to end date
|
||||||
|
* Menghasilkan array tanggal dari start sampai end date
|
||||||
|
*
|
||||||
|
* @param string $startDate
|
||||||
|
* @param string $endDate
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function generateDateRange(string $startDate, string $endDate): array
|
||||||
|
{
|
||||||
|
$dates = [];
|
||||||
|
$current = Carbon::createFromFormat('Ymd', $startDate);
|
||||||
|
$end = Carbon::createFromFormat('Ymd', $endDate);
|
||||||
|
|
||||||
|
while ($current->lte($end)) {
|
||||||
|
$dates[] = $current->format('Ymd');
|
||||||
|
$current->addDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Generated date range for bulk processing', [
|
||||||
|
'start_date' => $startDate,
|
||||||
|
'end_date' => $endDate,
|
||||||
|
'total_dates' => count($dates)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $dates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get account list based on options
|
||||||
|
* Mengambil daftar rekening berdasarkan parameter atau menggunakan default
|
||||||
|
*
|
||||||
|
* @param string|null $accountsOption
|
||||||
|
* @param string|null $clientFilter
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function getAccountList(?string $accountsOption, ?string $clientFilter): array
|
||||||
|
{
|
||||||
|
// Jika ada parameter accounts, gunakan itu
|
||||||
|
if ($accountsOption) {
|
||||||
|
$accounts = array_map('trim', explode(',', $accountsOption));
|
||||||
|
$accounts = array_filter($accounts); // Remove empty values
|
||||||
|
|
||||||
|
Log::info('Using custom account list from parameter', [
|
||||||
|
'total_accounts' => count($accounts),
|
||||||
|
'accounts' => $accounts
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ['CUSTOM' => $accounts];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika tidak ada parameter accounts, gunakan default list
|
||||||
|
$accountList = ['DEFAULT' => $this->defaultAccounts, 'QRIS' => $this->qrisAccount];
|
||||||
|
|
||||||
|
// Filter by client jika ada (untuk backward compatibility)
|
||||||
|
if ($clientFilter) {
|
||||||
|
// Untuk saat ini, client filter tidak digunakan karena kita pakai list baru
|
||||||
|
// Tapi tetap log untuk tracking
|
||||||
|
Log::info('Client filter specified but using default account list', [
|
||||||
|
'client_filter' => $clientFilter
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Using default account list', [
|
||||||
|
'total_accounts' => count($this->defaultAccounts)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $accountList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show summary of accounts and dates to be processed
|
||||||
|
* Menampilkan ringkasan rekening dan tanggal yang akan diproses
|
||||||
|
*
|
||||||
|
* @param array $accountList
|
||||||
|
* @param array $dateRange
|
||||||
|
* @param bool $isDryRun
|
||||||
|
*/
|
||||||
|
private function showSummary(array $accountList, array $dateRange, bool $isDryRun): void
|
||||||
|
{
|
||||||
|
$this->info('\n=== SUMMARY ===');
|
||||||
|
$this->info("Date Range: {$dateRange[0]} to {$dateRange[count($dateRange)-1]} ({" . count($dateRange) . "} days)");
|
||||||
|
$this->info("Mode: " . ($isDryRun ? 'DRY RUN' : 'LIVE'));
|
||||||
|
$this->info('');
|
||||||
|
|
||||||
|
$totalAccounts = 0;
|
||||||
|
foreach ($accountList as $groupName => $accounts) {
|
||||||
|
$accountCount = count($accounts);
|
||||||
|
$totalAccounts += $accountCount;
|
||||||
|
$this->info("Group: {$groupName} ({$accountCount} accounts)");
|
||||||
|
|
||||||
|
// Show first 10 accounts, then summarize if more
|
||||||
|
$displayAccounts = array_slice($accounts, 0, 10);
|
||||||
|
foreach ($displayAccounts as $account) {
|
||||||
|
$this->line(" - {$account}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($accounts) > 10) {
|
||||||
|
$remaining = count($accounts) - 10;
|
||||||
|
$this->line(" ... and {$remaining} more accounts");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalJobs = $totalAccounts * count($dateRange);
|
||||||
|
$this->info("\nTotal accounts: {$totalAccounts}");
|
||||||
|
$this->info("Total dates: " . count($dateRange));
|
||||||
|
$this->info("Total jobs to be created: {$totalJobs}");
|
||||||
|
$this->info('===============\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process all accounts for all dates in range
|
||||||
|
* Memproses semua rekening untuk semua tanggal dalam range
|
||||||
|
*
|
||||||
|
* @param array $accountList
|
||||||
|
* @param array $dateRange
|
||||||
|
* @param int $userId
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function processAccountsWithDateRange(array $accountList, array $dateRange, int $userId): array
|
||||||
|
{
|
||||||
|
$results = [
|
||||||
|
'success' => [],
|
||||||
|
'failed' => [],
|
||||||
|
'total' => 0
|
||||||
|
];
|
||||||
|
|
||||||
|
$totalJobs = $this->getTotalAccountCount($accountList) * count($dateRange);
|
||||||
|
|
||||||
|
$this->info('Starting report generation for date range...');
|
||||||
|
$progressBar = $this->output->createProgressBar($totalJobs);
|
||||||
|
$progressBar->start();
|
||||||
|
|
||||||
|
foreach ($dateRange as $period) {
|
||||||
|
foreach ($accountList as $groupName => $accounts) {
|
||||||
|
foreach ($accounts as $accountNumber) {
|
||||||
|
$results['total']++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
// Create report log entry
|
||||||
|
$reportLog = $this->createReportLog($accountNumber, $period, $userId, $groupName);
|
||||||
|
|
||||||
|
if (!$reportLog) {
|
||||||
|
throw new Exception('Failed to create report log entry');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch the job
|
||||||
|
GenerateClosingBalanceReportJob::dispatch($accountNumber, $period, $reportLog->id, $groupName);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
$results['success'][] = [
|
||||||
|
'group' => $groupName,
|
||||||
|
'account' => $accountNumber,
|
||||||
|
'period' => $period,
|
||||||
|
'report_log_id' => $reportLog->id
|
||||||
|
];
|
||||||
|
|
||||||
|
Log::info('Bulk command: Report job dispatched successfully', [
|
||||||
|
'group' => $groupName,
|
||||||
|
'account_number' => $accountNumber,
|
||||||
|
'period' => $period,
|
||||||
|
'report_log_id' => $reportLog->id,
|
||||||
|
'user_id' => $userId
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
DB::rollback();
|
||||||
|
|
||||||
|
$results['failed'][] = [
|
||||||
|
'group' => $groupName,
|
||||||
|
'account' => $accountNumber,
|
||||||
|
'period' => $period,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
];
|
||||||
|
|
||||||
|
Log::error('Bulk command: Error processing account', [
|
||||||
|
'group' => $groupName,
|
||||||
|
'account_number' => $accountNumber,
|
||||||
|
'period' => $period,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$progressBar->advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$progressBar->finish();
|
||||||
|
$this->info('\n');
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create report log entry
|
||||||
|
* Membuat entry log laporan
|
||||||
|
*
|
||||||
|
* @param string $accountNumber
|
||||||
|
* @param string $period
|
||||||
|
* @param int $userId
|
||||||
|
* @param string $groupName
|
||||||
|
* @return ClosingBalanceReportLog|null
|
||||||
|
*/
|
||||||
|
private function createReportLog(string $accountNumber, string $period, int $userId, string $groupName): ?ClosingBalanceReportLog
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Convert period string to Carbon date
|
||||||
|
$reportDate = Carbon::createFromFormat('Ymd', $period);
|
||||||
|
|
||||||
|
$reportLog = ClosingBalanceReportLog::create([
|
||||||
|
'account_number' => $accountNumber,
|
||||||
|
'period' => $period,
|
||||||
|
'report_date' => $reportDate,
|
||||||
|
'status' => 'pending',
|
||||||
|
'user_id' => $userId,
|
||||||
|
'created_by' => $userId,
|
||||||
|
'updated_by' => $userId,
|
||||||
|
'ip_address' => request()->ip() ?? '127.0.0.1',
|
||||||
|
'user_agent' => 'Console Command - Bulk Range',
|
||||||
|
'remarks' => "Bulk generation for group: {$groupName}, period: {$period}",
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $reportLog;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('Bulk command: Error creating report log', [
|
||||||
|
'account_number' => $accountNumber,
|
||||||
|
'period' => $period,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'group_name' => $groupName,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total account count
|
||||||
|
* Menghitung total jumlah rekening
|
||||||
|
*
|
||||||
|
* @param array $accountList
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private function getTotalAccountCount(array $accountList): int
|
||||||
|
{
|
||||||
|
$total = 0;
|
||||||
|
foreach ($accountList as $accounts) {
|
||||||
|
$total += count($accounts);
|
||||||
|
}
|
||||||
|
return $total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show processing results
|
||||||
|
* Menampilkan hasil pemrosesan
|
||||||
|
*
|
||||||
|
* @param array $results
|
||||||
|
*/
|
||||||
|
private function showResults(array $results): void
|
||||||
|
{
|
||||||
|
$this->info('\n=== RESULTS ===');
|
||||||
|
$this->info("Total processed: {$results['total']}");
|
||||||
|
$this->info("Successful: " . count($results['success']));
|
||||||
|
$this->info("Failed: " . count($results['failed']));
|
||||||
|
|
||||||
|
if (!empty($results['failed'])) {
|
||||||
|
$this->error('\nFailed jobs:');
|
||||||
|
foreach (array_slice($results['failed'], 0, 10) as $failed) {
|
||||||
|
$this->error(" - {$failed['group']}: {$failed['account']} ({$failed['period']}) - {$failed['error']}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($results['failed']) > 10) {
|
||||||
|
$remaining = count($results['failed']) - 10;
|
||||||
|
$this->error(" ... and {$remaining} more failed jobs");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($results['success'])) {
|
||||||
|
$this->info('\nSample successful jobs:');
|
||||||
|
foreach (array_slice($results['success'], 0, 5) as $success) {
|
||||||
|
$this->info(" - {$success['group']}: {$success['account']} ({$success['period']}) - Log ID: {$success['report_log_id']}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($results['success']) > 5) {
|
||||||
|
$remaining = count($results['success']) - 5;
|
||||||
|
$this->info(" ... and {$remaining} more successful jobs");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('\nCheck the closing_balance_report_logs table for progress.');
|
||||||
|
$this->info('===============\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
211
app/Console/GenerateClosingBalanceReportCommand.php
Normal file
211
app/Console/GenerateClosingBalanceReportCommand.php
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Console;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Modules\Usermanagement\Models\User;
|
||||||
|
use Modules\Webstatement\Jobs\GenerateClosingBalanceReportJob;
|
||||||
|
use Modules\Webstatement\Models\ClosingBalanceReportLog;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Console command untuk generate laporan closing balance
|
||||||
|
* Command ini dapat dijalankan secara manual atau dijadwalkan
|
||||||
|
*/
|
||||||
|
class GenerateClosingBalanceReportCommand extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'webstatement:generate-closing-balance-report
|
||||||
|
{account_number : Nomor rekening untuk generate laporan}
|
||||||
|
{period : Period laporan format YYYYMMDD, contoh: 20250515}
|
||||||
|
{--user_id=1 : ID user yang menjalankan command (default: 1)}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Generate Closing Balance report untuk nomor rekening dan periode tertentu';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
* Menjalankan proses generate laporan closing balance
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->info('Starting Closing Balance report generation...');
|
||||||
|
|
||||||
|
// Get parameters
|
||||||
|
$accountNumber = $this->argument('account_number');
|
||||||
|
$period = $this->argument('period');
|
||||||
|
$userId = $this->option('user_id');
|
||||||
|
|
||||||
|
// Validate parameters
|
||||||
|
if (!$this->validateParameters($accountNumber, $period, $userId)) {
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
// Log start of process
|
||||||
|
Log::info('Console command: Starting closing balance report generation', [
|
||||||
|
'account_number' => $accountNumber,
|
||||||
|
'period' => $period,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'command' => 'webstatement:generate-closing-balance-report'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create report log entry
|
||||||
|
$reportLog = $this->createReportLog($accountNumber, $period, $userId);
|
||||||
|
|
||||||
|
if (!$reportLog) {
|
||||||
|
$this->error('Failed to create report log entry');
|
||||||
|
DB::rollback();
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch the job
|
||||||
|
GenerateClosingBalanceReportJob::dispatch($accountNumber, $period, $reportLog->id);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
$this->info("Closing Balance report generation job queued successfully!");
|
||||||
|
$this->info("Account Number: {$accountNumber}");
|
||||||
|
$this->info("Period: {$period}");
|
||||||
|
$this->info("Report Log ID: {$reportLog->id}");
|
||||||
|
$this->info('The report will be generated in the background.');
|
||||||
|
$this->info('Check the closing_balance_report_logs table for progress.');
|
||||||
|
|
||||||
|
// Log successful dispatch
|
||||||
|
Log::info('Console command: Closing balance report job dispatched successfully', [
|
||||||
|
'account_number' => $accountNumber,
|
||||||
|
'period' => $period,
|
||||||
|
'report_log_id' => $reportLog->id,
|
||||||
|
'user_id' => $userId
|
||||||
|
]);
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
DB::rollback();
|
||||||
|
|
||||||
|
$this->error('Error queuing Closing Balance report job: ' . $e->getMessage());
|
||||||
|
|
||||||
|
// Log error
|
||||||
|
Log::error('Console command: Error generating closing balance report', [
|
||||||
|
'account_number' => $accountNumber,
|
||||||
|
'period' => $period,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate command parameters
|
||||||
|
* Validasi parameter command
|
||||||
|
*
|
||||||
|
* @param string $accountNumber
|
||||||
|
* @param string $period
|
||||||
|
* @param int $userId
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function validateParameters(string $accountNumber, string $period, int $userId): bool
|
||||||
|
{
|
||||||
|
// Validate account number
|
||||||
|
if (empty($accountNumber)) {
|
||||||
|
$this->error('Account number parameter is required.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate period format (YYYYMMDD)
|
||||||
|
if (!preg_match('/^\\d{8}$/', $period)) {
|
||||||
|
$this->error('Invalid period format. Use YYYYMMDD format (example: 20250515)');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate date
|
||||||
|
$year = substr($period, 0, 4);
|
||||||
|
$month = substr($period, 4, 2);
|
||||||
|
$day = substr($period, 6, 2);
|
||||||
|
|
||||||
|
if (!checkdate($month, $day, $year)) {
|
||||||
|
$this->error('Invalid date in period parameter.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate user exists
|
||||||
|
$user = User::find($userId);
|
||||||
|
if (!$user) {
|
||||||
|
$this->error("User with ID {$userId} not found.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create report log entry
|
||||||
|
* Membuat entry log laporan
|
||||||
|
*
|
||||||
|
* @param string $accountNumber
|
||||||
|
* @param string $period
|
||||||
|
* @param int $userId
|
||||||
|
* @return ClosingBalanceReportLog|null
|
||||||
|
*/
|
||||||
|
private function createReportLog(string $accountNumber, string $period, int $userId): ?ClosingBalanceReportLog
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Convert period string to Carbon date
|
||||||
|
$reportDate = Carbon::createFromFormat('Ymd', $period);
|
||||||
|
|
||||||
|
$reportLog = ClosingBalanceReportLog::create([
|
||||||
|
'account_number' => $accountNumber,
|
||||||
|
'period' => $period,
|
||||||
|
'report_date' => $reportDate, // Required field yang sebelumnya missing
|
||||||
|
'status' => 'pending',
|
||||||
|
'user_id' => $userId,
|
||||||
|
'created_by' => $userId, // Required field yang sebelumnya missing
|
||||||
|
'updated_by' => $userId,
|
||||||
|
'ip_address' => request()->ip() ?? '127.0.0.1', // Default untuk console
|
||||||
|
'user_agent' => 'Console Command',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now()
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::info('Console command: Report log created', [
|
||||||
|
'report_log_id' => $reportLog->id,
|
||||||
|
'account_number' => $accountNumber,
|
||||||
|
'period' => $period,
|
||||||
|
'report_date' => $reportDate->format('Y-m-d'),
|
||||||
|
'user_id' => $userId
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $reportLog;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('Console command: Error creating report log', [
|
||||||
|
'account_number' => $accountNumber,
|
||||||
|
'period' => $period,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
67
app/Console/ProcessDailyMigration.php
Normal file
67
app/Console/ProcessDailyMigration.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Console;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Modules\Webstatement\Http\Controllers\MigrasiController;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
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}
|
||||||
|
{--period= : Period to process (default: -1 day, format: Ymd or relative date)}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Process data migration for the specified period (default: previous day)';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$processParameter = $this->option('process_parameter');
|
||||||
|
$period = $this->option('period');
|
||||||
|
|
||||||
|
// Log start of process
|
||||||
|
Log::info('Starting daily data migration process', [
|
||||||
|
'process_parameter' => $processParameter ?? 'false',
|
||||||
|
'period' => $period ?? '-1 day'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->info('Starting daily data migration process...');
|
||||||
|
$this->info('Process Parameter: ' . ($processParameter ?? 'False'));
|
||||||
|
$this->info('Period: ' . ($period ?? '-1 day (default)'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
$controller = app(MigrasiController::class);
|
||||||
|
$response = $controller->index($processParameter, $period);
|
||||||
|
|
||||||
|
$responseData = json_decode($response->getContent(), true);
|
||||||
|
$message = $responseData['message'] ?? 'Process completed';
|
||||||
|
|
||||||
|
$this->info($message);
|
||||||
|
Log::info('Daily migration process completed successfully', ['message' => $message]);
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$errorMessage = 'Error processing daily migration: ' . $e->getMessage();
|
||||||
|
$this->error($errorMessage);
|
||||||
|
Log::error($errorMessage, ['exception' => $e->getTraceAsString()]);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
96
app/Helpers/helpers.php
Normal file
96
app/Helpers/helpers.php
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Modules\Webstatement\Models\Account;
|
||||||
|
use Modules\Webstatement\Models\ProvinceCore;
|
||||||
|
|
||||||
|
if(!function_exists('calculatePeriodDates')) {
|
||||||
|
/**
|
||||||
|
* Fungsi untuk menghitung tanggal periode berdasarkan periode yang diberikan
|
||||||
|
* Jika periode 202505, mulai dari tanggal 9 sampai akhir bulan
|
||||||
|
* Jika periode lain, mulai dari tanggal 1 sampai akhir bulan
|
||||||
|
*/
|
||||||
|
function calculatePeriodDates($period)
|
||||||
|
{
|
||||||
|
$year = substr($period, 0, 4);
|
||||||
|
$month = substr($period, 4, 2);
|
||||||
|
|
||||||
|
// Log untuk debugging
|
||||||
|
Log::info('Calculating period dates', [
|
||||||
|
'period' => $period,
|
||||||
|
'year' => $year,
|
||||||
|
'month' => $month,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($period === '202505') {
|
||||||
|
// Untuk periode 202505, mulai dari tanggal 9
|
||||||
|
$startDate = \Carbon\Carbon::createFromDate($year, $month, 9,'Asia/Jakarta');
|
||||||
|
} else {
|
||||||
|
// Untuk periode lain, mulai dari tanggal 1
|
||||||
|
$startDate = \Carbon\Carbon::createFromDate($year, $month, 1,'Asia/Jakarta');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tanggal akhir selalu akhir bulan
|
||||||
|
$endDate = \Carbon\Carbon::createFromDate($year, $month, 1)->endOfMonth();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'start' => $startDate,
|
||||||
|
'end' => $endDate,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!function_exists('getProvinceCoreName')){
|
||||||
|
function getProvinceCoreName($code){
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!function_exists('generatePassword')){
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
161
app/Http/Controllers/CombinePdfController.php
Normal file
161
app/Http/Controllers/CombinePdfController.php
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<?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 = 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
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
571
app/Http/Controllers/LaporanClosingBalanceController.php
Normal file
571
app/Http/Controllers/LaporanClosingBalanceController.php
Normal file
@@ -0,0 +1,571 @@
|
|||||||
|
<?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\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Modules\Webstatement\Jobs\GenerateClosingBalanceReportJob;
|
||||||
|
use Modules\Webstatement\Models\ClosingBalanceReportLog;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller untuk mengelola laporan closing balance
|
||||||
|
* Menggunakan job processing untuk menangani laporan dengan banyak transaksi
|
||||||
|
*/
|
||||||
|
class LaporanClosingBalanceController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Menampilkan halaman utama laporan closing balance
|
||||||
|
* dengan form untuk membuat permintaan laporan
|
||||||
|
*
|
||||||
|
* @return \Illuminate\View\View
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
Log::info('Mengakses halaman laporan closing balance');
|
||||||
|
return view('webstatement::laporan-closing-balance.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Membuat permintaan laporan closing balance baru
|
||||||
|
* Menggunakan job untuk memproses laporan secara asynchronous
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @return \Illuminate\Http\RedirectResponse
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
Log::info('Membuat permintaan laporan closing balance', [
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
'request_data' => $request->all()
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'account_number' => ['required', 'string', 'max:50'],
|
||||||
|
'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 = [
|
||||||
|
'account_number' => $validated['account_number'],
|
||||||
|
'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 = ClosingBalanceReportLog::create($reportData);
|
||||||
|
|
||||||
|
// Dispatch the job to generate the report
|
||||||
|
GenerateClosingBalanceReportJob::dispatch(
|
||||||
|
$validated['account_number'],
|
||||||
|
$period,
|
||||||
|
$reportRequest->id
|
||||||
|
);
|
||||||
|
|
||||||
|
$reportRequest->update([
|
||||||
|
'status' => 'processing',
|
||||||
|
'updated_by' => Auth::id()
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
Log::info('Permintaan laporan closing balance berhasil dibuat', [
|
||||||
|
'report_id' => $reportRequest->id,
|
||||||
|
'account_number' => $validated['account_number'],
|
||||||
|
'period' => $period
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('laporan-closing-balance.index')
|
||||||
|
->with('success', 'Permintaan laporan closing balance berhasil dibuat dan sedang diproses.');
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
DB::rollback();
|
||||||
|
|
||||||
|
Log::error('Error saat membuat permintaan laporan closing balance', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->back()
|
||||||
|
->withInput()
|
||||||
|
->with('error', 'Terjadi kesalahan saat membuat permintaan laporan: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menampilkan form untuk membuat permintaan laporan baru
|
||||||
|
*
|
||||||
|
* @return \Illuminate\View\View
|
||||||
|
*/
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
Log::info('Menampilkan form pembuatan laporan closing balance');
|
||||||
|
return view('webstatement::laporan-closing-balance.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menampilkan detail permintaan laporan
|
||||||
|
*
|
||||||
|
* @param ClosingBalanceReportLog $closingBalanceReport
|
||||||
|
* @return \Illuminate\View\View
|
||||||
|
*/
|
||||||
|
public function show(ClosingBalanceReportLog $closingBalanceReport)
|
||||||
|
{
|
||||||
|
Log::info('Menampilkan detail laporan closing balance', [
|
||||||
|
'report_id' => $closingBalanceReport->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
$closingBalanceReport->load(['user', 'creator', 'authorizer']);
|
||||||
|
return view('webstatement::laporan-closing-balance.show', compact('closingBalanceReport'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authorize permintaan laporan
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @param ClosingBalanceReportLog $closingBalanceReport
|
||||||
|
* @return \Illuminate\Http\RedirectResponse
|
||||||
|
*/
|
||||||
|
public function authorize(Request $request, ClosingBalanceReportLog $closingBalanceReport)
|
||||||
|
{
|
||||||
|
Log::info('Authorize laporan closing balance', [
|
||||||
|
'report_id' => $closingBalanceReport->id,
|
||||||
|
'user_id' => Auth::id()
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'authorization_status' => ['required', Rule::in(['approved', 'rejected'])],
|
||||||
|
'remarks' => ['nullable', 'string', 'max:255'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Update authorization status
|
||||||
|
$closingBalanceReport->update([
|
||||||
|
'authorization_status' => $request->authorization_status,
|
||||||
|
'authorized_by' => Auth::id(),
|
||||||
|
'authorized_at' => now(),
|
||||||
|
'remarks' => $request->remarks,
|
||||||
|
'updated_by' => Auth::id()
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
$statusText = $request->authorization_status === 'approved' ? 'disetujui' : 'ditolak';
|
||||||
|
|
||||||
|
Log::info('Laporan closing balance berhasil diauthorize', [
|
||||||
|
'report_id' => $closingBalanceReport->id,
|
||||||
|
'status' => $request->authorization_status
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('laporan-closing-balance.show', $closingBalanceReport->id)
|
||||||
|
->with('success', "Permintaan laporan closing balance berhasil {$statusText}.");
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
DB::rollback();
|
||||||
|
|
||||||
|
Log::error('Error saat authorize laporan', [
|
||||||
|
'report_id' => $closingBalanceReport->id,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('error', 'Terjadi kesalahan saat authorize laporan.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menyediakan data untuk datatables
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function dataForDatatables(Request $request)
|
||||||
|
{
|
||||||
|
Log::info('Mengambil data untuk datatables laporan closing balance', [
|
||||||
|
'filters' => $request->all()
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Retrieve data from the database
|
||||||
|
$query = ClosingBalanceReportLog::query();
|
||||||
|
|
||||||
|
// Apply search filter if provided (handle JSON search parameters)
|
||||||
|
if ($request->has('search') && !empty($request->get('search'))) {
|
||||||
|
$search = $request->get('search');
|
||||||
|
|
||||||
|
// Check if search is JSON format
|
||||||
|
if (is_string($search) && json_decode($search, true) !== null) {
|
||||||
|
$searchParams = json_decode($search, true);
|
||||||
|
|
||||||
|
// Apply account number filter
|
||||||
|
if (!empty($searchParams['account_number'])) {
|
||||||
|
$query->where('account_number', 'LIKE', "%{$searchParams['account_number']}%");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply date range filter
|
||||||
|
if (!empty($searchParams['start_date'])) {
|
||||||
|
$startPeriod = Carbon::createFromFormat('Y-m-d', $searchParams['start_date'])->format('Ymd');
|
||||||
|
$query->where('period', '>=', $startPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($searchParams['end_date'])) {
|
||||||
|
$endPeriod = Carbon::createFromFormat('Y-m-d', $searchParams['end_date'])->format('Ymd');
|
||||||
|
$query->where('period', '<=', $endPeriod);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle regular string search (fallback)
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('account_number', 'LIKE', "%$search%")
|
||||||
|
->orWhere('period', 'LIKE', "%$search%")
|
||||||
|
->orWhere('status', 'LIKE', "%$search%")
|
||||||
|
->orWhere('authorization_status', 'LIKE', "%$search%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply individual parameter filters (for backward compatibility)
|
||||||
|
if ($request->has('account_number') && !empty($request->get('account_number'))) {
|
||||||
|
$query->where('account_number', 'LIKE', "%{$request->get('account_number')}%");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('start_date') && !empty($request->get('start_date'))) {
|
||||||
|
$startPeriod = Carbon::createFromFormat('Y-m-d', $request->get('start_date'))->format('Ymd');
|
||||||
|
$query->where('period', '>=', $startPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('end_date') && !empty($request->get('end_date'))) {
|
||||||
|
$endPeriod = Carbon::createFromFormat('Y-m-d', $request->get('end_date'))->format('Ymd');
|
||||||
|
$query->where('period', '<=', $endPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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']);
|
||||||
|
} else if ($filter['column'] === 'account_number') {
|
||||||
|
$query->where('account_number', 'LIKE', "%{$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 = [
|
||||||
|
'account_number' => 'account_number',
|
||||||
|
'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
|
||||||
|
$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,
|
||||||
|
'account_number' => $item->account_number,
|
||||||
|
'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' => $item->created_at->format('Y-m-d H:i:s'),
|
||||||
|
'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,
|
||||||
|
'record_count' => $item->record_count,
|
||||||
|
'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;
|
||||||
|
|
||||||
|
Log::info('Data laporan closing balance berhasil diambil', [
|
||||||
|
'total_records' => $totalRecords,
|
||||||
|
'filtered_records' => $filteredRecords
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'draw' => $request->get('draw'),
|
||||||
|
'recordsTotal' => $totalRecords,
|
||||||
|
'recordsFiltered' => $filteredRecords,
|
||||||
|
'pageCount' => $pageCount,
|
||||||
|
'page' => $currentPage,
|
||||||
|
'totalCount' => $totalRecords,
|
||||||
|
'data' => $data,
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('Error saat mengambil data datatables', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'error' => 'Terjadi kesalahan saat mengambil data laporan',
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hapus permintaan laporan
|
||||||
|
*
|
||||||
|
* @param ClosingBalanceReportLog $closingBalanceReport
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function destroy(ClosingBalanceReportLog $closingBalanceReport)
|
||||||
|
{
|
||||||
|
Log::info('Menghapus laporan closing balance', [
|
||||||
|
'report_id' => $closingBalanceReport->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
// Delete the file if exists
|
||||||
|
if ($closingBalanceReport->file_path && Storage::exists($closingBalanceReport->file_path)) {
|
||||||
|
Storage::delete($closingBalanceReport->file_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the report request
|
||||||
|
$closingBalanceReport->delete();
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
Log::info('Laporan closing balance berhasil dihapus', [
|
||||||
|
'report_id' => $closingBalanceReport->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Laporan closing balance berhasil dihapus.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
DB::rollback();
|
||||||
|
|
||||||
|
Log::error('Error saat menghapus laporan', [
|
||||||
|
'report_id' => $closingBalanceReport->id,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'error' => 'Terjadi kesalahan saat menghapus laporan',
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry generating laporan closing balance
|
||||||
|
*
|
||||||
|
* @param ClosingBalanceReportLog $closingBalanceReport
|
||||||
|
* @return \Illuminate\Http\RedirectResponse
|
||||||
|
*/
|
||||||
|
public function retry(ClosingBalanceReportLog $closingBalanceReport)
|
||||||
|
{
|
||||||
|
Log::info('Retry laporan closing balance', [
|
||||||
|
'report_id' => $closingBalanceReport->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if retry is allowed
|
||||||
|
$allowedStatuses = ['failed', 'pending'];
|
||||||
|
$isProcessingTooLong = $closingBalanceReport->status === 'processing' &&
|
||||||
|
$closingBalanceReport->updated_at->diffInHours(now()) >= 1;
|
||||||
|
|
||||||
|
if (!in_array($closingBalanceReport->status, $allowedStatuses) && !$isProcessingTooLong) {
|
||||||
|
return back()->with('error', 'Laporan hanya dapat diulang jika status failed, pending, atau processing lebih dari 1 jam.');
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
// If it was processing for too long, mark it as failed first
|
||||||
|
if ($isProcessingTooLong) {
|
||||||
|
$closingBalanceReport->update([
|
||||||
|
'status' => 'failed',
|
||||||
|
'error_message' => 'Processing timeout - melebihi batas waktu 1 jam',
|
||||||
|
'updated_by' => Auth::id()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the report status and clear previous data
|
||||||
|
$closingBalanceReport->update([
|
||||||
|
'status' => 'processing',
|
||||||
|
'error_message' => null,
|
||||||
|
'file_path' => null,
|
||||||
|
'file_size' => null,
|
||||||
|
'record_count' => null,
|
||||||
|
'updated_by' => Auth::id()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Dispatch the job again
|
||||||
|
GenerateClosingBalanceReportJob::dispatch(
|
||||||
|
$closingBalanceReport->account_number,
|
||||||
|
$closingBalanceReport->period,
|
||||||
|
$closingBalanceReport->id
|
||||||
|
);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
Log::info('Laporan closing balance berhasil diulang', [
|
||||||
|
'report_id' => $closingBalanceReport->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('success', 'Job laporan closing balance berhasil diulang.');
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
DB::rollback();
|
||||||
|
|
||||||
|
Log::error('Error saat retry laporan', [
|
||||||
|
'report_id' => $closingBalanceReport->id,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
|
||||||
|
$closingBalanceReport->update([
|
||||||
|
'status' => 'failed',
|
||||||
|
'error_message' => $e->getMessage(),
|
||||||
|
'updated_by' => Auth::id()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('error', 'Gagal mengulang generate laporan: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download laporan berdasarkan nomor rekening dan periode
|
||||||
|
*
|
||||||
|
* @param string $accountNumber
|
||||||
|
* @param string $period
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
public function download($accountNumber, $period)
|
||||||
|
{
|
||||||
|
Log::info('Download laporan closing balance', [
|
||||||
|
'account_number' => $accountNumber,
|
||||||
|
'period' => $period,
|
||||||
|
'user_id' => Auth::id()
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Cari laporan berdasarkan account number dan period
|
||||||
|
$closingBalanceReport = ClosingBalanceReportLog::where('account_number', $accountNumber)
|
||||||
|
->where('period', $period)
|
||||||
|
->where('status', 'completed')
|
||||||
|
->whereNotNull('file_path')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$closingBalanceReport) {
|
||||||
|
Log::warning('Laporan tidak ditemukan atau belum selesai', [
|
||||||
|
'account_number' => $accountNumber,
|
||||||
|
'period' => $period
|
||||||
|
]);
|
||||||
|
return back()->with('error', 'Laporan tidak ditemukan atau belum selesai diproses.');
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
// Update download status
|
||||||
|
$closingBalanceReport->update([
|
||||||
|
'is_downloaded' => true,
|
||||||
|
'downloaded_at' => now(),
|
||||||
|
'updated_by' => Auth::id()
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
// Download the file
|
||||||
|
$filePath = $closingBalanceReport->file_path;
|
||||||
|
if (Storage::exists($filePath)) {
|
||||||
|
$fileName = "closing_balance_report_{$accountNumber}_{$period}.csv";
|
||||||
|
|
||||||
|
Log::info('File laporan berhasil didownload', [
|
||||||
|
'account_number' => $accountNumber,
|
||||||
|
'period' => $period,
|
||||||
|
'file_path' => $filePath
|
||||||
|
]);
|
||||||
|
|
||||||
|
return Storage::download($filePath, $fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::error('File laporan tidak ditemukan di storage', [
|
||||||
|
'account_number' => $accountNumber,
|
||||||
|
'period' => $period,
|
||||||
|
'file_path' => $filePath
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('error', 'File laporan tidak ditemukan.');
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
DB::rollback();
|
||||||
|
|
||||||
|
Log::error('Error saat download laporan', [
|
||||||
|
'account_number' => $accountNumber,
|
||||||
|
'period' => $period,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('error', 'Terjadi kesalahan saat mengunduh laporan: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,150 +1,199 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Modules\Webstatement\Http\Controllers;
|
namespace Modules\Webstatement\Http\Controllers;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Exception;
|
use BadMethodCallException;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Exception;
|
||||||
use Log;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Modules\Webstatement\Jobs\ProcessAccountDataJob;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Modules\Webstatement\Jobs\ProcessArrangementDataJob;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Modules\Webstatement\Jobs\ProcessBillDetailDataJob;
|
use Modules\Webstatement\Jobs\{ProcessAccountDataJob,
|
||||||
use Modules\Webstatement\Jobs\ProcessCustomerDataJob;
|
ProcessArrangementDataJob,
|
||||||
use Modules\Webstatement\Jobs\ProcessFundsTransferDataJob;
|
ProcessAtmTransactionJob,
|
||||||
use Modules\Webstatement\Jobs\ProcessStmtEntryDataJob;
|
ProcessBillDetailDataJob,
|
||||||
use Modules\Webstatement\Jobs\ProcessStmtNarrFormatDataJob;
|
ProcessCategoryDataJob,
|
||||||
use Modules\Webstatement\Jobs\ProcessStmtNarrParamDataJob;
|
ProcessCompanyDataJob,
|
||||||
use Modules\Webstatement\Jobs\ProcessTransactionDataJob;
|
ProcessCustomerDataJob,
|
||||||
|
ProcessDataCaptureDataJob,
|
||||||
|
ProcessFtTxnTypeConditionJob,
|
||||||
|
ProcessFundsTransferDataJob,
|
||||||
|
ProcessStmtEntryDataJob,
|
||||||
|
ProcessStmtNarrFormatDataJob,
|
||||||
|
ProcessStmtNarrParamDataJob,
|
||||||
|
ProcessTellerDataJob,
|
||||||
|
ProcessTransactionDataJob,
|
||||||
|
ProcessSectorDataJob,
|
||||||
|
ProcessProvinceDataJob,
|
||||||
|
ProcessStmtEntryDetailDataJob};
|
||||||
|
|
||||||
class MigrasiController extends Controller
|
class MigrasiController extends Controller
|
||||||
{
|
|
||||||
|
|
||||||
public function processArrangementData($periods)
|
|
||||||
{
|
{
|
||||||
try {
|
private const PROCESS_TYPES = [
|
||||||
ProcessArrangementDataJob::dispatch($periods);
|
'transaction' => ProcessTransactionDataJob::class,
|
||||||
return response()->json(['message' => 'Data Arrangement processing job has been successfully']);
|
'stmtNarrParam' => ProcessStmtNarrParamDataJob::class,
|
||||||
} catch (Exception $e) {
|
'stmtNarrFormat' => ProcessStmtNarrFormatDataJob::class,
|
||||||
return response()->json(['error' => $e->getMessage()], 500);
|
'ftTxnTypeCondition' => ProcessFtTxnTypeConditionJob::class,
|
||||||
}
|
'category' => ProcessCategoryDataJob::class,
|
||||||
}
|
'company' => ProcessCompanyDataJob::class,
|
||||||
|
'customer' => ProcessCustomerDataJob::class,
|
||||||
|
'account' => ProcessAccountDataJob::class,
|
||||||
|
'stmtEntry' => ProcessStmtEntryDataJob::class,
|
||||||
|
'stmtEntryDetail' => ProcessStmtEntryDetailDataJob::class, // Tambahan baru
|
||||||
|
'dataCapture' => ProcessDataCaptureDataJob::class,
|
||||||
|
'fundsTransfer' => ProcessFundsTransferDataJob::class,
|
||||||
|
'teller' => ProcessTellerDataJob::class,
|
||||||
|
'atmTransaction' => ProcessAtmTransactionJob::class,
|
||||||
|
'arrangement' => ProcessArrangementDataJob::class,
|
||||||
|
'billDetail' => ProcessBillDetailDataJob::class,
|
||||||
|
'sector' => ProcessSectorDataJob::class,
|
||||||
|
'province' => ProcessProvinceDataJob::class
|
||||||
|
];
|
||||||
|
|
||||||
public function processCustomerData($periods)
|
private const PARAMETER_PROCESSES = [
|
||||||
{
|
'transaction',
|
||||||
try {
|
'stmtNarrParam',
|
||||||
// Pass the periods to the job for processing
|
'stmtNarrFormat',
|
||||||
ProcessCustomerDataJob::dispatch($periods);
|
'ftTxnTypeCondition',
|
||||||
|
'sector',
|
||||||
|
'province'
|
||||||
|
];
|
||||||
|
|
||||||
return response()->json([
|
private const DATA_PROCESSES = [
|
||||||
'message' => 'Data Customer processing job has been successfully queued',
|
'category',
|
||||||
'periods' => $periods
|
'company',
|
||||||
]);
|
'customer',
|
||||||
} catch (Exception $e) {
|
'account',
|
||||||
Log::error('Error in processCustomerData: ' . $e->getMessage());
|
'stmtEntry',
|
||||||
return response()->json(['error' => $e->getMessage()], 500);
|
'stmtEntryDetail', // Tambahan baru
|
||||||
}
|
'dataCapture',
|
||||||
}
|
'fundsTransfer',
|
||||||
|
'teller',
|
||||||
|
'atmTransaction',
|
||||||
|
'arrangement',
|
||||||
|
'billDetail'
|
||||||
|
];
|
||||||
|
|
||||||
public function processBillDetailData($periods)
|
public function __call($method, $parameters)
|
||||||
{
|
{
|
||||||
try {
|
if (strpos($method, 'process') === 0) {
|
||||||
ProcessBillDetailDataJob::dispatch($periods);
|
$type = lcfirst(substr($method, 7));
|
||||||
return response()->json(['message' => 'Data Bill Details processing job has been successfully']);
|
if (isset(self::PROCESS_TYPES[$type])) {
|
||||||
} catch (Exception $e) {
|
return $this->processData($type, $parameters[0] ?? '');
|
||||||
return response()->json(['error' => $e->getMessage()], 500);
|
}
|
||||||
}
|
}
|
||||||
}
|
throw new BadMethodCallException("Method {$method} does not exist.");
|
||||||
|
|
||||||
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 processStmtEntryData($periods){
|
|
||||||
try {
|
|
||||||
ProcessStmtEntryDataJob::dispatch($periods);
|
|
||||||
return response()->json(['message' => 'Data TempStmtEntry 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']);
|
|
||||||
|
|
||||||
|
|
||||||
// Filter out the _parameter folder
|
|
||||||
$periods = array_filter($allDirectories, function($dir) {
|
|
||||||
return $dir !== '_parameter';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort periods by date (descending)
|
|
||||||
usort($periods, function($a, $b) {
|
|
||||||
return strcmp($b, $a); // Reverse comparison for descending order
|
|
||||||
});
|
|
||||||
|
|
||||||
if (empty($periods)) {
|
|
||||||
return response()->json(['message' => 'No valid period folders found in SFTP storage'], 404);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->processCustomerData(['20250519']);
|
private function processData(string $type, string $period)
|
||||||
$this->processAccountData(['20250519']);
|
: JsonResponse
|
||||||
$this->processArrangementData($periods);
|
{
|
||||||
$this->processBillDetailData($periods);
|
try {
|
||||||
$this->processFundsTransferData($periods);
|
$jobClass = self::PROCESS_TYPES[$type];
|
||||||
$this->processStmtEntryData($periods);
|
$jobClass::dispatch($period);
|
||||||
|
|
||||||
return response()->json(['message' => 'Data processing job has been successfully']);
|
$message = sprintf('%s data processing job has been queued successfully', ucfirst($type));
|
||||||
|
Log::info($message);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Proses migrasi data dengan parameter dan periode yang dapat dikustomisasi
|
||||||
|
*
|
||||||
|
* @param bool|string $processParameter Flag untuk memproses parameter
|
||||||
|
* @param string|null $period Periode yang akan diproses (default: -1 day)
|
||||||
|
* @return JsonResponse
|
||||||
|
*/
|
||||||
|
public function index($processParameter = false, $period = null)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Log::info('Starting migration process', [
|
||||||
|
'process_parameter' => $processParameter,
|
||||||
|
'period' => $period
|
||||||
|
]);
|
||||||
|
|
||||||
|
$disk = Storage::disk('sftpStatement');
|
||||||
|
|
||||||
|
if ($processParameter) {
|
||||||
|
Log::info('Processing parameter data');
|
||||||
|
|
||||||
|
foreach (self::PARAMETER_PROCESSES as $process) {
|
||||||
|
$this->processData($process, '_parameter');
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Parameter processes completed successfully');
|
||||||
|
return response()->json(['message' => 'Parameter processes completed successfully']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tentukan periode yang akan diproses
|
||||||
|
$targetPeriod = $this->determinePeriod($period);
|
||||||
|
|
||||||
|
Log::info('Processing data for period', ['period' => $targetPeriod]);
|
||||||
|
|
||||||
|
if (!$disk->exists($targetPeriod)) {
|
||||||
|
$errorMessage = "Period {$targetPeriod} folder not found in SFTP storage";
|
||||||
|
Log::warning($errorMessage);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
"message" => $errorMessage
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (self::DATA_PROCESSES as $process) {
|
||||||
|
$this->processData($process, $targetPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
$successMessage = "Data processing for period {$targetPeriod} has been queued successfully";
|
||||||
|
Log::info($successMessage);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => $successMessage
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('Error in migration index method: ' . $e->getMessage());
|
||||||
|
return response()->json(['error' => $e->getMessage()], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tentukan periode berdasarkan input atau gunakan default
|
||||||
|
*
|
||||||
|
* @param string|null $period Input periode
|
||||||
|
* @return string Periode dalam format Ymd
|
||||||
|
*/
|
||||||
|
private function determinePeriod($period = null): string
|
||||||
|
{
|
||||||
|
if ($period === null) {
|
||||||
|
// Default: -1 day
|
||||||
|
$calculatedPeriod = date('Ymd', strtotime('-1 day'));
|
||||||
|
Log::info('Using default period', ['period' => $calculatedPeriod]);
|
||||||
|
return $calculatedPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika periode sudah dalam format Ymd (8 digit)
|
||||||
|
if (preg_match('/^\d{8}$/', $period)) {
|
||||||
|
Log::info('Using provided period in Ymd format', ['period' => $period]);
|
||||||
|
return $period;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika periode dalam format relative date (contoh: -2 days, -1 week, etc.)
|
||||||
|
try {
|
||||||
|
$calculatedPeriod = date('Ymd', strtotime($period));
|
||||||
|
Log::info('Calculated period from relative date', [
|
||||||
|
'input' => $period,
|
||||||
|
'calculated' => $calculatedPeriod
|
||||||
|
]);
|
||||||
|
return $calculatedPeriod;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::warning('Invalid period format, using default', [
|
||||||
|
'input' => $period,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
return date('Ymd', strtotime('-1 day'));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,65 +1,204 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Modules\Webstatement\Http\Controllers;
|
namespace Modules\Webstatement\Http\Controllers;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Contracts\Bus\Dispatcher;
|
||||||
|
use Modules\Webstatement\Jobs\ExportStatementJob;
|
||||||
|
use Modules\Webstatement\Models\AccountBalance;
|
||||||
|
use Modules\Webstatement\Jobs\ExportStatementPeriodJob;
|
||||||
|
|
||||||
class WebstatementController extends Controller
|
class WebstatementController extends Controller
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Display a listing of the resource.
|
|
||||||
*/
|
|
||||||
public function index()
|
|
||||||
{
|
{
|
||||||
return view('webstatement::index');
|
/**
|
||||||
}
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$jobIds = [];
|
||||||
|
$data = [];
|
||||||
|
|
||||||
/**
|
foreach ($this->listAccount() as $clientName => $accounts) {
|
||||||
* Show the form for creating a new resource.
|
foreach ($accounts as $accountNumber) {
|
||||||
*/
|
foreach ($this->listPeriod() as $period) {
|
||||||
public function create()
|
$job = new ExportStatementJob(
|
||||||
{
|
$accountNumber,
|
||||||
return view('webstatement::create');
|
$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([
|
||||||
* Store a newly created resource in storage.
|
'message' => 'Statement export jobs have been queued',
|
||||||
*/
|
'jobs' => array_map(function ($index, $jobId) use ($data) {
|
||||||
public function store(Request $request)
|
return [
|
||||||
{
|
'job_id' => $jobId,
|
||||||
//
|
'client_name' => $data[$index]['client_name'],
|
||||||
}
|
'account_number' => $data[$index]['account_number'],
|
||||||
|
'period' => $data[$index]['period'],
|
||||||
|
'file_name' => "{$data[$index]['client_name']}_{$data[$index]['account_number']}_{$data[$index]['period']}.csv"
|
||||||
|
];
|
||||||
|
}, array_keys($jobIds), $jobIds)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
function listAccount(){
|
||||||
* Show the specified resource.
|
return [
|
||||||
*/
|
'PLUANG' => [
|
||||||
public function show($id)
|
'1080426085',
|
||||||
{
|
'1080425781',
|
||||||
return view('webstatement::show');
|
],
|
||||||
}
|
'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',
|
||||||
|
],
|
||||||
|
"AWAN_LINTANG_SOLUSI"=> [
|
||||||
|
"1084269430"
|
||||||
|
],
|
||||||
|
"MONETA"=> [
|
||||||
|
"1085667890"
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
function listPeriod(){
|
||||||
* Show the form for editing the specified resource.
|
return [
|
||||||
*/
|
date('Ymd', strtotime('-1 day'))
|
||||||
public function edit($id)
|
];
|
||||||
{
|
}
|
||||||
return view('webstatement::edit');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the specified resource in storage.
|
|
||||||
*/
|
|
||||||
public function update(Request $request, $id)
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
function getAccountBalance($accountNumber, $period)
|
||||||
* Remove the specified resource from storage.
|
{
|
||||||
*/
|
$accountBalance = AccountBalance::where('account_number', $accountNumber)
|
||||||
public function destroy($id)
|
->where('period', '<', $period)
|
||||||
{
|
->orderBy('period', 'desc')
|
||||||
//
|
->first();
|
||||||
|
|
||||||
|
return $accountBalance->actual_balance ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
try {
|
||||||
|
\Log::info("Starting statement export for account: {$accountNumber}, period: {$period}, client: {$clientName}");
|
||||||
|
|
||||||
|
// Validate inputs
|
||||||
|
if (empty($accountNumber) || empty($period) || empty($clientName)) {
|
||||||
|
throw new \Exception('Required parameters missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch the job
|
||||||
|
$job = ExportStatementPeriodJob::dispatch($accountNumber, $period, $balance, $clientName);
|
||||||
|
|
||||||
|
\Log::info("Statement export job dispatched successfully", [
|
||||||
|
'job_id' => $job->job_id ?? null,
|
||||||
|
'account' => $accountNumber,
|
||||||
|
'period' => $period,
|
||||||
|
'client' => $clientName
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Statement export job queued successfully',
|
||||||
|
'data' => [
|
||||||
|
'job_id' => $job->job_id ?? null,
|
||||||
|
'account_number' => $accountNumber,
|
||||||
|
'period' => $period,
|
||||||
|
'client_name' => $clientName
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
} 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,43 +1,73 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Modules\Webstatement\Http\Requests;
|
namespace Modules\Webstatement\Http\Requests;
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
use Modules\Webstatement\Models\PrintStatementLog as Statement;
|
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
|
||||||
{
|
{
|
||||||
/**
|
return true;
|
||||||
* Determine if the user is authorized to make this request.
|
}
|
||||||
*/
|
|
||||||
public function authorize()
|
|
||||||
: bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
*
|
*/
|
||||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
public function rules(): array
|
||||||
*/
|
{
|
||||||
public function rules()
|
$rules = [
|
||||||
: array
|
'branch_code' => ['required', 'string'],
|
||||||
{
|
// account_number required jika stmt_sent_type tidak diisi atau kosong
|
||||||
$rules = [
|
'account_number' => [
|
||||||
'branch_code' => ['required', 'string', 'exists:branches,code'],
|
function ($attribute, $value, $fail) {
|
||||||
'account_number' => ['required', 'string'],
|
$stmtSentType = $this->input('stmt_sent_type');
|
||||||
'is_period_range' => ['sometimes', 'boolean'],
|
|
||||||
'email' => ['nullable', 'email'],
|
// Jika stmt_sent_type kosong atau tidak ada, maka account_number wajib diisi
|
||||||
'email_sent_at' => ['nullable', 'timestamp'],
|
if (empty($stmtSentType) || (is_array($stmtSentType) && count(array_filter($stmtSentType)) === 0)) {
|
||||||
'period_from' => [
|
if (empty($value)) {
|
||||||
'required',
|
$fail('Account number is required when statement type is not specified.');
|
||||||
'string',
|
}
|
||||||
'regex:/^\d{6}$/', // YYYYMM format
|
}
|
||||||
// Prevent duplicate requests with same account number and period
|
}
|
||||||
function ($attribute, $value, $fail) {
|
],
|
||||||
|
'stmt_sent_type' => ['nullable', 'array'],
|
||||||
|
'stmt_sent_type.*' => ['string', 'in:ALL,BY.EMAIL,BY.MAIL.TO.DOM.ADDR,BY.MAIL.TO.KTP.ADDR,NO.PRINT,PRINT'],
|
||||||
|
'is_period_range' => ['sometimes', 'boolean'],
|
||||||
|
'email' => ['nullable', 'email'],
|
||||||
|
'email_sent_at' => ['nullable', 'timestamp'],
|
||||||
|
'request_type' => ['sometimes', 'string', 'in:single_account,branch,all_branches,multi_account'],
|
||||||
|
'batch_id' => ['nullable', 'string'],
|
||||||
|
// Password wajib diisi jika request_type diisi
|
||||||
|
'password' => [
|
||||||
|
function ($attribute, $value, $fail) {
|
||||||
|
$requestType = $this->input('stmt_sent_type');
|
||||||
|
|
||||||
|
// Jika request_type diisi, maka password wajib diisi
|
||||||
|
if (!empty($requestType) && empty($value)) {
|
||||||
|
$fail('Password is required when statement sent type is specified.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'period_from' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'regex:/^\d{6}$/', // YYYYMM format
|
||||||
|
// Prevent duplicate requests with same account number and period
|
||||||
|
function ($attribute, $value, $fail) {
|
||||||
|
// Hanya cek duplikasi jika account_number ada
|
||||||
|
if (!empty($this->input('account_number'))) {
|
||||||
$query = Statement::where('account_number', $this->input('account_number'))
|
$query = Statement::where('account_number', $this->input('account_number'))
|
||||||
->where('authorization_status', '!=', 'rejected')
|
->where('authorization_status', '!=', 'rejected')
|
||||||
|
->where(function($query) {
|
||||||
|
$query->where('is_available', true)
|
||||||
|
->orWhere('is_generated', true);
|
||||||
|
})
|
||||||
|
->where('user_id', $this->user()->id)
|
||||||
->where('period_from', $value);
|
->where('period_from', $value);
|
||||||
|
|
||||||
// If this is an update request, exclude the current record
|
// If this is an update request, exclude the current record
|
||||||
@@ -57,69 +87,74 @@
|
|||||||
$fail('A statement request with this account number and period already exists.');
|
$fail('A statement request with this account number and period already exists.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
}
|
||||||
];
|
],
|
||||||
|
];
|
||||||
|
|
||||||
// If it's a period range, require period_to
|
// If it's a period range, require period_to
|
||||||
if ($this->input('period_to')) {
|
if ($this->input('period_to')) {
|
||||||
$rules['period_to'] = [
|
$rules['period_to'] = [
|
||||||
'required',
|
'required',
|
||||||
'string',
|
'string',
|
||||||
'regex:/^\d{6}$/', // YYYYMM format
|
'regex:/^\d{6}$/', // YYYYMM format
|
||||||
'gte:period_from' // period_to must be greater than or equal to period_from
|
'gte:period_from' // period_to must be greater than or equal to period_from
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $rules;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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',
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return $rules;
|
||||||
* 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),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->has('period_to')) {
|
/**
|
||||||
//conver to YYYYMM format
|
* Get custom messages for validator errors.
|
||||||
$this->merge([
|
*/
|
||||||
'period_to' => substr($this->period_to, 0, 4) . substr($this->period_to, 5, 2),
|
public function messages(): array
|
||||||
]);
|
{
|
||||||
}
|
return [
|
||||||
|
'branch_code.required' => 'Branch code is required',
|
||||||
|
'branch_code.string' => 'Branch code must be a string',
|
||||||
|
'account_number.required' => 'Account number is required when statement type is not specified',
|
||||||
|
'stmt_sent_type.*.in' => 'Invalid statement type selected',
|
||||||
|
'period_from.required' => 'Period is required',
|
||||||
|
'period_from.regex' => 'Period must be in YYYYMM format',
|
||||||
|
'period_to.required' => 'End period is required for period range',
|
||||||
|
'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, all_branches, or multi_account',
|
||||||
|
'password.required' => 'Password is required when statement sent type is specified',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// 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([
|
$this->merge([
|
||||||
'is_period_range' => true,
|
'is_period_range' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set default request_type if not provided
|
||||||
|
if (!$this->has('request_type')) {
|
||||||
|
$this->merge([
|
||||||
|
'request_type' => 'single_account',
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
348
app/Jobs/AutoSendStatementEmailJob.php
Normal file
348
app/Jobs/AutoSendStatementEmailJob.php
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Jobs;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use ZipArchive;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Modules\Webstatement\Models\Account;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Modules\Webstatement\Mail\StatementEmail;
|
||||||
|
use Modules\Webstatement\Models\PrintStatementLog;
|
||||||
|
|
||||||
|
class AutoSendStatementEmailJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeout untuk job dalam detik (10 menit)
|
||||||
|
*/
|
||||||
|
public $timeout = 600;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jumlah maksimal retry jika job gagal
|
||||||
|
*/
|
||||||
|
public $tries = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
// Constructor kosong karena job ini tidak memerlukan parameter
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job untuk mengirim email statement secara otomatis
|
||||||
|
*
|
||||||
|
* Job ini akan:
|
||||||
|
* 1. Mencari statement yang siap dikirim (is_available/is_generated = true, email_sent_at = null)
|
||||||
|
* 2. Memvalidasi keberadaan email
|
||||||
|
* 3. Mengirim email dengan attachment PDF
|
||||||
|
* 4. Update status email_sent_at
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Log::info('AutoSendStatementEmailJob: Memulai proses auto send email');
|
||||||
|
|
||||||
|
// Ambil statement yang siap dikirim email
|
||||||
|
$statements = $this->getPendingEmailStatements();
|
||||||
|
|
||||||
|
Log::info($statements);
|
||||||
|
if ($statements->isEmpty()) {
|
||||||
|
Log::info('AutoSendStatementEmailJob: Tidak ada statement yang perlu dikirim email');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('AutoSendStatementEmailJob: Ditemukan statement untuk dikirim', [
|
||||||
|
'count' => $statements->count(),
|
||||||
|
'statement_ids' => $statements->pluck('id')->toArray()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Proses setiap statement
|
||||||
|
foreach ($statements as $statement) {
|
||||||
|
$this->processSingleStatement($statement);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('AutoSendStatementEmailJob: Selesai memproses semua statement');
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('AutoSendStatementEmailJob: Error dalam proses auto send email', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mengambil statement yang siap untuk dikirim email
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Collection
|
||||||
|
*/
|
||||||
|
private function getPendingEmailStatements()
|
||||||
|
{
|
||||||
|
return PrintStatementLog::where(function ($query) {
|
||||||
|
// Statement yang sudah available atau generated
|
||||||
|
$query->where('is_available', true)
|
||||||
|
->orWhere('is_generated', true);
|
||||||
|
})
|
||||||
|
->whereNotNull('email') // Harus ada email
|
||||||
|
->where('email', '!=', '') // Email tidak kosong
|
||||||
|
->whereNull('email_sent_at') // Belum pernah dikirim
|
||||||
|
->whereNull('deleted_at') // Tidak soft deleted
|
||||||
|
->orderBy('created_at', 'desc') // Prioritas yang lama dulu
|
||||||
|
->limit(1) // Batasi maksimal 50 per run untuk performa
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memproses pengiriman email untuk satu statement
|
||||||
|
*
|
||||||
|
* @param PrintStatementLog $statement
|
||||||
|
*/
|
||||||
|
private function processSingleStatement(PrintStatementLog $statement): void
|
||||||
|
{
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Log::info('AutoSendStatementEmailJob: Memproses statement', [
|
||||||
|
'statement_id' => $statement->id,
|
||||||
|
'account_number' => $statement->account_number,
|
||||||
|
'email' => $statement->email
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Inisialisasi disk local dan SFTP
|
||||||
|
$localDisk = Storage::disk('local');
|
||||||
|
$sftpDisk = Storage::disk('sftpStatement');
|
||||||
|
|
||||||
|
$filePath = "{$statement->period_from}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fungsi helper untuk mendapatkan file dari disk dengan prioritas local
|
||||||
|
* @param string $path - Path file yang dicari
|
||||||
|
* @return array - [disk, exists, content]
|
||||||
|
*/
|
||||||
|
$getFileFromDisk = function($path) use ($localDisk, $sftpDisk) {
|
||||||
|
// Cek di local disk terlebih dahulu
|
||||||
|
if ($localDisk->exists("statements/{$path}")) {
|
||||||
|
Log::info('AutoSendStatementEmailJob: File found in local disk', ['path' => "statements/{$path}"]);
|
||||||
|
return [
|
||||||
|
'disk' => $localDisk,
|
||||||
|
'exists' => true,
|
||||||
|
'path' => "statements/{$path}",
|
||||||
|
'source' => 'local'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika tidak ada di local, cek di SFTP
|
||||||
|
if ($sftpDisk->exists($path)) {
|
||||||
|
Log::info('AutoSendStatementEmailJob: File found in SFTP disk', ['path' => $path]);
|
||||||
|
return [
|
||||||
|
'disk' => $sftpDisk,
|
||||||
|
'exists' => true,
|
||||||
|
'path' => $path,
|
||||||
|
'source' => 'sftp'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::warning('AutoSendStatementEmailJob: File not found in any disk', ['path' => $path]);
|
||||||
|
return [
|
||||||
|
'disk' => null,
|
||||||
|
'exists' => false,
|
||||||
|
'path' => $path,
|
||||||
|
'source' => 'none'
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($statement->is_period_range && $statement->period_to) {
|
||||||
|
$this->processMultiPeriodStatement($statement, $getFileFromDisk);
|
||||||
|
} else {
|
||||||
|
$this->processSinglePeriodStatement($statement, $getFileFromDisk);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update statement record to mark as emailed
|
||||||
|
$statement->update([
|
||||||
|
'email_sent_at' => now(),
|
||||||
|
'updated_by' => 1 // System user ID, bisa disesuaikan
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::info('AutoSendStatementEmailJob: Email berhasil dikirim', [
|
||||||
|
'statement_id' => $statement->id,
|
||||||
|
'email' => $statement->email
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
|
||||||
|
Log::error('AutoSendStatementEmailJob: Gagal mengirim email untuk statement', [
|
||||||
|
'statement_id' => $statement->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Jangan throw exception untuk statement individual agar tidak menghentikan proses lainnya
|
||||||
|
// Hanya log error saja
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memproses statement dengan multiple period (range)
|
||||||
|
*
|
||||||
|
* @param PrintStatementLog $statement
|
||||||
|
* @param callable $getFileFromDisk
|
||||||
|
*/
|
||||||
|
private function processMultiPeriodStatement(PrintStatementLog $statement, callable $getFileFromDisk): void
|
||||||
|
{
|
||||||
|
$periodFrom = Carbon::createFromFormat('Ym', $statement->period_from);
|
||||||
|
$periodTo = Carbon::createFromFormat('Ym', $statement->period_to);
|
||||||
|
|
||||||
|
// Loop through each month in the range
|
||||||
|
$missingPeriods = [];
|
||||||
|
$availablePeriods = [];
|
||||||
|
$periodFiles = []; // Menyimpan info file untuk setiap periode
|
||||||
|
|
||||||
|
for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) {
|
||||||
|
$periodFormatted = $period->format('Ym');
|
||||||
|
$periodPath = "{$periodFormatted}/{$statement->branch_code}/{$statement->account_number}_{$periodFormatted}.pdf";
|
||||||
|
|
||||||
|
$fileInfo = $getFileFromDisk($periodPath);
|
||||||
|
|
||||||
|
if ($fileInfo['exists']) {
|
||||||
|
$availablePeriods[] = $periodFormatted;
|
||||||
|
$periodFiles[$periodFormatted] = $fileInfo;
|
||||||
|
} else {
|
||||||
|
$missingPeriods[] = $periodFormatted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any period is available, create a zip and send it
|
||||||
|
if (count($availablePeriods) > 0) {
|
||||||
|
// Create a temporary zip file
|
||||||
|
$zipFileName = "{$statement->account_number}_{$statement->period_from}_to_{$statement->period_to}.zip";
|
||||||
|
$zipFilePath = storage_path("app/temp/{$zipFileName}");
|
||||||
|
|
||||||
|
// Ensure the temp directory exists
|
||||||
|
if (!file_exists(storage_path('app/temp'))) {
|
||||||
|
mkdir(storage_path('app/temp'), 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new zip archive
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) {
|
||||||
|
// Add each available statement to the zip
|
||||||
|
foreach ($availablePeriods as $period) {
|
||||||
|
$fileInfo = $periodFiles[$period];
|
||||||
|
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf");
|
||||||
|
|
||||||
|
// Download/copy the file to local temp storage
|
||||||
|
file_put_contents($localFilePath, $fileInfo['disk']->get($fileInfo['path']));
|
||||||
|
|
||||||
|
Log::info('AutoSendStatementEmailJob: File retrieved for zip', [
|
||||||
|
'period' => $period,
|
||||||
|
'source' => $fileInfo['source'],
|
||||||
|
'path' => $fileInfo['path']
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add the file to the zip
|
||||||
|
$zip->addFile($localFilePath, "{$statement->account_number}_{$period}.pdf");
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
// Send email with zip attachment
|
||||||
|
Mail::to($statement->email)
|
||||||
|
->send(new StatementEmail($statement, $zipFilePath, true));
|
||||||
|
|
||||||
|
// Clean up temporary files
|
||||||
|
foreach ($availablePeriods as $period) {
|
||||||
|
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf");
|
||||||
|
if (file_exists($localFilePath)) {
|
||||||
|
unlink($localFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the zip file after sending
|
||||||
|
if (file_exists($zipFilePath)) {
|
||||||
|
unlink($zipFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('AutoSendStatementEmailJob: Multi-period statement email sent successfully', [
|
||||||
|
'statement_id' => $statement->id,
|
||||||
|
'periods' => $availablePeriods,
|
||||||
|
'sources' => array_map(fn($p) => $periodFiles[$p]['source'], $availablePeriods)
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
throw new Exception('Failed to create zip archive for email.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Exception('No statements available for sending.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memproses statement dengan single period
|
||||||
|
*
|
||||||
|
* @param PrintStatementLog $statement
|
||||||
|
* @param callable $getFileFromDisk
|
||||||
|
*/
|
||||||
|
private function processSinglePeriodStatement(PrintStatementLog $statement, callable $getFileFromDisk): void
|
||||||
|
{
|
||||||
|
$account = Account::where('account_number',$statement->account_number)->first();
|
||||||
|
$filePath = "{$statement->period_from}/{$account->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf";
|
||||||
|
$fileInfo = $getFileFromDisk($filePath);
|
||||||
|
|
||||||
|
if ($fileInfo['exists']) {
|
||||||
|
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$statement->period_from}.pdf");
|
||||||
|
|
||||||
|
// Ensure the temp directory exists
|
||||||
|
if (!file_exists(storage_path('app/temp'))) {
|
||||||
|
mkdir(storage_path('app/temp'), 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download/copy the file to local temp storage
|
||||||
|
file_put_contents($localFilePath, $fileInfo['disk']->get($fileInfo['path']));
|
||||||
|
|
||||||
|
Log::info('AutoSendStatementEmailJob: Single period file retrieved', [
|
||||||
|
'source' => $fileInfo['source'],
|
||||||
|
'path' => $fileInfo['path']
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Send email with PDF attachment
|
||||||
|
Mail::to($statement->email)
|
||||||
|
->send(new StatementEmail($statement, $localFilePath, false));
|
||||||
|
|
||||||
|
// Delete the temporary file
|
||||||
|
if (file_exists($localFilePath)) {
|
||||||
|
unlink($localFilePath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Exception('Statement file not found.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle job failure
|
||||||
|
*
|
||||||
|
* @param Exception $exception
|
||||||
|
*/
|
||||||
|
public function failed(Exception $exception): void
|
||||||
|
{
|
||||||
|
Log::error('AutoSendStatementEmailJob: Job failed completely', [
|
||||||
|
'error' => $exception->getMessage(),
|
||||||
|
'trace' => $exception->getTraceAsString()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
463
app/Jobs/ExportStatementJob.php
Normal file
463
app/Jobs/ExportStatementJob.php
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
<?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 ExportStatementJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
protected $account_number;
|
||||||
|
protected $period;
|
||||||
|
protected $saldo;
|
||||||
|
protected $disk;
|
||||||
|
protected $client;
|
||||||
|
protected $fileName;
|
||||||
|
protected $chunkSize = 1000; // Proses data dalam chunk untuk mengurangi penggunaan memori
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*
|
||||||
|
* @param string $account_number
|
||||||
|
* @param string $period
|
||||||
|
* @param string $saldo
|
||||||
|
* @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";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
: void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Log::info("Starting export statement job for account: {$this->account_number}, period: {$this->period}");
|
||||||
|
|
||||||
|
$this->processStatementData();
|
||||||
|
|
||||||
|
// Export data yang sudah diproses ke CSV
|
||||||
|
$this->exportToCsv();
|
||||||
|
|
||||||
|
Log::info("Export statement job completed successfully for account: {$this->account_number}, period: {$this->period}");
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error("Error in ExportStatementJob: " . $e->getMessage());
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processStatementData()
|
||||||
|
: void
|
||||||
|
{
|
||||||
|
$accountQuery = [
|
||||||
|
'account_number' => $this->account_number,
|
||||||
|
'period' => $this->period
|
||||||
|
];
|
||||||
|
|
||||||
|
$totalCount = $this->getTotalEntryCount($accountQuery);
|
||||||
|
$existingDataCount = $this->getExistingProcessedCount($accountQuery);
|
||||||
|
|
||||||
|
// Hanya proses jika data belum lengkap diproses
|
||||||
|
//if ($existingDataCount !== $totalCount) {
|
||||||
|
$this->deleteExistingProcessedData($accountQuery);
|
||||||
|
$this->processAndSaveStatementEntries($totalCount);
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getTotalEntryCount(array $criteria)
|
||||||
|
: int
|
||||||
|
{
|
||||||
|
return StmtEntry::where('account_number', $criteria['account_number'])
|
||||||
|
->where('booking_date', $criteria['period'])
|
||||||
|
->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)
|
||||||
|
->where('booking_date', $this->period)
|
||||||
|
->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,
|
||||||
|
'recipt_no' => $item->ft?->recipt_no ?? '-',
|
||||||
|
'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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
$accountPath = "{$basePath}/{$this->account_number}";
|
||||||
|
|
||||||
|
// PERBAIKAN: Selalu pastikan direktori dibuat
|
||||||
|
Storage::disk($this->disk)->makeDirectory($basePath);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Tambahkan di awal fungsi exportToCsv
|
||||||
|
Log::info("Starting CSV export", [
|
||||||
|
'disk' => $this->disk,
|
||||||
|
'client' => $this->client,
|
||||||
|
'account_number' => $this->account_number,
|
||||||
|
'period' => $this->period,
|
||||||
|
'base_path' => $basePath,
|
||||||
|
'account_path' => $accountPath,
|
||||||
|
'file_path' => $filePath
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Cek apakah disk storage berfungsi
|
||||||
|
$testFile = 'test_' . time() . '.txt';
|
||||||
|
Storage::disk($this->disk)->put($testFile, 'test content');
|
||||||
|
if (Storage::disk($this->disk)->exists($testFile)) {
|
||||||
|
Log::info("Storage disk is working");
|
||||||
|
Storage::disk($this->disk)->delete($testFile);
|
||||||
|
} else {
|
||||||
|
Log::error("Storage disk is not working properly");
|
||||||
|
}
|
||||||
|
|
||||||
|
// PERBAIKAN: Buat file header terlebih dahulu
|
||||||
|
$csvContent = "NO|TRANSACTION.DATE|REFERENCE.NUMBER|TRANSACTION.AMOUNT|TRANSACTION.TYPE|DESCRIPTION|END.BALANCE|ACTUAL.DATE|NO.RECEIPT\n";
|
||||||
|
Storage::disk($this->disk)->put($filePath, $csvContent);
|
||||||
|
|
||||||
|
// Ambil data yang sudah diproses dalam chunk
|
||||||
|
ProcessedStatement::where('account_number', $this->account_number)
|
||||||
|
->where('period', $this->period)
|
||||||
|
->orderBy('sequence_no')
|
||||||
|
->chunk($this->chunkSize, function ($statements) use ($filePath) {
|
||||||
|
$csvContent = '';
|
||||||
|
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,
|
||||||
|
$statement->recipt_no
|
||||||
|
]) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append ke file
|
||||||
|
if (!empty($csvContent)) {
|
||||||
|
Storage::disk($this->disk)->append($filePath, $csvContent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PERBAIKAN: Verifikasi file benar-benar ada
|
||||||
|
if (Storage::disk($this->disk)->exists($filePath)) {
|
||||||
|
$fileSize = Storage::disk($this->disk)->size($filePath);
|
||||||
|
Log::info("Statement exported successfully", [
|
||||||
|
'disk' => $this->disk,
|
||||||
|
'file_path' => $filePath,
|
||||||
|
'file_size' => $fileSize,
|
||||||
|
'account_number' => $this->account_number,
|
||||||
|
'period' => $this->period
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
Log::error("File was not created despite successful processing", [
|
||||||
|
'disk' => $this->disk,
|
||||||
|
'file_path' => $filePath,
|
||||||
|
'account_number' => $this->account_number,
|
||||||
|
'period' => $this->period
|
||||||
|
]);
|
||||||
|
throw new \Exception("Failed to create CSV file: {$filePath}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get transaction data by reference and field
|
||||||
|
*/
|
||||||
|
private function getTransaction($ref, $field)
|
||||||
|
{
|
||||||
|
$trans = TempFundsTransfer::where('ref_no', $ref)->first();
|
||||||
|
return $trans->$field ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
660
app/Jobs/ExportStatementPeriodJob.php
Normal file
660
app/Jobs/ExportStatementPeriodJob.php
Normal file
@@ -0,0 +1,660 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Jobs;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Bus\{
|
||||||
|
Queueable
|
||||||
|
};
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Queue\{
|
||||||
|
InteractsWithQueue,
|
||||||
|
SerializesModels
|
||||||
|
};
|
||||||
|
use Illuminate\Support\Facades\{
|
||||||
|
DB,
|
||||||
|
Log,
|
||||||
|
Storage
|
||||||
|
};
|
||||||
|
use Spatie\Browsershot\Browsershot;
|
||||||
|
use Modules\Webstatement\Models\{
|
||||||
|
PrintStatementLog,
|
||||||
|
ProcessedStatement,
|
||||||
|
StmtEntry,
|
||||||
|
TempFundsTransfer,
|
||||||
|
TempStmtNarrFormat,
|
||||||
|
TempStmtNarrParam,
|
||||||
|
Account,
|
||||||
|
Customer
|
||||||
|
};
|
||||||
|
use Modules\Basicdata\Models\Branch;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Owenoj\PDFPasswordProtect\Facade\PDFPasswordProtect;
|
||||||
|
|
||||||
|
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;
|
||||||
|
protected $toCsv;
|
||||||
|
protected $statementId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(int $statementId, string $account_number, string $period, string $saldo, string $client = '', string $disk = 'local', bool $toCsv = true)
|
||||||
|
{
|
||||||
|
$this->statementId = $statementId;
|
||||||
|
$this->account_number = $account_number;
|
||||||
|
$this->period = $period;
|
||||||
|
$this->saldo = $saldo;
|
||||||
|
$this->disk = $disk;
|
||||||
|
$this->client = $client;
|
||||||
|
$this->fileName = "{$account_number}_{$period}.csv";
|
||||||
|
$this->toCsv = $toCsv;
|
||||||
|
|
||||||
|
// Calculate start and end dates based on period
|
||||||
|
$this->calculatePeriodDates();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, 9)->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();
|
||||||
|
if($this->toCsv){
|
||||||
|
$this->exportToCsv();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate PDF setelah data diproses
|
||||||
|
$this->generatePdf();
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
$query = StmtEntry::where('account_number', $this->account_number)
|
||||||
|
->whereBetween('booking_date', [
|
||||||
|
$this->startDate->format('Ymd'),
|
||||||
|
$this->endDate->format('Ymd')
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::info("Getting total entry count with query: " . $query->toSql(), [
|
||||||
|
'bindings' => $query->getBindings(),
|
||||||
|
'account' => $this->account_number,
|
||||||
|
'start_date' => $this->startDate->format('Ymd'),
|
||||||
|
'end_date' => $this->endDate->format('Ymd')
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $query->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getExistingProcessedCount(array $criteria): int
|
||||||
|
{
|
||||||
|
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}");
|
||||||
|
|
||||||
|
$entry = StmtEntry::with(['ft', 'transaction'])
|
||||||
|
->where('account_number', $this->account_number)
|
||||||
|
->whereBetween('booking_date', [
|
||||||
|
$this->startDate->format('Ymd'),
|
||||||
|
$this->endDate->format('Ymd')
|
||||||
|
])
|
||||||
|
->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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if($entry){
|
||||||
|
$printLog = PrintStatementLog::find($this->statementId);
|
||||||
|
if($printLog){
|
||||||
|
$printLog->update(['is_generated' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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' => $item->booking_date,
|
||||||
|
'reference_number' => $item->trans_reference,
|
||||||
|
'transaction_amount' => $item->amount_lcy,
|
||||||
|
'transaction_type' => $item->amount_lcy < 0 ? 'D' : 'C',
|
||||||
|
'description' => $this->generateNarrative($item),
|
||||||
|
'end_balance' => $runningBalance,
|
||||||
|
'actual_date' => $actualDate,
|
||||||
|
'recipt_no' => $item->ft?->recipt_no ?? '-',
|
||||||
|
'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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate PDF statement untuk account yang diproses
|
||||||
|
* Menggunakan data yang sudah diproses dari ProcessedStatement
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
private function generatePdf(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
Log::info('ExportStatementPeriodJob: Memulai generate PDF', [
|
||||||
|
'account_number' => $this->account_number,
|
||||||
|
'period' => $this->period,
|
||||||
|
'statement_id' => $this->statementId
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Ambil data account dan customer
|
||||||
|
$account = Account::where('account_number', $this->account_number)->first();
|
||||||
|
if (!$account) {
|
||||||
|
throw new Exception("Account tidak ditemukan: {$this->account_number}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$customer = Customer::where('customer_code', $account->customer_code)->first();
|
||||||
|
if (!$customer) {
|
||||||
|
throw new Exception("Customer tidak ditemukan untuk account: {$this->account_number}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ambil data branch
|
||||||
|
$branch = Branch::where('code', $account->branch_code)->first();
|
||||||
|
if (!$branch) {
|
||||||
|
throw new Exception("Branch tidak ditemukan: {$account->branch_code}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ambil statement entries yang sudah diproses
|
||||||
|
$stmtEntries = ProcessedStatement::where('account_number', $this->account_number)
|
||||||
|
->where('period', $this->period)
|
||||||
|
->orderBy('sequence_no')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($stmtEntries->isEmpty()) {
|
||||||
|
throw new Exception("Tidak ada data statement yang diproses untuk account: {$this->account_number}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare header table background (convert to base64 if needed)
|
||||||
|
$headerImagePath = public_path('assets/media/images/bg-header-table.png');
|
||||||
|
$headerTableBg = file_exists($headerImagePath)
|
||||||
|
? base64_encode(file_get_contents($headerImagePath))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Hitung saldo awal bulan
|
||||||
|
$saldoAwalBulan = (object) ['actual_balance' => (float) $this->saldo];
|
||||||
|
|
||||||
|
// Generate filename
|
||||||
|
$filename = "{$this->account_number}_{$this->period}.pdf";
|
||||||
|
|
||||||
|
// Tentukan path storage
|
||||||
|
$storagePath = "statements/{$this->period}/{$account->branch_code}";
|
||||||
|
$tempPath = storage_path("app/temp/{$filename}");
|
||||||
|
$fullStoragePath = "{$storagePath}/{$filename}";
|
||||||
|
|
||||||
|
// Buat direktori temp jika belum ada
|
||||||
|
if (!is_dir(dirname($tempPath))) {
|
||||||
|
mkdir(dirname($tempPath), 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pastikan direktori storage ada
|
||||||
|
Storage::makeDirectory($storagePath);
|
||||||
|
|
||||||
|
$period = $this->period;
|
||||||
|
|
||||||
|
// Render HTML view
|
||||||
|
$html = view('webstatement::statements.stmt', compact(
|
||||||
|
'stmtEntries',
|
||||||
|
'account',
|
||||||
|
'customer',
|
||||||
|
'headerTableBg',
|
||||||
|
'branch',
|
||||||
|
'period',
|
||||||
|
'saldoAwalBulan'
|
||||||
|
))->render();
|
||||||
|
|
||||||
|
Log::info('ExportStatementPeriodJob: HTML view berhasil di-render', [
|
||||||
|
'account_number' => $this->account_number,
|
||||||
|
'html_length' => strlen($html)
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
// Di dalam fungsi generatePdf(), setelah Browsershot::html()->save($tempPath)
|
||||||
|
// Generate PDF menggunakan Browsershot
|
||||||
|
Browsershot::html($html)
|
||||||
|
->showBackground()
|
||||||
|
->setOption('addStyleTag', json_encode(['content' => '@page { margin: 0; }']))
|
||||||
|
->setOption('protocolTimeout', 2147483) // 2 menit timeout
|
||||||
|
->setOption('headless', true)
|
||||||
|
->noSandbox()
|
||||||
|
->format('A4')
|
||||||
|
->margins(0, 0, 0, 0)
|
||||||
|
->waitUntil('load')
|
||||||
|
->waitUntilNetworkIdle()
|
||||||
|
->timeout(2147483)
|
||||||
|
->save($tempPath);
|
||||||
|
|
||||||
|
// Verifikasi file berhasil dibuat
|
||||||
|
if (!file_exists($tempPath)) {
|
||||||
|
throw new Exception('PDF file gagal dibuat');
|
||||||
|
}
|
||||||
|
|
||||||
|
$printLog = PrintStatementLog::find($this->statementId);
|
||||||
|
|
||||||
|
// Apply password protection jika diperlukan
|
||||||
|
$password = $printLog->password ?? generatePassword($account); // Ambil dari config atau set default
|
||||||
|
if (!empty($password)) {
|
||||||
|
$tempProtectedPath = storage_path("app/temp/protected_{$filename}");
|
||||||
|
|
||||||
|
// Encrypt PDF dengan password
|
||||||
|
PDFPasswordProtect::encrypt($tempPath, $tempProtectedPath, $password);
|
||||||
|
|
||||||
|
// Ganti file original dengan yang sudah diproteksi
|
||||||
|
if (file_exists($tempProtectedPath)) {
|
||||||
|
unlink($tempPath); // Hapus file original
|
||||||
|
rename($tempProtectedPath, $tempPath); // Rename protected file ke original path
|
||||||
|
|
||||||
|
Log::info('ExportStatementPeriodJob: PDF password protection applied', [
|
||||||
|
'account_number' => $this->account_number,
|
||||||
|
'period' => $this->period
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileSize = filesize($tempPath);
|
||||||
|
|
||||||
|
// Pindahkan file ke storage permanen
|
||||||
|
$pdfContent = file_get_contents($tempPath);
|
||||||
|
Storage::put($fullStoragePath, $pdfContent);
|
||||||
|
|
||||||
|
// Update print statement log
|
||||||
|
|
||||||
|
if ($printLog) {
|
||||||
|
$printLog->update([
|
||||||
|
'is_available' => true,
|
||||||
|
'is_generated' => true,
|
||||||
|
'pdf_path' => $fullStoragePath,
|
||||||
|
'file_size' => $fileSize
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hapus file temporary
|
||||||
|
if (file_exists($tempPath)) {
|
||||||
|
unlink($tempPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('ExportStatementPeriodJob: PDF berhasil dibuat dan disimpan', [
|
||||||
|
'account_number' => $this->account_number,
|
||||||
|
'period' => $this->period,
|
||||||
|
'storage_path' => $fullStoragePath,
|
||||||
|
'file_size' => $fileSize,
|
||||||
|
'statement_id' => $this->statementId
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
|
||||||
|
Log::error('ExportStatementPeriodJob: Gagal generate PDF', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'account_number' => $this->account_number,
|
||||||
|
'period' => $this->period,
|
||||||
|
'statement_id' => $this->statementId,
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Update print statement log dengan status error
|
||||||
|
$printLog = PrintStatementLog::find($this->statementId);
|
||||||
|
if ($printLog) {
|
||||||
|
$printLog->update([
|
||||||
|
'is_available' => false,
|
||||||
|
'error_message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception('Gagal generate PDF: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export processed data to CSV file
|
||||||
|
*/
|
||||||
|
private function exportToCsv(): void
|
||||||
|
{
|
||||||
|
// Determine the base path based on client
|
||||||
|
$account = Account::where('account_number', $this->account_number)->first();
|
||||||
|
|
||||||
|
$storagePath = "statements/{$this->period}/{$account->branch_code}";
|
||||||
|
Storage::disk($this->disk)->makeDirectory($storagePath);
|
||||||
|
|
||||||
|
$filePath = "{$storagePath}/{$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);
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
$this->updateCsvLogStart();
|
||||||
|
|
||||||
// Generate CSV file
|
// Generate CSV file
|
||||||
$result = $this->generateAtmCardCsv();
|
// $result = $this->generateAtmCardCsv();
|
||||||
|
|
||||||
|
$result = $this->generateSingleAtmCardCsv();
|
||||||
|
|
||||||
// Update status CSV generation berhasil
|
// Update status CSV generation berhasil
|
||||||
$this->updateCsvLogSuccess($result);
|
$this->updateCsvLogSuccess($result);
|
||||||
@@ -159,23 +161,53 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get eligible ATM cards from database
|
* Get eligible ATM cards from database
|
||||||
|
* Mengambil data kartu ATM yang memenuhi syarat untuk dikenakan biaya admin
|
||||||
|
* dengan filter khusus untuk mengecualikan product_code 6021 yang ctdesc nya gold
|
||||||
*
|
*
|
||||||
* @return \Illuminate\Database\Eloquent\Collection
|
* @return \Illuminate\Database\Eloquent\Collection
|
||||||
*/
|
*/
|
||||||
private function getEligibleAtmCards()
|
private function getEligibleAtmCards()
|
||||||
{
|
{
|
||||||
|
// Log: Memulai proses pengambilan data kartu ATM yang eligible
|
||||||
|
Log::info('Starting to fetch eligible ATM cards', [
|
||||||
|
'periode' => $this->periode
|
||||||
|
]);
|
||||||
|
|
||||||
$cardTypes = array_keys($this->getDefaultFees());
|
$cardTypes = array_keys($this->getDefaultFees());
|
||||||
|
|
||||||
return Atmcard::where('crsts', 1)
|
$query = Atmcard::where('crsts', 1)
|
||||||
->whereNotNull('accflag')
|
->whereNotNull('accflag')
|
||||||
->where('accflag', '!=', '')
|
->where('accflag', '!=', '')
|
||||||
->where('flag','')
|
->where('flag','')
|
||||||
->whereNotNull('branch')
|
->whereNotNull('branch')
|
||||||
->where('branch', '!=', '')
|
->where('branch', '!=', '')
|
||||||
->whereNotNull('currency')
|
->whereNotNull('currency')
|
||||||
->where('currency', '!=', '')
|
->where('currency', '!=', '')
|
||||||
->whereIn('ctdesc', $cardTypes)
|
->whereIn('ctdesc', $cardTypes)
|
||||||
->get();
|
->whereNotIn('product_code',['6002','6004','6042','6031']) // Hapus 6021 dari sini
|
||||||
|
->where('branch','!=','ID0019999')
|
||||||
|
// Filter khusus: Kecualikan product_code 6021 yang ctdesc nya gold
|
||||||
|
->where(function($subQuery) {
|
||||||
|
$subQuery->where('product_code', '!=', '6021')
|
||||||
|
->orWhere(function($nestedQuery) {
|
||||||
|
$nestedQuery->where('product_code', '6021')
|
||||||
|
->where('ctdesc', '!=', 'GOLD');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$cards = $query->get();
|
||||||
|
|
||||||
|
// Log: Hasil pengambilan data kartu ATM
|
||||||
|
Log::info('Eligible ATM cards fetched successfully', [
|
||||||
|
'total_cards' => $cards->count(),
|
||||||
|
'periode' => $this->periode,
|
||||||
|
'excluded_product_codes' => ['6002','6004','6042','6031'],
|
||||||
|
'special_filter' => 'product_code 6021 dengan ctdesc gold dikecualikan'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $cards;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -413,4 +445,155 @@
|
|||||||
|
|
||||||
Log::error('Pembuatan file CSV gagal: ' . $errorMessage);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
606
app/Jobs/GenerateClosingBalanceReportJob.php
Normal file
606
app/Jobs/GenerateClosingBalanceReportJob.php
Normal file
@@ -0,0 +1,606 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Jobs;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Exception;
|
||||||
|
use Modules\Webstatement\Models\AccountBalance;
|
||||||
|
use Modules\Webstatement\Models\ClosingBalanceReportLog;
|
||||||
|
use Modules\Webstatement\Models\StmtEntry;
|
||||||
|
use Modules\Webstatement\Models\StmtEntryDetail;
|
||||||
|
use Modules\Webstatement\Models\TempFundsTransfer;
|
||||||
|
use Modules\Webstatement\Models\DataCapture;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job untuk generate laporan closing balance
|
||||||
|
* Mengambil data transaksi dan menghitung closing balance
|
||||||
|
*/
|
||||||
|
class GenerateClosingBalanceReportJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
protected $accountNumber;
|
||||||
|
protected $period;
|
||||||
|
protected $reportLogId;
|
||||||
|
protected $groupName;
|
||||||
|
protected $chunkSize = 1000;
|
||||||
|
protected $disk = 'local';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*
|
||||||
|
* @param string $accountNumber
|
||||||
|
* @param string $period
|
||||||
|
* @param int $reportLogId
|
||||||
|
*/
|
||||||
|
public function __construct(string $accountNumber, string $period, int $reportLogId, string $groupName='DEFAULT')
|
||||||
|
{
|
||||||
|
$this->accountNumber = $accountNumber;
|
||||||
|
$this->period = $period;
|
||||||
|
$this->reportLogId = $reportLogId;
|
||||||
|
$this->groupName = $groupName ?? 'DEFAULT';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
* Memproses data transaksi dan generate laporan closing balance
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$reportLog = ClosingBalanceReportLog::find($this->reportLogId);
|
||||||
|
|
||||||
|
if (!$reportLog) {
|
||||||
|
Log::error('Closing balance report log not found', ['id' => $this->reportLogId]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Log::info('Starting closing balance report generation', [
|
||||||
|
'account_number' => $this->accountNumber,
|
||||||
|
'period' => $this->period,
|
||||||
|
'report_log_id' => $this->reportLogId
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
// Update status to processing
|
||||||
|
$reportLog->update([
|
||||||
|
'status' => 'processing',
|
||||||
|
'updated_at' => now()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get opening balance
|
||||||
|
$openingBalance = $this->getOpeningBalance();
|
||||||
|
|
||||||
|
// Generate report data
|
||||||
|
$reportData = $this->generateReportData($openingBalance);
|
||||||
|
|
||||||
|
// Export to CSV
|
||||||
|
$filePath = $this->exportToCsv($reportData);
|
||||||
|
|
||||||
|
// Update report log with success
|
||||||
|
$reportLog->update([
|
||||||
|
'status' => 'completed',
|
||||||
|
'file_path' => $filePath,
|
||||||
|
'file_size' => Storage::disk($this->disk)->size($filePath),
|
||||||
|
'record_count' => count($reportData),
|
||||||
|
'updated_at' => now()
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
Log::info('Closing balance report generation completed successfully', [
|
||||||
|
'account_number' => $this->accountNumber,
|
||||||
|
'period' => $this->period,
|
||||||
|
'file_path' => $filePath,
|
||||||
|
'record_count' => count($reportData)
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
DB::rollback();
|
||||||
|
|
||||||
|
Log::error('Error generating closing balance report', [
|
||||||
|
'account_number' => $this->accountNumber,
|
||||||
|
'period' => $this->period,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
$reportLog->update([
|
||||||
|
'status' => 'failed',
|
||||||
|
'error_message' => $e->getMessage(),
|
||||||
|
'updated_at' => now()
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get opening balance from account balance table
|
||||||
|
* Mengambil saldo awal dari tabel account balance
|
||||||
|
*/
|
||||||
|
private function getOpeningBalance(): float
|
||||||
|
{
|
||||||
|
Log::info('Getting opening balance', [
|
||||||
|
'account_number' => $this->accountNumber,
|
||||||
|
'period' => $this->period
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get previous period based on current period
|
||||||
|
$previousPeriod = $this->period === '20250512'
|
||||||
|
? Carbon::createFromFormat('Ymd', $this->period)->subDays(2)->format('Ymd')
|
||||||
|
: Carbon::createFromFormat('Ymd', $this->period)->subDay()->format('Ymd');
|
||||||
|
|
||||||
|
$accountBalance = AccountBalance::where('account_number', $this->accountNumber)
|
||||||
|
->where('period', $previousPeriod)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$accountBalance) {
|
||||||
|
Log::warning('Account balance not found, using 0 as opening balance', [
|
||||||
|
'account_number' => $this->accountNumber,
|
||||||
|
'period' => $this->period
|
||||||
|
]);
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$openingBalance = (float) $accountBalance->actual_balance;
|
||||||
|
|
||||||
|
Log::info('Opening balance retrieved', [
|
||||||
|
'account_number' => $this->accountNumber,
|
||||||
|
'opening_balance' => $openingBalance
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $openingBalance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build transaction query using pure Eloquent relationships
|
||||||
|
* Membangun query transaksi menggunakan relasi Eloquent murni
|
||||||
|
*/
|
||||||
|
private function buildTransactionQuery()
|
||||||
|
{
|
||||||
|
Log::info('Building transaction query using pure Eloquent relationships', [
|
||||||
|
'group_name' => $this->groupName,
|
||||||
|
'account_number' => $this->accountNumber,
|
||||||
|
'period' => $this->period
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Tentukan model berdasarkan group name
|
||||||
|
$modelClass = $this->getModelByGroup();
|
||||||
|
|
||||||
|
// Build query menggunakan pure Eloquent dengan eager loading
|
||||||
|
$query = $modelClass::with([
|
||||||
|
'ft' => function($query) {
|
||||||
|
$query->select([
|
||||||
|
'_id',
|
||||||
|
'ref_no',
|
||||||
|
'debit_acct_no',
|
||||||
|
'debit_value_date',
|
||||||
|
'credit_acct_no',
|
||||||
|
'bif_rcv_acct',
|
||||||
|
'bif_rcv_name',
|
||||||
|
'credit_value_date',
|
||||||
|
'at_unique_id',
|
||||||
|
'bif_ref_no',
|
||||||
|
'atm_order_id',
|
||||||
|
'recipt_no',
|
||||||
|
'api_iss_acct',
|
||||||
|
'api_benff_acct',
|
||||||
|
'authoriser',
|
||||||
|
'remarks',
|
||||||
|
'payment_details',
|
||||||
|
'merchant_id',
|
||||||
|
'term_id',
|
||||||
|
'date_time'
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
'dc' => function($query) {
|
||||||
|
$query->select([
|
||||||
|
'id',
|
||||||
|
'date_time'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
])
|
||||||
|
->select([
|
||||||
|
'id',
|
||||||
|
'trans_reference',
|
||||||
|
'booking_date',
|
||||||
|
'amount_lcy',
|
||||||
|
'date_time'
|
||||||
|
])
|
||||||
|
->where('account_number', $this->accountNumber)
|
||||||
|
->where('booking_date', $this->period)
|
||||||
|
->orderBy('booking_date')
|
||||||
|
->orderBy('date_time');
|
||||||
|
|
||||||
|
Log::info('Transaction query built successfully using pure Eloquent', [
|
||||||
|
'model_class' => $modelClass,
|
||||||
|
'account_number' => $this->accountNumber,
|
||||||
|
'period' => $this->period
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get model class based on group name
|
||||||
|
* Mendapatkan class model berdasarkan group name
|
||||||
|
*/
|
||||||
|
private function getModelByGroup()
|
||||||
|
{
|
||||||
|
Log::info('Determining model by group', [
|
||||||
|
'group_name' => $this->groupName
|
||||||
|
]);
|
||||||
|
|
||||||
|
$model = $this->groupName === 'QRIS' ? StmtEntryDetail::class : StmtEntry::class;
|
||||||
|
|
||||||
|
Log::info('Model determined', [
|
||||||
|
'group_name' => $this->groupName,
|
||||||
|
'model_class' => $model
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process transaction data from ORM result
|
||||||
|
* Memproses data transaksi dari hasil ORM
|
||||||
|
*/
|
||||||
|
private function processTransactionData($transaction): array
|
||||||
|
{
|
||||||
|
Log::info('Processing transaction data', [
|
||||||
|
'trans_reference' => $transaction->trans_reference,
|
||||||
|
'has_ft_relation' => !is_null($transaction->ft),
|
||||||
|
'has_dc_relation' => !is_null($transaction->dc)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Hitung debit dan credit amount
|
||||||
|
$debitAmount = $transaction->amount_lcy < 0 ? abs($transaction->amount_lcy) : null;
|
||||||
|
$creditAmount = $transaction->amount_lcy > 0 ? $transaction->amount_lcy : null;
|
||||||
|
|
||||||
|
// Ambil date_time dari prioritas: ft -> dc -> stmt
|
||||||
|
$dateTime = $transaction->ft?->date_time ??
|
||||||
|
$transaction->dc?->date_time ??
|
||||||
|
$transaction->date_time;
|
||||||
|
|
||||||
|
$processedData = [
|
||||||
|
'trans_reference' => $transaction->trans_reference,
|
||||||
|
'booking_date' => $transaction->booking_date,
|
||||||
|
'amount_lcy' => $transaction->amount_lcy,
|
||||||
|
'debit_amount' => $debitAmount,
|
||||||
|
'credit_amount' => $creditAmount,
|
||||||
|
'date_time' => $dateTime,
|
||||||
|
// Data dari TempFundsTransfer melalui relasi
|
||||||
|
'debit_acct_no' => $transaction->ft?->debit_acct_no,
|
||||||
|
'debit_value_date' => $transaction->ft?->debit_value_date,
|
||||||
|
'credit_acct_no' => $transaction->ft?->credit_acct_no,
|
||||||
|
'bif_rcv_acct' => $transaction->ft?->bif_rcv_acct,
|
||||||
|
'bif_rcv_name' => $transaction->ft?->bif_rcv_name,
|
||||||
|
'credit_value_date' => $transaction->ft?->credit_value_date,
|
||||||
|
'at_unique_id' => $transaction->ft?->at_unique_id,
|
||||||
|
'bif_ref_no' => $transaction->ft?->bif_ref_no,
|
||||||
|
'atm_order_id' => $transaction->ft?->atm_order_id,
|
||||||
|
'recipt_no' => $transaction->ft?->recipt_no,
|
||||||
|
'api_iss_acct' => $transaction->ft?->api_iss_acct,
|
||||||
|
'api_benff_acct' => $transaction->ft?->api_benff_acct,
|
||||||
|
'authoriser' => $transaction->ft?->authoriser,
|
||||||
|
'remarks' => $transaction->ft?->remarks,
|
||||||
|
'payment_details' => $transaction->ft?->payment_details,
|
||||||
|
'ref_no' => $transaction->ft?->ref_no,
|
||||||
|
'merchant_id' => $transaction->ft?->merchant_id,
|
||||||
|
'term_id' => $transaction->ft?->term_id,
|
||||||
|
];
|
||||||
|
|
||||||
|
Log::info('Transaction data processed successfully', [
|
||||||
|
'trans_reference' => $transaction->trans_reference,
|
||||||
|
'final_date_time' => $dateTime,
|
||||||
|
'debit_amount' => $debitAmount,
|
||||||
|
'credit_amount' => $creditAmount
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $processedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updated generateReportData method using pure ORM
|
||||||
|
* Method generateReportData yang diperbarui menggunakan ORM murni
|
||||||
|
*/
|
||||||
|
private function generateReportData(): array
|
||||||
|
{
|
||||||
|
Log::info('Starting report data generation using pure ORM', [
|
||||||
|
'account_number' => $this->accountNumber,
|
||||||
|
'period' => $this->period,
|
||||||
|
'group_name' => $this->groupName,
|
||||||
|
'chunk_size' => $this->chunkSize
|
||||||
|
]);
|
||||||
|
|
||||||
|
$reportData = [];
|
||||||
|
$runningBalance = $this->getOpeningBalance();
|
||||||
|
$sequenceNo = 1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
// Build query menggunakan pure ORM
|
||||||
|
$query = $this->buildTransactionQuery();
|
||||||
|
|
||||||
|
// Process data dalam chunks untuk efisiensi memory
|
||||||
|
$query->chunk($this->chunkSize, function ($transactions) use (&$reportData, &$runningBalance, &$sequenceNo) {
|
||||||
|
Log::info('Processing transaction chunk', [
|
||||||
|
'chunk_size' => $transactions->count(),
|
||||||
|
'current_sequence' => $sequenceNo,
|
||||||
|
'current_balance' => $runningBalance
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($transactions as $transaction) {
|
||||||
|
// Process transaction data
|
||||||
|
$processedData = $this->processTransactionData($transaction);
|
||||||
|
|
||||||
|
// Update running balance
|
||||||
|
$amount = (float) $transaction->amount_lcy;
|
||||||
|
$runningBalance += $amount;
|
||||||
|
|
||||||
|
// Format transaction date
|
||||||
|
$transactionDate = $this->formatDateTime($processedData['date_time']);
|
||||||
|
|
||||||
|
// Build report data row
|
||||||
|
$reportData[] = $this->buildReportDataRow(
|
||||||
|
(object) $processedData,
|
||||||
|
$sequenceNo,
|
||||||
|
$transactionDate,
|
||||||
|
$runningBalance
|
||||||
|
);
|
||||||
|
|
||||||
|
$sequenceNo++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Chunk processed successfully', [
|
||||||
|
'processed_count' => $transactions->count(),
|
||||||
|
'total_records_so_far' => count($reportData),
|
||||||
|
'current_balance' => $runningBalance
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
Log::info('Report data generation completed using pure ORM', [
|
||||||
|
'total_records' => count($reportData),
|
||||||
|
'final_balance' => $runningBalance,
|
||||||
|
'final_sequence' => $sequenceNo - 1
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
DB::rollback();
|
||||||
|
|
||||||
|
Log::error('Error generating report data using pure ORM', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
'account_number' => $this->accountNumber,
|
||||||
|
'period' => $this->period
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $reportData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get table name based on group name
|
||||||
|
* Mendapatkan nama tabel berdasarkan group name
|
||||||
|
*/
|
||||||
|
private function getTableNameByGroup(): string
|
||||||
|
{
|
||||||
|
return $this->groupName === 'QRIS' ? 'stmt_entry' : 'stmt_entry_details';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get select fields for the query
|
||||||
|
* Mendapatkan field select untuk query
|
||||||
|
*/
|
||||||
|
private function getSelectFields(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
's.trans_reference',
|
||||||
|
's.booking_date',
|
||||||
|
's.amount_lcy',
|
||||||
|
'ft.debit_acct_no',
|
||||||
|
'ft.debit_value_date',
|
||||||
|
DB::raw('CASE WHEN s.amount_lcy::numeric < 0 THEN s.amount_lcy::numeric ELSE NULL END AS debit_amount'),
|
||||||
|
'ft.credit_acct_no',
|
||||||
|
'ft.bif_rcv_acct',
|
||||||
|
'ft.bif_rcv_name',
|
||||||
|
'ft.credit_value_date',
|
||||||
|
DB::raw('CASE WHEN s.amount_lcy::numeric > 0 THEN s.amount_lcy::numeric ELSE NULL END AS credit_amount'),
|
||||||
|
'ft.at_unique_id',
|
||||||
|
'ft.bif_ref_no',
|
||||||
|
'ft.atm_order_id',
|
||||||
|
'ft.recipt_no',
|
||||||
|
'ft.api_iss_acct',
|
||||||
|
'ft.api_benff_acct',
|
||||||
|
DB::raw('COALESCE(ft.date_time, dc.date_time, s.date_time) AS date_time'),
|
||||||
|
'ft.authoriser',
|
||||||
|
'ft.remarks',
|
||||||
|
'ft.payment_details',
|
||||||
|
'ft.ref_no',
|
||||||
|
'ft.merchant_id',
|
||||||
|
'ft.term_id'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build report data row from transaction
|
||||||
|
* Membangun baris data laporan dari transaksi
|
||||||
|
*/
|
||||||
|
private function buildReportDataRow($transaction, int $sequenceNo, string $transactionDate, float $runningBalance): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'sequence_no' => $sequenceNo,
|
||||||
|
'trans_reference' => $transaction->trans_reference,
|
||||||
|
'booking_date' => $transaction->booking_date,
|
||||||
|
'transaction_date' => $transactionDate,
|
||||||
|
'amount_lcy' => $transaction->amount_lcy,
|
||||||
|
'debit_acct_no' => $transaction->debit_acct_no,
|
||||||
|
'debit_value_date' => $transaction->debit_value_date,
|
||||||
|
'debit_amount' => $transaction->debit_amount,
|
||||||
|
'credit_acct_no' => $transaction->credit_acct_no,
|
||||||
|
'bif_rcv_acct' => $transaction->bif_rcv_acct,
|
||||||
|
'bif_rcv_name' => $transaction->bif_rcv_name,
|
||||||
|
'credit_value_date' => $transaction->credit_value_date,
|
||||||
|
'credit_amount' => $transaction->credit_amount,
|
||||||
|
'at_unique_id' => $transaction->at_unique_id,
|
||||||
|
'bif_ref_no' => $transaction->bif_ref_no,
|
||||||
|
'atm_order_id' => $transaction->atm_order_id,
|
||||||
|
'recipt_no' => $transaction->recipt_no,
|
||||||
|
'api_iss_acct' => $transaction->api_iss_acct,
|
||||||
|
'api_benff_acct' => $transaction->api_benff_acct,
|
||||||
|
'authoriser' => $transaction->authoriser,
|
||||||
|
'remarks' => $transaction->remarks,
|
||||||
|
'payment_details' => $transaction->payment_details,
|
||||||
|
'ref_no' => $transaction->ref_no,
|
||||||
|
'merchant_id' => $transaction->merchant_id,
|
||||||
|
'term_id' => $transaction->term_id,
|
||||||
|
'closing_balance' => $runningBalance
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format datetime string
|
||||||
|
* Memformat string datetime
|
||||||
|
*/
|
||||||
|
private function formatDateTime(?string $datetime): string
|
||||||
|
{
|
||||||
|
if (!$datetime) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Carbon::createFromFormat('ymdHi', $datetime)->format('d/m/Y H:i');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::warning('Error formatting datetime', [
|
||||||
|
'datetime' => $datetime,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
return $datetime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export report data to CSV file
|
||||||
|
* Export data laporan ke file CSV
|
||||||
|
*/
|
||||||
|
private function exportToCsv(array $reportData): string
|
||||||
|
{
|
||||||
|
Log::info('Starting CSV export for closing balance report', [
|
||||||
|
'account_number' => $this->accountNumber,
|
||||||
|
'period' => $this->period,
|
||||||
|
'record_count' => count($reportData)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create directory structure
|
||||||
|
$basePath = "closing_balance_reports";
|
||||||
|
$accountPath = "{$basePath}/{$this->accountNumber}";
|
||||||
|
|
||||||
|
Storage::disk($this->disk)->makeDirectory($basePath);
|
||||||
|
Storage::disk($this->disk)->makeDirectory($accountPath);
|
||||||
|
|
||||||
|
// Generate filename
|
||||||
|
$fileName = "closing_balance_{$this->accountNumber}_{$this->period}.csv";
|
||||||
|
$filePath = "{$accountPath}/{$fileName}";
|
||||||
|
|
||||||
|
// Delete existing file if exists
|
||||||
|
if (Storage::disk($this->disk)->exists($filePath)) {
|
||||||
|
Storage::disk($this->disk)->delete($filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create CSV header
|
||||||
|
$csvHeader = [
|
||||||
|
'NO',
|
||||||
|
'TRANS_REFERENCE',
|
||||||
|
'BOOKING_DATE',
|
||||||
|
'TRANSACTION_DATE',
|
||||||
|
'AMOUNT_LCY',
|
||||||
|
'DEBIT_ACCT_NO',
|
||||||
|
'DEBIT_VALUE_DATE',
|
||||||
|
'DEBIT_AMOUNT',
|
||||||
|
'CREDIT_ACCT_NO',
|
||||||
|
'BIF_RCV_ACCT',
|
||||||
|
'BIF_RCV_NAME',
|
||||||
|
'CREDIT_VALUE_DATE',
|
||||||
|
'CREDIT_AMOUNT',
|
||||||
|
'AT_UNIQUE_ID',
|
||||||
|
'BIF_REF_NO',
|
||||||
|
'ATM_ORDER_ID',
|
||||||
|
'RECIPT_NO',
|
||||||
|
'API_ISS_ACCT',
|
||||||
|
'API_BENFF_ACCT',
|
||||||
|
'AUTHORISER',
|
||||||
|
'REMARKS',
|
||||||
|
'PAYMENT_DETAILS',
|
||||||
|
'REF_NO',
|
||||||
|
'MERCHANT_ID',
|
||||||
|
'TERM_ID',
|
||||||
|
'CLOSING_BALANCE'
|
||||||
|
];
|
||||||
|
|
||||||
|
$csvContent = implode('|', $csvHeader) . "\n";
|
||||||
|
|
||||||
|
// Add data rows
|
||||||
|
foreach ($reportData as $row) {
|
||||||
|
$csvRow = [
|
||||||
|
$row['sequence_no'],
|
||||||
|
$row['trans_reference'] ?? '',
|
||||||
|
$row['booking_date'] ?? '',
|
||||||
|
$row['transaction_date'] ?? '',
|
||||||
|
$row['amount_lcy'] ?? '',
|
||||||
|
$row['debit_acct_no'] ?? '',
|
||||||
|
$row['debit_value_date'] ?? '',
|
||||||
|
$row['debit_amount'] ?? '',
|
||||||
|
$row['credit_acct_no'] ?? '',
|
||||||
|
$row['bif_rcv_acct'] ?? '',
|
||||||
|
$row['bif_rcv_name'] ?? '',
|
||||||
|
$row['credit_value_date'] ?? '',
|
||||||
|
$row['credit_amount'] ?? '',
|
||||||
|
$row['at_unique_id'] ?? '',
|
||||||
|
$row['bif_ref_no'] ?? '',
|
||||||
|
$row['atm_order_id'] ?? '',
|
||||||
|
$row['recipt_no'] ?? '',
|
||||||
|
$row['api_iss_acct'] ?? '',
|
||||||
|
$row['api_benff_acct'] ?? '',
|
||||||
|
$row['authoriser'] ?? '',
|
||||||
|
$row['remarks'] ?? '',
|
||||||
|
$row['payment_details'] ?? '',
|
||||||
|
$row['ref_no'] ?? '',
|
||||||
|
$row['merchant_id'] ?? '',
|
||||||
|
$row['term_id'] ?? '',
|
||||||
|
$row['closing_balance'] ?? ''
|
||||||
|
];
|
||||||
|
|
||||||
|
$csvContent .= implode('|', $csvRow) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save file
|
||||||
|
Storage::disk($this->disk)->put($filePath, $csvContent);
|
||||||
|
|
||||||
|
// Verify file creation
|
||||||
|
if (!Storage::disk($this->disk)->exists($filePath)) {
|
||||||
|
throw new Exception("Failed to create CSV file: {$filePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('CSV export completed successfully', [
|
||||||
|
'file_path' => $filePath,
|
||||||
|
'file_size' => Storage::disk($this->disk)->size($filePath)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $filePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
759
app/Jobs/GenerateMultiAccountPdfJob.php
Normal file
759
app/Jobs/GenerateMultiAccountPdfJob.php
Normal file
@@ -0,0 +1,759 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Jobs;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use ZipArchive;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Support\Facades\{
|
||||||
|
DB,
|
||||||
|
Log,
|
||||||
|
Storage
|
||||||
|
};
|
||||||
|
use Spatie\Browsershot\Browsershot;
|
||||||
|
use Modules\Basicdata\Models\Branch;
|
||||||
|
use Illuminate\Queue\{
|
||||||
|
SerializesModels,
|
||||||
|
InteractsWithQueue
|
||||||
|
};
|
||||||
|
use Modules\Webstatement\Models\{
|
||||||
|
StmtEntry,
|
||||||
|
AccountBalance,
|
||||||
|
PrintStatementLog,
|
||||||
|
ProcessedStatement,
|
||||||
|
TempStmtNarrParam,
|
||||||
|
TempStmtNarrFormat
|
||||||
|
};
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
|
||||||
|
class GenerateMultiAccountPdfJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
protected $statement;
|
||||||
|
protected $accounts;
|
||||||
|
protected $period;
|
||||||
|
protected $clientName;
|
||||||
|
protected $chunkSize = 10; // Process 10 accounts at a time
|
||||||
|
protected $startDate;
|
||||||
|
protected $endDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*
|
||||||
|
* @param PrintStatementLog $statement
|
||||||
|
* @param \Illuminate\Database\Eloquent\Collection $accounts
|
||||||
|
* @param string $period
|
||||||
|
* @param string $clientName
|
||||||
|
*/
|
||||||
|
public function __construct($statement, $accounts, $period, $clientName)
|
||||||
|
{
|
||||||
|
$this->statement = $statement;
|
||||||
|
$this->accounts = $accounts;
|
||||||
|
$this->period = $period;
|
||||||
|
$this->clientName = $clientName;
|
||||||
|
|
||||||
|
// Calculate period dates using same logic as ExportStatementPeriodJob
|
||||||
|
$this->calculatePeriodDates();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate start and end dates for the given period
|
||||||
|
* Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||||
|
*/
|
||||||
|
private function calculatePeriodDates(): void
|
||||||
|
{
|
||||||
|
$year = substr($this->period, 0, 4);
|
||||||
|
$month = substr($this->period, 4, 2);
|
||||||
|
|
||||||
|
// Special case for May 2025 - start from 9th
|
||||||
|
if ($this->period === '202505') {
|
||||||
|
$this->startDate = Carbon::createFromDate($year, $month, 9)->startOfDay();
|
||||||
|
} else {
|
||||||
|
// For all other periods, start from 1st of the month
|
||||||
|
$this->startDate = Carbon::createFromDate($year, $month, 1)->startOfDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
// End date is always the last day of the month
|
||||||
|
$this->endDate = Carbon::createFromDate($year, $month, 1)->endOfMonth()->endOfDay();
|
||||||
|
|
||||||
|
Log::info('Period dates calculated for PDF generation', [
|
||||||
|
'period' => $this->period,
|
||||||
|
'start_date' => $this->startDate->format('Y-m-d'),
|
||||||
|
'end_date' => $this->endDate->format('Y-m-d')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
|
||||||
|
Log::info('Starting multi account PDF generation', [
|
||||||
|
'statement_id' => $this->statement->id,
|
||||||
|
'total_accounts' => $this->accounts->count(),
|
||||||
|
'period' => $this->period,
|
||||||
|
'date_range' => $this->startDate->format('Y-m-d') . ' to ' . $this->endDate->format('Y-m-d')
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pdfFiles = [];
|
||||||
|
$successCount = 0;
|
||||||
|
$failedCount = 0;
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
// Process each account
|
||||||
|
foreach ($this->accounts as $account) {
|
||||||
|
try {
|
||||||
|
$pdfPath = $this->generateAccountPdf($account);
|
||||||
|
if ($pdfPath) {
|
||||||
|
$pdfFiles[] = $pdfPath;
|
||||||
|
$successCount++;
|
||||||
|
|
||||||
|
Log::info('PDF generated successfully for account', [
|
||||||
|
'account_number' => $account->account_number,
|
||||||
|
'pdf_path' => $pdfPath
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory cleanup after each account
|
||||||
|
gc_collect_cycles();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$failedCount++;
|
||||||
|
$errors[] = [
|
||||||
|
'account_number' => $account->account_number,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
];
|
||||||
|
|
||||||
|
Log::error('Failed to generate PDF for account', [
|
||||||
|
'account_number' => $account->account_number,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ZIP file if there are PDFs
|
||||||
|
$zipPath = null;
|
||||||
|
if (!empty($pdfFiles)) {
|
||||||
|
$zipPath = $this->createZipFile($pdfFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update statement log
|
||||||
|
$this->statement->update([
|
||||||
|
'processed_accounts' => $this->accounts->count(),
|
||||||
|
'success_count' => $successCount,
|
||||||
|
'failed_count' => $failedCount,
|
||||||
|
'status' => $failedCount > 0 ? 'completed_with_errors' : 'completed',
|
||||||
|
'completed_at' => now(),
|
||||||
|
'is_available' => $zipPath ? true : false,
|
||||||
|
'is_generated' => $zipPath ? true : false,
|
||||||
|
'error_message' => !empty($errors) ? json_encode($errors) : null
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
Log::info('Multi account PDF generation completed', [
|
||||||
|
'statement_id' => $this->statement->id,
|
||||||
|
'success_count' => $successCount,
|
||||||
|
'failed_count' => $failedCount,
|
||||||
|
'zip_path' => $zipPath
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
|
||||||
|
Log::error('Multi account PDF generation failed', [
|
||||||
|
'statement_id' => $this->statement->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Update statement with error status
|
||||||
|
$this->statement->update([
|
||||||
|
'status' => 'failed',
|
||||||
|
'completed_at' => now(),
|
||||||
|
'error_message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate PDF untuk satu account
|
||||||
|
* Menggunakan data dari ProcessedStatement yang sudah diproses oleh ExportStatementPeriodJob
|
||||||
|
*
|
||||||
|
* @param Account $account
|
||||||
|
* @return string|null Path to generated PDF
|
||||||
|
*/
|
||||||
|
protected function generateAccountPdf($account)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Prepare account query untuk processing
|
||||||
|
$accountQuery = [
|
||||||
|
'account_number' => $account->account_number,
|
||||||
|
'period' => $this->period
|
||||||
|
];
|
||||||
|
|
||||||
|
// Get total entry count
|
||||||
|
$totalCount = $this->getTotalEntryCount($account->account_number);
|
||||||
|
|
||||||
|
// Delete existing processed data dan process ulang
|
||||||
|
$this->deleteExistingProcessedData($accountQuery);
|
||||||
|
$this->processAndSaveStatementEntries($account, $totalCount);
|
||||||
|
|
||||||
|
// Get statement entries from ProcessedStatement (data yang sudah diproses)
|
||||||
|
$stmtEntries = $this->getProcessedStatementEntries($account->account_number);
|
||||||
|
|
||||||
|
// Get saldo awal bulan menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||||
|
$saldoAwalBulan = $this->getSaldoAwalBulan($account->account_number);
|
||||||
|
|
||||||
|
// Get branch info
|
||||||
|
$branch = Branch::where('code', $account->branch_code)->first();
|
||||||
|
|
||||||
|
// Prepare images for PDF
|
||||||
|
$images = $this->prepareImagesForPdf();
|
||||||
|
|
||||||
|
$headerImagePath = public_path('assets/media/images/bg-header-table.png');
|
||||||
|
$headerTableBg = file_exists($headerImagePath)
|
||||||
|
? base64_encode(file_get_contents($headerImagePath))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Render HTML
|
||||||
|
$html = view('webstatement::statements.stmt', [
|
||||||
|
'stmtEntries' => $stmtEntries,
|
||||||
|
'account' => $account,
|
||||||
|
'customer' => $account->customer,
|
||||||
|
'images' => $images,
|
||||||
|
'branch' => $branch,
|
||||||
|
'period' => $this->period,
|
||||||
|
'saldoAwalBulan' => $saldoAwalBulan,
|
||||||
|
'headerTableBg' => $headerTableBg,
|
||||||
|
])->render();
|
||||||
|
|
||||||
|
// Generate PDF filename
|
||||||
|
$filename = "statement_{$account->account_number}_{$this->period}_" . now()->format('YmdHis') . '.pdf';
|
||||||
|
$storagePath = "statements/{$this->period}/multi_account/{$this->statement->id}";
|
||||||
|
$fullStoragePath = "{$storagePath}/{$filename}";
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
Storage::disk('local')->makeDirectory($storagePath);
|
||||||
|
|
||||||
|
// Generate PDF path
|
||||||
|
$pdfPath = storage_path("app/{$fullStoragePath}");
|
||||||
|
|
||||||
|
// Generate PDF using Browsershot
|
||||||
|
Browsershot::html($html)
|
||||||
|
->showBackground()
|
||||||
|
->setOption('addStyleTag', json_encode(['content' => '@page { margin: 0; }']))
|
||||||
|
->setOption('protocolTimeout', 2147483) // 2 menit timeout
|
||||||
|
->setOption('headless', true)
|
||||||
|
->noSandbox()
|
||||||
|
->format('A4')
|
||||||
|
->margins(0, 0, 0, 0)
|
||||||
|
->waitUntil('load')
|
||||||
|
->waitUntilNetworkIdle()
|
||||||
|
->timeout(2147483)
|
||||||
|
->save($pdfPath);
|
||||||
|
|
||||||
|
// Verify file was created
|
||||||
|
if (!file_exists($pdfPath)) {
|
||||||
|
throw new Exception('PDF file was not created');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear variables to free memory
|
||||||
|
unset($html, $stmtEntries, $images);
|
||||||
|
|
||||||
|
return $pdfPath;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('Failed to generate PDF for account', [
|
||||||
|
'account_number' => $account->account_number,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total entry count untuk account
|
||||||
|
* Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||||
|
*
|
||||||
|
* @param string $accountNumber
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
protected function getTotalEntryCount($accountNumber): int
|
||||||
|
{
|
||||||
|
$query = StmtEntry::where('account_number', $accountNumber)
|
||||||
|
->whereBetween('booking_date', [
|
||||||
|
$this->startDate->format('Ymd'),
|
||||||
|
$this->endDate->format('Ymd')
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::info("Getting total entry count for PDF generation", [
|
||||||
|
'account' => $accountNumber,
|
||||||
|
'start_date' => $this->startDate->format('Ymd'),
|
||||||
|
'end_date' => $this->endDate->format('Ymd')
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $query->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete existing processed data untuk account
|
||||||
|
* Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||||
|
*
|
||||||
|
* @param array $criteria
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function deleteExistingProcessedData(array $criteria): void
|
||||||
|
{
|
||||||
|
Log::info('Deleting existing processed data for PDF generation', [
|
||||||
|
'account_number' => $criteria['account_number'],
|
||||||
|
'period' => $criteria['period']
|
||||||
|
]);
|
||||||
|
|
||||||
|
ProcessedStatement::where('account_number', $criteria['account_number'])
|
||||||
|
->where('period', $criteria['period'])
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process dan save statement entries untuk account
|
||||||
|
* Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||||
|
*
|
||||||
|
* @param Account $account
|
||||||
|
* @param int $totalCount
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function processAndSaveStatementEntries($account, int $totalCount): void
|
||||||
|
{
|
||||||
|
// Get saldo awal dari AccountBalance
|
||||||
|
$saldoAwalBulan = $this->getSaldoAwalBulan($account->account_number);
|
||||||
|
$runningBalance = (float) $saldoAwalBulan->actual_balance;
|
||||||
|
$globalSequence = 0;
|
||||||
|
|
||||||
|
Log::info("Processing {$totalCount} statement entries for PDF generation", [
|
||||||
|
'account_number' => $account->account_number,
|
||||||
|
'starting_balance' => $runningBalance
|
||||||
|
]);
|
||||||
|
|
||||||
|
StmtEntry::with(['ft', 'transaction'])
|
||||||
|
->where('account_number', $account->account_number)
|
||||||
|
->whereBetween('booking_date', [
|
||||||
|
$this->startDate->format('Ymd'),
|
||||||
|
$this->endDate->format('Ymd')
|
||||||
|
])
|
||||||
|
->orderBy('date_time', 'ASC')
|
||||||
|
->orderBy('trans_reference', 'ASC')
|
||||||
|
->chunk(1000, function ($entries) use (&$runningBalance, &$globalSequence, $account) {
|
||||||
|
$processedData = $this->prepareProcessedData($entries, $runningBalance, $globalSequence, $account->account_number);
|
||||||
|
|
||||||
|
if (!empty($processedData)) {
|
||||||
|
DB::table('processed_statements')->insert($processedData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare processed data untuk batch insert
|
||||||
|
* Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||||
|
*
|
||||||
|
* @param $entries
|
||||||
|
* @param float $runningBalance
|
||||||
|
* @param int $globalSequence
|
||||||
|
* @param string $accountNumber
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function prepareProcessedData($entries, &$runningBalance, &$globalSequence, $accountNumber): array
|
||||||
|
{
|
||||||
|
$processedData = [];
|
||||||
|
|
||||||
|
foreach ($entries as $item) {
|
||||||
|
$globalSequence++;
|
||||||
|
$runningBalance += (float) $item->amount_lcy;
|
||||||
|
|
||||||
|
$actualDate = $this->formatActualDate($item);
|
||||||
|
|
||||||
|
$processedData[] = [
|
||||||
|
'account_number' => $accountNumber,
|
||||||
|
'period' => $this->period,
|
||||||
|
'sequence_no' => $globalSequence,
|
||||||
|
'transaction_date' => $item->booking_date,
|
||||||
|
'reference_number' => $item->trans_reference,
|
||||||
|
'transaction_amount' => $item->amount_lcy,
|
||||||
|
'transaction_type' => $item->amount_lcy < 0 ? 'D' : 'C',
|
||||||
|
'description' => $this->generateNarrative($item),
|
||||||
|
'end_balance' => $runningBalance,
|
||||||
|
'actual_date' => $actualDate,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $processedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format actual date dari item
|
||||||
|
* Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||||
|
*
|
||||||
|
* @param $item
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function formatActualDate($item): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$prefix = substr($item->trans_reference ?? '', 0, 2);
|
||||||
|
$relationMap = [
|
||||||
|
'FT' => 'ft',
|
||||||
|
'TT' => 'tt',
|
||||||
|
'DC' => 'dc',
|
||||||
|
'AA' => 'aa'
|
||||||
|
];
|
||||||
|
|
||||||
|
$datetime = $item->date_time;
|
||||||
|
if (isset($relationMap[$prefix])) {
|
||||||
|
$relation = $relationMap[$prefix];
|
||||||
|
$datetime = $item->$relation?->date_time ?? $datetime;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Carbon::createFromFormat(
|
||||||
|
'ymdHi',
|
||||||
|
$datetime
|
||||||
|
)->format('d/m/Y H:i');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::warning("Error formatting actual date: " . $e->getMessage());
|
||||||
|
return Carbon::now()->format('d/m/Y H:i');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate narrative untuk statement entry
|
||||||
|
* Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||||
|
*
|
||||||
|
* @param $item
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function generateNarrative($item)
|
||||||
|
{
|
||||||
|
$narr = [];
|
||||||
|
|
||||||
|
if ($item->transaction) {
|
||||||
|
if ($item->transaction->stmt_narr) {
|
||||||
|
$narr[] = $item->transaction->stmt_narr;
|
||||||
|
}
|
||||||
|
if ($item->narrative) {
|
||||||
|
$narr[] = $item->narrative;
|
||||||
|
}
|
||||||
|
if ($item->transaction->narr_type) {
|
||||||
|
$narr[] = $this->getFormatNarrative($item->transaction->narr_type, $item);
|
||||||
|
}
|
||||||
|
} else if ($item->narrative) {
|
||||||
|
$narr[] = $item->narrative;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($item->ft?->recipt_no) {
|
||||||
|
$narr[] = 'Receipt No: ' . $item->ft->recipt_no;
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' ', array_filter($narr));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get formatted narrative berdasarkan narrative type
|
||||||
|
* Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||||
|
*
|
||||||
|
* @param $narr
|
||||||
|
* @param $item
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getFormatNarrative($narr, $item)
|
||||||
|
{
|
||||||
|
|
||||||
|
$narrParam = TempStmtNarrParam::where('_id', $narr)->first();
|
||||||
|
|
||||||
|
if (!$narrParam) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$fmt = '';
|
||||||
|
if ($narrParam->_id == 'FTIN') {
|
||||||
|
$fmt = 'FT.IN';
|
||||||
|
} else if ($narrParam->_id == 'FTOUT') {
|
||||||
|
$fmt = 'FT.OUT';
|
||||||
|
} else if ($narrParam->_id == 'TTTRFOUT') {
|
||||||
|
$fmt = 'TT.O.TRF';
|
||||||
|
} else if ($narrParam->_id == 'TTTRFIN') {
|
||||||
|
$fmt = 'TT.I.TRF';
|
||||||
|
} else if ($narrParam->_id == 'APITRX'){
|
||||||
|
$fmt = 'API.TSEL';
|
||||||
|
} else if ($narrParam->_id == 'ONUSCR'){
|
||||||
|
$fmt = 'ONUS.CR';
|
||||||
|
} else if ($narrParam->_id == 'ONUSDR'){
|
||||||
|
$fmt = 'ONUS.DR';
|
||||||
|
}else {
|
||||||
|
$fmt = $narrParam->_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$narrFormat = TempStmtNarrFormat::where('_id', $fmt)->first();
|
||||||
|
|
||||||
|
if (!$narrFormat) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the format string from the database
|
||||||
|
$formatString = $narrFormat->text_data ?? '';
|
||||||
|
|
||||||
|
// Parse the format string
|
||||||
|
// Split by the separator ']'
|
||||||
|
$parts = explode(']', $formatString);
|
||||||
|
|
||||||
|
$result = '';
|
||||||
|
|
||||||
|
foreach ($parts as $index => $part) {
|
||||||
|
if (empty($part)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($index === 0) {
|
||||||
|
// For the first part, take only what's before the '!'
|
||||||
|
$splitPart = explode('!', $part);
|
||||||
|
if (count($splitPart) > 0) {
|
||||||
|
// Remove quotes, backslashes, and other escape characters
|
||||||
|
$cleanPart = trim($splitPart[0]).' ';
|
||||||
|
// Remove quotes at the beginning and end
|
||||||
|
$cleanPart = preg_replace('/^["\'\\\\]+|["\'\\\\]+$/', '', $cleanPart);
|
||||||
|
// Remove any remaining backslashes
|
||||||
|
$cleanPart = str_replace('\\', '', $cleanPart);
|
||||||
|
// Remove any remaining quotes
|
||||||
|
$cleanPart = str_replace('"', '', $cleanPart);
|
||||||
|
$result .= $cleanPart;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For other parts, these are field placeholders
|
||||||
|
$fieldName = strtolower(str_replace('.', '_', $part));
|
||||||
|
|
||||||
|
// Get the corresponding parameter value from narrParam
|
||||||
|
$paramValue = null;
|
||||||
|
|
||||||
|
// Check if the field exists as a property in narrParam
|
||||||
|
if (property_exists($narrParam, $fieldName)) {
|
||||||
|
$paramValue = $narrParam->$fieldName;
|
||||||
|
} else if (isset($narrParam->$fieldName)) {
|
||||||
|
$paramValue = $narrParam->$fieldName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found a value, add it to the result
|
||||||
|
if ($paramValue !== null) {
|
||||||
|
$result .= $paramValue;
|
||||||
|
} else {
|
||||||
|
// If no value found, try to use the original field name as a fallback
|
||||||
|
if ($fieldName !== 'recipt_no') {
|
||||||
|
$prefix = substr($item->trans_reference ?? '', 0, 2);
|
||||||
|
$relationMap = [
|
||||||
|
'FT' => 'ft',
|
||||||
|
'TT' => 'tt',
|
||||||
|
'DC' => 'dc',
|
||||||
|
'AA' => 'aa'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isset($relationMap[$prefix])) {
|
||||||
|
$relation = $relationMap[$prefix];
|
||||||
|
$result .= ($item->$relation?->$fieldName ?? '') . ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str_replace('<NL>', ' ', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get processed statement entries untuk account
|
||||||
|
* Menggunakan data dari tabel ProcessedStatement yang sudah diproses
|
||||||
|
*
|
||||||
|
* @param string $accountNumber
|
||||||
|
* @return \Illuminate\Database\Eloquent\Collection
|
||||||
|
*/
|
||||||
|
protected function getProcessedStatementEntries($accountNumber)
|
||||||
|
{
|
||||||
|
Log::info('Getting processed statement entries', [
|
||||||
|
'account_number' => $accountNumber,
|
||||||
|
'period' => $this->period
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ProcessedStatement::where('account_number', $accountNumber)
|
||||||
|
->where('period', $this->period)
|
||||||
|
->orderBy('sequence_no', 'ASC')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get saldo awal bulan untuk account
|
||||||
|
* Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||||
|
*
|
||||||
|
* @param string $accountNumber
|
||||||
|
* @return object
|
||||||
|
*/
|
||||||
|
protected function getSaldoAwalBulan($accountNumber)
|
||||||
|
{
|
||||||
|
// Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||||
|
// Ambil saldo dari ProcessedStatement entry pertama dikurangi transaction_amount
|
||||||
|
$firstEntry = ProcessedStatement::where('account_number', $accountNumber)
|
||||||
|
->where('period', $this->period)
|
||||||
|
->orderBy('sequence_no', 'ASC')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($firstEntry) {
|
||||||
|
$saldoAwal = $firstEntry->end_balance - $firstEntry->transaction_amount;
|
||||||
|
return (object) ['actual_balance' => $saldoAwal];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback ke AccountBalance jika tidak ada ProcessedStatement
|
||||||
|
$saldoPeriod = $this->calculateSaldoPeriod($this->period);
|
||||||
|
|
||||||
|
$saldo = AccountBalance::where('account_number', $accountNumber)
|
||||||
|
->where('period', $saldoPeriod)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
return $saldo ?: (object) ['actual_balance' => 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate saldo period berdasarkan aturan bisnis
|
||||||
|
* Menggunakan logika yang sama dengan ExportStatementPeriodJob
|
||||||
|
*
|
||||||
|
* @param string $period
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function calculateSaldoPeriod($period)
|
||||||
|
{
|
||||||
|
if ($period === '202505') {
|
||||||
|
return '20250510';
|
||||||
|
}
|
||||||
|
|
||||||
|
// For periods after 202505, get last day of previous month
|
||||||
|
if ($period > '202505') {
|
||||||
|
$year = substr($period, 0, 4);
|
||||||
|
$month = substr($period, 4, 2);
|
||||||
|
$firstDay = Carbon::createFromFormat('Ym', $period)->startOfMonth();
|
||||||
|
return $firstDay->copy()->subDay()->format('Ymd');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $period . '01';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare images as base64 for PDF
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function prepareImagesForPdf()
|
||||||
|
{
|
||||||
|
$images = [];
|
||||||
|
|
||||||
|
$imagePaths = [
|
||||||
|
'headerTableBg' => 'assets/media/images/bg-header-table.png',
|
||||||
|
'watermark' => 'assets/media/images/watermark.png',
|
||||||
|
'logoArthagraha' => 'assets/media/images/logo-arthagraha.png',
|
||||||
|
'logoAgi' => 'assets/media/images/logo-agi.png',
|
||||||
|
'bannerFooter' => 'assets/media/images/banner-footer.png'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($imagePaths as $key => $path) {
|
||||||
|
$fullPath = public_path($path);
|
||||||
|
if (file_exists($fullPath)) {
|
||||||
|
$images[$key] = base64_encode(file_get_contents($fullPath));
|
||||||
|
} else {
|
||||||
|
$images[$key] = null;
|
||||||
|
Log::warning('Image file not found', ['path' => $fullPath]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $images;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create ZIP file dari multiple PDF files dengan password protection
|
||||||
|
*
|
||||||
|
* @param array $pdfFiles
|
||||||
|
* @return string|null Path to ZIP file
|
||||||
|
*/
|
||||||
|
protected function createZipFile($pdfFiles)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$zipFilename = "statements_{$this->period}_multi_account_{$this->statement->id}_" . now()->format('YmdHis') . '.zip';
|
||||||
|
$zipStoragePath = "statements/{$this->period}/multi_account/{$this->statement->id}";
|
||||||
|
$fullZipPath = "{$zipStoragePath}/{$zipFilename}";
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
Storage::disk('local')->makeDirectory($zipStoragePath);
|
||||||
|
|
||||||
|
$zipPath = storage_path("app/{$fullZipPath}");
|
||||||
|
|
||||||
|
// Get password from statement or use default
|
||||||
|
$password = $this->statement->password ?? config('webstatement.zip_password', 'statement123');
|
||||||
|
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
if ($zip->open($zipPath, ZipArchive::CREATE) !== TRUE) {
|
||||||
|
throw new Exception('Cannot create ZIP file');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set password for the ZIP file
|
||||||
|
if (!empty($password)) {
|
||||||
|
$zip->setPassword($password);
|
||||||
|
Log::info('ZIP password protection enabled', [
|
||||||
|
'statement_id' => $this->statement->id,
|
||||||
|
'zip_path' => $zipPath
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($pdfFiles as $pdfFile) {
|
||||||
|
if (file_exists($pdfFile)) {
|
||||||
|
$filename = basename($pdfFile);
|
||||||
|
$zip->addFile($pdfFile, $filename);
|
||||||
|
|
||||||
|
// Set encryption for each file in ZIP
|
||||||
|
if (!empty($password)) {
|
||||||
|
$zip->setEncryptionName($filename, ZipArchive::EM_AES_256);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
// Verify ZIP file was created
|
||||||
|
if (!file_exists($zipPath)) {
|
||||||
|
throw new Exception('ZIP file was not created');
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('ZIP file created successfully with password protection', [
|
||||||
|
'zip_path' => $zipPath,
|
||||||
|
'pdf_count' => count($pdfFiles),
|
||||||
|
'statement_id' => $this->statement->id,
|
||||||
|
'password_protected' => !empty($password)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Clean up individual PDF files after creating ZIP
|
||||||
|
foreach ($pdfFiles as $pdfFile) {
|
||||||
|
if (file_exists($pdfFile)) {
|
||||||
|
unlink($pdfFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $zipPath;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('Failed to create ZIP file', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'statement_id' => $this->statement->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,123 +1,269 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Modules\Webstatement\Jobs;
|
namespace Modules\Webstatement\Jobs;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Modules\Webstatement\Models\Account;
|
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();
|
||||||
|
|
||||||
/**
|
if ($this->period === '') {
|
||||||
* Create a new job instance.
|
Log::warning('No period provided for account data processing');
|
||||||
*/
|
return;
|
||||||
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();
|
|
||||||
$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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$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;
|
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.
|
* Create a new job instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(array $periods = [])
|
public function __construct(string $period = '')
|
||||||
{
|
{
|
||||||
$this->periods = $periods;
|
$this->period = $period;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* Execute the job.
|
||||||
*/
|
*/
|
||||||
public function handle(): void
|
public function handle()
|
||||||
|
: void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
set_time_limit(24 * 60 * 60);
|
$this->initializeJob();
|
||||||
$disk = Storage::disk('sftpStatement');
|
|
||||||
$processedCount = 0;
|
|
||||||
$errorCount = 0;
|
|
||||||
|
|
||||||
if (empty($this->periods)) {
|
if ($this->period === '') {
|
||||||
Log::warning('No periods provided for arrangement data processing');
|
Log::warning('No period provided for arrangement data processing');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->periods as $period) {
|
$this->processPeriod();
|
||||||
// Skip the _parameter folder
|
$this->logJobCompletion();
|
||||||
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");
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Log::error('Error in ProcessArrangementDataJob: ' . $e->getMessage());
|
Log::error('Error in ProcessArrangementDataJob: ' . $e->getMessage());
|
||||||
throw $e;
|
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}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
263
app/Jobs/ProcessAtmTransactionJob.php
Normal file
263
app/Jobs/ProcessAtmTransactionJob.php
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
<?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\AtmTransaction;
|
||||||
|
|
||||||
|
class ProcessAtmTransactionJob 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.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',
|
||||||
|
'txn_type' => 'txn_type',
|
||||||
|
'merchant_id' => 'merchant_id',
|
||||||
|
'txn_amount' => 'txn_amount',
|
||||||
|
'booking_date' => 'booking_date',
|
||||||
|
'trans_ref' => 'trans_ref',
|
||||||
|
'retrieval_ref_no' => 'retrieval_ref_no',
|
||||||
|
'stmt_nos' => 'stmt_nos',
|
||||||
|
'debit_acct_no' => 'debit_acct_no',
|
||||||
|
'credit_acct_no' => 'credit_acct_no',
|
||||||
|
'chrg_amount' => 'chrg_amount',
|
||||||
|
'value_date' => 'value_date',
|
||||||
|
'stan_no' => 'stan_no',
|
||||||
|
'trans_status' => 'trans_status',
|
||||||
|
'proc_code' => 'proc_code'
|
||||||
|
];
|
||||||
|
|
||||||
|
private string $period = '';
|
||||||
|
private int $processedCount = 0;
|
||||||
|
private int $errorCount = 0;
|
||||||
|
private array $atmTransactionBatch = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 ATM transaction data processing');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->processPeriod();
|
||||||
|
$this->logJobCompletion();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('Error in ProcessAtmTransactionJob: ' . $e->getMessage());
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function initializeJob()
|
||||||
|
: void
|
||||||
|
{
|
||||||
|
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||||
|
$this->processedCount = 0;
|
||||||
|
$this->errorCount = 0;
|
||||||
|
$this->atmTransactionBatch = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ATM transaction 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;
|
||||||
|
$chunkCount = 0;
|
||||||
|
|
||||||
|
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||||
|
$rowCount++;
|
||||||
|
$this->processRow($headerRow, $row, $rowCount, $filePath);
|
||||||
|
|
||||||
|
// 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)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process any remaining records
|
||||||
|
if (!empty($this->atmTransactionBatch)) {
|
||||||
|
$this->saveBatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
$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) {
|
||||||
|
$data[$modelField] = $rawData[$csvField] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip header row if it was included in the data
|
||||||
|
if ($data['transaction_id'] === 'id') {
|
||||||
|
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 {
|
||||||
|
// Add timestamp fields
|
||||||
|
$now = now();
|
||||||
|
$data['created_at'] = $now;
|
||||||
|
$data['updated_at'] = $now;
|
||||||
|
|
||||||
|
// Add to batch
|
||||||
|
$this->atmTransactionBatch[] = $data;
|
||||||
|
$this->processedCount++;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$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
|
<?php
|
||||||
|
|
||||||
namespace Modules\Webstatement\Jobs;
|
namespace Modules\Webstatement\Jobs;
|
||||||
|
|
||||||
use Illuminate\Bus\Queueable;
|
use Exception;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Modules\Webstatement\Models\TempBillDetail;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Exception;
|
use Modules\Webstatement\Models\TempBillDetail;
|
||||||
|
|
||||||
class ProcessBillDetailDataJob implements ShouldQueue
|
class ProcessBillDetailDataJob implements ShouldQueue
|
||||||
{
|
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
||||||
|
|
||||||
protected $periods;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new job instance.
|
|
||||||
*/
|
|
||||||
public function __construct(array $periods = [])
|
|
||||||
{
|
{
|
||||||
$this->periods = $periods;
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
private const CSV_DELIMITER = '~';
|
||||||
* Execute the job.
|
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||||
*/
|
private const FILENAME = 'ST.AA.BILL.DETAILS.csv';
|
||||||
public function handle()
|
private const DISK_NAME = 'sftpStatement';
|
||||||
{
|
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
|
||||||
try {
|
|
||||||
set_time_limit(24 * 60 * 60);
|
|
||||||
$disk = Storage::disk('sftpStatement');
|
|
||||||
$processedCount = 0;
|
|
||||||
$errorCount = 0;
|
|
||||||
|
|
||||||
if (empty($this->periods)) {
|
private string $period = '';
|
||||||
Log::warning('No periods provided for bill detail data processing');
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->periods as $period) {
|
$tempFilePath = $this->createTemporaryFile($disk, $filePath, $filename);
|
||||||
// Skip the _parameter folder
|
$this->processFile($tempFilePath, $filePath);
|
||||||
if ($period === '_parameter') {
|
$this->cleanup($tempFilePath);
|
||||||
Log::info("Skipping _parameter folder");
|
}
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct the filename based on the period folder name
|
private function validateFile($disk, string $filePath)
|
||||||
$filename = "$period.ST.AA.BILL.DETAILS.csv";
|
: bool
|
||||||
$filePath = "$period/$filename";
|
{
|
||||||
|
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)) {
|
return true;
|
||||||
Log::warning("File not found: $filePath");
|
}
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a temporary local copy of the file
|
private function createTemporaryFile($disk, string $filePath, string $filename)
|
||||||
$tempFilePath = storage_path("app/temp_$filename");
|
: string
|
||||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
{
|
||||||
|
$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();
|
||||||
$headers = (new TempBillDetail())->getFillable();
|
$rowCount = 0;
|
||||||
$rowCount = 0;
|
$chunkCount = 0;
|
||||||
|
|
||||||
while (($row = fgetcsv($handle, 0, ";")) !== false) {
|
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||||
$rowCount++;
|
$rowCount++;
|
||||||
|
$this->processRow($row, $headers, $rowCount, $filePath);
|
||||||
|
|
||||||
if (count($headers) === count($row)) {
|
// Process in chunks to avoid memory issues
|
||||||
$data = array_combine($headers, $row);
|
if (count($this->billDetailBatch) >= self::CHUNK_SIZE) {
|
||||||
try {
|
$this->saveBatch();
|
||||||
if (isset($data['_id']) && $data['_id'] !== '_id') {
|
$chunkCount++;
|
||||||
TempBillDetail::updateOrCreate(
|
Log::info("Processed chunk $chunkCount ({$this->processedCount} records so far)");
|
||||||
['_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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
fclose($handle);
|
||||||
Log::error('Error in ProcessBillDetailDataJob: ' . $e->getMessage());
|
Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors.");
|
||||||
throw $e;
|
}
|
||||||
|
|
||||||
|
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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
202
app/Jobs/ProcessCategoryDataJob.php
Normal file
202
app/Jobs/ProcessCategoryDataJob.php
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<?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\Category;
|
||||||
|
|
||||||
|
class ProcessCategoryDataJob 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.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(string $period = '')
|
||||||
|
{
|
||||||
|
$this->period = $period;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
: void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->initializeJob();
|
||||||
|
|
||||||
|
if ($this->period === '') {
|
||||||
|
Log::warning('No period provided for category data processing');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
215
app/Jobs/ProcessCompanyDataJob.php
Normal file
215
app/Jobs/ProcessCompanyDataJob.php
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
<?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\Basicdata\Models\Branch;
|
||||||
|
|
||||||
|
class ProcessCompanyDataJob 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.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'];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->processRow($row, $rowCount, $filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
// Exclude the last field from CSV
|
||||||
|
if (count($row) > 0) {
|
||||||
|
array_pop($row);
|
||||||
|
Log::info("Excluded last field from row $rowCount. New column count: " . count($row));
|
||||||
|
}
|
||||||
|
|
||||||
|
$csvHeaders = array_keys(self::FIELD_MAP);
|
||||||
|
|
||||||
|
if (count($csvHeaders) !== count($row)) {
|
||||||
|
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||||
|
count($csvHeaders) . ", Got: " . count($row));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$csvData = array_combine($csvHeaders, $row);
|
||||||
|
$this->mapAndSaveRecord($csvData, $rowCount, $filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->saveRecord($branchData, $rowCount, $filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
<?php
|
||||||
|
|
||||||
namespace Modules\Webstatement\Jobs;
|
namespace Modules\Webstatement\Jobs;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Modules\Webstatement\Models\Customer;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Modules\Webstatement\Models\Customer;
|
||||||
|
|
||||||
class ProcessCustomerDataJob implements ShouldQueue
|
class ProcessCustomerDataJob implements ShouldQueue
|
||||||
{
|
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
||||||
|
|
||||||
protected $periods;
|
|
||||||
|
|
||||||
public function __construct(array $periods = [])
|
|
||||||
{
|
{
|
||||||
$this->periods = $periods;
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
}
|
|
||||||
|
|
||||||
public function handle()
|
private const CSV_DELIMITER = '~';
|
||||||
{
|
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||||
try {
|
private const FILENAME = 'ST.CUSTOMER.csv';
|
||||||
set_time_limit(24 * 60 * 60);
|
private const DISK_NAME = 'sftpStatement';
|
||||||
$disk = Storage::disk('sftpStatement');
|
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
|
||||||
$processedCount = 0;
|
|
||||||
$errorCount = 0;
|
|
||||||
|
|
||||||
if (empty($this->periods)) {
|
private string $period = '';
|
||||||
Log::warning('No periods provided for customer data processing');
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->periods as $period) {
|
$tempFilePath = $this->createTemporaryFile($disk, $filePath, $filename);
|
||||||
|
$this->processFile($tempFilePath, $filePath);
|
||||||
|
$this->cleanup($tempFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
// Skip the _parameter folder
|
private function validateFile($disk, string $filePath)
|
||||||
if ($period === '_parameter') {
|
: bool
|
||||||
Log::info("Skipping _parameter folder");
|
{
|
||||||
continue;
|
Log::info("Processing customer file: $filePath");
|
||||||
}
|
|
||||||
|
|
||||||
// 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");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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) {
|
private function createTemporaryFile($disk, string $filePath, string $filename)
|
||||||
Log::error('Error in ProcessCustomerDataJob: ' . $e->getMessage());
|
: string
|
||||||
throw $e;
|
{
|
||||||
|
$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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
267
app/Jobs/ProcessDataCaptureDataJob.php
Normal file
267
app/Jobs/ProcessDataCaptureDataJob.php
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
<?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\DataCapture;
|
||||||
|
|
||||||
|
class ProcessDataCaptureDataJob 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.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(string $period = '')
|
||||||
|
{
|
||||||
|
$this->period = $period;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
: void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->initializeJob();
|
||||||
|
|
||||||
|
if ($this->period === '') {
|
||||||
|
Log::warning('No period provided for data capture processing');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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
|
||||||
|
{
|
||||||
|
// Exclude the last field from CSV
|
||||||
|
if (count($row) > 0) {
|
||||||
|
array_pop($row);
|
||||||
|
Log::info("Excluded last field from row $rowCount. New column count: " . count($row));
|
||||||
|
}
|
||||||
|
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
175
app/Jobs/ProcessFtTxnTypeConditionJob.php
Normal file
175
app/Jobs/ProcessFtTxnTypeConditionJob.php
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<?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\FtTxnTypeCondition;
|
||||||
|
|
||||||
|
class ProcessFtTxnTypeConditionJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tempFilePath = $this->createTemporaryFile($disk, $filePath);
|
||||||
|
$this->processFile($tempFilePath, $filePath);
|
||||||
|
$this->cleanup($tempFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateFile($disk, string $filePath)
|
||||||
|
: bool
|
||||||
|
{
|
||||||
|
Log::info("Processing transaction type condition 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
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.
|
* Create a new job instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(array $periods = [])
|
public function __construct(string $period = '')
|
||||||
{
|
{
|
||||||
$this->periods = $periods;
|
$this->period = $period;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* Execute the job.
|
||||||
*/
|
*/
|
||||||
public function handle(): void
|
public function handle()
|
||||||
|
: void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
set_time_limit(24 * 60 * 60);
|
$this->initializeJob();
|
||||||
$disk = Storage::disk('sftpStatement');
|
|
||||||
$processedCount = 0;
|
|
||||||
$errorCount = 0;
|
|
||||||
|
|
||||||
if (empty($this->periods)) {
|
if ($this->period === '') {
|
||||||
Log::warning('No periods provided for funds transfer data processing');
|
Log::warning('No period provided for funds transfer data processing');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->periods as $period) {
|
$this->processPeriod();
|
||||||
// Skip the _parameter folder
|
$this->logJobCompletion();
|
||||||
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");
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Log::error('Error in ProcessFundsTransferDataJob: ' . $e->getMessage());
|
Log::error('Error in ProcessFundsTransferDataJob: ' . $e->getMessage());
|
||||||
throw $e;
|
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}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
300
app/Jobs/ProcessProvinceDataJob.php
Normal file
300
app/Jobs/ProcessProvinceDataJob.php
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
<?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\DB;
|
||||||
|
use Modules\Webstatement\Models\ProvinceCore;
|
||||||
|
|
||||||
|
class ProcessProvinceDataJob 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.PROVINCE.csv';
|
||||||
|
private const DISK_NAME = 'sftpStatement';
|
||||||
|
|
||||||
|
private string $period = '';
|
||||||
|
private int $processedCount = 0;
|
||||||
|
private int $errorCount = 0;
|
||||||
|
private int $skippedCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Membuat instance job baru untuk memproses data provinsi
|
||||||
|
*
|
||||||
|
* @param string $period Periode data yang akan diproses
|
||||||
|
*/
|
||||||
|
public function __construct(string $period = '')
|
||||||
|
{
|
||||||
|
$this->period = $period;
|
||||||
|
Log::info('ProcessProvinceDataJob: Job dibuat untuk periode: ' . $period);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menjalankan job untuk memproses file ST.PROVINCE.csv
|
||||||
|
* Menggunakan transaction untuk memastikan konsistensi data
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Log::info('ProcessProvinceDataJob: Memulai pemrosesan data provinsi');
|
||||||
|
|
||||||
|
$this->initializeJob();
|
||||||
|
|
||||||
|
if ($this->period === '') {
|
||||||
|
Log::warning('ProcessProvinceDataJob: Tidak ada periode yang diberikan untuk pemrosesan data provinsi');
|
||||||
|
DB::rollback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->processPeriod();
|
||||||
|
$this->logJobCompletion();
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
Log::info('ProcessProvinceDataJob: Transaction berhasil di-commit');
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
DB::rollback();
|
||||||
|
Log::error('ProcessProvinceDataJob: Error dalam pemrosesan, transaction di-rollback: ' . $e->getMessage());
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inisialisasi pengaturan job
|
||||||
|
* Mengatur timeout dan reset counter
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function initializeJob(): void
|
||||||
|
{
|
||||||
|
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||||
|
$this->processedCount = 0;
|
||||||
|
$this->errorCount = 0;
|
||||||
|
$this->skippedCount = 0;
|
||||||
|
|
||||||
|
Log::info('ProcessProvinceDataJob: Job diinisialisasi dengan timeout ' . self::MAX_EXECUTION_TIME . ' detik');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memproses file untuk periode tertentu
|
||||||
|
* Mengambil file dari SFTP dan memproses data
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function processPeriod(): void
|
||||||
|
{
|
||||||
|
$disk = Storage::disk(self::DISK_NAME);
|
||||||
|
$filePath = "$this->period/" . self::FILENAME;
|
||||||
|
|
||||||
|
Log::info('ProcessProvinceDataJob: Memproses periode ' . $this->period);
|
||||||
|
|
||||||
|
if (!$this->validateFile($disk, $filePath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tempFilePath = $this->createTemporaryFile($disk, $filePath);
|
||||||
|
$this->processFile($tempFilePath, $filePath);
|
||||||
|
$this->cleanup($tempFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validasi keberadaan file di storage
|
||||||
|
*
|
||||||
|
* @param mixed $disk Storage disk instance
|
||||||
|
* @param string $filePath Path file yang akan divalidasi
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function validateFile($disk, string $filePath): bool
|
||||||
|
{
|
||||||
|
Log::info("ProcessProvinceDataJob: Memvalidasi file provinsi: $filePath");
|
||||||
|
|
||||||
|
if (!$disk->exists($filePath)) {
|
||||||
|
Log::warning("ProcessProvinceDataJob: File tidak ditemukan: $filePath");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info("ProcessProvinceDataJob: File ditemukan dan valid: $filePath");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Membuat file temporary untuk pemrosesan
|
||||||
|
*
|
||||||
|
* @param mixed $disk Storage disk instance
|
||||||
|
* @param string $filePath Path file sumber
|
||||||
|
* @return string Path file temporary
|
||||||
|
*/
|
||||||
|
private function createTemporaryFile($disk, string $filePath): string
|
||||||
|
{
|
||||||
|
$tempFilePath = storage_path("app/temp_" . self::FILENAME);
|
||||||
|
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||||
|
|
||||||
|
Log::info("ProcessProvinceDataJob: File temporary dibuat: $tempFilePath");
|
||||||
|
return $tempFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memproses file CSV dan mengimpor data ke database
|
||||||
|
* Format CSV: id~date_time~province~province_name
|
||||||
|
*
|
||||||
|
* @param string $tempFilePath Path file temporary
|
||||||
|
* @param string $filePath Path file asli untuk logging
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function processFile(string $tempFilePath, string $filePath): void
|
||||||
|
{
|
||||||
|
$handle = fopen($tempFilePath, "r");
|
||||||
|
if ($handle === false) {
|
||||||
|
Log::error("ProcessProvinceDataJob: Tidak dapat membuka file: $filePath");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info("ProcessProvinceDataJob: Memulai pemrosesan file: $filePath");
|
||||||
|
|
||||||
|
$rowCount = 0;
|
||||||
|
$isFirstRow = true;
|
||||||
|
|
||||||
|
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||||
|
$rowCount++;
|
||||||
|
|
||||||
|
// Skip header row
|
||||||
|
if ($isFirstRow) {
|
||||||
|
$isFirstRow = false;
|
||||||
|
Log::info("ProcessProvinceDataJob: Melewati header row: " . implode(self::CSV_DELIMITER, $row));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->processRow($row, $rowCount, $filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose($handle);
|
||||||
|
Log::info("ProcessProvinceDataJob: Selesai memproses $filePath. Total baris: $rowCount, Diproses: {$this->processedCount}, Error: {$this->errorCount}, Dilewati: {$this->skippedCount}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memproses satu baris data CSV
|
||||||
|
*
|
||||||
|
* @param array $row Data baris CSV
|
||||||
|
* @param int $rowCount Nomor baris untuk logging
|
||||||
|
* @param string $filePath Path file untuk logging
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function processRow(array $row, int $rowCount, string $filePath): void
|
||||||
|
{
|
||||||
|
// Validasi jumlah kolom (id~date_time~province~province_name = 4 kolom)
|
||||||
|
if (count($row) !== 4) {
|
||||||
|
Log::warning("ProcessProvinceDataJob: Baris $rowCount di $filePath memiliki jumlah kolom yang salah. Diharapkan: 4, Ditemukan: " . count($row));
|
||||||
|
$this->skippedCount++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map data sesuai format CSV
|
||||||
|
$data = [
|
||||||
|
'code' => trim($row[2]), // province code
|
||||||
|
'name' => trim($row[3]) // province_name
|
||||||
|
];
|
||||||
|
|
||||||
|
Log::debug("ProcessProvinceDataJob: Memproses baris $rowCount dengan data: " . json_encode($data));
|
||||||
|
|
||||||
|
$this->saveRecord($data, $rowCount, $filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menyimpan record provinsi ke database
|
||||||
|
* Menggunakan updateOrCreate untuk menghindari duplikasi
|
||||||
|
*
|
||||||
|
* @param array $data Data provinsi yang akan disimpan
|
||||||
|
* @param int $rowCount Nomor baris untuk logging
|
||||||
|
* @param string $filePath Path file untuk logging
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function saveRecord(array $data, int $rowCount, string $filePath): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Validasi data wajib
|
||||||
|
if (empty($data['code']) || empty($data['name'])) {
|
||||||
|
Log::warning("ProcessProvinceDataJob: Baris $rowCount di $filePath memiliki data kosong. Code: '{$data['code']}', Name: '{$data['name']}'");
|
||||||
|
$this->skippedCount++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simpan atau update data provinsi
|
||||||
|
$province = ProvinceCore::updateOrCreate(
|
||||||
|
['code' => $data['code']], // Kondisi pencarian
|
||||||
|
['name' => $data['name']] // Data yang akan diupdate/insert
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->processedCount++;
|
||||||
|
Log::debug("ProcessProvinceDataJob: Berhasil menyimpan provinsi ID: {$province->id}, Code: {$data['code']}, Name: {$data['name']}");
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->errorCount++;
|
||||||
|
Log::error("ProcessProvinceDataJob: Error menyimpan data provinsi pada baris $rowCount di $filePath: " . $e->getMessage());
|
||||||
|
Log::error("ProcessProvinceDataJob: Data yang error: " . json_encode($data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Membersihkan file temporary
|
||||||
|
*
|
||||||
|
* @param string $tempFilePath Path file temporary yang akan dihapus
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function cleanup(string $tempFilePath): void
|
||||||
|
{
|
||||||
|
if (file_exists($tempFilePath)) {
|
||||||
|
unlink($tempFilePath);
|
||||||
|
Log::info("ProcessProvinceDataJob: File temporary dihapus: $tempFilePath");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logging hasil akhir pemrosesan job
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function logJobCompletion(): void
|
||||||
|
{
|
||||||
|
$message = "ProcessProvinceDataJob: Pemrosesan data provinsi selesai. " .
|
||||||
|
"Total diproses: {$this->processedCount}, " .
|
||||||
|
"Total error: {$this->errorCount}, " .
|
||||||
|
"Total dilewati: {$this->skippedCount}";
|
||||||
|
|
||||||
|
Log::info($message);
|
||||||
|
|
||||||
|
// Log summary untuk monitoring
|
||||||
|
if ($this->errorCount > 0) {
|
||||||
|
Log::warning("ProcessProvinceDataJob: Terdapat {$this->errorCount} error dalam pemrosesan");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->skippedCount > 0) {
|
||||||
|
Log::info("ProcessProvinceDataJob: Terdapat {$this->skippedCount} baris yang dilewati");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle job failure
|
||||||
|
*
|
||||||
|
* @param Exception $exception
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function failed(Exception $exception): void
|
||||||
|
{
|
||||||
|
Log::error('ProcessProvinceDataJob: Job gagal dijalankan: ' . $exception->getMessage());
|
||||||
|
Log::error('ProcessProvinceDataJob: Stack trace: ' . $exception->getTraceAsString());
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
<?php
|
||||||
|
|
||||||
namespace Modules\Webstatement\Jobs;
|
namespace Modules\Webstatement\Jobs;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
@@ -10,103 +9,247 @@
|
|||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Modules\Webstatement\Models\TempStmtEntry;
|
use Modules\Webstatement\Models\StmtEntry;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class ProcessStmtEntryDataJob implements ShouldQueue
|
class ProcessStmtEntryDataJob implements ShouldQueue
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
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.
|
* Create a new job instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(array $periods = [])
|
public function __construct(string $period = '')
|
||||||
{
|
{
|
||||||
$this->periods = $periods;
|
$this->period = $period;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* Execute the job.
|
||||||
*/
|
*/
|
||||||
public function handle(): void
|
public function handle()
|
||||||
|
: void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
set_time_limit(24 * 60 * 60);
|
$this->initializeJob();
|
||||||
$disk = Storage::disk('sftpStatement');
|
|
||||||
$processedCount = 0;
|
|
||||||
$errorCount = 0;
|
|
||||||
|
|
||||||
if (empty($this->periods)) {
|
if ($this->period === '') {
|
||||||
Log::warning('No periods provided for statement entry data processing');
|
Log::warning('No period provided for statement entry data processing');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->periods as $period) {
|
$this->processPeriod();
|
||||||
// Skip the _parameter folder
|
$this->logJobCompletion();
|
||||||
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 TempStmtEntry())->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') {
|
|
||||||
TempStmtEntry::updateOrCreate(
|
|
||||||
['_id' => $data['_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");
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Log::error('Error in ProcessStmtEntryDataJob: ' . $e->getMessage());
|
Log::error('Error in ProcessStmtEntryDataJob: ' . $e->getMessage());
|
||||||
throw $e;
|
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}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
399
app/Jobs/ProcessStmtEntryDetailDataJob.php
Normal file
399
app/Jobs/ProcessStmtEntryDetailDataJob.php
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
<?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\StmtEntryDetail;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class ProcessStmtEntryDetailDataJob 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.STMT.ENTRY.DETAIL.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.
|
||||||
|
*
|
||||||
|
* @param string $period Periode data yang akan diproses
|
||||||
|
*/
|
||||||
|
public function __construct(string $period = '')
|
||||||
|
{
|
||||||
|
$this->period = $period;
|
||||||
|
Log::info('ProcessStmtEntryDetailDataJob initialized', ['period' => $period]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Log::info('Memulai ProcessStmtEntryDetailDataJob', ['period' => $this->period]);
|
||||||
|
|
||||||
|
$this->initializeJob();
|
||||||
|
|
||||||
|
if ($this->period === '') {
|
||||||
|
Log::warning('No period provided for statement entry detail data processing');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->processPeriod();
|
||||||
|
$this->logJobCompletion();
|
||||||
|
|
||||||
|
Log::info('ProcessStmtEntryDetailDataJob selesai berhasil');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('Error in ProcessStmtEntryDetailDataJob: ' . $e->getMessage());
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inisialisasi job dengan pengaturan awal
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function initializeJob(): void
|
||||||
|
{
|
||||||
|
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||||
|
$this->processedCount = 0;
|
||||||
|
$this->errorCount = 0;
|
||||||
|
$this->entryBatch = [];
|
||||||
|
|
||||||
|
Log::info('Job initialized', [
|
||||||
|
'max_execution_time' => self::MAX_EXECUTION_TIME,
|
||||||
|
'chunk_size' => self::CHUNK_SIZE
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proses data untuk periode tertentu
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function processPeriod(): void
|
||||||
|
{
|
||||||
|
$disk = Storage::disk(self::DISK_NAME);
|
||||||
|
$filename = "{$this->period}." . self::FILENAME;
|
||||||
|
$filePath = "{$this->period}/$filename";
|
||||||
|
|
||||||
|
Log::info('Memulai proses periode', ['file_path' => $filePath]);
|
||||||
|
|
||||||
|
if (!$this->validateFile($disk, $filePath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tempFilePath = $this->createTemporaryFile($disk, $filePath, $filename);
|
||||||
|
$this->processFile($tempFilePath, $filePath);
|
||||||
|
$this->cleanup($tempFilePath);
|
||||||
|
|
||||||
|
Log::info('Proses periode selesai', ['file_path' => $filePath]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validasi keberadaan file
|
||||||
|
*
|
||||||
|
* @param mixed $disk Storage disk instance
|
||||||
|
* @param string $filePath Path file yang akan divalidasi
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function validateFile($disk, string $filePath): bool
|
||||||
|
{
|
||||||
|
Log::info("Processing statement entry detail file: $filePath");
|
||||||
|
|
||||||
|
if (!$disk->exists($filePath)) {
|
||||||
|
Log::warning("File not found: $filePath");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info("File validated successfully: $filePath");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buat file temporary untuk proses
|
||||||
|
*
|
||||||
|
* @param mixed $disk Storage disk instance
|
||||||
|
* @param string $filePath Path file sumber
|
||||||
|
* @param string $filename Nama file
|
||||||
|
* @return string Path file temporary
|
||||||
|
*/
|
||||||
|
private function createTemporaryFile($disk, string $filePath, string $filename): string
|
||||||
|
{
|
||||||
|
$tempFilePath = storage_path("app/temp_$filename");
|
||||||
|
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||||
|
|
||||||
|
Log::info('Temporary file created', ['temp_path' => $tempFilePath]);
|
||||||
|
return $tempFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proses file CSV
|
||||||
|
*
|
||||||
|
* @param string $tempFilePath Path file temporary
|
||||||
|
* @param string $filePath Path file asli
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
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 StmtEntryDetail())->getFillable();
|
||||||
|
// Tambahkan field 'id' ke headers untuk menangani kolom tambahan di akhir CSV
|
||||||
|
$expectedHeaders = array_merge($headers, ['id']);
|
||||||
|
$rowCount = 0;
|
||||||
|
$chunkCount = 0;
|
||||||
|
|
||||||
|
Log::info('Memulai proses file', [
|
||||||
|
'file_path' => $filePath,
|
||||||
|
'headers_count' => count($headers),
|
||||||
|
'expected_headers_count' => count($expectedHeaders)
|
||||||
|
]);
|
||||||
|
|
||||||
|
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||||
|
$rowCount++;
|
||||||
|
$this->processRow($row, $expectedHeaders, $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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proses setiap baris data dengan penanganan field id tambahan
|
||||||
|
*
|
||||||
|
* @param array $row Data baris
|
||||||
|
* @param array $expectedHeaders Header kolom yang diharapkan (termasuk id)
|
||||||
|
* @param int $rowCount Nomor baris
|
||||||
|
* @param string $filePath Path file
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function processRow(array $row, array $expectedHeaders, int $rowCount, string $filePath): void
|
||||||
|
{
|
||||||
|
// Validasi jumlah kolom - sekarang menggunakan expectedHeaders yang sudah include field 'id'
|
||||||
|
if (count($expectedHeaders) !== count($row)) {
|
||||||
|
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||||
|
count($expectedHeaders) . ", Got: " . count($row));
|
||||||
|
$this->errorCount++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kombinasikan data dengan headers
|
||||||
|
$data = array_combine($expectedHeaders, $row);
|
||||||
|
|
||||||
|
// Log untuk debugging struktur data
|
||||||
|
Log::debug('Processing row data', [
|
||||||
|
'row_count' => $rowCount,
|
||||||
|
'stmt_entry_id' => $data['stmt_entry_id'] ?? 'not_set',
|
||||||
|
'id' => $data['id'] ?? 'not_set'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Logika untuk menggunakan field 'id' sebagai fallback jika stmt_entry_id kosong
|
||||||
|
$this->handleStmtEntryIdFallback($data);
|
||||||
|
|
||||||
|
// Hapus field 'id' dari data sebelum disimpan karena tidak ada di fillable model
|
||||||
|
unset($data['id']);
|
||||||
|
|
||||||
|
$this->cleanTransReference($data);
|
||||||
|
$this->addToBatch($data, $rowCount, $filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menangani logika fallback untuk stmt_entry_id menggunakan field id
|
||||||
|
*
|
||||||
|
* @param array $data Data yang akan diproses
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function handleStmtEntryIdFallback(array &$data): void
|
||||||
|
{
|
||||||
|
// Jika stmt_entry_id kosong atau null, gunakan value dari field 'id'
|
||||||
|
if (empty($data['stmt_entry_id']) || $data['stmt_entry_id'] === '' || $data['stmt_entry_id'] === null) {
|
||||||
|
if (isset($data['id']) && !empty($data['id'])) {
|
||||||
|
$data['stmt_entry_id'] = $data['id'];
|
||||||
|
|
||||||
|
Log::info('Using id as stmt_entry_id fallback', [
|
||||||
|
'original_stmt_entry_id' => $data['stmt_entry_id'] ?? 'empty',
|
||||||
|
'fallback_id' => $data['id']
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
Log::warning('Both stmt_entry_id and id are empty', [
|
||||||
|
'stmt_entry_id' => $data['stmt_entry_id'] ?? 'not_set',
|
||||||
|
'id' => $data['id'] ?? 'not_set'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tambahkan record ke batch untuk proses bulk insert
|
||||||
|
*
|
||||||
|
* @param array $data Data record
|
||||||
|
* @param int $rowCount Nomor baris
|
||||||
|
* @param string $filePath Path file
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function addToBatch(array $data, int $rowCount, string $filePath): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Validasi bahwa stmt_entry_id tidak kosong dan bukan header
|
||||||
|
if (isset($data['stmt_entry_id']) &&
|
||||||
|
$data['stmt_entry_id'] !== 'stmt_entry_id' &&
|
||||||
|
!empty($data['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++;
|
||||||
|
|
||||||
|
Log::debug('Record added to batch', [
|
||||||
|
'row' => $rowCount,
|
||||||
|
'stmt_entry_id' => $data['stmt_entry_id']
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
Log::warning('Skipping row due to invalid stmt_entry_id', [
|
||||||
|
'row' => $rowCount,
|
||||||
|
'stmt_entry_id' => $data['stmt_entry_id'] ?? 'not_set'
|
||||||
|
]);
|
||||||
|
$this->errorCount++;
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->errorCount++;
|
||||||
|
Log::error("Error processing Statement Entry Detail at row $rowCount in $filePath: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bersihkan trans_reference dari karakter tidak diinginkan
|
||||||
|
*
|
||||||
|
* @param array $data Data yang akan dibersihkan
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
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']);
|
||||||
|
Log::debug('Trans reference cleaned', ['original' => $data['trans_reference']]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simpan batch data ke database menggunakan updateOrCreate
|
||||||
|
* untuk menghindari error unique constraint
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
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
|
||||||
|
StmtEntryDetail::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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bersihkan file temporary
|
||||||
|
*
|
||||||
|
* @param string $tempFilePath Path file temporary
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function cleanup(string $tempFilePath): void
|
||||||
|
{
|
||||||
|
if (file_exists($tempFilePath)) {
|
||||||
|
unlink($tempFilePath);
|
||||||
|
Log::info('Temporary file cleaned up', ['temp_path' => $tempFilePath]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log penyelesaian job
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function logJobCompletion(): void
|
||||||
|
{
|
||||||
|
Log::info("Statement Entry Detail data processing completed. " .
|
||||||
|
"Total processed: {$this->processedCount}, Total errors: {$this->errorCount}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,112 +1,164 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Modules\Webstatement\Jobs;
|
namespace Modules\Webstatement\Jobs;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Modules\Webstatement\Models\TempStmtNarrFormat;
|
use Modules\Webstatement\Models\TempStmtNarrFormat;
|
||||||
|
|
||||||
class ProcessStmtNarrFormatDataJob implements ShouldQueue
|
class ProcessStmtNarrFormatDataJob implements ShouldQueue
|
||||||
{
|
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
||||||
|
|
||||||
protected $periods;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new job instance.
|
|
||||||
*/
|
|
||||||
public function __construct(array $periods = [])
|
|
||||||
{
|
{
|
||||||
$this->periods = $periods;
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
private const CSV_DELIMITER = '~';
|
||||||
* Execute the job.
|
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||||
*/
|
private const FILENAME = 'ST.STMT.NARR.FORMAT.csv';
|
||||||
public function handle(): void
|
private const DISK_NAME = 'sftpStatement';
|
||||||
{
|
|
||||||
try {
|
|
||||||
set_time_limit(24 * 60 * 60);
|
|
||||||
$disk = Storage::disk('sftpStatement');
|
|
||||||
$processedCount = 0;
|
|
||||||
$errorCount = 0;
|
|
||||||
|
|
||||||
if (empty($this->periods)) {
|
private string $period = '';
|
||||||
Log::warning('No periods provided for statement narrative format data processing');
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->periods as $period) {
|
$tempFilePath = $this->createTemporaryFile($disk, $filePath);
|
||||||
// Skip the _parameter folder
|
$this->processFile($tempFilePath, $filePath);
|
||||||
/*if ($period === '_parameter') {
|
$this->cleanup($tempFilePath);
|
||||||
Log::info("Skipping _parameter folder");
|
}
|
||||||
continue;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
// Construct the filename based on the period folder name
|
private function validateFile($disk, string $filePath)
|
||||||
$filename = "ST.STMT.NARR.FORMAT.csv";
|
: bool
|
||||||
$filePath = "$period/$filename";
|
{
|
||||||
|
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");
|
||||||
if (!$disk->exists($filePath)) {
|
return false;
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log::info("Statement Narrative Format data processing completed. Total processed: $processedCount, Total errors: $errorCount");
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
} catch (Exception $e) {
|
private function createTemporaryFile($disk, string $filePath)
|
||||||
Log::error('Error in ProcessStmtNarrFormatDataJob: ' . $e->getMessage());
|
: string
|
||||||
throw $e;
|
{
|
||||||
|
$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;
|
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.
|
* Create a new job instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(array $periods = [])
|
public function __construct(string $period = '')
|
||||||
{
|
{
|
||||||
$this->periods = $periods;
|
$this->period = $period;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* Execute the job.
|
||||||
*/
|
*/
|
||||||
public function handle(): void
|
public function handle()
|
||||||
|
: void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
set_time_limit(24 * 60 * 60);
|
$this->initializeJob();
|
||||||
$disk = Storage::disk('sftpStatement');
|
|
||||||
$processedCount = 0;
|
|
||||||
$errorCount = 0;
|
|
||||||
|
|
||||||
if (empty($this->periods)) {
|
if ($this->period === '') {
|
||||||
Log::warning('No periods provided for statement narrative parameter data processing');
|
Log::warning('No periods provided for statement narrative parameter data processing');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->periods as $period) {
|
$this->processPeriod();
|
||||||
// Skip the _parameter folder
|
$this->logJobCompletion();
|
||||||
/*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");
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Log::error('Error in ProcessStmtNarrParamDataJob: ' . $e->getMessage());
|
Log::error('Error in ProcessStmtNarrParamDataJob: ' . $e->getMessage());
|
||||||
throw $e;
|
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}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
332
app/Jobs/ProcessTellerDataJob.php
Normal file
332
app/Jobs/ProcessTellerDataJob.php
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
<?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\Teller;
|
||||||
|
|
||||||
|
class ProcessTellerDataJob 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.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',
|
||||||
|
'amount_local_1' => 'amount_local_1',
|
||||||
|
'value_date_1' => 'value_date_1',
|
||||||
|
'account_2' => 'account_2',
|
||||||
|
'new_cust_bal' => 'new_cust_bal',
|
||||||
|
'term_type' => 'term_type',
|
||||||
|
'term_id' => 'term_id',
|
||||||
|
'trans_reff' => 'trans_reff',
|
||||||
|
'card_no' => 'card_no',
|
||||||
|
'recipt_no' => 'recipt_no',
|
||||||
|
'transaction_code' => 'transaction_code',
|
||||||
|
'date_time' => 'date_time',
|
||||||
|
'record_status' => 'record_status',
|
||||||
|
'amount_local_2' => 'amount_local_2',
|
||||||
|
'co_code' => 'co_code',
|
||||||
|
'narrative_1' => 'narrative_1',
|
||||||
|
'wic_flag' => 'wic_flag',
|
||||||
|
'wic_cust_type' => 'wic_cust_type',
|
||||||
|
'wic_full_name' => 'wic_full_name',
|
||||||
|
'wic_alias_name' => 'wic_alias_name',
|
||||||
|
'wic_acct_no' => 'wic_acct_no',
|
||||||
|
'wic_id_type' => 'wic_id_type',
|
||||||
|
'wic_id_no' => 'wic_id_no',
|
||||||
|
'wic_npwp' => 'wic_npwp',
|
||||||
|
'wic_nationality' => 'wic_nationality',
|
||||||
|
'wic_ind_birthpl' => 'wic_ind_birthpl',
|
||||||
|
'wic_ind_birthdt' => 'wic_ind_birthdt',
|
||||||
|
'wic_address_id' => 'wic_address_id',
|
||||||
|
'wic_address_cur' => 'wic_address_cur',
|
||||||
|
'wic_city' => 'wic_city',
|
||||||
|
'wic_province' => 'wic_province',
|
||||||
|
'wic_post_code' => 'wic_post_code',
|
||||||
|
'wic_phone' => 'wic_phone',
|
||||||
|
'wic_gender' => 'wic_gender',
|
||||||
|
'wic_marital_sts' => 'wic_marital_sts',
|
||||||
|
'wic_occptn' => 'wic_occptn',
|
||||||
|
'wic_occptn_dur' => 'wic_occptn_dur',
|
||||||
|
'wic_income_avg' => 'wic_income_avg',
|
||||||
|
'wic_cor_name' => 'wic_cor_name',
|
||||||
|
'wic_cor_address' => 'wic_cor_address',
|
||||||
|
'wic_cor_phone' => 'wic_cor_phone',
|
||||||
|
'wic_cor_lgl_typ' => 'wic_cor_lgl_typ',
|
||||||
|
'wic_cor_lic_no' => 'wic_cor_lic_no',
|
||||||
|
'wic_cor_birthpl' => 'wic_cor_birthpl',
|
||||||
|
'wic_cor_birthdt' => 'wic_cor_birthdt',
|
||||||
|
'wic_cor_rel' => 'wic_cor_rel',
|
||||||
|
'wic_party_rel' => 'wic_party_rel',
|
||||||
|
'wic_amount' => 'wic_amount',
|
||||||
|
'wic_amount_type' => 'wic_amount_type',
|
||||||
|
'wic_amtbnk_name' => 'wic_amtbnk_name',
|
||||||
|
'wic_amtbnk_cunm' => 'wic_amtbnk_cunm',
|
||||||
|
'wic_fund_source' => 'wic_fund_source',
|
||||||
|
'wic_fund_use' => 'wic_fund_use',
|
||||||
|
'dr_cr_marker' => 'dr_cr_marker',
|
||||||
|
'charge_code' => 'charge_code',
|
||||||
|
'chrg_amt_local' => 'chrg_amt_local',
|
||||||
|
'charge_category' => 'charge_category',
|
||||||
|
'charge_account' => 'charge_account',
|
||||||
|
'amount_fcy_1' => 'amount_fcy_1',
|
||||||
|
'rate_1' => 'rate_1',
|
||||||
|
'deal_rate' => 'deal_rate',
|
||||||
|
'l_wic_id' => 'l_wic_id',
|
||||||
|
'account_1_co_code' => 'account_1_co_code',
|
||||||
|
'account_2_co_code' => 'account_2_co_code',
|
||||||
|
'l_charge_amt' => 'l_charge_amt',
|
||||||
|
'bl_cust_no' => 'bl_cust_no',
|
||||||
|
'stmt_no' => 'stmt_no',
|
||||||
|
'bil_customer' => 'bil_customer',
|
||||||
|
'value_date_2' => 'value_date_2',
|
||||||
|
'cheq_type' => 'cheq_type',
|
||||||
|
'cheque_number' => 'cheque_number',
|
||||||
|
'inputter' => 'inputter',
|
||||||
|
'authoriser' => 'authoriser',
|
||||||
|
'bil_product' => 'bil_product',
|
||||||
|
'fx_document' => 'fx_document',
|
||||||
|
'fx_purpose' => 'fx_purpose',
|
||||||
|
'narrative_2' => 'narrative_2',
|
||||||
|
'customer_2' => 'customer_2',
|
||||||
|
'l_sms_1' => 'l_sms_1',
|
||||||
|
'l_phone_1' => 'l_phone_1',
|
||||||
|
'kyc_incom_rng' => 'kyc_incom_rng',
|
||||||
|
'wic_rt' => 'wic_rt',
|
||||||
|
'wic_rw' => 'wic_rw',
|
||||||
|
'ktp_kelurahan' => 'ktp_kelurahan',
|
||||||
|
'ktp_kecamatan' => 'ktp_kecamatan',
|
||||||
|
'ktp_provinsi' => 'ktp_provinsi',
|
||||||
|
'wic_jenis_kelam' => 'wic_jenis_kelam',
|
||||||
|
'kyc_sumber_dana' => 'kyc_sumber_dana',
|
||||||
|
'l_manual_risk' => 'l_manual_risk',
|
||||||
|
'l_slip_no' => 'l_slip_no',
|
||||||
|
'dest_bank_id' => 'dest_bank_id',
|
||||||
|
'dr_narrative' => 'dr_narrative',
|
||||||
|
'inter_type' => 'inter_type',
|
||||||
|
'l_va_number' => 'l_va_number',
|
||||||
|
'inter_bank_id' => 'inter_bank_id',
|
||||||
|
'term_narr' => 'term_narr',
|
||||||
|
'currency_2' => 'currency_2',
|
||||||
|
'amount_fcy_2' => 'amount_fcy_2',
|
||||||
|
'rate_2' => 'rate_2',
|
||||||
|
'customer_1' => 'customer_1',
|
||||||
|
'last_version' => 'last_version',
|
||||||
|
'dealer_desk' => 'dealer_desk',
|
||||||
|
];
|
||||||
|
|
||||||
|
private string $period = '';
|
||||||
|
private int $processedCount = 0;
|
||||||
|
private int $errorCount = 0;
|
||||||
|
private array $tellerBatch = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 teller data processing');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->processPeriod();
|
||||||
|
$this->logJobCompletion();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('Error in ProcessTellerDataJob: ' . $e->getMessage());
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function initializeJob()
|
||||||
|
: void
|
||||||
|
{
|
||||||
|
set_time_limit(self::MAX_EXECUTION_TIME);
|
||||||
|
$this->processedCount = 0;
|
||||||
|
$this->errorCount = 0;
|
||||||
|
$this->tellerBatch = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
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 teller 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;
|
||||||
|
$chunkCount = 0;
|
||||||
|
|
||||||
|
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||||
|
$rowCount++;
|
||||||
|
$this->processRow($headerRow, $row, $rowCount, $filePath);
|
||||||
|
|
||||||
|
// 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)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process any remaining records
|
||||||
|
if (!empty($this->tellerBatch)) {
|
||||||
|
$this->saveBatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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_teller'] === 'id') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Add timestamps
|
||||||
|
$now = now();
|
||||||
|
$data['created_at'] = $now;
|
||||||
|
$data['updated_at'] = $now;
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
$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
|
<?php
|
||||||
|
|
||||||
namespace Modules\Webstatement\Jobs;
|
namespace Modules\Webstatement\Jobs;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Modules\Webstatement\Models\TempTransaction;
|
use Modules\Webstatement\Models\TempTransaction;
|
||||||
|
|
||||||
class ProcessTransactionDataJob implements ShouldQueue
|
class ProcessTransactionDataJob implements ShouldQueue
|
||||||
{
|
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
||||||
|
|
||||||
protected $periods;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new job instance.
|
|
||||||
*/
|
|
||||||
public function __construct(array $periods = [])
|
|
||||||
{
|
{
|
||||||
$this->periods = $periods;
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
private const CSV_DELIMITER = '~';
|
||||||
* Execute the job.
|
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||||
*/
|
private const FILENAME = 'ST.TRANSACTION.csv';
|
||||||
public function handle(): void
|
private const DISK_NAME = 'sftpStatement';
|
||||||
{
|
|
||||||
try {
|
|
||||||
set_time_limit(24 * 60 * 60);
|
|
||||||
$disk = Storage::disk('sftpStatement');
|
|
||||||
$processedCount = 0;
|
|
||||||
$errorCount = 0;
|
|
||||||
|
|
||||||
if (empty($this->periods)) {
|
private string $period = '';
|
||||||
Log::warning('No periods provided for transaction data processing');
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->periods as $period) {
|
$tempFilePath = $this->createTemporaryFile($disk, $filePath);
|
||||||
// Skip the _parameter folder
|
$this->processFile($tempFilePath, $filePath);
|
||||||
/*if ($period === '_parameter') {
|
$this->cleanup($tempFilePath);
|
||||||
Log::info("Skipping _parameter folder");
|
}
|
||||||
continue;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
// Construct the filename based on the period folder name
|
private function validateFile($disk, string $filePath)
|
||||||
$filename = "ST.TRANSACTION.csv";
|
: bool
|
||||||
$filePath = "$period/$filename";
|
{
|
||||||
|
Log::info("Processing transaction file: $filePath");
|
||||||
|
|
||||||
Log::info("Processing transaction file: $filePath");
|
if (!$disk->exists($filePath)) {
|
||||||
|
Log::warning("File not found: $filePath");
|
||||||
if (!$disk->exists($filePath)) {
|
return false;
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log::info("Transaction data processing completed. Total processed: $processedCount, Total errors: $errorCount");
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
} catch (Exception $e) {
|
private function createTemporaryFile($disk, string $filePath)
|
||||||
Log::error('Error in ProcessTransactionDataJob: ' . $e->getMessage());
|
: string
|
||||||
throw $e;
|
{
|
||||||
|
$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 Exception;
|
||||||
use Illuminate\Bus\Queueable;
|
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\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
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
|
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
|
* @param string $accountNumber
|
||||||
* @return array|null
|
* @return array|null
|
||||||
@@ -85,10 +86,26 @@ class UpdateAtmCardBranchCurrencyJob implements ShouldQueue
|
|||||||
private function getAccountInfo(string $accountNumber): ?array
|
private function getAccountInfo(string $accountNumber): ?array
|
||||||
{
|
{
|
||||||
try {
|
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;
|
$url = env('FIORANO_URL') . self::API_BASE_PATH;
|
||||||
$path = self::API_INQUIRY_PATH;
|
$path = self::API_INQUIRY_PATH;
|
||||||
$data = [
|
$data = [
|
||||||
'accountNo' => $accountNumber
|
'accountNo' => $accountNumber,
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = Http::post($url . $path, $data);
|
$response = Http::post($url . $path, $data);
|
||||||
@@ -110,6 +127,7 @@ class UpdateAtmCardBranchCurrencyJob implements ShouldQueue
|
|||||||
$cardData = [
|
$cardData = [
|
||||||
'branch' => !empty($accountInfo['acctCompany']) ? $accountInfo['acctCompany'] : null,
|
'branch' => !empty($accountInfo['acctCompany']) ? $accountInfo['acctCompany'] : null,
|
||||||
'currency' => !empty($accountInfo['acctCurrency']) ? $accountInfo['acctCurrency'] : null,
|
'currency' => !empty($accountInfo['acctCurrency']) ? $accountInfo['acctCurrency'] : null,
|
||||||
|
'product_code' => !empty($accountInfo['acctType']) ? $accountInfo['acctType'] : null,
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->card->update($cardData);
|
$this->card->update($cardData);
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Modules\Webstatement\Mail;
|
namespace Modules\Webstatement\Mail;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Exception;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Mail\Mailable;
|
use Illuminate\Mail\Mailable;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Config;
|
||||||
|
|
||||||
|
use Modules\Webstatement\Models\Account;
|
||||||
use Modules\Webstatement\Models\PrintStatementLog;
|
use Modules\Webstatement\Models\PrintStatementLog;
|
||||||
|
use Symfony\Component\Mailer\Mailer;
|
||||||
|
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class StatementEmail extends Mailable
|
class StatementEmail extends Mailable
|
||||||
{
|
{
|
||||||
@@ -14,9 +22,11 @@
|
|||||||
protected $statement;
|
protected $statement;
|
||||||
protected $filePath;
|
protected $filePath;
|
||||||
protected $isZip;
|
protected $isZip;
|
||||||
|
protected $message;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new message instance.
|
* Create a new message instance.
|
||||||
|
* Membuat instance email baru untuk pengiriman statement
|
||||||
*
|
*
|
||||||
* @param PrintStatementLog $statement
|
* @param PrintStatementLog $statement
|
||||||
* @param string $filePath
|
* @param string $filePath
|
||||||
@@ -29,43 +39,164 @@
|
|||||||
$this->isZip = $isZip;
|
$this->isZip = $isZip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override the send method to use EsmtpTransport directly
|
||||||
|
* Using the working configuration from Python script with multiple fallback methods
|
||||||
|
*/
|
||||||
|
public function send($mailer)
|
||||||
|
{
|
||||||
|
// Get mail configuration
|
||||||
|
$host = Config::get('mail.mailers.smtp.host');
|
||||||
|
$port = Config::get('mail.mailers.smtp.port');
|
||||||
|
$username = Config::get('mail.mailers.smtp.username');
|
||||||
|
$password = Config::get('mail.mailers.smtp.password');
|
||||||
|
|
||||||
|
Log::info('StatementEmail: Attempting to send email with multiple fallback methods');
|
||||||
|
|
||||||
|
// Define connection methods like in Python script
|
||||||
|
$method =
|
||||||
|
// Method 3: STARTTLS with original port
|
||||||
|
[
|
||||||
|
'port' => $port,
|
||||||
|
'ssl' => false,
|
||||||
|
'name' => 'STARTTLS (Port $port)'
|
||||||
|
];
|
||||||
|
|
||||||
|
$lastException = null;
|
||||||
|
|
||||||
|
// Try each connection method until one succeeds
|
||||||
|
try {
|
||||||
|
Log::info('StatementEmail: Trying ' . $method['name']);
|
||||||
|
|
||||||
|
// Create EsmtpTransport with current method
|
||||||
|
$transport = new EsmtpTransport($host, $method['port'], $method['ssl']);
|
||||||
|
|
||||||
|
// Set username and password
|
||||||
|
if ($username) {
|
||||||
|
$transport->setUsername($username);
|
||||||
|
}
|
||||||
|
if ($password) {
|
||||||
|
$transport->setPassword($password);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable SSL verification for development
|
||||||
|
$streamOptions = [
|
||||||
|
'ssl' => [
|
||||||
|
'verify_peer' => false,
|
||||||
|
'verify_peer_name' => false,
|
||||||
|
'allow_self_signed' => true
|
||||||
|
]
|
||||||
|
];
|
||||||
|
$transport->getStream()->setStreamOptions($streamOptions);
|
||||||
|
|
||||||
|
// Build the email content
|
||||||
|
$this->build();
|
||||||
|
|
||||||
|
// Start transport connection
|
||||||
|
$transport->start();
|
||||||
|
|
||||||
|
// Create Symfony mailer
|
||||||
|
$symfonyMailer = new Mailer($transport);
|
||||||
|
|
||||||
|
// Convert Laravel message to Symfony Email
|
||||||
|
$email = $this->toSymfonyEmail();
|
||||||
|
|
||||||
|
// Send the email
|
||||||
|
$symfonyMailer->send($email);
|
||||||
|
|
||||||
|
// Close connection
|
||||||
|
$transport->stop();
|
||||||
|
|
||||||
|
Log::info('StatementEmail: Successfully sent email using ' . $method['name']);
|
||||||
|
return $this;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$lastException = $e;
|
||||||
|
Log::warning('StatementEmail: Failed to send with ' . $method['name'] . ': ' . $e->getMessage());
|
||||||
|
// Continue to next method
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return parent::send($mailer);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('StatementEmail: Laravel mailer also failed: ' . $e->getMessage());
|
||||||
|
// If we got here, throw the last exception from our custom methods
|
||||||
|
throw $lastException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the message.
|
* Build the message.
|
||||||
|
* Membangun struktur email dengan attachment statement
|
||||||
*
|
*
|
||||||
* @return $this
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function build()
|
public function build()
|
||||||
{
|
{
|
||||||
$subject = 'Your Account Statement';
|
$subject = 'Statement Rekening Bank Artha Graha Internasional';
|
||||||
|
|
||||||
if ($this->statement->is_period_range) {
|
if ($this->statement->is_period_range) {
|
||||||
$subject .= " - {$this->statement->period_from} to {$this->statement->period_to}";
|
$subject .= " - {$this->statement->period_from} to {$this->statement->period_to}";
|
||||||
} else {
|
} else {
|
||||||
$subject .= " - {$this->statement->period_from}";
|
$subject .= " - " . Carbon::createFromFormat('Ym', $this->statement->period_from)
|
||||||
|
->locale('id')
|
||||||
|
->isoFormat('MMMM Y');
|
||||||
}
|
}
|
||||||
|
|
||||||
$email = $this->subject($subject)
|
$email = $this->subject($subject);
|
||||||
->view('webstatement::statements.email')
|
|
||||||
->with([
|
|
||||||
'statement' => $this->statement,
|
|
||||||
'accountNumber' => $this->statement->account_number,
|
|
||||||
'periodFrom' => $this->statement->period_from,
|
|
||||||
'periodTo' => $this->statement->period_to,
|
|
||||||
'isRange' => $this->statement->is_period_range,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($this->isZip) {
|
// Store the email in the message property for later use in toSymfonyEmail()
|
||||||
$fileName = "{$this->statement->account_number}_{$this->statement->period_from}_to_{$this->statement->period_to}.zip";
|
$this->message = $email;
|
||||||
$email->attach($this->filePath, [
|
|
||||||
'as' => $fileName,
|
return $email;
|
||||||
'mime' => 'application/zip',
|
}
|
||||||
]);
|
|
||||||
} else {
|
/**
|
||||||
$fileName = "{$this->statement->account_number}_{$this->statement->period_from}.pdf";
|
* Convert Laravel message to Symfony Email
|
||||||
$email->attach($this->filePath, [
|
*/
|
||||||
'as' => $fileName,
|
protected function toSymfonyEmail()
|
||||||
'mime' => 'application/pdf',
|
{
|
||||||
]);
|
// 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;
|
return $email;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace Modules\Webstatement\Models;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Modules\Basicdata\Models\Branch;
|
||||||
// use Modules\Webstatement\Database\Factories\AccountFactory;
|
// use Modules\Webstatement\Database\Factories\AccountFactory;
|
||||||
|
|
||||||
class Account extends Model
|
class Account extends Model
|
||||||
@@ -34,4 +35,27 @@ class Account extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(Customer::class, 'customer_code', 'customer_code');
|
return $this->belongsTo(Customer::class, 'customer_code', 'customer_code');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all balances for this account.
|
||||||
|
*/
|
||||||
|
public function balances()
|
||||||
|
{
|
||||||
|
return $this->hasMany(AccountBalance::class, 'account_number', 'account_number');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get balance for a specific period.
|
||||||
|
*
|
||||||
|
* @param string $period Format: YYYY-MM
|
||||||
|
* @return AccountBalance|null
|
||||||
|
*/
|
||||||
|
public function getBalanceForPeriod($period)
|
||||||
|
{
|
||||||
|
return $this->balances()->where('period', $period)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function branch(){
|
||||||
|
return $this->belongsTo(Branch::class, 'branch_code','code');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
app/Models/AccountBalance.php
Normal file
59
app/Models/AccountBalance.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
||||||
|
class AccountBalance extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'account_number',
|
||||||
|
'period',
|
||||||
|
'actual_balance',
|
||||||
|
'cleared_balance',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the account that owns the balance.
|
||||||
|
*/
|
||||||
|
public function account()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Account::class, 'account_number', 'account_number');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope a query to filter by account number.
|
||||||
|
*/
|
||||||
|
public function scopeForAccount($query, $accountNumber)
|
||||||
|
{
|
||||||
|
return $query->where('account_number', $accountNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope a query to filter by period.
|
||||||
|
*/
|
||||||
|
public function scopeForPeriod($query, $period)
|
||||||
|
{
|
||||||
|
return $query->where('period', $period);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get balance for a specific account and period.
|
||||||
|
*
|
||||||
|
* @param string $accountNumber
|
||||||
|
* @param string $period Format: YYYY-MM
|
||||||
|
* @return AccountBalance|null
|
||||||
|
*/
|
||||||
|
public static function getBalance($accountNumber, $period)
|
||||||
|
{
|
||||||
|
return self::where('account_number', $accountNumber)
|
||||||
|
->where('period', $period)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/Models/AtmTransaction.php
Normal file
33
app/Models/AtmTransaction.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class AtmTransaction extends Model
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'transaction_id',
|
||||||
|
'card_acc_id',
|
||||||
|
'pan_number',
|
||||||
|
'txn_type',
|
||||||
|
'merchant_id',
|
||||||
|
'txn_amount',
|
||||||
|
'booking_date',
|
||||||
|
'trans_ref',
|
||||||
|
'retrieval_ref_no',
|
||||||
|
'stmt_nos',
|
||||||
|
'debit_acct_no',
|
||||||
|
'credit_acct_no',
|
||||||
|
'chrg_amount',
|
||||||
|
'value_date',
|
||||||
|
'stan_no',
|
||||||
|
'trans_status',
|
||||||
|
'proc_code',
|
||||||
|
];
|
||||||
|
}
|
||||||
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\Model;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
// use Modules\Webstatement\Database\Factories\AtmcardFactory;
|
// use Modules\Webstatement\Database\Factories\AtmcardFactory;
|
||||||
|
|
||||||
class Atmcard extends Model
|
class Atmcard extends Model
|
||||||
@@ -15,7 +16,64 @@ class Atmcard extends Model
|
|||||||
*/
|
*/
|
||||||
protected $guarded = ['id'];
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relasi ke tabel JenisKartu untuk mendapatkan informasi biaya kartu
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
*/
|
||||||
public function biaya(){
|
public function biaya(){
|
||||||
|
Log::info('Mengakses relasi biaya untuk ATM card', ['card_id' => $this->id]);
|
||||||
return $this->belongsTo(JenisKartu::class,'ctdesc','code');
|
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 Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Spatie\Activitylog\LogOptions;
|
use Spatie\Activitylog\LogOptions;
|
||||||
use Spatie\Activitylog\Traits\LogsActivity;
|
use Spatie\Activitylog\Traits\LogsActivity;
|
||||||
use Wildside\Userstamps\Userstamps;
|
use Mattiverse\Userstamps\Traits\Userstamps;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
24
app/Models/Category.php
Normal file
24
app/Models/Category.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
||||||
|
class Category extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'id_category',
|
||||||
|
'date_time',
|
||||||
|
'description',
|
||||||
|
'short_name',
|
||||||
|
'system_ind',
|
||||||
|
'record_status',
|
||||||
|
'co_code',
|
||||||
|
'curr_no',
|
||||||
|
'l_db_cr_ind',
|
||||||
|
'category_code'
|
||||||
|
];
|
||||||
|
}
|
||||||
75
app/Models/ClosingBalanceReportLog.php
Normal file
75
app/Models/ClosingBalanceReportLog.php
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Modules\Usermanagement\Models\User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model untuk menyimpan log permintaan laporan closing balance
|
||||||
|
* Menyimpan informasi status, file path, dan tracking user
|
||||||
|
*/
|
||||||
|
class ClosingBalanceReportLog extends Model
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'account_number',
|
||||||
|
'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',
|
||||||
|
'remarks',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,9 +21,16 @@ class Customer extends Model
|
|||||||
'postal_code',
|
'postal_code',
|
||||||
'branch_code',
|
'branch_code',
|
||||||
'date_of_birth',
|
'date_of_birth',
|
||||||
'email'
|
'email',
|
||||||
|
'sector',
|
||||||
|
'customer_type',
|
||||||
|
'birth_incorp_date',
|
||||||
|
'home_rt',
|
||||||
|
'home_rw',
|
||||||
|
'ktp_rt',
|
||||||
|
'ktp_rw',
|
||||||
|
'local_ref'
|
||||||
];
|
];
|
||||||
|
|
||||||
public function accounts(){
|
public function accounts(){
|
||||||
return $this->hasMany(Account::class, 'customer_code', 'customer_code');
|
return $this->hasMany(Account::class, 'customer_code', 'customer_code');
|
||||||
}
|
}
|
||||||
|
|||||||
64
app/Models/DataCapture.php
Normal file
64
app/Models/DataCapture.php
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class DataCapture extends Model
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Indicates if the model's ID is auto-incrementing.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $incrementing = false;
|
||||||
|
protected $table = 'data_captures';
|
||||||
|
/**
|
||||||
|
* The data type of the auto-incrementing ID.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $keyType = 'string';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'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'
|
||||||
|
];
|
||||||
|
}
|
||||||
52
app/Models/FtTxnTypeCondition.php
Normal file
52
app/Models/FtTxnTypeCondition.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
// use Modules\Webstatement\Database\Factories\FtTxnTypeConditionFactory;
|
||||||
|
|
||||||
|
class FtTxnTypeCondition extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the IDs are auto-incrementing.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $incrementing = false;
|
||||||
|
/**
|
||||||
|
* The table associated with the model.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $table = 'ft_txn_type_condition';
|
||||||
|
/**
|
||||||
|
* The primary key for the model.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $primaryKey = 'id';
|
||||||
|
/**
|
||||||
|
* The data type of the auto-incrementing ID.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $keyType = 'string';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'id',
|
||||||
|
'date_time',
|
||||||
|
'transaction_type',
|
||||||
|
'short_descr',
|
||||||
|
'txn_code_cr',
|
||||||
|
'txn_code_dr',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,186 +1,300 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Modules\Webstatement\Models;
|
namespace Modules\Webstatement\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Modules\Basicdata\Models\Branch;
|
use Modules\Basicdata\Models\Branch;
|
||||||
use Modules\Usermanagement\Models\User;
|
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',
|
||||||
|
'stmt_sent_type',
|
||||||
|
'is_generated',
|
||||||
|
'password', // Tambahan field password
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_period_range' => 'boolean',
|
||||||
|
'is_available' => 'boolean',
|
||||||
|
'is_generated' => 'boolean',
|
||||||
|
'is_downloaded' => 'boolean',
|
||||||
|
'downloaded_at' => 'datetime',
|
||||||
|
'authorized_at' => 'datetime',
|
||||||
|
'started_at' => 'datetime',
|
||||||
|
'completed_at' => 'datetime',
|
||||||
|
'target_accounts' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $hidden = [
|
||||||
|
'password', // Hide password dari serialization
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the formatted period display
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getPeriodDisplayAttribute()
|
||||||
{
|
{
|
||||||
use HasFactory, SoftDeletes;
|
if ($this->is_period_range) {
|
||||||
|
return $this->formatPeriod($this->period_from) . ' - ' . $this->formatPeriod($this->period_to);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function account(){
|
||||||
|
return $this->belongsTo(Account::class, 'account_number','account_number');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
22
app/Models/ProcessedStatement.php
Normal file
22
app/Models/ProcessedStatement.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ProcessedStatement extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'account_number',
|
||||||
|
'period',
|
||||||
|
'sequence_no',
|
||||||
|
'transaction_date',
|
||||||
|
'reference_number',
|
||||||
|
'transaction_amount',
|
||||||
|
'transaction_type',
|
||||||
|
'description',
|
||||||
|
'end_balance',
|
||||||
|
'actual_date',
|
||||||
|
'recipt_no'
|
||||||
|
];
|
||||||
|
}
|
||||||
161
app/Models/ProvinceCore.php
Normal file
161
app/Models/ProvinceCore.php
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class ProvinceCore extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nama tabel yang digunakan oleh model
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $table = 'province_core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field yang dapat diisi secara mass assignment
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'code',
|
||||||
|
'name',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field yang di-cast ke tipe data tertentu
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $casts = [
|
||||||
|
'created_at' => 'datetime',
|
||||||
|
'updated_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope untuk mencari berdasarkan kode provinsi
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||||
|
* @param string $code
|
||||||
|
* @return \Illuminate\Database\Eloquent\Builder
|
||||||
|
*/
|
||||||
|
public function scopeByCode($query, $code)
|
||||||
|
{
|
||||||
|
Log::info('ProvinceCore: Mencari provinsi dengan kode: ' . $code);
|
||||||
|
return $query->where('code', $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope untuk mencari berdasarkan nama provinsi
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||||
|
* @param string $name
|
||||||
|
* @return \Illuminate\Database\Eloquent\Builder
|
||||||
|
*/
|
||||||
|
public function scopeByName($query, $name)
|
||||||
|
{
|
||||||
|
Log::info('ProvinceCore: Mencari provinsi dengan nama: ' . $name);
|
||||||
|
return $query->where('name', 'ILIKE', '%' . $name . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope untuk mendapatkan semua provinsi yang aktif
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||||
|
* @return \Illuminate\Database\Eloquent\Builder
|
||||||
|
*/
|
||||||
|
public function scopeActive($query)
|
||||||
|
{
|
||||||
|
Log::info('ProvinceCore: Mengambil semua provinsi aktif');
|
||||||
|
return $query->orderBy('name', 'asc');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mendapatkan provinsi berdasarkan kode
|
||||||
|
*
|
||||||
|
* @param string $code
|
||||||
|
* @return ProvinceCore|null
|
||||||
|
*/
|
||||||
|
public static function getByCode($code)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Log::info('ProvinceCore: Mengambil provinsi dengan kode: ' . $code);
|
||||||
|
return self::byCode($code)->first();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('ProvinceCore: Error mengambil provinsi dengan kode ' . $code . ': ' . $e->getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mendapatkan semua provinsi untuk dropdown/select
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Collection
|
||||||
|
*/
|
||||||
|
public static function getForDropdown()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Log::info('ProvinceCore: Mengambil data provinsi untuk dropdown');
|
||||||
|
return self::active()->pluck('name', 'code');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('ProvinceCore: Error mengambil data dropdown provinsi: ' . $e->getMessage());
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validasi kode provinsi
|
||||||
|
*
|
||||||
|
* @param string $code
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function isValidCode($code)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Log::info('ProvinceCore: Validasi kode provinsi: ' . $code);
|
||||||
|
return self::byCode($code)->exists();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('ProvinceCore: Error validasi kode provinsi ' . $code . ': ' . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boot method untuk model events
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
static::creating(function ($model) {
|
||||||
|
Log::info('ProvinceCore: Membuat data provinsi baru dengan kode: ' . $model->code);
|
||||||
|
});
|
||||||
|
|
||||||
|
static::created(function ($model) {
|
||||||
|
Log::info('ProvinceCore: Data provinsi berhasil dibuat dengan ID: ' . $model->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
static::updating(function ($model) {
|
||||||
|
Log::info('ProvinceCore: Mengupdate data provinsi dengan ID: ' . $model->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
static::updated(function ($model) {
|
||||||
|
Log::info('ProvinceCore: Data provinsi berhasil diupdate dengan ID: ' . $model->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
static::deleting(function ($model) {
|
||||||
|
Log::info('ProvinceCore: Menghapus data provinsi dengan ID: ' . $model->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
static::deleted(function ($model) {
|
||||||
|
Log::info('ProvinceCore: Data provinsi berhasil dihapus dengan ID: ' . $model->id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
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',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -56,4 +56,24 @@ class StmtEntry extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(Account::class, 'account_number', 'account_number');
|
return $this->belongsTo(Account::class, 'account_number', 'account_number');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function ft(){
|
||||||
|
return $this->belongsTo(TempFundsTransfer::class, 'trans_reference', 'ref_no');
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
113
app/Models/StmtEntryDetail.php
Normal file
113
app/Models/StmtEntryDetail.php
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
||||||
|
class StmtEntryDetail extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The table associated with the model.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $table = 'stmt_entry_detail';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'stmt_entry_id',
|
||||||
|
'account_number',
|
||||||
|
'company_code',
|
||||||
|
'amount_lcy',
|
||||||
|
'transaction_code',
|
||||||
|
'narrative',
|
||||||
|
'product_category',
|
||||||
|
'value_date',
|
||||||
|
'amount_fcy',
|
||||||
|
'exchange_rate',
|
||||||
|
'trans_reference',
|
||||||
|
'booking_date',
|
||||||
|
'stmt_no',
|
||||||
|
'date_time',
|
||||||
|
'currency',
|
||||||
|
'crf_type',
|
||||||
|
'consol_key',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $casts = [
|
||||||
|
'created_at' => 'datetime',
|
||||||
|
'updated_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relasi ke model Account
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
*/
|
||||||
|
public function account()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Account::class, 'account_number', 'account_number');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relasi ke model TempFundsTransfer
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
*/
|
||||||
|
public function ft()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(TempFundsTransfer::class, 'trans_reference', 'ref_no');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relasi ke model TempTransaction
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
*/
|
||||||
|
public function transaction()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(TempTransaction::class, 'transaction_code', 'transaction_code');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relasi ke model Teller
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
*/
|
||||||
|
public function tt()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Teller::class, 'trans_reference', 'id_teller');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relasi ke model DataCapture
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
*/
|
||||||
|
public function dc()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(DataCapture::class, 'trans_reference', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relasi ke model TempArrangement
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
*/
|
||||||
|
public function aa()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(TempArrangement::class, 'trans_reference', 'arrangement_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
118
app/Models/Teller.php
Normal file
118
app/Models/Teller.php
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
||||||
|
class Teller extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'id_teller',
|
||||||
|
'account_1',
|
||||||
|
'currency_1',
|
||||||
|
'amount_local_1',
|
||||||
|
'value_date_1',
|
||||||
|
'account_2',
|
||||||
|
'new_cust_bal',
|
||||||
|
'term_type',
|
||||||
|
'term_id',
|
||||||
|
'trans_reff',
|
||||||
|
'card_no',
|
||||||
|
'recipt_no',
|
||||||
|
'transaction_code',
|
||||||
|
'date_time',
|
||||||
|
'record_status',
|
||||||
|
'amount_local_2',
|
||||||
|
'co_code',
|
||||||
|
'narrative_1',
|
||||||
|
'wic_flag',
|
||||||
|
'wic_cust_type',
|
||||||
|
'wic_full_name',
|
||||||
|
'wic_alias_name',
|
||||||
|
'wic_acct_no',
|
||||||
|
'wic_id_type',
|
||||||
|
'wic_id_no',
|
||||||
|
'wic_npwp',
|
||||||
|
'wic_nationality',
|
||||||
|
'wic_ind_birthpl',
|
||||||
|
'wic_ind_birthdt',
|
||||||
|
'wic_address_id',
|
||||||
|
'wic_address_cur',
|
||||||
|
'wic_city',
|
||||||
|
'wic_province',
|
||||||
|
'wic_post_code',
|
||||||
|
'wic_phone',
|
||||||
|
'wic_gender',
|
||||||
|
'wic_marital_sts',
|
||||||
|
'wic_occptn',
|
||||||
|
'wic_occptn_dur',
|
||||||
|
'wic_income_avg',
|
||||||
|
'wic_cor_name',
|
||||||
|
'wic_cor_address',
|
||||||
|
'wic_cor_phone',
|
||||||
|
'wic_cor_lgl_typ',
|
||||||
|
'wic_cor_lic_no',
|
||||||
|
'wic_cor_birthpl',
|
||||||
|
'wic_cor_birthdt',
|
||||||
|
'wic_cor_rel',
|
||||||
|
'wic_party_rel',
|
||||||
|
'wic_amount',
|
||||||
|
'wic_amount_type',
|
||||||
|
'wic_amtbnk_name',
|
||||||
|
'wic_amtbnk_cunm',
|
||||||
|
'wic_fund_source',
|
||||||
|
'wic_fund_use',
|
||||||
|
'dr_cr_marker',
|
||||||
|
'charge_code',
|
||||||
|
'chrg_amt_local',
|
||||||
|
'charge_category',
|
||||||
|
'charge_account',
|
||||||
|
'amount_fcy_1',
|
||||||
|
'rate_1',
|
||||||
|
'deal_rate',
|
||||||
|
'l_wic_id',
|
||||||
|
'account_1_co_code',
|
||||||
|
'account_2_co_code',
|
||||||
|
'l_charge_amt',
|
||||||
|
'bl_cust_no',
|
||||||
|
'stmt_no',
|
||||||
|
'bil_customer',
|
||||||
|
'value_date_2',
|
||||||
|
'cheq_type',
|
||||||
|
'cheque_number',
|
||||||
|
'inputter',
|
||||||
|
'authoriser',
|
||||||
|
'bil_product',
|
||||||
|
'fx_document',
|
||||||
|
'fx_purpose',
|
||||||
|
'narrative_2',
|
||||||
|
'customer_2',
|
||||||
|
'l_sms_1',
|
||||||
|
'l_phone_1',
|
||||||
|
'kyc_incom_rng',
|
||||||
|
'wic_rt',
|
||||||
|
'wic_rw',
|
||||||
|
'ktp_kelurahan',
|
||||||
|
'ktp_kecamatan',
|
||||||
|
'ktp_provinsi',
|
||||||
|
'wic_jenis_kelam',
|
||||||
|
'kyc_sumber_dana',
|
||||||
|
'l_manual_risk',
|
||||||
|
'l_slip_no',
|
||||||
|
'dest_bank_id',
|
||||||
|
'dr_narrative',
|
||||||
|
'inter_type',
|
||||||
|
'l_va_number',
|
||||||
|
'inter_bank_id',
|
||||||
|
'term_narr',
|
||||||
|
'currency_2',
|
||||||
|
'amount_fcy_2',
|
||||||
|
'rate_2',
|
||||||
|
'customer_1',
|
||||||
|
'last_version',
|
||||||
|
'dealer_desk'
|
||||||
|
];
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -2,13 +2,28 @@
|
|||||||
|
|
||||||
namespace Modules\Webstatement\Providers;
|
namespace Modules\Webstatement\Providers;
|
||||||
|
|
||||||
use Illuminate\Console\Scheduling\Schedule;
|
|
||||||
use Illuminate\Support\Facades\Blade;
|
use Illuminate\Support\Facades\Blade;
|
||||||
use Illuminate\Support\ServiceProvider;
|
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 Nwidart\Modules\Traits\PathNamespace;
|
||||||
|
use Illuminate\Console\Scheduling\Schedule;
|
||||||
|
use Modules\Webstatement\Console\{
|
||||||
|
UnlockPdf,
|
||||||
|
CombinePdf,
|
||||||
|
ConvertHtmlToPdf,
|
||||||
|
ExportDailyStatements,
|
||||||
|
ProcessDailyMigration,
|
||||||
|
ExportPeriodStatements,
|
||||||
|
UpdateAllAtmCardsCommand,
|
||||||
|
CheckEmailProgressCommand,
|
||||||
|
GenerateBiayakartuCommand,
|
||||||
|
SendStatementEmailCommand,
|
||||||
|
GenerateAtmTransactionReport,
|
||||||
|
GenerateBiayaKartuCsvCommand,
|
||||||
|
AutoSendStatementEmailCommand,
|
||||||
|
GenerateClosingBalanceReportCommand,
|
||||||
|
GenerateClosingBalanceReportBulkCommand,
|
||||||
|
};
|
||||||
|
use Modules\Webstatement\Jobs\UpdateAtmCardBranchCurrencyJob;
|
||||||
|
|
||||||
class WebstatementServiceProvider extends ServiceProvider
|
class WebstatementServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -52,7 +67,20 @@ class WebstatementServiceProvider extends ServiceProvider
|
|||||||
{
|
{
|
||||||
$this->commands([
|
$this->commands([
|
||||||
GenerateBiayakartuCommand::class,
|
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,
|
||||||
|
AutoSendStatementEmailCommand::class,
|
||||||
|
GenerateClosingBalanceReportCommand::class,
|
||||||
|
GenerateClosingBalanceReportBulkCommand::class,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +110,34 @@ class WebstatementServiceProvider extends ServiceProvider
|
|||||||
->appendOutputTo(storage_path('logs/biaya-kartu-csv-scheduler.log'));
|
->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'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
301
app/Services/PHPMailerService.php
Normal file
301
app/Services/PHPMailerService.php
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Webstatement\Services;
|
||||||
|
|
||||||
|
use PHPMailer\PHPMailer\PHPMailer;
|
||||||
|
use PHPMailer\PHPMailer\SMTP;
|
||||||
|
use PHPMailer\PHPMailer\Exception;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service untuk menangani pengiriman email menggunakan PHPMailer
|
||||||
|
* dengan dukungan autentikasi NTLM dan GSSAPI
|
||||||
|
*/
|
||||||
|
class PHPMailerService
|
||||||
|
{
|
||||||
|
protected $mailer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inisialisasi PHPMailer dengan konfigurasi NTLM/GSSAPI
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->mailer = new PHPMailer(true);
|
||||||
|
$this->configureSMTP();
|
||||||
|
|
||||||
|
Log::info('PHPMailerService initialized with NTLM/GSSAPI support');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konfigurasi SMTP dengan dukungan NTLM/GSSAPI dan fallback untuk development
|
||||||
|
*/
|
||||||
|
protected function configureSMTP(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Server settings
|
||||||
|
$this->mailer->isSMTP();
|
||||||
|
$this->mailer->Host = config('mail.mailers.phpmailer.host', env('MAIL_HOST'));
|
||||||
|
$this->mailer->Port = config('mail.mailers.phpmailer.port', env('MAIL_PORT', 587));
|
||||||
|
|
||||||
|
// Deteksi apakah perlu autentikasi berdasarkan username
|
||||||
|
$username = config('mail.mailers.phpmailer.username', env('MAIL_USERNAME'));
|
||||||
|
$password = config('mail.mailers.phpmailer.password', env('MAIL_PASSWORD'));
|
||||||
|
|
||||||
|
// Hanya aktifkan autentikasi jika username dan password tersedia
|
||||||
|
if (!empty($username) && $username !== 'null' && !empty($password) && $password !== 'null') {
|
||||||
|
$this->mailer->SMTPAuth = true;
|
||||||
|
$this->mailer->Username = $username;
|
||||||
|
$this->mailer->Password = $password;
|
||||||
|
|
||||||
|
Log::info('SMTP authentication enabled', [
|
||||||
|
'username' => $username,
|
||||||
|
'host' => $this->mailer->Host
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Dukungan NTLM/GSSAPI untuk production
|
||||||
|
$authType = config('mail.mailers.phpmailer.auth_type', env('MAIL_AUTH_TYPE', 'NTLM'));
|
||||||
|
|
||||||
|
if (strtoupper($authType) === 'NTLM') {
|
||||||
|
$this->mailer->AuthType = 'NTLM';
|
||||||
|
$this->mailer->Realm = config('mail.mailers.phpmailer.realm', env('MAIL_REALM', ''));
|
||||||
|
$this->mailer->Workstation = config('mail.mailers.phpmailer.workstation', env('MAIL_WORKSTATION', ''));
|
||||||
|
|
||||||
|
Log::info('NTLM authentication configured', [
|
||||||
|
'realm' => $this->mailer->Realm,
|
||||||
|
'workstation' => $this->mailer->Workstation
|
||||||
|
]);
|
||||||
|
} elseif (strtoupper($authType) === 'GSSAPI') {
|
||||||
|
$this->mailer->AuthType = 'XOAUTH2';
|
||||||
|
Log::info('GSSAPI authentication configured');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Untuk development server seperti Mailpit
|
||||||
|
$this->mailer->SMTPAuth = false;
|
||||||
|
|
||||||
|
Log::info('SMTP authentication disabled for development', [
|
||||||
|
'host' => $this->mailer->Host,
|
||||||
|
'port' => $this->mailer->Port
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encryption configuration
|
||||||
|
$encryption = config('mail.mailers.phpmailer.encryption', env('MAIL_ENCRYPTION'));
|
||||||
|
$port = $this->mailer->Port;
|
||||||
|
|
||||||
|
if (!empty($encryption) && $encryption !== 'null') {
|
||||||
|
if ($encryption === 'tls' && ($port == 587 || $port == 25)) {
|
||||||
|
$this->mailer->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
|
||||||
|
Log::info('Using STARTTLS encryption', ['port' => $port]);
|
||||||
|
} elseif ($encryption === 'ssl' && ($port == 465 || $port == 993)) {
|
||||||
|
$this->mailer->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
|
||||||
|
Log::info('Using SSL encryption', ['port' => $port]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Untuk development/testing server
|
||||||
|
$this->mailer->SMTPSecure = false;
|
||||||
|
$this->mailer->SMTPAutoTLS = false;
|
||||||
|
Log::info('Using no encryption (plain text)', ['port' => $port]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tambahan konfigurasi untuk kompatibilitas
|
||||||
|
$this->mailer->SMTPOptions = array(
|
||||||
|
'ssl' => array(
|
||||||
|
'verify_peer' => false,
|
||||||
|
'verify_peer_name' => false,
|
||||||
|
'allow_self_signed' => true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- TAMBAHKAN BAGIAN INI UNTUK MENGABAIKAN VALIDASI SERTIFIKAT ---
|
||||||
|
if (isset($config['ignore_certificate_errors']) && $config['ignore_certificate_errors']) {
|
||||||
|
$this->mailer->SMTPOptions = [
|
||||||
|
'ssl' => [
|
||||||
|
'verify_peer' => false,
|
||||||
|
'verify_peer_name' => false,
|
||||||
|
'allow_self_signed' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// --- AKHIR TAMBAHAN ---
|
||||||
|
|
||||||
|
// Debug mode
|
||||||
|
if (config('app.debug')) {
|
||||||
|
$this->mailer->SMTPDebug = SMTP::DEBUG_SERVER;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeout settings
|
||||||
|
$this->mailer->Timeout = config('mail.mailers.phpmailer.timeout', 30);
|
||||||
|
$this->mailer->SMTPKeepAlive = true;
|
||||||
|
|
||||||
|
Log::info('SMTP configured successfully', [
|
||||||
|
'host' => $this->mailer->Host,
|
||||||
|
'port' => $this->mailer->Port,
|
||||||
|
'auth_enabled' => $this->mailer->SMTPAuth,
|
||||||
|
'encryption' => $encryption ?: 'none',
|
||||||
|
'smtp_secure' => $this->mailer->SMTPSecure ?: 'none'
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('Failed to configure SMTP', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kirim email dengan attachment
|
||||||
|
*
|
||||||
|
* @param string $to Email tujuan
|
||||||
|
* @param string $subject Subjek email
|
||||||
|
* @param string $body Body email (HTML)
|
||||||
|
* @param string|null $attachmentPath Path file attachment
|
||||||
|
* @param string|null $attachmentName Nama file attachment
|
||||||
|
* @param string|null $mimeType MIME type attachment
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Kirim email dengan handling khusus untuk development dan production
|
||||||
|
*
|
||||||
|
* @param string $to Email tujuan
|
||||||
|
* @param string $subject Subjek email
|
||||||
|
* @param string $body Body email (HTML)
|
||||||
|
* @param string|null $attachmentPath Path file attachment
|
||||||
|
* @param string|null $attachmentName Nama file attachment
|
||||||
|
* @param string|null $mimeType MIME type attachment
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function sendEmail(
|
||||||
|
string $to,
|
||||||
|
string $subject,
|
||||||
|
string $body,
|
||||||
|
?string $attachmentPath = null,
|
||||||
|
?string $attachmentName = null,
|
||||||
|
?string $mimeType = 'application/pdf'
|
||||||
|
): bool {
|
||||||
|
try {
|
||||||
|
// Reset recipients dan attachments
|
||||||
|
$this->mailer->clearAddresses();
|
||||||
|
$this->mailer->clearAttachments();
|
||||||
|
|
||||||
|
// Set sender
|
||||||
|
$fromAddress = config('mail.from.address', env('MAIL_FROM_ADDRESS'));
|
||||||
|
$fromName = config('mail.from.name', env('MAIL_FROM_NAME'));
|
||||||
|
|
||||||
|
if (!empty($fromAddress)) {
|
||||||
|
$this->mailer->setFrom($fromAddress, $fromName);
|
||||||
|
} else {
|
||||||
|
// Fallback untuk development
|
||||||
|
$this->mailer->setFrom('noreply@localhost', 'Development Server');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add recipient
|
||||||
|
$this->mailer->addAddress($to);
|
||||||
|
|
||||||
|
// Content
|
||||||
|
$this->mailer->isHTML(true);
|
||||||
|
$this->mailer->Subject = $subject;
|
||||||
|
$this->mailer->Body = $body;
|
||||||
|
$this->mailer->AltBody = strip_tags($body);
|
||||||
|
|
||||||
|
// Attachment
|
||||||
|
if ($attachmentPath && file_exists($attachmentPath)) {
|
||||||
|
$this->mailer->addAttachment(
|
||||||
|
$attachmentPath,
|
||||||
|
$attachmentName ?: basename($attachmentPath),
|
||||||
|
'base64',
|
||||||
|
$mimeType
|
||||||
|
);
|
||||||
|
|
||||||
|
Log::info('Attachment added to email', [
|
||||||
|
'path' => $attachmentPath,
|
||||||
|
'name' => $attachmentName,
|
||||||
|
'mime_type' => $mimeType,
|
||||||
|
'file_size' => filesize($attachmentPath)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to send
|
||||||
|
$result = $this->mailer->send();
|
||||||
|
|
||||||
|
Log::info('Email sent successfully via PHPMailer', [
|
||||||
|
'to' => $to,
|
||||||
|
'subject' => $subject,
|
||||||
|
'has_attachment' => !is_null($attachmentPath),
|
||||||
|
'host' => $this->mailer->Host,
|
||||||
|
'port' => $this->mailer->Port,
|
||||||
|
'auth_enabled' => $this->mailer->SMTPAuth
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('Failed to send email via PHPMailer', [
|
||||||
|
'to' => $to,
|
||||||
|
'subject' => $subject,
|
||||||
|
'host' => $this->mailer->Host,
|
||||||
|
'port' => $this->mailer->Port,
|
||||||
|
'auth_enabled' => $this->mailer->SMTPAuth,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'error_code' => $e->getCode(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test koneksi SMTP dengan fallback encryption
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function testConnection(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Coba koneksi dengan konfigurasi saat ini
|
||||||
|
$this->mailer->smtpConnect();
|
||||||
|
$this->mailer->smtpClose();
|
||||||
|
|
||||||
|
Log::info('SMTP connection test successful with current config');
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::warning('SMTP connection failed, trying fallback', [
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Fallback: coba tanpa encryption
|
||||||
|
try {
|
||||||
|
$this->mailer->SMTPSecure = false;
|
||||||
|
$this->mailer->SMTPAutoTLS = false;
|
||||||
|
|
||||||
|
$this->mailer->smtpConnect();
|
||||||
|
$this->mailer->smtpClose();
|
||||||
|
|
||||||
|
Log::info('SMTP connection successful with fallback (no encryption)');
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (Exception $fallbackError) {
|
||||||
|
Log::error('SMTP connection test failed completely', [
|
||||||
|
'original_error' => $e->getMessage(),
|
||||||
|
'fallback_error' => $fallbackError->getMessage()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dapatkan instance PHPMailer
|
||||||
|
*
|
||||||
|
* @return PHPMailer
|
||||||
|
*/
|
||||||
|
public function getMailer(): PHPMailer
|
||||||
|
{
|
||||||
|
return $this->mailer;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,4 +2,7 @@
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'name' => 'Webstatement',
|
'name' => 'Webstatement',
|
||||||
|
|
||||||
|
// ZIP file password configuration
|
||||||
|
'zip_password' => env('WEBSTATEMENT_ZIP_PASSWORD', 'statement123'),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?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('ft_txn_type_condition', function (Blueprint $table) {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->dateTime('date_time')->nullable();
|
||||||
|
$table->string('transaction_type')->nullable();
|
||||||
|
$table->string('short_descr')->nullable();
|
||||||
|
$table->string('txn_code_cr')->nullable();
|
||||||
|
$table->string('txn_code_dr')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('ft_txn_type_condition');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?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('data_captures', function (Blueprint $table) {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->string('account_number')->nullable();
|
||||||
|
$table->string('sign')->nullable();
|
||||||
|
$table->decimal('amount_lcy', 20, 2)->nullable();
|
||||||
|
$table->string('transaction_code')->nullable();
|
||||||
|
$table->string('their_reference')->nullable();
|
||||||
|
$table->text('narrative')->nullable();
|
||||||
|
$table->string('pl_category')->nullable();
|
||||||
|
$table->string('customer_id')->nullable();
|
||||||
|
$table->string('account_officer')->nullable();
|
||||||
|
$table->string('product_category')->nullable();
|
||||||
|
$table->date('value_date')->nullable();
|
||||||
|
$table->string('currency')->nullable();
|
||||||
|
$table->decimal('amount_fcy', 20, 2)->nullable();
|
||||||
|
$table->decimal('exchange_rate', 20, 6)->nullable();
|
||||||
|
$table->string('neg_ref_no')->nullable();
|
||||||
|
$table->string('position_type')->nullable();
|
||||||
|
$table->string('our_reference')->nullable();
|
||||||
|
$table->string('reversal_marker')->nullable();
|
||||||
|
$table->date('exposure_date')->nullable();
|
||||||
|
$table->string('currency_market')->nullable();
|
||||||
|
$table->string('iblc_country')->nullable();
|
||||||
|
$table->string('last_version')->nullable();
|
||||||
|
$table->string('otor_version')->nullable();
|
||||||
|
$table->string('department_code')->nullable();
|
||||||
|
$table->string('dealer_desk')->nullable();
|
||||||
|
$table->string('bank_sort_cde')->nullable();
|
||||||
|
$table->string('cheque_number')->nullable();
|
||||||
|
$table->date('accounting_date')->nullable();
|
||||||
|
$table->string('contingent_acct')->nullable();
|
||||||
|
$table->string('cheq_type')->nullable();
|
||||||
|
$table->string('tfs_reference')->nullable();
|
||||||
|
$table->string('accounting_company')->nullable();
|
||||||
|
$table->string('stmt_no')->nullable();
|
||||||
|
$table->string('curr_no')->nullable();
|
||||||
|
$table->string('inputter')->nullable();
|
||||||
|
$table->string('authoriser')->nullable();
|
||||||
|
$table->string('co_code')->nullable();
|
||||||
|
$table->dateTime('date_time')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('data_captures');
|
||||||
|
}
|
||||||
|
};
|
||||||
File diff suppressed because one or more lines are too long
@@ -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::create('categories', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('id_category')->nullable();
|
||||||
|
$table->string('date_time')->nullable();
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->string('short_name')->nullable();
|
||||||
|
$table->string('system_ind')->nullable();
|
||||||
|
$table->string('record_status')->nullable();
|
||||||
|
$table->string('co_code')->nullable();
|
||||||
|
$table->string('curr_no')->nullable();
|
||||||
|
$table->string('l_db_cr_ind')->nullable();
|
||||||
|
$table->string('category_code')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('categories');
|
||||||
|
}
|
||||||
|
};
|
||||||
130
database/migrations/2025_05_21_144332_create_tellers_table.php
Normal file
130
database/migrations/2025_05_21_144332_create_tellers_table.php
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<?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('tellers', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('id_teller')->nullable();
|
||||||
|
$table->string('account_1')->nullable();
|
||||||
|
$table->string('currency_1')->nullable();
|
||||||
|
$table->string('amount_local_1')->nullable();
|
||||||
|
$table->string('value_date_1')->nullable();
|
||||||
|
$table->string('account_2')->nullable();
|
||||||
|
$table->string('new_cust_bal')->nullable();
|
||||||
|
$table->string('term_type')->nullable();
|
||||||
|
$table->string('term_id')->nullable();
|
||||||
|
$table->string('trans_reff')->nullable();
|
||||||
|
$table->string('card_no')->nullable();
|
||||||
|
$table->string('recipt_no')->nullable();
|
||||||
|
$table->string('transaction_code')->nullable();
|
||||||
|
$table->string('date_time')->nullable();
|
||||||
|
$table->string('record_status')->nullable();
|
||||||
|
$table->string('amount_local_2')->nullable();
|
||||||
|
$table->string('co_code')->nullable();
|
||||||
|
$table->text('narrative_1')->nullable();
|
||||||
|
$table->string('wic_flag')->nullable();
|
||||||
|
$table->string('wic_cust_type')->nullable();
|
||||||
|
$table->string('wic_full_name')->nullable();
|
||||||
|
$table->string('wic_alias_name')->nullable();
|
||||||
|
$table->string('wic_acct_no')->nullable();
|
||||||
|
$table->string('wic_id_type')->nullable();
|
||||||
|
$table->string('wic_id_no')->nullable();
|
||||||
|
$table->string('wic_npwp')->nullable();
|
||||||
|
$table->string('wic_nationality')->nullable();
|
||||||
|
$table->string('wic_ind_birthpl')->nullable();
|
||||||
|
$table->string('wic_ind_birthdt')->nullable();
|
||||||
|
$table->string('wic_address_id')->nullable();
|
||||||
|
$table->string('wic_address_cur')->nullable();
|
||||||
|
$table->string('wic_city')->nullable();
|
||||||
|
$table->string('wic_province')->nullable();
|
||||||
|
$table->string('wic_post_code')->nullable();
|
||||||
|
$table->string('wic_phone')->nullable();
|
||||||
|
$table->string('wic_gender')->nullable();
|
||||||
|
$table->string('wic_marital_sts')->nullable();
|
||||||
|
$table->string('wic_occptn')->nullable();
|
||||||
|
$table->string('wic_occptn_dur')->nullable();
|
||||||
|
$table->string('wic_income_avg')->nullable();
|
||||||
|
$table->string('wic_cor_name')->nullable();
|
||||||
|
$table->string('wic_cor_address')->nullable();
|
||||||
|
$table->string('wic_cor_phone')->nullable();
|
||||||
|
$table->string('wic_cor_lgl_typ')->nullable();
|
||||||
|
$table->string('wic_cor_lic_no')->nullable();
|
||||||
|
$table->string('wic_cor_birthpl')->nullable();
|
||||||
|
$table->string('wic_cor_birthdt')->nullable();
|
||||||
|
$table->string('wic_cor_rel')->nullable();
|
||||||
|
$table->string('wic_party_rel')->nullable();
|
||||||
|
$table->string('wic_amount')->nullable();
|
||||||
|
$table->string('wic_amount_type')->nullable();
|
||||||
|
$table->string('wic_amtbnk_name')->nullable();
|
||||||
|
$table->string('wic_amtbnk_cunm')->nullable();
|
||||||
|
$table->string('wic_fund_source')->nullable();
|
||||||
|
$table->string('wic_fund_use')->nullable();
|
||||||
|
$table->string('dr_cr_marker')->nullable();
|
||||||
|
$table->string('charge_code')->nullable();
|
||||||
|
$table->string('chrg_amt_local')->nullable();
|
||||||
|
$table->string('charge_category')->nullable();
|
||||||
|
$table->string('charge_account')->nullable();
|
||||||
|
$table->string('amount_fcy_1')->nullable();
|
||||||
|
$table->string('rate_1')->nullable();
|
||||||
|
$table->string('deal_rate')->nullable();
|
||||||
|
$table->string('l_wic_id')->nullable();
|
||||||
|
$table->string('account_1_co_code')->nullable();
|
||||||
|
$table->string('account_2_co_code')->nullable();
|
||||||
|
$table->string('l_charge_amt')->nullable();
|
||||||
|
$table->string('bl_cust_no')->nullable();
|
||||||
|
$table->string('stmt_no')->nullable();
|
||||||
|
$table->string('bil_customer')->nullable();
|
||||||
|
$table->string('value_date_2')->nullable();
|
||||||
|
$table->string('cheq_type')->nullable();
|
||||||
|
$table->string('cheque_number')->nullable();
|
||||||
|
$table->string('inputter')->nullable();
|
||||||
|
$table->string('authoriser')->nullable();
|
||||||
|
$table->string('bil_product')->nullable();
|
||||||
|
$table->string('fx_document')->nullable();
|
||||||
|
$table->string('fx_purpose')->nullable();
|
||||||
|
$table->text('narrative_2')->nullable();
|
||||||
|
$table->string('customer_2')->nullable();
|
||||||
|
$table->string('l_sms_1')->nullable();
|
||||||
|
$table->string('l_phone_1')->nullable();
|
||||||
|
$table->string('kyc_incom_rng')->nullable();
|
||||||
|
$table->string('wic_rt')->nullable();
|
||||||
|
$table->string('wic_rw')->nullable();
|
||||||
|
$table->string('ktp_kelurahan')->nullable();
|
||||||
|
$table->string('ktp_kecamatan')->nullable();
|
||||||
|
$table->string('ktp_provinsi')->nullable();
|
||||||
|
$table->string('wic_jenis_kelam')->nullable();
|
||||||
|
$table->string('kyc_sumber_dana')->nullable();
|
||||||
|
$table->string('l_manual_risk')->nullable();
|
||||||
|
$table->string('l_slip_no')->nullable();
|
||||||
|
$table->string('dest_bank_id')->nullable();
|
||||||
|
$table->text('dr_narrative')->nullable();
|
||||||
|
$table->string('inter_type')->nullable();
|
||||||
|
$table->string('l_va_number')->nullable();
|
||||||
|
$table->string('inter_bank_id')->nullable();
|
||||||
|
$table->text('term_narr')->nullable();
|
||||||
|
$table->string('currency_2')->nullable();
|
||||||
|
$table->string('amount_fcy_2')->nullable();
|
||||||
|
$table->string('rate_2')->nullable();
|
||||||
|
$table->string('customer_1')->nullable();
|
||||||
|
$table->string('last_version')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('tellers');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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.
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
: void
|
||||||
|
{
|
||||||
|
Schema::create('atm_transactions', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('transaction_id')->nullable()->index();
|
||||||
|
$table->string('card_acc_id')->nullable();
|
||||||
|
$table->string('pan_number')->nullable();
|
||||||
|
$table->string('txn_type')->nullable();
|
||||||
|
$table->string('merchant_id')->nullable();
|
||||||
|
$table->string('txn_amount')->nullable();
|
||||||
|
$table->string('booking_date')->nullable();
|
||||||
|
$table->string('trans_ref')->nullable();
|
||||||
|
$table->string('retrieval_ref_no')->nullable();
|
||||||
|
$table->string('stmt_nos')->nullable();
|
||||||
|
$table->string('debit_acct_no')->nullable();
|
||||||
|
$table->string('credit_acct_no')->nullable();
|
||||||
|
$table->string('chrg_amount')->nullable();
|
||||||
|
$table->string('value_date')->nullable();
|
||||||
|
$table->string('stan_no')->nullable();
|
||||||
|
$table->string('trans_status')->nullable();
|
||||||
|
$table->string('proc_code')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
: void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('atm_transactions');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?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('processed_statements', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('account_number');
|
||||||
|
$table->string('period');
|
||||||
|
$table->integer('sequence_no');
|
||||||
|
$table->string('transaction_date');
|
||||||
|
$table->string('reference_number');
|
||||||
|
$table->decimal('transaction_amount', 20, 2);
|
||||||
|
$table->char('transaction_type', 1);
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->decimal('end_balance', 20, 2);
|
||||||
|
$table->string('actual_date');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Indeks untuk pencarian cepat
|
||||||
|
$table->index(['account_number', 'period']);
|
||||||
|
$table->index('sequence_no');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('processed_statements');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?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) {
|
||||||
|
$table->index(['account_number', 'booking_date']);
|
||||||
|
$table->index(['date_time']);
|
||||||
|
$table->index(['trans_reference']);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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::create('account_balances', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('account_number');
|
||||||
|
$table->string('period'); // Format: YYYY-MM
|
||||||
|
$table->string('actual_balance')->default(0);
|
||||||
|
$table->string('cleared_balance')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Create a unique constraint to ensure one record per account per period
|
||||||
|
$table->unique(['account_number', 'period']);
|
||||||
|
|
||||||
|
// Add indexes for faster queries
|
||||||
|
$table->index('account_number');
|
||||||
|
$table->index('period');
|
||||||
|
$table->index('created_at');
|
||||||
|
|
||||||
|
// Add foreign key if needed
|
||||||
|
// $table->foreign('account_number')->references('account_number')->on('accounts');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('account_balances');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user