*/ public function sync(bool $allowFallback = true, ?bool $deactivateMissing = null): array { if (! (bool) config('skinbase-countries.enabled', true)) { throw new RuntimeException('Countries sync is disabled by configuration.'); } $summary = [ 'source' => null, 'total_fetched' => 0, 'inserted' => 0, 'updated' => 0, 'skipped' => 0, 'invalid' => 0, 'deactivated' => 0, 'backfilled_users' => 0, ]; try { $records = $this->remoteProvider->fetchAll(); $summary['source'] = (string) config('skinbase-countries.remote_source', 'remote'); } catch (Throwable $exception) { if (! $allowFallback || ! (bool) config('skinbase-countries.fallback_seed_enabled', true)) { throw new RuntimeException('Country sync failed: '.$exception->getMessage(), previous: $exception); } $records = $this->loadFallbackRecords(); $summary['source'] = 'fallback'; } if ($records === []) { throw new RuntimeException('Country sync did not yield any valid country records.'); } $summary['total_fetched'] = count($records); $seenIso2 = []; $featured = array_values(array_filter(array_map( static fn (mixed $iso2): string => strtoupper(trim((string) $iso2)), (array) config('skinbase-countries.featured_countries', []), ))); $featuredOrder = array_flip($featured); DB::transaction(function () use (&$summary, $records, &$seenIso2, $featuredOrder, $deactivateMissing): void { foreach ($records as $record) { $iso2 = strtoupper((string) ($record['iso2'] ?? '')); if (! preg_match('/^[A-Z]{2}$/', $iso2)) { $summary['invalid']++; continue; } if (isset($seenIso2[$iso2])) { $summary['skipped']++; continue; } $seenIso2[$iso2] = true; $country = Country::query()->firstOrNew(['iso2' => $iso2]); $exists = $country->exists; $featuredIndex = $featuredOrder[$iso2] ?? null; $country->fill([ 'iso' => $iso2, 'iso3' => $record['iso3'] ?? null, 'numeric_code' => $record['numeric_code'] ?? null, 'name' => $record['name_common'], 'native' => $record['name_official'] ?? null, 'continent' => $this->continentCode($record['region'] ?? null), 'name_common' => $record['name_common'], 'name_official' => $record['name_official'] ?? null, 'region' => $record['region'] ?? null, 'subregion' => $record['subregion'] ?? null, 'flag_svg_url' => $record['flag_svg_url'] ?? null, 'flag_png_url' => $record['flag_png_url'] ?? null, 'flag_emoji' => $record['flag_emoji'] ?? null, 'active' => true, 'is_featured' => $featuredIndex !== null, 'sort_order' => $featuredIndex !== null ? $featuredIndex + 1 : 1000, ]); if (! $exists) { $country->save(); $summary['inserted']++; continue; } if ($country->isDirty()) { $country->save(); $summary['updated']++; continue; } $summary['skipped']++; } if ($deactivateMissing ?? (bool) config('skinbase-countries.deactivate_missing', false)) { $summary['deactivated'] = Country::query() ->where('active', true) ->whereNotIn('iso2', array_keys($seenIso2)) ->update(['active' => false]); } }); $summary['backfilled_users'] = $this->backfillUsersFromLegacyProfileCodes(); $this->catalog->flushCache(); return $summary; } /** * @return array> */ private function loadFallbackRecords(): array { $path = (string) config('skinbase-countries.fallback_seed_path', database_path('data/countries-fallback.json')); if (! is_file($path)) { throw new RuntimeException('Country fallback dataset is missing.'); } try { $decoded = json_decode((string) file_get_contents($path), true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $exception) { throw new RuntimeException('Country fallback dataset is invalid JSON.', previous: $exception); } if (! is_array($decoded)) { throw new RuntimeException('Country fallback dataset is not a JSON array.'); } return $this->remoteProvider->normalizePayload($decoded); } private function backfillUsersFromLegacyProfileCodes(): int { if (! Schema::hasTable('user_profiles') || ! Schema::hasTable('users') || ! Schema::hasColumn('users', 'country_id')) { return 0; } $rows = DB::table('users as users') ->join('user_profiles as profiles', 'profiles.user_id', '=', 'users.id') ->join('countries as countries', 'countries.iso2', '=', 'profiles.country_code') ->whereNull('users.country_id') ->whereNotNull('profiles.country_code') ->select(['users.id as user_id', 'countries.id as country_id']) ->get(); foreach ($rows as $row) { DB::table('users') ->where('id', (int) $row->user_id) ->update(['country_id' => (int) $row->country_id]); } return $rows->count(); } private function continentCode(?string $region): ?string { return Arr::get([ 'Africa' => 'AF', 'Americas' => 'AM', 'Asia' => 'AS', 'Europe' => 'EU', 'Oceania' => 'OC', 'Antarctic' => 'AN', ], trim((string) $region)); } }