feat(webstatement): tambah console command bulk untuk generate laporan closing balance

- Membuat GenerateClosingBalanceReportBulkCommand untuk bulk processing
- Support untuk memproses banyak rekening sekaligus berdasarkan daftar client
- Fitur client filter untuk memproses client tertentu saja
- Mode dry-run untuk preview rekening yang akan diproses
- Progress bar untuk monitoring proses bulk generation
- Interactive confirmation sebelum menjalankan job
- Error handling per rekening tanpa menghentikan proses keseluruhan
- Database transaction terpisah untuk setiap rekening
- Comprehensive logging untuk monitoring dan debugging
- Detailed summary sebelum dan sesudah pemrosesan
- Daftar client dan rekening sama dengan WebstatementController
- Integrasi dengan existing GenerateClosingBalanceReportJob
- Remarks field untuk tracking bulk generation dengan client info
- Validasi parameter lengkap dan user-friendly error messages
This commit is contained in:
Daeng Deni Mardaeni
2025-07-18 07:36:40 +07:00
parent 3c01c1728c
commit 5de1c19d09
2 changed files with 535 additions and 1 deletions

View File

@@ -0,0 +1,532 @@
<?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',
'IDR1354500010001',
'IDR1354500020001',
'IDR1354500030001',
'IDR1354500040001',
'IDR1354500050001',
'IDR1354500060001',
'IDR1354500070001',
'IDR1354500080001',
'IDR1354500090001',
'IDR1354500100001',
'IDR1720500010001',
'1078333878',
'1081647484',
'1085552121',
'1085677889',
'1086677889',
'IDR1744200010001',
'IDR1744300010001',
'IDR1744100010001',
'IDR1744400010001',
'IDR1364100010001',
'IDR1723100010001',
'IDR1354200010001'
];
/**
* 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];
// 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);
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');
}
}

View File

@@ -20,7 +20,8 @@ use Modules\Webstatement\Console\{
GenerateAtmTransactionReport,
GenerateBiayaKartuCsvCommand,
AutoSendStatementEmailCommand,
GenerateClosingBalanceReportCommand
GenerateClosingBalanceReportCommand,
GenerateClosingBalanceReportBulkCommand,
};
use Modules\Webstatement\Jobs\UpdateAtmCardBranchCurrencyJob;
@@ -79,6 +80,7 @@ class WebstatementServiceProvider extends ServiceProvider
UpdateAllAtmCardsCommand::class,
AutoSendStatementEmailCommand::class,
GenerateClosingBalanceReportCommand::class,
GenerateClosingBalanceReportBulkCommand::class,
]);
}