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
This commit is contained in:
@@ -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');
|
||||
return view('webstatement::closing-balance-reports.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('closing-balance-reports.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::closing-balance-reports.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::closing-balance-reports.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('closing-balance-reports.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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
386
app/Jobs/GenerateClosingBalanceReportJob.php
Normal file
386
app/Jobs/GenerateClosingBalanceReportJob.php
Normal file
@@ -0,0 +1,386 @@
|
||||
<?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
|
||||
]);
|
||||
|
||||
$accountBalance = AccountBalance::where('account_number', $this->accountNumber)
|
||||
->where('period', $this->period)
|
||||
->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;
|
||||
}
|
||||
}
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user