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
523 lines
19 KiB
PHP
523 lines
19 KiB
PHP
<?php
|
|
|
|
namespace Modules\Webstatement\Http\Controllers;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use Carbon\Carbon;
|
|
use Exception;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Validation\Rule;
|
|
use Modules\Webstatement\Jobs\GenerateClosingBalanceReportJob;
|
|
use Modules\Webstatement\Models\ClosingBalanceReportLog;
|
|
|
|
/**
|
|
* Controller untuk mengelola laporan closing balance
|
|
* Menggunakan job processing untuk menangani laporan dengan banyak transaksi
|
|
*/
|
|
class LaporanClosingBalanceController extends Controller
|
|
{
|
|
/**
|
|
* Menampilkan halaman utama laporan closing balance
|
|
* dengan form untuk membuat permintaan laporan
|
|
*
|
|
* @return \Illuminate\View\View
|
|
*/
|
|
public function index()
|
|
{
|
|
Log::info('Mengakses halaman laporan closing balance');
|
|
return view('webstatement::laporan-closing-balance.index');
|
|
}
|
|
|
|
/**
|
|
* Membuat permintaan laporan closing balance baru
|
|
* Menggunakan job untuk memproses laporan secara asynchronous
|
|
*
|
|
* @param Request $request
|
|
* @return \Illuminate\Http\RedirectResponse
|
|
*/
|
|
public function store(Request $request)
|
|
{
|
|
Log::info('Membuat permintaan laporan closing balance', [
|
|
'user_id' => Auth::id(),
|
|
'request_data' => $request->all()
|
|
]);
|
|
|
|
try {
|
|
DB::beginTransaction();
|
|
|
|
$validated = $request->validate([
|
|
'account_number' => ['required', 'string', 'max:50'],
|
|
'report_date' => ['required', 'date_format:Y-m-d'],
|
|
]);
|
|
|
|
// Convert date to Ymd format for period
|
|
$period = Carbon::createFromFormat('Y-m-d', $validated['report_date'])->format('Ymd');
|
|
|
|
// Add user tracking data
|
|
$reportData = [
|
|
'account_number' => $validated['account_number'],
|
|
'period' => $period,
|
|
'report_date' => $validated['report_date'],
|
|
'user_id' => Auth::id(),
|
|
'created_by' => Auth::id(),
|
|
'ip_address' => $request->ip(),
|
|
'user_agent' => $request->userAgent(),
|
|
'status' => 'pending',
|
|
];
|
|
|
|
// Create the report request log
|
|
$reportRequest = ClosingBalanceReportLog::create($reportData);
|
|
|
|
// Dispatch the job to generate the report
|
|
GenerateClosingBalanceReportJob::dispatch(
|
|
$validated['account_number'],
|
|
$period,
|
|
$reportRequest->id
|
|
);
|
|
|
|
$reportRequest->update([
|
|
'status' => 'processing',
|
|
'updated_by' => Auth::id()
|
|
]);
|
|
|
|
DB::commit();
|
|
|
|
Log::info('Permintaan laporan closing balance berhasil dibuat', [
|
|
'report_id' => $reportRequest->id,
|
|
'account_number' => $validated['account_number'],
|
|
'period' => $period
|
|
]);
|
|
|
|
return redirect()->route('laporan-closing-balance.index')
|
|
->with('success', 'Permintaan laporan closing balance berhasil dibuat dan sedang diproses.');
|
|
|
|
} catch (Exception $e) {
|
|
DB::rollback();
|
|
|
|
Log::error('Error saat membuat permintaan laporan closing balance', [
|
|
'error' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString()
|
|
]);
|
|
|
|
return redirect()->back()
|
|
->withInput()
|
|
->with('error', 'Terjadi kesalahan saat membuat permintaan laporan: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Menampilkan form untuk membuat permintaan laporan baru
|
|
*
|
|
* @return \Illuminate\View\View
|
|
*/
|
|
public function create()
|
|
{
|
|
Log::info('Menampilkan form pembuatan laporan closing balance');
|
|
return view('webstatement::laporan-closing-balance.create');
|
|
}
|
|
|
|
/**
|
|
* Menampilkan detail permintaan laporan
|
|
*
|
|
* @param ClosingBalanceReportLog $closingBalanceReport
|
|
* @return \Illuminate\View\View
|
|
*/
|
|
public function show(ClosingBalanceReportLog $closingBalanceReport)
|
|
{
|
|
Log::info('Menampilkan detail laporan closing balance', [
|
|
'report_id' => $closingBalanceReport->id
|
|
]);
|
|
|
|
$closingBalanceReport->load(['user', 'creator', 'authorizer']);
|
|
return view('webstatement::laporan-closing-balance.show', compact('closingBalanceReport'));
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
public function dataForDatatables(Request $request)
|
|
{
|
|
Log::info('Mengambil data untuk datatables laporan closing balance', [
|
|
'filters' => $request->all()
|
|
]);
|
|
|
|
try {
|
|
// Retrieve data from the database
|
|
$query = ClosingBalanceReportLog::query();
|
|
|
|
// Apply search filter if provided
|
|
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%");
|
|
});
|
|
}
|
|
|
|
// Apply column filters if provided
|
|
if ($request->has('filters') && !empty($request->get('filters'))) {
|
|
$filters = json_decode($request->get('filters'), true);
|
|
|
|
foreach ($filters as $filter) {
|
|
if (!empty($filter['value'])) {
|
|
if ($filter['column'] === 'status') {
|
|
$query->where('status', $filter['value']);
|
|
} else if ($filter['column'] === 'authorization_status') {
|
|
$query->where('authorization_status', $filter['value']);
|
|
} else if ($filter['column'] === 'account_number') {
|
|
$query->where('account_number', 'LIKE', "%{$filter['value']}%");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply sorting if provided
|
|
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
|
|
$order = $request->get('sortOrder');
|
|
$column = $request->get('sortField');
|
|
|
|
// Map frontend column names to database column names if needed
|
|
$columnMap = [
|
|
'account_number' => 'account_number',
|
|
'period' => 'period',
|
|
'status' => 'status',
|
|
];
|
|
|
|
$dbColumn = $columnMap[$column] ?? $column;
|
|
$query->orderBy($dbColumn, $order);
|
|
} else {
|
|
// Default sorting
|
|
$query->latest('created_at');
|
|
}
|
|
|
|
// Get the total count of records
|
|
$totalRecords = $query->count();
|
|
|
|
// Apply pagination if provided
|
|
if ($request->has('page') && $request->has('size')) {
|
|
$page = $request->get('page');
|
|
$size = $request->get('size');
|
|
$offset = ($page - 1) * $size;
|
|
|
|
$query->skip($offset)->take($size);
|
|
}
|
|
|
|
// Get the filtered count of records
|
|
$filteredRecords = $query->count();
|
|
|
|
// Eager load relationships
|
|
$query->with(['user', 'authorizer']);
|
|
|
|
// Get the data for the current page
|
|
$data = $query->get()->map(function ($item) {
|
|
$processingHours = $item->status === 'processing' ? $item->updated_at->diffInHours(now()) : 0;
|
|
$isProcessingTimeout = $item->status === 'processing' && $processingHours >= 1;
|
|
|
|
return [
|
|
'id' => $item->id,
|
|
'account_number' => $item->account_number,
|
|
'period' => $item->period,
|
|
'report_date' => Carbon::createFromFormat('Ymd', $item->period)->format('Y-m-d'),
|
|
'status' => $item->status,
|
|
'status_display' => $item->status . ($isProcessingTimeout ? ' (Timeout)' : ''),
|
|
'processing_hours' => $processingHours,
|
|
'is_processing_timeout' => $isProcessingTimeout,
|
|
'authorization_status' => $item->authorization_status,
|
|
'is_downloaded' => $item->is_downloaded,
|
|
'created_at' => $item->created_at->format('Y-m-d H:i:s'),
|
|
'created_by' => $item->user->name ?? 'N/A',
|
|
'authorized_by' => $item->authorizer ? $item->authorizer->name : null,
|
|
'authorized_at' => $item->authorized_at ? $item->authorized_at->format('Y-m-d H:i:s') : null,
|
|
'file_path' => $item->file_path,
|
|
'record_count' => $item->record_count,
|
|
'can_retry' => in_array($item->status, ['failed', 'pending']) || $isProcessingTimeout || ($item->status === 'completed' && !$item->file_path),
|
|
];
|
|
});
|
|
|
|
// Calculate the page count
|
|
$pageCount = ceil($filteredRecords / ($request->get('size') ?: 1));
|
|
$currentPage = $request->get('page') ?: 1;
|
|
|
|
Log::info('Data laporan closing balance berhasil diambil', [
|
|
'total_records' => $totalRecords,
|
|
'filtered_records' => $filteredRecords
|
|
]);
|
|
|
|
return response()->json([
|
|
'draw' => $request->get('draw'),
|
|
'recordsTotal' => $totalRecords,
|
|
'recordsFiltered' => $filteredRecords,
|
|
'pageCount' => $pageCount,
|
|
'page' => $currentPage,
|
|
'totalCount' => $totalRecords,
|
|
'data' => $data,
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
Log::error('Error saat mengambil data datatables', [
|
|
'error' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString()
|
|
]);
|
|
|
|
return response()->json([
|
|
'error' => 'Terjadi kesalahan saat mengambil data laporan',
|
|
'message' => $e->getMessage()
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hapus permintaan laporan
|
|
*
|
|
* @param ClosingBalanceReportLog $closingBalanceReport
|
|
* @return \Illuminate\Http\JsonResponse
|
|
*/
|
|
public function destroy(ClosingBalanceReportLog $closingBalanceReport)
|
|
{
|
|
Log::info('Menghapus laporan closing balance', [
|
|
'report_id' => $closingBalanceReport->id
|
|
]);
|
|
|
|
try {
|
|
DB::beginTransaction();
|
|
|
|
// Delete the file if exists
|
|
if ($closingBalanceReport->file_path && Storage::exists($closingBalanceReport->file_path)) {
|
|
Storage::delete($closingBalanceReport->file_path);
|
|
}
|
|
|
|
// Delete the report request
|
|
$closingBalanceReport->delete();
|
|
|
|
DB::commit();
|
|
|
|
Log::info('Laporan closing balance berhasil dihapus', [
|
|
'report_id' => $closingBalanceReport->id
|
|
]);
|
|
|
|
return response()->json([
|
|
'message' => 'Laporan closing balance berhasil dihapus.',
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
DB::rollback();
|
|
|
|
Log::error('Error saat menghapus laporan', [
|
|
'report_id' => $closingBalanceReport->id,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
return response()->json([
|
|
'error' => 'Terjadi kesalahan saat menghapus laporan',
|
|
'message' => $e->getMessage()
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retry generating laporan closing balance
|
|
*
|
|
* @param ClosingBalanceReportLog $closingBalanceReport
|
|
* @return \Illuminate\Http\RedirectResponse
|
|
*/
|
|
public function retry(ClosingBalanceReportLog $closingBalanceReport)
|
|
{
|
|
Log::info('Retry laporan closing balance', [
|
|
'report_id' => $closingBalanceReport->id
|
|
]);
|
|
|
|
try {
|
|
// Check if retry is allowed
|
|
$allowedStatuses = ['failed', 'pending'];
|
|
$isProcessingTooLong = $closingBalanceReport->status === 'processing' &&
|
|
$closingBalanceReport->updated_at->diffInHours(now()) >= 1;
|
|
|
|
if (!in_array($closingBalanceReport->status, $allowedStatuses) && !$isProcessingTooLong) {
|
|
return back()->with('error', 'Laporan hanya dapat diulang jika status failed, pending, atau processing lebih dari 1 jam.');
|
|
}
|
|
|
|
DB::beginTransaction();
|
|
|
|
// If it was processing for too long, mark it as failed first
|
|
if ($isProcessingTooLong) {
|
|
$closingBalanceReport->update([
|
|
'status' => 'failed',
|
|
'error_message' => 'Processing timeout - melebihi batas waktu 1 jam',
|
|
'updated_by' => Auth::id()
|
|
]);
|
|
}
|
|
|
|
// Reset the report status and clear previous data
|
|
$closingBalanceReport->update([
|
|
'status' => 'processing',
|
|
'error_message' => null,
|
|
'file_path' => null,
|
|
'file_size' => null,
|
|
'record_count' => null,
|
|
'updated_by' => Auth::id()
|
|
]);
|
|
|
|
// Dispatch the job again
|
|
GenerateClosingBalanceReportJob::dispatch(
|
|
$closingBalanceReport->account_number,
|
|
$closingBalanceReport->period,
|
|
$closingBalanceReport->id
|
|
);
|
|
|
|
DB::commit();
|
|
|
|
Log::info('Laporan closing balance berhasil diulang', [
|
|
'report_id' => $closingBalanceReport->id
|
|
]);
|
|
|
|
return back()->with('success', 'Job laporan closing balance berhasil diulang.');
|
|
|
|
} catch (Exception $e) {
|
|
DB::rollback();
|
|
|
|
Log::error('Error saat retry laporan', [
|
|
'report_id' => $closingBalanceReport->id,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
$closingBalanceReport->update([
|
|
'status' => 'failed',
|
|
'error_message' => $e->getMessage(),
|
|
'updated_by' => Auth::id()
|
|
]);
|
|
|
|
return back()->with('error', 'Gagal mengulang generate laporan: ' . $e->getMessage());
|
|
}
|
|
}
|
|
}
|