From 04f6f027023075ce151eb9b3bb5ceeb9a3c7e212 Mon Sep 17 00:00:00 2001 From: Daeng Deni Mardaeni Date: Thu, 22 May 2025 09:03:34 +0700 Subject: [PATCH] feat(webstatement): tambahkan fitur eksport dan download statement Penambahan fitur untuk mendukung proses eksport dan download statement secara dinamis. Fitur ini mencakup: - Penambahan `ExportStatementJob` untuk menjadwalkan proses eksport statement dalam bentuk file CSV ke dalam queue. - Penambahan endpoint untuk: 1. `index`: Menjadwalkan pekerjaan eksport ke dalam queue. 2. `generateAndDownload`: Proses pembuatan statement secara langsung dan mengunduh hasilnya. 3. `downloadStatement`: Mendukung pengunduhan file statement yang telah dibuat sebelumnya. 4. `queueExport`: Menambahkan job eksport ke queue dengan ID job yang dirilis. 5. `checkExportStatus`: Memastikan status job apakah sedang berjalan, selesai, atau tidak ditemukan. - Refactoring fitur narrative generator untuk mendukung format dinamis berdasarkan konfigurasi database dengan parsing format. - Refactoring data transformation untuk memastikan urutan dan perhitungan running balance sebelum dieksport. - Penggunaan storage lokal untuk menyimpan hasil file CSV, dan implementasi header respons yang benar untuk file unduhan. Signed-off-by: Daeng Deni Mardaeni --- .../Controllers/WebstatementController.php | 473 ++++++++++-------- app/Jobs/ExportStatementJob.php | 243 +++++++++ 2 files changed, 514 insertions(+), 202 deletions(-) create mode 100644 app/Jobs/ExportStatementJob.php diff --git a/app/Http/Controllers/WebstatementController.php b/app/Http/Controllers/WebstatementController.php index 5bb791e..c0eb342 100644 --- a/app/Http/Controllers/WebstatementController.php +++ b/app/Http/Controllers/WebstatementController.php @@ -1,226 +1,295 @@ where('account_number',$account_number)->where('booking_date',$period)->get(); - $saldo = '23984352604'; - $runningBalance = (float) $saldo; + return response()->json([ + 'message' => 'Statement export job has been queued', + 'account_number' => $account_number, + 'period' => $period, + 'file_name' => "{$account_number}_{$period}.csv" + ]); + } - // Map the data to transform or format specific fields - $mappedData = $stmt->sortBy(['ACTUAL.DATE', 'REFERENCE.NUMBER'])->map(function ($item, $index) use (&$runningBalance) { - $runningBalance += (float) $item->amount_lcy; - return [ - 'NO' => 0, // Use $index instead of $item->count() - 'TRANSACTION.DATE' => \Carbon\Carbon::createFromFormat('YmdHi', $item->booking_date . substr($item->ft?->date_time ?? '0000000000', 6, 4))->format('d/m/Y H:i'), - 'REFERENCE.NUMBER' => $item->trans_reference, - 'TRANSACTION.AMOUNT' => $item->amount_lcy, - 'TRANSACTION.TYPE' => $item->amount_lcy < 0 ? 'D' : 'C', - 'DESCRIPTION' => $this->generateNarative($item), - 'END.BALANCE' => $runningBalance, - 'ACTUAL.DATE' => \Carbon\Carbon::createFromFormat('ymdHi', $item->ft?->date_time ?? '2505120000') - ->format('d/m/Y H:i'), + /** + * Download a previously exported statement + */ + public function downloadStatement(Request $request) + { + $account_number = $request->input('account_number', '1080425781'); + $period = $request->input('period', '20250512'); + $fileName = "{$account_number}_{$period}.csv"; + $filePath = "statements/{$fileName}"; + + if (!Storage::disk('local')->exists($filePath)) { + return response()->json([ + 'message' => 'Statement file not found. It may still be processing.' + ], 404); + } + + return Storage::disk('local')->download($filePath, $fileName, [ + "Content-Type" => "text/csv", + ]); + } + + /** + * Generate statement on-demand and return as download + */ + public function generateAndDownload(Request $request) + { + $account_number = $request->input('account_number', '1080425781'); + $period = $request->input('period', '20250512'); + $saldo = $request->input('saldo', '23984352604'); + + $stmt = StmtEntry::with(['ft', 'transaction']) + ->where('account_number', $account_number) + ->where('booking_date', $period) + ->orderBy('date_time', 'ASC') + ->orderBy('trans_reference', 'ASC') + ->get(); + + if ($stmt->isEmpty()) { + return response()->json([ + 'message' => 'No statement data found for the specified account and period.' + ], 404); + } + + $runningBalance = (float) $saldo; + // Map the data to transform or format specific fields + $mappedData = $stmt->sortBy(['ACTUAL.DATE', 'REFERENCE.NUMBER']) + ->map(function ($item, $index) use (&$runningBalance) { + $runningBalance += (float) $item->amount_lcy; + return [ + 'NO' => 0, // Will be updated later + 'TRANSACTION.DATE' => Carbon::createFromFormat('YmdHi', $item->booking_date . substr($item->ft?->date_time ?? '0000000000', 6, 4)) + ->format('d/m/Y H:i'), + 'REFERENCE.NUMBER' => $item->trans_reference, + 'TRANSACTION.AMOUNT' => $item->amount_lcy, + 'TRANSACTION.TYPE' => $item->amount_lcy < 0 ? 'D' : 'C', + 'DESCRIPTION' => $this->generateNarrative($item), + 'END.BALANCE' => $runningBalance, + 'ACTUAL.DATE' => Carbon::createFromFormat('ymdHi', $item->ft?->date_time ?? '2505120000') + ->format('d/m/Y H:i'), + ]; + }) + ->values(); + + // Then apply the sequential numbers + $mappedData = $mappedData->map(function ($item, $index) { + $item['NO'] = $index + 1; + return $item; + }); + + $csvFileName = $account_number . "_" . $period . ".csv"; + $headers = [ + "Content-Type" => "text/csv", + "Content-Disposition" => "attachment; filename={$csvFileName}" ]; - })->values(); - - // Then apply the sequential numbers - $mappedData = $mappedData->map(function ($item, $index) { - $item['NO'] = $index + 1; - return $item; - }); - - - return response()->json($mappedData); - - $csvFileName = $account_number."_".$period.".csv"; - $headers = [ - "Content-Type" => "text/csv", - "Content-Disposition" => "attachment; filename={$csvFileName}" - ]; - - $callback = function () use ($mappedData) { - $file = fopen('php://output', 'w'); - // Write headers without quotes, using pipe separator - fputs($file, implode('|', array_keys($mappedData[0])) . "\n"); - // Write data rows without quotes, using pipe separator - foreach ($mappedData as $row) { - fputs($file, implode('|', $row) . "\n"); - } - fclose($file); - }; - - - return response()->stream($callback, 200, $headers); - } - - - function generateNarative($item){ - $narr = ''; - if($item->transaction->narr_type){ - $narr .= $item->transaction->stmt_narr.' '; - $narr .= $this->getFormatNarrative($item->transaction->narr_type,$item); - } else { - $narr .= $item->transaction->stmt_narr.' '; - } - - if($item->ft?->recipt_no) { - $narr .= 'Receipt No: ' . $item->ft->recipt_no; - } - return $narr; - } - - - function getFormatNarrative($narr,$item){ - $narrParam = TempStmtNarrParam::where('_id', $narr)->first(); - - if (!$narrParam) { - return ''; - } - - $fmt = ''; - if($narrParam->_id=='FTIN'){ - $fmt = 'FT.IN'; - }elseif($narrParam->_id=='FTOUT'){ - $fmt = 'FT.IN'; - } else { - $fmt = $narrParam->_id; - } - - $narrFormat = TempStmtNarrFormat::where('_id', $fmt)->first(); - - if (!$narrFormat) { - return ''; - } - - // Get the format string from the database - $formatString = $narrFormat->text_data ?? ''; - - // Parse the format string - // Split by the separator ']' - $parts = explode(']', $formatString); - - $result = ''; - - foreach ($parts as $index => $part) { - if (empty($part)) { - continue; - } - - if ($index === 0) { - // For the first part, take only what's before the '!' - $splitPart = explode('!', $part); - if (count($splitPart) > 0) { - // Remove quotes, backslashes, and other escape characters - $cleanPart = trim($splitPart[0]); - // Remove quotes at the beginning and end - $cleanPart = preg_replace('/^["\'\\\\]+|["\'\\\\]+$/', '', $cleanPart); - // Remove any remaining backslashes - $cleanPart = str_replace('\\', '', $cleanPart); - // Remove any remaining quotes - $cleanPart = str_replace('"', '', $cleanPart); - $result .= $cleanPart; + $callback = function () use ($mappedData) { + $file = fopen('php://output', 'w'); + // Write headers without quotes, using pipe separator + fputs($file, implode('|', array_keys($mappedData[0])) . "\n"); + // Write data rows without quotes, using pipe separator + foreach ($mappedData as $row) { + fputs($file, implode('|', $row) . "\n"); } + fclose($file); + }; + + return response()->stream($callback, 200, $headers); + } + + /** + * Generate narrative for a statement entry + */ + private function generateNarrative($item) + { + $narr = ''; + if ($item->transaction && $item->transaction->narr_type) { + $narr .= $item->transaction->stmt_narr . ' '; + $narr .= $this->getFormatNarrative($item->transaction->narr_type, $item); + } else if ($item->transaction) { + $narr .= $item->transaction->stmt_narr . ' '; + } + + if ($item->ft && $item->ft->recipt_no) { + $narr .= 'Receipt No: ' . $item->ft->recipt_no; + } + return $narr; + } + + /** + * Get formatted narrative based on narrative type + */ + private function getFormatNarrative($narr, $item) + { + $narrParam = TempStmtNarrParam::where('_id', $narr)->first(); + + if (!$narrParam) { + return ''; + } + + $fmt = ''; + if ($narrParam->_id == 'FTIN') { + $fmt = 'FT.IN'; + } else if ($narrParam->_id == 'FTOUT') { + $fmt = 'FT.IN'; } else { - // For other parts, these are field placeholders - $fieldName = strtolower(str_replace('.', '_', $part)); + $fmt = $narrParam->_id; + } - // Get the corresponding parameter value from narrParam - $paramValue = null; + $narrFormat = TempStmtNarrFormat::where('_id', $fmt)->first(); - // Check if the field exists as a property in narrParam - if (property_exists($narrParam, $fieldName)) { - $paramValue = $narrParam->$fieldName; - } elseif (isset($narrParam->$fieldName)) { - $paramValue = $narrParam->$fieldName; + if (!$narrFormat) { + return ''; + } + + // Get the format string from the database + $formatString = $narrFormat->text_data ?? ''; + + // Parse the format string + // Split by the separator ']' + $parts = explode(']', $formatString); + + $result = ''; + + foreach ($parts as $index => $part) { + if (empty($part)) { + continue; } - // If we found a value, add it to the result - if ($paramValue !== null) { - $result .= $paramValue; + if ($index === 0) { + // For the first part, take only what's before the '!' + $splitPart = explode('!', $part); + if (count($splitPart) > 0) { + // Remove quotes, backslashes, and other escape characters + $cleanPart = trim($splitPart[0]); + // Remove quotes at the beginning and end + $cleanPart = preg_replace('/^["\'\\\\]+|["\'\\\\]+$/', '', $cleanPart); + // Remove any remaining backslashes + $cleanPart = str_replace('\\', '', $cleanPart); + // Remove any remaining quotes + $cleanPart = str_replace('"', '', $cleanPart); + $result .= $cleanPart; + } } else { + // For other parts, these are field placeholders + $fieldName = strtolower(str_replace('.', '_', $part)); - // If no value found, try to use the original field name as a fallback - if($fieldName!='recipt_no') { - $result .= $this->getTransaction($item->trans_reference, $fieldName).' '; - // $result .= "[$fieldName]"; + // Get the corresponding parameter value from narrParam + $paramValue = null; + + // Check if the field exists as a property in narrParam + if (property_exists($narrParam, $fieldName)) { + $paramValue = $narrParam->$fieldName; + } else if (isset($narrParam->$fieldName)) { + $paramValue = $narrParam->$fieldName; + } + + // If we found a value, add it to the result + if ($paramValue !== null) { + $result .= $paramValue; + } else { + // If no value found, try to use the original field name as a fallback + if ($fieldName != 'recipt_no') { + $result .= $this->getTransaction($item->trans_reference, $fieldName) . ' '; + } } } } + + return $result; } - return $result; + /** + * Get transaction data by reference and field + */ + private function getTransaction($ref, $field) + { + $trans = TempFundsTransfer::where('ref_no', $ref)->first(); + return $trans ? ($trans->$field ?? "") : ""; + } + + /** + * Queue a statement export job and return job ID + */ + public function queueExport(Request $request) + { + $account_number = $request->input('account_number', '1080425781'); + $period = $request->input('period', '20250512'); + $saldo = $request->input('saldo', '23984352604'); + + // Dispatch the job and get the job ID + $job = new ExportStatementJob($account_number, $period, $saldo); + $jobId = app(Dispatcher::class)->dispatch($job); + + return response()->json([ + 'message' => 'Statement export job has been queued', + 'job_id' => $jobId, + 'account_number' => $account_number, + 'period' => $period, + 'file_name' => "{$account_number}_{$period}.csv" + ]); + } + + /** + * Check the status of an export job + */ + public function checkExportStatus(Request $request, $jobId) + { + // Get job status from the queue + $job = DB::table('jobs') + ->where('id', $jobId) + ->first(); + + if (!$job) { + // Check if job is completed + $completedJob = DB::table('job_batches') + ->where('id', $jobId) + ->first(); + + if ($completedJob) { + return response()->json([ + 'status' => 'completed', + 'message' => 'Export job has been completed' + ]); + } + + return response()->json([ + 'status' => 'not_found', + 'message' => 'Export job not found' + ], 404); + } + + return response()->json([ + 'status' => 'pending', + 'message' => 'Export job is still processing' + ]); + } } - - - public function getTransaction($ref, $field){ - $trans = TempFundsTransfer::where('ref_no', $ref)->first(); - - return $trans->$field ?? ""; - } - - /** - * Show the form for creating a new resource. - */ - public function create() - { - return view('webstatement::create'); - } - - /** - * Store a newly created resource in storage. - */ - public function store(Request $request) - { - // - } - - /** - * Show the specified resource. - */ - public function show($id) - { - return view('webstatement::show'); - } - - /** - * Show the form for editing the specified resource. - */ - public function edit($id) - { - return view('webstatement::edit'); - } - - /** - * Update the specified resource in storage. - */ - public function update(Request $request, $id) - { - // - } - - /** - * Remove the specified resource from storage. - */ - public function destroy($id) - { - // - } -} diff --git a/app/Jobs/ExportStatementJob.php b/app/Jobs/ExportStatementJob.php new file mode 100644 index 0000000..a18098b --- /dev/null +++ b/app/Jobs/ExportStatementJob.php @@ -0,0 +1,243 @@ +account_number = $account_number; + $this->period = $period; + $this->saldo = $saldo; + $this->disk = $disk; + $this->fileName = "{$account_number}_{$period}.csv"; + } + + /** + * Execute the job. + */ + public function handle() + : void + { + try { + Log::info("Starting export statement job for account: {$this->account_number}, period: {$this->period}"); + + $stmt = $this->getStatementData(); + $mappedData = $this->mapStatementData($stmt); + $this->exportToCsv($mappedData); + + Log::info("Export statement job completed successfully for account: {$this->account_number}, period: {$this->period}"); + } catch (Exception $e) { + Log::error("Error in ExportStatementJob: " . $e->getMessage()); + throw $e; + } + } + + /** + * Get statement data from database + */ + private function getStatementData() + { + return StmtEntry::with(['ft', 'transaction']) + ->where('account_number', $this->account_number) + ->where('booking_date', $this->period) + ->orderBy('date_time', 'ASC') + ->orderBy('trans_reference', 'ASC') + ->get(); + } + + /** + * Map statement data to the required format + */ + private function mapStatementData($stmt) + { + $runningBalance = (float) $this->saldo; + + // Map the data to transform or format specific fields + $mappedData = $stmt->sortBy(['ACTUAL.DATE', 'REFERENCE.NUMBER']) + ->map(function ($item, $index) use (&$runningBalance) { + $runningBalance += (float) $item->amount_lcy; + return [ + 'NO' => 0, // Will be updated later + 'TRANSACTION.DATE' => Carbon::createFromFormat('YmdHi', $item->booking_date . substr($item->ft?->date_time ?? '0000000000', 6, 4)) + ->format('d/m/Y H:i'), + 'REFERENCE.NUMBER' => $item->trans_reference, + 'TRANSACTION.AMOUNT' => $item->amount_lcy, + 'TRANSACTION.TYPE' => $item->amount_lcy < 0 ? 'D' : 'C', + 'DESCRIPTION' => $this->generateNarrative($item), + 'END.BALANCE' => $runningBalance, + 'ACTUAL.DATE' => Carbon::createFromFormat('ymdHi', $item->ft?->date_time ?? '2505120000') + ->format('d/m/Y H:i'), + ]; + }) + ->values(); + + // Apply sequential numbers + return $mappedData->map(function ($item, $index) { + $item['NO'] = $index + 1; + return $item; + }); + } + + /** + * Generate narrative for a statement entry + */ + private function generateNarrative($item) + { + $narr = ''; + if ($item->transaction->narr_type) { + $narr .= $item->transaction->stmt_narr . ' '; + $narr .= $this->getFormatNarrative($item->transaction->narr_type, $item); + } else { + $narr .= $item->transaction->stmt_narr . ' '; + } + + if ($item->ft?->recipt_no) { + $narr .= 'Receipt No: ' . $item->ft->recipt_no; + } + return $narr; + } + + /** + * Get formatted narrative based on narrative type + */ + private function getFormatNarrative($narr, $item) + { + $narrParam = TempStmtNarrParam::where('_id', $narr)->first(); + + if (!$narrParam) { + return ''; + } + + $fmt = ''; + if ($narrParam->_id == 'FTIN') { + $fmt = 'FT.IN'; + } else if ($narrParam->_id == 'FTOUT') { + $fmt = 'FT.IN'; + } else { + $fmt = $narrParam->_id; + } + + $narrFormat = TempStmtNarrFormat::where('_id', $fmt)->first(); + + if (!$narrFormat) { + return ''; + } + + // Get the format string from the database + $formatString = $narrFormat->text_data ?? ''; + + // Parse the format string + // Split by the separator ']' + $parts = explode(']', $formatString); + + $result = ''; + + foreach ($parts as $index => $part) { + if (empty($part)) { + continue; + } + + if ($index === 0) { + // For the first part, take only what's before the '!' + $splitPart = explode('!', $part); + if (count($splitPart) > 0) { + // Remove quotes, backslashes, and other escape characters + $cleanPart = trim($splitPart[0]); + // Remove quotes at the beginning and end + $cleanPart = preg_replace('/^["\'\\\\]+|["\'\\\\]+$/', '', $cleanPart); + // Remove any remaining backslashes + $cleanPart = str_replace('\\', '', $cleanPart); + // Remove any remaining quotes + $cleanPart = str_replace('"', '', $cleanPart); + $result .= $cleanPart; + } + } else { + // For other parts, these are field placeholders + $fieldName = strtolower(str_replace('.', '_', $part)); + + // Get the corresponding parameter value from narrParam + $paramValue = null; + + // Check if the field exists as a property in narrParam + if (property_exists($narrParam, $fieldName)) { + $paramValue = $narrParam->$fieldName; + } else if (isset($narrParam->$fieldName)) { + $paramValue = $narrParam->$fieldName; + } + + // If we found a value, add it to the result + if ($paramValue !== null) { + $result .= $paramValue; + } else { + // If no value found, try to use the original field name as a fallback + if ($fieldName != 'recipt_no') { + $result .= $this->getTransaction($item->trans_reference, $fieldName) . ' '; + } + } + } + } + + return $result; + } + + /** + * Get transaction data by reference and field + */ + private function getTransaction($ref, $field) + { + $trans = TempFundsTransfer::where('ref_no', $ref)->first(); + return $trans->$field ?? ""; + } + + /** + * Export data to CSV file + */ + private function exportToCsv($mappedData) + { + $csvContent = ''; + + // Add headers + $csvContent .= implode('|', array_keys($mappedData[0])) . "\n"; + + // Add data rows + foreach ($mappedData as $row) { + $csvContent .= implode('|', $row) . "\n"; + } + + // Save to storage + Storage::disk($this->disk)->put("statements/{$this->fileName}", $csvContent); + + Log::info("Statement exported to {$this->disk} disk: statements/{$this->fileName}"); + } + }