phase 2 dev
This commit is contained in:
parent
5a7478907e
commit
ba48745809
59 changed files with 2692 additions and 1994 deletions
156
app/Console/Commands/BookingsAuditStornoPriceTotal.php
Normal file
156
app/Console/Commands/BookingsAuditStornoPriceTotal.php
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
<?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, '/'));
|
||||
}
|
||||
}
|
||||
|
|
@ -37,7 +37,7 @@ class ContactsFindDuplicates extends Command
|
|||
|
||||
// ── HIGH: gleiche E-Mail ──────────────────────────────────────────
|
||||
if ($this->shouldCheck('HIGH')) {
|
||||
$emailDupes = DB::table('customer')
|
||||
$emailDupes = DB::table('contacts')
|
||||
->select('email', DB::raw('COUNT(*) as cnt'), DB::raw('GROUP_CONCAT(id ORDER BY updated_at DESC) as ids'))
|
||||
->whereNotNull('email')
|
||||
->where('email', '!=', '')
|
||||
|
|
@ -60,7 +60,7 @@ class ContactsFindDuplicates extends Command
|
|||
|
||||
// ── MEDIUM: Name + Vorname + Geburtsdatum ────────────────────────
|
||||
if ($this->shouldCheck('MEDIUM')) {
|
||||
$nameBdDupes = DB::table('customer')
|
||||
$nameBdDupes = DB::table('contacts')
|
||||
->select(
|
||||
'name', 'firstname', 'birthdate',
|
||||
DB::raw('COUNT(*) as cnt'),
|
||||
|
|
@ -88,7 +88,7 @@ class ContactsFindDuplicates extends Command
|
|||
|
||||
// ── LOW: Name + Vorname + PLZ ─────────────────────────────────────
|
||||
if ($this->shouldCheck('LOW')) {
|
||||
$nameZipDupes = DB::table('customer')
|
||||
$nameZipDupes = DB::table('contacts')
|
||||
->select(
|
||||
'name', 'firstname', 'zip',
|
||||
DB::raw('COUNT(*) as cnt'),
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ class ContactsMergeDuplicates extends Command
|
|||
|
||||
private function findByEmail(): array
|
||||
{
|
||||
return DB::table('customer')
|
||||
return DB::table('contacts')
|
||||
->select('email', DB::raw('GROUP_CONCAT(id ORDER BY updated_at DESC, id DESC) as ids'))
|
||||
->whereNotNull('email')
|
||||
->where('email', '!=', '')
|
||||
|
|
@ -104,7 +104,7 @@ class ContactsMergeDuplicates extends Command
|
|||
|
||||
private function findByNameBirthdate(): array
|
||||
{
|
||||
return DB::table('customer')
|
||||
return DB::table('contacts')
|
||||
->select(DB::raw('GROUP_CONCAT(id ORDER BY updated_at DESC, id DESC) as ids'))
|
||||
->whereNotNull('name')
|
||||
->whereNotNull('firstname')
|
||||
|
|
@ -119,7 +119,7 @@ class ContactsMergeDuplicates extends Command
|
|||
|
||||
private function findByNameZip(): array
|
||||
{
|
||||
return DB::table('customer')
|
||||
return DB::table('contacts')
|
||||
->select(DB::raw('GROUP_CONCAT(id ORDER BY updated_at DESC, id DESC) as ids'))
|
||||
->whereNotNull('name')
|
||||
->whereNotNull('firstname')
|
||||
|
|
@ -173,11 +173,11 @@ class ContactsMergeDuplicates extends Command
|
|||
private function mergeInto(int $masterId, int $dupeId): void
|
||||
{
|
||||
// 1. Leads umhängen
|
||||
$leadCount = DB::table('lead')->where('customer_id', $dupeId)->count();
|
||||
$leadCount = DB::table('inquiries')->where('customer_id', $dupeId)->count();
|
||||
if ($leadCount > 0) {
|
||||
$this->line(" lead.customer_id: {$leadCount} Zeile(n) → #{$masterId}");
|
||||
if (!$this->dryRun) {
|
||||
DB::table('lead')
|
||||
DB::table('inquiries')
|
||||
->where('customer_id', $dupeId)
|
||||
->update(['customer_id' => $masterId]);
|
||||
}
|
||||
|
|
@ -220,7 +220,7 @@ class ContactsMergeDuplicates extends Command
|
|||
|
||||
// 5. Duplikat als zusammengeführt markieren
|
||||
if (!$this->dryRun) {
|
||||
DB::table('customer')
|
||||
DB::table('contacts')
|
||||
->where('id', $dupeId)
|
||||
->update([
|
||||
'merged_into_id' => $masterId,
|
||||
|
|
|
|||
|
|
@ -193,7 +193,8 @@ class SyncNewsletterKulturreisen extends Command
|
|||
'description' => 'Kontakt durch Kulturreisen-Buchung erstellt',
|
||||
'metadata' => [
|
||||
'booking_id' => $booking->id,
|
||||
'lead_id' => $booking->lead_id,
|
||||
// Metadaten-Key bleibt `lead_id`; Wert kommt aus booking.inquiry_id.
|
||||
'lead_id' => $booking->inquiry_id,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue