[ 'connection' => 'mysql_presseecho', 'portal_enum' => Portal::Presseecho, ], 'businessportal24' => [ 'connection' => 'mysql_businessportal', 'portal_enum' => Portal::Businessportal24, ], ]; private const CHUNK_SIZE = 200; public function handle(): int { $portalOpt = $this->option('portal'); $isDryRun = (bool) $this->option('dry-run'); $isForce = (bool) $this->option('force'); $portals = $portalOpt === 'all' ? array_keys(self::PORTAL_CONFIG) : [$portalOpt]; if (array_diff($portals, array_keys(self::PORTAL_CONFIG)) !== []) { $this->error("Unbekanntes Portal: {$portalOpt}"); return self::FAILURE; } if ($isDryRun) { $this->warn('[DRY-RUN] Kein tatsächlicher Schreibvorgang.'); } $totalImported = 0; $totalSkipped = 0; $totalErrors = 0; $report = [ 'generated_at' => now()->toIso8601String(), 'portal' => $portalOpt, 'dry_run' => $isDryRun, 'force' => $isForce, 'portals' => [], ]; $start = microtime(true); foreach ($portals as $portal) { $config = self::PORTAL_CONFIG[$portal]; $portalReport = $this->archivePortal( $portal, $config['connection'], $isDryRun, $isForce, ); $report['portals'][$portal] = $portalReport; $imported = $portalReport['imported']; $skipped = $portalReport['skipped']; $errors = $portalReport['errors']; $this->line(" [{$portal}] Archiviert: {$imported} | Übersprungen: {$skipped} | Fehler: {$errors}"); $this->line(" [{$portal}] Quelle: {$portalReport['source_count']} | ohne User-Mapping: {$portalReport['unmapped_user_count']} | PDF-Payload: {$portalReport['pdf_payload_count']}"); $totalImported += $imported; $totalSkipped += $skipped; $totalErrors += $errors; } $elapsed = round(microtime(true) - $start, 1); $this->newLine(); $this->info("Gesamt: {$totalImported} archiviert | {$totalSkipped} übersprungen | {$totalErrors} Fehler | {$elapsed}s"); if (! (bool) $this->option('no-report')) { $path = 'migration/legacy-invoices-'.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 $totalErrors > 0 ? self::FAILURE : self::SUCCESS; } private function safeDateOrNull(mixed $value): ?string { if (blank($value) || $value === '0000-00-00' || $value === '0000-00-00 00:00:00') { return null; } return (string) $value; } private function archivePortal( string $portal, string $connection, bool $isDryRun, bool $isForce, ): array { $imported = 0; $skipped = 0; $errors = 0; $unmappedUserCount = 0; $pdfPayloadCount = 0; $amountCents = 0; $statusCounts = []; $hasBillingAddressTable = Schema::connection($connection)->hasTable('invoice_billing_address'); $hasUserPaymentTable = Schema::connection($connection)->hasTable('user_payment'); $hasUserPaymentOptionTable = Schema::connection($connection)->hasTable('user_payment_option'); $hasPaymentOptionTable = Schema::connection($connection)->hasTable('payment_option'); $hasPaymentOptionTranslationTable = Schema::connection($connection)->hasTable('payment_option_translation'); $sourceCount = DB::connection($connection)->table('invoice')->count(); DB::connection($connection) ->table('invoice') ->orderBy('id') ->chunk(self::CHUNK_SIZE, function ($rows) use ( $portal, $connection, $isDryRun, $isForce, $hasBillingAddressTable, $hasUserPaymentTable, $hasUserPaymentOptionTable, $hasPaymentOptionTable, $hasPaymentOptionTranslationTable, &$imported, &$skipped, &$errors, &$unmappedUserCount, &$pdfPayloadCount, &$amountCents, &$statusCounts, ): void { foreach ($rows as $row) { try { $status = (string) ($row->status ?? 'unknown'); $rowAmountCents = (int) round((float) $row->amount * 100); $amountCents += $rowAmountCents; $statusCounts[$status] = ($statusCounts[$status] ?? 0) + 1; $existingInvoice = LegacyInvoice::query() ->where('legacy_portal', $portal) ->where('legacy_id', $row->id) ->first(); if ($existingInvoice && ! $isForce) { $skipped++; continue; } if ($isDryRun) { $imported++; continue; } $userId = null; if ($row->user_id) { $userMap = LegacyImportMap::query() ->where('legacy_portal', $portal) ->where('legacy_table', 'sf_guard_user') ->where('legacy_id', $row->user_id) ->first(); $userId = $userMap?->target_id; } if ($userId === null) { $unmappedUserCount++; } $pdfPayload = $this->pdfPayload( $connection, $row, $hasBillingAddressTable, $hasUserPaymentTable, $hasUserPaymentOptionTable, $hasPaymentOptionTable, $hasPaymentOptionTranslationTable, ); $pdfPayloadCount++; $taxCents = 0; $totalCents = $rowAmountCents; $paidAt = $this->safeDateOrNull($row->pay_date); LegacyInvoice::query()->updateOrCreate( [ 'legacy_portal' => $portal, 'legacy_id' => $row->id, ], [ 'user_id' => $userId, 'legacy_user_id' => $row->user_id, 'number' => (string) $row->number, 'amount_cents' => $rowAmountCents, 'tax_cents' => $taxCents, 'total_cents' => $totalCents, 'status' => $status, 'invoice_date' => $this->safeDateOrNull($row->invoice_date), 'due_date' => $this->safeDateOrNull($row->due_date), 'paid_at' => $paidAt, 'payment_method' => $row->payment_method ?: null, 'pdf_path' => $existingInvoice?->pdf_path, 'raw_snapshot' => (array) $row, 'pdf_payload' => $pdfPayload, 'imported_at' => now(), ] ); $imported++; } catch (\Throwable $e) { $errors++; $this->warn(" ! Invoice {$row->id}: {$e->getMessage()}"); } } }); ksort($statusCounts); return [ 'source_count' => $sourceCount, 'imported' => $imported, 'skipped' => $skipped, 'errors' => $errors, 'unmapped_user_count' => $unmappedUserCount, 'pdf_payload_count' => $pdfPayloadCount, 'amount_cents' => $amountCents, 'status_counts' => $statusCounts, ]; } /** * @return array */ private function pdfPayload( string $connection, object $invoice, bool $hasBillingAddressTable, bool $hasUserPaymentTable, bool $hasUserPaymentOptionTable, bool $hasPaymentOptionTable, bool $hasPaymentOptionTranslationTable, ): array { $userPayment = $hasUserPaymentTable && $invoice->user_payment_id ? $this->legacyRow($connection, 'user_payment', (int) $invoice->user_payment_id) : null; $userPaymentOption = $userPayment && $hasUserPaymentOptionTable ? $this->legacyRow($connection, 'user_payment_option', (int) $userPayment['user_payment_option_id']) : null; $paymentOption = $userPaymentOption && $hasPaymentOptionTable ? $this->legacyRow($connection, 'payment_option', (int) $userPaymentOption['payment_option_id']) : null; return [ 'invoice' => (array) $invoice, 'billing_address' => $hasBillingAddressTable && $invoice->billing_address_id ? $this->legacyRow($connection, 'invoice_billing_address', (int) $invoice->billing_address_id) : null, 'user_payment' => $userPayment, 'user_payment_option' => $userPaymentOption, 'payment_option' => $paymentOption, 'payment_option_translation' => $paymentOption && $hasPaymentOptionTranslationTable ? $this->paymentOptionTranslation($connection, (int) $paymentOption['id']) : null, ]; } /** * @return array|null */ private function paymentOptionTranslation(string $connection, int $paymentOptionId): ?array { $row = DB::connection($connection) ->table('payment_option_translation') ->where('id', $paymentOptionId) ->whereIn('lang', ['de', 'de_DE', 'de_AT', 'de_CH', 'en']) ->orderByRaw("CASE lang WHEN 'de' THEN 0 WHEN 'de_DE' THEN 1 WHEN 'de_AT' THEN 2 WHEN 'de_CH' THEN 3 ELSE 4 END") ->first(); return $row ? (array) $row : null; } /** * @return array|null */ private function legacyRow(string $connection, string $table, int $id): ?array { $row = DB::connection($connection) ->table($table) ->where('id', $id) ->first(); return $row ? (array) $row : null; } }