Compare commits

...

3 Commits

Author SHA1 Message Date
Daeng Deni Mardaeni
5de1c19d09 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
2025-07-18 07:36:40 +07:00
Daeng Deni Mardaeni
3c01c1728c feat(webstatement): tambah console command dan perbaikan field required untuk laporan closing balance
Menambahkan fitur command line untuk generate laporan closing balance sekaligus memperbaiki pengisian field yang required di database.

Perubahan yang dilakukan:
- Membuat command `webstatement:generate-closing-balance-report` dengan parameter:
  - `account_number`: nomor rekening (required)
  - `period`: format tanggal YYYYMMDD (required)
  - `--user_id=`: ID user (optional, default 1)
- Menambahkan field `report_date` dengan konversi dari parameter `period` menggunakan Carbon
- Menambahkan field `created_by` dan `updated_by` untuk kebutuhan audit trail
- Menambahkan field `ip_address` dan `user_agent` dengan default 'console' untuk identifikasi proses non-web
- Memperbaiki validasi parameter dengan regex dan proper escaping
- Menghindari error SQLSTATE[23502] terkait field not null di database schema
- Menggunakan database transaction untuk menjaga konsistensi data
- Mengupdate fungsi `closing_balance_report_logs` untuk menyimpan semua field yang dibutuhkan
- Integrasi dengan `GenerateClosingBalanceReportJob` untuk pemrosesan laporan secara background
- Menambahkan logging komprehensif untuk monitoring `report_date` dan proses lainnya
- Mendukung eksekusi manual dan penjadwalan via Laravel scheduler
- Kompatibel dengan proses laporan closing balance via web dan CLI

Tujuan perubahan:
- Mempermudah proses generate laporan closing balance melalui CLI secara manual atau terjadwal
- Memastikan seluruh field wajib di `closing_balance_report_logs` terisi dengan benar
- Menyediakan audit trail lengkap dan logging yang detail untuk proses via console
- Meningkatkan keandalan sistem dengan validasi dan error handling yang lebih baik
2025-07-18 07:36:02 +07:00
Daeng Deni Mardaeni
3beaf78872 feat(webstatement): implementasi job processing untuk laporan closing balance
Menambahkan fitur job processing untuk memproses laporan closing balance secara asynchronous dengan dukungan data besar.

Perubahan yang dilakukan:
- Membuat model `ClosingBalanceReportLog` untuk mencatat permintaan laporan dan status proses
- Membuat job `GenerateClosingBalanceReportJob` untuk memproses laporan closing balance di background queue
- Memodifikasi `LaporanClosingBalanceController` untuk mengintegrasikan job processing saat generate laporan
- Menambahkan migration `closing_balance_report_logs` untuk menyimpan log permintaan, path file, dan status
- Menggunakan query custom dari input user untuk pengambilan data transaksi
- Menambahkan field `closing_balance` yang dihitung otomatis (saldo awal + amount_lcy)
- Mengimplementasikan chunking data untuk memproses transaksi dalam jumlah besar secara efisien
- Menambahkan logging detail untuk memudahkan monitoring, debugging, dan audit trail
- Menggunakan database transaction untuk menjaga konsistensi data selama proses job
- Menambahkan fitur retry otomatis pada job jika terjadi kegagalan atau timeout
- Mengekspor hasil laporan ke file CSV dengan delimiter pipe `|` untuk kebutuhan integrasi sistem lain
- Menambahkan workflow approval untuk validasi laporan sebelum download
- Implementasi download tracking dan manajemen file untuk memudahkan kontrol akses

Tujuan perubahan:
- Memungkinkan pemrosesan laporan closing balance dengan jumlah data besar secara efisien dan aman
- Mengurangi beban proses synchronous pada server dengan pemanfaatan queue
- Menyediakan audit trail lengkap untuk setiap proses generate laporan
- Meningkatkan pengalaman pengguna dengan proses generate yang lebih responsif dan terkontrol
2025-07-17 19:49:22 +07:00
8 changed files with 1710 additions and 142 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

