From d5495d721e44833aa406a352f36feebeae45f16b Mon Sep 17 00:00:00 2001 From: Daeng Deni Mardaeni Date: Wed, 21 May 2025 22:14:58 +0700 Subject: [PATCH] feat(webstatement): tambah pemrosesan data ATM Transaction - Menambahkan job `ProcessAtmTransactionJob` untuk memproses data transaksi ATM dari file CSV. - Implementasi pemrosesan file CSV termasuk pembacaan, pemetaan header, dan simpan data ke model. - Menyediakan logging untuk pemantauan jumlah data yang diproses dan jumlah error. - Menambahkan mekanisme penanganan error pada setiap proses baris dan file. - Menambahkan model `AtmTransaction`: - Mendeklarasikan atribut yang bisa diisi (`fillable`) seperti `transaction_id`, `txn_amount`, dan lainnya. - Mendefinisikan tipe data casting untuk beberapa atribut seperti `txn_amount` dalam tipe decimal dan `booking_date` dalam tipe datetime. - Menambahkan migration `2025_05_21_150736_create_atm_transactions_table` untuk tabel `atm_transactions`: - Tabel memiliki kolom seperti `transaction_id`, `txn_amount`, `booking_date`, dan indeks untuk kolom tertentu. - Meng-handle struktur kolom untuk mendukung atribut yang diperlukan di model. - Memperbarui `MigrasiController`: - Menambahkan fungsi `ProcessAtmTransaction` untuk menjadwalkan `ProcessAtmTransactionJob`. - Memperbaiki pesan response pada beberapa fungsi agar lebih deskriptif dan konsisten. - Memperbarui pemanggilan fungsi dari `__invoke` di bagian pemrosesan data (`ProcessAtmTransaction`) untuk period tertentu. Signed-off-by: Daeng Deni Mardaeni --- app/Http/Controllers/MigrasiController.php | 23 +- app/Jobs/ProcessAtmTransactionJob.php | 235 ++++++++++++++++++ app/Models/AtmTransaction.php | 45 ++++ ...1_150736_create_atm_transactions_table.php | 45 ++++ 4 files changed, 342 insertions(+), 6 deletions(-) create mode 100644 app/Jobs/ProcessAtmTransactionJob.php create mode 100644 app/Models/AtmTransaction.php create mode 100644 database/migrations/2025_05_21_150736_create_atm_transactions_table.php diff --git a/app/Http/Controllers/MigrasiController.php b/app/Http/Controllers/MigrasiController.php index 1d46fcb..2b29bc2 100644 --- a/app/Http/Controllers/MigrasiController.php +++ b/app/Http/Controllers/MigrasiController.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Storage; use Log; use Modules\Webstatement\Jobs\ProcessAccountDataJob; use Modules\Webstatement\Jobs\ProcessArrangementDataJob; +use Modules\Webstatement\Jobs\ProcessAtmTransactionJob; use Modules\Webstatement\Jobs\ProcessBillDetailDataJob; use Modules\Webstatement\Jobs\ProcessCategoryDataJob; use Modules\Webstatement\Jobs\ProcessCompanyDataJob; @@ -110,7 +111,7 @@ class MigrasiController extends Controller public function ProcessFtTxnTypeConditioData($periods){ try { ProcessFtTxnTypeConditionJob::dispatch($periods); - return response()->json(['message' => 'Data FtTxnTypeCondition processing job has been successfully']); + return response()->json(['message' => 'FtTxnTypeCondition processing job has been successfully']); } catch (Exception $e) { return response()->json(['error' => $e->getMessage()], 500); } @@ -119,7 +120,7 @@ class MigrasiController extends Controller public function processStmtEntryData($periods){ try { ProcessStmtEntryDataJob::dispatch($periods); - return response()->json(['message' => 'Data TempStmtEntry processing job has been successfully']); + return response()->json(['message' => 'Stmt Entry processing job has been successfully']); } catch (Exception $e) { return response()->json(['error' => $e->getMessage()], 500); } @@ -129,7 +130,7 @@ class MigrasiController extends Controller public function ProcessCompanyData($periods){ try { ProcessCompanyDataJob::dispatch($periods); - return response()->json(['message' => 'Data TempStmtEntry processing job has been successfully']); + return response()->json(['message' => 'Company processing job has been successfully']); } catch (Exception $e) { return response()->json(['error' => $e->getMessage()], 500); } @@ -138,7 +139,7 @@ class MigrasiController extends Controller public function ProcessDataCaptureData($periods){ try { ProcessDataCaptureDataJob::dispatch($periods); - return response()->json(['message' => 'Data TempStmtEntry processing job has been successfully']); + return response()->json(['message' => 'Data Capture processing job has been successfully']); } catch (Exception $e) { return response()->json(['error' => $e->getMessage()], 500); } @@ -147,7 +148,7 @@ class MigrasiController extends Controller public function ProcessCategoryData($periods){ try { ProcessCategoryDataJob::dispatch($periods); - return response()->json(['message' => 'Data TempStmtEntry processing job has been successfully']); + return response()->json(['message' => 'Category processing job has been successfully']); } catch (Exception $e) { return response()->json(['error' => $e->getMessage()], 500); } @@ -156,7 +157,16 @@ class MigrasiController extends Controller public function ProcessTellerData($periods){ try { ProcessTellerDataJob::dispatch($periods); - return response()->json(['message' => 'Data TempStmtEntry processing job has been successfully']); + return response()->json(['message' => 'Teller processing job has been successfully']); + } catch (Exception $e) { + return response()->json(['error' => $e->getMessage()], 500); + } + } + + public function ProcessAtmTransaction($periods){ + try { + ProcessAtmTransactionJob::dispatch($periods); + return response()->json(['message' => 'AtmTransaction processing job has been successfully']); } catch (Exception $e) { return response()->json(['error' => $e->getMessage()], 500); } @@ -198,6 +208,7 @@ class MigrasiController extends Controller //$this->ProcessDataCaptureData($periods); //$this->processFundsTransferData($periods); $this->ProcessTellerData($periods); + $this->ProcessAtmTransaction($periods); //$this->processArrangementData($periods); //$this->processBillDetailData($periods); diff --git a/app/Jobs/ProcessAtmTransactionJob.php b/app/Jobs/ProcessAtmTransactionJob.php new file mode 100644 index 0000000..1669c87 --- /dev/null +++ b/app/Jobs/ProcessAtmTransactionJob.php @@ -0,0 +1,235 @@ + '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' + ]; + + // Pemetaan bidang header ke kolom model + protected array $periods; + + /** + * Create a new job instance. + */ + public function __construct(array $periods = []) + { + $this->periods = $periods; + } + + /** + * Execute the job. + */ + public function handle(): void + { + try { + set_time_limit(24 * 60 * 60); + + if (empty($this->periods)) { + Log::warning('No periods provided for ATM transaction data processing'); + return; + } + + $stats = $this->processPeriods(); + + Log::info("ProcessAtmTransactionJob completed. Total processed: {$stats['processed']}, Total errors: {$stats['errors']}"); + } catch (Exception $e) { + Log::error("Error in ProcessAtmTransactionJob: " . $e->getMessage()); + throw $e; + } + } + + /** + * Process all periods and return statistics + */ + private function processPeriods(): array + { + $disk = Storage::disk(self::DISK_NAME); + $processedCount = 0; + $errorCount = 0; + + foreach ($this->periods as $period) { + // Skip the parameter folder + if ($period === self::PARAMETER_FOLDER) { + Log::info("Skipping " . self::PARAMETER_FOLDER . " folder"); + continue; + } + + $result = $this->processPeriodFile($disk, $period); + $processedCount += $result['processed']; + $errorCount += $result['errors']; + } + + return [ + 'processed' => $processedCount, + 'errors' => $errorCount + ]; + } + + /** + * Process a single period file + */ + private function processPeriodFile($disk, string $period): array + { + $filename = $period . self::FILE_EXTENSION; + $filePath = "$period/$filename"; + $processedCount = 0; + $errorCount = 0; + + Log::info("Processing ATM transaction file: $filePath"); + + if (!$disk->exists($filePath)) { + Log::warning("File not found: $filePath"); + return ['processed' => 0, 'errors' => 0]; + } + + $tempFilePath = $this->createTempFile($disk, $filePath, $filename); + + $result = $this->processCSVFile($tempFilePath, $filePath); + $processedCount += $result['processed']; + $errorCount += $result['errors']; + + // Clean up the temporary file + if (file_exists($tempFilePath)) { + unlink($tempFilePath); + } + + Log::info("Completed processing $filePath. Processed {$result['processed']} records with {$result['errors']} errors."); + + return [ + 'processed' => $processedCount, + 'errors' => $errorCount + ]; + } + + /** + * Create a temporary file for processing + */ + private function createTempFile($disk, string $filePath, string $filename): string + { + $tempFilePath = storage_path("app/temp_$filename"); + file_put_contents($tempFilePath, $disk->get($filePath)); + return $tempFilePath; + } + + /** + * Process a CSV file and import data + */ + private function processCSVFile(string $tempFilePath, string $originalFilePath): array + { + $processedCount = 0; + $errorCount = 0; + + $handle = fopen($tempFilePath, "r"); + if ($handle === false) { + Log::error("Unable to open file: $originalFilePath"); + return ['processed' => 0, 'errors' => 0]; + } + + // Get the headers from the first row + $headerRow = fgetcsv($handle, 0, self::CSV_DELIMITER); + if (!$headerRow) { + fclose($handle); + return ['processed' => 0, 'errors' => 0]; + } + + $rowCount = 0; + while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) { + $rowCount++; + + if (count($headerRow) !== count($row)) { + Log::warning("Row $rowCount in $originalFilePath has incorrect column count. Expected: " . count($headerRow) . ", Got: " . count($row)); + continue; + } + + $result = $this->processRow($headerRow, $row, $rowCount, $originalFilePath); + $processedCount += $result['processed']; + $errorCount += $result['errors']; + } + + fclose($handle); + + return [ + 'processed' => $processedCount, + 'errors' => $errorCount + ]; + } + + /** + * Process a single row from the CSV file + */ + private function processRow(array $headerRow, array $row, int $rowCount, string $filePath): array + { + // Combine the header row with the data row + $rawData = array_combine($headerRow, $row); + + // 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 ['processed' => 0, 'errors' => 0]; + } + + try { + // Format dates if needed + /*if (!empty($data['booking_date'])) { + $data['booking_date'] = date('Y-m-d H:i:s', strtotime($data['booking_date'])); + } + + if (!empty($data['value_date'])) { + $data['value_date'] = date('Y-m-d H:i:s', strtotime($data['value_date'])); + }*/ + + // Create or update the record + AtmTransaction::updateOrCreate( + ['transaction_id' => $data['transaction_id']], + $data + ); + + return ['processed' => 1, 'errors' => 0]; + } catch (Exception $e) { + Log::error("Error processing row $rowCount in $filePath: " . $e->getMessage()); + return ['processed' => 0, 'errors' => 1]; + } + } + } diff --git a/app/Models/AtmTransaction.php b/app/Models/AtmTransaction.php new file mode 100644 index 0000000..312eb55 --- /dev/null +++ b/app/Models/AtmTransaction.php @@ -0,0 +1,45 @@ + + */ + protected $fillable = [ + 'transaction_id', + 'card_acc_id', + 'pan_number', + 'txn_type', + 'merchant_id', + 'txn_amount', + 'booking_date', + 'trans_ref', + 'retrieval_ref_no', + 'stmt_nos', + 'debit_acct_no', + 'credit_acct_no', + 'chrg_amount', + 'value_date', + 'stan_no', + 'trans_status', + 'proc_code', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'booking_date' => 'datetime', + 'value_date' => 'datetime', + 'txn_amount' => 'decimal:2', + 'chrg_amount' => 'decimal:2', + ]; + } diff --git a/database/migrations/2025_05_21_150736_create_atm_transactions_table.php b/database/migrations/2025_05_21_150736_create_atm_transactions_table.php new file mode 100644 index 0000000..b1f23db --- /dev/null +++ b/database/migrations/2025_05_21_150736_create_atm_transactions_table.php @@ -0,0 +1,45 @@ +id(); + $table->string('transaction_id')->nullable()->index(); + $table->string('card_acc_id')->nullable(); + $table->string('pan_number')->nullable(); + $table->string('txn_type')->nullable(); + $table->string('merchant_id')->nullable(); + $table->string('txn_amount')->nullable(); + $table->string('booking_date')->nullable(); + $table->string('trans_ref')->nullable(); + $table->string('retrieval_ref_no')->nullable(); + $table->string('stmt_nos')->nullable(); + $table->string('debit_acct_no')->nullable(); + $table->string('credit_acct_no')->nullable(); + $table->string('chrg_amount')->nullable(); + $table->string('value_date')->nullable(); + $table->string('stan_no')->nullable(); + $table->string('trans_status')->nullable(); + $table->string('proc_code')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + : void + { + Schema::dropIfExists('atm_transactions'); + } + };