- Menambahkan konstanta baru pada setiap job untuk meningkatkan keterbacaan: - `CSV_DELIMITER` untuk delimiter CSV. - `MAX_EXECUTION_TIME` untuk batas waktu eksekusi (86400 detik). - `FILENAME` untuk nama file masing-masing job. - `DISK_NAME` untuk disk yang digunakan. - Mengubah `protected` menjadi `private` untuk properti seperti: - `$period`, `$processedCount`, dan `$errorCount` di semua job. - Memindahkan logika proses menjadi metode modular untuk meningkatkan modularitas: - Metode `initializeJob` untuk inisialisasi counter. - Metode `processPeriod` untuk menangani file dan memulai proses. - Metode `validateFile` untuk validasi keberadaan file. - Metode `createTemporaryFile` untuk menyalin file sementara. - Metode `processFile` untuk membaca isi file CSV dan memproses. - Metode `processRow`/`mapAndSaveRecord`/`saveRecord` untuk pemrosesan dan penyimpanan data. - Metode `cleanup` untuk menghapus file sementara. - Metode `logJobCompletion` untuk logging hasil akhir. - Menerapkan pengolahan file dengan sistem logging terperinci: - Logging row dengan kolom tidak sesuai akan menghasilkan peringatan. - Mencatat jumlah record berhasil diproses serta error. Refaktor ini bertujuan untuk meningkatkan kualitas kode melalui modularisasi, keterbacaan, dan kemudahan pengujian ulang di seluruh kelas job.
212 lines
6.9 KiB
PHP
212 lines
6.9 KiB
PHP
<?php
|
|
|
|
namespace Modules\Webstatement\Jobs;
|
|
|
|
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\AtmTransaction;
|
|
|
|
class ProcessAtmTransactionJob implements ShouldQueue
|
|
{
|
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
|
private const CSV_DELIMITER = '~';
|
|
private const MAX_EXECUTION_TIME = 86400; // 24 hours in seconds
|
|
private const FILENAME = 'ST.ATM.TRANSACTION.csv';
|
|
private const DISK_NAME = 'sftpStatement';
|
|
private const HEADER_MAP = [
|
|
'id' => 'transaction_id',
|
|
'card_acc_id' => 'card_acc_id',
|
|
'pan_number' => 'pan_number',
|
|
'txn_type' => 'txn_type',
|
|
'merchant_id' => 'merchant_id',
|
|
'txn_amount' => 'txn_amount',
|
|
'booking_date' => 'booking_date',
|
|
'trans_ref' => 'trans_ref',
|
|
'retrieval_ref_no' => 'retrieval_ref_no',
|
|
'stmt_nos' => 'stmt_nos',
|
|
'debit_acct_no' => 'debit_acct_no',
|
|
'credit_acct_no' => 'credit_acct_no',
|
|
'chrg_amount' => 'chrg_amount',
|
|
'value_date' => 'value_date',
|
|
'stan_no' => 'stan_no',
|
|
'trans_status' => 'trans_status',
|
|
'proc_code' => 'proc_code'
|
|
];
|
|
|
|
private string $period;
|
|
private int $processedCount = 0;
|
|
private int $errorCount = 0;
|
|
|
|
/**
|
|
* Create a new job instance.
|
|
*/
|
|
public function __construct(string $period = '')
|
|
{
|
|
$this->period = $period;
|
|
}
|
|
|
|
/**
|
|
* Execute the job.
|
|
*/
|
|
public function handle()
|
|
: void
|
|
{
|
|
try {
|
|
$this->initializeJob();
|
|
|
|
if ($this->period === '') {
|
|
Log::warning('No period provided for ATM transaction data processing');
|
|
return;
|
|
}
|
|
|
|
$this->processPeriod();
|
|
$this->logJobCompletion();
|
|
} catch (Exception $e) {
|
|
Log::error('Error in ProcessAtmTransactionJob: ' . $e->getMessage());
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
private function initializeJob()
|
|
: void
|
|
{
|
|
set_time_limit(self::MAX_EXECUTION_TIME);
|
|
$this->processedCount = 0;
|
|
$this->errorCount = 0;
|
|
}
|
|
|
|
private function processPeriod()
|
|
: void
|
|
{
|
|
$disk = Storage::disk(self::DISK_NAME);
|
|
$filename = "{$this->period}." . self::FILENAME;
|
|
$filePath = "{$this->period}/$filename";
|
|
|
|
if (!$this->validateFile($disk, $filePath)) {
|
|
return;
|
|
}
|
|
|
|
$tempFilePath = $this->createTemporaryFile($disk, $filePath, $filename);
|
|
$this->processFile($tempFilePath, $filePath);
|
|
$this->cleanup($tempFilePath);
|
|
}
|
|
|
|
private function validateFile($disk, string $filePath)
|
|
: bool
|
|
{
|
|
Log::info("Processing ATM transaction file: $filePath");
|
|
|
|
if (!$disk->exists($filePath)) {
|
|
Log::warning("File not found: $filePath");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function createTemporaryFile($disk, string $filePath, string $filename)
|
|
: string
|
|
{
|
|
$tempFilePath = storage_path("app/temp_$filename");
|
|
file_put_contents($tempFilePath, $disk->get($filePath));
|
|
return $tempFilePath;
|
|
}
|
|
|
|
private function processFile(string $tempFilePath, string $filePath)
|
|
: void
|
|
{
|
|
$handle = fopen($tempFilePath, "r");
|
|
if ($handle === false) {
|
|
Log::error("Unable to open file: $filePath");
|
|
return;
|
|
}
|
|
|
|
// Get the headers from the first row
|
|
$headerRow = fgetcsv($handle, 0, self::CSV_DELIMITER);
|
|
if (!$headerRow) {
|
|
fclose($handle);
|
|
return;
|
|
}
|
|
|
|
$rowCount = 0;
|
|
while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) {
|
|
$rowCount++;
|
|
$this->processRow($headerRow, $row, $rowCount, $filePath);
|
|
}
|
|
|
|
fclose($handle);
|
|
Log::info("Completed processing $filePath. Processed {$this->processedCount} records with {$this->errorCount} errors.");
|
|
}
|
|
|
|
private function processRow(array $headerRow, array $row, int $rowCount, string $filePath)
|
|
: void
|
|
{
|
|
if (count($headerRow) !== count($row)) {
|
|
Log::warning("Row $rowCount in $filePath has incorrect column count. Expected: " .
|
|
count($headerRow) . ", Got: " . count($row));
|
|
return;
|
|
}
|
|
|
|
// Combine the header row with the data row
|
|
$rawData = array_combine($headerRow, $row);
|
|
$this->mapAndSaveRecord($rawData, $rowCount, $filePath);
|
|
}
|
|
|
|
private function mapAndSaveRecord(array $rawData, int $rowCount, string $filePath)
|
|
: void
|
|
{
|
|
// Map the raw data to our model fields
|
|
$data = [];
|
|
foreach (self::HEADER_MAP as $csvField => $modelField) {
|
|
$data[$modelField] = $rawData[$csvField] ?? null;
|
|
}
|
|
|
|
// Skip header row if it was included in the data
|
|
if ($data['transaction_id'] === 'id') {
|
|
return;
|
|
}
|
|
|
|
$this->saveRecord($data, $rowCount, $filePath);
|
|
}
|
|
|
|
private function saveRecord(array $data, int $rowCount, string $filePath)
|
|
: void
|
|
{
|
|
try {
|
|
|
|
// Create or update the record
|
|
AtmTransaction::updateOrCreate(
|
|
['transaction_id' => $data['transaction_id']],
|
|
$data
|
|
);
|
|
|
|
$this->processedCount++;
|
|
} catch (Exception $e) {
|
|
$this->errorCount++;
|
|
Log::error("Error processing row $rowCount in $filePath: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
private function cleanup(string $tempFilePath)
|
|
: void
|
|
{
|
|
if (file_exists($tempFilePath)) {
|
|
unlink($tempFilePath);
|
|
}
|
|
}
|
|
|
|
private function logJobCompletion()
|
|
: void
|
|
{
|
|
Log::info("ATM transaction data processing completed. " .
|
|
"Total processed: {$this->processedCount}, Total errors: {$this->errorCount}");
|
|
}
|
|
}
|