Files
webstatement/app/Http/Controllers/LaporanClosingBalanceController.php
Daeng Deni Mardaeni ad9780ccd6 feat(webstatement): tambah stmt_entry_detail migrasi, model, dan job processing
Menambahkan fitur pengelolaan data stmt_entry_detail untuk integrasi transaksi dengan detail yang lebih lengkap.

Perubahan yang dilakukan:

- Membuat migrasi create_stmt_entry_detail_table dengan struktur field sesuai kebutuhan bisnis
- Menambahkan index pada kolom penting untuk meningkatkan performa query

- Membuat model StmtEntryDetail dengan relasi ke:
  - Account
  - TempFundsTransfer
  - TempTransaction
  - Teller
  - DataCapture
  - TempArrangement
- Mengimplementasikan $fillable dan $casts sesuai struktur tabel
- Menambahkan relasi untuk memudahkan integrasi antar modul

- Membuat job ProcessStmtEntryDetailDataJob untuk memproses file CSV dengan batch processing
- Mengimplementasikan chunking untuk menangani file besar secara efisien
- Membersihkan trans_reference dari karakter tidak valid sebelum penyimpanan
- Menggunakan updateOrCreate untuk mencegah duplikasi primary key
- Menggunakan database transaction untuk menjaga konsistensi data
- Menambahkan logging komprehensif untuk monitoring dan debugging
- Mengimplementasikan error handling yang robust untuk menghindari job failure tanpa informasi
- Memastikan penggunaan resource memory tetap optimal saat memproses data besar

- Menambahkan case baru di MigrasiController untuk memproses stmt_entry_detail
- Konsisten dengan pattern migrasi data yang sudah ada di sistem

Tujuan perubahan:

- Menyediakan sistem import dan pengolahan data stmt_entry_detail dengan proses yang aman dan efisien
- Memudahkan integrasi transaksi dengan detail tambahan di modul Webstatement
- Menjamin integritas data dengan penggunaan transaction, logging, dan error handling yang komprehensif
2025-07-21 11:21:42 +07:00

