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 <ddeni05@gmail.com>
This commit is contained in:
Daeng Deni Mardaeni
2025-05-22 09:03:34 +07:00
parent d5495d721e
commit 04f6f02702
2 changed files with 514 additions and 202 deletions

View File

@@ -0,0 +1,243 @@
<?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\Log;
use Illuminate\Support\Facades\Storage;
use Modules\Webstatement\Models\StmtEntry;
use Modules\Webstatement\Models\TempFundsTransfer;
use Modules\Webstatement\Models\TempStmtNarrFormat;
use Modules\Webstatement\Models\TempStmtNarrParam;
class ExportStatementJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $account_number;
protected $period;
protected $saldo;
protected $disk;
protected $fileName;
/**
* Create a new job instance.
*
* @param string $account_number
* @param string $period
* @param string $saldo
* @param string $disk
*/
public function __construct(string $account_number, string $period, string $saldo, string $disk = 'local')
{
$this->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}");
}
}