Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50e60eb587 | ||
|
|
9373325399 | ||
|
|
ea23401473 | ||
|
|
5d0dbfcf21 | ||
|
|
291e791114 | ||
|
|
00681a8e30 | ||
|
|
adda3122f8 | ||
|
|
e53b522f77 | ||
|
|
ffdb528360 | ||
|
|
1ff4035b98 | ||
|
|
f324f9e3f6 | ||
|
|
7af5bf2fe5 | ||
|
|
8a6469ecc9 | ||
|
|
aae0c4ab15 | ||
|
|
150d52f8da | ||
|
|
8736ccf5f8 | ||
|
|
710cbb5232 | ||
|
|
13e077073b | ||
|
|
eff951c600 | ||
|
|
6ad5aff358 | ||
|
|
bd72eb7dfa | ||
|
|
8eb7e69b21 | ||
|
|
4ee5c2e419 | ||
|
|
ca92f32ccb | ||
|
|
e1740c0850 | ||
|
|
d88f4a242e | ||
|
|
c0e5ddd37a | ||
|
|
5f9a82ec20 | ||
|
|
33b1255dfb | ||
|
|
aff6039b33 | ||
|
|
51e432c74f | ||
|
|
9cdc7f9487 | ||
|
|
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 | ||
|
|
0cbb7c9a3c | ||
|
|
fabc35e729 |
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Console;
|
||||
namespace Modules\Webstatement\Console;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\Webstatement\Http\Controllers\WebstatementController;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\Webstatement\Http\Controllers\WebstatementController;
|
||||
|
||||
class ExportDailyStatements extends Command
|
||||
/**
|
||||
* Console command untuk export daily statements
|
||||
* Command ini dapat dijalankan secara manual atau dijadwalkan
|
||||
*/
|
||||
class ExportDailyStatements extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'webstatement:export-statements
|
||||
{--queue_name=default : Queue name untuk menjalankan export jobs (default: default)}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Export daily statements for all configured client accounts dengan queue name yang dapat dikustomisasi';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
* Menjalankan proses export daily statements
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'webstatement:export-statements';
|
||||
$queueName = $this->option('queue_name');
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Export daily statements for all configured client accounts';
|
||||
// Log start of process
|
||||
Log::info('Starting daily statement export process', [
|
||||
'queue_name' => $queueName ?? 'default',
|
||||
'command' => 'webstatement:export-statements'
|
||||
]);
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Starting daily statement export process...');
|
||||
$this->info('Starting daily statement export process...');
|
||||
$this->info('Queue Name: ' . ($queueName ?? 'default'));
|
||||
|
||||
try {
|
||||
$controller = app(WebstatementController::class);
|
||||
$response = $controller->index();
|
||||
try {
|
||||
$controller = app(WebstatementController::class);
|
||||
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->info($responseData['message']);
|
||||
// Pass queue name to controller if needed
|
||||
// Jika controller membutuhkan queue name, bisa ditambahkan sebagai parameter
|
||||
$response = $controller->index($queueName);
|
||||
|
||||
// Display summary of jobs queued
|
||||
$jobCount = count($responseData['jobs'] ?? []);
|
||||
$this->info("Successfully queued {$jobCount} statement export jobs");
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$message = $responseData['message'] ?? 'Export process completed';
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (Exception $e) {
|
||||
$this->error('Error exporting statements: ' . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$this->info($message);
|
||||
|
||||
// Display summary of jobs queued
|
||||
$jobCount = count($responseData['jobs'] ?? []);
|
||||
$this->info("Successfully queued {$jobCount} statement export jobs");
|
||||
$this->info("Jobs dispatched to queue: {$queueName}");
|
||||
|
||||
// Log successful completion
|
||||
Log::info('Daily statement export process completed successfully', [
|
||||
'message' => $message,
|
||||
'job_count' => $jobCount,
|
||||
'queue_name' => $queueName ?? 'default'
|
||||
]);
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (Exception $e) {
|
||||
$errorMessage = 'Error exporting statements: ' . $e->getMessage();
|
||||
$this->error($errorMessage);
|
||||
|
||||
// Log error with queue information
|
||||
Log::error($errorMessage, [
|
||||
'exception' => $e->getTraceAsString(),
|
||||
'queue_name' => $queueName ?? 'default'
|
||||
]);
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
229
app/Console/GenerateClosingBalanceReportCommand.php
Normal file
229
app/Console/GenerateClosingBalanceReportCommand.php
Normal file
@@ -0,0 +1,229 @@
|
||||
<?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}
|
||||
{group=DEFAULT : Group transaksi QRIS atau DEFAULT}
|
||||
{--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, periode, dan group 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');
|
||||
$group = $this->argument('group');
|
||||
$userId = $this->option('user_id');
|
||||
|
||||
// Validate parameters
|
||||
if (!$this->validateParameters($accountNumber, $period, $group, $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,
|
||||
'group' => $group,
|
||||
'user_id' => $userId,
|
||||
'command' => 'webstatement:generate-closing-balance-report'
|
||||
]);
|
||||
|
||||
// Create report log entry
|
||||
$reportLog = $this->createReportLog($accountNumber, $period, $group, $userId);
|
||||
|
||||
if (!$reportLog) {
|
||||
$this->error('Failed to create report log entry');
|
||||
DB::rollback();
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// Dispatch the job with group parameter
|
||||
GenerateClosingBalanceReportJob::dispatch($accountNumber, $period, $reportLog->id, $group);
|
||||
|
||||
DB::commit();
|
||||
|
||||
$this->info("Closing Balance report generation job queued successfully!");
|
||||
$this->info("Account Number: {$accountNumber}");
|
||||
$this->info("Period: {$period}");
|
||||
$this->info("Group: {$group}");
|
||||
$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,
|
||||
'group' => $group,
|
||||
'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,
|
||||
'group' => $group,
|
||||
'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 string $group
|
||||
* @param int $userId
|
||||
* @return bool
|
||||
*/
|
||||
private function validateParameters(string $accountNumber, string $period, string $group, 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 group parameter
|
||||
$allowedGroups = ['QRIS', 'DEFAULT'];
|
||||
if (!in_array(strtoupper($group), $allowedGroups)) {
|
||||
$this->error('Invalid group parameter. Allowed values: ' . implode(', ', $allowedGroups));
|
||||
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 string $group
|
||||
* @param int $userId
|
||||
* @return ClosingBalanceReportLog|null
|
||||
*/
|
||||
private function createReportLog(string $accountNumber, string $period, string $group, 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
|
||||
'group_name' => strtoupper($group), // Tambahkan group_name ke log
|
||||
'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,
|
||||
'group' => $group,
|
||||
'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,
|
||||
'group' => $group,
|
||||
'user_id' => $userId,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Console;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\Webstatement\Http\Controllers\MigrasiController;
|
||||
|
||||
class ProcessDailyMigration extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'webstatement:process-daily-migration
|
||||
{--process_parameter= : To process migration parameter true/false}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Process data migration for the previous day\'s period';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$processParameter = $this->option('process_parameter');
|
||||
|
||||
$this->info('Starting daily data migration process...');
|
||||
$this->info('Process Parameter: ' . ($processParameter ?? 'False'));
|
||||
|
||||
try {
|
||||
$controller = app(MigrasiController::class);
|
||||
$response = $controller->index($processParameter);
|
||||
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->info($responseData['message'] ?? 'Process completed');
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (Exception $e) {
|
||||
$this->error('Error processing daily migration: ' . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
85
app/Console/ProcessDailyStaging.php
Normal file
85
app/Console/ProcessDailyStaging.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Console;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\Webstatement\Http\Controllers\StagingController;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Console command untuk memproses data staging harian
|
||||
* Command ini dapat dijalankan secara manual atau dijadwalkan
|
||||
*/
|
||||
class ProcessDailyStaging extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'webstatement:process-daily-staging
|
||||
{--process_parameter= : To process staging parameter true/false}
|
||||
{--period= : Period to process (default: -1 day, format: Ymd or relative date)}
|
||||
{--queue_name=default : Queue name untuk menjalankan job (default: default)}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Process data staging for the specified period (default: previous day) dengan queue name yang dapat dikustomisasi';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
* Menjalankan proses staging data harian
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$processParameter = $this->option('process_parameter');
|
||||
$period = $this->option('period');
|
||||
$queueName = $this->option('queue_name');
|
||||
|
||||
// Log start of process
|
||||
Log::info('Starting daily data staging process', [
|
||||
'process_parameter' => $processParameter ?? 'false',
|
||||
'period' => $period ?? '-1 day',
|
||||
'queue_name' => $queueName ?? 'default'
|
||||
]);
|
||||
|
||||
$this->info('Starting daily data staging process...');
|
||||
$this->info('Process Parameter: ' . ($processParameter ?? 'False'));
|
||||
$this->info('Period: ' . ($period ?? '-1 day (default)'));
|
||||
$this->info('Queue Name: ' . ($queueName ?? 'default'));
|
||||
|
||||
try {
|
||||
$controller = app(StagingController::class);
|
||||
|
||||
// Pass queue name to controller if needed
|
||||
// Jika controller membutuhkan queue name, bisa ditambahkan sebagai parameter
|
||||
$response = $controller->index($processParameter, $period, $queueName);
|
||||
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$message = $responseData['message'] ?? 'Process completed';
|
||||
|
||||
$this->info($message);
|
||||
Log::info('Daily staging process completed successfully', [
|
||||
'message' => $message,
|
||||
'queue_name' => $queueName ?? 'default'
|
||||
]);
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (Exception $e) {
|
||||
$errorMessage = 'Error processing daily staging: ' . $e->getMessage();
|
||||
$this->error($errorMessage);
|
||||
Log::error($errorMessage, [
|
||||
'exception' => $e->getTraceAsString(),
|
||||
'queue_name' => $queueName ?? 'default'
|
||||
]);
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
140
app/Enums/ResponseCode.php
Normal file
140
app/Enums/ResponseCode.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Enums;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Response Code Enum untuk standarisasi response API
|
||||
*
|
||||
* @category Enums
|
||||
* @package Modules\Webstatement\Enums
|
||||
*/
|
||||
enum ResponseCode: string
|
||||
{
|
||||
// Success Codes
|
||||
case SUCCESS = '00';
|
||||
|
||||
// Data Error Codes
|
||||
case INVALID_FIELD = '01';
|
||||
case MISSING_FIELD = '02';
|
||||
case INVALID_FORMAT = '03';
|
||||
case DATA_NOT_FOUND = '04';
|
||||
case DUPLICATE_REQUEST = '05';
|
||||
case ACCOUNT_ALREADY_EXISTS = '06';
|
||||
case ACCOUNT_NOT_FOUND = '07';
|
||||
|
||||
// Auth Error Codes
|
||||
case INVALID_TOKEN = '10';
|
||||
case UNAUTHORIZED = '11';
|
||||
|
||||
// System Error Codes
|
||||
case SYSTEM_MALFUNCTION = '96';
|
||||
case TIMEOUT = '97';
|
||||
case SERVICE_UNAVAILABLE = '98';
|
||||
case GENERAL_ERROR = '99';
|
||||
|
||||
/**
|
||||
* Mendapatkan pesan response berdasarkan kode
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMessage(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::SUCCESS => 'Success',
|
||||
self::INVALID_FIELD => 'Invalid Field',
|
||||
self::MISSING_FIELD => 'Missing Field',
|
||||
self::INVALID_FORMAT => 'Invalid Format',
|
||||
self::DATA_NOT_FOUND => 'Data Not Found',
|
||||
self::DUPLICATE_REQUEST => 'Duplicate Request',
|
||||
self::ACCOUNT_ALREADY_EXISTS => 'Account Already Exists',
|
||||
self::ACCOUNT_NOT_FOUND => 'Account Not Found',
|
||||
self::INVALID_TOKEN => 'Invalid Token',
|
||||
self::UNAUTHORIZED => 'Unauthorized',
|
||||
self::SYSTEM_MALFUNCTION => 'System Malfunction',
|
||||
self::TIMEOUT => 'Timeout',
|
||||
self::SERVICE_UNAVAILABLE => 'Service Unavailable',
|
||||
self::GENERAL_ERROR => 'General Error',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mendapatkan deskripsi response berdasarkan kode
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::SUCCESS => 'Permintaan berhasil',
|
||||
self::INVALID_FIELD => 'Field tertentu tidak sesuai aturan',
|
||||
self::MISSING_FIELD => 'Field wajib tidak dikirim',
|
||||
self::INVALID_FORMAT => 'Format salah',
|
||||
self::DATA_NOT_FOUND => 'Data yang diminta tidak ditemukan',
|
||||
self::DUPLICATE_REQUEST => 'Request ID sama, sudah pernah diproses',
|
||||
self::ACCOUNT_ALREADY_EXISTS => 'Nomor rekening / username / email sudah terdaftar',
|
||||
self::ACCOUNT_NOT_FOUND => 'Nomor rekening / akun tidak ditemukan',
|
||||
self::INVALID_TOKEN => 'Token tidak valid',
|
||||
self::UNAUTHORIZED => 'Tidak punya akses',
|
||||
self::SYSTEM_MALFUNCTION => 'Gangguan teknis di server',
|
||||
self::TIMEOUT => 'Request timeout',
|
||||
self::SERVICE_UNAVAILABLE => 'Layanan tidak tersedia',
|
||||
self::GENERAL_ERROR => 'Kesalahan umum',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mendapatkan HTTP status code berdasarkan response code
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getHttpStatus(): int
|
||||
{
|
||||
return match($this) {
|
||||
self::SUCCESS => 200,
|
||||
self::INVALID_FIELD,
|
||||
self::MISSING_FIELD,
|
||||
self::INVALID_FORMAT => 400,
|
||||
self::DATA_NOT_FOUND,
|
||||
self::ACCOUNT_NOT_FOUND => 404,
|
||||
self::DUPLICATE_REQUEST,
|
||||
self::ACCOUNT_ALREADY_EXISTS => 409,
|
||||
self::INVALID_TOKEN,
|
||||
self::UNAUTHORIZED => 401,
|
||||
self::SYSTEM_MALFUNCTION,
|
||||
self::GENERAL_ERROR => 500,
|
||||
self::TIMEOUT => 408,
|
||||
self::SERVICE_UNAVAILABLE => 503,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Membuat response array standar
|
||||
*
|
||||
* @param mixed $data
|
||||
* @param string|null $message
|
||||
* @return array
|
||||
*/
|
||||
public function toResponse($data = null, ?string $message = null): array
|
||||
{
|
||||
$response = [
|
||||
'status' => $this->value == '00' ? true : false,
|
||||
'response_code' => $this->value,
|
||||
'response_message' => $this->getMessage() . ($message ? ' | ' . $message : ''),
|
||||
];
|
||||
|
||||
if (isset($data['errors'])) {
|
||||
$response['errors'] = $data['errors'];
|
||||
} else {
|
||||
$response['data'] = $data;
|
||||
}
|
||||
|
||||
$response['meta'] = [
|
||||
'generated_at' => now()->toDateTimeString(),
|
||||
'request_id' => request()->header('X-Request-ID', uniqid('req_'))
|
||||
];
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
97
app/Http/Controllers/Api/AccountBalanceController.php
Normal file
97
app/Http/Controllers/Api/AccountBalanceController.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\Webstatement\Http\Requests\BalanceSummaryRequest;
|
||||
use Modules\Webstatement\Http\Requests\DetailedBalanceRequest;
|
||||
use Modules\Webstatement\Services\AccountBalanceService;
|
||||
use Modules\Webstatement\Http\Resources\BalanceSummaryResource;
|
||||
use Modules\Webstatement\Http\Resources\DetailedBalanceResource;
|
||||
use Modules\Webstatement\Enums\ResponseCode;
|
||||
use Exception;
|
||||
|
||||
class AccountBalanceController extends Controller
|
||||
{
|
||||
protected AccountBalanceService $accountBalanceService;
|
||||
|
||||
public function __construct(AccountBalanceService $accountBalanceService)
|
||||
{
|
||||
$this->accountBalanceService = $accountBalanceService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get account balance summary (opening and closing balance)
|
||||
*
|
||||
* @param BalanceSummaryRequest $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function getBalanceSummary(BalanceSummaryRequest $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$accountNumber = $request->input('account_number');
|
||||
$startDate = $request->input('start_date');
|
||||
$endDate = $request->input('end_date');
|
||||
|
||||
Log::info('Account balance summary requested', [
|
||||
'account_number' => $accountNumber,
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent()
|
||||
]);
|
||||
|
||||
$result = $this->accountBalanceService->getBalanceSummary(
|
||||
$accountNumber,
|
||||
$startDate,
|
||||
$endDate
|
||||
);
|
||||
|
||||
if (empty($result)) {
|
||||
return response()->json(
|
||||
ResponseCode::DATA_NOT_FOUND->toResponse(
|
||||
null,
|
||||
'Rekening tidak ditemukan'
|
||||
),
|
||||
ResponseCode::DATA_NOT_FOUND->getHttpStatus()
|
||||
);
|
||||
}
|
||||
|
||||
return response()->json(
|
||||
ResponseCode::SUCCESS->toResponse(
|
||||
(new BalanceSummaryResource($result))->toArray($request),
|
||||
|
||||
),
|
||||
ResponseCode::SUCCESS->getHttpStatus()
|
||||
);
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error getting account balance summary', [
|
||||
'error' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
$responseCode = match ($e->getCode()) {
|
||||
404 => ResponseCode::DATA_NOT_FOUND,
|
||||
401 => ResponseCode::UNAUTHORIZED,
|
||||
403 => ResponseCode::UNAUTHORIZED,
|
||||
408 => ResponseCode::TIMEOUT,
|
||||
503 => ResponseCode::SERVICE_UNAVAILABLE,
|
||||
400 => ResponseCode::INVALID_FIELD,
|
||||
default => ResponseCode::SYSTEM_MALFUNCTION
|
||||
};
|
||||
|
||||
return response()->json(
|
||||
$responseCode->toResponse(
|
||||
null,
|
||||
config('app.debug') ? $e->getMessage() : 'Terjadi kesalahan sistem'
|
||||
),
|
||||
$responseCode->getHttpStatus()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,7 @@ class CombinePdfController extends Controller
|
||||
|
||||
try {
|
||||
// Generate password based on customer relation data
|
||||
$password = $this->generatePassword($account);
|
||||
$password = generatePassword($account);
|
||||
|
||||
// Dispatch job to combine PDFs or apply password protection
|
||||
CombinePdfJob::dispatch($pdfFiles, $outputDir, $outputFilename, $password, $output_destination, $branchCode, $period);
|
||||
@@ -158,58 +158,4 @@ class CombinePdfController extends Controller
|
||||
'period' => $period
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate password based on customer relation data
|
||||
* Format: date+end 2 digit account_number
|
||||
* Example: 05Oct202585
|
||||
*
|
||||
* @param Account $account
|
||||
* @return string
|
||||
*/
|
||||
private function generatePassword(Account $account)
|
||||
{
|
||||
$customer = $account->customer;
|
||||
$accountNumber = $account->account_number;
|
||||
|
||||
// Get last 2 digits of account number
|
||||
$lastTwoDigits = substr($accountNumber, -2);
|
||||
|
||||
// Determine which date to use based on sector
|
||||
$dateToUse = null;
|
||||
|
||||
if ($customer && $customer->sector) {
|
||||
$firstDigitSector = substr($customer->sector, 0, 1);
|
||||
|
||||
if ($firstDigitSector === '1') {
|
||||
// Use date_of_birth if available, otherwise birth_incorp_date
|
||||
$dateToUse = $customer->date_of_birth ?: $customer->birth_incorp_date;
|
||||
} else {
|
||||
// Use birth_incorp_date for sector > 1
|
||||
$dateToUse = $customer->birth_incorp_date;
|
||||
}
|
||||
}
|
||||
|
||||
// If no date found, fallback to account number
|
||||
if (!$dateToUse) {
|
||||
Log::warning("No date found for account {$accountNumber}, using account number as password");
|
||||
return $accountNumber;
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse the date and format it
|
||||
$date = Carbon::parse($dateToUse);
|
||||
$day = $date->format('d');
|
||||
$month = $date->format('M'); // 3-letter month abbreviation
|
||||
$year = $date->format('Y');
|
||||
|
||||
// Format: ddMmmyyyyXX (e.g., 05Oct202585)
|
||||
$password = $day . $month . $year . $lastTwoDigits;
|
||||
|
||||
return $password;
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error parsing date for account {$accountNumber}: {$e->getMessage()}");
|
||||
return $accountNumber; // Fallback to account number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,123 +0,0 @@
|
||||
<?php
|
||||
namespace Modules\Webstatement\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use BadMethodCallException;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Log;
|
||||
use Modules\Webstatement\Jobs\{ProcessAccountDataJob,
|
||||
ProcessArrangementDataJob,
|
||||
ProcessAtmTransactionJob,
|
||||
ProcessBillDetailDataJob,
|
||||
ProcessCategoryDataJob,
|
||||
ProcessCompanyDataJob,
|
||||
ProcessCustomerDataJob,
|
||||
ProcessDataCaptureDataJob,
|
||||
ProcessFtTxnTypeConditionJob,
|
||||
ProcessFundsTransferDataJob,
|
||||
ProcessStmtEntryDataJob,
|
||||
ProcessStmtNarrFormatDataJob,
|
||||
ProcessStmtNarrParamDataJob,
|
||||
ProcessTellerDataJob,
|
||||
ProcessTransactionDataJob,
|
||||
ProcessSectorDataJob};
|
||||
|
||||
class MigrasiController extends Controller
|
||||
{
|
||||
private const PROCESS_TYPES = [
|
||||
'transaction' => ProcessTransactionDataJob::class,
|
||||
'stmtNarrParam' => ProcessStmtNarrParamDataJob::class,
|
||||
'stmtNarrFormat' => ProcessStmtNarrFormatDataJob::class,
|
||||
'ftTxnTypeCondition' => ProcessFtTxnTypeConditionJob::class,
|
||||
'category' => ProcessCategoryDataJob::class,
|
||||
'company' => ProcessCompanyDataJob::class,
|
||||
'customer' => ProcessCustomerDataJob::class,
|
||||
'account' => ProcessAccountDataJob::class,
|
||||
'stmtEntry' => ProcessStmtEntryDataJob::class,
|
||||
'dataCapture' => ProcessDataCaptureDataJob::class,
|
||||
'fundsTransfer' => ProcessFundsTransferDataJob::class,
|
||||
'teller' => ProcessTellerDataJob::class,
|
||||
'atmTransaction' => ProcessAtmTransactionJob::class,
|
||||
'arrangement' => ProcessArrangementDataJob::class,
|
||||
'billDetail' => ProcessBillDetailDataJob::class,
|
||||
'sector' => ProcessSectorDataJob::class
|
||||
];
|
||||
|
||||
private const PARAMETER_PROCESSES = [
|
||||
'transaction',
|
||||
'stmtNarrParam',
|
||||
'stmtNarrFormat',
|
||||
'ftTxnTypeCondition',
|
||||
'sector'
|
||||
];
|
||||
|
||||
private const DATA_PROCESSES = [
|
||||
'category',
|
||||
'company',
|
||||
'customer',
|
||||
'account',
|
||||
'stmtEntry',
|
||||
'dataCapture',
|
||||
'fundsTransfer',
|
||||
'teller',
|
||||
'atmTransaction',
|
||||
'arrangement',
|
||||
'billDetail'
|
||||
];
|
||||
|
||||
public function __call($method, $parameters)
|
||||
{
|
||||
if (strpos($method, 'process') === 0) {
|
||||
$type = lcfirst(substr($method, 7));
|
||||
if (isset(self::PROCESS_TYPES[$type])) {
|
||||
return $this->processData($type, $parameters[0] ?? '');
|
||||
}
|
||||
}
|
||||
throw new BadMethodCallException("Method {$method} does not exist.");
|
||||
}
|
||||
|
||||
private function processData(string $type, string $period)
|
||||
: JsonResponse
|
||||
{
|
||||
try {
|
||||
$jobClass = self::PROCESS_TYPES[$type];
|
||||
$jobClass::dispatch($period);
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
public function index($processParameter = false)
|
||||
{
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
|
||||
if ($processParameter) {
|
||||
foreach (self::PARAMETER_PROCESSES as $process) {
|
||||
$this->processData($process, '_parameter');
|
||||
}
|
||||
return response()->json(['message' => 'Parameter processes completed successfully']);
|
||||
}
|
||||
|
||||
$period = date('Ymd', strtotime('-1 day'));
|
||||
if (!$disk->exists($period)) {
|
||||
return response()->json([
|
||||
"message" => "Period {$period} folder not found in SFTP storage"
|
||||
], 404);
|
||||
}
|
||||
|
||||
foreach (self::DATA_PROCESSES as $process) {
|
||||
$this->processData($process, $period);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => "Data processing for period {$period} has been queued successfully"
|
||||
]);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
240
app/Http/Controllers/StagingController.php
Normal file
240
app/Http/Controllers/StagingController.php
Normal file
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use BadMethodCallException;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\Webstatement\Jobs\{ProcessAccountDataJob,
|
||||
ProcessArrangementDataJob,
|
||||
ProcessAtmTransactionJob,
|
||||
ProcessBillDetailDataJob,
|
||||
ProcessCategoryDataJob,
|
||||
ProcessCompanyDataJob,
|
||||
ProcessCustomerDataJob,
|
||||
ProcessDataCaptureDataJob,
|
||||
ProcessFtTxnTypeConditionJob,
|
||||
ProcessFundsTransferDataJob,
|
||||
ProcessStmtEntryDataJob,
|
||||
ProcessStmtNarrFormatDataJob,
|
||||
ProcessStmtNarrParamDataJob,
|
||||
ProcessTellerDataJob,
|
||||
ProcessTransactionDataJob,
|
||||
ProcessSectorDataJob,
|
||||
ProcessProvinceDataJob,
|
||||
ProcessStmtEntryDetailDataJob};
|
||||
|
||||
class StagingController extends Controller
|
||||
{
|
||||
private const PROCESS_TYPES = [
|
||||
'transaction' => ProcessTransactionDataJob::class,
|
||||
'stmtNarrParam' => ProcessStmtNarrParamDataJob::class,
|
||||
'stmtNarrFormat' => ProcessStmtNarrFormatDataJob::class,
|
||||
'ftTxnTypeCondition' => ProcessFtTxnTypeConditionJob::class,
|
||||
'category' => ProcessCategoryDataJob::class,
|
||||
'company' => ProcessCompanyDataJob::class,
|
||||
'customer' => ProcessCustomerDataJob::class,
|
||||
'account' => ProcessAccountDataJob::class,
|
||||
'stmtEntry' => ProcessStmtEntryDataJob::class,
|
||||
'stmtEntryDetail' => ProcessStmtEntryDetailDataJob::class,
|
||||
'dataCapture' => ProcessDataCaptureDataJob::class,
|
||||
'fundsTransfer' => ProcessFundsTransferDataJob::class,
|
||||
'teller' => ProcessTellerDataJob::class,
|
||||
'atmTransaction' => ProcessAtmTransactionJob::class,
|
||||
'arrangement' => ProcessArrangementDataJob::class,
|
||||
'billDetail' => ProcessBillDetailDataJob::class,
|
||||
'sector' => ProcessSectorDataJob::class,
|
||||
'province' => ProcessProvinceDataJob::class
|
||||
];
|
||||
|
||||
private const PARAMETER_PROCESSES = [
|
||||
'transaction',
|
||||
'stmtNarrParam',
|
||||
'stmtNarrFormat',
|
||||
'ftTxnTypeCondition',
|
||||
'sector',
|
||||
'province'
|
||||
];
|
||||
|
||||
private const DATA_PROCESSES = [
|
||||
'category',
|
||||
'company',
|
||||
'customer',
|
||||
'account',
|
||||
'stmtEntry',
|
||||
'stmtEntryDetail',
|
||||
'dataCapture',
|
||||
'fundsTransfer',
|
||||
'teller',
|
||||
'atmTransaction',
|
||||
'arrangement',
|
||||
'billDetail'
|
||||
];
|
||||
|
||||
public function __call($method, $parameters)
|
||||
{
|
||||
if (strpos($method, 'process') === 0) {
|
||||
$type = lcfirst(substr($method, 7));
|
||||
if (isset(self::PROCESS_TYPES[$type])) {
|
||||
return $this->processData($type, $parameters[0] ?? '', $parameters[1] ?? 'default');
|
||||
}
|
||||
}
|
||||
throw new BadMethodCallException("Method {$method} does not exist.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Memproses data dengan queue name yang dapat dikustomisasi
|
||||
*
|
||||
* @param string $type Tipe proses yang akan dijalankan
|
||||
* @param string $period Periode data yang akan diproses
|
||||
* @param string $queueName Nama queue untuk menjalankan job
|
||||
* @return JsonResponse
|
||||
*/
|
||||
private function processData(string $type, string $period, string $queueName = 'default'): JsonResponse
|
||||
{
|
||||
try {
|
||||
$jobClass = self::PROCESS_TYPES[$type];
|
||||
|
||||
// Dispatch job dengan queue name yang spesifik
|
||||
$jobClass::dispatch($period)->onQueue($queueName);
|
||||
|
||||
$message = sprintf('%s data processing job has been queued successfully on queue: %s', ucfirst($type), $queueName);
|
||||
Log::info($message, [
|
||||
'type' => $type,
|
||||
'period' => $period,
|
||||
'queue_name' => $queueName
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => $message,
|
||||
'queue_name' => $queueName
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
Log::error(sprintf('Error in %s processing: %s', $type, $e->getMessage()), [
|
||||
'type' => $type,
|
||||
'period' => $period,
|
||||
'queue_name' => $queueName,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proses migrasi data dengan parameter, periode, dan queue name yang dapat dikustomisasi
|
||||
*
|
||||
* @param bool|string $processParameter Flag untuk memproses parameter
|
||||
* @param string|null $period Periode yang akan diproses (default: -1 day)
|
||||
* @param string $queueName Nama queue untuk menjalankan job (default: default)
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function index($processParameter = false, $period = null, $queueName = 'default')
|
||||
{
|
||||
try {
|
||||
Log::info('Starting migration process', [
|
||||
'process_parameter' => $processParameter,
|
||||
'period' => $period,
|
||||
'queue_name' => $queueName
|
||||
]);
|
||||
|
||||
$disk = Storage::disk('staging');
|
||||
|
||||
if ($processParameter) {
|
||||
Log::info('Processing parameter data', ['queue_name' => $queueName]);
|
||||
|
||||
foreach (self::PARAMETER_PROCESSES as $process) {
|
||||
$this->processData($process, '_parameter', $queueName);
|
||||
}
|
||||
|
||||
Log::info('Parameter processes completed successfully', ['queue_name' => $queueName]);
|
||||
return response()->json([
|
||||
'message' => 'Parameter processes completed successfully',
|
||||
'queue_name' => $queueName
|
||||
]);
|
||||
}
|
||||
|
||||
// Tentukan periode yang akan diproses
|
||||
$targetPeriod = $this->determinePeriod($period);
|
||||
|
||||
Log::info('Processing data for period', [
|
||||
'period' => $targetPeriod,
|
||||
'queue_name' => $queueName
|
||||
]);
|
||||
|
||||
if (!$disk->exists($targetPeriod)) {
|
||||
$errorMessage = "Period {$targetPeriod} folder not found in SFTP storage";
|
||||
Log::warning($errorMessage, ['queue_name' => $queueName]);
|
||||
|
||||
return response()->json([
|
||||
"message" => $errorMessage,
|
||||
'queue_name' => $queueName
|
||||
], 404);
|
||||
}
|
||||
|
||||
foreach (self::DATA_PROCESSES as $process) {
|
||||
$this->processData($process, $targetPeriod, $queueName);
|
||||
}
|
||||
|
||||
$successMessage = "Data processing for period {$targetPeriod} has been queued successfully on queue: {$queueName}";
|
||||
Log::info($successMessage, [
|
||||
'period' => $targetPeriod,
|
||||
'queue_name' => $queueName
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => $successMessage,
|
||||
'queue_name' => $queueName
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error in migration index method: ' . $e->getMessage(), [
|
||||
'queue_name' => $queueName,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
return response()->json([
|
||||
'error' => $e->getMessage(),
|
||||
'queue_name' => $queueName
|
||||
], 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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,198 +1,247 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Http\Controllers;
|
||||
namespace Modules\Webstatement\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Modules\Webstatement\Jobs\ExportStatementJob;
|
||||
use Modules\Webstatement\Models\AccountBalance;
|
||||
use Modules\Webstatement\Jobs\ExportStatementPeriodJob;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\Webstatement\Jobs\ExportStatementJob;
|
||||
use Modules\Webstatement\Models\AccountBalance;
|
||||
use Modules\Webstatement\Jobs\ExportStatementPeriodJob;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class WebstatementController extends Controller
|
||||
class WebstatementController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
* Menjalankan export statement untuk semua akun dengan queue name yang dapat dikustomisasi
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function index(string $queueName='default')
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$jobIds = [];
|
||||
$data = [];
|
||||
Log::info('Starting statement export process', [
|
||||
'queue_name' => $queueName
|
||||
]);
|
||||
|
||||
foreach ($this->listAccount() as $clientName => $accounts) {
|
||||
foreach ($accounts as $accountNumber) {
|
||||
foreach ($this->listPeriod() as $period) {
|
||||
$job = new ExportStatementJob(
|
||||
$accountNumber,
|
||||
$period,
|
||||
$this->getAccountBalance($accountNumber, $period),
|
||||
$clientName // Pass the client name to the job
|
||||
);
|
||||
$jobIds[] = app(Dispatcher::class)->dispatch($job);
|
||||
$data[] = [
|
||||
'client_name' => $clientName,
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $period
|
||||
];
|
||||
}
|
||||
$jobIds = [];
|
||||
$data = [];
|
||||
|
||||
foreach ($this->listAccount() as $clientName => $accounts) {
|
||||
foreach ($accounts as $accountNumber) {
|
||||
foreach ($this->listPeriod() as $period) {
|
||||
$job = new ExportStatementJob(
|
||||
$accountNumber,
|
||||
$period,
|
||||
$this->getAccountBalance($accountNumber, $period),
|
||||
$clientName // Pass the client name to the job
|
||||
);
|
||||
|
||||
// Dispatch job dengan queue name yang spesifik
|
||||
$jobIds[] = app(Dispatcher::class)->dispatch($job->onQueue($queueName));
|
||||
$data[] = [
|
||||
'client_name' => $clientName,
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $period,
|
||||
'queue_name' => $queueName
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log::info('Statement export jobs queued successfully', [
|
||||
'total_jobs' => count($jobIds),
|
||||
'queue_name' => $queueName
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Statement export jobs have been queued',
|
||||
'queue_name' => $queueName,
|
||||
'jobs' => array_map(function ($index, $jobId) use ($data) {
|
||||
return [
|
||||
'job_id' => $jobId,
|
||||
'client_name' => $data[$index]['client_name'],
|
||||
'account_number' => $data[$index]['account_number'],
|
||||
'period' => $data[$index]['period'],
|
||||
'queue_name' => $data[$index]['queue_name'],
|
||||
'file_name' => "{$data[$index]['client_name']}_{$data[$index]['account_number']}_{$data[$index]['period']}.csv"
|
||||
];
|
||||
}, array_keys($jobIds), $jobIds)
|
||||
]);
|
||||
}
|
||||
|
||||
function listAccount(){
|
||||
return [
|
||||
'PLUANG' => [
|
||||
'1080426085',
|
||||
'1080425781',
|
||||
],
|
||||
'OY' => [
|
||||
'1081647484',
|
||||
'1081647485',
|
||||
],
|
||||
'INDORAYA' => [
|
||||
'1083123710',
|
||||
'1083123711',
|
||||
'1083123712',
|
||||
'1083123713',
|
||||
'1083123714',
|
||||
'1083123715',
|
||||
'1083123716',
|
||||
'1083123718',
|
||||
'1083123719',
|
||||
'1083123721',
|
||||
'1083123722',
|
||||
'1083123723',
|
||||
'1083123724',
|
||||
'1083123726',
|
||||
'1083123727',
|
||||
'1083123728',
|
||||
'1083123730',
|
||||
'1083123731',
|
||||
'1083123732',
|
||||
'1083123734',
|
||||
'1083123735',
|
||||
],
|
||||
'TDC' => [
|
||||
'1086677889',
|
||||
'1086677890',
|
||||
'1086677891',
|
||||
'1086677892',
|
||||
'1086677893',
|
||||
'1086677894',
|
||||
'1086677895',
|
||||
'1086677896',
|
||||
'1086677897',
|
||||
],
|
||||
'ASIA_PARKING' => [
|
||||
'1080119298',
|
||||
'1080119361',
|
||||
'1080119425',
|
||||
'1080119387',
|
||||
'1082208069',
|
||||
],
|
||||
'DAU' => [
|
||||
'1085151668',
|
||||
],
|
||||
'EGR' => [
|
||||
'1085368601',
|
||||
],
|
||||
'SARANA_PACTINDO' => [
|
||||
'1078333878',
|
||||
],
|
||||
'SWADAYA_PANDU' => [
|
||||
'0081272689',
|
||||
],
|
||||
"AWAN_LINTANG_SOLUSI"=> [
|
||||
"1084269430"
|
||||
],
|
||||
"MONETA"=> [
|
||||
"1085667890"
|
||||
],
|
||||
"SILOT" => [
|
||||
"1083972676"
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
function listPeriod(){
|
||||
return [
|
||||
date('Ymd', strtotime('-1 day'))
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
function getAccountBalance($accountNumber, $period)
|
||||
{
|
||||
$accountBalance = AccountBalance::where('account_number', $accountNumber)
|
||||
->where('period', '<', $period)
|
||||
->orderBy('period', 'desc')
|
||||
->first();
|
||||
|
||||
return $accountBalance->actual_balance ?? 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Print statement rekening dengan queue name yang dapat dikustomisasi
|
||||
*
|
||||
* @param string $accountNumber
|
||||
* @param string|null $period
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
function printStatementRekening($accountNumber, $period = null, Request $request = null) {
|
||||
$queueName = $request ? $request->get('queue_name', 'default') : 'default';
|
||||
$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}", [
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $period,
|
||||
'client_name' => $clientName,
|
||||
'queue_name' => $queueName
|
||||
]);
|
||||
|
||||
// Validate inputs
|
||||
if (empty($accountNumber) || empty($period) || empty($clientName)) {
|
||||
throw new \Exception('Required parameters missing');
|
||||
}
|
||||
|
||||
// Dispatch the job dengan queue name yang spesifik
|
||||
$job = ExportStatementPeriodJob::dispatch($accountNumber, $period, $balance, $clientName)
|
||||
->onQueue($queueName);
|
||||
|
||||
Log::info("Statement export job dispatched successfully", [
|
||||
'job_id' => $job->job_id ?? null,
|
||||
'account' => $accountNumber,
|
||||
'period' => $period,
|
||||
'client' => $clientName,
|
||||
'queue_name' => $queueName
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Statement export jobs have been queued',
|
||||
'jobs' => array_map(function ($index, $jobId) use ($data) {
|
||||
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)
|
||||
'success' => true,
|
||||
'message' => 'Statement export job queued successfully',
|
||||
'data' => [
|
||||
'job_id' => $job->job_id ?? null,
|
||||
'account_number' => $accountNumber,
|
||||
'period' => $period,
|
||||
'client_name' => $clientName,
|
||||
'queue_name' => $queueName
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Failed to export statement", [
|
||||
'error' => $e->getMessage(),
|
||||
'account' => $accountNumber,
|
||||
'period' => $period,
|
||||
'queue_name' => $queueName
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to queue statement export job',
|
||||
'error' => $e->getMessage(),
|
||||
'queue_name' => $queueName
|
||||
]);
|
||||
}
|
||||
|
||||
function listAccount(){
|
||||
return [
|
||||
'PLUANG' => [
|
||||
'1080426085',
|
||||
'1080425781',
|
||||
],
|
||||
'OY' => [
|
||||
'1081647484',
|
||||
'1081647485',
|
||||
],
|
||||
'INDORAYA' => [
|
||||
'1083123710',
|
||||
'1083123711',
|
||||
'1083123712',
|
||||
'1083123713',
|
||||
'1083123714',
|
||||
'1083123715',
|
||||
'1083123716',
|
||||
'1083123718',
|
||||
'1083123719',
|
||||
'1083123721',
|
||||
'1083123722',
|
||||
'1083123723',
|
||||
'1083123724',
|
||||
'1083123726',
|
||||
'1083123727',
|
||||
'1083123728',
|
||||
'1083123730',
|
||||
'1083123731',
|
||||
'1083123732',
|
||||
'1083123734',
|
||||
'1083123735',
|
||||
],
|
||||
'TDC' => [
|
||||
'1086677889',
|
||||
'1086677890',
|
||||
'1086677891',
|
||||
'1086677892',
|
||||
'1086677893',
|
||||
'1086677894',
|
||||
'1086677895',
|
||||
'1086677896',
|
||||
'1086677897',
|
||||
],
|
||||
'ASIA_PARKING' => [
|
||||
'1080119298',
|
||||
'1080119361',
|
||||
'1080119425',
|
||||
'1080119387',
|
||||
'1082208069',
|
||||
],
|
||||
'DAU' => [
|
||||
'1085151668',
|
||||
],
|
||||
'EGR' => [
|
||||
'1085368601',
|
||||
],
|
||||
'SARANA_PACTINDO' => [
|
||||
'1078333878',
|
||||
],
|
||||
'SWADAYA_PANDU' => [
|
||||
'0081272689',
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
function listPeriod(){
|
||||
return [
|
||||
date('Ymd', strtotime('-1 day'))
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
function getAccountBalance($accountNumber, $period)
|
||||
{
|
||||
$accountBalance = AccountBalance::where('account_number', $accountNumber)
|
||||
->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()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
317
app/Http/Requests/BalanceSummaryRequest.php
Normal file
317
app/Http/Requests/BalanceSummaryRequest.php
Normal file
@@ -0,0 +1,317 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use Modules\Webstatement\Enums\ResponseCode;
|
||||
use Modules\Webstatement\Models\AccountBalance;
|
||||
|
||||
class BalanceSummaryRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
try {
|
||||
// Ambil parameter dari header
|
||||
$signature = $this->header('X-Signature');
|
||||
$timestamp = $this->header('X-Timestamp');
|
||||
$apiKey = $this->header('X-Api-Key');
|
||||
|
||||
// Validasi keberadaan header yang diperlukan
|
||||
if (!$signature || !$timestamp || !$apiKey) {
|
||||
Log::warning('HMAC validation failed - missing required headers', [
|
||||
'signature' => $signature,
|
||||
'timestamp' => $timestamp,
|
||||
'apiKey' => $apiKey,
|
||||
'ip' => $this->ip(),
|
||||
'user_agent' => $this->userAgent()
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validasi API key dari config
|
||||
$expectedApiKey = config('webstatement.api_key');
|
||||
if ($apiKey !== $expectedApiKey) {
|
||||
Log::warning('HMAC validation failed - invalid API key', [
|
||||
'provided_api_key' => $apiKey,
|
||||
'expected_api_key' => $expectedApiKey,
|
||||
'ip' => $this->ip(),
|
||||
'user_agent' => $this->userAgent()
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ambil secret key dari config
|
||||
$secretKey = config('webstatement.secret_key');
|
||||
|
||||
// Ambil parameter untuk validasi HMAC
|
||||
$httpMethod = $this->method();
|
||||
$relativeUrl = $this->path();
|
||||
$requestBody = $this->getContent();
|
||||
|
||||
// Validasi HMAC signature
|
||||
$isValid = validateHmac512(
|
||||
$httpMethod,
|
||||
$relativeUrl,
|
||||
$apiKey,
|
||||
$requestBody,
|
||||
$timestamp,
|
||||
$secretKey,
|
||||
$signature
|
||||
);
|
||||
|
||||
if (!$isValid) {
|
||||
Log::warning('HMAC validation failed - invalid signature', [
|
||||
'http_method' => $httpMethod,
|
||||
'relative_url' => $relativeUrl,
|
||||
'api_key' => $apiKey,
|
||||
'timestamp' => $timestamp,
|
||||
'ip' => $this->ip(),
|
||||
'user_agent' => $this->userAgent()
|
||||
]);
|
||||
}
|
||||
|
||||
return $isValid;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('HMAC validation error', [
|
||||
'error' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'ip' => $this->ip(),
|
||||
'user_agent' => $this->userAgent()
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'account_number' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:10',
|
||||
'min:10',
|
||||
'exists:account_balances,account_number',
|
||||
'regex:/^[0-9]+$/' // Numeric only
|
||||
],
|
||||
'start_date' => [
|
||||
'required',
|
||||
'date_format:Y-m-d',
|
||||
'before_or_equal:end_date',
|
||||
'after_or_equal:1900-01-01',
|
||||
'before_or_equal:today'
|
||||
],
|
||||
'end_date' => [
|
||||
'required',
|
||||
'date_format:Y-m-d',
|
||||
'after_or_equal:start_date',
|
||||
'after_or_equal:1900-01-01',
|
||||
'before_or_equal:today'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'account_number.exists' => 'Nomor rekening tidak ditemukan.',
|
||||
'account_number.required' => 'Nomor rekening wajib diisi.',
|
||||
'account_number.string' => 'Nomor rekening harus berupa teks.',
|
||||
'account_number.max' => 'Nomor rekening maksimal :max karakter.',
|
||||
'account_number.min' => 'Nomor rekening minimal :min karakter.',
|
||||
'account_number.regex' => 'Nomor rekening hanya boleh mengandung angka.',
|
||||
'start_date.required' => 'Tanggal awal wajib diisi.',
|
||||
'start_date.date_format' => 'Format tanggal awal harus YYYY-MM-DD.',
|
||||
'start_date.before_or_equal' => 'Tanggal awal harus sebelum atau sama dengan tanggal akhir.',
|
||||
'end_date.required' => 'Tanggal akhir wajib diisi.',
|
||||
'end_date.date_format' => 'Format tanggal akhir harus YYYY-MM-DD.',
|
||||
'end_date.after_or_equal' => 'Tanggal akhir harus sesudah atau sama dengan tanggal awal.',
|
||||
'end_date.before_or_equal' => 'Tanggal akhir harus sebelum atau sama dengan hari ini.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a failed validation attempt.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Validation\Validator $validator
|
||||
* @return void
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
protected function failedValidation($validator)
|
||||
{
|
||||
$errors = $validator->errors();
|
||||
|
||||
if($errors->has('account_number') && $errors->first('account_number') === 'Nomor rekening tidak ditemukan.') {
|
||||
throw new HttpResponseException(
|
||||
response()->json(
|
||||
ResponseCode::ACCOUNT_NOT_FOUND->toResponse(
|
||||
null,
|
||||
'Nomor rekening tidak ditemukan'
|
||||
),
|
||||
ResponseCode::ACCOUNT_NOT_FOUND->getHttpStatus()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
throw new HttpResponseException(
|
||||
response()->json(
|
||||
ResponseCode::INVALID_FIELD->toResponse(
|
||||
['errors' => $errors->all()],
|
||||
'Validasi gagal'
|
||||
),
|
||||
ResponseCode::INVALID_FIELD->getHttpStatus()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle failed authorization.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
protected function failedAuthorization()
|
||||
{
|
||||
$xApiKey = $this->header('X-Api-Key');
|
||||
$xSignature = $this->header('X-Signature');
|
||||
$xTimestamp = $this->header('X-Timestamp');
|
||||
|
||||
$expectedApiKey = config('webstatement.api_key');
|
||||
|
||||
if(!$xApiKey){
|
||||
throw new HttpResponseException(
|
||||
response()->json(
|
||||
ResponseCode::INVALID_FIELD->toResponse(
|
||||
['errors' => ['X-Api-Key' => 'API Key wajib diisi']],
|
||||
'Validasi gagal'
|
||||
),
|
||||
ResponseCode::INVALID_FIELD->getHttpStatus()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if(!$xSignature){
|
||||
throw new HttpResponseException(
|
||||
response()->json(
|
||||
ResponseCode::INVALID_FIELD->toResponse(
|
||||
['errors' => ['X-Signature' => 'Signature wajib diisi']],
|
||||
'Validasi gagal'
|
||||
),
|
||||
ResponseCode::INVALID_FIELD->getHttpStatus()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if(!$xTimestamp){
|
||||
throw new HttpResponseException(
|
||||
response()->json(
|
||||
ResponseCode::INVALID_FIELD->toResponse(
|
||||
['errors' => ['X-Timestamp' => 'Timestamp wajib diisi']],
|
||||
'Validasi gagal'
|
||||
),
|
||||
ResponseCode::INVALID_FIELD->getHttpStatus()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Validasi format timestamp ISO 8601
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/', $xTimestamp)) {
|
||||
throw new HttpResponseException(
|
||||
response()->json(
|
||||
ResponseCode::INVALID_FIELD->toResponse(
|
||||
['errors' => ['X-Timestamp' => 'Format timestamp tidak valid. Gunakan format ISO 8601 (YYYY-MM-DDTHH:MM:SS.sssZ)']],
|
||||
'Validasi gagal'
|
||||
),
|
||||
ResponseCode::INVALID_FIELD->getHttpStatus()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Validasi timestamp tidak lebih dari 5 menit dari waktu sekarang
|
||||
try {
|
||||
$timestamp = new \DateTime($xTimestamp);
|
||||
$now = new \DateTime();
|
||||
$diff = $now->getTimestamp() - $timestamp->getTimestamp();
|
||||
|
||||
if (abs($diff) > 300) { // 5 menit = 300 detik
|
||||
throw new HttpResponseException(
|
||||
response()->json(
|
||||
ResponseCode::INVALID_FIELD->toResponse(
|
||||
['errors' => ['X-Timestamp' => 'Timestamp expired. Maksimal selisih 5 menit dari waktu sekarang']],
|
||||
'Validasi gagal'
|
||||
),
|
||||
ResponseCode::INVALID_FIELD->getHttpStatus()
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
throw new HttpResponseException(
|
||||
response()->json(
|
||||
ResponseCode::INVALID_FIELD->toResponse(
|
||||
['errors' => ['X-Timestamp' => 'Timestamp tidak dapat diproses']],
|
||||
'Validasi gagal'
|
||||
),
|
||||
ResponseCode::INVALID_FIELD->getHttpStatus()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Cek apakah ini karena invalid API key
|
||||
if ($xApiKey !== $expectedApiKey) {
|
||||
throw new HttpResponseException(
|
||||
response()->json(
|
||||
ResponseCode::INVALID_TOKEN->toResponse(
|
||||
null,
|
||||
'API Key tidak valid'
|
||||
),
|
||||
ResponseCode::INVALID_TOKEN->getHttpStatus()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Untuk kasus HMAC signature tidak valid
|
||||
throw new HttpResponseException(
|
||||
response()->json(
|
||||
ResponseCode::UNAUTHORIZED->toResponse(
|
||||
null,
|
||||
'Signature tidak valid'
|
||||
),
|
||||
ResponseCode::UNAUTHORIZED->getHttpStatus()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
Log::info('Balance summary request received', [
|
||||
'input' => $this->all(),
|
||||
'ip' => $this->ip(),
|
||||
'user_agent' => $this->userAgent()
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -21,38 +21,71 @@ class PrintStatementRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'account_number' => ['required', 'string'],
|
||||
'branch_code' => ['required', 'string'],
|
||||
// account_number required jika stmt_sent_type tidak diisi atau kosong
|
||||
'account_number' => [
|
||||
function ($attribute, $value, $fail) {
|
||||
$stmtSentType = $this->input('stmt_sent_type');
|
||||
|
||||
// Jika stmt_sent_type kosong atau tidak ada, maka account_number wajib diisi
|
||||
if (empty($stmtSentType) || (is_array($stmtSentType) && count(array_filter($stmtSentType)) === 0)) {
|
||||
if (empty($value)) {
|
||||
$fail('Account number is required when statement type is not specified.');
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
'stmt_sent_type' => ['nullable', 'array'],
|
||||
'stmt_sent_type.*' => ['string', 'in:ALL,BY.EMAIL,BY.MAIL.TO.DOM.ADDR,BY.MAIL.TO.KTP.ADDR,NO.PRINT,PRINT'],
|
||||
'is_period_range' => ['sometimes', 'boolean'],
|
||||
'email' => ['nullable', 'email'],
|
||||
'email_sent_at' => ['nullable', 'timestamp'],
|
||||
'request_type' => ['sometimes', 'string', 'in:single_account,branch,all_branches'],
|
||||
'request_type' => ['sometimes', 'string', 'in:single_account,branch,all_branches,multi_account'],
|
||||
'batch_id' => ['nullable', 'string'],
|
||||
// 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) {
|
||||
$query = Statement::where('account_number', $this->input('account_number'))
|
||||
->where('authorization_status', '!=', 'rejected')
|
||||
->where('is_available', true)
|
||||
->where('period_from', $value);
|
||||
// Hanya cek duplikasi jika account_number ada
|
||||
if (!empty($this->input('account_number'))) {
|
||||
$query = Statement::where('account_number', $this->input('account_number'))
|
||||
//->where('authorization_status', '!=', 'rejected')
|
||||
->where(function($query) {
|
||||
$query->where('is_available', true)
|
||||
->orWhere('is_generated', true);
|
||||
})
|
||||
->where('user_id', $this->user()->id)
|
||||
->where('period_from', $value);
|
||||
|
||||
// If this is an update request, exclude the current record
|
||||
if ($this->route('statement')) {
|
||||
$query->where('id', '!=', $this->route('statement'));
|
||||
}
|
||||
// If this is an update request, exclude the current record
|
||||
if ($this->route('statement')) {
|
||||
$query->where('id', '!=', $this->route('statement'));
|
||||
}
|
||||
|
||||
// If period_to is provided, check for overlapping periods
|
||||
if ($this->input('period_to')) {
|
||||
$query->where(function ($q) use ($value) {
|
||||
$q->where('period_from', '<=', $this->input('period_to'))
|
||||
->where('period_to', '>=', $value);
|
||||
});
|
||||
}
|
||||
// If period_to is provided, check for overlapping periods
|
||||
if ($this->input('period_to')) {
|
||||
$query->where(function ($q) use ($value) {
|
||||
$q->where('period_from', '<=', $this->input('period_to'))
|
||||
->where('period_to', '>=', $value);
|
||||
});
|
||||
}
|
||||
|
||||
if ($query->exists()) {
|
||||
$fail('A statement request with this account number and period already exists.');
|
||||
if ($query->exists()) {
|
||||
//$fail('A statement request with this account number and period already exists.');
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -77,13 +110,17 @@ class PrintStatementRequest extends FormRequest
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'account_number.required' => 'Account number is required',
|
||||
'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, or all_branches',
|
||||
'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',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
44
app/Http/Resources/BalanceSummaryResource.php
Normal file
44
app/Http/Resources/BalanceSummaryResource.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class BalanceSummaryResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'account_number' => $this['account_number'],
|
||||
'period' => [
|
||||
'start_date' => $this['period']['start_date'],
|
||||
'end_date' => $this['period']['end_date'],
|
||||
],
|
||||
'opening_balance' => [
|
||||
'date' => $this['opening_balance']['date'],
|
||||
'balance' => $this['opening_balance']['balance'],
|
||||
'formatted_balance' => number_format($this['opening_balance']['balance'], 2, ',', '.'),
|
||||
],
|
||||
'closing_balance' => [
|
||||
'date' => $this['closing_balance']['date'],
|
||||
'balance' => $this['closing_balance']['balance'],
|
||||
'formatted_balance' => number_format($this['closing_balance']['balance'], 2, ',', '.'),
|
||||
'base_balance' => [
|
||||
'date' => $this['closing_balance']['base_balance']['date'],
|
||||
'balance' => $this['closing_balance']['base_balance']['balance'],
|
||||
'formatted_balance' => number_format($this['closing_balance']['base_balance']['balance'], 2, ',', '.'),
|
||||
],
|
||||
'transactions_on_end_date' => $this['closing_balance']['transactions_on_end_date'],
|
||||
'formatted_transactions_on_end_date' => number_format($this['closing_balance']['transactions_on_end_date'], 2, ',', '.'),
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
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()
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -81,10 +81,10 @@
|
||||
$existingDataCount = $this->getExistingProcessedCount($accountQuery);
|
||||
|
||||
// Hanya proses jika data belum lengkap diproses
|
||||
if ($existingDataCount !== $totalCount) {
|
||||
//if ($existingDataCount !== $totalCount) {
|
||||
$this->deleteExistingProcessedData($accountQuery);
|
||||
$this->processAndSaveStatementEntries($totalCount);
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
||||
private function getTotalEntryCount(array $criteria)
|
||||
@@ -156,6 +156,7 @@
|
||||
'description' => $this->generateNarrative($item),
|
||||
'end_balance' => $runningBalance,
|
||||
'actual_date' => $actualDate,
|
||||
'recipt_no' => $item->ft?->recipt_no ?? '-',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
@@ -242,9 +243,9 @@
|
||||
$narr[] = $item->narrative;
|
||||
}
|
||||
|
||||
if ($item->ft?->recipt_no) {
|
||||
/*if ($item->ft?->recipt_no) {
|
||||
$narr[] = 'Receipt No: ' . $item->ft->recipt_no;
|
||||
}
|
||||
}*/
|
||||
|
||||
return implode(' ', array_filter($narr));
|
||||
}
|
||||
@@ -356,23 +357,21 @@
|
||||
/**
|
||||
* Export processed data to CSV file
|
||||
*/
|
||||
private function exportToCsv()
|
||||
: void
|
||||
private function exportToCsv(): void
|
||||
{
|
||||
// Determine the base path based on client
|
||||
$basePath = !empty($this->client)
|
||||
? "statements/{$this->client}"
|
||||
: "statements";
|
||||
? "partners/{$this->client}"
|
||||
: "partners";
|
||||
|
||||
// Create client directory if it doesn't exist
|
||||
if (!empty($this->client)) {
|
||||
Storage::disk($this->disk)->makeDirectory($basePath);
|
||||
}
|
||||
|
||||
// Create account directory
|
||||
$accountPath = "{$basePath}/{$this->account_number}";
|
||||
|
||||
// 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
|
||||
@@ -380,13 +379,38 @@
|
||||
Storage::disk($this->disk)->delete($filePath);
|
||||
}
|
||||
|
||||
$csvContent = "NO|TRANSACTION.DATE|REFERENCE.NUMBER|TRANSACTION.AMOUNT|TRANSACTION.TYPE|DESCRIPTION|END.BALANCE|ACTUAL.DATE\n";
|
||||
|
||||
// Ambil data yang sudah diproses dalam chunk untuk mengurangi penggunaan memori
|
||||
// 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 (&$csvContent, $filePath) {
|
||||
->chunk($this->chunkSize, function ($statements) use ($filePath) {
|
||||
$csvContent = '';
|
||||
foreach ($statements as $statement) {
|
||||
$csvContent .= implode('|', [
|
||||
$statement->sequence_no,
|
||||
@@ -396,16 +420,36 @@
|
||||
$statement->transaction_type,
|
||||
$statement->description,
|
||||
$statement->end_balance,
|
||||
$statement->actual_date
|
||||
$statement->actual_date,
|
||||
$statement->recipt_no
|
||||
]) . "\n";
|
||||
}
|
||||
|
||||
// Tulis ke file secara bertahap untuk mengurangi penggunaan memori
|
||||
Storage::disk($this->disk)->append($filePath, $csvContent);
|
||||
$csvContent = ''; // Reset content setelah ditulis
|
||||
// Append ke file
|
||||
if (!empty($csvContent)) {
|
||||
Storage::disk($this->disk)->append($filePath, $csvContent);
|
||||
}
|
||||
});
|
||||
|
||||
Log::info("Statement exported to {$this->disk} disk: {$filePath}");
|
||||
// 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}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,19 +4,33 @@ namespace Modules\Webstatement\Jobs;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Bus\Queueable;
|
||||
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 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;
|
||||
use Owenoj\PDFPasswordProtect\Facade\PDFPasswordProtect;
|
||||
|
||||
class ExportStatementPeriodJob implements ShouldQueue
|
||||
{
|
||||
@@ -24,6 +38,7 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
|
||||
protected $account_number;
|
||||
protected $period; // Format: YYYYMM (e.g., 202505)
|
||||
protected $endPeriod; // Format: YYYYMM (e.g., 202505)
|
||||
protected $saldo;
|
||||
protected $disk;
|
||||
protected $client;
|
||||
@@ -31,6 +46,8 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
protected $chunkSize = 1000;
|
||||
protected $startDate;
|
||||
protected $endDate;
|
||||
protected $toCsv;
|
||||
protected $statementId;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
@@ -41,30 +58,33 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
* @param string $client
|
||||
* @param string $disk
|
||||
*/
|
||||
public function __construct(string $account_number, string $period, string $saldo, string $client = '', string $disk = 'local')
|
||||
public function __construct(int $statementId, string $account_number, string $period, string $endPeriod, string $saldo, string $client = '', string $disk = 'local', bool $toCsv = true)
|
||||
{
|
||||
$this->statementId = $statementId;
|
||||
$this->account_number = $account_number;
|
||||
$this->period = $period;
|
||||
$this->endPeriod = $endPeriod;
|
||||
$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();
|
||||
$this->formatPeriodForFolder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate start and end dates for the given period
|
||||
*/
|
||||
private function calculatePeriodDates(): void
|
||||
private function formatPeriodForFolder(): void
|
||||
{
|
||||
$year = substr($this->period, 0, 4);
|
||||
$month = substr($this->period, 4, 2);
|
||||
|
||||
// Special case for May 2025 - start from 12th
|
||||
if ($this->period === '202505') {
|
||||
$this->startDate = Carbon::createFromDate($year, $month, 12)->startOfDay();
|
||||
$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();
|
||||
@@ -72,6 +92,13 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
|
||||
// End date is always the last day of the month
|
||||
$this->endDate = Carbon::createFromDate($year, $month, 1)->endOfMonth()->endOfDay();
|
||||
|
||||
// If endPeriod is provided, use it instead of endDate
|
||||
if($this->endPeriod){
|
||||
$year = substr($this->endPeriod, 0, 4);
|
||||
$month = substr($this->endPeriod, 4, 2);
|
||||
$this->endDate = Carbon::createFromDate($year, $month, 1)->endOfMonth()->endOfDay();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +111,12 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
Log::info("Date range: {$this->startDate->format('Y-m-d')} to {$this->endDate->format('Y-m-d')}");
|
||||
|
||||
$this->processStatementData();
|
||||
$this->exportToCsv();
|
||||
if($this->toCsv){
|
||||
$this->exportToCsv();
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -112,12 +144,20 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
|
||||
private function getTotalEntryCount(): int
|
||||
{
|
||||
return StmtEntry::where('account_number', $this->account_number)
|
||||
->whereBetween('date_time', [
|
||||
$this->startDate->format('ymdHi'),
|
||||
$this->endDate->format('ymdHi')
|
||||
])
|
||||
->count();
|
||||
$query = StmtEntry::where('account_number', $this->account_number)
|
||||
->whereBetween('booking_date', [
|
||||
$this->startDate->format('Ymd'),
|
||||
$this->endDate->format('Ymd')
|
||||
]);
|
||||
|
||||
Log::info("Getting total entry count with query: " . $query->toSql(), [
|
||||
'bindings' => $query->getBindings(),
|
||||
'account' => $this->account_number,
|
||||
'start_date' => $this->startDate->format('Ymd'),
|
||||
'end_date' => $this->endDate->format('Ymd')
|
||||
]);
|
||||
|
||||
return $query->count();
|
||||
}
|
||||
|
||||
private function getExistingProcessedCount(array $criteria): int
|
||||
@@ -141,11 +181,11 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
|
||||
Log::info("Processing {$totalCount} statement entries for account: {$this->account_number}");
|
||||
|
||||
StmtEntry::with(['ft', 'transaction'])
|
||||
$entry = StmtEntry::with(['ft', 'transaction'])
|
||||
->where('account_number', $this->account_number)
|
||||
->whereBetween('date_time', [
|
||||
$this->startDate->format('ymdHi'),
|
||||
$this->endDate->format('ymdHi')
|
||||
->whereBetween('booking_date', [
|
||||
$this->startDate->format('Ymd'),
|
||||
$this->endDate->format('Ymd')
|
||||
])
|
||||
->orderBy('date_time', 'ASC')
|
||||
->orderBy('trans_reference', 'ASC')
|
||||
@@ -156,6 +196,13 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
DB::table('processed_statements')->insert($processedData);
|
||||
}
|
||||
});
|
||||
|
||||
if($entry){
|
||||
$printLog = PrintStatementLog::find($this->statementId);
|
||||
if($printLog){
|
||||
$printLog->update(['is_generated' => true]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function prepareProcessedData($entries, &$runningBalance, &$globalSequence): array
|
||||
@@ -163,26 +210,31 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
$processedData = [];
|
||||
|
||||
foreach ($entries as $item) {
|
||||
$globalSequence++;
|
||||
$runningBalance += (float) $item->amount_lcy;
|
||||
$globalSequence++;
|
||||
|
||||
$transactionDate = $this->formatTransactionDate($item);
|
||||
$actualDate = $this->formatActualDate($item);
|
||||
$actualDate = $this->formatActualDate($item);
|
||||
|
||||
$processedData[] = [
|
||||
'account_number' => $this->account_number,
|
||||
'period' => $this->period,
|
||||
'sequence_no' => $globalSequence,
|
||||
'transaction_date' => $transactionDate,
|
||||
'reference_number' => $item->trans_reference,
|
||||
'transaction_amount' => $item->amount_lcy,
|
||||
'transaction_type' => $item->amount_lcy < 0 ? 'D' : 'C',
|
||||
'description' => $this->generateNarrative($item),
|
||||
'end_balance' => $runningBalance,
|
||||
'actual_date' => $actualDate,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
$amount = $item->amount_fcy;
|
||||
if($item->currency=='IDR'){
|
||||
$amount = $item->amount_lcy;
|
||||
}
|
||||
$runningBalance += (float) $amount;
|
||||
|
||||
$processedData[] = [
|
||||
'account_number' => $this->account_number,
|
||||
'period' => $this->period,
|
||||
'sequence_no' => $globalSequence,
|
||||
'transaction_date' => $item->booking_date,
|
||||
'reference_number' => $item->trans_reference,
|
||||
'transaction_amount' => $amount,
|
||||
'transaction_type' => $amount < 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;
|
||||
@@ -378,26 +430,209 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
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 dengan format folder baru
|
||||
$periodPath = formatPeriodForFolder($this->period);
|
||||
$storagePath = "statements/{$periodPath}/{$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;
|
||||
$endPeriod = $this->endPeriod;
|
||||
|
||||
// Render HTML view
|
||||
$html = view('webstatement::statements.stmt', compact(
|
||||
'stmtEntries',
|
||||
'account',
|
||||
'customer',
|
||||
'headerTableBg',
|
||||
'branch',
|
||||
'period',
|
||||
'endPeriod',
|
||||
'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
|
||||
$basePath = !empty($this->client)
|
||||
? "statements/{$this->client}"
|
||||
: "statements";
|
||||
$account = Account::where('account_number', $this->account_number)->first();
|
||||
|
||||
// Create client directory if it doesn't exist
|
||||
if (!empty($this->client)) {
|
||||
Storage::disk($this->disk)->makeDirectory($basePath);
|
||||
}
|
||||
$periodPath = formatPeriodForFolder($this->period);
|
||||
$storagePath = "statements/{$periodPath}/{$account->branch_code}";
|
||||
Storage::disk($this->disk)->makeDirectory($storagePath);
|
||||
|
||||
// Create account directory
|
||||
$accountPath = "{$basePath}/{$this->account_number}";
|
||||
Storage::disk($this->disk)->makeDirectory($accountPath);
|
||||
|
||||
$filePath = "{$accountPath}/{$this->fileName}";
|
||||
$filePath = "{$storagePath}/{$this->fileName}";
|
||||
|
||||
// Delete existing file if it exists
|
||||
if (Storage::disk($this->disk)->exists($filePath)) {
|
||||
@@ -426,7 +661,6 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
|
||||
// Write to file incrementally to reduce memory usage
|
||||
Storage::disk($this->disk)->append($filePath, $csvContent);
|
||||
$csvContent = ''; // Reset content after writing
|
||||
});
|
||||
|
||||
Log::info("Statement exported to {$this->disk} disk: {$filePath}");
|
||||
|
||||
@@ -161,25 +161,51 @@
|
||||
|
||||
/**
|
||||
* Get eligible ATM cards from database
|
||||
* Mengambil data kartu ATM yang memenuhi syarat untuk dikenakan biaya admin
|
||||
* dengan filter khusus untuk mengecualikan product_code 6021 yang ctdesc nya gold
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
private function getEligibleAtmCards()
|
||||
{
|
||||
// Log: Memulai proses pengambilan data kartu ATM yang eligible
|
||||
Log::info('Starting to fetch eligible ATM cards', [
|
||||
'periode' => $this->periode
|
||||
]);
|
||||
|
||||
$cardTypes = array_keys($this->getDefaultFees());
|
||||
|
||||
return Atmcard::where('crsts', 1)
|
||||
->whereNotNull('accflag')
|
||||
->where('accflag', '!=', '')
|
||||
->where('flag','')
|
||||
->whereNotNull('branch')
|
||||
->where('branch', '!=', '')
|
||||
->whereNotNull('currency')
|
||||
->where('currency', '!=', '')
|
||||
->whereIn('ctdesc', $cardTypes)
|
||||
->whereNotIn('product_code',['6002','6004','6042','6031'])
|
||||
->where('branch','!=','ID0019999')
|
||||
->get();
|
||||
$query = Atmcard::where('crsts', 1)
|
||||
->whereNotNull('accflag')
|
||||
->where('accflag', '!=', '')
|
||||
->where('flag','')
|
||||
->whereNotNull('branch')
|
||||
->where('branch', '!=', '')
|
||||
->whereNotNull('currency')
|
||||
->where('currency', '!=', '')
|
||||
->whereIn('ctdesc', $cardTypes)
|
||||
->whereNotIn('product_code',['6031','6021','6042']) // Hapus 6021 dari sini
|
||||
->where('branch','!=','ID0019999')
|
||||
->where(function($query) {
|
||||
$query->whereNot(function($q) {
|
||||
$q->where('product_code', '6004')
|
||||
->where('ctdesc', 'CLASSIC');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
$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' => ['6021','6042','6031'],
|
||||
'special_filter' => 'product_code 6004 dengan ctdesc classic dikecualikan'
|
||||
]);
|
||||
|
||||
return $cards;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,6 +249,8 @@
|
||||
: array
|
||||
{
|
||||
$today = date('Ymd');
|
||||
// Generate hash string unik 16 digit
|
||||
$uniqueHash = substr(hash('sha256', $card->crdno . $today . microtime(true) . uniqid()), 0, 16);
|
||||
|
||||
return [
|
||||
'',
|
||||
@@ -244,7 +272,8 @@
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'ACAT'
|
||||
'ACAT',
|
||||
$uniqueHash
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
641
app/Jobs/GenerateClosingBalanceReportJob.php
Normal file
641
app/Jobs/GenerateClosingBalanceReportJob.php
Normal file
@@ -0,0 +1,641 @@
|
||||
<?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, SerializesModels};
|
||||
use Illuminate\Support\Facades\{DB, Log, Storage};
|
||||
use Modules\Webstatement\Models\{AccountBalance,
|
||||
ClosingBalanceReportLog,
|
||||
ProcessedClosingBalance,
|
||||
StmtEntry,
|
||||
StmtEntryDetail};
|
||||
|
||||
/**
|
||||
* Job untuk generate laporan closing balance dengan optimasi performa
|
||||
* Menggunakan database staging sebelum export CSV
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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 dengan optimasi performa
|
||||
*/
|
||||
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 optimized closing balance report generation', [
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period,
|
||||
'group_name' => $this->groupName,
|
||||
'report_log_id' => $this->reportLogId
|
||||
]);
|
||||
|
||||
// Update status to processing
|
||||
$reportLog->update([
|
||||
'status' => 'processing',
|
||||
'updated_at' => now()
|
||||
]);
|
||||
|
||||
// Gunakan satu transaksi untuk seluruh proses
|
||||
DB::transaction(function () use ($reportLog) {
|
||||
// Step 1: Process and save to database (fast)
|
||||
$this->processAndSaveClosingBalanceData();
|
||||
|
||||
// Step 2: Export from database to CSV (fast)
|
||||
$filePath = $this->exportFromDatabaseToCsv();
|
||||
|
||||
// Get record count from database
|
||||
$recordCount = $this->getProcessedRecordCount();
|
||||
|
||||
// Update report log with success
|
||||
$reportLog->update([
|
||||
'status' => 'completed',
|
||||
'file_path' => $filePath,
|
||||
'file_size' => Storage::disk($this->disk)->size($filePath),
|
||||
'record_count' => $recordCount,
|
||||
'updated_at' => now()
|
||||
]);
|
||||
|
||||
Log::info('Optimized closing balance report generation completed successfully', [
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period,
|
||||
'file_path' => $filePath,
|
||||
'record_count' => $recordCount
|
||||
]);
|
||||
});
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error generating optimized 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process and save closing balance data to database dengan proteksi duplikasi
|
||||
* Memproses dan menyimpan data closing balance dengan perlindungan terhadap duplikasi
|
||||
*/
|
||||
private function processAndSaveClosingBalanceData(): void
|
||||
{
|
||||
$criteria = [
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period,
|
||||
'group_name' => $this->groupName
|
||||
];
|
||||
|
||||
// HAPUS DB::beginTransaction() - sudah ada di handle()
|
||||
|
||||
// Sederhana: hapus data existing terlebih dahulu seperti ExportStatementJob
|
||||
$this->deleteExistingProcessedData($criteria);
|
||||
|
||||
// Get opening balance
|
||||
$runningBalance = $this->getOpeningBalance();
|
||||
$sequenceNo = 0;
|
||||
|
||||
Log::info('Starting to process closing balance data', [
|
||||
'opening_balance' => $runningBalance,
|
||||
'criteria' => $criteria
|
||||
]);
|
||||
|
||||
// Build query yang sederhana tanpa eliminasi duplicate rumit
|
||||
$query = $this->buildTransactionQuery();
|
||||
|
||||
// Proses dan insert data dengan batch updateOrCreate untuk efisiensi
|
||||
$query->chunk($this->chunkSize, function ($transactions) use (&$runningBalance, &$sequenceNo) {
|
||||
$processedData = $this->prepareProcessedClosingBalanceData($transactions, $runningBalance, $sequenceNo);
|
||||
|
||||
if (!empty($processedData)) {
|
||||
// Gunakan batch processing untuk updateOrCreate
|
||||
$this->batchUpdateOrCreate($processedData);
|
||||
}
|
||||
});
|
||||
|
||||
// HAPUS DB::commit() - akan di-handle di handle()
|
||||
|
||||
$recordCount = $this->getProcessedRecordCount();
|
||||
Log::info('Closing balance data processing completed successfully', [
|
||||
'final_sequence' => $sequenceNo,
|
||||
'final_balance' => $runningBalance,
|
||||
'record_count' => $recordCount
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 dengan pendekatan sederhana tanpa eliminasi duplicate rumit
|
||||
*/
|
||||
private function buildTransactionQuery()
|
||||
{
|
||||
Log::info('Building transaction query', [
|
||||
'group_name' => $this->groupName,
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period
|
||||
]);
|
||||
|
||||
$modelClass = $this->getModelByGroup();
|
||||
|
||||
$query = $modelClass::select([
|
||||
'id',
|
||||
'trans_reference',
|
||||
'booking_date',
|
||||
'amount_lcy',
|
||||
'date_time'
|
||||
])
|
||||
->with([
|
||||
'ft' => function ($query) {
|
||||
$query->select('ref_no', 'date_time', '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', 'ref_no', 'merchant_id', 'term_id');
|
||||
},
|
||||
'dc' => function ($query) {
|
||||
$query->select('id', 'date_time');
|
||||
}
|
||||
])
|
||||
->where('account_number', $this->accountNumber)
|
||||
->where('booking_date', $this->period)
|
||||
->orderBy('booking_date')
|
||||
->orderBy('date_time');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare processed closing balance data tanpa validasi duplikasi
|
||||
*/
|
||||
private function prepareProcessedClosingBalanceData($transactions, &$runningBalance, &$sequenceNo)
|
||||
: array
|
||||
{
|
||||
$processedData = [];
|
||||
|
||||
foreach ($transactions as $transaction) {
|
||||
$sequenceNo++;
|
||||
|
||||
// Process transaction data
|
||||
$processedTransactionData = $this->processTransactionData($transaction);
|
||||
|
||||
// Update running balance
|
||||
$amount = (float) $transaction->amount_lcy;
|
||||
$runningBalance += $amount;
|
||||
|
||||
// Format transaction date
|
||||
$transactionDate = $this->formatDateTime($processedTransactionData['date_time']);
|
||||
|
||||
// Prepare data untuk database insert tanpa unique_hash
|
||||
$processedData[] = [
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period,
|
||||
'group_name' => $this->groupName,
|
||||
'sequence_no' => $sequenceNo,
|
||||
'trans_reference' => $processedTransactionData['trans_reference'],
|
||||
'booking_date' => $processedTransactionData['booking_date'],
|
||||
'transaction_date' => $transactionDate,
|
||||
'amount_lcy' => $processedTransactionData['amount_lcy'],
|
||||
'debit_acct_no' => $processedTransactionData['debit_acct_no'],
|
||||
'debit_value_date' => $processedTransactionData['debit_value_date'],
|
||||
'debit_amount' => $processedTransactionData['debit_amount'],
|
||||
'credit_acct_no' => $processedTransactionData['credit_acct_no'],
|
||||
'bif_rcv_acct' => $processedTransactionData['bif_rcv_acct'],
|
||||
'bif_rcv_name' => $processedTransactionData['bif_rcv_name'],
|
||||
'credit_value_date' => $processedTransactionData['credit_value_date'],
|
||||
'credit_amount' => $processedTransactionData['credit_amount'],
|
||||
'at_unique_id' => $processedTransactionData['at_unique_id'],
|
||||
'bif_ref_no' => $processedTransactionData['bif_ref_no'],
|
||||
'atm_order_id' => $processedTransactionData['atm_order_id'],
|
||||
'recipt_no' => $processedTransactionData['recipt_no'],
|
||||
'api_iss_acct' => $processedTransactionData['api_iss_acct'],
|
||||
'api_benff_acct' => $processedTransactionData['api_benff_acct'],
|
||||
'authoriser' => $processedTransactionData['authoriser'],
|
||||
'remarks' => $processedTransactionData['remarks'],
|
||||
'payment_details' => $processedTransactionData['payment_details'],
|
||||
'ref_no' => $processedTransactionData['ref_no'],
|
||||
'merchant_id' => $processedTransactionData['merchant_id'],
|
||||
'term_id' => $processedTransactionData['term_id'],
|
||||
'closing_balance' => $runningBalance,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
Log::info('Processed closing balance data prepared', [
|
||||
'total_records' => count($processedData)
|
||||
]);
|
||||
|
||||
return $processedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 from database to CSV (very fast)
|
||||
*/
|
||||
private function exportFromDatabaseToCsv()
|
||||
: string
|
||||
{
|
||||
Log::info('Starting CSV export from database for closing balance report', [
|
||||
'account_number' => $this->accountNumber,
|
||||
'period' => $this->period,
|
||||
'group_name' => $this->groupName
|
||||
]);
|
||||
|
||||
// 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}_{$this->groupName}.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";
|
||||
Storage::disk($this->disk)->put($filePath, $csvContent);
|
||||
|
||||
// Inisialisasi counter untuk sequence number
|
||||
$sequenceCounter = 1;
|
||||
$processedHashes = [];
|
||||
|
||||
ProcessedClosingBalance::where('account_number', $this->accountNumber)
|
||||
->where('period', $this->period)
|
||||
->where('group_name', $this->groupName)
|
||||
->orderBy('sequence_no')
|
||||
->chunk($this->chunkSize, function ($records) use ($filePath, &$sequenceCounter, &$processedHashes) {
|
||||
$csvContent = [];
|
||||
foreach ($records as $record) {
|
||||
// Pengecekan unique_hash: skip jika sudah diproses
|
||||
if (in_array($record->unique_hash, $processedHashes)) {
|
||||
Log::debug('Skipping duplicate unique_hash in CSV export', [
|
||||
'unique_hash' => $record->unique_hash,
|
||||
'trans_reference' => $record->trans_reference
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tandai unique_hash sebagai sudah diproses
|
||||
$processedHashes[] = $record->unique_hash;
|
||||
|
||||
$csvRow = [
|
||||
$sequenceCounter++,
|
||||
// Gunakan counter yang bertambah, bukan sequence_no dari database
|
||||
$record->trans_reference ?? '',
|
||||
$record->booking_date ?? '',
|
||||
$record->transaction_date ?? '',
|
||||
$record->amount_lcy ?? '',
|
||||
$record->debit_acct_no ?? '',
|
||||
$record->debit_value_date ?? '',
|
||||
$record->debit_amount ?? '',
|
||||
$record->credit_acct_no ?? '',
|
||||
$record->bif_rcv_acct ?? '',
|
||||
$record->bif_rcv_name ?? '',
|
||||
$record->credit_value_date ?? '',
|
||||
$record->credit_amount ?? '',
|
||||
$record->at_unique_id ?? '',
|
||||
$record->bif_ref_no ?? '',
|
||||
$record->atm_order_id ?? '',
|
||||
$record->recipt_no ?? '',
|
||||
$record->api_iss_acct ?? '',
|
||||
$record->api_benff_acct ?? '',
|
||||
$record->authoriser ?? '',
|
||||
$record->remarks ?? '',
|
||||
$record->payment_details ?? '',
|
||||
$record->ref_no ?? '',
|
||||
$record->merchant_id ?? '',
|
||||
$record->term_id ?? '',
|
||||
$record->closing_balance ?? ''
|
||||
];
|
||||
|
||||
$csvContent .= implode('|', $csvRow) . "\n";
|
||||
}
|
||||
|
||||
if (!empty($csvContent)) {
|
||||
Storage::disk($this->disk)->append($filePath, $csvContent);
|
||||
|
||||
Log::debug('CSV content appended', [
|
||||
'records_processed' => substr_count($csvContent, "\n"),
|
||||
'current_sequence' => $sequenceCounter - 1
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify file creation
|
||||
if (!Storage::disk($this->disk)->exists($filePath)) {
|
||||
throw new Exception("Failed to create CSV file: {$filePath}");
|
||||
}
|
||||
|
||||
Log::info('CSV export from database completed successfully', [
|
||||
'file_path' => $filePath,
|
||||
'file_size' => Storage::disk($this->disk)->size($filePath)
|
||||
]);
|
||||
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get processed record count
|
||||
*/
|
||||
private function getProcessedRecordCount()
|
||||
: int
|
||||
{
|
||||
return ProcessedClosingBalance::where('account_number', $this->accountNumber)
|
||||
->where('period', $this->period)
|
||||
->where('group_name', $this->groupName)
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete existing processed data dengan pendekatan sederhana seperti ExportStatementJob
|
||||
*/
|
||||
private function deleteExistingProcessedData(array $criteria)
|
||||
: void
|
||||
{
|
||||
Log::info('Deleting existing processed data', $criteria);
|
||||
|
||||
$deletedCount = ProcessedClosingBalance::where('account_number', $criteria['account_number'])
|
||||
->where('period', $criteria['period'])
|
||||
->delete();
|
||||
|
||||
Log::info('Existing processed data deleted', [
|
||||
'deleted_count' => $deletedCount,
|
||||
'criteria' => $criteria
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch update or create untuk mengurangi jumlah query dan lock
|
||||
* Menggunakan pendekatan yang lebih efisien untuk menghindari max_lock_per_transaction
|
||||
*/
|
||||
private function batchUpdateOrCreate(array $processedData): void
|
||||
{
|
||||
Log::info('Starting batch updateOrCreate', [
|
||||
'batch_size' => count($processedData)
|
||||
]);
|
||||
|
||||
// Kumpulkan semua trans_reference yang akan diproses
|
||||
$transReferences = collect($processedData)->pluck('trans_reference')->toArray();
|
||||
|
||||
// Ambil data yang sudah ada dalam satu query
|
||||
$existingRecords = ProcessedClosingBalance::where('account_number', $this->accountNumber)
|
||||
->where('period', $this->period)
|
||||
->where('group_name', $this->groupName)
|
||||
->whereIn('trans_reference', $transReferences)
|
||||
->get()
|
||||
->keyBy(function ($item) {
|
||||
return $item->trans_reference . '_' . $item->amount_lcy;
|
||||
});
|
||||
|
||||
$toInsert = [];
|
||||
$toUpdate = [];
|
||||
|
||||
foreach ($processedData as $data) {
|
||||
$key = $data['trans_reference'] . '_' . $data['amount_lcy'];
|
||||
|
||||
if ($existingRecords->has($key)) {
|
||||
// Record sudah ada, siapkan untuk update
|
||||
$existingRecord = $existingRecords->get($key);
|
||||
$toUpdate[] = [
|
||||
'id' => $existingRecord->id,
|
||||
'data' => $data
|
||||
];
|
||||
} else {
|
||||
// Record baru, siapkan untuk insert
|
||||
$toInsert[] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
// Batch insert untuk record baru
|
||||
if (!empty($toInsert)) {
|
||||
DB::table('processed_closing_balances')->insert($toInsert);
|
||||
Log::info('Batch insert completed', ['count' => count($toInsert)]);
|
||||
}
|
||||
|
||||
// Batch update untuk record yang sudah ada
|
||||
if (!empty($toUpdate)) {
|
||||
foreach ($toUpdate as $updateItem) {
|
||||
ProcessedClosingBalance::where('id', $updateItem['id'])
|
||||
->update($updateItem['data']);
|
||||
}
|
||||
Log::info('Batch update completed', ['count' => count($toUpdate)]);
|
||||
}
|
||||
|
||||
Log::info('Batch updateOrCreate completed successfully', [
|
||||
'inserted' => count($toInsert),
|
||||
'updated' => count($toUpdate)
|
||||
]);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ class ProcessAccountDataJob implements ShouldQueue
|
||||
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 DISK_NAME = 'staging';
|
||||
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
|
||||
|
||||
private string $period = '';
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
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 DISK_NAME = 'staging';
|
||||
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
|
||||
|
||||
private string $period = '';
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
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 DISK_NAME = 'staging';
|
||||
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
|
||||
private const HEADER_MAP = [
|
||||
'id' => 'transaction_id',
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.AA.BILL.DETAILS.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
private const DISK_NAME = 'staging';
|
||||
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
|
||||
|
||||
private string $period = '';
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
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 DISK_NAME = 'staging';
|
||||
private const HEADER_MAP = [
|
||||
'id' => 'id_category',
|
||||
'date_time' => 'date_time',
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
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 DISK_NAME = 'staging';
|
||||
private const FIELD_MAP = [
|
||||
'id' => null, // Not mapped to model
|
||||
'date_time' => null, // Not mapped to model
|
||||
@@ -144,6 +144,12 @@
|
||||
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)) {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
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\Customer;
|
||||
@@ -19,7 +20,7 @@
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.CUSTOMER.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
private const DISK_NAME = 'staging';
|
||||
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
|
||||
|
||||
private string $period = '';
|
||||
@@ -112,13 +113,28 @@
|
||||
return;
|
||||
}
|
||||
|
||||
$headers = (new Customer())->getFillable();
|
||||
// Read header from CSV file
|
||||
$csvHeaders = fgetcsv($handle, 0, self::CSV_DELIMITER);
|
||||
if ($csvHeaders === false) {
|
||||
Log::error("Unable to read headers from file: $filePath");
|
||||
fclose($handle);
|
||||
return;
|
||||
}
|
||||
|
||||
// Map CSV headers to database fields
|
||||
$headerMapping = $this->getHeaderMapping($csvHeaders);
|
||||
|
||||
Log::info("CSV Headers found", [
|
||||
'csv_headers' => $csvHeaders,
|
||||
'mapped_fields' => array_values($headerMapping)
|
||||
]);
|
||||
|
||||
$rowCount = 0;
|
||||
$chunkCount = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
||||
$rowCount++;
|
||||
$this->processRow($row, $headers, $rowCount, $filePath);
|
||||
$this->processRow($row, $csvHeaders, $headerMapping, $rowCount, $filePath);
|
||||
|
||||
// Process in chunks to avoid memory issues
|
||||
if (count($this->customerBatch) >= self::CHUNK_SIZE) {
|
||||
@@ -137,16 +153,62 @@
|
||||
Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors.");
|
||||
}
|
||||
|
||||
private function processRow(array $row, array $headers, int $rowCount, string $filePath)
|
||||
/**
|
||||
* Map CSV headers to database field names
|
||||
* Memetakan header CSV ke nama field database
|
||||
*/
|
||||
private function getHeaderMapping(array $csvHeaders): array
|
||||
{
|
||||
$mapping = [];
|
||||
$fillableFields = (new Customer())->getFillable();
|
||||
|
||||
foreach ($csvHeaders as $index => $csvHeader) {
|
||||
$csvHeader = trim($csvHeader);
|
||||
|
||||
// Direct mapping untuk field yang sama
|
||||
if (in_array($csvHeader, $fillableFields)) {
|
||||
$mapping[$index] = $csvHeader;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Custom mapping untuk field yang berbeda nama
|
||||
$customMapping = [
|
||||
'co_code' => 'branch_code', // co_code di CSV menjadi branch_code di database
|
||||
'name_1' => 'name'
|
||||
];
|
||||
|
||||
if (isset($customMapping[$csvHeader])) {
|
||||
$mapping[$index] = $customMapping[$csvHeader];
|
||||
} else {
|
||||
// Jika field ada di fillable, gunakan langsung
|
||||
if (in_array($csvHeader, $fillableFields)) {
|
||||
$mapping[$index] = $csvHeader;
|
||||
}
|
||||
// Jika tidak ada mapping, skip field ini
|
||||
}
|
||||
}
|
||||
|
||||
return $mapping;
|
||||
}
|
||||
|
||||
private function processRow(array $row, array $csvHeaders, array $headerMapping, int $rowCount, string $filePath)
|
||||
: void
|
||||
{
|
||||
if (count($headers) !== count($row)) {
|
||||
if (count($csvHeaders) !== count($row)) {
|
||||
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
||||
count($headers) . ", Got: " . count($row));
|
||||
count($csvHeaders) . ", Got: " . count($row));
|
||||
return;
|
||||
}
|
||||
|
||||
$data = array_combine($headers, $row);
|
||||
// Map CSV data to database fields
|
||||
$data = [];
|
||||
foreach ($row as $index => $value) {
|
||||
if (isset($headerMapping[$index])) {
|
||||
$fieldName = $headerMapping[$index];
|
||||
$data[$fieldName] = trim($value);
|
||||
}
|
||||
}
|
||||
|
||||
$this->addToBatch($data, $rowCount, $filePath);
|
||||
}
|
||||
|
||||
@@ -175,12 +237,20 @@
|
||||
|
||||
/**
|
||||
* Save batched records to the database
|
||||
* Menyimpan data customer dalam batch ke database dengan transaksi
|
||||
*/
|
||||
private function saveBatch()
|
||||
: void
|
||||
{
|
||||
if (empty($this->customerBatch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$batchSize = count($this->customerBatch);
|
||||
Log::info("Starting batch save", ['batch_size' => $batchSize]);
|
||||
|
||||
try {
|
||||
if (!empty($this->customerBatch)) {
|
||||
DB::transaction(function () use ($batchSize) {
|
||||
// Bulk insert/update customers
|
||||
Customer::upsert(
|
||||
$this->customerBatch,
|
||||
@@ -188,14 +258,26 @@
|
||||
array_diff((new Customer())->getFillable(), ['customer_code']) // Update columns
|
||||
);
|
||||
|
||||
// Reset customer batch after processing
|
||||
$this->customerBatch = [];
|
||||
}
|
||||
Log::info("Batch save completed successfully", ['batch_size' => $batchSize]);
|
||||
});
|
||||
|
||||
// Reset customer batch after successful processing
|
||||
$this->customerBatch = [];
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error in saveBatch: " . $e->getMessage());
|
||||
$this->errorCount += count($this->customerBatch);
|
||||
Log::error("Error in saveBatch", [
|
||||
'error' => $e->getMessage(),
|
||||
'batch_size' => $batchSize,
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
$this->errorCount += $batchSize;
|
||||
|
||||
// Reset batch even if there's an error to prevent reprocessing the same failed records
|
||||
$this->customerBatch = [];
|
||||
|
||||
// Re-throw exception untuk handling di level atas
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
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 DISK_NAME = 'staging';
|
||||
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
|
||||
private const CSV_HEADERS = [
|
||||
'id',
|
||||
@@ -185,6 +185,12 @@
|
||||
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));
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
];
|
||||
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 const DISK_NAME = 'staging';
|
||||
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
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 const DISK_NAME = 'staging';
|
||||
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
|
||||
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 = 'staging';
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ class ProcessSectorDataJob implements ShouldQueue
|
||||
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 const DISK_NAME = 'staging';
|
||||
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
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 DISK_NAME = 'staging';
|
||||
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
|
||||
|
||||
private string $period = '';
|
||||
|
||||
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 = 'staging';
|
||||
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}");
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.STMT.NARR.FORMAT.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
private const DISK_NAME = 'staging';
|
||||
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
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 const DISK_NAME = 'staging';
|
||||
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
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 DISK_NAME = 'staging';
|
||||
private const CHUNK_SIZE = 1000; // Process data in chunks to reduce memory usage
|
||||
private const HEADER_MAP = [
|
||||
'id' => 'id_teller',
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.TRANSACTION.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
private const DISK_NAME = 'staging';
|
||||
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
|
||||
@@ -231,6 +231,7 @@
|
||||
break;
|
||||
|
||||
case 'all_branches':
|
||||
$query->orderBy('branch_code', 'asc');
|
||||
// Tidak ada filter tambahan, ambil semua
|
||||
break;
|
||||
|
||||
@@ -238,6 +239,7 @@
|
||||
throw new InvalidArgumentException("Invalid request type: {$this->requestType}");
|
||||
}
|
||||
|
||||
$query->orderBy('account_number');
|
||||
$accounts = $query->get();
|
||||
|
||||
// Filter accounts yang memiliki email
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Mail;
|
||||
|
||||
use Carbon\Carbon;
|
||||
@@ -8,12 +7,13 @@
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Log;
|
||||
|
||||
use Modules\Webstatement\Models\Account;
|
||||
use Modules\Webstatement\Models\PrintStatementLog;
|
||||
use Symfony\Component\Mailer\Mailer;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class StatementEmail extends Mailable
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Modules\Webstatement\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Modules\Basicdata\Models\Branch;
|
||||
// use Modules\Webstatement\Database\Factories\AccountFactory;
|
||||
|
||||
class Account extends Model
|
||||
@@ -34,7 +35,7 @@ class Account extends Model
|
||||
{
|
||||
return $this->belongsTo(Customer::class, 'customer_code', 'customer_code');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all balances for this account.
|
||||
*/
|
||||
@@ -42,10 +43,10 @@ class Account extends Model
|
||||
{
|
||||
return $this->hasMany(AccountBalance::class, 'account_number', 'account_number');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get balance for a specific period.
|
||||
*
|
||||
*
|
||||
* @param string $period Format: YYYY-MM
|
||||
* @return AccountBalance|null
|
||||
*/
|
||||
@@ -53,4 +54,8 @@ class Account extends Model
|
||||
{
|
||||
return $this->balances()->where('period', $period)->first();
|
||||
}
|
||||
|
||||
public function branch(){
|
||||
return $this->belongsTo(Branch::class, 'branch_code','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');
|
||||
}
|
||||
}
|
||||
@@ -24,9 +24,38 @@ class Customer extends Model
|
||||
'email',
|
||||
'sector',
|
||||
'customer_type',
|
||||
'birth_incorp_date'
|
||||
'birth_incorp_date',
|
||||
'home_rt',
|
||||
'home_rw',
|
||||
'ktp_rt',
|
||||
'ktp_rw',
|
||||
'local_ref',
|
||||
'ktp_kelurahan',
|
||||
'ktp_kecamatan',
|
||||
'town_country',
|
||||
'ktp_provinsi',
|
||||
'post_code',
|
||||
'l_dom_street',
|
||||
'l_dom_rt',
|
||||
'l_dom_kelurahan',
|
||||
'l_dom_rw',
|
||||
'l_dom_kecamatan',
|
||||
'l_dom_provinsi',
|
||||
'l_dom_t_country',
|
||||
'l_dom_post_code'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
* Mendefinisikan casting untuk field-field tertentu
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'date_of_birth' => 'date',
|
||||
'birth_incorp_date' => 'date',
|
||||
];
|
||||
}
|
||||
public function accounts(){
|
||||
return $this->hasMany(Account::class, 'customer_code', 'customer_code');
|
||||
}
|
||||
|
||||
@@ -44,11 +44,15 @@ class PrintStatementLog extends Model
|
||||
'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',
|
||||
@@ -57,6 +61,10 @@ class PrintStatementLog extends Model
|
||||
'target_accounts' => 'array',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'password', // Hide password dari serialization
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the formatted period display
|
||||
*
|
||||
@@ -285,4 +293,8 @@ class PrintStatementLog extends Model
|
||||
{
|
||||
return $query->where('request_type', 'single_account');
|
||||
}
|
||||
|
||||
public function account(){
|
||||
return $this->belongsTo(Account::class, 'account_number','account_number');
|
||||
}
|
||||
}
|
||||
|
||||
48
app/Models/ProcessedClosingBalance.php
Normal file
48
app/Models/ProcessedClosingBalance.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ProcessedClosingBalance extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'account_number',
|
||||
'period',
|
||||
'group_name',
|
||||
'sequence_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',
|
||||
'unique_hash',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'amount_lcy' => 'decimal:2',
|
||||
'debit_amount' => 'decimal:2',
|
||||
'credit_amount' => 'decimal:2',
|
||||
'closing_balance' => 'decimal:2'
|
||||
];
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
'transaction_type',
|
||||
'description',
|
||||
'end_balance',
|
||||
'actual_date'
|
||||
'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);
|
||||
});
|
||||
}
|
||||
}
|
||||
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');
|
||||
}
|
||||
}
|
||||
31
app/Providers/BalanceServiceProvider.php
Normal file
31
app/Providers/BalanceServiceProvider.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\Webstatement\Services\AccountBalanceService;
|
||||
|
||||
class BalanceServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register the service provider.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(AccountBalanceService::class, function ($app) {
|
||||
return new AccountBalanceService();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function provides(): array
|
||||
{
|
||||
return [AccountBalanceService::class];
|
||||
}
|
||||
}
|
||||
@@ -6,19 +6,24 @@ use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Nwidart\Modules\Traits\PathNamespace;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Modules\Webstatement\Console\UnlockPdf;
|
||||
use Modules\Webstatement\Console\CombinePdf;
|
||||
use Modules\Webstatement\Console\ConvertHtmlToPdf;
|
||||
use Modules\Webstatement\Console\ExportDailyStatements;
|
||||
use Modules\Webstatement\Console\ProcessDailyMigration;
|
||||
use Modules\Webstatement\Console\ExportPeriodStatements;
|
||||
use Modules\Webstatement\Console\UpdateAllAtmCardsCommand;
|
||||
use Modules\Webstatement\Console\CheckEmailProgressCommand;
|
||||
use Modules\Webstatement\Console\GenerateBiayakartuCommand;
|
||||
use Modules\Webstatement\Console\SendStatementEmailCommand;
|
||||
use Modules\Webstatement\Console\{
|
||||
UnlockPdf,
|
||||
CombinePdf,
|
||||
ConvertHtmlToPdf,
|
||||
ExportDailyStatements,
|
||||
ProcessDailyStaging,
|
||||
ExportPeriodStatements,
|
||||
UpdateAllAtmCardsCommand,
|
||||
CheckEmailProgressCommand,
|
||||
GenerateBiayakartuCommand,
|
||||
SendStatementEmailCommand,
|
||||
GenerateAtmTransactionReport,
|
||||
GenerateBiayaKartuCsvCommand,
|
||||
AutoSendStatementEmailCommand,
|
||||
GenerateClosingBalanceReportCommand,
|
||||
GenerateClosingBalanceReportBulkCommand,
|
||||
};
|
||||
use Modules\Webstatement\Jobs\UpdateAtmCardBranchCurrencyJob;
|
||||
use Modules\Webstatement\Console\GenerateAtmTransactionReport;
|
||||
use Modules\Webstatement\Console\GenerateBiayaKartuCsvCommand;
|
||||
|
||||
class WebstatementServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -52,6 +57,7 @@ class WebstatementServiceProvider extends ServiceProvider
|
||||
{
|
||||
$this->app->register(EventServiceProvider::class);
|
||||
$this->app->register(RouteServiceProvider::class);
|
||||
$this->app->register(BalanceServiceProvider::class);
|
||||
$this->app->bind(UpdateAtmCardBranchCurrencyJob::class);
|
||||
}
|
||||
|
||||
@@ -63,7 +69,7 @@ class WebstatementServiceProvider extends ServiceProvider
|
||||
$this->commands([
|
||||
GenerateBiayakartuCommand::class,
|
||||
GenerateBiayaKartuCsvCommand::class,
|
||||
ProcessDailyMigration::class,
|
||||
ProcessDailyStaging::class,
|
||||
ExportDailyStatements::class,
|
||||
CombinePdf::class,
|
||||
ConvertHtmlToPdf::class,
|
||||
@@ -72,7 +78,10 @@ class WebstatementServiceProvider extends ServiceProvider
|
||||
GenerateAtmTransactionReport::class,
|
||||
SendStatementEmailCommand::class,
|
||||
CheckEmailProgressCommand::class,
|
||||
UpdateAllAtmCardsCommand::class
|
||||
UpdateAllAtmCardsCommand::class,
|
||||
AutoSendStatementEmailCommand::class,
|
||||
GenerateClosingBalanceReportCommand::class,
|
||||
GenerateClosingBalanceReportBulkCommand::class,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
130
app/Services/AccountBalanceService.php
Normal file
130
app/Services/AccountBalanceService.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Webstatement\Services;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Carbon\Carbon;
|
||||
use Modules\Webstatement\Models\AccountBalance;
|
||||
use Modules\Webstatement\Models\StmtEntry;
|
||||
|
||||
class AccountBalanceService
|
||||
{
|
||||
/**
|
||||
* Get balance summary (opening and closing balance)
|
||||
*
|
||||
* @param string $accountNumber
|
||||
* @param string $startDate
|
||||
* @param string $endDate
|
||||
* @return array
|
||||
*/
|
||||
public function getBalanceSummary(string $accountNumber, string $startDate, string $endDate): array
|
||||
{
|
||||
return DB::transaction(function () use ($accountNumber, $startDate, $endDate) {
|
||||
Log::info('Calculating balance summary', [
|
||||
'account_number' => $accountNumber,
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate
|
||||
]);
|
||||
|
||||
// Convert dates to Carbon instances
|
||||
$startDateCarbon = Carbon::parse($startDate);
|
||||
$endDateCarbon = Carbon::parse($endDate);
|
||||
|
||||
// Get opening balance (balance from previous day)
|
||||
$openingBalanceDate = $startDateCarbon->copy()->subDay();
|
||||
$openingBalance = $this->getAccountBalance($accountNumber, $openingBalanceDate);
|
||||
|
||||
// Get closing balance date (previous day from end date)
|
||||
$closingBalanceDate = $endDateCarbon->copy()->subDay();
|
||||
$closingBalanceBase = $this->getAccountBalance($accountNumber, $closingBalanceDate);
|
||||
|
||||
// Get transactions on end date
|
||||
$transactionsOnEndDate = $this->getTransactionsOnDate($accountNumber, $endDate);
|
||||
|
||||
// Calculate closing balance
|
||||
$closingBalance = $closingBalanceBase + $transactionsOnEndDate;
|
||||
|
||||
$result = [
|
||||
'account_number' => $accountNumber,
|
||||
'period' => [
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate
|
||||
],
|
||||
'opening_balance' => [
|
||||
'date' => $openingBalanceDate->format('Y-m-d'),
|
||||
'balance' => $openingBalance,
|
||||
'formatted_balance' => number_format($openingBalance, 2)
|
||||
],
|
||||
'closing_balance' => [
|
||||
'date' => $endDate,
|
||||
'balance' => $closingBalance,
|
||||
'formatted_balance' => number_format($closingBalance, 2),
|
||||
'base_balance' => [
|
||||
'date' => $closingBalanceDate->format('Y-m-d'),
|
||||
'balance' => $closingBalanceBase,
|
||||
'formatted_balance' => number_format($closingBalanceBase, 2)
|
||||
],
|
||||
'transactions_on_end_date' => $transactionsOnEndDate,
|
||||
'formatted_transactions_on_end_date' => number_format($transactionsOnEndDate, 2)
|
||||
]
|
||||
];
|
||||
|
||||
Log::info('Balance summary calculated successfully', $result);
|
||||
|
||||
return $result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get account balance for specific date
|
||||
*
|
||||
* @param string $accountNumber
|
||||
* @param Carbon $date
|
||||
* @return float
|
||||
*/
|
||||
private function getAccountBalance(string $accountNumber, Carbon $date): float
|
||||
{
|
||||
$balance = AccountBalance::where('account_number', $accountNumber)
|
||||
->where('period', $date->format('Ymd'))
|
||||
->value('actual_balance');
|
||||
|
||||
if ($balance === null) {
|
||||
Log::warning('Account balance not found', [
|
||||
'account_number' => $accountNumber,
|
||||
'date' => $date->format('Y-m-d'),
|
||||
'period' => $date->format('Ymd')
|
||||
]);
|
||||
return 0.00;
|
||||
}
|
||||
|
||||
return (float) $balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transactions on specific date
|
||||
*
|
||||
* @param string $accountNumber
|
||||
* @param string $date
|
||||
* @return float
|
||||
*/
|
||||
private function getTransactionsOnDate(string $accountNumber, string $date): float
|
||||
{
|
||||
$total = StmtEntry::where('account_number', $accountNumber)
|
||||
->whereDate('value_date', $date)
|
||||
->sum(DB::raw('CAST(amount_lcy AS DECIMAL(15,2))'));
|
||||
|
||||
return (float) $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if account exists
|
||||
*
|
||||
* @param string $accountNumber
|
||||
* @return bool
|
||||
*/
|
||||
public function validateAccount(string $accountNumber): bool
|
||||
{
|
||||
return AccountBalance::where('account_number', $accountNumber)->exists();
|
||||
}
|
||||
}
|
||||
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,20 @@
|
||||
|
||||
return [
|
||||
'name' => 'Webstatement',
|
||||
|
||||
// ZIP file password configuration
|
||||
'zip_password' => env('WEBSTATEMENT_ZIP_PASSWORD', 'statement123'),
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| API Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration values are used for API authentication using HMAC
|
||||
| signature validation. These keys are used to validate incoming API
|
||||
| requests and ensure secure communication.
|
||||
|
|
||||
*/
|
||||
|
||||
'api_key' => env('API_KEY'),
|
||||
'secret_key' => env('SECRET_KEY'),
|
||||
];
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('print_statement_logs', function (Blueprint $table) {
|
||||
$table->string('stmt_sent_type')->after('status')->nullable();
|
||||
$table->boolean('is_generated')->after('is_available')->nullable()->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('print_statement_logs', function (Blueprint $table) {
|
||||
$table->dropColumn('stmt_sent_type');
|
||||
$table->dropColumn('is_generated');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* Menambahkan field tambahan ke tabel customers:
|
||||
* - home_rt: RT alamat rumah
|
||||
* - home_rw: RW alamat rumah
|
||||
* - ktp_rt: RT alamat KTP
|
||||
* - ktp_rw: RW alamat KTP
|
||||
* - local_ref: Referensi lokal dengan data panjang
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('customers', function (Blueprint $table) {
|
||||
// Field RT dan RW untuk alamat rumah
|
||||
$table->string('home_rt', 10)->nullable()->comment('RT alamat rumah');
|
||||
$table->string('home_rw', 10)->nullable()->comment('RW alamat rumah');
|
||||
|
||||
// Field RT dan RW untuk alamat KTP
|
||||
$table->string('ktp_rt', 10)->nullable()->comment('RT alamat KTP');
|
||||
$table->string('ktp_rw', 10)->nullable()->comment('RW alamat KTP');
|
||||
|
||||
// Field untuk referensi lokal dengan tipe data TEXT untuk menampung data panjang
|
||||
$table->text('local_ref')->nullable()->comment('Referensi lokal dengan data panjang');
|
||||
|
||||
// Menambahkan index untuk performa query jika diperlukan
|
||||
$table->index(['home_rt', 'home_rw'], 'idx_customers_home_rt_rw');
|
||||
$table->index(['ktp_rt', 'ktp_rw'], 'idx_customers_ktp_rt_rw');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* Menghapus field yang ditambahkan jika migration di-rollback
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('customers', function (Blueprint $table) {
|
||||
// Hapus index terlebih dahulu
|
||||
$table->dropIndex('idx_customers_home_rt_rw');
|
||||
$table->dropIndex('idx_customers_ktp_rt_rw');
|
||||
|
||||
// Hapus kolom yang ditambahkan
|
||||
$table->dropColumn([
|
||||
'home_rt',
|
||||
'home_rw',
|
||||
'ktp_rt',
|
||||
'ktp_rw',
|
||||
'local_ref'
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Menjalankan migrasi untuk menambahkan support multi_account
|
||||
* Menggunakan constraint check sebagai alternatif enum
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
// Hapus constraint enum yang lama jika ada
|
||||
DB::statement("ALTER TABLE print_statement_logs DROP CONSTRAINT IF EXISTS print_statement_logs_request_type_check");
|
||||
|
||||
// Ubah kolom menjadi varchar
|
||||
Schema::table('print_statement_logs', function (Blueprint $table) {
|
||||
$table->string('request_type', 50)->change();
|
||||
});
|
||||
|
||||
// Tambahkan constraint check baru dengan multi_account
|
||||
DB::statement("
|
||||
ALTER TABLE print_statement_logs
|
||||
ADD CONSTRAINT print_statement_logs_request_type_check
|
||||
CHECK (request_type IN ('single_account', 'branch', 'all_branches', 'multi_account'))
|
||||
");
|
||||
|
||||
DB::commit();
|
||||
Log::info('Migration berhasil: request_type sekarang mendukung multi_account');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollback();
|
||||
Log::error('Migration gagal: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Membalikkan migrasi
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
// Hapus constraint yang baru
|
||||
DB::statement("ALTER TABLE print_statement_logs DROP CONSTRAINT IF EXISTS print_statement_logs_request_type_check");
|
||||
|
||||
// Kembalikan constraint lama tanpa multi_account
|
||||
DB::statement("
|
||||
ALTER TABLE print_statement_logs
|
||||
ADD CONSTRAINT print_statement_logs_request_type_check
|
||||
CHECK (request_type IN ('single_account', 'branch', 'all_branches'))
|
||||
");
|
||||
|
||||
DB::commit();
|
||||
Log::info('Migration rollback berhasil: multi_account dihapus dari request_type');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollback();
|
||||
Log::error('Migration rollback gagal: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Menjalankan migrasi untuk membuat tabel province_core
|
||||
* Tabel ini menyimpan data master provinsi dengan kode dan nama
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
Schema::create('province_core', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('code', 10)->unique()->comment('Kode provinsi unik');
|
||||
$table->string('name', 255)->comment('Nama provinsi');
|
||||
$table->timestamps();
|
||||
|
||||
// Index untuk performa pencarian
|
||||
$table->index(['code']);
|
||||
$table->index(['name']);
|
||||
});
|
||||
|
||||
DB::commit();
|
||||
Log::info('Migration province_core table berhasil dibuat');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollback();
|
||||
Log::error('Migration province_core table gagal: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Membalikkan migrasi dengan menghapus tabel province_core
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
Schema::dropIfExists('province_core');
|
||||
|
||||
DB::commit();
|
||||
Log::info('Migration rollback province_core table berhasil');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollback();
|
||||
Log::error('Migration rollback province_core table gagal: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
{
|
||||
/**
|
||||
* Menjalankan migrasi untuk menambahkan kolom password ke tabel print_statement_logs
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('print_statement_logs', function (Blueprint $table) {
|
||||
// Menambahkan kolom password setelah kolom stmt_sent_type
|
||||
$table->string('password', 255)->nullable()->after('stmt_sent_type')
|
||||
->comment('Password untuk proteksi PDF statement');
|
||||
|
||||
// Menambahkan index untuk performa query jika diperlukan
|
||||
$table->index(['password'], 'idx_print_statement_logs_password');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Membalikkan migrasi dengan menghapus kolom password
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('print_statement_logs', function (Blueprint $table) {
|
||||
// Hapus index terlebih dahulu
|
||||
$table->dropIndex('idx_print_statement_logs_password');
|
||||
|
||||
// Hapus kolom password
|
||||
$table->dropColumn('password');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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::table('processed_statements', function (Blueprint $table) {
|
||||
$table->string('recipt_no')->nullable()->after('reference_number');
|
||||
|
||||
// Menambahkan index untuk field no_receipt jika diperlukan untuk pencarian
|
||||
$table->index('recipt_no');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('processed_statements', function (Blueprint $table) {
|
||||
$table->dropIndex(['recipt_no']);
|
||||
$table->dropColumn('recipt_no');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
<?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('closing_balance_report_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('account_number', 50);
|
||||
$table->string('period', 8); // Format: YYYYMMDD
|
||||
$table->date('report_date');
|
||||
$table->enum('status', ['pending', 'processing', 'completed', 'failed'])->default('pending');
|
||||
$table->enum('authorization_status', ['pending', 'approved', 'rejected'])->nullable();
|
||||
$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->text('remarks')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes
|
||||
$table->index(['account_number', 'period']);
|
||||
$table->index('status');
|
||||
$table->index('authorization_status');
|
||||
$table->index('created_at');
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('closing_balance_report_logs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateStmtEntryDetailTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('stmt_entry_detail', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('stmt_entry_id')->nullable();
|
||||
$table->string('account_number')->nullable();
|
||||
$table->string('company_code')->nullable();
|
||||
$table->string('amount_lcy')->nullable();
|
||||
$table->string('transaction_code')->nullable();
|
||||
$table->string('narrative')->nullable();
|
||||
$table->string('product_category')->nullable();
|
||||
$table->string('value_date')->nullable();
|
||||
$table->string('amount_fcy')->nullable();
|
||||
$table->string('exchange_rate')->nullable();
|
||||
$table->string('trans_reference')->nullable();
|
||||
$table->string('booking_date')->nullable();
|
||||
$table->string('stmt_no')->nullable();
|
||||
$table->string('date_time')->nullable();
|
||||
$table->string('currency')->nullable();
|
||||
$table->string('crf_type')->nullable();
|
||||
$table->string('consol_key')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
// Index untuk performa query
|
||||
$table->index('stmt_entry_id');
|
||||
$table->index('account_number');
|
||||
$table->index('trans_reference');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('stmt_entry_detail');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('processed_closing_balances', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('account_number', 20)->index();
|
||||
$table->string('period', 8)->index();
|
||||
$table->string('group_name', 20)->default('DEFAULT')->index();
|
||||
$table->integer('sequence_no');
|
||||
$table->string('trans_reference', 50)->nullable();
|
||||
$table->string('booking_date', 8)->nullable();
|
||||
$table->string('transaction_date', 20)->nullable();
|
||||
$table->decimal('amount_lcy', 15, 2)->nullable();
|
||||
$table->string('debit_acct_no', 20)->nullable();
|
||||
$table->string('debit_value_date', 8)->nullable();
|
||||
$table->decimal('debit_amount', 15, 2)->nullable();
|
||||
$table->string('credit_acct_no', 20)->nullable();
|
||||
$table->string('bif_rcv_acct', 20)->nullable();
|
||||
$table->string('bif_rcv_name', 100)->nullable();
|
||||
$table->string('credit_value_date', 8)->nullable();
|
||||
$table->decimal('credit_amount', 15, 2)->nullable();
|
||||
$table->string('at_unique_id', 50)->nullable();
|
||||
$table->string('bif_ref_no', 50)->nullable();
|
||||
$table->string('atm_order_id', 50)->nullable();
|
||||
$table->string('recipt_no', 50)->nullable();
|
||||
$table->string('api_iss_acct', 20)->nullable();
|
||||
$table->string('api_benff_acct', 20)->nullable();
|
||||
$table->string('authoriser', 50)->nullable();
|
||||
$table->text('remarks')->nullable();
|
||||
$table->text('payment_details')->nullable();
|
||||
$table->string('ref_no', 50)->nullable();
|
||||
$table->string('merchant_id', 50)->nullable();
|
||||
$table->string('term_id', 50)->nullable();
|
||||
$table->decimal('closing_balance', 15, 2)->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
// Composite index untuk performa query
|
||||
$table->index(['account_number', 'period', 'group_name']);
|
||||
$table->index(['account_number', 'period', 'sequence_no']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('processed_closing_balances');
|
||||
}
|
||||
};
|
||||
@@ -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('processed_closing_balances', function (Blueprint $table) {
|
||||
$table->string('unique_hash')->after('id')->unique();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('processed_closing_balances', function (Blueprint $table) {
|
||||
$table->dropColumn('unique_hash');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
* Menambahkan field-field yang belum ada pada tabel customers
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('customers', function (Blueprint $table) {
|
||||
// Field yang belum ada berdasarkan CSV header
|
||||
$table->string('ktp_kelurahan')->nullable()->after('local_ref')->comment('Kelurahan sesuai KTP');
|
||||
$table->string('ktp_kecamatan')->nullable()->after('ktp_kelurahan')->comment('Kecamatan sesuai KTP');
|
||||
$table->string('town_country')->nullable()->after('ktp_kecamatan')->comment('Kota/Negara');
|
||||
$table->string('ktp_provinsi')->nullable()->after('town_country')->comment('Provinsi sesuai KTP');
|
||||
$table->string('post_code')->nullable()->after('ktp_provinsi')->comment('Kode pos alternatif');
|
||||
$table->string('l_dom_street')->nullable()->after('post_code')->comment('Alamat domisili - jalan');
|
||||
$table->string('l_dom_rt')->nullable()->after('l_dom_street')->comment('Alamat domisili - RT');
|
||||
$table->string('l_dom_kelurahan')->nullable()->after('l_dom_rt')->comment('Alamat domisili - kelurahan');
|
||||
$table->string('l_dom_rw')->nullable()->after('l_dom_kelurahan')->comment('Alamat domisili - RW');
|
||||
$table->string('l_dom_kecamatan')->nullable()->after('l_dom_rw')->comment('Alamat domisili - kecamatan');
|
||||
$table->string('l_dom_provinsi')->nullable()->after('l_dom_kecamatan')->comment('Alamat domisili - provinsi');
|
||||
$table->string('l_dom_t_country')->nullable()->after('l_dom_provinsi')->comment('Alamat domisili - kota/negara');
|
||||
$table->string('l_dom_post_code')->nullable()->after('l_dom_t_country')->comment('Alamat domisili - kode pos');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
* Menghapus field-field yang ditambahkan
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('customers', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'ktp_kelurahan',
|
||||
'ktp_kecamatan',
|
||||
'town_country',
|
||||
'ktp_provinsi',
|
||||
'post_code',
|
||||
'l_dom_street',
|
||||
'l_dom_rt',
|
||||
'l_dom_kelurahan',
|
||||
'l_dom_rw',
|
||||
'l_dom_kecamatan',
|
||||
'l_dom_provinsi',
|
||||
'l_dom_t_country',
|
||||
'l_dom_post_code'
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
14
module.json
14
module.json
@@ -8,7 +8,9 @@
|
||||
"providers": [
|
||||
"Modules\\Webstatement\\Providers\\WebstatementServiceProvider"
|
||||
],
|
||||
"files": [],
|
||||
"files": [
|
||||
"app/Helpers/helpers.php"
|
||||
],
|
||||
"menu": {
|
||||
"main": [
|
||||
{
|
||||
@@ -75,6 +77,16 @@
|
||||
"roles": [
|
||||
"administrator"
|
||||
]
|
||||
},{
|
||||
"title": "Laporan Closing Balance",
|
||||
"path": "laporan-closing-balance",
|
||||
"icon": "ki-filled ki-printer text-lg text-primary",
|
||||
"classes": "",
|
||||
"attributes": [],
|
||||
"permission": "",
|
||||
"roles": [
|
||||
"administrator"
|
||||
]
|
||||
}
|
||||
],
|
||||
"master": [
|
||||
|
||||
303
resources/views/laporan-closing-balance/index.blade.php
Normal file
303
resources/views/laporan-closing-balance/index.blade.php
Normal file
@@ -0,0 +1,303 @@
|
||||
@extends('layouts.main')
|
||||
|
||||
@section('breadcrumbs')
|
||||
{{ Breadcrumbs::render('laporan-closing-balance.index') }}
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="grid">
|
||||
<div class="min-w-full card card-grid" data-datatable="false" data-datatable-page-size="10"
|
||||
data-datatable-state-save="false" id="laporan-closing-balance-table"
|
||||
data-api-url="{{ route('laporan-closing-balance.datatables') }}">
|
||||
<div class="flex-wrap py-5 card-header">
|
||||
<h3 class="card-title">
|
||||
Laporan Closing Balance
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-2 lg:gap-5">
|
||||
<!-- Filter Form -->
|
||||
<div class="flex flex-wrap gap-2.5 items-end">
|
||||
<!-- Nomor Rekening Filter -->
|
||||
<div class="flex flex-col">
|
||||
<input type="text" id="account-number-filter" class="input w-[200px]"
|
||||
placeholder="Masukkan nomor rekening">
|
||||
</div>
|
||||
|
||||
<!-- Tanggal Mulai Filter -->
|
||||
<div class="flex flex-col">
|
||||
<input type="date" id="start-date-filter" class="input w-[150px]">
|
||||
</div>
|
||||
|
||||
<!-- Tanggal Akhir Filter -->
|
||||
<div class="flex flex-col">
|
||||
<input type="date" id="end-date-filter" class="input w-[150px]">
|
||||
</div>
|
||||
|
||||
<!-- Tombol Filter -->
|
||||
<button type="button" id="apply-filter" class="btn btn-primary">
|
||||
<i class="ki-filled ki-magnifier"></i>
|
||||
Filter
|
||||
</button>
|
||||
|
||||
<!-- Tombol Reset -->
|
||||
<button type="button" id="reset-filter" class="btn btn-light">
|
||||
<i class="ki-filled ki-arrows-circle"></i>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="scrollable-x-auto">
|
||||
<table class="table text-sm font-medium text-gray-700 align-middle table-auto table-border"
|
||||
data-datatable-table="true">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-14">
|
||||
<input class="checkbox checkbox-sm" data-datatable-check="true" type="checkbox" />
|
||||
</th>
|
||||
<th class="min-w-[200px]" data-datatable-column="account_number">
|
||||
<span class="sort">
|
||||
<span class="sort-label">Nomor Rekening</span>
|
||||
<span class="sort-icon"></span>
|
||||
</span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="period">
|
||||
<span class="sort">
|
||||
<span class="sort-label">Periode</span>
|
||||
<span class="sort-icon"></span>
|
||||
</span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="updated_at">
|
||||
<span class="sort">
|
||||
<span class="sort-label">Tanggal Update</span>
|
||||
<span class="sort-icon"></span>
|
||||
</span>
|
||||
</th>
|
||||
<th class="min-w-[100px] text-center" data-datatable-column="actions">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
<div
|
||||
class="flex-col gap-3 justify-center font-medium text-gray-600 card-footer md:justify-between md:flex-row text-2sm">
|
||||
<div class="flex gap-2 items-center">
|
||||
Show
|
||||
<select class="w-16 select select-sm" data-datatable-size="true" name="perpage"></select> per page
|
||||
</div>
|
||||
<div class="flex gap-4 items-center">
|
||||
<span data-datatable-info="true"></span>
|
||||
<div class="pagination" data-datatable-pagination="true">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
/**
|
||||
* Fungsi untuk memformat angka menjadi format mata uang
|
||||
* @param {number} amount - Jumlah yang akan diformat
|
||||
* @returns {string} - String yang sudah diformat
|
||||
*/
|
||||
function formatCurrency(amount) {
|
||||
return new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 2
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fungsi untuk memformat periode dari format YYYYMMDD ke format yang lebih readable
|
||||
* @param {string} period - Periode dalam format YYYYMMDD
|
||||
* @returns {string} - Periode yang sudah diformat
|
||||
*/
|
||||
function formatPeriod(period) {
|
||||
if (!period || period.length !== 8) return period;
|
||||
|
||||
const year = period.substring(0, 4);
|
||||
const month = period.substring(4, 6);
|
||||
const day = period.substring(6, 8);
|
||||
|
||||
return `${day}/${month}/${year}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fungsi untuk memformat tanggal
|
||||
* @param {string} dateString - String tanggal
|
||||
* @returns {string} - Tanggal yang sudah diformat
|
||||
*/
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('id-ID', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
const element = document.querySelector('#laporan-closing-balance-table');
|
||||
const accountNumberFilter = document.getElementById('account-number-filter');
|
||||
const startDateFilter = document.getElementById('start-date-filter');
|
||||
const endDateFilter = document.getElementById('end-date-filter');
|
||||
const applyFilterBtn = document.getElementById('apply-filter');
|
||||
const resetFilterBtn = document.getElementById('reset-filter');
|
||||
const exportBtn = document.getElementById('export-btn');
|
||||
|
||||
const apiUrl = element.getAttribute('data-api-url');
|
||||
|
||||
// Set default date range (last 30 days) SEBELUM inisialisasi DataTable
|
||||
const today = new Date();
|
||||
const thirtyDaysAgo = new Date(today.getTime() - (30 * 24 * 60 * 60 * 1000));
|
||||
|
||||
endDateFilter.value = today.toISOString().split('T')[0];
|
||||
startDateFilter.value = thirtyDaysAgo.toISOString().split('T')[0];
|
||||
|
||||
// Prepare initial filters
|
||||
let initialFilters = {
|
||||
start_date: startDateFilter.value,
|
||||
end_date: endDateFilter.value
|
||||
};
|
||||
|
||||
// Konfigurasi DataTable dengan filter awal
|
||||
const dataTableOptions = {
|
||||
apiEndpoint: apiUrl,
|
||||
pageSize: 10,
|
||||
searchParams: initialFilters, // Set filter awal di sini
|
||||
columns: {
|
||||
select: {
|
||||
render: (item, data, context) => {
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.className = 'checkbox checkbox-sm';
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.value = data.id ? data.id.toString() : '';
|
||||
checkbox.setAttribute('data-datatable-row-check', 'true');
|
||||
return checkbox.outerHTML.trim();
|
||||
},
|
||||
},
|
||||
account_number: {
|
||||
title: 'Nomor Rekening',
|
||||
render: (item, data) => {
|
||||
return `<span class="font-medium">${data.account_number || '-'}</span>`;
|
||||
}
|
||||
},
|
||||
period: {
|
||||
title: 'Periode',
|
||||
render: (item, data) => {
|
||||
return `<span class="text-gray-700">${formatPeriod(data.period)}</span>`;
|
||||
}
|
||||
},
|
||||
updated_at: {
|
||||
title: 'Tanggal Update',
|
||||
render: (item, data) => {
|
||||
return `<span class="text-sm text-gray-600">${formatDate(data.created_at)}</span>`;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
title: 'Action',
|
||||
render: (item, data) => {
|
||||
const downloadUrl =
|
||||
`{{ route('laporan-closing-balance.download', ['accountNumber' => '__ACCOUNT__', 'period' => '__PERIOD__']) }}`
|
||||
.replace('__ACCOUNT__', data.account_number)
|
||||
.replace('__PERIOD__', data.period);
|
||||
|
||||
return `<div class="flex flex-nowrap justify-center">
|
||||
<a class="btn btn-sm btn-icon btn-clear btn-success"
|
||||
href="${downloadUrl}"
|
||||
title="Download Laporan"
|
||||
download>
|
||||
<i class="ki-outline ki-file-down"></i>
|
||||
</a>
|
||||
</div>`;
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Inisialisasi DataTable dengan filter awal sudah terset
|
||||
let dataTable = new KTDataTable(element, dataTableOptions);
|
||||
|
||||
// Update export URL dengan filter awal
|
||||
updateExportUrl(initialFilters);
|
||||
|
||||
/**
|
||||
* Fungsi untuk menerapkan filter
|
||||
*/
|
||||
function applyFilters() {
|
||||
let filters = {};
|
||||
|
||||
if (accountNumberFilter.value.trim()) {
|
||||
filters.account_number = accountNumberFilter.value.trim();
|
||||
}
|
||||
|
||||
if (startDateFilter.value) {
|
||||
filters.start_date = startDateFilter.value;
|
||||
}
|
||||
|
||||
if (endDateFilter.value) {
|
||||
filters.end_date = endDateFilter.value;
|
||||
}
|
||||
|
||||
console.log('Applying filters:', filters);
|
||||
dataTable.search(filters);
|
||||
|
||||
// Update export URL dengan filter
|
||||
updateExportUrl(filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fungsi untuk mereset filter
|
||||
*/
|
||||
function resetFilters() {
|
||||
accountNumberFilter.value = '';
|
||||
startDateFilter.value = '';
|
||||
endDateFilter.value = '';
|
||||
|
||||
dataTable.search({});
|
||||
updateExportUrl({});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fungsi untuk update URL export dengan parameter filter
|
||||
*/
|
||||
function updateExportUrl(filters) {
|
||||
const baseUrl = '{{ route('laporan-closing-balance.export') }}';
|
||||
const params = new URLSearchParams();
|
||||
|
||||
Object.keys(filters).forEach(key => {
|
||||
if (filters[key]) {
|
||||
params.append(key, filters[key]);
|
||||
}
|
||||
});
|
||||
|
||||
const newUrl = params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl;
|
||||
exportBtn.href = newUrl;
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
applyFilterBtn.addEventListener('click', applyFilters);
|
||||
resetFilterBtn.addEventListener('click', resetFilters);
|
||||
|
||||
// Auto apply filter saat enter di input
|
||||
[accountNumberFilter, startDateFilter, endDateFilter].forEach(input => {
|
||||
input.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
applyFilters();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// HAPUS bagian ini yang menyebabkan double API call:
|
||||
// setTimeout(() => {
|
||||
// applyFilters();
|
||||
// }, 100);
|
||||
</script>
|
||||
@endpush
|
||||
291
resources/views/laporan-closing-balance/show.blade.php
Normal file
291
resources/views/laporan-closing-balance/show.blade.php
Normal file
@@ -0,0 +1,291 @@
|
||||
@extends('layouts.main')
|
||||
|
||||
@section('breadcrumbs')
|
||||
{{ Breadcrumbs::render('laporan-closing-balance.show', $closingBalance) }}
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="grid">
|
||||
<!-- Header Card -->
|
||||
<div class="card mb-5">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
Detail Laporan Closing Balance
|
||||
</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="{{ route('laporan-closing-balance.index') }}" class="btn btn-light">
|
||||
<i class="ki-filled ki-left"></i>
|
||||
Kembali
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detail Information Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
Informasi Rekening
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Informasi Dasar -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col">
|
||||
<label class="text-sm font-medium text-gray-600 mb-1">Nomor Rekening</label>
|
||||
<div class="text-lg font-semibold text-gray-900">
|
||||
{{ $closingBalance->account_number }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<label class="text-sm font-medium text-gray-600 mb-1">Periode</label>
|
||||
<div class="text-lg font-semibold text-gray-900">
|
||||
@php
|
||||
$period = $closingBalance->period;
|
||||
if (strlen($period) === 8) {
|
||||
$formatted = substr($period, 6, 2) . '/' . substr($period, 4, 2) . '/' . substr($period, 0, 4);
|
||||
echo $formatted;
|
||||
} else {
|
||||
echo $period;
|
||||
}
|
||||
@endphp
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<label class="text-sm font-medium text-gray-600 mb-1">Tanggal Update Terakhir</label>
|
||||
<div class="text-lg font-semibold text-gray-900">
|
||||
{{ $closingBalance->updated_at ? $closingBalance->updated_at->format('d/m/Y H:i:s') : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informasi Saldo -->
|
||||
<div class="space-y-4">
|
||||
<div class="bg-blue-50 p-4 rounded-lg border border-blue-200">
|
||||
<label class="text-sm font-medium text-blue-700 mb-2 block">Saldo Cleared</label>
|
||||
<div class="text-2xl font-bold text-blue-800">
|
||||
@php
|
||||
$clearedBalance = $closingBalance->cleared_balance ?? 0;
|
||||
echo 'Rp ' . number_format($clearedBalance, 2, ',', '.');
|
||||
@endphp
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-green-50 p-4 rounded-lg border border-green-200">
|
||||
<label class="text-sm font-medium text-green-700 mb-2 block">Saldo Aktual</label>
|
||||
<div class="text-2xl font-bold text-green-800">
|
||||
@php
|
||||
$actualBalance = $closingBalance->actual_balance ?? 0;
|
||||
echo 'Rp ' . number_format($actualBalance, 2, ',', '.');
|
||||
@endphp
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 p-4 rounded-lg border border-gray-200">
|
||||
<label class="text-sm font-medium text-gray-700 mb-2 block">Selisih Saldo</label>
|
||||
<div class="text-2xl font-bold {{ ($actualBalance - $clearedBalance) >= 0 ? 'text-green-800' : 'text-red-800' }}">
|
||||
@php
|
||||
$difference = $actualBalance - $clearedBalance;
|
||||
$sign = $difference >= 0 ? '+' : '';
|
||||
echo $sign . 'Rp ' . number_format($difference, 2, ',', '.');
|
||||
@endphp
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Information Card -->
|
||||
@if($closingBalance->created_at || $closingBalance->updated_at)
|
||||
<div class="card mt-5">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
Informasi Sistem
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
@if($closingBalance->created_at)
|
||||
<div class="flex flex-col">
|
||||
<label class="text-sm font-medium text-gray-600 mb-1">Tanggal Dibuat</label>
|
||||
<div class="text-base text-gray-900">
|
||||
{{ $closingBalance->created_at->format('d/m/Y H:i:s') }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ $closingBalance->created_at->diffForHumans() }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($closingBalance->updated_at)
|
||||
<div class="flex flex-col">
|
||||
<label class="text-sm font-medium text-gray-600 mb-1">Tanggal Diperbarui</label>
|
||||
<div class="text-base text-gray-900">
|
||||
{{ $closingBalance->updated_at->format('d/m/Y H:i:s') }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ $closingBalance->updated_at->diffForHumans() }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="card mt-5">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap gap-3 justify-center lg:justify-start">
|
||||
<a href="{{ route('laporan-closing-balance.index') }}" class="btn btn-light">
|
||||
<i class="ki-filled ki-left"></i>
|
||||
Kembali ke Daftar
|
||||
</a>
|
||||
|
||||
<a href="{{ route('laporan-closing-balance.export', ['account_number' => $closingBalance->account_number, 'start_date' => $closingBalance->period, 'end_date' => $closingBalance->period]) }}"
|
||||
class="btn btn-primary">
|
||||
<i class="ki-filled ki-file-down"></i>
|
||||
Export Data Ini
|
||||
</a>
|
||||
|
||||
<button type="button" class="btn btn-info" onclick="window.print()">
|
||||
<i class="ki-filled ki-printer"></i>
|
||||
Print
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.btn, .card-header .flex, nav, .breadcrumb {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: none !important;
|
||||
border: 1px solid #ddd !important;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: 1.5rem !important;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 1.125rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
/**
|
||||
* Fungsi untuk memformat angka menjadi format mata uang Indonesia
|
||||
* @param {number} amount - Jumlah yang akan diformat
|
||||
* @returns {string} - String yang sudah diformat
|
||||
*/
|
||||
function formatCurrency(amount) {
|
||||
return new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 2
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fungsi untuk copy nomor rekening ke clipboard
|
||||
*/
|
||||
function copyAccountNumber() {
|
||||
const accountNumber = '{{ $closingBalance->account_number }}';
|
||||
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(accountNumber).then(function() {
|
||||
// Show success message
|
||||
if (typeof Swal !== 'undefined') {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berhasil!',
|
||||
text: 'Nomor rekening berhasil disalin ke clipboard',
|
||||
timer: 2000,
|
||||
showConfirmButton: false
|
||||
});
|
||||
} else {
|
||||
alert('Nomor rekening berhasil disalin ke clipboard');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
console.error('Error copying to clipboard: ', err);
|
||||
fallbackCopyTextToClipboard(accountNumber);
|
||||
});
|
||||
} else {
|
||||
fallbackCopyTextToClipboard(accountNumber);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback function untuk copy text jika clipboard API tidak tersedia
|
||||
*/
|
||||
function fallbackCopyTextToClipboard(text) {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
|
||||
// Avoid scrolling to bottom
|
||||
textArea.style.top = '0';
|
||||
textArea.style.left = '0';
|
||||
textArea.style.position = 'fixed';
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
if (typeof Swal !== 'undefined') {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berhasil!',
|
||||
text: 'Nomor rekening berhasil disalin ke clipboard',
|
||||
timer: 2000,
|
||||
showConfirmButton: false
|
||||
});
|
||||
} else {
|
||||
alert('Nomor rekening berhasil disalin ke clipboard');
|
||||
}
|
||||
} else {
|
||||
console.error('Fallback: Copying text command was unsuccessful');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fallback: Oops, unable to copy', err);
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
// Add click event to account number for easy copying
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const accountNumberElements = document.querySelectorAll('.account-number-clickable');
|
||||
accountNumberElements.forEach(element => {
|
||||
element.style.cursor = 'pointer';
|
||||
element.title = 'Klik untuk menyalin nomor rekening';
|
||||
element.addEventListener('click', copyAccountNumber);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
@section('content')
|
||||
<div class="grid grid-cols-8 gap-5">
|
||||
<div class="col-span-2 card">
|
||||
<div class="col-span-2 bg-gray-100 card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Request Print Stetement</h3>
|
||||
</div>
|
||||
@@ -20,22 +20,22 @@
|
||||
@endif
|
||||
|
||||
<div class="grid grid-cols-1 gap-5">
|
||||
|
||||
@if ($multiBranch)
|
||||
@if (!$multiBranch)
|
||||
<div class="form-group">
|
||||
<label class="form-label required" for="branch_id">Branch/Cabang</label>
|
||||
<select class="input form-control tomselect @error('branch_id') is-invalid @enderror"
|
||||
id="branch_id" name="branch_id" required>
|
||||
<label class="form-label required" for="branch_code">Branch/Cabang</label>
|
||||
<select
|
||||
class="input form-control tomselect @error('branch_code') border-danger bg-danger-light @enderror"
|
||||
id="branch_code" name="branch_code" required>
|
||||
<option value="">Pilih Branch/Cabang</option>
|
||||
@foreach ($branches as $branchOption)
|
||||
<option value="{{ $branchOption->code }}"
|
||||
{{ old('branch_id', $statement->branch_id ?? ($branch->code ?? '')) == $branchOption->code ? 'selected' : '' }}>
|
||||
{{ old('branch_code', $statement->branch_code ?? ($branch->code ?? '')) == $branchOption->code ? 'selected' : '' }}>
|
||||
{{ $branchOption->code }} - {{ $branchOption->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('branch_id')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@error('branch_code')
|
||||
<div class="text-sm alert text-danger">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
@else
|
||||
@@ -43,27 +43,79 @@
|
||||
<label class="form-label" for="branch_display">Branch/Cabang</label>
|
||||
<input type="text" class="input form-control" id="branch_display"
|
||||
value="{{ $branch->code ?? '' }} - {{ $branch->name ?? '' }}" readonly>
|
||||
<input type="hidden" name="branch_id" value="{{ $branch->code ?? '' }}">
|
||||
<input type="hidden" name="branch_code" value="{{ $branch->code ?? '' }}">
|
||||
@error('branch_code')
|
||||
<div class="text-sm alert text-danger">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label required" for="account_number">Account Number</label>
|
||||
<input type="text" class="input form-control @error('account_number') is-invalid @enderror"
|
||||
<label class="form-label" for="stmt_sent_type">Statement Type</label>
|
||||
<select
|
||||
class="select tomselect @error('stmt_sent_type') border-danger bg-danger-light @enderror"
|
||||
id="stmt_sent_type" name="stmt_sent_type[]" multiple>
|
||||
<option value="ALL"
|
||||
{{ in_array('ALL', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}>
|
||||
ALL
|
||||
</option>
|
||||
<option value="BY.EMAIL"
|
||||
{{ in_array('BY.EMAIL', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}>
|
||||
BY EMAIL
|
||||
</option>
|
||||
<option value="BY.MAIL.TO.DOM.ADDR"
|
||||
{{ in_array('BY.MAIL.TO.DOM.ADDR', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}>
|
||||
BY MAIL TO DOM ADDR
|
||||
</option>
|
||||
<option value="BY.MAIL.TO.KTP.ADDR"
|
||||
{{ in_array('BY.MAIL.TO.KTP.ADDR', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}>
|
||||
BY MAIL TO KTP ADDR
|
||||
</option>
|
||||
<option value="PRINT"
|
||||
{{ in_array('PRINT', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}>
|
||||
PRINT
|
||||
</option>
|
||||
</select>
|
||||
@error('stmt_sent_type')
|
||||
<div class="text-sm alert text-danger">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="account_number">Account Number</label>
|
||||
<input type="text"
|
||||
class="input form-control @error('account_number') border-danger bg-danger-light @enderror"
|
||||
id="account_number" name="account_number"
|
||||
value="{{ old('account_number', $statement->account_number ?? '') }}" required>
|
||||
value="{{ old('account_number', $statement->account_number ?? '') }}">
|
||||
@error('account_number')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
<div class="text-sm alert text-danger">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="email">Email</label>
|
||||
<input type="email" class="input form-control @error('email') is-invalid @enderror"
|
||||
<input type="email"
|
||||
class="input form-control @error('email') border-danger bg-danger-light @enderror"
|
||||
id="email" name="email" value="{{ old('email', $statement->email ?? '') }}"
|
||||
placeholder="Optional email for send statement">
|
||||
@error('email')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
<div class="text-sm alert text-danger">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Tambahan field password -->
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password">PDF Password</label>
|
||||
<input type="password"
|
||||
class="input form-control @error('password') border-danger bg-danger-light @enderror"
|
||||
id="password" name="password" value="{{ old('password', $statement->password ?? '') }}"
|
||||
placeholder="Optional password untuk proteksi PDF statement" autocomplete="new-password">
|
||||
<div class="mt-1 text-xs text-primary">
|
||||
<i class="text-sm ki-outline ki-information-5"></i>
|
||||
Jika dikosongkan password default statement akan diberlakukan
|
||||
</div>
|
||||
@error('password')
|
||||
<div class="text-sm alert text-danger">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
@@ -79,6 +131,7 @@
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
@if (auth()->user()->branch->code === '0988')
|
||||
<div class="form-group">
|
||||
<label class="form-label required" for="end_date">End Date</label>
|
||||
<input class="input @error('period_to') border-danger bg-danger-light @enderror" type="month"
|
||||
@@ -88,6 +141,7 @@
|
||||
<em class="text-sm alert text-danger">{{ $message }}</em>
|
||||
@enderror
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-5 text-end">
|
||||
@@ -101,317 +155,373 @@
|
||||
</div>
|
||||
<div class="col-span-6">
|
||||
<div class="min-w-full card card-grid" data-datatable="false" data-datatable-page-size="10"
|
||||
data-datatable-state-save="false" id="statement-table" data-api-url="{{ route('statements.datatables') }}">
|
||||
data-datatable-state-save="false" id="statement-table"
|
||||
data-api-url="{{ route('statements.datatables') }}">
|
||||
<div class="flex-wrap py-5 card-header">
|
||||
<h3 class="card-title">
|
||||
Daftar Statement Request
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-2 lg:gap-5">
|
||||
<div class="flex">
|
||||
<label class="input input-sm"> <i class="ki-filled ki-magnifier"> </i>
|
||||
<input placeholder="Search Statement" id="search" type="text" value="">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="scrollable-x-auto">
|
||||
<table class="table text-sm font-medium text-gray-700 align-middle table-auto table-border"
|
||||
data-datatable-table="true">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-14">
|
||||
<input class="checkbox checkbox-sm" data-datatable-check="true" type="checkbox" />
|
||||
</th>
|
||||
<th class="min-w-[100px]" data-datatable-column="id">
|
||||
<span class="sort"> <span class="sort-label"> ID </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="branch_name">
|
||||
<span class="sort"> <span class="sort-label"> Branch </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="account_number">
|
||||
<span class="sort"> <span class="sort-label"> Account Number </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="period">
|
||||
<span class="sort"> <span class="sort-label"> Period </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="is_available">
|
||||
<span class="sort"> <span class="sort-label"> Available </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="remarks">
|
||||
<span class="sort"> <span class="sort-label"> Notes </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[180px]" data-datatable-column="created_at">
|
||||
<span class="sort"> <span class="sort-label"> Created At </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[50px] text-center" data-datatable-column="actions">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
<div
|
||||
class="flex-col gap-3 justify-center font-medium text-gray-600 card-footer md:justify-between md:flex-row text-2sm">
|
||||
<div class="flex gap-2 items-center">
|
||||
Show
|
||||
<select class="w-16 select select-sm" data-datatable-size="true" name="perpage"> </select>
|
||||
per page
|
||||
</div>
|
||||
<div class="flex gap-4 items-center">
|
||||
<span data-datatable-info="true"> </span>
|
||||
<div class="pagination" data-datatable-pagination="true">
|
||||
<div class="min-w-full card card-grid" data-datatable="false" data-datatable-page-size="10"
|
||||
data-datatable-state-save="false" id="statement-table"
|
||||
data-api-url="{{ route('statements.datatables') }}">
|
||||
<div class="flex-wrap py-5 card-header">
|
||||
<h3 class="card-title">
|
||||
Daftar Statement Request
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-2 lg:gap-5">
|
||||
<div class="flex">
|
||||
<label class="input input-sm"> <i class="ki-filled ki-magnifier"> </i>
|
||||
<input placeholder="Search Statement" id="search" type="text"
|
||||
value="">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
<div class="card-body">
|
||||
<div class="scrollable-x-auto">
|
||||
<table class="table text-sm font-medium text-gray-700 align-middle table-auto table-border"
|
||||
data-datatable-table="true">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-14">
|
||||
<input class="checkbox checkbox-sm" data-datatable-check="true"
|
||||
type="checkbox" />
|
||||
</th>
|
||||
<th class="min-w-[100px]" data-datatable-column="id">
|
||||
<span class="sort"> <span class="sort-label"> ID </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="branch_name">
|
||||
<span class="sort"> <span class="sort-label"> Branch </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="account_number">
|
||||
<span class="sort"> <span class="sort-label"> Account Number </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="period">
|
||||
<span class="sort"> <span class="sort-label"> Period </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="is_available">
|
||||
<span class="sort"> <span class="sort-label"> Available </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="is_generated">
|
||||
<span class="sort"> <span class="sort-label"> Generated </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[150px]" data-datatable-column="remarks">
|
||||
<span class="sort"> <span class="sort-label"> Notes </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[180px]" data-datatable-column="created_at">
|
||||
<span class="sort"> <span class="sort-label"> Created At </span>
|
||||
<span class="sort-icon"> </span> </span>
|
||||
</th>
|
||||
<th class="min-w-[50px] text-center" data-datatable-column="actions">
|
||||
Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
<div
|
||||
class="flex-col gap-3 justify-center font-medium text-gray-600 card-footer md:justify-between md:flex-row text-2sm">
|
||||
<div class="flex gap-2 items-center">
|
||||
<div
|
||||
class="flex-col gap-3 justify-center font-medium text-gray-600 card-footer md:justify-between md:flex-row text-2sm">
|
||||
<div class="flex gap-2 items-center">
|
||||
Show
|
||||
<select class="w-16 select select-sm" data-datatable-size="true"
|
||||
name="perpage"> </select>
|
||||
per page
|
||||
</div>
|
||||
<div class="flex gap-4 items-center">
|
||||
<div class="flex gap-4 items-center">
|
||||
<span data-datatable-info="true"> </span>
|
||||
<div class="pagination" data-datatable-pagination="true">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
/**
|
||||
* Fungsi untuk menghapus data statement
|
||||
* @param {number} data - ID statement yang akan dihapus
|
||||
*/
|
||||
function deleteData(data) {
|
||||
Swal.fire({
|
||||
title: 'Are you sure?',
|
||||
text: "You won't be able to revert this!",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Yes, delete it!'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$.ajaxSetup({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
}
|
||||
});
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
/**
|
||||
* Fungsi untuk menghapus data statement
|
||||
* @param {number} data - ID statement yang akan dihapus
|
||||
*/
|
||||
function deleteData(data) {
|
||||
Swal.fire({
|
||||
title: 'Are you sure?',
|
||||
text: "You won't be able to revert this!",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Yes, delete it!'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$.ajaxSetup({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
}
|
||||
});
|
||||
|
||||
$.ajax(`statements/${data}`, {
|
||||
type: 'DELETE'
|
||||
}).then((response) => {
|
||||
swal.fire('Deleted!', 'Statement request has been deleted.', 'success').then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.error('Error:', error);
|
||||
Swal.fire('Error!', 'An error occurred while deleting the record.', 'error');
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
$.ajax(`statements/${data}`, {
|
||||
type: 'DELETE'
|
||||
}).then((response) => {
|
||||
swal.fire('Deleted!', 'Statement request has been deleted.', 'success').then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.error('Error:', error);
|
||||
Swal.fire('Error!', 'An error occurred while deleting the record.', 'error');
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Konfirmasi email sebelum submit form
|
||||
* Menampilkan SweetAlert jika email diisi untuk konfirmasi pengiriman
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.querySelector('form');
|
||||
const emailInput = document.getElementById('email');
|
||||
/**
|
||||
* Konfirmasi password dan email sebelum submit form
|
||||
* Menampilkan SweetAlert jika password atau email diisi untuk konfirmasi
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.querySelector('form');
|
||||
const emailInput = document.getElementById('email');
|
||||
const passwordInput = document.getElementById('password');
|
||||
|
||||
// Log: Inisialisasi event listener untuk konfirmasi email
|
||||
console.log('Email confirmation listener initialized');
|
||||
// Log: Inisialisasi event listener untuk konfirmasi
|
||||
console.log('Form confirmation listener initialized');
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
const emailValue = emailInput.value.trim();
|
||||
form.addEventListener('submit', function(e) {
|
||||
const emailValue = emailInput.value.trim();
|
||||
const passwordValue = passwordInput.value.trim();
|
||||
|
||||
// Jika email diisi, tampilkan konfirmasi
|
||||
if (emailValue) {
|
||||
e.preventDefault(); // Hentikan submit form sementara
|
||||
let confirmationNeeded = false;
|
||||
let confirmationMessage = '';
|
||||
|
||||
// Log: Email terdeteksi, menampilkan konfirmasi
|
||||
console.log('Email detected:', emailValue);
|
||||
// Jika email diisi
|
||||
if (emailValue) {
|
||||
confirmationNeeded = true;
|
||||
confirmationMessage += `• Statement akan dikirim ke email: ${emailValue}\n`;
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: 'Konfirmasi Pengiriman Email',
|
||||
text: `Apakah Anda yakin ingin mengirimkan statement ke email: ${emailValue}?`,
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Ya, Kirim Email',
|
||||
cancelButtonText: 'Batal',
|
||||
reverseButtons: true
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
// Log: User konfirmasi pengiriman email
|
||||
console.log('User confirmed email sending');
|
||||
// Jika password diisi
|
||||
if (passwordValue) {
|
||||
confirmationNeeded = true;
|
||||
confirmationMessage += `• PDF akan diproteksi dengan password\n`;
|
||||
}
|
||||
|
||||
// Submit form setelah konfirmasi
|
||||
form.submit();
|
||||
} else {
|
||||
// Log: User membatalkan pengiriman email
|
||||
console.log('User cancelled email sending');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Log: Tidak ada email, submit form normal
|
||||
console.log('No email provided, submitting form normally');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
// Jika ada yang perlu dikonfirmasi
|
||||
if (confirmationNeeded) {
|
||||
e.preventDefault(); // Hentikan submit form sementara
|
||||
|
||||
<script type="module">
|
||||
const element = document.querySelector('#statement-table');
|
||||
const searchInput = document.getElementById('search');
|
||||
// Log: Konfirmasi diperlukan
|
||||
console.log('Confirmation needed:', {
|
||||
email: emailValue,
|
||||
hasPassword: !!passwordValue
|
||||
});
|
||||
|
||||
const apiUrl = element.getAttribute('data-api-url');
|
||||
const dataTableOptions = {
|
||||
apiEndpoint: apiUrl,
|
||||
pageSize: 10,
|
||||
columns: {
|
||||
select: {
|
||||
render: (item, data, context) => {
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.className = 'checkbox checkbox-sm';
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.value = data.id.toString();
|
||||
checkbox.setAttribute('data-datatable-row-check', 'true');
|
||||
return checkbox.outerHTML.trim();
|
||||
},
|
||||
},
|
||||
id: {
|
||||
title: 'ID',
|
||||
},
|
||||
branch_name: {
|
||||
title: 'Branch',
|
||||
},
|
||||
account_number: {
|
||||
title: 'Account Number',
|
||||
},
|
||||
period: {
|
||||
title: 'Period',
|
||||
render: (item, data) => {
|
||||
const monthNames = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'
|
||||
];
|
||||
Swal.fire({
|
||||
title: 'Konfirmasi Request Statement',
|
||||
text: `Mohon konfirmasi pengaturan berikut:\n\n${confirmationMessage}\nApakah Anda yakin ingin melanjutkan?`,
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Ya, Lanjutkan',
|
||||
cancelButtonText: 'Batal',
|
||||
reverseButtons: true,
|
||||
preConfirm: () => {
|
||||
// Validasi password jika diisi
|
||||
if (passwordValue && passwordValue.length < 6) {
|
||||
Swal.showValidationMessage('Password minimal 6 karakter');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
// Log: User konfirmasi
|
||||
console.log('User confirmed form submission');
|
||||
|
||||
const formatPeriod = (period) => {
|
||||
if (!period) return '';
|
||||
const year = period.substring(0, 4);
|
||||
const month = parseInt(period.substring(4, 6));
|
||||
return `${monthNames[month - 1]} ${year}`;
|
||||
};
|
||||
// Submit form setelah konfirmasi
|
||||
form.submit();
|
||||
} else {
|
||||
// Log: User membatalkan
|
||||
console.log('User cancelled form submission');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
const fromPeriod = formatPeriod(data.period_from);
|
||||
const toPeriod = data.period_to ? ` - ${formatPeriod(data.period_to)}` : '';
|
||||
<script type="module">
|
||||
const element = document.querySelector('#statement-table');
|
||||
const searchInput = document.getElementById('search');
|
||||
|
||||
return fromPeriod + toPeriod;
|
||||
},
|
||||
},
|
||||
is_available: {
|
||||
title: 'Available',
|
||||
render: (item, data) => {
|
||||
let statusClass = data.is_available ? 'badge badge-light-success' :
|
||||
'badge badge-light-danger';
|
||||
let statusText = data.is_available ? 'Yes' : 'No';
|
||||
return `<span class="${statusClass}">${statusText}</span>`;
|
||||
},
|
||||
},
|
||||
remarks: {
|
||||
title: 'Notes',
|
||||
},
|
||||
created_at: {
|
||||
title: 'Created At',
|
||||
render: (item, data) => {
|
||||
return data.created_at ?? '';
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
title: 'Actions',
|
||||
render: (item, data) => {
|
||||
let buttons = `<div class="flex flex-nowrap justify-center">
|
||||
const apiUrl = element.getAttribute('data-api-url');
|
||||
const dataTableOptions = {
|
||||
apiEndpoint: apiUrl,
|
||||
pageSize: 10,
|
||||
columns: {
|
||||
select: {
|
||||
render: (item, data, context) => {
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.className = 'checkbox checkbox-sm';
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.value = data.id.toString();
|
||||
checkbox.setAttribute('data-datatable-row-check', 'true');
|
||||
return checkbox.outerHTML.trim();
|
||||
},
|
||||
},
|
||||
id: {
|
||||
title: 'ID',
|
||||
},
|
||||
branch_name: {
|
||||
title: 'Branch',
|
||||
},
|
||||
account_number: {
|
||||
title: 'Account Number'
|
||||
},
|
||||
period: {
|
||||
title: 'Period',
|
||||
render: (item, data) => {
|
||||
const monthNames = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'
|
||||
];
|
||||
|
||||
const formatPeriod = (period) => {
|
||||
if (!period) return '';
|
||||
const year = period.substring(0, 4);
|
||||
const month = parseInt(period.substring(4, 6));
|
||||
return `${monthNames[month - 1]} ${year}`;
|
||||
};
|
||||
|
||||
const fromPeriod = formatPeriod(data.period_from);
|
||||
const toPeriod = data.period_to ? ` - ${formatPeriod(data.period_to)}` : '';
|
||||
|
||||
return fromPeriod + toPeriod;
|
||||
},
|
||||
},
|
||||
is_available: {
|
||||
title: 'Available',
|
||||
render: (item, data) => {
|
||||
let statusClass = data.is_available ? 'badge badge-light-success' :
|
||||
|
||||
'badge badge-light-danger';
|
||||
let statusText = data.is_available ? 'Yes' : 'No';
|
||||
return `<span class="${statusClass}">${statusText}</span>`;
|
||||
},
|
||||
},
|
||||
is_generated: {
|
||||
title: 'Generated',
|
||||
render: (item, data) => {
|
||||
let statusClass = data.is_generated ? 'badge badge-light-success' :
|
||||
'badge badge-light-danger';
|
||||
let statusText = data.is_generated ? 'Yes' : 'No';
|
||||
return `<span class="${statusClass}">${statusText}</span>`;
|
||||
},
|
||||
},
|
||||
remarks: {
|
||||
title: 'Notes',
|
||||
},
|
||||
created_at: {
|
||||
title: 'Created At',
|
||||
render: (item, data) => {
|
||||
return data.created_at ?? '';
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
title: 'Actions',
|
||||
render: (item, data) => {
|
||||
let buttons = `<div class="flex flex-nowrap justify-center">
|
||||
<a class="btn btn-sm btn-icon btn-clear btn-info" href="statements/${data.id}">
|
||||
<i class="ki-outline ki-eye"></i>
|
||||
</a>`;
|
||||
|
||||
// Show download button if statement is approved and available but not downloaded
|
||||
//if (data.authorization_status === 'approved' && data.is_available && !data.is_downloaded) {
|
||||
if (data.is_available) {
|
||||
buttons += `<a class="btn btn-sm btn-icon btn-clear btn-success" href="statements/${data.id}/download">
|
||||
// Show download button if statement is approved and available but not downloaded
|
||||
//if (data.authorization_status === 'approved' && data.is_available && !data.is_downloaded) {
|
||||
if (data.is_available) {
|
||||
buttons += `<a class="btn btn-sm btn-icon btn-clear btn-success" href="statements/${data.id}/download">
|
||||
<i class="ki-outline ki-cloud-download"></i>
|
||||
</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Show send email button if email is not empty and statement is available
|
||||
if (data.is_available && data.email) {
|
||||
buttons += `<a class="btn btn-sm btn-icon btn-clear btn-primary" href="statements/${data.id}/send-email" title="Send to Email">
|
||||
// Show send email button if email is not empty and statement is available
|
||||
if (data.is_available && data.email) {
|
||||
buttons += `<a class="btn btn-sm btn-icon btn-clear btn-primary" href="statements/${data.id}/send-email" title="Send to Email">
|
||||
<i class="ki-outline ki-paper-plane"></i>
|
||||
</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Only show delete button if status is pending
|
||||
if (data.authorization_status === 'pending') {
|
||||
buttons += `<a onclick="deleteData(${data.id})" class="delete btn btn-sm btn-icon btn-clear btn-danger">
|
||||
// Only show delete button if status is pending
|
||||
if (data.authorization_status === 'pending') {
|
||||
buttons += `<a onclick="deleteData(${data.id})" class="delete btn btn-sm btn-icon btn-clear btn-danger">
|
||||
<i class="ki-outline ki-trash"></i>
|
||||
</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
buttons += `</div>`;
|
||||
return buttons;
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
buttons += `</div>`;
|
||||
return buttons;
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let dataTable = new KTDataTable(element, dataTableOptions);
|
||||
// Custom search functionality
|
||||
searchInput.addEventListener('input', function() {
|
||||
const searchValue = this.value.trim();
|
||||
dataTable.search(searchValue, true);
|
||||
});
|
||||
let dataTable = new KTDataTable(element, dataTableOptions);
|
||||
// Custom search functionality
|
||||
searchInput.addEventListener('input', function() {
|
||||
searchInput.addEventListener('input', function() {
|
||||
const searchValue = this.value.trim();
|
||||
dataTable.search(searchValue, true);
|
||||
});
|
||||
|
||||
// Handle the "select all" checkbox
|
||||
const selectAllCheckbox = document.querySelector('input[data-datatable-check="true"]');
|
||||
if (selectAllCheckbox) {
|
||||
selectAllCheckbox.addEventListener('change', function() {
|
||||
const isChecked = this.checked;
|
||||
const rowCheckboxes = document.querySelectorAll('input[data-datatable-row-check="true"]');
|
||||
// Handle the "select all" checkbox
|
||||
const selectAllCheckbox = document.querySelector('input[data-datatable-check="true"]');
|
||||
if (selectAllCheckbox) {
|
||||
selectAllCheckbox.addEventListener('change', function() {
|
||||
const isChecked = this.checked;
|
||||
const rowCheckboxes = document.querySelectorAll(
|
||||
'input[data-datatable-row-check="true"]');
|
||||
|
||||
rowCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = isChecked;
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
rowCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = isChecked;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Validate that end date is after start date
|
||||
const startDateInput = document.getElementById('start_date');
|
||||
const endDateInput = document.getElementById('end_date');
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Validate that end date is after start date
|
||||
const startDateInput = document.getElementById('start_date');
|
||||
const endDateInput = document.getElementById('end_date');
|
||||
|
||||
function validateDates() {
|
||||
const startDate = new Date(startDateInput.value);
|
||||
const endDate = new Date(endDateInput.value);
|
||||
function validateDates() {
|
||||
const startDate = new Date(startDateInput.value);
|
||||
const endDate = new Date(endDateInput.value);
|
||||
|
||||
if (startDate > endDate) {
|
||||
endDateInput.setCustomValidity('End date must be after start date');
|
||||
} else {
|
||||
endDateInput.setCustomValidity('');
|
||||
}
|
||||
}
|
||||
if (startDate > endDate) {
|
||||
endDateInput.setCustomValidity('End date must be after start date');
|
||||
} else {
|
||||
endDateInput.setCustomValidity('');
|
||||
}
|
||||
}
|
||||
|
||||
startDateInput.addEventListener('change', validateDates);
|
||||
endDateInput.addEventListener('change', validateDates);
|
||||
startDateInput.addEventListener('change', validateDates);
|
||||
endDateInput.addEventListener('change', validateDates);
|
||||
|
||||
// Set max date for date inputs to today
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
startDateInput.setAttribute('max', today);
|
||||
endDateInput.setAttribute('max', today);
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
// Set max date for date inputs to today
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
startDateInput.setAttribute('max', today);
|
||||
endDateInput.setAttribute('max', today);
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
595
resources/views/statements/stmt.blade.php
Normal file
595
resources/views/statements/stmt.blade.php
Normal file
@@ -0,0 +1,595 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Rekening Tabungan</title>
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #fff;
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 190mm;
|
||||
min-height: 277mm;
|
||||
margin: 10mm auto;
|
||||
background: #ffffff;
|
||||
border: 1px solid #ccc;
|
||||
padding: 10mm;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.watermark {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 1;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.watermark img {
|
||||
margin: 0px 50px;
|
||||
width: 85%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
/* Ensure content is above watermark */
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
padding-bottom: 10px;
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
.header .title {
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
color: #0056b3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.header .title h1 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header .logo {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.header .logo img {
|
||||
max-height: 50px;
|
||||
margin-right: 10px
|
||||
}
|
||||
|
||||
.info-section {
|
||||
text-transform: uppercase;
|
||||
padding: 10px 0;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.info-section .column {
|
||||
width: 48%;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.info-section .column p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
padding-top: 15px;
|
||||
flex: 1;
|
||||
/* Allow table section to grow */
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
table th,
|
||||
table td {
|
||||
padding: 5px;
|
||||
text-align: left;
|
||||
font-size: 10px;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
table th {
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
table td.text-right {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
table td.text-center {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
table td.text-left {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
margin-top: auto;
|
||||
position: relative;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
/* Ensure footer is above watermark */
|
||||
}
|
||||
|
||||
.footer p {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.footer .highlight {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.left-25 {
|
||||
margin-left: 25px !important;
|
||||
}
|
||||
|
||||
.same-size {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.sponsor {
|
||||
border-top: 1.5px solid black;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
margin: 10px 0px 0px 0px;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
tbody td {
|
||||
border-bottom: none;
|
||||
border-top: none;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* Menghilangkan padding dan margin untuk baris narrative tambahan */
|
||||
tbody tr td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
/* Khusus untuk baris narrative tambahan - tanpa padding/margin */
|
||||
tbody tr.narrative-line td {
|
||||
padding: 2px 5px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Column width classes */
|
||||
.col-date {
|
||||
width: 10%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.col-desc {
|
||||
width: 25%;
|
||||
min-width: 150px;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.col-valuta {
|
||||
width: 10%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.col-referensi {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.col-debet,
|
||||
.col-kredit {
|
||||
width: 12.5%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.col-saldo {
|
||||
width: 15%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.page-number {
|
||||
position: absolute;
|
||||
bottom: 5mm;
|
||||
right: 10mm;
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0;
|
||||
border: initial;
|
||||
border-radius: initial;
|
||||
width: initial;
|
||||
min-height: initial;
|
||||
box-shadow: initial;
|
||||
background: initial;
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
.container:last-of-type {
|
||||
page-break-after: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@php
|
||||
$saldo = $saldoAwalBulan->actual_balance ?? 0;
|
||||
$totalDebit = 0;
|
||||
$totalKredit = 0;
|
||||
$line = 1;
|
||||
$linePerPage = 23;
|
||||
@endphp
|
||||
@php
|
||||
// Hitung tanggal periode berdasarkan $period
|
||||
$periodDates = calculatePeriodDates($period);
|
||||
|
||||
// Jika endPeriod ada, gunakan endPeriod sebagai batas akhir, jika tidak, gunakan period
|
||||
$endPeriodDate = $endPeriod ? calculatePeriodDates($endPeriod) : $periodDates;
|
||||
|
||||
$startDate = $periodDates['start'];
|
||||
$endDate = $endPeriodDate['end'] ?? $periodDates['end'];
|
||||
|
||||
// Log hasil perhitungan
|
||||
\Log::info('Period dates calculated', [
|
||||
'period' => $period,
|
||||
'start_date' => $startDate->format('d/m/Y'),
|
||||
'end_date' => $endDate->format('d/m/Y'),
|
||||
]);
|
||||
@endphp
|
||||
|
||||
@php
|
||||
// Calculate total pages based on actual line count
|
||||
$totalLines = 0;
|
||||
foreach ($stmtEntries as $entry) {
|
||||
// Split narrative into multiple lines of approximately 35 characters, breaking at word boundaries
|
||||
$narrative = $entry->description ?? '';
|
||||
$words = explode(' ', $narrative);
|
||||
|
||||
$narrativeLineCount = 0;
|
||||
$currentLine = '';
|
||||
|
||||
foreach ($words as $word) {
|
||||
if (strlen($currentLine . ' ' . $word) > 30) {
|
||||
$narrativeLineCount++;
|
||||
$currentLine = $word;
|
||||
} else {
|
||||
$currentLine .= ($currentLine ? ' ' : '') . $word;
|
||||
}
|
||||
}
|
||||
if ($currentLine) {
|
||||
$narrativeLineCount++;
|
||||
}
|
||||
|
||||
// Each entry takes at least one line for the main data + narrative lines + gap row
|
||||
$totalLines += $narrativeLineCount; // +1 for gap row
|
||||
}
|
||||
|
||||
// Add 1 for the "Saldo Awal Bulan" row
|
||||
$totalLines += 1;
|
||||
|
||||
// Calculate total pages ($linePerPage lines per page)
|
||||
$totalPages = ceil($totalLines / $linePerPage);
|
||||
$pageNumber = 0;
|
||||
|
||||
$footerContent =
|
||||
'
|
||||
<div class="footer">
|
||||
<p class="sponsor">Belanja puas di Electronic City! Dapatkan cashback hingga 250 ribu dan nikmati makan enak dengan cashback hingga 25 ribu. Bayar pakai QRIS AGI. S&K berlaku. Info lengkap: www.arthagraha.com</p>
|
||||
|
||||
<p class="sponsor">Waspada dalam bertransaksi QRIS! Periksa kembali identitas penjual & nominal pembayaran sebelum melanjutkan transaksi. Info terkait Bank Artha Graha Internasional, kunjungi website www.arthagraha.com</p>
|
||||
|
||||
<div class="highlight">
|
||||
<img src="' .
|
||||
public_path('assets/media/images/banner-footer.png') .
|
||||
'" alt="Logo Bank" style="width: 100%; height: auto;">
|
||||
</div>
|
||||
</div>
|
||||
';
|
||||
@endphp
|
||||
|
||||
<div class="container">
|
||||
<div class="watermark">
|
||||
<img src="{{ public_path('assets/media/images/watermark.png') }}" alt="Watermark">
|
||||
</div>
|
||||
<div class="content-wrapper">
|
||||
<!-- Header Section -->
|
||||
<div class="header">
|
||||
<div class="logo">
|
||||
<img src="{{ public_path('assets/media/images/logo-arthagraha.png') }}" alt="Logo Bank">
|
||||
<img src="{{ public_path('assets/media/images/logo-agi.png') }}" alt="Logo Bank">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bank Information Section -->
|
||||
<div class="info-section">
|
||||
<div class="column">
|
||||
<p>{{ $branch->name }}</p>
|
||||
<p style="text-transform: capitalize">Kepada</p>
|
||||
<p>{{ $customer->name }}</p>
|
||||
@if ($account->stmt_sent_type == 'BY.MAIL.TO.DOM.ADDR')
|
||||
<p>{{ $customer->l_dom_street ?? $customer->address }}</p>
|
||||
<p>{{ $customer->district }}
|
||||
{{ ($customer->ktp_rt ?: $customer->home_rt) ? 'RT ' . ($customer->ktp_rt ?: $customer->home_rt) : '' }}
|
||||
{{ ($customer->ktp_rw ?: $customer->home_rw) ? 'RW ' . ($customer->ktp_rw ?: $customer->home_rw) : '' }}
|
||||
</p>
|
||||
<p>{{ trim($customer->city . ' ' . ($customer->province ? getProvinceCoreName($customer->province) . ' ' : '') . ($customer->postal_code ?? '')) }}
|
||||
@else
|
||||
<p>{{ $customer->address }}</p>
|
||||
<p>{{ $customer->district }}
|
||||
{{ ($customer->ktp_rt ?: $customer->home_rt) ? 'RT ' . ($customer->ktp_rt ?: $customer->home_rt) : '' }}
|
||||
{{ ($customer->ktp_rw ?: $customer->home_rw) ? 'RW ' . ($customer->ktp_rw ?: $customer->home_rw) : '' }}
|
||||
</p>
|
||||
<p>{{ trim($customer->city . ' ' . ($customer->province ? getProvinceCoreName($customer->province) . ' ' : '') . ($customer->postal_code ?? '')) }}
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
<div style="text-transform: capitalize;" class="column">
|
||||
<p style="padding-left:50px"><span class="same-size">Periode Statement </span>:
|
||||
{{ dateFormat($startDate) }} <span style="text-transform:lowercase !important">s/d</span>
|
||||
{{ dateFormat($endDate) }}</p>
|
||||
<p style="padding-left:50px"><span class="same-size">Nomor Rekening</span>:
|
||||
{{ $account->account_number }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table Section -->
|
||||
<div class="table-section">
|
||||
<table>
|
||||
<thead>
|
||||
<tr
|
||||
style="@if ($headerTableBg) background-image: url('data:image/png;base64,{{ $headerTableBg }}'); background-repeat: no-repeat; background-size: cover; background-position: center; @else background-color: #0056b3; @endif height: 30px;">
|
||||
<th class="col-date">Tanggal</th>
|
||||
<th class="col-valuta">Tanggal<br>Valuta</th>
|
||||
<th class="text-left col-desc">Keterangan</th>
|
||||
<th class="text-left col-referensi">Referensi</th>
|
||||
<th class="col-debet">Debet</th>
|
||||
<th class="col-kredit">Kredit</th>
|
||||
<th class="col-saldo">Saldo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center"></td>
|
||||
<td class="text-center"> </td>
|
||||
<td><strong>Saldo Awal Bulan</strong></td>
|
||||
<td class="text-center"> </td>
|
||||
<td class="text-right"> </td>
|
||||
<td class="text-right"> </td>
|
||||
<td class="text-right">
|
||||
<strong>{{ number_format((float) $saldoAwalBulan->actual_balance, 2, ',', '.') }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@foreach ($stmtEntries as $row)
|
||||
@php
|
||||
$debit = $row->transaction_amount < 0 ? abs($row->transaction_amount) : 0;
|
||||
$kredit = $row->transaction_amount > 0 ? $row->transaction_amount : 0;
|
||||
$saldo += $kredit - $debit;
|
||||
$totalDebit += $debit;
|
||||
$totalKredit += $kredit;
|
||||
|
||||
// Split narrative into multiple lines of approximately 35 characters, breaking at word boundaries
|
||||
$narrative = $row->description ?? '';
|
||||
$words = explode(' ', $narrative);
|
||||
$narrativeLines = [];
|
||||
$currentLine = '';
|
||||
foreach ($words as $word) {
|
||||
if (strlen($currentLine . ' ' . $word) > 30) {
|
||||
$narrativeLines[] = trim($currentLine);
|
||||
$currentLine = $word;
|
||||
} else {
|
||||
$currentLine .= ($currentLine ? ' ' : '') . $word;
|
||||
}
|
||||
}
|
||||
if ($currentLine) {
|
||||
$narrativeLines[] = trim($currentLine);
|
||||
}
|
||||
|
||||
@endphp
|
||||
<tr>
|
||||
<td class="text-center">{{ date('d/m/Y', strtotime($row->transaction_date)) }}</td>
|
||||
<td class="text-center">{{ substr($row->actual_date, 0, 10) }}</td>
|
||||
<td>{{ str_replace(['[', ']'], ' ', $narrativeLines[0] ?? '') }}</td>
|
||||
<td>{{ $row->reference_number }}</td>
|
||||
<td class="text-right">
|
||||
{{ $debit > 0 ? number_format((float) $debit, 2, ',', '.') : '' }}</td>
|
||||
<td class="text-right">
|
||||
{{ $kredit > 0 ? number_format((float) $kredit, 2, ',', '.') : '' }}
|
||||
</td>
|
||||
<td class="text-right">{{ number_format((float) $saldo, 2, ',', '.') }}</td>
|
||||
</tr>
|
||||
@for ($i = 1; $i < count($narrativeLines); $i++)
|
||||
<tr class="narrative-line">
|
||||
<td class="text-center"></td>
|
||||
<td class="text-center"></td>
|
||||
<td>{{ str_replace(['[', ']'], ' ', $narrativeLines[$i] ?? '') }}</td>
|
||||
<td class="text-center"></td>
|
||||
<td class="text-right"></td>
|
||||
<td class="text-right"></td>
|
||||
<td class="text-right"></td>
|
||||
</tr>
|
||||
@endfor
|
||||
@php $line += count($narrativeLines); @endphp
|
||||
<!-- Add a gap row -->
|
||||
<tr class="gap-row">
|
||||
<td class="text-center"></td>
|
||||
<td class="text-center"></td>
|
||||
<td class="text-center"></td>
|
||||
<td></td>
|
||||
<td class="text-right"></td>
|
||||
<td class="text-right"></td>
|
||||
<td class="text-right"></td>
|
||||
</tr>
|
||||
|
||||
@if ($line >= $linePerPage && !$loop->last)
|
||||
@php
|
||||
$line = 0;
|
||||
$pageNumber++;
|
||||
@endphp
|
||||
<tr>
|
||||
<td class="text-center"></td>
|
||||
<td class="text-center"></td>
|
||||
<td><strong>Pindah ke Halaman Berikutnya</strong></td>
|
||||
<td class="text-center"></td>
|
||||
<td class="text-right"> </td>
|
||||
<td class="text-right"> </td>
|
||||
<td class="text-right"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{!! $footerContent !!}
|
||||
</div>
|
||||
<div class="page-number">Halaman {{ $pageNumber }} dari {{ $totalPages }}</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="watermark">
|
||||
<img src="{{ public_path('assets/media/images/watermark.png') }}" alt="Watermark">
|
||||
</div>
|
||||
<div class="content-wrapper">
|
||||
<!-- Header Section for continuation page -->
|
||||
<div class="header">
|
||||
<div class="logo">
|
||||
<img src="{{ public_path('assets/media/images/logo-arthagraha.png') }}" alt="Logo Bank">
|
||||
<img src="{{ public_path('assets/media/images/logo-agi.png') }}" alt="Logo Bank">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bank Information Section for continuation page -->
|
||||
<div class="info-section">
|
||||
<div class="column">
|
||||
<p>{{ $branch->name }}</p>
|
||||
<p style="text-transform: capitalize">Kepada</p>
|
||||
<p>{{ $account->customer->name }}</p>
|
||||
<p>{{ $account->customer->address }}</p>
|
||||
<p>{{ $account->customer->district }}
|
||||
{{ ($account->customer->ktp_rt ?: $account->customer->home_rt) ? 'RT ' . ($account->customer->ktp_rt ?: $account->customer->home_rt) : '' }}
|
||||
{{ ($account->customer->ktp_rw ?: $account->customer->home_rw) ? 'RW ' . ($account->customer->ktp_rw ?: $account->customer->home_rw) : '' }}
|
||||
</p>
|
||||
<p>{{ trim($account->customer->city . ' ' . ($account->customer->province ? getProvinceCoreName($account->customer->province) . ' ' : '') . ($account->customer->postal_code ?? '')) }}
|
||||
</p>
|
||||
</div>
|
||||
<div style="text-transform: capitalize;" class="column">
|
||||
<p style="padding-left:50px"><span class="same-size">Periode Statement </span>:
|
||||
{{ dateFormat($startDate) }} <span style="text-transform:lowercase !important">s/d</span>
|
||||
{{ dateFormat($endDate) }}</p>
|
||||
<p style="padding-left:50px"><span class="same-size">Nomor Rekening</span>:
|
||||
{{ $account->account_number }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-section">
|
||||
<table>
|
||||
<thead>
|
||||
<tr
|
||||
style="@if ($headerTableBg) background-image: url('data:image/png;base64,{{ $headerTableBg }}'); background-repeat: no-repeat; background-size: cover; background-position: center; @else background-color: #0056b3; @endif height: 30px;">
|
||||
<th class="col-date">Tanggal</th>
|
||||
<th class="col-valuta">Tanggal<br>Valuta</th>
|
||||
<th class="text-left col-desc">Keterangan</th>
|
||||
<th class="col-referensi">Referensi</th>
|
||||
<th class="col-debet">Debet</th>
|
||||
<th class="col-kredit">Kredit</th>
|
||||
<th class="col-saldo">Saldo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@endif
|
||||
@endforeach
|
||||
@for ($i = 0; $i < $linePerPage - $line; $i++)
|
||||
<tr>
|
||||
<td class="text-center"></td>
|
||||
<td class="text-center"></td>
|
||||
<td></td>
|
||||
<td class="text-center"></td>
|
||||
<td class="text-right"></td>
|
||||
<td class="text-right"></td>
|
||||
<td class="text-right"></td>
|
||||
</tr>
|
||||
@endfor
|
||||
<tr>
|
||||
<td class="text-center"></td>
|
||||
<td class="text-center"></td>
|
||||
<td><strong>Total Akhir</strong></td>
|
||||
<td class="text-center"></td>
|
||||
<td class="text-right"><strong>{{ number_format($totalDebit, 2, ',', '.') }}</strong></td>
|
||||
<td class="text-right"><strong>{{ number_format($totalKredit, 2, ',', '.') }}</strong>
|
||||
</td>
|
||||
<td class="text-right"><strong>{{ number_format($saldo, 2, ',', '.') }}</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Footer Section -->
|
||||
{!! $footerContent !!}
|
||||
</div>
|
||||
<div class="page-number">Halaman {{ $pageNumber + 1 }} dari {{ $totalPages }}</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -3,7 +3,13 @@
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Webstatement\Http\Controllers\CustomerController;
|
||||
use Modules\Webstatement\Http\Controllers\EmailBlastController;
|
||||
use Modules\Webstatement\Http\Controllers\Api\AccountBalanceController;
|
||||
|
||||
Route::post('/email-blast', [EmailBlastController::class, 'sendEmailBlast']);
|
||||
Route::get('/email-blast-history', [EmailBlastController::class, 'getEmailBlastHistory']);
|
||||
Route::get('/customers/search', [CustomerController::class, 'search']);
|
||||
|
||||
// Account Balance API Routes
|
||||
Route::prefix('balance')->group(function () {
|
||||
Route::post('/', [AccountBalanceController::class, 'getBalanceSummary']);
|
||||
});
|
||||
|
||||
@@ -125,3 +125,18 @@
|
||||
$trail->parent('home');
|
||||
$trail->push('Statement Email Logs', route('email-statement-logs.index'));
|
||||
});
|
||||
|
||||
// Home > Laporan Closing Balance
|
||||
Breadcrumbs::for('laporan-closing-balance.index', function (BreadcrumbTrail $trail) {
|
||||
$trail->parent('home');
|
||||
$trail->push('Laporan Closing Balance', route('laporan-closing-balance.index'));
|
||||
});
|
||||
|
||||
// Home > Laporan Closing Balance > Detail
|
||||
Breadcrumbs::for('laporan-closing-balance.show', function (BreadcrumbTrail $trail, $closingBalance) {
|
||||
$trail->parent('laporan-closing-balance.index');
|
||||
$trail->push('Detail - ' . $closingBalance->account_number, route('laporan-closing-balance.show', [
|
||||
'accountNumber' => $closingBalance->account_number,
|
||||
'period' => $closingBalance->period
|
||||
]));
|
||||
});
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Webstatement\Http\Controllers\PeriodeStatementController;
|
||||
use Modules\Webstatement\Http\Controllers\PrintStatementController;
|
||||
use Modules\Webstatement\Http\Controllers\SyncLogsController;
|
||||
use Modules\Webstatement\Http\Controllers\JenisKartuController;
|
||||
use Modules\Webstatement\Http\Controllers\KartuAtmController;
|
||||
use Modules\Webstatement\Http\Controllers\MigrasiController;
|
||||
use Modules\Webstatement\Http\Controllers\CustomerController;
|
||||
use Modules\Webstatement\Http\Controllers\EmailBlastController;
|
||||
use Modules\Webstatement\Http\Controllers\WebstatementController;
|
||||
use Modules\Webstatement\Http\Controllers\DebugStatementController;
|
||||
use Modules\Webstatement\Http\Controllers\EmailStatementLogController;
|
||||
use Modules\Webstatement\Http\Controllers\AtmTransactionReportController;
|
||||
use Modules\Webstatement\Http\Controllers\{
|
||||
PeriodeStatementController,
|
||||
PrintStatementController,
|
||||
SyncLogsController,
|
||||
JenisKartuController,
|
||||
KartuAtmController,
|
||||
CustomerController,
|
||||
EmailBlastController,
|
||||
WebstatementController,
|
||||
DebugStatementController,
|
||||
EmailStatementLogController,
|
||||
AtmTransactionReportController,
|
||||
LaporanClosingBalanceController
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -110,15 +112,19 @@ Route::middleware(['auth'])->group(function () {
|
||||
Route::post('/{id}/resend-email', [EmailStatementLogController::class, 'resendEmail'])->name('resend-email');
|
||||
});
|
||||
Route::resource('email-statement-logs', EmailStatementLogController::class)->only(['index', 'show']);
|
||||
|
||||
// Laporan Closing Balance Routes
|
||||
Route::group(['prefix' => 'laporan-closing-balance', 'as' => 'laporan-closing-balance.', 'middleware' => ['auth']], function () {
|
||||
Route::get('/datatables', [LaporanClosingBalanceController::class, 'dataForDatatables'])->name('datatables');
|
||||
Route::get('/export', [LaporanClosingBalanceController::class, 'export'])->name('export');
|
||||
Route::get('/{accountNumber}/{period}/download', [LaporanClosingBalanceController::class, 'download'])->name('download');
|
||||
Route::get('/{accountNumber}/{period}', [LaporanClosingBalanceController::class, 'show'])->name('show');
|
||||
});
|
||||
Route::resource('laporan-closing-balance', LaporanClosingBalanceController::class)->only(['index']);
|
||||
});
|
||||
|
||||
Route::get('migrasi', [MigrasiController::class, 'index'])->name('migrasi.index');
|
||||
Route::get('biaya-kartu', [SyncLogsController::class, 'index'])->name('biaya-kartu.index');
|
||||
|
||||
Route::get('/stmt-entries/{accountNumber}', [MigrasiController::class, 'getStmtEntryByAccount']);
|
||||
Route::get('/stmt-export-csv', [WebstatementController::class, 'index'])->name('webstatement.index');
|
||||
|
||||
|
||||
Route::prefix('debug')->group(function () {
|
||||
Route::get('/test-statement',[WebstatementController::class,'printStatementRekening'])->name('webstatement.test');
|
||||
Route::post('/statement', [DebugStatementController::class, 'debugStatement'])->name('debug.statement');
|
||||
|
||||
Reference in New Issue
Block a user