572 lines
21 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'));
}
/**
* Authorize permintaan laporan
*
* @param Request $request
* @param ClosingBalanceReportLog $closingBalanceReport
* @return \Illuminate\Http\RedirectResponse
*/
public function authorize(Request $request, ClosingBalanceReportLog $closingBalanceReport)
{
Log::info('Authorize laporan closing balance', [
'report_id' => $closingBalanceReport->id,
'user_id' => Auth::id()
]);
try {
DB::beginTransaction();
$request->validate([
'authorization_status' => ['required', Rule::in(['approved', 'rejected'])],
'remarks' => ['nullable', 'string', 'max:255'],
]);
// Update authorization status
$closingBalanceReport->update([
'authorization_status' => $request->authorization_status,
'authorized_by' => Auth::id(),
'authorized_at' => now(),
'remarks' => $request->remarks,
'updated_by' => Auth::id()
]);
DB::commit();
$statusText = $request->authorization_status === 'approved' ? 'disetujui' : 'ditolak';
Log::info('Laporan closing balance berhasil diauthorize', [
'report_id' => $closingBalanceReport->id,
'status' => $request->authorization_status
]);
return redirect()->route('laporan-closing-balance.show', $closingBalanceReport->id)
->with('success', "Permintaan laporan closing balance berhasil {$statusText}.");
} catch (Exception $e) {
DB::rollback();
Log::error('Error saat authorize laporan', [
'report_id' => $closingBalanceReport->id,
'error' => $e->getMessage()
]);
return back()->with('error', 'Terjadi kesalahan saat authorize laporan.');
}
}
/**
* Menyediakan data untuk datatables
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function dataForDatatables(Request $request)
{
Log::info('Mengambil data untuk datatables laporan closing balance', [
'filters' => $request->all()
]);
try {
// Retrieve data from the database
$query = ClosingBalanceReportLog::query();
// Apply search filter if provided (handle JSON search parameters)
if ($request->has('search') && !empty($request->get('search'))) {
$search = $request->get('search');
// Check if search is JSON format
if (is_string($search) && json_decode($search, true) !== null) {
$searchParams = json_decode($search, true);
// Apply account number filter
if (!empty($searchParams['account_number'])) {
$query->where('account_number', 'LIKE', "%{$searchParams['account_number']}%");
}
// Apply date range filter
if (!empty($searchParams['start_date'])) {
$startPeriod = Carbon::createFromFormat('Y-m-d', $searchParams['start_date'])->format('Ymd');
$query->where('period', '>=', $startPeriod);
}
if (!empty($searchParams['end_date'])) {
$endPeriod = Carbon::createFromFormat('Y-m-d', $searchParams['end_date'])->format('Ymd');
$query->where('period', '<=', $endPeriod);
}
} else {
// Handle regular string search (fallback)
$query->where(function ($q) use ($search) {
$q->where('account_number', 'LIKE', "%$search%")
->orWhere('period', 'LIKE', "%$search%")
->orWhere('status', 'LIKE', "%$search%")
->orWhere('authorization_status', 'LIKE', "%$search%");
});
}
}
// Apply individual parameter filters (for backward compatibility)
if ($request->has('account_number') && !empty($request->get('account_number'))) {
$query->where('account_number', 'LIKE', "%{$request->get('account_number')}%");
}
if ($request->has('start_date') && !empty($request->get('start_date'))) {
$startPeriod = Carbon::createFromFormat('Y-m-d', $request->get('start_date'))->format('Ymd');
$query->where('period', '>=', $startPeriod);
}
if ($request->has('end_date') && !empty($request->get('end_date'))) {
$endPeriod = Carbon::createFromFormat('Y-m-d', $request->get('end_date'))->format('Ymd');
$query->where('period', '<=', $endPeriod);
}
// Apply column filters if provided
if ($request->has('filters') && !empty($request->get('filters'))) {
$filters = json_decode($request->get('filters'), true);
foreach ($filters as $filter) {
if (!empty($filter['value'])) {
if ($filter['column'] === 'status') {
$query->where('status', $filter['value']);
} else if ($filter['column'] === 'authorization_status') {
$query->where('authorization_status', $filter['value']);
} else if ($filter['column'] === 'account_number') {
$query->where('account_number', 'LIKE', "%{$filter['value']}%");
}
}
}
}
// Apply sorting if provided
if ($request->has('sortOrder') && !empty($request->get('sortOrder'))) {
$order = $request->get('sortOrder');
$column = $request->get('sortField');
// Map frontend column names to database column names if needed
$columnMap = [
'account_number' => 'account_number',
'period' => 'period',
'status' => 'status',
];
$dbColumn = $columnMap[$column] ?? $column;
$query->orderBy($dbColumn, $order);
} else {
// Default sorting
$query->latest('created_at');
}
// Get the total count of records
$totalRecords = $query->count();
// Apply pagination if provided
if ($request->has('page') && $request->has('size')) {
$page = $request->get('page');
$size = $request->get('size');
$offset = ($page - 1) * $size;
$query->skip($offset)->take($size);
}
// Get the filtered count of records
$filteredRecords = $query->count();
// Eager load relationships
$query->with(['user', 'authorizer']);
// Get the data for the current page
$data = $query->get()->map(function ($item) {
$processingHours = $item->status === 'processing' ? $item->updated_at->diffInHours(now()) : 0;
$isProcessingTimeout = $item->status === 'processing' && $processingHours >= 1;
return [
'id' => $item->id,
'account_number' => $item->account_number,
'period' => $item->period,
'report_date' => Carbon::createFromFormat('Ymd', $item->period)->format('Y-m-d'),
'status' => $item->status,
'status_display' => $item->status . ($isProcessingTimeout ? ' (Timeout)' : ''),
'processing_hours' => $processingHours,
'is_processing_timeout' => $isProcessingTimeout,
'authorization_status' => $item->authorization_status,
'is_downloaded' => $item->is_downloaded,
'created_at' => $item->created_at->format('Y-m-d H:i:s'),
'created_by' => $item->user->name ?? 'N/A',
'authorized_by' => $item->authorizer ? $item->authorizer->name : null,
'authorized_at' => $item->authorized_at ? $item->authorized_at->format('Y-m-d H:i:s') : null,
'file_path' => $item->file_path,
'record_count' => $item->record_count,
'can_retry' => in_array($item->status, ['failed', 'pending']) || $isProcessingTimeout || ($item->status === 'completed' && !$item->file_path),
];
});
// Calculate the page count
$pageCount = ceil($filteredRecords / ($request->get('size') ?: 1));
$currentPage = $request->get('page') ?: 1;
Log::info('Data laporan closing balance berhasil diambil', [
'total_records' => $totalRecords,
'filtered_records' => $filteredRecords
]);
return response()->json([
'draw' => $request->get('draw'),
'recordsTotal' => $totalRecords,
'recordsFiltered' => $filteredRecords,
'pageCount' => $pageCount,
'page' => $currentPage,
'totalCount' => $totalRecords,
'data' => $data,
]);
} catch (Exception $e) {
Log::error('Error saat mengambil data datatables', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return response()->json([
'error' => 'Terjadi kesalahan saat mengambil data laporan',
'message' => $e->getMessage()
], 500);
}
}
/**
* Hapus permintaan laporan
*
* @param ClosingBalanceReportLog $closingBalanceReport
* @return \Illuminate\Http\JsonResponse
*/
public function destroy(ClosingBalanceReportLog $closingBalanceReport)
{
Log::info('Menghapus laporan closing balance', [
'report_id' => $closingBalanceReport->id
]);
try {
DB::beginTransaction();
// Delete the file if exists
if ($closingBalanceReport->file_path && Storage::exists($closingBalanceReport->file_path)) {
Storage::delete($closingBalanceReport->file_path);
}
// Delete the report request
$closingBalanceReport->delete();
DB::commit();
Log::info('Laporan closing balance berhasil dihapus', [
'report_id' => $closingBalanceReport->id
]);
return response()->json([
'message' => 'Laporan closing balance berhasil dihapus.',
]);
} catch (Exception $e) {
DB::rollback();
Log::error('Error saat menghapus laporan', [
'report_id' => $closingBalanceReport->id,
'error' => $e->getMessage()
]);
return response()->json([
'error' => 'Terjadi kesalahan saat menghapus laporan',
'message' => $e->getMessage()
], 500);
}
}
/**
* Retry generating laporan closing balance
*
* @param ClosingBalanceReportLog $closingBalanceReport
* @return \Illuminate\Http\RedirectResponse
*/
public function retry(ClosingBalanceReportLog $closingBalanceReport)
{
Log::info('Retry laporan closing balance', [
'report_id' => $closingBalanceReport->id
]);
try {
// Check if retry is allowed
$allowedStatuses = ['failed', 'pending'];
$isProcessingTooLong = $closingBalanceReport->status === 'processing' &&
$closingBalanceReport->updated_at->diffInHours(now()) >= 1;
if (!in_array($closingBalanceReport->status, $allowedStatuses) && !$isProcessingTooLong) {
return back()->with('error', 'Laporan hanya dapat diulang jika status failed, pending, atau processing lebih dari 1 jam.');
}
DB::beginTransaction();
// If it was processing for too long, mark it as failed first
if ($isProcessingTooLong) {
$closingBalanceReport->update([
'status' => 'failed',
'error_message' => 'Processing timeout - melebihi batas waktu 1 jam',
'updated_by' => Auth::id()
]);
}
// Reset the report status and clear previous data
$closingBalanceReport->update([
'status' => 'processing',
'error_message' => null,
'file_path' => null,
'file_size' => null,
'record_count' => null,
'updated_by' => Auth::id()
]);
// Dispatch the job again
GenerateClosingBalanceReportJob::dispatch(
$closingBalanceReport->account_number,
$closingBalanceReport->period,
$closingBalanceReport->id
);
DB::commit();
Log::info('Laporan closing balance berhasil diulang', [
'report_id' => $closingBalanceReport->id
]);
return back()->with('success', 'Job laporan closing balance berhasil diulang.');
} catch (Exception $e) {
DB::rollback();
Log::error('Error saat retry laporan', [
'report_id' => $closingBalanceReport->id,
'error' => $e->getMessage()
]);
$closingBalanceReport->update([
'status' => 'failed',
'error_message' => $e->getMessage(),
'updated_by' => Auth::id()
]);
return back()->with('error', 'Gagal mengulang generate laporan: ' . $e->getMessage());
}
}
/**
* Download laporan berdasarkan nomor rekening dan periode
*
* @param string $accountNumber
* @param string $period
* @return \Illuminate\Http\Response
*/
public function download($accountNumber, $period)
{
Log::info('Download laporan closing balance', [
'account_number' => $accountNumber,
'period' => $period,
'user_id' => Auth::id()
]);
try {
// Cari laporan berdasarkan account number dan period
$closingBalanceReport = ClosingBalanceReportLog::where('account_number', $accountNumber)
->where('period', $period)
->where('status', 'completed')
->whereNotNull('file_path')
->first();
if (!$closingBalanceReport) {
Log::warning('Laporan tidak ditemukan atau belum selesai', [
'account_number' => $accountNumber,
'period' => $period
]);
return back()->with('error', 'Laporan tidak ditemukan atau belum selesai diproses.');
}
DB::beginTransaction();
// Update download status
$closingBalanceReport->update([
'is_downloaded' => true,
'downloaded_at' => now(),
'updated_by' => Auth::id()
]);
DB::commit();
// Download the file
$filePath = $closingBalanceReport->file_path;
if (Storage::exists($filePath)) {
$fileName = "closing_balance_report_{$accountNumber}_{$period}.csv";
Log::info('File laporan berhasil didownload', [
'account_number' => $accountNumber,
'period' => $period,
'file_path' => $filePath
]);
return Storage::download($filePath, $fileName);
}
Log::error('File laporan tidak ditemukan di storage', [
'account_number' => $accountNumber,
'period' => $period,
'file_path' => $filePath
]);
return back()->with('error', 'File laporan tidak ditemukan.');
} catch (Exception $e) {
DB::rollback();
Log::error('Error saat download laporan', [
'account_number' => $accountNumber,
'period' => $period,
'error' => $e->getMessage()
]);
return back()->with('error', 'Terjadi kesalahan saat mengunduh laporan: ' . $e->getMessage());
}
}
}