@@ -0,0 +1,211 @@
<?php
namespace Modules\Webstatement\Console;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Modules\Usermanagement\Models\User;
use Modules\Webstatement\Jobs\GenerateClosingBalanceReportJob;
use Modules\Webstatement\Models\ClosingBalanceReportLog;
use Carbon\Carbon;
/**
* Console command untuk generate laporan closing balance
* Command ini dapat dijalankan secara manual atau dijadwalkan
*/
class GenerateClosingBalanceReportCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'webstatement:generate-closing-balance-report
{account_number : Nomor rekening untuk generate laporan}
{period : Period laporan format YYYYMMDD, contoh: 20250515}
{--user_id=1 : ID user yang menjalankan command (default: 1)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate Closing Balance report untuk nomor rekening dan periode tertentu';
/**
* Execute the console command.
* Menjalankan proses generate laporan closing balance
*
* @return int
*/
public function handle()
{
$this->info('Starting Closing Balance report generation...');
// Get parameters
$accountNumber = $this->argument('account_number');
$period = $this->argument('period');
$userId = $this->option('user_id');
// Validate parameters
if (!$this->validateParameters($accountNumber, $period, $userId)) {
return Command::FAILURE;
}
try {
DB::beginTransaction();
// Log start of process
Log::info('Console command: Starting closing balance report generation', [
'account_number' => $accountNumber,
'period' => $period,
'user_id' => $userId,
'command' => 'webstatement:generate-closing-balance-report'
]);
// Create report log entry
$reportLog = $this->createReportLog($accountNumber, $period, $userId);
if (!$reportLog) {
$this->error('Failed to create report log entry');
DB::rollback();
return Command::FAILURE;
}
// Dispatch the job
GenerateClosingBalanceReportJob::dispatch($accountNumber, $period, $reportLog->id);
DB::commit();
$this->info("Closing Balance report generation job queued successfully!");
$this->info("Account Number: {$accountNumber}");
$this->info("Period: {$period}");
$this->info("Report Log ID: {$reportLog->id}");
$this->info('The report will be generated in the background.');
$this->info('Check the closing_balance_report_logs table for progress.');
// Log successful dispatch
Log::info('Console command: Closing balance report job dispatched successfully', [
'account_number' => $accountNumber,
'period' => $period,
'report_log_id' => $reportLog->id,
'user_id' => $userId
]);
return Command::SUCCESS;
} catch (Exception $e) {
DB::rollback();
$this->error('Error queuing Closing Balance report job: ' . $e->getMessage());
// Log error
Log::error('Console command: Error generating closing balance report', [
'account_number' => $accountNumber,
'period' => $period,
'user_id' => $userId,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return Command::FAILURE;
}
}
/**
* Validate command parameters
* Validasi parameter command
*
* @param string $accountNumber
* @param string $period
* @param int $userId
* @return bool
*/
private function validateParameters(string $accountNumber, string $period, int $userId): bool
{
// Validate account number
if (empty($accountNumber)) {
$this->error('Account number parameter is required.');
return false;
}
// Validate period format (YYYYMMDD)
if (!preg_match('/^\\d{8}$/', $period)) {
$this->error('Invalid period format. Use YYYYMMDD format (example: 20250515)');
return false;
}
// Validate date
$year = substr($period, 0, 4);
$month = substr($period, 4, 2);
$day = substr($period, 6, 2);
if (!checkdate($month, $day, $year)) {
$this->error('Invalid date in period parameter.');
return false;
}
// Validate user exists
$user = User::find($userId);
if (!$user) {
$this->error("User with ID {$userId} not found.");
return false;
}
return true;
}
/**
* Create report log entry
* Membuat entry log laporan
*
* @param string $accountNumber
* @param string $period
* @param int $userId
* @return ClosingBalanceReportLog|null
*/
private function createReportLog(string $accountNumber, string $period, int $userId): ?ClosingBalanceReportLog
{
try {
// Convert period string to Carbon date
$reportDate = Carbon::createFromFormat('Ymd', $period);
$reportLog = ClosingBalanceReportLog::create([
'account_number' => $accountNumber,
'period' => $period,
'report_date' => $reportDate, // Required field yang sebelumnya missing
'status' => 'pending',
'user_id' => $userId,
'created_by' => $userId, // Required field yang sebelumnya missing
'updated_by' => $userId,
'ip_address' => request()->ip() ?? '127.0.0.1', // Default untuk console
'user_agent' => 'Console Command',
'created_at' => now(),
'updated_at' => now()
]);
Log::info('Console command: Report log created', [
'report_log_id' => $reportLog->id,
'account_number' => $accountNumber,
'period' => $period,
'report_date' => $reportDate->format('Y-m-d'),
'user_id' => $userId
]);
return $reportLog;
} catch (Exception $e) {
Log::error('Console command: Error creating report log', [
'account_number' => $accountNumber,
'period' => $period,
'user_id' => $userId,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return null;
}
}
}

View File

@@ -3,36 +3,262 @@
namespace Modules\Webstatement\Http\Controllers;
use App\Http\Controllers\Controller;
use Carbon\Carbon;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
use Modules\Webstatement\Models\AccountBalance;
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
* Menyediakan form input nomor rekening dan rentang tanggal
* serta menampilkan data closing balance berdasarkan filter
* Menggunakan job processing untuk menangani laporan dengan banyak transaksi
*/
class LaporanClosingBalanceController extends Controller
{
/**
* Menampilkan halaman utama laporan closing balance
* dengan form filter nomor rekening dan rentang tanggal
* 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');
}
/**
* Mengambil data laporan closing balance berdasarkan filter
* yang dikirim melalui AJAX untuk datatables
* 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'));
}
/**
* Download laporan jika tersedia
*
* @param ClosingBalanceReportLog $closingBalanceReport
* @return \Illuminate\Http\Response
*/
public function download(ClosingBalanceReportLog $closingBalanceReport)
{
Log::info('Download laporan closing balance', [
'report_id' => $closingBalanceReport->id,
'user_id' => Auth::id()
]);
try {
// Check if report is available
if ($closingBalanceReport->status !== 'completed' || !$closingBalanceReport->file_path) {
Log::warning('Laporan tidak tersedia untuk download', [
'report_id' => $closingBalanceReport->id,
'status' => $closingBalanceReport->status
]);
return back()->with('error', 'Laporan tidak tersedia untuk download.');
}
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_{$closingBalanceReport->account_number}_{$closingBalanceReport->period}.csv";
Log::info('File laporan berhasil didownload', [
'report_id' => $closingBalanceReport->id,
'file_path' => $filePath
]);
return Storage::download($filePath, $fileName);
}
Log::error('File laporan tidak ditemukan', [
'report_id' => $closingBalanceReport->id,
'file_path' => $filePath
]);
return back()->with('error', 'File laporan tidak ditemukan.');
} catch (Exception $e) {
DB::rollback();
Log::error('Error saat download laporan', [
'report_id' => $closingBalanceReport->id,
'error' => $e->getMessage()
]);
return back()->with('error', 'Terjadi kesalahan saat download laporan.');
}
}
/**
* 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
@@ -44,63 +270,121 @@ class LaporanClosingBalanceController extends Controller
]);
try {
DB::beginTransaction();
// Retrieve data from the database
$query = ClosingBalanceReportLog::query();
$query = AccountBalance::query();
// Filter berdasarkan nomor rekening jika ada
if ($request->filled('account_number')) {
$query->where('account_number', 'like', '%' . $request->account_number . '%');
Log::info('Filter nomor rekening diterapkan', ['account_number' => $request->account_number]);
// Apply search filter if provided
if ($request->has('search') && !empty($request->get('search'))) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
$q->where('account_number', 'LIKE', "%$search%")
->orWhere('period', 'LIKE', "%$search%")
->orWhere('status', 'LIKE', "%$search%")
->orWhere('authorization_status', 'LIKE', "%$search%");
});
}
// Filter berdasarkan rentang tanggal jika ada
if ($request->filled('start_date') && $request->filled('end_date')) {
$startDate = Carbon::parse($request->start_date)->format('Ymd');
$endDate = Carbon::parse($request->end_date)->format('Ymd');
$query->whereBetween('period', [$startDate, $endDate]);
Log::info('Filter rentang tanggal diterapkan', [
'start_date' => $startDate,
'end_date' => $endDate
]);
// 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']}%");
}
}
}
}
// Sorting
$sortColumn = $request->get('sort', 'period');
$sortDirection = $request->get('direction', 'desc');
$query->orderBy($sortColumn, $sortDirection);
// Apply sorting if provided
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
$order = $request->get('sortOrder');
$column = $request->get('sortField');
// Pagination
$perPage = $request->get('per_page', 10);
$page = $request->get('page', 1);
$results = $query->paginate($perPage, ['*'], 'page', $page);
// 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;
DB::commit();
Log::info('Data laporan closing balance berhasil diambil', [
'total' => $results->total(),
'per_page' => $perPage,
'current_page' => $page
'total_records' => $totalRecords,
'filtered_records' => $filteredRecords
]);
return response()->json([
'data' => $results->items(),
'pagination' => [
'current_page' => $results->currentPage(),
'last_page' => $results->lastPage(),
'per_page' => $results->perPage(),
'total' => $results->total(),
'from' => $results->firstItem(),
'to' => $results->lastItem()
]
'draw' => $request->get('draw'),
'recordsTotal' => $totalRecords,
'recordsFiltered' => $filteredRecords,
'pageCount' => $pageCount,
'page' => $currentPage,
'totalCount' => $totalRecords,
'data' => $data,
]);
} catch (\Exception $e) {
DB::rollback();
Log::error('Error saat mengambil data laporan closing balance', [
} catch (Exception $e) {
Log::error('Error saat mengambil data datatables', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
@@ -113,120 +397,126 @@ class LaporanClosingBalanceController extends Controller
}
/**
* Export data laporan closing balance ke format Excel
* Hapus permintaan laporan
*
* @param Request $request
* @return \Illuminate\Http\Response
* @param ClosingBalanceReportLog $closingBalanceReport
* @return \Illuminate\Http\JsonResponse
*/
public function export(Request $request)
public function destroy(ClosingBalanceReportLog $closingBalanceReport)
{
Log::info('Export laporan closing balance dimulai', [
'filters' => $request->all()
Log::info('Menghapus laporan closing balance', [
'report_id' => $closingBalanceReport->id
]);
try {
DB::beginTransaction();
$query = AccountBalance::query();
// Terapkan filter yang sama seperti di datatables
if ($request->filled('account_number')) {
$query->where('account_number', 'like', '%' . $request->account_number . '%');
// Delete the file if exists
if ($closingBalanceReport->file_path && Storage::exists($closingBalanceReport->file_path)) {
Storage::delete($closingBalanceReport->file_path);
}
if ($request->filled('start_date') && $request->filled('end_date')) {
$startDate = Carbon::parse($request->start_date)->format('Ymd');
$endDate = Carbon::parse($request->end_date)->format('Ymd');
$query->whereBetween('period', [$startDate, $endDate]);
}
$data = $query->orderBy('period', 'desc')->get();
// Delete the report request
$closingBalanceReport->delete();
DB::commit();
Log::info('Export laporan closing balance berhasil', [
'total_records' => $data->count()
]);
// Generate CSV content
$csvContent = "Nomor Rekening,Periode,Saldo Aktual,Saldo Cleared,Tanggal Update\n";
foreach ($data as $item) {
$csvContent .= sprintf(
"%s,%s,%s,%s,%s\n",
$item->account_number,
$item->period,
number_format($item->actual_balance, 2),
number_format($item->cleared_balance, 2),
$item->updated_at ? $item->updated_at->format('Y-m-d H:i:s') : '-'
);
}
$filename = 'laporan_closing_balance_' . date('Y-m-d_H-i-s') . '.csv';
return response($csvContent)
->header('Content-Type', 'text/csv')
->header('Content-Disposition', 'attachment; filename="' . $filename . '"');
} catch (\Exception $e) {
DB::rollback();
Log::error('Error saat export laporan closing balance', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
Log::info('Laporan closing balance berhasil dihapus', [
'report_id' => $closingBalanceReport->id
]);
return response()->json([
'error' => 'Terjadi kesalahan saat export laporan',
'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);
}
}
/**
* Menampilkan detail laporan closing balance untuk periode tertentu
* Retry generating laporan closing balance
*
* @param string $accountNumber
* @param string $period
* @return \Illuminate\View\View
* @param ClosingBalanceReportLog $closingBalanceReport
* @return \Illuminate\Http\RedirectResponse
*/
public function show($accountNumber, $period)
public function retry(ClosingBalanceReportLog $closingBalanceReport)
{
Log::info('Menampilkan detail laporan closing balance', [
'account_number' => $accountNumber,
'period' => $period
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();
$closingBalance = AccountBalance::where('account_number', $accountNumber)
->where('period', $period)
->firstOrFail();
// 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('Detail laporan closing balance berhasil diambil', [
'account_number' => $accountNumber,
'period' => $period,
'balance' => $closingBalance->actual_balance
Log::info('Laporan closing balance berhasil diulang', [
'report_id' => $closingBalanceReport->id
]);
return view('webstatement::laporan-closing-balance.show', [
'closingBalance' => $closingBalance
]);
return back()->with('success', 'Job laporan closing balance berhasil diulang.');
} catch (\Exception $e) {
} catch (Exception $e) {
DB::rollback();
Log::error('Error saat menampilkan detail laporan closing balance', [
'account_number' => $accountNumber,
'period' => $period,
Log::error('Error saat retry laporan', [
'report_id' => $closingBalanceReport->id,
'error' => $e->getMessage()
]);
return redirect()->route('laporan-closing-balance.index')
->with('error', 'Data laporan closing balance tidak ditemukan');
$closingBalanceReport->update([
'status' => 'failed',
'error_message' => $e->getMessage(),
'updated_by' => Auth::id()
]);
return back()->with('error', 'Gagal mengulang generate laporan: ' . $e->getMessage());
}
}
}
}

View File

@@ -0,0 +1,391 @@
<?php
namespace Modules\Webstatement\Jobs;
use Carbon\Carbon;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Modules\Webstatement\Models\AccountBalance;
use Modules\Webstatement\Models\ClosingBalanceReportLog;
use Modules\Webstatement\Models\StmtEntry;
/**
* Job untuk generate laporan closing balance
* Mengambil data transaksi dan menghitung closing balance
*/
class GenerateClosingBalanceReportJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $accountNumber;
protected $period;
protected $reportLogId;
protected $chunkSize = 1000;
protected $disk = 'local';
/**
* Create a new job instance.
*
* @param string $accountNumber
* @param string $period
* @param int $reportLogId
*/
public function __construct(string $accountNumber, string $period, int $reportLogId)
{
$this->accountNumber = $accountNumber;
$this->period = $period;
$this->reportLogId = $reportLogId;
}
/**
* Execute the job.
* Memproses data transaksi dan generate laporan closing balance
*/
public function handle(): void
{
$reportLog = ClosingBalanceReportLog::find($this->reportLogId);
if (!$reportLog) {
Log::error('Closing balance report log not found', ['id' => $this->reportLogId]);
return;
}
try {
Log::info('Starting closing balance report generation', [
'account_number' => $this->accountNumber,
'period' => $this->period,
'report_log_id' => $this->reportLogId
]);
DB::beginTransaction();
// Update status to processing
$reportLog->update([
'status' => 'processing',
'updated_at' => now()
]);
// Get opening balance
$openingBalance = $this->getOpeningBalance();
// Generate report data
$reportData = $this->generateReportData($openingBalance);
// Export to CSV
$filePath = $this->exportToCsv($reportData);
// Update report log with success
$reportLog->update([
'status' => 'completed',
'file_path' => $filePath,
'file_size' => Storage::disk($this->disk)->size($filePath),
'record_count' => count($reportData),
'updated_at' => now()
]);
DB::commit();
Log::info('Closing balance report generation completed successfully', [
'account_number' => $this->accountNumber,
'period' => $this->period,
'file_path' => $filePath,
'record_count' => count($reportData)
]);
} catch (Exception $e) {
DB::rollback();
Log::error('Error generating closing balance report', [
'account_number' => $this->accountNumber,
'period' => $this->period,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
$reportLog->update([
'status' => 'failed',
'error_message' => $e->getMessage(),
'updated_at' => now()
]);
throw $e;
}
}
/**
* Get opening balance from account balance table
* Mengambil saldo awal dari tabel account balance
*/
private function getOpeningBalance(): float
{
Log::info('Getting opening balance', [
'account_number' => $this->accountNumber,
'period' => $this->period
]);
// Get previous period based on current period
$previousPeriod = $this->period === '20250512'
? Carbon::createFromFormat('Ymd', $this->period)->subDays(2)->format('Ymd')
: Carbon::createFromFormat('Ymd', $this->period)->subDay()->format('Ymd');
$accountBalance = AccountBalance::where('account_number', $this->accountNumber)
->where('period', $previousPeriod)
->first();
if (!$accountBalance) {
Log::warning('Account balance not found, using 0 as opening balance', [
'account_number' => $this->accountNumber,
'period' => $this->period
]);
return 0.0;
}
$openingBalance = (float) $accountBalance->actual_balance;
Log::info('Opening balance retrieved', [
'account_number' => $this->accountNumber,
'opening_balance' => $openingBalance
]);
return $openingBalance;
}
/**
* Generate report data based on the provided SQL query
* Menggenerate data laporan berdasarkan query yang diberikan
*/
private function generateReportData(float $openingBalance): array
{
Log::info('Generating closing balance report data', [
'account_number' => $this->accountNumber,
'period' => $this->period,
'opening_balance' => $openingBalance
]);
$reportData = [];
$runningBalance = $openingBalance;
$sequenceNo = 0;
// Query berdasarkan SQL yang diberikan user
$query = DB::table('stmt_entry as s')
->leftJoin('temp_funds_transfer as ft', 'ft._id', '=', 's.trans_reference')
->leftJoin('data_captures as dc', 'dc.id', '=', 's.trans_reference')
->select([
's.trans_reference',
's.booking_date',
's.amount_lcy',
'ft.debit_acct_no',
'ft.debit_value_date',
DB::raw('CASE WHEN s.amount_lcy::numeric < 0 THEN s.amount_lcy::numeric ELSE NULL END AS debit_amount'),
'ft.credit_acct_no',
'ft.bif_rcv_acct',
'ft.bif_rcv_name',
'ft.credit_value_date',
DB::raw('CASE WHEN s.amount_lcy::numeric > 0 THEN s.amount_lcy::numeric ELSE NULL END AS credit_amount'),
'ft.at_unique_id',
'ft.bif_ref_no',
'ft.atm_order_id',
'ft.recipt_no',
'ft.api_iss_acct',
'ft.api_benff_acct',
DB::raw('COALESCE(ft.date_time, dc.date_time, s.date_time) AS date_time'),
'ft.authoriser',
'ft.remarks',
'ft.payment_details',
'ft.ref_no',
'ft.merchant_id',
'ft.term_id'
])
->where('s.account_number', $this->accountNumber)
->where('s.booking_date', $this->period)
->orderBy('s.booking_date')
->orderBy('date_time');
// Process data in chunks
$query->chunk($this->chunkSize, function ($transactions) use (&$reportData, &$runningBalance, &$sequenceNo) {
foreach ($transactions as $transaction) {
$sequenceNo++;
// Calculate running balance
$runningBalance += (float) $transaction->amount_lcy;
// Format transaction date
$transactionDate = $this->formatDateTime($transaction->date_time);
$reportData[] = [
'sequence_no' => $sequenceNo,
'trans_reference' => $transaction->trans_reference,
'booking_date' => $transaction->booking_date,
'transaction_date' => $transactionDate,
'amount_lcy' => $transaction->amount_lcy,
'debit_acct_no' => $transaction->debit_acct_no,
'debit_value_date' => $transaction->debit_value_date,
'debit_amount' => $transaction->debit_amount,
'credit_acct_no' => $transaction->credit_acct_no,
'bif_rcv_acct' => $transaction->bif_rcv_acct,
'bif_rcv_name' => $transaction->bif_rcv_name,
'credit_value_date' => $transaction->credit_value_date,
'credit_amount' => $transaction->credit_amount,
'at_unique_id' => $transaction->at_unique_id,
'bif_ref_no' => $transaction->bif_ref_no,
'atm_order_id' => $transaction->atm_order_id,
'recipt_no' => $transaction->recipt_no,
'api_iss_acct' => $transaction->api_iss_acct,
'api_benff_acct' => $transaction->api_benff_acct,
'authoriser' => $transaction->authoriser,
'remarks' => $transaction->remarks,
'payment_details' => $transaction->payment_details,
'ref_no' => $transaction->ref_no,
'merchant_id' => $transaction->merchant_id,
'term_id' => $transaction->term_id,
'closing_balance' => $runningBalance
];
}
});
Log::info('Report data generated', [
'total_records' => count($reportData),
'final_balance' => $runningBalance
]);
return $reportData;
}
/**
* Format datetime string
* Memformat string datetime
*/
private function formatDateTime(?string $datetime): string
{
if (!$datetime) {
return '';
}
try {
return Carbon::createFromFormat('ymdHi', $datetime)->format('d/m/Y H:i');
} catch (Exception $e) {
Log::warning('Error formatting datetime', [
'datetime' => $datetime,
'error' => $e->getMessage()
]);
return $datetime;
}
}
/**
* Export report data to CSV file
* Export data laporan ke file CSV
*/
private function exportToCsv(array $reportData): string
{
Log::info('Starting CSV export for closing balance report', [
'account_number' => $this->accountNumber,
'period' => $this->period,
'record_count' => count($reportData)
]);
// Create directory structure
$basePath = "closing_balance_reports";
$accountPath = "{$basePath}/{$this->accountNumber}";
Storage::disk($this->disk)->makeDirectory($basePath);
Storage::disk($this->disk)->makeDirectory($accountPath);
// Generate filename
$fileName = "closing_balance_{$this->accountNumber}_{$this->period}.csv";
$filePath = "{$accountPath}/{$fileName}";
// Delete existing file if exists
if (Storage::disk($this->disk)->exists($filePath)) {
Storage::disk($this->disk)->delete($filePath);
}
// Create CSV header
$csvHeader = [
'NO',
'TRANS_REFERENCE',
'BOOKING_DATE',
'TRANSACTION_DATE',
'AMOUNT_LCY',
'DEBIT_ACCT_NO',
'DEBIT_VALUE_DATE',
'DEBIT_AMOUNT',
'CREDIT_ACCT_NO',
'BIF_RCV_ACCT',
'BIF_RCV_NAME',
'CREDIT_VALUE_DATE',
'CREDIT_AMOUNT',
'AT_UNIQUE_ID',
'BIF_REF_NO',
'ATM_ORDER_ID',
'RECIPT_NO',
'API_ISS_ACCT',
'API_BENFF_ACCT',
'AUTHORISER',
'REMARKS',
'PAYMENT_DETAILS',
'REF_NO',
'MERCHANT_ID',
'TERM_ID',
'CLOSING_BALANCE'
];
$csvContent = implode('|', $csvHeader) . "\n";
// Add data rows
foreach ($reportData as $row) {
$csvRow = [
$row['sequence_no'],
$row['trans_reference'] ?? '',
$row['booking_date'] ?? '',
$row['transaction_date'] ?? '',
$row['amount_lcy'] ?? '',
$row['debit_acct_no'] ?? '',
$row['debit_value_date'] ?? '',
$row['debit_amount'] ?? '',
$row['credit_acct_no'] ?? '',
$row['bif_rcv_acct'] ?? '',
$row['bif_rcv_name'] ?? '',
$row['credit_value_date'] ?? '',
$row['credit_amount'] ?? '',
$row['at_unique_id'] ?? '',
$row['bif_ref_no'] ?? '',
$row['atm_order_id'] ?? '',
$row['recipt_no'] ?? '',
$row['api_iss_acct'] ?? '',
$row['api_benff_acct'] ?? '',
$row['authoriser'] ?? '',
$row['remarks'] ?? '',
$row['payment_details'] ?? '',
$row['ref_no'] ?? '',
$row['merchant_id'] ?? '',
$row['term_id'] ?? '',
$row['closing_balance'] ?? ''
];
$csvContent .= implode('|', $csvRow) . "\n";
}
// Save file
Storage::disk($this->disk)->put($filePath, $csvContent);
// Verify file creation
if (!Storage::disk($this->disk)->exists($filePath)) {
throw new Exception("Failed to create CSV file: {$filePath}");
}
Log::info('CSV export completed successfully', [
'file_path' => $filePath,
'file_size' => Storage::disk($this->disk)->size($filePath)
]);
return $filePath;
}
}

View 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');
}
}

View File

@@ -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,
ProcessDailyMigration,
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
{
@@ -74,7 +78,9 @@ class WebstatementServiceProvider extends ServiceProvider
SendStatementEmailCommand::class,
CheckEmailProgressCommand::class,
UpdateAllAtmCardsCommand::class,
AutoSendStatementEmailCommand::class
AutoSendStatementEmailCommand::class,
GenerateClosingBalanceReportCommand::class,
GenerateClosingBalanceReportBulkCommand::class,
]);
}

View File

@@ -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');
}
};

View File

@@ -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": [