option('file'); $filePath = $this->resolvePath($fileOpt); if (!is_file($filePath)) { $this->error("❌ JSON file not found: {$filePath}"); return 1; } $electionId = $this->option('election_id'); $electionId = $electionId !== null && $electionId !== '' ? (int) $electionId : (int) DB::table('voter')->max('election_id'); if ($electionId <= 0) { $this->error("❌ Cannot determine latest election_id (got {$electionId})."); return 1; } $json = file_get_contents($filePath); $rows = json_decode($json, true); if (!is_array($rows)) { $this->error("❌ Invalid JSON: expected array of rows."); return 1; } $map = []; foreach ($rows as $row) { if (!is_array($row)) { continue; } $noAnggota = isset($row['no_anggota']) ? trim((string) $row['no_anggota']) : ''; if ($noAnggota === '') { continue; } $map[$noAnggota] = [ 'pelaburan' => $this->cleanNumber($row['pelaburan'] ?? 0), 'accumulated_amount' => $this->cleanNumber($row['accumulated_amount'] ?? 0), ]; } if (count($map) === 0) { $this->error("❌ No usable rows found in JSON (missing no_anggota?)."); return 1; } $dryRun = (bool) $this->option('dry-run'); $this->info("Target election_id: {$electionId}"); $this->info("JSON members loaded: " . count($map)); $this->info($dryRun ? "Mode: DRY RUN (no DB updates)" : "Mode: UPDATE"); $totalInElection = (int) DB::table('voter')->where('election_id', $electionId)->count(); $this->info("Voters in election: {$totalInElection}"); $updated = 0; $matched = 0; $missingInJson = 0; $unchanged = 0; $bar = $this->output->createProgressBar(max(1, $totalInElection)); $bar->start(); DB::table('voter') ->select(['id', 'no_anggota', 'pelaburan', 'accumulated_amount']) ->where('election_id', $electionId) ->orderBy('id') ->chunkById(500, function ($chunk) use ($map, $dryRun, &$updated, &$matched, &$missingInJson, &$unchanged, $bar) { foreach ($chunk as $voter) { $noAnggota = $voter->no_anggota !== null ? trim((string) $voter->no_anggota) : ''; if ($noAnggota === '' || !array_key_exists($noAnggota, $map)) { $missingInJson++; $bar->advance(); continue; } $matched++; $jsonRow = $map[$noAnggota]; $oldPelaburan = $voter->pelaburan !== null ? (float) $voter->pelaburan : 0.0; $oldAccumulated = $voter->accumulated_amount !== null ? (float) $voter->accumulated_amount : 0.0; if ( abs($oldPelaburan - $jsonRow['pelaburan']) < 0.00001 && abs($oldAccumulated - $jsonRow['accumulated_amount']) < 0.00001 ) { $unchanged++; $bar->advance(); continue; } if (!$dryRun) { DB::table('voter') ->where('id', $voter->id) ->update([ 'pelaburan' => $jsonRow['pelaburan'], 'accumulated_amount' => $jsonRow['accumulated_amount'], 'updated_at' => now(), ]); } $updated++; $bar->advance(); } }, 'id'); $bar->finish(); $this->line(''); $this->info("✅ Matched in JSON: {$matched}"); $this->info("✅ Updated: {$updated}" . ($dryRun ? " (would update)" : "")); $this->info("✅ Unchanged: {$unchanged}"); $this->info("⚠️ Missing in JSON: {$missingInJson}"); return 0; } private function resolvePath(string $path): string { if (strpos($path, DIRECTORY_SEPARATOR) === 0) { return $path; } return base_path($path); } private function cleanNumber($value): float { if ($value === null) { return 0.0; } $value = (string) $value; $value = str_replace(['RM', ',', ' '], '', $value); if (preg_match('/^\((.*)\)$/', $value, $matches)) { $value = $matches[1]; } $value = preg_replace('/[^0-9\.\-]/', '', $value); return is_numeric($value) ? (float) $value : 0.0; } }