Compare commits
46 Commits
2dd8024586
...
staging
| 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 |
@@ -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;
|
||||
}
|
||||
}
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
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,127 +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 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};
|
||||
|
||||
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,
|
||||
'province' => ProcessProvinceDataJob::class
|
||||
];
|
||||
|
||||
private const PARAMETER_PROCESSES = [
|
||||
'transaction',
|
||||
'stmtNarrParam',
|
||||
'stmtNarrFormat',
|
||||
'ftTxnTypeCondition',
|
||||
'sector',
|
||||
'province'
|
||||
];
|
||||
|
||||
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"
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,11 @@ ini_set('max_execution_time', 300000);
|
||||
->get();
|
||||
|
||||
$branch = Branch::find(Auth::user()->branch_id);
|
||||
$multiBranch = session('MULTI_BRANCH') ?? false;
|
||||
|
||||
$multiBranch = false;
|
||||
if(Auth::user()->hasRole(['administrator','sentra_operasi'])){
|
||||
$multiBranch = session('MULTI_BRANCH') ?? false;
|
||||
}
|
||||
|
||||
return view('webstatement::statements.index', compact('branches', 'branch', 'multiBranch'));
|
||||
}
|
||||
@@ -168,7 +172,11 @@ ini_set('max_execution_time', 300000);
|
||||
|
||||
try {
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
$filePath = "{$statement->period_from}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf";
|
||||
|
||||
// Convert period format from YYYYMM to YYYYMMDD.YYYYMMDD for folder path
|
||||
$periodPath = formatPeriodForFolder($statement->period_from);
|
||||
|
||||
$filePath = "{$periodPath}/PRINT/{$statement->branch_code}/{$statement->account_number}.1.pdf";
|
||||
|
||||
// Log untuk debugging
|
||||
Log::info('Checking SFTP file path', [
|
||||
@@ -186,7 +194,8 @@ ini_set('max_execution_time', 300000);
|
||||
|
||||
for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) {
|
||||
$periodFormatted = $period->format('Ym');
|
||||
$periodPath = $periodFormatted . "/{$statement->branch_code}/{$statement->account_number}_{$periodFormatted}.pdf";
|
||||
$periodFolderPath = formatPeriodForFolder($periodFormatted);
|
||||
$periodPath = $periodFolderPath . "/{$statement->branch_code}/{$statement->account_number}_{$periodFormatted}.pdf";
|
||||
|
||||
if ($disk->exists($periodPath)) {
|
||||
$availablePeriods[] = $periodFormatted;
|
||||
@@ -316,7 +325,8 @@ ini_set('max_execution_time', 300000);
|
||||
|
||||
// Generate or fetch the statement file
|
||||
$disk = Storage::disk('sftpStatement');
|
||||
$filePath = "{$statement->period_from}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf";
|
||||
$periodPath = formatPeriodForFolder($statement->period_from);
|
||||
$filePath = "{$periodPath}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf";
|
||||
|
||||
if ($statement->is_period_range && $statement->period_to) {
|
||||
// Log: Memulai proses download period range
|
||||
@@ -339,7 +349,8 @@ ini_set('max_execution_time', 300000);
|
||||
|
||||
for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) {
|
||||
$periodFormatted = $period->format('Ym');
|
||||
$periodPath = $periodFormatted . "/{$statement->branch_code}/{$statement->account_number}_{$periodFormatted}.pdf";
|
||||
$periodFolderPath = formatPeriodForFolder($periodFormatted);
|
||||
$periodPath = $periodFolderPath . "/{$statement->branch_code}/{$statement->account_number}_{$periodFormatted}.pdf";
|
||||
|
||||
if ($disk->exists($periodPath)) {
|
||||
$availablePeriods[] = $periodFormatted;
|
||||
@@ -383,7 +394,8 @@ ini_set('max_execution_time', 300000);
|
||||
|
||||
// Add each available statement to the zip
|
||||
foreach ($availablePeriods as $period) {
|
||||
$periodFilePath = "{$period}/{$statement->branch_code}/{$statement->account_number}_{$period}.pdf";
|
||||
$periodFolderPath = formatPeriodForFolder($period);
|
||||
$periodFilePath = "{$periodFolderPath}/{$statement->branch_code}/{$statement->account_number}_{$period}.pdf";
|
||||
$localFilePath = storage_path("app/temp/{$statement->account_number}_{$period}.pdf");
|
||||
|
||||
try {
|
||||
@@ -512,7 +524,7 @@ ini_set('max_execution_time', 300000);
|
||||
$query = PrintStatementLog::query();
|
||||
$query->whereNotNull('user_id');
|
||||
|
||||
if (!Auth::user()->role === 'administrator') {
|
||||
if (!Auth::user()->hasRole(['administrator','sentra_operasi'])) {
|
||||
$query->where(function($q) {
|
||||
$q->where('user_id', Auth::id())
|
||||
->orWhere('branch_code', Auth::user()->branch->code);
|
||||
@@ -668,7 +680,8 @@ ini_set('max_execution_time', 300000);
|
||||
$localDisk = Storage::disk('local');
|
||||
$sftpDisk = Storage::disk('sftpStatement');
|
||||
|
||||
$filePath = "{$statement->period_from}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf";
|
||||
$periodPath = formatPeriodForFolder($statement->period_from);
|
||||
$filePath = "{$periodPath}/{$statement->branch_code}/{$statement->account_number}_{$statement->period_from}.pdf";
|
||||
|
||||
/**
|
||||
* Fungsi helper untuk mendapatkan file dari disk dengan prioritas local
|
||||
@@ -718,7 +731,8 @@ ini_set('max_execution_time', 300000);
|
||||
|
||||
for ($period = clone $periodFrom; $period->lte($periodTo); $period->addMonth()) {
|
||||
$periodFormatted = $period->format('Ym');
|
||||
$periodPath = "{$periodFormatted}/{$statement->branch_code}/{$statement->account_number}_{$periodFormatted}.pdf";
|
||||
$periodFolderPath = formatPeriodForFolder($periodFormatted);
|
||||
$periodPath = "{$periodFolderPath}/{$statement->branch_code}/{$statement->account_number}_{$periodFormatted}.pdf";
|
||||
|
||||
$fileInfo = $getFileFromDisk($periodPath);
|
||||
|
||||
@@ -906,6 +920,7 @@ ini_set('max_execution_time', 300000);
|
||||
|
||||
$norek = $statement->account_number;
|
||||
$period = $statement->period_from;
|
||||
$endPeriod = $statement->period_to ?? $period;
|
||||
$format='pdf';
|
||||
|
||||
// Generate nama file PDF
|
||||
@@ -977,20 +992,21 @@ ini_set('max_execution_time', 300000);
|
||||
Log::info('Statement data prepared successfully', [
|
||||
'account_number' => $norek,
|
||||
'period' => $period,
|
||||
'endPeriod' => $endPeriod ?? $period,
|
||||
'saldo_period' => $saldoPeriod,
|
||||
'saldo_awal' => $saldoAwalBulan->actual_balance ?? 0,
|
||||
'entries_count' => $stmtEntries->count()
|
||||
]);
|
||||
|
||||
$periodDates = calculatePeriodDates($period);
|
||||
$periodDates = formatPeriodForFolder($period);
|
||||
|
||||
// Jika format adalah PDF, generate PDF
|
||||
if ($format === 'pdf') {
|
||||
return $this->generateStatementPdf($norek, $period, $stmtEntries, $account, $customer, $headerTableBg, $branch, $saldoAwalBulan, $statement->id, $tempPath, $filename);
|
||||
return $this->generateStatementPdf($norek, $period, $endPeriod, $stmtEntries, $account, $customer, $headerTableBg, $branch, $saldoAwalBulan, $statement->id, $tempPath, $filename);
|
||||
}
|
||||
|
||||
// Default return HTML view
|
||||
return view('webstatement::statements.stmt', compact('stmtEntries', 'account', 'customer', 'headerTableBg', 'branch', 'period', 'saldoAwalBulan'));
|
||||
return view('webstatement::statements.stmt', compact('stmtEntries', 'account', 'customer', 'headerTableBg', 'branch', 'period', 'saldoAwalBulan', 'endPeriod'));
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollBack();
|
||||
@@ -1028,7 +1044,7 @@ ini_set('max_execution_time', 300000);
|
||||
* @param object $saldoAwalBulan Data saldo awal
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
protected function generateStatementPdf($norek, $period, $stmtEntries, $account, $customer, $headerTableBg, $branch, $saldoAwalBulan, $statementId, $tempPath, $filename)
|
||||
protected function generateStatementPdf($norek, $period, $endPeriod, $stmtEntries, $account, $customer, $headerTableBg, $branch, $saldoAwalBulan, $statementId, $tempPath, $filename)
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
@@ -1036,6 +1052,7 @@ ini_set('max_execution_time', 300000);
|
||||
Log::info('Starting PDF generation with storage', [
|
||||
'account_number' => $norek,
|
||||
'period' => $period,
|
||||
'endPeriod' => $endPeriod ?? $period,
|
||||
'user_id' => Auth::id()
|
||||
]);
|
||||
|
||||
@@ -1047,10 +1064,12 @@ ini_set('max_execution_time', 300000);
|
||||
'headerTableBg',
|
||||
'branch',
|
||||
'period',
|
||||
'endPeriod',
|
||||
'saldoAwalBulan'
|
||||
))->render();
|
||||
// Tentukan path storage
|
||||
$storagePath = "statements/{$period}/{$norek}";
|
||||
// Tentukan path storage dengan format folder baru
|
||||
$periodPath = formatPeriodForFolder($period);
|
||||
$storagePath = "statements/{$periodPath}/{$norek}";
|
||||
$fullStoragePath = "{$storagePath}/{$filename}";
|
||||
|
||||
// Generate PDF menggunakan Browsershot dan simpan langsung ke storage
|
||||
@@ -1226,7 +1245,8 @@ ini_set('max_execution_time', 300000);
|
||||
|
||||
$account = Account::where('account_number',$norek)->first();
|
||||
|
||||
$storagePath = "statements/{$period}/{$account->branch_code}/{$filename}";
|
||||
$periodPath = formatPeriodForFolder($period);
|
||||
$storagePath = "statements/{$periodPath}/{$account->branch_code}/{$filename}";
|
||||
|
||||
// Cek apakah file ada di storage
|
||||
if (!Storage::disk('local')->exists($storagePath)) {
|
||||
@@ -1285,7 +1305,8 @@ ini_set('max_execution_time', 300000);
|
||||
$filename = $this->generatePdfFileName($norek, $period);
|
||||
}
|
||||
|
||||
$storagePath = "statements/{$period}/{$norek}/{$filename}";
|
||||
$periodPath = formatPeriodForFolder($period);
|
||||
$storagePath = "statements/{$periodPath}/{$norek}/{$filename}";
|
||||
|
||||
if (Storage::disk('local')->exists($storagePath)) {
|
||||
$deleted = Storage::disk('local')->delete($storagePath);
|
||||
@@ -1519,6 +1540,8 @@ ini_set('max_execution_time', 300000);
|
||||
|
||||
$accountNumber = $statement->account_number;
|
||||
$period = $statement->period_from ?? date('Ym');
|
||||
$endPeriod = $statement->period_to ?? $period;
|
||||
|
||||
$balance = AccountBalance::where('account_number', $accountNumber)
|
||||
->when($period === '202505', function($query) {
|
||||
return $query->where('period', '>=', '20250512')
|
||||
@@ -1542,7 +1565,7 @@ ini_set('max_execution_time', 300000);
|
||||
}
|
||||
|
||||
// Dispatch the job
|
||||
$job = ExportStatementPeriodJob::dispatch($statement->id, $accountNumber, $period, $balance, $clientName);
|
||||
$job = ExportStatementPeriodJob::dispatch($statement->id, $accountNumber, $period, $endPeriod, $balance, $clientName);
|
||||
|
||||
Log::info("Statement export job dispatched successfully", [
|
||||
'job_id' => $job->job_id ?? null,
|
||||
@@ -1602,8 +1625,9 @@ ini_set('max_execution_time', 300000);
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Find ZIP file
|
||||
$zipFiles = Storage::disk('local')->files("statements/{$statement->period_from}/multi_account/{$statementId}");
|
||||
// Find ZIP file dengan format folder baru
|
||||
$periodPath = formatPeriodForFolder($statement->period_from);
|
||||
$zipFiles = Storage::disk('local')->files("statements/{$periodPath}/multi_account/{$statementId}");
|
||||
|
||||
$zipFile = null;
|
||||
foreach ($zipFiles as $file) {
|
||||
|
||||
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()
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ class PrintStatementRequest extends FormRequest
|
||||
// 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('authorization_status', '!=', 'rejected')
|
||||
->where(function($query) {
|
||||
$query->where('is_available', true)
|
||||
->orWhere('is_generated', true);
|
||||
@@ -84,7 +84,7 @@ class PrintStatementRequest extends FormRequest
|
||||
}
|
||||
|
||||
if ($query->exists()) {
|
||||
$fail('A statement request with this account number and period already exists.');
|
||||
//$fail('A statement request with this account number and period already exists.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, ',', '.'),
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,7 +156,7 @@
|
||||
'description' => $this->generateNarrative($item),
|
||||
'end_balance' => $runningBalance,
|
||||
'actual_date' => $actualDate,
|
||||
'recipt_no' => $item->ft?->recipt_no ?? '-',
|
||||
'recipt_no' => $item->ft?->recipt_no ?? '-',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
@@ -357,46 +357,20 @@
|
||||
/**
|
||||
* 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";
|
||||
|
||||
$accountPath = "{$basePath}/{$this->account_number}";
|
||||
|
||||
// Create client directory if it doesn't exist
|
||||
if (!empty($this->client)) {
|
||||
// Di fungsi exportToCsv untuk basePath
|
||||
Storage::disk($this->disk)->makeDirectory($basePath);
|
||||
// Tambahkan permission dan ownership setelah membuat directory
|
||||
if ($this->disk === 'local') {
|
||||
$fullPath = storage_path("app/{$basePath}");
|
||||
if (is_dir($fullPath)) {
|
||||
chmod($fullPath, 0777);
|
||||
if (function_exists('chown') && posix_getuid() === 0) {
|
||||
@chown(dirname($fullPath), 'www-data'); // Gunakan www-data instead of root
|
||||
@chgrp(dirname($fullPath), 'www-data');
|
||||
}
|
||||
}
|
||||
}
|
||||
// PERBAIKAN: Selalu pastikan direktori dibuat
|
||||
Storage::disk($this->disk)->makeDirectory($basePath);
|
||||
Storage::disk($this->disk)->makeDirectory($accountPath);
|
||||
|
||||
|
||||
// Untuk accountPath
|
||||
Storage::disk($this->disk)->makeDirectory($accountPath);
|
||||
// Tambahkan permission dan ownership setelah membuat directory
|
||||
if ($this->disk === 'local') {
|
||||
$fullPath = storage_path("app/{$accountPath}");
|
||||
if (is_dir($fullPath)) {
|
||||
chmod($fullPath, 0777);
|
||||
if (function_exists('chown') && posix_getuid() === 0) {
|
||||
@chown(dirname($fullPath), 'www-data'); // Gunakan www-data instead of root
|
||||
@chgrp(dirname($fullPath), 'www-data');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$filePath = "{$accountPath}/{$this->fileName}";
|
||||
|
||||
@@ -405,13 +379,38 @@
|
||||
Storage::disk($this->disk)->delete($filePath);
|
||||
}
|
||||
|
||||
$csvContent = "NO|TRANSACTION.DATE|REFERENCE.NUMBER|TRANSACTION.AMOUNT|TRANSACTION.TYPE|DESCRIPTION|END.BALANCE|ACTUAL.DATE|NO.RECEIPT\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,
|
||||
@@ -426,12 +425,31 @@
|
||||
]) . "\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}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,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;
|
||||
@@ -57,11 +58,12 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
* @param string $client
|
||||
* @param string $disk
|
||||
*/
|
||||
public function __construct(int $statementId, string $account_number, string $period, string $saldo, string $client = '', string $disk = 'local', bool $toCsv = true)
|
||||
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;
|
||||
@@ -69,13 +71,13 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
$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);
|
||||
@@ -90,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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,25 +210,31 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
$processedData = [];
|
||||
|
||||
foreach ($entries as $item) {
|
||||
$globalSequence++;
|
||||
$runningBalance += (float) $item->amount_lcy;
|
||||
$globalSequence++;
|
||||
|
||||
$actualDate = $this->formatActualDate($item);
|
||||
$actualDate = $this->formatActualDate($item);
|
||||
|
||||
$processedData[] = [
|
||||
'account_number' => $this->account_number,
|
||||
'period' => $this->period,
|
||||
'sequence_no' => $globalSequence,
|
||||
'transaction_date' => $item->booking_date,
|
||||
'reference_number' => $item->trans_reference,
|
||||
'transaction_amount' => $item->amount_lcy,
|
||||
'transaction_type' => $item->amount_lcy < 0 ? 'D' : 'C',
|
||||
'description' => $this->generateNarrative($item),
|
||||
'end_balance' => $runningBalance,
|
||||
'actual_date' => $actualDate,
|
||||
'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;
|
||||
@@ -472,37 +487,22 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
// Generate filename
|
||||
$filename = "{$this->account_number}_{$this->period}.pdf";
|
||||
|
||||
// Tentukan path storage
|
||||
$storagePath = "statements/{$this->period}/{$account->branch_code}";
|
||||
// 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);
|
||||
// Tambahkan pengecekan function dan error handling
|
||||
if (function_exists('chown') && posix_getuid() === 0) {
|
||||
@chown(dirname($tempPath), 'www-data'); // Gunakan www-data instead of root
|
||||
@chgrp(dirname($tempPath), 'www-data');
|
||||
}
|
||||
}
|
||||
|
||||
// Pastikan direktori storage ada
|
||||
Storage::makeDirectory($storagePath);
|
||||
// Tambahkan permission dan ownership setelah membuat directory
|
||||
if ($this->disk === 'local') {
|
||||
$fullPath = storage_path("app/{$storagePath}");
|
||||
if (is_dir($fullPath)) {
|
||||
chmod($fullPath, 0777);
|
||||
// Tambahkan pengecekan function dan error handling
|
||||
if (function_exists('chown') && posix_getuid() === 0) {
|
||||
@chown($fullPath, 'www-data'); // Gunakan www-data instead of root
|
||||
@chgrp($fullPath, 'www-data');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$period = $this->period;
|
||||
$endPeriod = $this->endPeriod;
|
||||
|
||||
// Render HTML view
|
||||
$html = view('webstatement::statements.stmt', compact(
|
||||
@@ -512,6 +512,7 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
'headerTableBg',
|
||||
'branch',
|
||||
'period',
|
||||
'endPeriod',
|
||||
'saldoAwalBulan'
|
||||
))->render();
|
||||
|
||||
@@ -627,19 +628,10 @@ class ExportStatementPeriodJob implements ShouldQueue
|
||||
// Determine the base path based on client
|
||||
$account = Account::where('account_number', $this->account_number)->first();
|
||||
|
||||
$storagePath = "statements/{$this->period}/{$account->branch_code}";
|
||||
$periodPath = formatPeriodForFolder($this->period);
|
||||
$storagePath = "statements/{$periodPath}/{$account->branch_code}";
|
||||
Storage::disk($this->disk)->makeDirectory($storagePath);
|
||||
// Tambahkan permission dan ownership setelah membuat directory
|
||||
if ($this->disk === 'local') {
|
||||
$fullPath = storage_path("app/{$storagePath}");
|
||||
if (is_dir($fullPath)) {
|
||||
chmod($fullPath, 0777);
|
||||
if (function_exists('chown') && posix_getuid() === 0) {
|
||||
@chown(dirname($fullPath), 'www-data'); // Gunakan www-data instead of root
|
||||
@chgrp(dirname($fullPath), 'www-data');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$filePath = "{$storagePath}/{$this->fileName}";
|
||||
|
||||
// Delete existing file if it exists
|
||||
@@ -669,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}");
|
||||
|
||||
@@ -184,18 +184,16 @@
|
||||
->whereNotNull('currency')
|
||||
->where('currency', '!=', '')
|
||||
->whereIn('ctdesc', $cardTypes)
|
||||
->whereNotIn('product_code',['6002','6004','6042','6031']) // Hapus 6021 dari sini
|
||||
->whereNotIn('product_code',['6031','6021','6042']) // Hapus 6021 dari sini
|
||||
->where('branch','!=','ID0019999')
|
||||
// Filter khusus: Kecualikan product_code 6021 yang ctdesc nya gold
|
||||
->where(function($subQuery) {
|
||||
$subQuery->where('product_code', '!=', '6021')
|
||||
->orWhere(function($nestedQuery) {
|
||||
$nestedQuery->where('product_code', '6021')
|
||||
->where('ctdesc', '!=', 'gold');
|
||||
});
|
||||
->where(function($query) {
|
||||
$query->whereNot(function($q) {
|
||||
$q->where('product_code', '6004')
|
||||
->where('ctdesc', 'CLASSIC');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
$cards = $query->get();
|
||||
|
||||
@@ -203,8 +201,8 @@
|
||||
Log::info('Eligible ATM cards fetched successfully', [
|
||||
'total_cards' => $cards->count(),
|
||||
'periode' => $this->periode,
|
||||
'excluded_product_codes' => ['6002','6004','6042','6031'],
|
||||
'special_filter' => 'product_code 6021 dengan ctdesc gold dikecualikan'
|
||||
'excluded_product_codes' => ['6021','6042','6031'],
|
||||
'special_filter' => 'product_code 6004 dengan ctdesc classic dikecualikan'
|
||||
]);
|
||||
|
||||
return $cards;
|
||||
@@ -251,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 [
|
||||
'',
|
||||
@@ -272,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)
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -239,16 +239,6 @@ class GenerateMultiAccountPdfJob implements ShouldQueue
|
||||
|
||||
// Ensure directory exists
|
||||
Storage::disk('local')->makeDirectory($storagePath);
|
||||
// Tambahkan permission dan ownership setelah membuat directory
|
||||
$fullPath = storage_path("app/{$storagePath}");
|
||||
if (is_dir($fullPath)) {
|
||||
chmod($fullPath, 0777);
|
||||
// Tambahkan pengecekan function dan error handling untuk chown
|
||||
if (function_exists('chown') && posix_getuid() === 0) {
|
||||
@chown($fullPath, 'www-data'); // Gunakan www-data instead of root
|
||||
@chgrp($fullPath, 'www-data');
|
||||
}
|
||||
}
|
||||
|
||||
// Generate PDF path
|
||||
$pdfPath = storage_path("app/{$fullStoragePath}");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -20,7 +20,7 @@ class ProcessProvinceDataJob implements ShouldQueue
|
||||
private const CSV_DELIMITER = '~';
|
||||
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
||||
private const FILENAME = 'ST.PROVINCE.csv';
|
||||
private const DISK_NAME = 'sftpStatement';
|
||||
private const DISK_NAME = 'staging';
|
||||
|
||||
private string $period = '';
|
||||
private int $processedCount = 0;
|
||||
@@ -29,7 +29,7 @@ class ProcessProvinceDataJob implements ShouldQueue
|
||||
|
||||
/**
|
||||
* Membuat instance job baru untuk memproses data provinsi
|
||||
*
|
||||
*
|
||||
* @param string $period Periode data yang akan diproses
|
||||
*/
|
||||
public function __construct(string $period = '')
|
||||
@@ -41,17 +41,17 @@ class ProcessProvinceDataJob implements ShouldQueue
|
||||
/**
|
||||
* 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 === '') {
|
||||
@@ -62,10 +62,10 @@ class ProcessProvinceDataJob implements ShouldQueue
|
||||
|
||||
$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());
|
||||
@@ -76,7 +76,7 @@ class ProcessProvinceDataJob implements ShouldQueue
|
||||
/**
|
||||
* Inisialisasi pengaturan job
|
||||
* Mengatur timeout dan reset counter
|
||||
*
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function initializeJob(): void
|
||||
@@ -85,14 +85,14 @@ class ProcessProvinceDataJob implements ShouldQueue
|
||||
$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
|
||||
@@ -101,7 +101,7 @@ class ProcessProvinceDataJob implements ShouldQueue
|
||||
$filePath = "$this->period/" . self::FILENAME;
|
||||
|
||||
Log::info('ProcessProvinceDataJob: Memproses periode ' . $this->period);
|
||||
|
||||
|
||||
if (!$this->validateFile($disk, $filePath)) {
|
||||
return;
|
||||
}
|
||||
@@ -113,7 +113,7 @@ class ProcessProvinceDataJob implements ShouldQueue
|
||||
|
||||
/**
|
||||
* Validasi keberadaan file di storage
|
||||
*
|
||||
*
|
||||
* @param mixed $disk Storage disk instance
|
||||
* @param string $filePath Path file yang akan divalidasi
|
||||
* @return bool
|
||||
@@ -133,7 +133,7 @@ class ProcessProvinceDataJob implements ShouldQueue
|
||||
|
||||
/**
|
||||
* Membuat file temporary untuk pemrosesan
|
||||
*
|
||||
*
|
||||
* @param mixed $disk Storage disk instance
|
||||
* @param string $filePath Path file sumber
|
||||
* @return string Path file temporary
|
||||
@@ -142,7 +142,7 @@ class ProcessProvinceDataJob implements ShouldQueue
|
||||
{
|
||||
$tempFilePath = storage_path("app/temp_" . self::FILENAME);
|
||||
file_put_contents($tempFilePath, $disk->get($filePath));
|
||||
|
||||
|
||||
Log::info("ProcessProvinceDataJob: File temporary dibuat: $tempFilePath");
|
||||
return $tempFilePath;
|
||||
}
|
||||
@@ -150,7 +150,7 @@ class ProcessProvinceDataJob implements ShouldQueue
|
||||
/**
|
||||
* 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
|
||||
@@ -164,20 +164,20 @@ class ProcessProvinceDataJob implements ShouldQueue
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ class ProcessProvinceDataJob implements ShouldQueue
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -207,16 +207,16 @@ class ProcessProvinceDataJob implements ShouldQueue
|
||||
'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
|
||||
@@ -237,10 +237,10 @@ class ProcessProvinceDataJob implements ShouldQueue
|
||||
['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());
|
||||
@@ -250,7 +250,7 @@ class ProcessProvinceDataJob implements ShouldQueue
|
||||
|
||||
/**
|
||||
* Membersihkan file temporary
|
||||
*
|
||||
*
|
||||
* @param string $tempFilePath Path file temporary yang akan dihapus
|
||||
* @return void
|
||||
*/
|
||||
@@ -264,7 +264,7 @@ class ProcessProvinceDataJob implements ShouldQueue
|
||||
|
||||
/**
|
||||
* Logging hasil akhir pemrosesan job
|
||||
*
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function logJobCompletion(): void
|
||||
@@ -273,14 +273,14 @@ class ProcessProvinceDataJob implements ShouldQueue
|
||||
"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");
|
||||
}
|
||||
@@ -288,7 +288,7 @@ class ProcessProvinceDataJob implements ShouldQueue
|
||||
|
||||
/**
|
||||
* Handle job failure
|
||||
*
|
||||
*
|
||||
* @param Exception $exception
|
||||
* @return void
|
||||
*/
|
||||
@@ -297,4 +297,4 @@ class ProcessProvinceDataJob implements ShouldQueue
|
||||
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
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -29,8 +29,33 @@ class Customer extends Model
|
||||
'home_rw',
|
||||
'ktp_rt',
|
||||
'ktp_rw',
|
||||
'local_ref'
|
||||
'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');
|
||||
}
|
||||
|
||||
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'
|
||||
];
|
||||
}
|
||||
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,20 +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\AutoSendStatementEmailCommand;
|
||||
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
|
||||
{
|
||||
@@ -53,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);
|
||||
}
|
||||
|
||||
@@ -64,7 +69,7 @@ class WebstatementServiceProvider extends ServiceProvider
|
||||
$this->commands([
|
||||
GenerateBiayakartuCommand::class,
|
||||
GenerateBiayaKartuCsvCommand::class,
|
||||
ProcessDailyMigration::class,
|
||||
ProcessDailyStaging::class,
|
||||
ExportDailyStatements::class,
|
||||
CombinePdf::class,
|
||||
ConvertHtmlToPdf::class,
|
||||
@@ -74,7 +79,9 @@ class WebstatementServiceProvider extends ServiceProvider
|
||||
SendStatementEmailCommand::class,
|
||||
CheckEmailProgressCommand::class,
|
||||
UpdateAllAtmCardsCommand::class,
|
||||
AutoSendStatementEmailCommand::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();
|
||||
}
|
||||
}
|
||||
@@ -5,4 +5,17 @@ return [
|
||||
|
||||
// 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,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'
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
10
module.json
10
module.json
@@ -77,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>
|
||||
@@ -71,10 +71,6 @@
|
||||
{{ in_array('BY.MAIL.TO.KTP.ADDR', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}>
|
||||
BY MAIL TO KTP ADDR
|
||||
</option>
|
||||
<option value="NO.PRINT"
|
||||
{{ in_array('NO.PRINT', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}>
|
||||
NO PRINT
|
||||
</option>
|
||||
<option value="PRINT"
|
||||
{{ in_array('PRINT', old('stmt_sent_type', $statement->stmt_sent_type ?? [])) ? 'selected' : '' }}>
|
||||
PRINT
|
||||
@@ -135,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"
|
||||
@@ -144,6 +141,7 @@
|
||||
<em class="text-sm alert text-danger">{{ $message }}</em>
|
||||
@enderror
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-5 text-end">
|
||||
|
||||
@@ -130,6 +130,11 @@
|
||||
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 {
|
||||
@@ -278,13 +283,17 @@
|
||||
$totalDebit = 0;
|
||||
$totalKredit = 0;
|
||||
$line = 1;
|
||||
$linePerPage = 26;
|
||||
$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 = $periodDates['end'];
|
||||
$endDate = $endPeriodDate['end'] ?? $periodDates['end'];
|
||||
|
||||
// Log hasil perhitungan
|
||||
\Log::info('Period dates calculated', [
|
||||
@@ -361,13 +370,22 @@
|
||||
<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>{{ $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">
|
||||
@@ -403,7 +421,7 @@
|
||||
<td class="text-right"> </td>
|
||||
<td class="text-right"> </td>
|
||||
<td class="text-right">
|
||||
<strong>{{ number_format((float)$saldoAwalBulan->actual_balance, 2, ',', '.') }}</strong>
|
||||
<strong>{{ number_format((float) $saldoAwalBulan->actual_balance, 2, ',', '.') }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -438,10 +456,12 @@
|
||||
<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 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>
|
||||
<td class="text-right">{{ number_format((float) $saldo, 2, ',', '.') }}</td>
|
||||
</tr>
|
||||
@for ($i = 1; $i < count($narrativeLines); $i++)
|
||||
<tr class="narrative-line">
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -91,7 +93,7 @@ Route::middleware(['auth'])->group(function () {
|
||||
});
|
||||
|
||||
Route::resource('statements', PrintStatementController::class);
|
||||
|
||||
|
||||
|
||||
// ATM Transaction Report Routes
|
||||
Route::group(['prefix' => 'atm-reports', 'as' => 'atm-reports.', 'middleware' => ['auth']], function () {
|
||||
@@ -110,6 +112,15 @@ 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('/stmt-export-csv', [WebstatementController::class, 'index'])->name('webstatement.index');
|
||||
|
||||
Reference in New Issue
Block a user