- Memberikan akses penuh fitur multi-branch untuk role `administrator` dan `sentra_operasi`.
- Menambahkan akun untuk client **SILOT** dalam daftar monitoring.
- Menonaktifkan validasi duplikasi statement di `PrintStatementRequest`.
- Memindahkan struktur penyimpanan file dari `statements/{client}` menjadi `partners/{client}`.
- Menambahkan pengurutan hasil berdasarkan `branch_code` dan `account_number` untuk laporan.
- Memperbaiki tampilan dropdown branch dan menyembunyikan field `end_date` yang tidak relevan.
- Menghapus opsi `NO.PRINT` dari dropdown `stmt_sent_type` untuk penyederhanaan UI.
- Peningkatan UI dan struktur direktori untuk mempermudah pembacaan dan pengelolaan statement.
464 lines
18 KiB
PHP
464 lines
18 KiB
PHP
<?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\ProcessedStatement;
|
|
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 $client;
|
|
protected $fileName;
|
|
protected $chunkSize = 1000; // Proses data dalam chunk untuk mengurangi penggunaan memori
|
|
|
|
/**
|
|
* 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 $client = '', string $disk = 'local')
|
|
{
|
|
$this->account_number = $account_number;
|
|
$this->period = $period;
|
|
$this->saldo = $saldo;
|
|
$this->disk = $disk;
|
|
$this->client = $client;
|
|
$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}");
|
|
|
|
$this->processStatementData();
|
|
|
|
// Export data yang sudah diproses ke CSV
|
|
$this->exportToCsv();
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
private function processStatementData()
|
|
: void
|
|
{
|
|
$accountQuery = [
|
|
'account_number' => $this->account_number,
|
|
'period' => $this->period
|
|
];
|
|
|
|
$totalCount = $this->getTotalEntryCount($accountQuery);
|
|
$existingDataCount = $this->getExistingProcessedCount($accountQuery);
|
|
|
|
// Hanya proses jika data belum lengkap diproses
|
|
//if ($existingDataCount !== $totalCount) {
|
|
$this->deleteExistingProcessedData($accountQuery);
|
|
$this->processAndSaveStatementEntries($totalCount);
|
|
//}
|
|
}
|
|
|
|
private function getTotalEntryCount(array $criteria)
|
|
: int
|
|
{
|
|
return StmtEntry::where('account_number', $criteria['account_number'])
|
|
->where('booking_date', $criteria['period'])
|
|
->count();
|
|
}
|
|
|
|
private function getExistingProcessedCount(array $criteria)
|
|
: int
|
|
{
|
|
return ProcessedStatement::where('account_number', $criteria['account_number'])
|
|
->where('period', $criteria['period'])
|
|
->count();
|
|
}
|
|
|
|
private function deleteExistingProcessedData(array $criteria)
|
|
: void
|
|
{
|
|
ProcessedStatement::where('account_number', $criteria['account_number'])
|
|
->where('period', $criteria['period'])
|
|
->delete();
|
|
}
|
|
|
|
private function processAndSaveStatementEntries(int $totalCount)
|
|
: void
|
|
{
|
|
$runningBalance = (float) $this->saldo;
|
|
$globalSequence = 0;
|
|
|
|
Log::info("Processing {$totalCount} statement entries for account: {$this->account_number}");
|
|
|
|
StmtEntry::with(['ft', 'transaction'])
|
|
->where('account_number', $this->account_number)
|
|
->where('booking_date', $this->period)
|
|
->orderBy('date_time', 'ASC')
|
|
->orderBy('trans_reference', 'ASC')
|
|
->chunk($this->chunkSize, function ($entries) use (&$runningBalance, &$globalSequence) {
|
|
$processedData = $this->prepareProcessedData($entries, $runningBalance, $globalSequence);
|
|
|
|
if (!empty($processedData)) {
|
|
DB::table('processed_statements')->insert($processedData);
|
|
}
|
|
});
|
|
}
|
|
|
|
private function prepareProcessedData($entries, &$runningBalance, &$globalSequence)
|
|
: array
|
|
{
|
|
$processedData = [];
|
|
|
|
foreach ($entries as $item) {
|
|
$globalSequence++;
|
|
$runningBalance += (float) $item->amount_lcy;
|
|
|
|
$transactionDate = $this->formatTransactionDate($item);
|
|
$actualDate = $this->formatActualDate($item);
|
|
|
|
$processedData[] = [
|
|
'account_number' => $this->account_number,
|
|
'period' => $this->period,
|
|
'sequence_no' => $globalSequence,
|
|
'transaction_date' => $transactionDate,
|
|
'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' => $actualDate,
|
|
'recipt_no' => $item->ft?->recipt_no ?? '-',
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
];
|
|
}
|
|
|
|
return $processedData;
|
|
}
|
|
|
|
private function formatTransactionDate($item)
|
|
: string
|
|
{
|
|
try {
|
|
$prefix = substr($item->trans_reference ?? '', 0, 2);
|
|
$relationMap = [
|
|
'FT' => 'ft',
|
|
'TT' => 'tt',
|
|
'DC' => 'dc',
|
|
'AA' => 'aa'
|
|
];
|
|
|
|
$datetime = $item->date_time;
|
|
if (isset($relationMap[$prefix])) {
|
|
$relation = $relationMap[$prefix];
|
|
$datetime = $item->$relation?->date_time ?? $datetime;
|
|
}
|
|
|
|
return Carbon::createFromFormat(
|
|
'YmdHi',
|
|
$item->booking_date . substr($datetime, 6, 4)
|
|
)->format('d/m/Y H:i');
|
|
} catch (Exception $e) {
|
|
Log::warning("Error formatting transaction date: " . $e->getMessage());
|
|
return Carbon::now()->format('d/m/Y H:i');
|
|
}
|
|
}
|
|
|
|
private function formatActualDate($item)
|
|
: string
|
|
{
|
|
try {
|
|
$prefix = substr($item->trans_reference ?? '', 0, 2);
|
|
$relationMap = [
|
|
'FT' => 'ft',
|
|
'TT' => 'tt',
|
|
'DC' => 'dc',
|
|
'AA' => 'aa'
|
|
];
|
|
|
|
$datetime = $item->date_time;
|
|
if (isset($relationMap[$prefix])) {
|
|
$relation = $relationMap[$prefix];
|
|
$datetime = $item->$relation?->date_time ?? $datetime;
|
|
}
|
|
|
|
|
|
return Carbon::createFromFormat(
|
|
'ymdHi',
|
|
$datetime
|
|
)->format('d/m/Y H:i');
|
|
} catch (Exception $e) {
|
|
Log::warning("Error formatting actual date: " . $e->getMessage());
|
|
return Carbon::now()->format('d/m/Y H:i');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate narrative for a statement entry
|
|
*/
|
|
private function generateNarrative($item)
|
|
{
|
|
$narr = [];
|
|
|
|
if ($item->transaction) {
|
|
if ($item->transaction->stmt_narr) {
|
|
$narr[] = $item->transaction->stmt_narr;
|
|
}
|
|
if ($item->narrative) {
|
|
$narr[] = $item->narrative;
|
|
}
|
|
if ($item->transaction->narr_type) {
|
|
$narr[] = $this->getFormatNarrative($item->transaction->narr_type, $item);
|
|
}
|
|
} else if ($item->narrative) {
|
|
$narr[] = $item->narrative;
|
|
}
|
|
|
|
/*if ($item->ft?->recipt_no) {
|
|
$narr[] = 'Receipt No: ' . $item->ft->recipt_no;
|
|
}*/
|
|
|
|
return implode(' ', array_filter($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.OUT';
|
|
} else if ($narrParam->_id == 'TTTRFOUT') {
|
|
$fmt = 'TT.O.TRF';
|
|
} else if ($narrParam->_id == 'TTTRFIN') {
|
|
$fmt = 'TT.I.TRF';
|
|
} else if ($narrParam->_id == 'APITRX'){
|
|
$fmt = 'API.TSEL';
|
|
} else if ($narrParam->_id == 'ONUSCR'){
|
|
$fmt = 'ONUS.CR';
|
|
} else if ($narrParam->_id == 'ONUSDR'){
|
|
$fmt = 'ONUS.DR';
|
|
}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') {
|
|
$prefix = substr($item->trans_reference ?? '', 0, 2);
|
|
$relationMap = [
|
|
'FT' => 'ft',
|
|
'TT' => 'tt',
|
|
'DC' => 'dc',
|
|
'AA' => 'aa'
|
|
];
|
|
|
|
if (isset($relationMap[$prefix])) {
|
|
$relation = $relationMap[$prefix];
|
|
$result .= ($item->$relation?->$fieldName ?? '') . ' ';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return str_replace('<NL>', ' ', $result);
|
|
}
|
|
|
|
/**
|
|
* Export processed data to CSV file
|
|
*/
|
|
private function exportToCsv(): void
|
|
{
|
|
// Determine the base path based on client
|
|
$basePath = !empty($this->client)
|
|
? "partners/{$this->client}"
|
|
: "partners";
|
|
|
|
$accountPath = "{$basePath}/{$this->account_number}";
|
|
|
|
// PERBAIKAN: Selalu pastikan direktori dibuat
|
|
Storage::disk($this->disk)->makeDirectory($basePath);
|
|
Storage::disk($this->disk)->makeDirectory($accountPath);
|
|
|
|
|
|
|
|
$filePath = "{$accountPath}/{$this->fileName}";
|
|
|
|
// Delete existing file if it exists
|
|
if (Storage::disk($this->disk)->exists($filePath)) {
|
|
Storage::disk($this->disk)->delete($filePath);
|
|
}
|
|
|
|
|
|
// Tambahkan di awal fungsi exportToCsv
|
|
Log::info("Starting CSV export", [
|
|
'disk' => $this->disk,
|
|
'client' => $this->client,
|
|
'account_number' => $this->account_number,
|
|
'period' => $this->period,
|
|
'base_path' => $basePath,
|
|
'account_path' => $accountPath,
|
|
'file_path' => $filePath
|
|
]);
|
|
|
|
// Cek apakah disk storage berfungsi
|
|
$testFile = 'test_' . time() . '.txt';
|
|
Storage::disk($this->disk)->put($testFile, 'test content');
|
|
if (Storage::disk($this->disk)->exists($testFile)) {
|
|
Log::info("Storage disk is working");
|
|
Storage::disk($this->disk)->delete($testFile);
|
|
} else {
|
|
Log::error("Storage disk is not working properly");
|
|
}
|
|
|
|
// PERBAIKAN: Buat file header terlebih dahulu
|
|
$csvContent = "NO|TRANSACTION.DATE|REFERENCE.NUMBER|TRANSACTION.AMOUNT|TRANSACTION.TYPE|DESCRIPTION|END.BALANCE|ACTUAL.DATE|NO.RECEIPT\n";
|
|
Storage::disk($this->disk)->put($filePath, $csvContent);
|
|
|
|
// Ambil data yang sudah diproses dalam chunk
|
|
ProcessedStatement::where('account_number', $this->account_number)
|
|
->where('period', $this->period)
|
|
->orderBy('sequence_no')
|
|
->chunk($this->chunkSize, function ($statements) use ($filePath) {
|
|
$csvContent = '';
|
|
foreach ($statements as $statement) {
|
|
$csvContent .= implode('|', [
|
|
$statement->sequence_no,
|
|
$statement->transaction_date,
|
|
$statement->reference_number,
|
|
$statement->transaction_amount,
|
|
$statement->transaction_type,
|
|
$statement->description,
|
|
$statement->end_balance,
|
|
$statement->actual_date,
|
|
$statement->recipt_no
|
|
]) . "\n";
|
|
}
|
|
|
|
// Append ke file
|
|
if (!empty($csvContent)) {
|
|
Storage::disk($this->disk)->append($filePath, $csvContent);
|
|
}
|
|
});
|
|
|
|
// PERBAIKAN: Verifikasi file benar-benar ada
|
|
if (Storage::disk($this->disk)->exists($filePath)) {
|
|
$fileSize = Storage::disk($this->disk)->size($filePath);
|
|
Log::info("Statement exported successfully", [
|
|
'disk' => $this->disk,
|
|
'file_path' => $filePath,
|
|
'file_size' => $fileSize,
|
|
'account_number' => $this->account_number,
|
|
'period' => $this->period
|
|
]);
|
|
} else {
|
|
Log::error("File was not created despite successful processing", [
|
|
'disk' => $this->disk,
|
|
'file_path' => $filePath,
|
|
'account_number' => $this->account_number,
|
|
'period' => $this->period
|
|
]);
|
|
throw new \Exception("Failed to create CSV file: {$filePath}");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get transaction data by reference and field
|
|
*/
|
|
private function getTransaction($ref, $field)
|
|
{
|
|
$trans = TempFundsTransfer::where('ref_no', $ref)->first();
|
|
return $trans->$field ?? "";
|
|
}
|
|
}
|