syncLogId = $syncLogId; $this->batchSize = $batchSize > 0 ? $batchSize : self::BATCH_SIZE; $this->filters = $filters; } /** * Execute the job untuk update seluruh kartu ATM * * @return void * @throws Exception */ public function handle(): void { set_time_limit(self::MAX_EXECUTION_TIME); Log::info('Memulai job update seluruh kartu ATM', [ 'sync_log_id' => $this->syncLogId, 'batch_size' => $this->batchSize, 'filters' => $this->filters ]); try { DB::beginTransaction(); // Load atau buat log sinkronisasi $this->loadOrCreateSyncLog(); // Update status job dimulai $this->updateJobStartStatus(); // Ambil total kartu yang akan diproses $totalCards = $this->getTotalCardsCount(); if ($totalCards === 0) { Log::info('Tidak ada kartu ATM yang perlu diupdate'); $this->updateJobCompletedStatus(0, 0); DB::commit(); return; } Log::info("Ditemukan {$totalCards} kartu ATM yang akan diproses"); // Proses kartu dalam batch $processedCount = $this->processCardsInBatches($totalCards); // Update status job selesai $this->updateJobCompletedStatus($totalCards, $processedCount); Log::info('Job update seluruh kartu ATM selesai', [ 'total_cards' => $totalCards, 'processed_count' => $processedCount, 'sync_log_id' => $this->syncLog->id ]); DB::commit(); } catch (Exception $e) { DB::rollBack(); $this->updateJobFailedStatus($e->getMessage()); Log::error('Gagal menjalankan job update seluruh kartu ATM: ' . $e->getMessage(), [ 'file' => $e->getFile(), 'line' => $e->getLine(), 'sync_log_id' => $this->syncLogId, 'trace' => $e->getTraceAsString() ]); throw $e; } } /** * Load atau buat log sinkronisasi baru * * @return void * @throws Exception */ private function loadOrCreateSyncLog(): void { Log::info('Loading atau membuat sync log', ['sync_log_id' => $this->syncLogId]); if ($this->syncLogId) { $this->syncLog = KartuSyncLog::find($this->syncLogId); if (!$this->syncLog) { throw new Exception("Sync log dengan ID {$this->syncLogId} tidak ditemukan"); } } else { // Buat log sinkronisasi baru $this->syncLog = KartuSyncLog::create([ 'periode' => now()->format('Y-m'), 'sync_notes' => 'Batch update seluruh kartu ATM dimulai', 'is_sync' => false, 'sync_at' => null, 'is_csv' => false, 'csv_at' => null, 'is_ftp' => false, 'ftp_at' => null ]); } Log::info('Sync log berhasil dimuat/dibuat', ['sync_log_id' => $this->syncLog->id]); } /** * Update status saat job dimulai * * @return void */ private function updateJobStartStatus(): void { Log::info('Memperbarui status job dimulai'); $this->syncLog->update([ 'sync_notes' => $this->syncLog->sync_notes . "\nBatch update seluruh kartu ATM dimulai pada " . now()->format('Y-m-d H:i:s'), 'is_sync' => false, 'sync_at' => null ]); } /** * Ambil total jumlah kartu yang akan diproses * * @return int */ private function getTotalCardsCount(): int { Log::info('Menghitung total kartu yang akan diproses', ['filters' => $this->filters]); $query = $this->buildCardQuery(); $count = $query->count(); Log::info("Total kartu ditemukan: {$count}"); return $count; } /** * Build query untuk mengambil kartu berdasarkan filter * * @return \Illuminate\Database\Eloquent\Builder */ private function buildCardQuery() { $query = Atmcard::where('crsts', 1) // Kartu aktif ->whereNotNull('accflag') ->where('accflag', '!=', ''); // Terapkan filter default untuk kartu yang perlu update branch/currency if (empty($this->filters) || !isset($this->filters['skip_branch_currency_filter'])) { $query->where(function ($q) { $q->whereNull('branch') ->orWhere('branch', '') ->orWhereNull('currency') ->orWhere('currency', ''); }); } // Terapkan filter tambahan jika ada if (!empty($this->filters)) { foreach ($this->filters as $field => $value) { if ($field === 'skip_branch_currency_filter') { continue; } if (is_array($value)) { $query->whereIn($field, $value); } else { $query->where($field, $value); } } } return $query; } /** * Proses kartu dalam batch * * @param int $totalCards * @return int Jumlah kartu yang berhasil diproses */ private function processCardsInBatches(int $totalCards): int { Log::info('Memulai pemrosesan kartu dalam batch', [ 'total_cards' => $totalCards, 'batch_size' => $this->batchSize ]); $processedCount = 0; $batchNumber = 1; $totalBatches = ceil($totalCards / $this->batchSize); // Proses kartu dalam chunk/batch $this->buildCardQuery()->chunk($this->batchSize, function ($cards) use (&$processedCount, &$batchNumber, $totalBatches, $totalCards) { Log::info("Memproses batch {$batchNumber}/{$totalBatches}", [ 'cards_in_batch' => $cards->count(), 'processed_so_far' => $processedCount ]); try { // Dispatch job untuk setiap kartu dalam batch dengan delay foreach ($cards as $index => $card) { // Hitung delay berdasarkan nomor batch dan index untuk menyebar eksekusi job $delay = (($batchNumber - 1) * $this->batchSize + $index) % self::MAX_DELAY_SPREAD; $delay += self::DELAY_BETWEEN_JOBS; // Tambah delay minimum // Dispatch job UpdateAtmCardBranchCurrencyJob UpdateAtmCardBranchCurrencyJob::dispatch($card, $this->syncLog->id) ->delay(now()->addSeconds($delay)) ->onQueue('default'); $processedCount++; } // Update progress di log setiap 10 batch if ($batchNumber % 10 === 0) { $this->updateProgressStatus($processedCount, $totalCards, $batchNumber, $totalBatches); } Log::info("Batch {$batchNumber} berhasil dijadwalkan", [ 'cards_scheduled' => $cards->count(), 'total_processed' => $processedCount ]); } catch (Exception $e) { Log::error("Error saat memproses batch {$batchNumber}: " . $e->getMessage(), [ 'batch_number' => $batchNumber, 'cards_count' => $cards->count(), 'error' => $e->getMessage() ]); throw $e; } $batchNumber++; }); Log::info('Selesai memproses semua batch', [ 'total_processed' => $processedCount, 'total_batches' => $batchNumber - 1 ]); return $processedCount; } /** * Update status progress pemrosesan * * @param int $processedCount * @param int $totalCards * @param int $batchNumber * @param int $totalBatches * @return void */ private function updateProgressStatus(int $processedCount, int $totalCards, int $batchNumber, int $totalBatches): void { Log::info('Memperbarui status progress', [ 'processed' => $processedCount, 'total' => $totalCards, 'batch' => $batchNumber, 'total_batches' => $totalBatches ]); $percentage = round(($processedCount / $totalCards) * 100, 2); $progressNote = "\nProgress: {$processedCount}/{$totalCards} kartu dijadwalkan ({$percentage}%) - Batch {$batchNumber}/{$totalBatches}"; $this->syncLog->update([ 'sync_notes' => $this->syncLog->sync_notes . $progressNote ]); } /** * Update status saat job selesai * * @param int $totalCards * @param int $processedCount * @return void */ private function updateJobCompletedStatus(int $totalCards, int $processedCount): void { Log::info('Memperbarui status job selesai', [ 'total_cards' => $totalCards, 'processed_count' => $processedCount ]); $completionNote = "\nBatch update selesai pada " . now()->format('Y-m-d H:i:s') . " - Total {$processedCount} kartu dari {$totalCards} berhasil dijadwalkan untuk update"; $this->syncLog->update([ 'is_sync' => true, 'sync_at' => now(), 'sync_notes' => $this->syncLog->sync_notes . $completionNote ]); } /** * Update status saat job gagal * * @param string $errorMessage * @return void */ private function updateJobFailedStatus(string $errorMessage): void { Log::error('Memperbarui status job gagal', ['error' => $errorMessage]); if ($this->syncLog) { $failureNote = "\nBatch update gagal pada " . now()->format('Y-m-d H:i:s') . " - Error: {$errorMessage}"; $this->syncLog->update([ 'is_sync' => false, 'sync_notes' => $this->syncLog->sync_notes . $failureNote ]); } } }