'mysql_presseecho', 'businessportal24' => 'mysql_businessportal', ]; private const ENTITY_MAP = [ 'users' => [ 'legacy_table' => 'sf_guard_user', 'target_table' => 'users', 'target_column' => 'legacy_id', ], 'companies' => [ 'legacy_table' => 'company', 'target_table' => 'companies', 'target_column' => 'legacy_id', ], 'contacts' => [ 'legacy_table' => 'contact', 'target_table' => 'contacts', 'target_column' => 'legacy_id', ], 'categories' => [ 'legacy_table' => 'category', 'target_table' => 'categories', 'target_column' => 'legacy_id', ], 'press_releases' => [ 'legacy_table' => 'press_release', 'target_table' => 'press_releases', 'target_column' => 'legacy_id', ], ]; /** * Execute the console command. */ public function handle(): int { $portalOption = (string) $this->option('portal'); $skipLegacy = (bool) $this->option('skip-legacy'); $portals = $portalOption === 'all' ? array_keys(self::PORTAL_CONNECTIONS) : [$portalOption]; if (array_diff($portals, array_keys(self::PORTAL_CONNECTIONS)) !== []) { $this->error("Unbekanntes Portal: {$portalOption}"); return self::FAILURE; } $report = [ 'generated_at' => now()->toIso8601String(), 'portal' => $portalOption, 'skip_legacy' => $skipLegacy, 'checks' => [], 'summary' => [ 'failures' => 0, 'warnings' => 0, ], ]; $this->info('Legacy-Import-Verifikation'); $this->newLine(); if (! $skipLegacy) { $this->verifyLegacyCounts($report, $portals); $this->verifyLegacyInvoices($report, $portals); } else { $this->warn('Legacy-DB-Checks werden übersprungen.'); } $this->verifyImportMapTargets($report, $portals); $this->verifyTargetForeignKeys($report); $this->verifyUserMerges($report); $this->verifyGrandfatheredSubscriptions($report); $this->renderSummary($report); if (! (bool) $this->option('no-report')) { $path = 'migration/verify-'.now()->format('Ymd-His').'.json'; Storage::disk('local')->put($path, json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); $this->line("Report geschrieben: storage/app/private/{$path}"); } return $report['summary']['failures'] > 0 ? self::FAILURE : self::SUCCESS; } /** * @param array $report * @param list $portals */ private function verifyLegacyCounts(array &$report, array $portals): void { $rows = []; foreach ($portals as $portal) { foreach (self::ENTITY_MAP as $entity => $config) { $legacyCount = $this->legacyCount($portal, $config['legacy_table']); $targetCount = DB::table($config['target_table']) ->where('legacy_portal', $portal) ->whereNotNull($config['target_column']) ->count(); $mappedCount = DB::table('legacy_import_map') ->where('legacy_portal', $portal) ->where('legacy_table', $config['legacy_table']) ->where('target_table', $config['target_table']) ->count(); $rows[] = [ 'portal' => $portal, 'entity' => $entity, 'legacy_count' => $legacyCount, 'target_count' => $targetCount, 'mapped_count' => $mappedCount, 'delta' => $legacyCount - $targetCount, ]; } } $this->addCheck($report, 'legacy_counts', 'ok', 'Legacy- und Ziel-Counts erfasst.', $rows); } /** * @param array $report * @param list $portals */ private function verifyLegacyInvoices(array &$report, array $portals): void { $rows = []; $failures = 0; foreach ($portals as $portal) { $legacy = DB::connection(self::PORTAL_CONNECTIONS[$portal]) ->table('invoice') ->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as amount') ->first(); $target = DB::table('legacy_invoices') ->where('legacy_portal', $portal) ->selectRaw('COUNT(*) as count, COALESCE(SUM(amount_cents), 0) as amount_cents') ->first(); $legacyAmountCents = (int) round((float) $legacy->amount * 100); $targetAmountCents = (int) $target->amount_cents; $countDelta = (int) $legacy->count - (int) $target->count; $amountDelta = $legacyAmountCents - $targetAmountCents; if ($countDelta !== 0 || $amountDelta !== 0) { $failures++; } $rows[] = [ 'portal' => $portal, 'legacy_count' => (int) $legacy->count, 'target_count' => (int) $target->count, 'count_delta' => $countDelta, 'legacy_amount_cents' => $legacyAmountCents, 'target_amount_cents' => $targetAmountCents, 'amount_delta_cents' => $amountDelta, ]; } $this->addCheck( $report, 'legacy_invoices', $failures === 0 ? 'ok' : 'fail', $failures === 0 ? 'Legacy-Rechnungen stimmen mit dem Archiv überein.' : 'Legacy-Rechnungsarchiv weicht von der Quelle ab.', $rows, ); } /** * @param array $report * @param list $portals */ private function verifyImportMapTargets(array &$report, array $portals): void { $rows = []; $failures = 0; foreach (self::ENTITY_MAP as $entity => $config) { foreach ($portals as $portal) { $missingTargets = DB::table('legacy_import_map as map') ->leftJoin($config['target_table'].' as target', 'target.id', '=', 'map.target_id') ->where('map.legacy_portal', $portal) ->where('map.legacy_table', $config['legacy_table']) ->where('map.target_table', $config['target_table']) ->whereNull('target.id') ->count(); if ($missingTargets > 0) { $failures++; } $rows[] = [ 'portal' => $portal, 'entity' => $entity, 'missing_targets' => $missingTargets, ]; } } $this->addCheck( $report, 'import_map_targets', $failures === 0 ? 'ok' : 'fail', $failures === 0 ? 'Alle Import-Map-Ziele existieren.' : 'Import-Map enthält verwaiste Ziel-IDs.', $rows, ); } /** * @param array $report */ private function verifyTargetForeignKeys(array &$report): void { $checks = [ 'companies.owner_user_id' => DB::table('companies as c') ->leftJoin('users as u', 'u.id', '=', 'c.owner_user_id') ->whereNotNull('c.owner_user_id') ->whereNull('u.id'), 'contacts.company_id' => DB::table('contacts as c') ->leftJoin('companies as co', 'co.id', '=', 'c.company_id') ->whereNull('co.id'), 'press_releases.user_id' => DB::table('press_releases as pr') ->leftJoin('users as u', 'u.id', '=', 'pr.user_id') ->whereNull('u.id'), 'press_releases.company_id' => DB::table('press_releases as pr') ->leftJoin('companies as c', 'c.id', '=', 'pr.company_id') ->whereNotNull('pr.company_id') ->whereNull('c.id'), 'press_releases.category_id' => DB::table('press_releases as pr') ->leftJoin('categories as c', 'c.id', '=', 'pr.category_id') ->whereNull('c.id'), 'company_user.company_id' => DB::table('company_user as pivot') ->leftJoin('companies as c', 'c.id', '=', 'pivot.company_id') ->whereNull('c.id'), 'company_user.user_id' => DB::table('company_user as pivot') ->leftJoin('users as u', 'u.id', '=', 'pivot.user_id') ->whereNull('u.id'), 'contact_user.contact_id' => DB::table('contact_user as pivot') ->leftJoin('contacts as c', 'c.id', '=', 'pivot.contact_id') ->whereNull('c.id'), 'contact_user.user_id' => DB::table('contact_user as pivot') ->leftJoin('users as u', 'u.id', '=', 'pivot.user_id') ->whereNull('u.id'), 'press_release_contact.press_release_id' => DB::table('press_release_contact as pivot') ->leftJoin('press_releases as pr', 'pr.id', '=', 'pivot.press_release_id') ->whereNull('pr.id'), 'press_release_contact.contact_id' => DB::table('press_release_contact as pivot') ->leftJoin('contacts as c', 'c.id', '=', 'pivot.contact_id') ->whereNull('c.id'), 'legacy_invoices.user_id' => DB::table('legacy_invoices as li') ->leftJoin('users as u', 'u.id', '=', 'li.user_id') ->whereNotNull('li.user_id') ->whereNull('u.id'), ]; $rows = []; $failures = 0; foreach ($checks as $name => $query) { $count = $query->count(); if ($count > 0) { $failures++; } $rows[] = [ 'relation' => $name, 'dangling_count' => $count, ]; } $this->addCheck( $report, 'target_foreign_keys', $failures === 0 ? 'ok' : 'fail', $failures === 0 ? 'Keine verwaisten Ziel-Foreign-Keys gefunden.' : 'Zieltabellen enthalten verwaiste Foreign Keys.', $rows, ); } /** * @param array $report */ private function verifyUserMerges(array &$report): void { $bothUsers = DB::table('users') ->where('portal', Portal::Both->value) ->count(); $importMapsForBothUsers = DB::table('legacy_import_map as map') ->join('users as users', 'users.id', '=', 'map.target_id') ->where('map.legacy_table', 'sf_guard_user') ->where('map.target_table', 'users') ->where('users.portal', Portal::Both->value) ->count(); $this->addCheck($report, 'user_merges', 'ok', 'Portalübergreifende User-Merges erfasst.', [ 'users_with_portal_both' => $bothUsers, 'legacy_maps_to_portal_both_users' => $importMapsForBothUsers, ]); } /** * @param array $report */ private function verifyGrandfatheredSubscriptions(array &$report): void { $rows = DB::table('user_payment_options') ->selectRaw('status, COUNT(*) as count, MIN(grandfathered_until) as earliest_grandfathered_until, MAX(grandfathered_until) as latest_grandfathered_until') ->where('status', 'grandfathered') ->groupBy('status') ->get() ->map(fn (object $row): array => [ 'status' => $row->status, 'count' => (int) $row->count, 'earliest_grandfathered_until' => $row->earliest_grandfathered_until, 'latest_grandfathered_until' => $row->latest_grandfathered_until, ]) ->all(); $this->addCheck($report, 'grandfathered_subscriptions', 'ok', 'Grandfathered Subscriptions erfasst.', $rows); } private function legacyCount(string $portal, string $table): int { return DB::connection(self::PORTAL_CONNECTIONS[$portal]) ->table($table) ->count(); } /** * @param array $report * @param array $details */ private function addCheck(array &$report, string $name, string $status, string $message, array $details): void { $report['checks'][$name] = [ 'status' => $status, 'message' => $message, 'details' => $details, ]; if ($status === 'fail') { $report['summary']['failures']++; } if ($status === 'warning') { $report['summary']['warnings']++; } } /** * @param array $report */ private function renderSummary(array $report): void { foreach ($report['checks'] as $name => $check) { $status = match ($check['status']) { 'ok' => 'OK', 'warning' => 'WARN', default => 'FAIL', }; $this->line("{$status} {$name}: {$check['message']}"); } $this->newLine(); $this->table( ['Status', 'Anzahl'], [ ['Fehler', $report['summary']['failures']], ['Warnungen', $report['summary']['warnings']], ], ); } }