156 lines
5.4 KiB
PHP
156 lines
5.4 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\Booking;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* Findet Buchungen, bei denen nach Storno price_total nicht mehr zu price_canceled passt
|
|
* (typisch nach Speichern / calculate_price_total ohne Storno-Schutz).
|
|
*
|
|
* php artisan bookings:audit-storno-price-total
|
|
* php artisan bookings:audit-storno-price-total --export=/tmp/mismatch.csv
|
|
* php artisan bookings:audit-storno-price-total --fix (setzt price_total = price_canceled)
|
|
*/
|
|
class BookingsAuditStornoPriceTotal extends Command
|
|
{
|
|
protected $signature = 'bookings:audit-storno-price-total
|
|
{--fix : Setzt price_total auf price_canceled für betroffene Zeilen}
|
|
{--export= : CSV-Datei (relativ zum Projektroot oder absolut)}
|
|
{--id=* : Nur diese Buchungs-IDs (booking.id)}';
|
|
|
|
protected $description = 'Abgleich Storno: price_total vs. price_canceled (inkl. optional --fix / --export)';
|
|
|
|
private const EPS = 0.009;
|
|
|
|
public function handle(): int
|
|
{
|
|
$query = Booking::query()
|
|
->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, '/'));
|
|
}
|
|
}
|