period = $period; Log::info('ProcessProvinceDataJob: Job dibuat untuk periode: ' . $period); } /** * Menjalankan job untuk memproses file ST.PROVINCE.csv * Menggunakan transaction untuk memastikan konsistensi data * * @return void * @throws Exception */ public function handle(): void { DB::beginTransaction(); try { Log::info('ProcessProvinceDataJob: Memulai pemrosesan data provinsi'); $this->initializeJob(); if ($this->period === '') { Log::warning('ProcessProvinceDataJob: Tidak ada periode yang diberikan untuk pemrosesan data provinsi'); DB::rollback(); return; } $this->processPeriod(); $this->logJobCompletion(); DB::commit(); Log::info('ProcessProvinceDataJob: Transaction berhasil di-commit'); } catch (Exception $e) { DB::rollback(); Log::error('ProcessProvinceDataJob: Error dalam pemrosesan, transaction di-rollback: ' . $e->getMessage()); throw $e; } } /** * Inisialisasi pengaturan job * Mengatur timeout dan reset counter * * @return void */ private function initializeJob(): void { set_time_limit(self::MAX_EXECUTION_TIME); $this->processedCount = 0; $this->errorCount = 0; $this->skippedCount = 0; Log::info('ProcessProvinceDataJob: Job diinisialisasi dengan timeout ' . self::MAX_EXECUTION_TIME . ' detik'); } /** * Memproses file untuk periode tertentu * Mengambil file dari SFTP dan memproses data * * @return void */ private function processPeriod(): void { $disk = Storage::disk(self::DISK_NAME); $filePath = "$this->period/" . self::FILENAME; Log::info('ProcessProvinceDataJob: Memproses periode ' . $this->period); if (!$this->validateFile($disk, $filePath)) { return; } $tempFilePath = $this->createTemporaryFile($disk, $filePath); $this->processFile($tempFilePath, $filePath); $this->cleanup($tempFilePath); } /** * Validasi keberadaan file di storage * * @param mixed $disk Storage disk instance * @param string $filePath Path file yang akan divalidasi * @return bool */ private function validateFile($disk, string $filePath): bool { Log::info("ProcessProvinceDataJob: Memvalidasi file provinsi: $filePath"); if (!$disk->exists($filePath)) { Log::warning("ProcessProvinceDataJob: File tidak ditemukan: $filePath"); return false; } Log::info("ProcessProvinceDataJob: File ditemukan dan valid: $filePath"); return true; } /** * Membuat file temporary untuk pemrosesan * * @param mixed $disk Storage disk instance * @param string $filePath Path file sumber * @return string Path file temporary */ private function createTemporaryFile($disk, string $filePath): string { $tempFilePath = storage_path("app/temp_" . self::FILENAME); file_put_contents($tempFilePath, $disk->get($filePath)); Log::info("ProcessProvinceDataJob: File temporary dibuat: $tempFilePath"); return $tempFilePath; } /** * Memproses file CSV dan mengimpor data ke database * Format CSV: id~date_time~province~province_name * * @param string $tempFilePath Path file temporary * @param string $filePath Path file asli untuk logging * @return void */ private function processFile(string $tempFilePath, string $filePath): void { $handle = fopen($tempFilePath, "r"); if ($handle === false) { Log::error("ProcessProvinceDataJob: Tidak dapat membuka file: $filePath"); return; } Log::info("ProcessProvinceDataJob: Memulai pemrosesan file: $filePath"); $rowCount = 0; $isFirstRow = true; while (($row = fgetcsv($handle, 0, self::CSV_DELIMITER)) !== false) { $rowCount++; // Skip header row if ($isFirstRow) { $isFirstRow = false; Log::info("ProcessProvinceDataJob: Melewati header row: " . implode(self::CSV_DELIMITER, $row)); continue; } $this->processRow($row, $rowCount, $filePath); } fclose($handle); Log::info("ProcessProvinceDataJob: Selesai memproses $filePath. Total baris: $rowCount, Diproses: {$this->processedCount}, Error: {$this->errorCount}, Dilewati: {$this->skippedCount}"); } /** * Memproses satu baris data CSV * * @param array $row Data baris CSV * @param int $rowCount Nomor baris untuk logging * @param string $filePath Path file untuk logging * @return void */ private function processRow(array $row, int $rowCount, string $filePath): void { // Validasi jumlah kolom (id~date_time~province~province_name = 4 kolom) if (count($row) !== 4) { Log::warning("ProcessProvinceDataJob: Baris $rowCount di $filePath memiliki jumlah kolom yang salah. Diharapkan: 4, Ditemukan: " . count($row)); $this->skippedCount++; return; } // Map data sesuai format CSV $data = [ 'code' => trim($row[2]), // province code 'name' => trim($row[3]) // province_name ]; Log::debug("ProcessProvinceDataJob: Memproses baris $rowCount dengan data: " . json_encode($data)); $this->saveRecord($data, $rowCount, $filePath); } /** * Menyimpan record provinsi ke database * Menggunakan updateOrCreate untuk menghindari duplikasi * * @param array $data Data provinsi yang akan disimpan * @param int $rowCount Nomor baris untuk logging * @param string $filePath Path file untuk logging * @return void */ private function saveRecord(array $data, int $rowCount, string $filePath): void { try { // Validasi data wajib if (empty($data['code']) || empty($data['name'])) { Log::warning("ProcessProvinceDataJob: Baris $rowCount di $filePath memiliki data kosong. Code: '{$data['code']}', Name: '{$data['name']}'"); $this->skippedCount++; return; } // Simpan atau update data provinsi $province = ProvinceCore::updateOrCreate( ['code' => $data['code']], // Kondisi pencarian ['name' => $data['name']] // Data yang akan diupdate/insert ); $this->processedCount++; Log::debug("ProcessProvinceDataJob: Berhasil menyimpan provinsi ID: {$province->id}, Code: {$data['code']}, Name: {$data['name']}"); } catch (Exception $e) { $this->errorCount++; Log::error("ProcessProvinceDataJob: Error menyimpan data provinsi pada baris $rowCount di $filePath: " . $e->getMessage()); Log::error("ProcessProvinceDataJob: Data yang error: " . json_encode($data)); } } /** * Membersihkan file temporary * * @param string $tempFilePath Path file temporary yang akan dihapus * @return void */ private function cleanup(string $tempFilePath): void { if (file_exists($tempFilePath)) { unlink($tempFilePath); Log::info("ProcessProvinceDataJob: File temporary dihapus: $tempFilePath"); } } /** * Logging hasil akhir pemrosesan job * * @return void */ private function logJobCompletion(): void { $message = "ProcessProvinceDataJob: Pemrosesan data provinsi selesai. " . "Total diproses: {$this->processedCount}, " . "Total error: {$this->errorCount}, " . "Total dilewati: {$this->skippedCount}"; Log::info($message); // Log summary untuk monitoring if ($this->errorCount > 0) { Log::warning("ProcessProvinceDataJob: Terdapat {$this->errorCount} error dalam pemrosesan"); } if ($this->skippedCount > 0) { Log::info("ProcessProvinceDataJob: Terdapat {$this->skippedCount} baris yang dilewati"); } } /** * Handle job failure * * @param Exception $exception * @return void */ public function failed(Exception $exception): void { Log::error('ProcessProvinceDataJob: Job gagal dijalankan: ' . $exception->getMessage()); Log::error('ProcessProvinceDataJob: Stack trace: ' . $exception->getTraceAsString()); } }