whereNotNull('canceled') ->whereNotNull('price_canceled') ->orderBy('id'); $ids = $this->option('id'); if ($ids !== []) { $query->whereIn('id', $ids); } $mismatch = []; $fixable = []; foreach ($query->cursor() as $booking) { $pt = (float) $booking->getPriceTotalRaw(); $pc = (float) $booking->getPriceCanceledRaw(); if (abs($pt - $pc) <= self::EPS) { continue; } $hasStornoRow = $booking->booking_strono()->exists(); $hasStornoDoc = $booking->hasDocument('storno'); $mismatch[] = [ 'id' => $booking->id, 'merlin_order_number' => $booking->merlin_order_number, 'price' => $booking->getPriceRaw(), 'price_canceled' => $pc, 'price_total_db' => $pt, 'price_balance' => $booking->getPriceBalanceRaw(), 'canceled_pct' => $booking->getCanceledRaw(), 'booking_storno_row' => $hasStornoRow ? 'ja' : 'nein', 'storno_pdf' => $hasStornoDoc ? 'ja' : 'nein', 'diff' => round($pt - $pc, 2), ]; $fixable[] = $booking->id; } $this->warn('Storno mit gesetztem price_canceled, aber price_total weicht ab:'); $this->newLine(); if ($mismatch === []) { $this->info('Keine Abweichungen gefunden.'); } else { $this->table( ['id', 'MyJack', 'price', 'price_canceled', 'price_total', 'diff', 'storno_row', 'storno_pdf'], collect($mismatch)->map(function ($r) { return [ $r['id'], $r['merlin_order_number'], $r['price'], $r['price_canceled'], $r['price_total_db'], $r['diff'], $r['booking_storno_row'], $r['storno_pdf'], ]; })->all() ); $this->newLine(); $this->line('Anzahl: '.count($mismatch)); } $exportPath = $this->option('export'); if ($exportPath && $mismatch !== []) { $path = $this->resolveExportPath($exportPath); $fp = fopen($path, 'w'); if ($fp === false) { $this->error('Export nicht schreibbar: '.$path); return self::FAILURE; } fputcsv($fp, array_keys($mismatch[0]), ';'); foreach ($mismatch as $row) { fputcsv($fp, $row, ';'); } fclose($fp); $this->info('CSV geschrieben: '.$path); } if ($this->option('fix')) { if ($fixable === []) { $this->info('Nichts zu korrigieren.'); return self::SUCCESS; } if (! $this->confirm('price_total für '.count($fixable).' Buchung(en) auf price_canceled setzen?', true)) { $this->warn('Abgebrochen.'); return self::SUCCESS; } $updated = 0; DB::transaction(function () use ($fixable, &$updated) { foreach ($fixable as $id) { $b = Booking::query()->lockForUpdate()->find($id); if (! $b) { continue; } $pc = (float) $b->getPriceCanceledRaw(); $pt = (float) $b->getPriceTotalRaw(); if (abs($pt - $pc) <= self::EPS) { continue; } $b->price_total = round($pc, 2); $b->save(); $updated++; } }); $this->info("Korrigiert: {$updated} Buchung(en)."); } $this->newLine(); $this->line('Hinweis: Buchungen mit canceled gesetzt, aber price_canceled NULL, werden hier nicht geprüft.'); return self::SUCCESS; } private function resolveExportPath(string $exportPath): string { if ($exportPath[0] === '/' || preg_match('#^[A-Za-z]:\\\\#', $exportPath)) { return $exportPath; } return base_path(trim($exportPath, '/')); } }