diff --git a/.env b/.env index 904c04d..8aae097 100755 --- a/.env +++ b/.env @@ -16,7 +16,6 @@ APP_DOMAIN_TLD=test LOG_CHANNEL=stack SUCCESS_KEY=f6077389c9ce710e554763a5de02c8ec -EXCEPTION_MAIL=exception@adametz.media # Standard Database Connection DB_CONNECTION=mysql diff --git a/app/Console/Commands/BookingsAuditStornoPriceTotal.php b/app/Console/Commands/BookingsAuditStornoPriceTotal.php deleted file mode 100644 index e7a6478..0000000 --- a/app/Console/Commands/BookingsAuditStornoPriceTotal.php +++ /dev/null @@ -1,156 +0,0 @@ -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, '/')); - } -} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index ca9146f..59c585d 100755 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -2,16 +2,7 @@ namespace App\Exceptions; -use Illuminate\Auth\Access\AuthorizationException; -use Illuminate\Auth\AuthenticationException; -use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; -use Illuminate\Session\TokenMismatchException; -use Illuminate\Support\Facades\Mail; -use Illuminate\Validation\ValidationException; -use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; -use Symfony\Component\ErrorHandler\Exception\FlattenException; -use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Throwable; class Handler extends ExceptionHandler @@ -46,65 +37,6 @@ class Handler extends ExceptionHandler public function report(Throwable $exception) { parent::report($exception); - - $e = $this->mapException($exception); - - if ($this->shouldMailServerError($e)) { - $this->sendEmail($e); - } - } - - /** - * E-Mail nur bei echten Serverfeilern (5xx bzw. unbehandelte Exceptions), nicht bei lokalem Entwickeln. - */ - protected function shouldMailServerError(Throwable $e): bool - { - if (app()->environment('local', 'testing')) { - return false; - } - - if (! config('app.exception_mail')) { - return false; - } - - return $this->exceptionIndicatesServerError($e); - } - - /** - * Entspricht dem, was üblicherweise als HTTP500 ausgeliefert würde. - */ - protected function exceptionIndicatesServerError(Throwable $e): bool - { - if ( - $e instanceof AuthenticationException - || $e instanceof AuthorizationException - || $e instanceof ModelNotFoundException - || $e instanceof ValidationException - || $e instanceof TokenMismatchException - ) { - return false; - } - - if ($e instanceof HttpExceptionInterface) { - return $e->getStatusCode() >= 500; - } - - return true; - } - - protected function exceptionMailContextLine(): string - { - if (app()->runningInConsole()) { - $argv = $_SERVER['argv'] ?? []; - - return 'CLI: ' . (count($argv) ? implode(' ', $argv) : php_sapi_name()); - } - - if (app()->bound('request') && request()) { - return request()->fullUrl(); - } - - return 'n/a'; } /** @@ -120,28 +52,4 @@ class Handler extends ExceptionHandler { return parent::render($request, $exception); } - - public function sendEmail(Throwable $exception) - { - try { - $e = FlattenException::create($exception); - $handler = new HtmlErrorRenderer(true); - $css = $handler->getStylesheet(); - $content = $handler->getBody($e); - $to = config('app.exception_mail'); - $subject = config('app.name') . ' Exception: ' . $this->exceptionMailContextLine(); - - if ($to) { - Mail::send('emails.exception', compact('css', 'content'), function ($message) use ($to, $subject) { - $message->to($to)->subject($subject); - }); - } - } catch (Throwable $ex) { - file_put_contents( - storage_path('logs/laravel-' . date('Y-m-d') . '.log'), - '[' . date('Y-m-d H:i:s') . '] exception-handler-error: ' . $ex->getMessage() . "\n", - FILE_APPEND - ); - } - } } diff --git a/app/Http/Controllers/Admin/ReportBookingController.php b/app/Http/Controllers/Admin/ReportBookingController.php index 0c518e9..6579c4c 100644 --- a/app/Http/Controllers/Admin/ReportBookingController.php +++ b/app/Http/Controllers/Admin/ReportBookingController.php @@ -172,10 +172,10 @@ class ReportBookingController extends Controller ->orderColumn('end_date', 'end_date $1') ->orderColumn('price', 'price $1') ->orderColumn('booking_date', 'booking_date $1') - ->orderColumn('customer.fullName', 'contacts.firstname $1') - ->orderColumn('customer.firstname', 'contacts.firstname $1') - ->orderColumn('customer.name', 'contacts.name $1') - //->orderColumn('lead.status_id', 'inquiries.status_id $1') + ->orderColumn('customer.fullName', 'customer.firstname $1') + ->orderColumn('customer.firstname', 'customer.firstname $1') + ->orderColumn('customer.name', 'customer.name $1') + //->orderColumn('lead.status_id', 'lead.status_id $1') //->orderColumn('is_cleared', 'is_cleared $1') ->rawColumns(['id', 'lead.status_id', 'service_provider.names']) ->make(true); @@ -384,9 +384,9 @@ class ReportBookingController extends Controller ->orderColumn('end_date', 'end_date $1') ->orderColumn('price', 'price $1') ->orderColumn('booking_date', 'booking_date $1') - ->orderColumn('customer.firstname', 'contacts.firstname $1') - ->orderColumn('customer.name', 'contacts.name $1') - //->orderColumn('lead.status_id', 'inquiries.status_id $1') + ->orderColumn('customer.firstname', 'customer.firstname $1') + ->orderColumn('customer.name', 'customer.name $1') + //->orderColumn('lead.status_id', 'lead.status_id $1') //->orderColumn('is_cleared', 'is_cleared $1') ->rawColumns(['id', 'old_crm', 'check_total', 'lead.status_id']) ->make(true); diff --git a/app/Http/Controllers/Admin/ReportController.php b/app/Http/Controllers/Admin/ReportController.php index 033d8b9..ab45f3d 100755 --- a/app/Http/Controllers/Admin/ReportController.php +++ b/app/Http/Controllers/Admin/ReportController.php @@ -189,10 +189,10 @@ class ReportController extends Controller ->orderColumn('end_date', 'end_date $1') ->orderColumn('price', 'price $1') ->orderColumn('booking_date', 'booking_date $1') - ->orderColumn('customer.fullName', 'contacts.firstname $1') - ->orderColumn('customer.firstname', 'contacts.firstname $1') - ->orderColumn('customer.name', 'contacts.name $1') - //->orderColumn('lead.status_id', 'inquiries.status_id $1') + ->orderColumn('customer.fullName', 'customer.firstname $1') + ->orderColumn('customer.firstname', 'customer.firstname $1') + ->orderColumn('customer.name', 'customer.name $1') + //->orderColumn('lead.status_id', 'lead.status_id $1') //->orderColumn('is_cleared', 'is_cleared $1') ->rawColumns(['id', 'lead.status_id', 'service_provider.names']) ->make(true); @@ -725,9 +725,9 @@ class ReportController extends Controller ->orderColumn('end_date', 'end_date $1') ->orderColumn('price', 'price $1') ->orderColumn('booking_date', 'booking_date $1') - ->orderColumn('customer.firstname', 'contacts.firstname $1') - ->orderColumn('customer.name', 'contacts.name $1') - //->orderColumn('lead.status_id', 'inquiries.status_id $1') + ->orderColumn('customer.firstname', 'customer.firstname $1') + ->orderColumn('customer.name', 'customer.name $1') + //->orderColumn('lead.status_id', 'lead.status_id $1') //->orderColumn('is_cleared', 'is_cleared $1') ->rawColumns(['id', 'old_crm', 'check_total', 'lead.status_id']) ->make(true); diff --git a/app/Http/Controllers/Admin/ReportLeadsController.php b/app/Http/Controllers/Admin/ReportLeadsController.php index 232ddee..ddfe25a 100644 --- a/app/Http/Controllers/Admin/ReportLeadsController.php +++ b/app/Http/Controllers/Admin/ReportLeadsController.php @@ -161,9 +161,9 @@ class ReportLeadsController extends Controller $orderByNum = [ 0 => 'id', 1 => 'customer_id', - 2 => 'contacts.firstname', - 3 => 'contacts.name', - 4 => 'contacts.email', + 2 => 'customer.firstname', + 3 => 'customer.name', + 4 => 'customer.email', 5 => 'request_date', 6 => 'travel_country', 7 => 'sf_guard_user.last_name', diff --git a/app/Http/Controllers/ContactController.php b/app/Http/Controllers/ContactController.php index 3ddedc9..effb97d 100644 --- a/app/Http/Controllers/ContactController.php +++ b/app/Http/Controllers/ContactController.php @@ -271,9 +271,9 @@ class ContactController extends Controller ->addColumn('leads_count', fn(Contact $contact) => $contact->leads_count) ->addColumn('bookings_count', fn(Contact $contact) => $contact->bookings_count) ->addColumn('deleted_at', fn(Contact $contact) => $contact->deleted_at?->format('d.m.Y H:i') ?? '') - ->orderColumn('contacts.id', 'contacts.id $1') - ->orderColumn('deleted_at', 'contacts.deleted_at $1') - ->filterColumn('contacts.id', function ($query, $keyword) { + ->orderColumn('id', 'customer.id $1') + ->orderColumn('deleted_at', 'customer.deleted_at $1') + ->filterColumn('id', function ($query, $keyword) { if ($keyword !== '') { $query->where('contacts.id', 'LIKE', '%' . $keyword . '%'); } diff --git a/app/Models/Booking.php b/app/Models/Booking.php index 80811b3..f3bf11c 100644 --- a/app/Models/Booking.php +++ b/app/Models/Booking.php @@ -787,6 +787,8 @@ class Booking extends Model $total_children += $prices['children']; } + + if ($travel_draft_item) { $travel_draft_item->setPriceAdultRaw($travel_price_adult); $travel_draft_item->setPriceChildrenRaw($travel_price_children); @@ -795,23 +797,10 @@ class Booking extends Model $travel_draft_item->save(); } $this->price = $total_adult + $total_children; - $this->setPriceTotalForCurrentState(); + $this->price_total = $this->getPriceRaw() + $this->getServiceTotal(true); $this->save(); } - /** - * Gesamtpreis Reise (price_total): bei Storno mit gesetztem Storno-Betrag = price_canceled - * (wie nach createPDF_Storno in BookingPDFRepository), sonst Reisepreis + Vermittlung. - */ - public function setPriceTotalForCurrentState(): void - { - if ($this->isCanceled() && $this->attributes['price_canceled'] !== null) { - $this->price_total = round((float) $this->getPriceCanceledRaw(), 2); - return; - } - $this->price_total = round((float) $this->getPriceRaw() + (float) $this->getServiceTotal(true), 2); - } - public function getPriceAttribute() { return Util::_number_format($this->attributes['price']); diff --git a/app/Models/Lead.php b/app/Models/Lead.php index f7ddf33..f9441f5 100644 --- a/app/Models/Lead.php +++ b/app/Models/Lead.php @@ -108,16 +108,16 @@ use Illuminate\Database\Eloquent\Collection; */ class Lead extends Model { - use HasFactory; + use HasFactory; - protected $connection = 'mysql'; + protected $connection = 'mysql'; - /** - * Modul 3 Phase 2: lead → inquiries (RENAME TABLE). - * Model-Name bleibt (um Breaking Changes in der gesamten Codebase zu vermeiden); - * fachlich ist das Modell jetzt eine "Inquiry" (Anfrage). - */ - protected $table = 'inquiries'; + /** + * Modul 3 Phase 2: lead → inquiries (RENAME TABLE). + * Model-Name bleibt (um Breaking Changes in der gesamten Codebase zu vermeiden); + * fachlich ist das Modell jetzt eine "Inquiry" (Anfrage). + */ + protected $table = 'inquiries'; protected $casts = [ 'customer_id' => 'int', @@ -126,8 +126,8 @@ class Lead extends Model 'travelagenda_id' => 'int', 'sf_guard_user_id' => 'int', 'is_closed' => 'bool', - 'is_rebook' => 'bool', - 'initialcontacttype_id' => 'int', + 'is_rebook' => 'bool', + 'initialcontacttype_id' => 'int', 'searchengine_id' => 'int', 'status_id' => 'int', 'website_id' => 'int', @@ -154,7 +154,7 @@ class Lead extends Model 'remarks', 'sf_guard_user_id', 'is_closed', - 'is_rebook', + 'is_rebook', 'initialcontacttype_id', 'searchengine_id', 'searchengine_keywords', @@ -169,24 +169,24 @@ class Lead extends Model 'participant_birthdate', 'participant_salutation_id' ]; - protected $passolutionPDFs = []; - + protected $passolutionPDFs = []; + public static $lead_mail_dirs = [ - 11 => ['name' => 'Entwürfe', 'icon' => 'ion-md-create'], - 12 => ['name' => 'Papierkorb', 'icon' => 'ion-md-trash'], - ]; + 11 => ['name' => 'Entwürfe', 'icon'=>'ion-md-create'], + 12 => ['name' => 'Papierkorb', 'icon'=>'ion-md-trash'], + ]; - public function updateNextDueDate($date = false) - { + public function updateNextDueDate($date = false){ - if (!$date) { - $carbon = Carbon::now(); - } else { + if(!$date){ + $carbon = Carbon::now(); + }else{ $carbon = Carbon::parse($date); } - $this->next_due_date = $carbon->modify('+ ' . $this->status->handling_days . ' days')->format("Y-m-d"); + $this->next_due_date = $carbon->modify('+ '.$this->status->handling_days.' days')->format("Y-m-d"); $this->save(); - } + + } public function customer() { return $this->belongsTo(Customer::class); @@ -227,18 +227,18 @@ class Lead extends Model return $this->belongsTo(TravelCategory::class, 'travelcategory_id'); } - //on crm - public function travel_country_crm() - { - return $this->belongsTo('App\Models\Sym\TravelCountry', 'travelcountry_id', 'id'); - } + //on crm + public function travel_country_crm() + { + return $this->belongsTo('App\Models\Sym\TravelCountry', 'travelcountry_id', 'id'); + } - //on stern other DB - public function travel_country() - { - return $this->belongsTo('App\Models\TravelCountry', 'travelcountry_id', 'crm_id'); - } + //on stern other DB + public function travel_country() + { + return $this->belongsTo('App\Models\TravelCountry', 'travelcountry_id', 'crm_id'); + } public function website() @@ -248,8 +248,7 @@ class Lead extends Model public function bookings() { - // Modul 3 Phase 2: FK heißt jetzt inquiry_id (vormals lead_id) - return $this->hasMany(Booking::class, 'inquiry_id'); + return $this->hasMany(Booking::class); } public function inquiries() @@ -273,156 +272,149 @@ class Lead extends Model } public function lead_files() - { + { //no lead_mail_id - return $this->hasMany(LeadFile::class, 'lead_id')->where('lead_mail_id', null); - } + return $this->hasMany(LeadFile::class, 'lead_id')->where('lead_mail_id', null); + } - public function lead_mails() - { - return $this->hasMany(LeadMail::class, 'lead_id', 'id'); - } + public function lead_mails() + { + return $this->hasMany(LeadMail::class, 'lead_id', 'id'); + } - public function lead_mails_sent_at() - { - return $this->hasMany(LeadMail::class, 'lead_id')->orderBy('sent_at', 'ASC'); - } + public function lead_mails_sent_at() + { + return $this->hasMany(LeadMail::class, 'lead_id')->orderBy('sent_at', 'ASC'); + } - public function lead_mail_last() - { - return $this->hasOne(LeadMail::class, 'lead_id')->latest(); - } + public function lead_mail_last() + { + return $this->hasOne(LeadMail::class, 'lead_id')->latest(); + } - public function lead_notices() - { - return $this->hasMany(LeadNotice::class, 'lead_id')->orderBy('updated_at', 'DESC'); - } + public function lead_notices() + { + return $this->hasMany(LeadNotice::class, 'lead_id')->orderBy('updated_at', 'DESC'); + } - public static function getSfGuardUserArray() - { - return SfGuardUser::where('is_active', 1)->get()->pluck('fullname', 'id'); - } + public static function getSfGuardUserArray(){ + return SfGuardUser::where('is_active', 1)->get()->pluck('fullname', 'id'); + } - public static function getTravelCountryArray($emtpy = false) - { + public static function getTravelCountryArray($emtpy = false){ $TravelCountry = TravelCountry::where('active_backend', 1)->orderBy('name')->get()->pluck('name', 'id'); - return $emtpy ? $TravelCountry->prepend('-', 0) : $TravelCountry; - } + return $emtpy ? $TravelCountry->prepend('-', 0) : $TravelCountry; - public static function getTravelCategoryArray($emtpy = false) - { + } + + public static function getTravelCategoryArray($emtpy = false){ $TravelCategory = TravelCategory::orderBy('name')->get()->pluck('name', 'id'); - return $emtpy ? $TravelCategory->prepend('-', 0) : $TravelCategory; + return $emtpy ? $TravelCategory->prepend('-', 0) : $TravelCategory; + } - public static function getTravelAgendaArray($emtpy = false) - { + public static function getTravelAgendaArray($emtpy = false){ $TravelAgenda = TravelAgenda::orderBy('name')->get()->pluck('name', 'id'); - return $emtpy ? $TravelAgenda->prepend('-', 0) : $TravelAgenda; + return $emtpy ? $TravelAgenda->prepend('-', 0) : $TravelAgenda; } - public static function getStatusArray($emtpy = false) - { + public static function getStatusArray($emtpy = false){ $Status = Status::orderBy('name')->get()->pluck('name', 'id'); - return $emtpy ? $Status->prepend('-', 0) : $Status; + return $emtpy ? $Status->prepend('-', 0) : $Status; } public function getStatusBadge($booking = null) { - if ($this->status_id && $this->status) { + if($this->status_id && $this->status){ $color = $this->status->color; $icon = ""; - if ($this->status_id == 14 && $this->is_rebook) { + if($this->status_id == 14 && $this->is_rebook){ $color = '#94ae59'; $icon = ' '; } - if ($this->status_id == 14 && !$this->is_rebook) { - $icon = ' '; + if($this->status_id == 14 && !$this->is_rebook){ + $icon = ' '; } - if ($this->status_id == 15) { + if($this->status_id == 15){ $icon = ' '; - if ($booking && $booking->lawyer_date) { - return '' . $icon . $booking->lawyer_date->format('d.m.Y') . ''; + if($booking && $booking->lawyer_date){ + return ''.$icon.$booking->lawyer_date->format('d.m.Y').''; } } - return '' . $icon . $this->status->name . ''; + return ''.$icon.$this->status->name.''; } return '-'; } - public function getTravelCountryDestco($badge = true) - { + public function getTravelCountryDestco($badge = true){ $out = ""; - if ($this->bookings->count()) { + if($this->bookings->count()){ $out .= $badge ? '' : ''; - foreach ($this->bookings as $booking) { - if ($booking->travel_country_id && $booking->travel_country) { + foreach ($this->bookings as $booking){ + if($booking->travel_country_id && $booking->travel_country) { $out .= $booking->travel_country->destco; } } $out .= $badge ? '' : ''; return $out; } - if ($this->travel_country) { - return $badge ? '' . $this->travel_country->destco . '' : $this->travel_country->destco; + if($this->travel_country){ + return $badge ? ''.$this->travel_country->destco.'' : $this->travel_country->destco; } return "-"; } - public function countLeadMailsBy($dir, $subdir = false) - { - if ($dir === 11) { - return $this->lead_mails->where('draft', true)->where('dir', '!=', 12)->count(); - } - if ($subdir) { - return $this->lead_mails->where('dir', $dir)->where('subdir', $subdir)->count(); - } - return $this->lead_mails->where('dir', $dir)->count(); - } + public function countLeadMailsBy($dir, $subdir=false){ + if($dir === 11){ + return $this->lead_mails->where('draft', true)->where('dir', '!=', 12)->count(); + } + if($subdir){ + return $this->lead_mails->where('dir', $dir)->where('subdir', $subdir)->count(); + } + return $this->lead_mails->where('dir', $dir)->count(); + } - public function getPassolutionPDF($create = false, $resync = false) - { + public function getPassolutionPDF($create = false, $resync = false){ - $nats = []; + $nats = []; - if (count($this->passolutionPDFs)) { - return $this->passolutionPDFs; - } + if(count($this->passolutionPDFs)){ + return $this->passolutionPDFs; + } - if (!$this->travel_country) { - return $this->passolutionPDFs; - } - - $destco = $this->travel_country->destco; + if(!$this->travel_country){ + return $this->passolutionPDFs; + } + + $destco = $this->travel_country->destco; //default no travel_nationality $nats['de'] = 'de'; + + if($this->lead_participants->count()){ + foreach ($this->lead_participants as $participant){ + if($participant->travel_nationality){ + $nats[$participant->travel_nationality->nat] = $participant->travel_nationality->nat; + } + } + } + if(empty($nats)){ + $nats['de'] = 'de'; + } + foreach ($nats as $nat){ + $data = [ + 'nat' => $nat, + 'destco' => $destco, + ]; + $passolution = new Passolution($data); + $this->passolutionPDFs[] = $passolution->findOrCreatePDF($create, $resync); + } + return $this->passolutionPDFs; + } - if ($this->lead_participants->count()) { - foreach ($this->lead_participants as $participant) { - if ($participant->travel_nationality) { - $nats[$participant->travel_nationality->nat] = $participant->travel_nationality->nat; - } - } - } - if (empty($nats)) { - $nats['de'] = 'de'; - } - foreach ($nats as $nat) { - $data = [ - 'nat' => $nat, - 'destco' => $destco, - ]; - $passolution = new Passolution($data); - $this->passolutionPDFs[] = $passolution->findOrCreatePDF($create, $resync); - } - return $this->passolutionPDFs; - } - - public function resyncPassolutionPDF() - { - return $this->getPassolutionPDF(true, true); - } + public function resyncPassolutionPDF(){ + return $this->getPassolutionPDF(true, true); + } } diff --git a/app/Repositories/BookingRepository.php b/app/Repositories/BookingRepository.php index be49e03..7c4838d 100644 --- a/app/Repositories/BookingRepository.php +++ b/app/Repositories/BookingRepository.php @@ -157,12 +157,11 @@ class BookingRepository extends BaseRepository { $this->model = Booking::findOrFail($id); - $this->model->setPriceTotalForCurrentState(); $fill = [ 'deposit_total' => $data['deposit_total'] ? Util::_clean_float($data['deposit_total']) : 0, 'final_payment' => $data['final_payment'] ? Util::_clean_float($data['final_payment']) : 0, 'final_payment_date' => $data['final_payment_date'] ? _reformat_date($data['final_payment_date']) : null, - 'price_total' => $this->model->getPriceTotalRaw(), + 'price_total' => ($this->model->getPriceRaw() + $this->model->getServiceTotal(true)), ]; $this->model->fill($fill); $this->model->save(); @@ -211,7 +210,7 @@ class BookingRepository extends BaseRepository } } } - $this->model->setPriceTotalForCurrentState(); + $this->model->price_total = ($this->model->getPriceRaw() + $this->model->getServiceTotal(true)); $this->model->save(); return $this->model; diff --git a/app/Services/Booking.php b/app/Services/Booking.php index 5ad24aa..f831388 100644 --- a/app/Services/Booking.php +++ b/app/Services/Booking.php @@ -15,7 +15,7 @@ class Booking ->pluck('slug', 'id'); } - public static function setOutputDirs(string $dir, ?string $subdir): void + public static function setOutputDirs(string $dir, string $subdir): void { MailDirService::setOutputDir($dir, $subdir); } @@ -39,12 +39,12 @@ class Booking return MailDirService::getCustomerMailDir($id); } - public static function getCustomerMailName(\App\Models\CMSContent $mailDir, ?int $mailDirId): string + public static function getCustomerMailName(\App\Models\CMSContent $mailDir, int $mailDirId): string { return MailDirService::getCustomerMailName($mailDir, $mailDirId); } - public static function getCustomerMailEmails(\App\Models\CMSContent $mailDir, ?int $mailDirId): array|string + public static function getCustomerMailEmails(\App\Models\CMSContent $mailDir, int $mailDirId): array|string { return MailDirService::getCustomerMailEmails($mailDir, $mailDirId); } diff --git a/app/Services/Lead.php b/app/Services/Lead.php index 6cbd91b..e6b47dd 100644 --- a/app/Services/Lead.php +++ b/app/Services/Lead.php @@ -15,7 +15,7 @@ class Lead ->pluck('slug', 'id'); } - public static function setOutputDirs(string $dir, ?string $subdir): void + public static function setOutputDirs(string $dir, string $subdir): void { MailDirService::setOutputDir($dir, $subdir); } @@ -39,12 +39,12 @@ class Lead return MailDirService::getCustomerMailDir($id); } - public static function getCustomerMailName(\App\Models\CMSContent $mailDir, ?int $mailDirId): string + public static function getCustomerMailName(\App\Models\CMSContent $mailDir, int $mailDirId): string { return MailDirService::getCustomerMailName($mailDir, $mailDirId); } - public static function getCustomerMailEmails(\App\Models\CMSContent $mailDir, ?int $mailDirId): array|string + public static function getCustomerMailEmails(\App\Models\CMSContent $mailDir, int $mailDirId): array|string { return MailDirService::getCustomerMailEmails($mailDir, $mailDirId); } diff --git a/app/Services/MailDirService.php b/app/Services/MailDirService.php index 47b5a35..f36cec6 100644 --- a/app/Services/MailDirService.php +++ b/app/Services/MailDirService.php @@ -11,7 +11,7 @@ class MailDirService { private static array $outputDirs = []; - public static function setOutputDir(string $dir, ?string $subdir): void + public static function setOutputDir(string $dir, string $subdir): void { self::$outputDirs[$dir][] = $subdir; } @@ -29,7 +29,7 @@ class MailDirService return CMSContent::where('identifier', '=', 'customer-mail-dirs')->where('pos', '=', $id)->first(); } - public static function getCustomerMailName(CMSContent $mailDir, ?int $mailDirId): string + public static function getCustomerMailName(CMSContent $mailDir, int $mailDirId): string { $model = self::resolveModel($mailDir, $mailDirId); @@ -47,7 +47,7 @@ class MailDirService /** * @return array|string */ - public static function getCustomerMailEmails(CMSContent $mailDir, ?int $mailDirId): array|string + public static function getCustomerMailEmails(CMSContent $mailDir, int $mailDirId): array|string { $model = self::resolveModel($mailDir, $mailDirId); @@ -79,11 +79,8 @@ class MailDirService return $result; } - private static function resolveModel(CMSContent $mailDir, ?int $mailDirId): mixed + private static function resolveModel(CMSContent $mailDir, int $mailDirId): mixed { - if ($mailDirId === null) { - return null; - } return match ($mailDir->getArrayContent('model')) { 'TravelCountry' => \App\Models\Sym\TravelCountry::find($mailDirId), 'Airline' => Airline::find($mailDirId), diff --git a/bootstrap/cache/config.php b/bootstrap/cache/config.php new file mode 100644 index 0000000..66c2e87 --- /dev/null +++ b/bootstrap/cache/config.php @@ -0,0 +1,1729 @@ + + array ( + 'name' => 'STERN TOURS CRM', + 'env' => 'local', + 'debug' => true, + 'url' => 'https://mein.sterntours.test', + 'old_url' => 'https://cms-stern-tours.test', + 'url_v2' => 'https://v2.sterntours.test', + 'url_stern' => 'https://sterntours.test', + 'domain_tld' => 'test', + 'timezone' => 'Europe/Berlin', + 'locale' => 'de', + 'fallback_locale' => 'de', + 'key' => 'base64:cxq+xNckU1xLwp8V9Bfj9+nOK5iZL6urcZ1EBO8usXg=', + 'cipher' => 'AES-256-CBC', + 'success_key' => 'f6077389c9ce710e554763a5de02c8ec', + 'providers' => + array ( + 0 => 'Illuminate\\Auth\\AuthServiceProvider', + 1 => 'Illuminate\\Broadcasting\\BroadcastServiceProvider', + 2 => 'Illuminate\\Bus\\BusServiceProvider', + 3 => 'Illuminate\\Cache\\CacheServiceProvider', + 4 => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', + 5 => 'Illuminate\\Cookie\\CookieServiceProvider', + 6 => 'Illuminate\\Database\\DatabaseServiceProvider', + 7 => 'Illuminate\\Encryption\\EncryptionServiceProvider', + 8 => 'Illuminate\\Filesystem\\FilesystemServiceProvider', + 9 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider', + 10 => 'Illuminate\\Hashing\\HashServiceProvider', + 11 => 'Illuminate\\Mail\\MailServiceProvider', + 12 => 'Illuminate\\Notifications\\NotificationServiceProvider', + 13 => 'Illuminate\\Pagination\\PaginationServiceProvider', + 14 => 'Illuminate\\Pipeline\\PipelineServiceProvider', + 15 => 'Illuminate\\Queue\\QueueServiceProvider', + 16 => 'Illuminate\\Redis\\RedisServiceProvider', + 17 => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider', + 18 => 'Illuminate\\Session\\SessionServiceProvider', + 19 => 'Illuminate\\Translation\\TranslationServiceProvider', + 20 => 'Illuminate\\Validation\\ValidationServiceProvider', + 21 => 'Illuminate\\View\\ViewServiceProvider', + 22 => 'Laravel\\Tinker\\TinkerServiceProvider', + 23 => 'Laravel\\Passport\\PassportServiceProvider', + 24 => 'App\\Providers\\AppServiceProvider', + 25 => 'App\\Providers\\AuthServiceProvider', + 26 => 'App\\Providers\\EventServiceProvider', + 27 => 'App\\Providers\\RouteServiceProvider', + 28 => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider', + 29 => 'Barryvdh\\DomPDF\\ServiceProvider', + 30 => 'Jenssegers\\Date\\DateServiceProvider', + 31 => 'Collective\\Html\\HtmlServiceProvider', + 32 => 'Intervention\\Image\\ImageServiceProvider', + 33 => 'Maatwebsite\\Excel\\ExcelServiceProvider', + 34 => 'Yajra\\DataTables\\DataTablesServiceProvider', + 35 => 'Reliese\\Coders\\CodersServiceProvider', + ), + 'aliases' => + array ( + 'App' => 'Illuminate\\Support\\Facades\\App', + 'Arr' => 'Illuminate\\Support\\Arr', + 'Artisan' => 'Illuminate\\Support\\Facades\\Artisan', + 'Auth' => 'Illuminate\\Support\\Facades\\Auth', + 'Blade' => 'Illuminate\\Support\\Facades\\Blade', + 'Broadcast' => 'Illuminate\\Support\\Facades\\Broadcast', + 'Bus' => 'Illuminate\\Support\\Facades\\Bus', + 'Cache' => 'Illuminate\\Support\\Facades\\Cache', + 'Config' => 'Illuminate\\Support\\Facades\\Config', + 'Cookie' => 'Illuminate\\Support\\Facades\\Cookie', + 'Crypt' => 'Illuminate\\Support\\Facades\\Crypt', + 'DB' => 'Illuminate\\Support\\Facades\\DB', + 'Eloquent' => 'Illuminate\\Database\\Eloquent\\Model', + 'Event' => 'Illuminate\\Support\\Facades\\Event', + 'File' => 'Illuminate\\Support\\Facades\\File', + 'Gate' => 'Illuminate\\Support\\Facades\\Gate', + 'Hash' => 'Illuminate\\Support\\Facades\\Hash', + 'Lang' => 'Illuminate\\Support\\Facades\\Lang', + 'Log' => 'Illuminate\\Support\\Facades\\Log', + 'Mail' => 'Illuminate\\Support\\Facades\\Mail', + 'Notification' => 'Illuminate\\Support\\Facades\\Notification', + 'Password' => 'Illuminate\\Support\\Facades\\Password', + 'Queue' => 'Illuminate\\Support\\Facades\\Queue', + 'Redirect' => 'Illuminate\\Support\\Facades\\Redirect', + 'Redis' => 'Illuminate\\Support\\Facades\\Redis', + 'Request' => 'Illuminate\\Support\\Facades\\Request', + 'Response' => 'Illuminate\\Support\\Facades\\Response', + 'Route' => 'Illuminate\\Support\\Facades\\Route', + 'Schema' => 'Illuminate\\Support\\Facades\\Schema', + 'Session' => 'Illuminate\\Support\\Facades\\Session', + 'Storage' => 'Illuminate\\Support\\Facades\\Storage', + 'Str' => 'Illuminate\\Support\\Str', + 'URL' => 'Illuminate\\Support\\Facades\\URL', + 'Validator' => 'Illuminate\\Support\\Facades\\Validator', + 'View' => 'Illuminate\\Support\\Facades\\View', + 'Input' => 'Illuminate\\Support\\Facades\\Request', + 'Form' => 'Collective\\Html\\FormFacade', + 'Image' => 'Intervention\\Image\\Facades\\Image', + 'Carbon' => 'Carbon\\Carbon', + 'Date' => 'Jenssegers\\Date\\Date', + 'HTMLHelper' => 'App\\Helper\\HTMLHelper', + 'Util' => 'App\\Services\\Util', + 'Excel' => 'Maatwebsite\\Excel\\Facades\\Excel', + 'DataTables' => 'Yajra\\DataTables\\Facades\\DataTables', + 'PDF' => 'Barryvdh\\DomPDF\\Facade', + ), + ), + 'auth' => + array ( + 'defaults' => + array ( + 'guard' => 'web', + 'passwords' => 'users', + ), + 'guards' => + array ( + 'web' => + array ( + 'driver' => 'session', + 'provider' => 'users', + ), + 'api' => + array ( + 'driver' => 'passport', + 'provider' => 'users', + ), + ), + 'providers' => + array ( + 'users' => + array ( + 'driver' => 'eloquent', + 'model' => 'App\\User', + ), + ), + 'passwords' => + array ( + 'users' => + array ( + 'provider' => 'users', + 'table' => 'password_resets', + 'expire' => 60, + ), + ), + ), + 'booking' => + array ( + 'identifier_general' => 'booking-pdf-g-', + 'identifier_general_name' => 'booking-pdf-general-name', + 'identifier_content' => 'booking-pdf-c-', + 'identifier_content_name' => 'booking-pdf-content-name', + 'max_interval_days' => 28, + 'deposit_percentage_rate' => 25, + 'max_deposit_interval_days' => 28, + 'coupon_default_value' => '50,00', + 'coupon_valid_date_month' => 24, + ), + 'broadcasting' => + array ( + 'default' => 'log', + 'connections' => + array ( + 'pusher' => + array ( + 'driver' => 'pusher', + 'key' => '', + 'secret' => '', + 'app_id' => '', + 'options' => + array ( + 'cluster' => 'mt1', + 'encrypted' => true, + ), + ), + 'redis' => + array ( + 'driver' => 'redis', + 'connection' => 'default', + ), + 'log' => + array ( + 'driver' => 'log', + ), + 'null' => + array ( + 'driver' => 'null', + ), + ), + ), + 'cache' => + array ( + 'default' => 'file', + 'stores' => + array ( + 'apc' => + array ( + 'driver' => 'apc', + ), + 'array' => + array ( + 'driver' => 'array', + ), + 'database' => + array ( + 'driver' => 'database', + 'table' => 'cache', + 'connection' => NULL, + ), + 'file' => + array ( + 'driver' => 'file', + 'path' => '/workspace/mein.sterntours.de/storage/framework/cache/data', + ), + 'memcached' => + array ( + 'driver' => 'memcached', + 'persistent_id' => NULL, + 'sasl' => + array ( + 0 => NULL, + 1 => NULL, + ), + 'options' => + array ( + ), + 'servers' => + array ( + 0 => + array ( + 'host' => '127.0.0.1', + 'port' => 11211, + 'weight' => 100, + ), + ), + ), + 'redis' => + array ( + 'driver' => 'redis', + 'connection' => 'default', + ), + ), + 'prefix' => 'stern_tours_crm_cache', + ), + 'database' => + array ( + 'default' => 'mysql', + 'connections' => + array ( + 'sqlite' => + array ( + 'driver' => 'sqlite', + 'database' => 'stern_crm', + 'prefix' => '', + ), + 'mysql' => + array ( + 'driver' => 'mysql', + 'host' => 'global-mysql', + 'port' => '3306', + 'database' => 'stern_crm', + 'username' => 'root', + 'password' => 'password', + 'unix_socket' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'strict' => true, + 'engine' => NULL, + ), + 'mysql_stern' => + array ( + 'driver' => 'mysql', + 'host' => 'global-mysql', + 'port' => '3306', + 'database' => 'stern_db', + 'username' => 'root', + 'password' => 'password', + 'unix_socket' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'strict' => true, + 'engine' => NULL, + ), + 'pgsql' => + array ( + 'driver' => 'pgsql', + 'host' => 'global-mysql', + 'port' => '3306', + 'database' => 'stern_crm', + 'username' => 'root', + 'password' => 'password', + 'charset' => 'utf8', + 'prefix' => '', + 'schema' => 'public', + 'sslmode' => 'prefer', + ), + 'sqlsrv' => + array ( + 'driver' => 'sqlsrv', + 'host' => 'global-mysql', + 'port' => '3306', + 'database' => 'stern_crm', + 'username' => 'root', + 'password' => 'password', + 'charset' => 'utf8', + 'prefix' => '', + ), + ), + 'migrations' => 'migrations', + 'redis' => + array ( + 'client' => 'predis', + 'default' => + array ( + 'host' => 'global-redis', + 'password' => NULL, + 'port' => '6379', + 'database' => 0, + ), + ), + ), + 'datatables' => + array ( + 'search' => + array ( + 'smart' => true, + 'multi_term' => true, + 'case_insensitive' => true, + 'use_wildcards' => false, + ), + 'index_column' => 'DT_Row_Index', + 'engines' => + array ( + 'eloquent' => 'Yajra\\DataTables\\EloquentDataTable', + 'query' => 'Yajra\\DataTables\\QueryDataTable', + 'collection' => 'Yajra\\DataTables\\CollectionDataTable', + 'resource' => 'Yajra\\DataTables\\ApiResourceDataTable', + ), + 'builders' => + array ( + ), + 'nulls_last_sql' => '%s %s NULLS LAST', + 'error' => NULL, + 'columns' => + array ( + 'excess' => + array ( + 0 => 'rn', + 1 => 'row_num', + ), + 'escape' => '*', + 'raw' => + array ( + 0 => 'action', + ), + 'blacklist' => + array ( + 0 => 'password', + 1 => 'remember_token', + ), + 'whitelist' => '*', + ), + 'json' => + array ( + 'header' => + array ( + ), + 'options' => 0, + ), + 'callback' => + array ( + 0 => '$', + 1 => '$.', + 2 => 'function', + ), + ), + 'debugbar' => + array ( + 'enabled' => NULL, + 'hide_empty_tabs' => true, + 'except' => + array ( + 0 => 'telescope*', + ), + 'storage' => + array ( + 'enabled' => false, + 'driver' => 'file', + 'path' => '/workspace/mein.sterntours.de/storage/debugbar', + 'connection' => NULL, + 'provider' => '', + ), + 'editor' => 'phpstorm', + 'remote_sites_path' => NULL, + 'local_sites_path' => NULL, + 'include_vendors' => true, + 'capture_ajax' => false, + 'add_ajax_timing' => false, + 'ajax_handler_auto_show' => true, + 'ajax_handler_enable_tab' => true, + 'defer_datasets' => false, + 'error_handler' => false, + 'error_level' => 32767, + 'clockwork' => false, + 'collectors' => + array ( + 'phpinfo' => true, + 'messages' => true, + 'time' => true, + 'memory' => true, + 'exceptions' => true, + 'log' => true, + 'db' => true, + 'views' => true, + 'route' => true, + 'auth' => false, + 'gate' => true, + 'session' => true, + 'symfony_request' => true, + 'mail' => true, + 'laravel' => false, + 'events' => false, + 'default_request' => false, + 'logs' => false, + 'files' => false, + 'config' => false, + 'cache' => false, + 'models' => false, + ), + 'options' => + array ( + 'auth' => + array ( + 'show_name' => true, + ), + 'db' => + array ( + 'with_params' => true, + 'backtrace' => true, + 'timeline' => false, + 'explain' => + array ( + 'enabled' => false, + 'types' => + array ( + 0 => 'SELECT', + ), + ), + 'hints' => true, + ), + 'mail' => + array ( + 'full_log' => false, + ), + 'views' => + array ( + 'data' => false, + ), + 'route' => + array ( + 'label' => true, + ), + 'logs' => + array ( + 'file' => NULL, + ), + 'cache' => + array ( + 'values' => true, + ), + ), + 'inject' => true, + 'route_prefix' => '_debugbar', + 'route_middleware' => + array ( + ), + 'route_domain' => NULL, + 'theme' => 'auto', + 'debug_backtrace_limit' => 50, + ), + 'dompdf' => + array ( + 'show_warnings' => false, + 'public_path' => NULL, + 'convert_entities' => true, + 'options' => + array ( + 'font_dir' => '/workspace/mein.sterntours.de/storage/fonts', + 'font_cache' => '/workspace/mein.sterntours.de/storage/fonts', + 'temp_dir' => '/tmp', + 'chroot' => '/workspace/mein.sterntours.de', + 'allowed_protocols' => + array ( + 'file://' => + array ( + 'rules' => + array ( + ), + ), + 'http://' => + array ( + 'rules' => + array ( + ), + ), + 'https://' => + array ( + 'rules' => + array ( + ), + ), + ), + 'log_output_file' => NULL, + 'enable_font_subsetting' => false, + 'pdf_backend' => 'CPDF', + 'default_media_type' => 'screen', + 'default_paper_size' => 'a4', + 'default_paper_orientation' => 'portrait', + 'default_font' => 'serif', + 'dpi' => 96, + 'enable_php' => false, + 'enable_javascript' => true, + 'enable_remote' => true, + 'font_height_ratio' => 1.1, + 'enable_html5_parser' => true, + ), + 'orientation' => 'portrait', + 'defines' => + array ( + 'font_dir' => '/workspace/mein.sterntours.de/storage/fonts/', + 'font_cache' => '/workspace/mein.sterntours.de/storage/fonts/', + 'temp_dir' => '/tmp', + 'chroot' => '/workspace/mein.sterntours.de', + 'enable_font_subsetting' => false, + 'pdf_backend' => 'CPDF', + 'default_media_type' => 'print', + 'default_paper_size' => 'a4', + 'default_font' => 'sans-serif', + 'dpi' => 300, + 'enable_php' => false, + 'enable_javascript' => true, + 'enable_remote' => true, + 'font_height_ratio' => 0.8, + 'enable_html5_parser' => false, + ), + ), + 'excel' => + array ( + 'exports' => + array ( + 'chunk_size' => 1000, + 'pre_calculate_formulas' => false, + 'csv' => + array ( + 'delimiter' => ',', + 'enclosure' => '"', + 'line_ending' => ' +', + 'use_bom' => false, + 'include_separator_line' => false, + 'excel_compatibility' => false, + ), + ), + 'imports' => + array ( + 'read_only' => true, + 'heading_row' => + array ( + 'formatter' => 'slug', + ), + 'csv' => + array ( + 'delimiter' => ',', + 'enclosure' => '"', + 'escape_character' => '\\', + 'contiguous' => false, + 'input_encoding' => 'UTF-8', + ), + ), + 'extension_detector' => + array ( + 'xlsx' => 'Xlsx', + 'xlsm' => 'Xlsx', + 'xltx' => 'Xlsx', + 'xltm' => 'Xlsx', + 'xls' => 'Xls', + 'xlt' => 'Xls', + 'ods' => 'Ods', + 'ots' => 'Ods', + 'slk' => 'Slk', + 'xml' => 'Xml', + 'gnumeric' => 'Gnumeric', + 'htm' => 'Html', + 'html' => 'Html', + 'csv' => 'Csv', + 'tsv' => 'Csv', + 'pdf' => 'Dompdf', + ), + 'value_binder' => + array ( + 'default' => 'Maatwebsite\\Excel\\DefaultValueBinder', + ), + 'cache' => + array ( + 'driver' => 'memory', + 'batch' => + array ( + 'memory_limit' => 60000, + ), + 'illuminate' => + array ( + 'store' => NULL, + ), + 'default_ttl' => 10800, + ), + 'transactions' => + array ( + 'handler' => 'db', + ), + 'temporary_files' => + array ( + 'local_path' => '/tmp', + 'remote_disk' => NULL, + ), + ), + 'fewo' => + array ( + 'identifier_content' => 'fewo-pdf-general', + 'identifier_fewo' => 'fewo-pdf-', + ), + 'filesystems' => + array ( + 'default' => 'local', + 'cloud' => 's3', + 'disks' => + array ( + 'local' => + array ( + 'driver' => 'local', + 'root' => '/workspace/mein.sterntours.de/storage/app', + ), + 'public' => + array ( + 'driver' => 'local', + 'root' => '/workspace/mein.sterntours.de/storage/app/public', + 'url' => 'https://mein.sterntours.test/storage', + 'visibility' => 'public', + ), + 'customer' => + array ( + 'driver' => 'local', + 'root' => '/workspace/mein.sterntours.de/storage/app/customer', + 'url' => 'https://mein.sterntours.test/storage/customer', + 'visibility' => 'public', + ), + 'travel_user' => + array ( + 'driver' => 'local', + 'root' => '/workspace/mein.sterntours.de/storage/app/travel_user', + 'url' => 'https://mein.sterntours.test/storage/travel_user', + 'visibility' => 'public', + ), + 'booking' => + array ( + 'driver' => 'local', + 'root' => '/workspace/mein.sterntours.de/storage/app/booking', + 'url' => 'https://mein.sterntours.test/storage/booking', + 'visibility' => 'public', + ), + 'general' => + array ( + 'driver' => 'local', + 'root' => '/workspace/mein.sterntours.de/storage/app/general', + 'url' => 'https://mein.sterntours.test/storage/general', + 'visibility' => 'public', + ), + 'booking_fewo' => + array ( + 'driver' => 'local', + 'root' => '/workspace/mein.sterntours.de/storage/app/booking_fewo', + 'url' => 'https://mein.sterntours.test/storage/booking_fewo', + 'visibility' => 'public', + ), + 'lead' => + array ( + 'driver' => 'local', + 'root' => '/workspace/mein.sterntours.de/storage/app/lead', + 'url' => 'https://mein.sterntours.test/storage/lead', + 'visibility' => 'public', + ), + 'fewo_invoices' => + array ( + 'driver' => 'local', + 'root' => '/workspace/mein.sterntours.de/storage/app/fewo/invoices', + 'url' => 'https://mein.sterntours.test/storage/fewo/invoices', + 'visibility' => 'public', + ), + 'fewo_infos' => + array ( + 'driver' => 'local', + 'root' => '/workspace/mein.sterntours.de/storage/app/fewo/infos', + 'url' => 'https://mein.sterntours.test/storage/fewo/infos', + 'visibility' => 'public', + ), + 's3' => + array ( + 'driver' => 's3', + 'key' => NULL, + 'secret' => NULL, + 'region' => NULL, + 'bucket' => NULL, + 'url' => NULL, + ), + ), + ), + 'fpdf' => + array ( + 'orientation' => 'P', + 'unit' => 'mm', + 'size' => 'A4', + 'useVaporHeaders' => false, + ), + 'google2fa' => + array ( + 'enabled' => true, + 'lifetime' => 0, + 'keep_alive' => true, + 'auth' => 'auth', + 'guard' => '', + 'session_var' => 'google2fa', + 'otp_input' => 'one_time_password', + 'window' => 1, + 'forbid_old_passwords' => false, + 'otp_secret_column' => 'secret_key', + 'view' => 'auth.google2fa', + 'error_messages' => + array ( + 'wrong_otp' => 'Das \'One Time Password\' ist falsch.', + 'cannot_be_empty' => 'Das \'One Time Password\' kann nicht leer sein.', + 'unknown' => 'Ein unbekannter Fehler ist aufgetreten. Bitte versuche es erneut.', + ), + 'throw_exceptions' => true, + 'qrcode_image_backend' => 'svg', + ), + 'hashing' => + array ( + 'driver' => 'bcrypt', + 'bcrypt' => + array ( + 'rounds' => 10, + ), + 'argon' => + array ( + 'memory' => 1024, + 'threads' => 2, + 'time' => 2, + ), + ), + 'ide-helper' => + array ( + 'filename' => '_ide_helper', + 'models_filename' => '_ide_helper_models.php', + 'meta_filename' => '.phpstorm.meta.php', + 'include_fluent' => false, + 'include_factory_builders' => false, + 'write_model_magic_where' => true, + 'write_model_external_builder_methods' => true, + 'write_model_relation_count_properties' => true, + 'write_eloquent_model_mixins' => false, + 'include_helpers' => false, + 'helper_files' => + array ( + 0 => '/workspace/mein.sterntours.de/vendor/laravel/framework/src/Illuminate/Support/helpers.php', + ), + 'model_locations' => + array ( + 0 => 'app', + 1 => 'packages', + ), + 'ignored_models' => + array ( + ), + 'model_hooks' => + array ( + ), + 'extra' => + array ( + 'Eloquent' => + array ( + 0 => 'Illuminate\\Database\\Eloquent\\Builder', + 1 => 'Illuminate\\Database\\Query\\Builder', + ), + 'Session' => + array ( + 0 => 'Illuminate\\Session\\Store', + ), + ), + 'magic' => + array ( + 'Log' => + array ( + 'debug' => 'Monolog\\Logger::addDebug', + 'info' => 'Monolog\\Logger::addInfo', + 'notice' => 'Monolog\\Logger::addNotice', + 'warning' => 'Monolog\\Logger::addWarning', + 'error' => 'Monolog\\Logger::addError', + 'critical' => 'Monolog\\Logger::addCritical', + 'alert' => 'Monolog\\Logger::addAlert', + 'emergency' => 'Monolog\\Logger::addEmergency', + ), + ), + 'interfaces' => + array ( + ), + 'model_camel_case_properties' => false, + 'type_overrides' => + array ( + 'integer' => 'int', + 'boolean' => 'bool', + ), + 'include_class_docblocks' => false, + 'force_fqn' => false, + 'use_generics_annotations' => true, + 'additional_relation_types' => + array ( + ), + 'additional_relation_return_types' => + array ( + ), + 'post_migrate' => + array ( + ), + 'format' => 'php', + 'custom_db_types' => + array ( + ), + ), + 'image' => + array ( + 'driver' => 'gd', + ), + 'lfm' => + array ( + 'use_package_routes' => true, + 'allow_multi_user' => false, + 'allow_share_folder' => false, + 'user_folder_name' => 'IqContent\\LaravelFilemanager\\Handlers\\ConfigHandler', + 'shared_folder_name' => 'shares', + 'thumb_folder_name' => 'thumbs', + 'folder_categories' => + array ( + 'file' => + array ( + 'folder_name' => 'files', + 'startup_view' => 'grid', + 'max_size' => 50000, + 'valid_mime' => + array ( + 0 => 'image/jpeg', + 1 => 'image/pjpeg', + 2 => 'image/png', + 3 => 'image/gif', + 4 => 'image/svg+xml', + 5 => 'application/pdf', + 6 => 'text/plain', + 7 => 'application/msword', + 8 => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 9 => 'application/vnd.ms-word.template.macroEnabled.12', + 10 => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 11 => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 12 => 'application/excel', + ), + ), + 'image' => + array ( + 'folder_name' => 'photos', + 'startup_view' => 'list', + 'max_size' => 50000, + 'valid_mime' => + array ( + 0 => 'image/jpeg', + 1 => 'image/pjpeg', + 2 => 'image/png', + 3 => 'image/gif', + 4 => 'image/svg+xml', + 5 => 'application/pdf', + 6 => 'text/plain', + ), + ), + ), + 'disk' => 'public', + 'rename_file' => false, + 'alphanumeric_filename' => true, + 'alphanumeric_directory' => true, + 'should_validate_size' => false, + 'should_validate_mime' => false, + 'create_folder_mode' => 493, + 'create_file_mode' => 420, + 'should_change_file_mode' => true, + 'over_write_on_duplicate' => false, + 'should_create_thumbnails' => true, + 'raster_mimetypes' => + array ( + 0 => 'image/jpeg', + 1 => 'image/pjpeg', + 2 => 'image/png', + ), + 'thumb_img_width' => 200, + 'thumb_img_height' => 200, + 'default_color' => '#ffc926', + 'resize_aspectRatio' => false, + 'resize_containment' => true, + 'file_type_array' => + array ( + 'pdf' => 'Adobe Acrobat', + 'doc' => 'Microsoft Word', + 'docx' => 'Microsoft Word', + 'xls' => 'Microsoft Excel', + 'xlsx' => 'Microsoft Excel', + 'zip' => 'Archive', + 'gif' => 'GIF Image', + 'jpg' => 'JPEG Image', + 'jpeg' => 'JPEG Image', + 'png' => 'PNG Image', + 'ppt' => 'Microsoft PowerPoint', + 'pptx' => 'Microsoft PowerPoint', + ), + 'file_icon_array' => + array ( + 'pdf' => 'fa-file-pdf', + 'doc' => 'fa-file-word', + 'docx' => 'fa-file-word', + 'xls' => 'fa-file-excel', + 'xlsx' => 'fa-file-excel', + 'zip' => 'fa-file-archive', + 'gif' => 'fa-file-image', + 'jpg' => 'fa-file-image', + 'jpeg' => 'fa-file-image', + 'png' => 'fa-file-image', + 'ppt' => 'fa-file-powerpoint', + 'pptx' => 'fa-file-powerpoint', + 'mp3' => 'fa-file-audio', + 'mp4' => 'fa-file-video', + 'txt' => 'fa-file-alt', + 'dwg' => 'fa-file-image', + 'youtube' => 'fab fa-youtube-square', + ), + 'php_ini_overrides' => + array ( + 'memory_limit' => '256M', + ), + ), + 'localization' => + array ( + 'supportedLocales' => + array ( + 'de' => + array ( + 'name' => 'German', + 'script' => 'Latn', + 'native' => 'Deutsch', + 'regional' => 'de_DE', + ), + ), + ), + 'logging' => + array ( + 'default' => 'stack', + 'channels' => + array ( + 'stack' => + array ( + 'driver' => 'stack', + 'channels' => + array ( + 0 => 'single', + ), + ), + 'single' => + array ( + 'driver' => 'single', + 'path' => '/workspace/mein.sterntours.de/storage/logs/laravel.log', + 'level' => 'debug', + ), + 'daily' => + array ( + 'driver' => 'daily', + 'path' => '/workspace/mein.sterntours.de/storage/logs/laravel.log', + 'level' => 'debug', + 'days' => 7, + ), + 'slack' => + array ( + 'driver' => 'slack', + 'url' => NULL, + 'username' => 'Laravel Log', + 'emoji' => ':boom:', + 'level' => 'critical', + ), + 'stderr' => + array ( + 'driver' => 'monolog', + 'handler' => 'Monolog\\Handler\\StreamHandler', + 'with' => + array ( + 'stream' => 'php://stderr', + ), + ), + 'syslog' => + array ( + 'driver' => 'syslog', + 'level' => 'debug', + ), + 'errorlog' => + array ( + 'driver' => 'errorlog', + 'level' => 'debug', + ), + 'browser' => + array ( + 'driver' => 'single', + 'path' => '/workspace/mein.sterntours.de/storage/logs/browser.log', + 'level' => 'debug', + 'days' => 14, + ), + ), + ), + 'mail' => + array ( + 'driver' => 'smtp', + 'host' => 'global-mailpit', + 'port' => '587', + 'from' => + array ( + 'address' => 'stern@sterntours.de', + 'name' => 'DEV Reisebüro STERN TOURS', + ), + 'mail_bbc' => + array ( + 0 => 'kevin@adametz.media', + ), + 'mail_fewo_employee' => 'kevin@adametz.media', + 'encryption' => 'TLS', + 'username' => 'stern@stern-tours.de', + 'password' => '13C!NlecB!Phil4beAxKl', + 'sendmail' => '/usr/sbin/sendmail -bs', + 'markdown' => + array ( + 'theme' => 'default', + 'paths' => + array ( + 0 => '/workspace/mein.sterntours.de/resources/views/vendor/mail', + ), + ), + ), + 'models' => + array ( + '*' => + array ( + 'path' => '/workspace/mein.sterntours.de/app/Models', + 'namespace' => 'App\\Models', + 'parent' => 'Illuminate\\Database\\Eloquent\\Model', + 'use' => + array ( + ), + 'connection' => false, + 'timestamps' => true, + 'soft_deletes' => true, + 'date_format' => 'Y-m-d H:i:s', + 'per_page' => 15, + 'base_files' => false, + 'snake_attributes' => true, + 'qualified_tables' => false, + 'hidden' => + array ( + 0 => '*secret*', + 1 => '*password', + 2 => '*token', + ), + 'guarded' => + array ( + ), + 'casts' => + array ( + '*_json' => 'json', + ), + 'except' => + array ( + 0 => 'migrations', + ), + ), + ), + 'permissions' => + array ( + 'groups' => + array ( + 0 => + array ( + 'my-dat' => + array ( + 'name' => 'Ihre Daten', + 'color' => 'client', + ), + ), + 1 => + array ( + 'crm' => + array ( + 'name' => 'ADMIN CRM ', + 'color' => 'admin', + ), + 'crm-tp' => + array ( + 'name' => 'ADMIN CRM > Reiseprogramme', + 'color' => 'admin', + ), + 'crm-tp-pr' => + array ( + 'name' => 'ADMIN CRM > Reiseprogramme > Programme', + 'color' => 'admin', + ), + 'crm-tp-dr' => + array ( + 'name' => 'ADMIN CRM > Reiseprogramme > Vorlagen', + 'color' => 'admin', + ), + 'crm-tp-tc' => + array ( + 'name' => 'ADMIN CRM > Reiseprogramme > Inhalte', + 'color' => 'admin', + ), + 'crm-bo' => + array ( + 'name' => 'ADMIN CRM > Buchungen', + 'color' => 'admin', + ), + 'crm-bo-re' => + array ( + 'name' => 'ADMIN CRM > Buchungen > Übersicht', + 'color' => 'admin', + ), + 'crm-bo-bo' => + array ( + 'name' => 'ADMIN CRM > Buchungen > Buchungen', + 'color' => 'admin', + ), + 'crm-bo-le' => + array ( + 'name' => 'ADMIN CRM > Buchungen > Anfragen', + 'color' => 'admin', + ), + 'crm-bo-cu' => + array ( + 'name' => 'ADMIN CRM > Buchungen > Kunden', + 'color' => 'admin', + ), + 'crm-cm' => + array ( + 'name' => 'ADMIN CRM > Kundenverwaltung', + 'color' => 'admin', + ), + 'crm-cm-cf' => + array ( + 'name' => 'ADMIN CRM > Kundenverwaltung > Kunden (FeWo)', + 'color' => 'admin', + ), + 'crm-cm-bf' => + array ( + 'name' => 'ADMIN CRM > Kundenverwaltung > Buchungen (FeWo)', + 'color' => 'admin', + ), + 'crm-mail' => + array ( + 'name' => 'ADMIN CRM > E-Mails', + 'color' => 'admin', + ), + 'crm-mail-le' => + array ( + 'name' => 'ADMIN CRM > E-Mails > Anfragen', + 'color' => 'admin', + ), + 'crm-mail-bo' => + array ( + 'name' => 'ADMIN CRM > E-Mails > Buchungen', + 'color' => 'admin', + ), + 'crm-mail-bf' => + array ( + 'name' => 'ADMIN CRM > E-Mails > Buchungen (Fewo)', + 'color' => 'admin', + ), + 'crm-iq-tl' => + array ( + 'name' => 'ADMIN CRM > Reisebausteine', + 'color' => 'admin', + ), + 'crm-iq-tl-pro' => + array ( + 'name' => 'ADMIN CRM > Reisebausteine > Programm', + 'color' => 'admin', + ), + 'crm-iq-tl-gp' => + array ( + 'name' => 'ADMIN CRM > Reisebausteine > Gruppe', + 'color' => 'admin', + ), + 'crm-iq-tl-it' => + array ( + 'name' => 'ADMIN CRM > Reisebausteine > Baustein', + 'color' => 'admin', + ), + 'crm-old-cm' => + array ( + 'name' => 'ADMIN CRM altes System > Kundenverwaltung', + 'color' => 'info', + ), + 'cms' => + array ( + 'name' => 'ADMIN CMS', + 'color' => 'secondary', + ), + 'cms-iq-assets' => + array ( + 'name' => 'ADMIN CMS > Medien', + 'color' => 'secondary', + ), + 'cms-tg' => + array ( + 'name' => 'ADMIN CMS > Reiseführer', + 'color' => 'secondary', + ), + 'cms-fewo' => + array ( + 'name' => 'ADMIN CMS > FeWo', + 'color' => 'secondary', + ), + 'cms-book' => + array ( + 'name' => 'ADMIN CMS > Buchungen', + 'color' => 'secondary', + ), + 'cms-fb' => + array ( + 'name' => 'ADMIN CMS > Feedback', + 'color' => 'secondary', + ), + 'cms-nw' => + array ( + 'name' => 'ADMIN CMS > News', + 'color' => 'secondary', + ), + 'cms-aq' => + array ( + 'name' => 'ADMIN CMS > Fragen & Antworten', + 'color' => 'secondary', + ), + 'cms-sb' => + array ( + 'name' => 'ADMIN CMS > Sidebar', + 'color' => 'secondary', + ), + 'cms-cn' => + array ( + 'name' => 'ADMIN CMS > Inhalte', + 'color' => 'secondary', + ), + 'cms-cn-in' => + array ( + 'name' => 'ADMIN CMS > Inhalte > Infos', + 'color' => 'secondary', + ), + 'cms-cn-al' => + array ( + 'name' => 'ADMIN CMS > Inhalte > Inhalte', + 'color' => 'secondary', + ), + 'cms-cn-au' => + array ( + 'name' => 'ADMIN CMS > Inhalte > Autor', + 'color' => 'secondary', + ), + 'cms-cn-co' => + array ( + 'name' => 'ADMIN CMS > Inhalte > Länder', + 'color' => 'secondary', + ), + 'cms-newsletter' => + array ( + 'name' => 'ADMIN CMS > Newsletter', + 'color' => 'secondary', + ), + 'crm-nav-api' => + array ( + 'name' => 'ADMIN CRM > Navigation API', + 'color' => 'secondary', + ), + ), + 2 => + array ( + 'sua-bo-n-edit' => + array ( + 'name' => 'SUPERADMIN > Buchungen > Notizen > bearbeiten', + 'color' => 'secondary', + ), + 'sua-fewo-n-edit' => + array ( + 'name' => 'SUPERADMIN > FeWo > Notizen > bearbeiten', + 'color' => 'secondary', + ), + 'sua-st' => + array ( + 'name' => 'SUPERADMIN > Einstellungen', + 'color' => 'superadmin', + ), + 'sua-st-al' => + array ( + 'name' => 'SUPERADMIN > Einstellungen > Airline', + 'color' => 'superadmin', + ), + 'sua-st-ap' => + array ( + 'name' => 'SUPERADMIN > Einstellungen > Airport', + 'color' => 'superadmin', + ), + 'sua-st-em' => + array ( + 'name' => 'SUPERADMIN > Einstellungen > E-Mails', + 'color' => 'superadmin', + ), + 'sua-st-ke' => + array ( + 'name' => 'SUPERADMIN > Einstellungen > Keywords', + 'color' => 'superadmin', + ), + 'sua-st-sp' => + array ( + 'name' => 'SUPERADMIN > Einstellungen > Leistungsträger', + 'color' => 'superadmin', + ), + 'sua-st-tn' => + array ( + 'name' => 'SUPERADMIN > Einstellungen > Nationalitäten', + 'color' => 'superadmin', + ), + 'sua-st-co' => + array ( + 'name' => 'SUPERADMIN > Einstellungen > Reiseländer', + 'color' => 'superadmin', + ), + 'sua-st-tp' => + array ( + 'name' => 'SUPERADMIN > Einstellungen > Reiseprogramme', + 'color' => 'superadmin', + ), + 'sua-st-tpl' => + array ( + 'name' => 'SUPERADMIN > Einstellungen > Reiseorte', + 'color' => 'superadmin', + ), + 'sua-st-bs' => + array ( + 'name' => 'SUPERADMIN > Einstellungen > Reisestatus', + 'color' => 'superadmin', + ), + 'sua-st-tc' => + array ( + 'name' => 'SUPERADMIN > Einstellungen > Veranstalter', + 'color' => 'superadmin', + ), + 'sua-st-tca' => + array ( + 'name' => 'SUPERADMIN > Einstellungen > Reiseart', + 'color' => 'superadmin', + ), + 'sua-st-tap' => + array ( + 'name' => 'SUPERADMIN > Einstellungen > Zielflughafen', + 'color' => 'superadmin', + ), + 'sua-st-in' => + array ( + 'name' => 'SUPERADMIN > Einstellungen > Versicherungen', + 'color' => 'superadmin', + ), + 'sua-st-ca' => + array ( + 'name' => 'SUPERADMIN > Einstellungen > Kategorien', + 'color' => 'superadmin', + ), + 'sua-st-tgn' => + array ( + 'name' => 'SUPERADMIN > Einstellungen > Reisehinweise', + 'color' => 'superadmin', + ), + 'sua-re' => + array ( + 'name' => 'SUPERADMIN > Export', + 'color' => 'superadmin', + ), + 'sua-re-bo' => + array ( + 'name' => 'SUPERADMIN > Export > Buchungen', + 'color' => 'superadmin', + ), + 'sua-re-pp' => + array ( + 'name' => 'SUPERADMIN > Export > Leistungsträger', + 'color' => 'superadmin', + ), + 'sua-re-fw' => + array ( + 'name' => 'SUPERADMIN > Export > Fewo', + 'color' => 'superadmin', + ), + 'sua-re-le' => + array ( + 'name' => 'SUPERADMIN > Export > Anfragen', + 'color' => 'superadmin', + ), + 'sua-ur-rt' => + array ( + 'name' => 'SUPERADMIN > User Rechte', + 'color' => 'danger', + ), + ), + ), + 'roles' => + array ( + 0 => 'Kunde', + 1 => 'Admin', + 2 => 'SuperAdmin', + ), + ), + 'queue' => + array ( + 'default' => 'sync', + 'connections' => + array ( + 'sync' => + array ( + 'driver' => 'sync', + ), + 'database' => + array ( + 'driver' => 'database', + 'table' => 'jobs', + 'queue' => 'default', + 'retry_after' => 90, + ), + 'beanstalkd' => + array ( + 'driver' => 'beanstalkd', + 'host' => 'localhost', + 'queue' => 'default', + 'retry_after' => 90, + ), + 'sqs' => + array ( + 'driver' => 'sqs', + 'key' => 'your-public-key', + 'secret' => 'your-secret-key', + 'prefix' => 'https://sqs.us-east-1.amazonaws.com/your-account-id', + 'queue' => 'your-queue-name', + 'region' => 'us-east-1', + ), + 'redis' => + array ( + 'driver' => 'redis', + 'connection' => 'default', + 'queue' => 'default', + 'retry_after' => 90, + 'block_for' => NULL, + ), + ), + 'failed' => + array ( + 'database' => 'mysql', + 'table' => 'failed_jobs', + ), + ), + 'services' => + array ( + 'mailgun' => + array ( + 'domain' => NULL, + 'secret' => NULL, + ), + 'ses' => + array ( + 'key' => NULL, + 'secret' => NULL, + 'region' => 'us-east-1', + ), + 'sparkpost' => + array ( + 'secret' => NULL, + ), + 'stripe' => + array ( + 'model' => 'App\\User', + 'key' => NULL, + 'secret' => NULL, + ), + ), + 'session' => + array ( + 'driver' => 'file', + 'lifetime' => '120', + 'expire_on_close' => false, + 'encrypt' => false, + 'files' => '/workspace/mein.sterntours.de/storage/framework/sessions', + 'connection' => NULL, + 'table' => 'sessions', + 'store' => NULL, + 'lottery' => + array ( + 0 => 2, + 1 => 100, + ), + 'cookie' => 'stern_tours_crm_session', + 'path' => '/', + 'domain' => NULL, + 'secure' => false, + 'http_only' => true, + 'same_site' => NULL, + ), + 'tinker' => + array ( + 'commands' => + array ( + ), + 'alias' => + array ( + ), + 'dont_alias' => + array ( + ), + 'trust_project' => 'always', + ), + 'trustedproxy' => + array ( + 'proxies' => NULL, + 'headers' => 62, + ), + 'view' => + array ( + 'paths' => + array ( + 0 => '/workspace/mein.sterntours.de/resources/views', + ), + 'compiled' => '/workspace/mein.sterntours.de/storage/framework/views', + ), + 'cart' => + array ( + 'tax' => 10, + 'database' => + array ( + 'connection' => NULL, + 'table' => 'shoppingcart', + ), + 'destroy_on_logout' => false, + 'format' => + array ( + 'decimals' => 2, + 'decimal_point' => '.', + 'thousand_seperator' => '', + ), + 'discountOnFees' => false, + ), + 'boost' => + array ( + 'enabled' => true, + 'browser_logs_watcher' => true, + 'executable_paths' => + array ( + 'php' => NULL, + 'composer' => NULL, + 'npm' => NULL, + 'vendor_bin' => NULL, + ), + ), + 'mcp' => + array ( + 'redirect_domains' => + array ( + 0 => '*', + ), + ), + 'passport' => + array ( + 'guard' => 'web', + 'private_key' => NULL, + 'public_key' => NULL, + 'client_uuids' => false, + 'personal_access_client' => + array ( + 'id' => NULL, + 'secret' => NULL, + ), + ), + 'flare' => + array ( + 'key' => NULL, + 'flare_middleware' => + array ( + 0 => 'Spatie\\FlareClient\\FlareMiddleware\\RemoveRequestIp', + 1 => 'Spatie\\FlareClient\\FlareMiddleware\\AddGitInformation', + 2 => 'Spatie\\LaravelIgnition\\FlareMiddleware\\AddNotifierName', + 3 => 'Spatie\\LaravelIgnition\\FlareMiddleware\\AddEnvironmentInformation', + 4 => 'Spatie\\LaravelIgnition\\FlareMiddleware\\AddExceptionInformation', + 5 => 'Spatie\\LaravelIgnition\\FlareMiddleware\\AddDumps', + 'Spatie\\LaravelIgnition\\FlareMiddleware\\AddLogs' => + array ( + 'maximum_number_of_collected_logs' => 200, + ), + 'Spatie\\LaravelIgnition\\FlareMiddleware\\AddQueries' => + array ( + 'maximum_number_of_collected_queries' => 200, + 'report_query_bindings' => true, + ), + 'Spatie\\LaravelIgnition\\FlareMiddleware\\AddJobs' => + array ( + 'max_chained_job_reporting_depth' => 5, + ), + 6 => 'Spatie\\LaravelIgnition\\FlareMiddleware\\AddContext', + 7 => 'Spatie\\LaravelIgnition\\FlareMiddleware\\AddExceptionHandledStatus', + 'Spatie\\FlareClient\\FlareMiddleware\\CensorRequestBodyFields' => + array ( + 'censor_fields' => + array ( + 0 => 'password', + 1 => 'password_confirmation', + ), + ), + 'Spatie\\FlareClient\\FlareMiddleware\\CensorRequestHeaders' => + array ( + 'headers' => + array ( + 0 => 'API-KEY', + 1 => 'Authorization', + 2 => 'Cookie', + 3 => 'Set-Cookie', + 4 => 'X-CSRF-TOKEN', + 5 => 'X-XSRF-TOKEN', + ), + ), + ), + 'send_logs_as_events' => true, + ), + 'ignition' => + array ( + 'editor' => 'phpstorm', + 'theme' => 'auto', + 'enable_share_button' => true, + 'register_commands' => false, + 'solution_providers' => + array ( + 0 => 'Spatie\\Ignition\\Solutions\\SolutionProviders\\BadMethodCallSolutionProvider', + 1 => 'Spatie\\Ignition\\Solutions\\SolutionProviders\\MergeConflictSolutionProvider', + 2 => 'Spatie\\Ignition\\Solutions\\SolutionProviders\\UndefinedPropertySolutionProvider', + 3 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\IncorrectValetDbCredentialsSolutionProvider', + 4 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\MissingAppKeySolutionProvider', + 5 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\DefaultDbNameSolutionProvider', + 6 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\TableNotFoundSolutionProvider', + 7 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\MissingImportSolutionProvider', + 8 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\InvalidRouteActionSolutionProvider', + 9 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\ViewNotFoundSolutionProvider', + 10 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\RunningLaravelDuskInProductionProvider', + 11 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\MissingColumnSolutionProvider', + 12 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\UnknownValidationSolutionProvider', + 13 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\MissingMixManifestSolutionProvider', + 14 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\MissingViteManifestSolutionProvider', + 15 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\MissingLivewireComponentSolutionProvider', + 16 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\UndefinedViewVariableSolutionProvider', + 17 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\GenericLaravelExceptionSolutionProvider', + 18 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\OpenAiSolutionProvider', + 19 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\SailNetworkSolutionProvider', + 20 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\UnknownMysql8CollationSolutionProvider', + 21 => 'Spatie\\LaravelIgnition\\Solutions\\SolutionProviders\\UnknownMariadbCollationSolutionProvider', + ), + 'ignored_solution_providers' => + array ( + ), + 'enable_runnable_solutions' => NULL, + 'remote_sites_path' => '/workspace/mein.sterntours.de', + 'local_sites_path' => '', + 'housekeeping_endpoint_prefix' => '_ignition', + 'settings_file_path' => '', + 'recorders' => + array ( + 0 => 'Spatie\\LaravelIgnition\\Recorders\\DumpRecorder\\DumpRecorder', + 1 => 'Spatie\\LaravelIgnition\\Recorders\\JobRecorder\\JobRecorder', + 2 => 'Spatie\\LaravelIgnition\\Recorders\\LogRecorder\\LogRecorder', + 3 => 'Spatie\\LaravelIgnition\\Recorders\\QueryRecorder\\QueryRecorder', + ), + 'open_ai_key' => NULL, + 'with_stack_frame_arguments' => true, + 'argument_reducers' => + array ( + 0 => 'Spatie\\Backtrace\\Arguments\\Reducers\\BaseTypeArgumentReducer', + 1 => 'Spatie\\Backtrace\\Arguments\\Reducers\\ArrayArgumentReducer', + 2 => 'Spatie\\Backtrace\\Arguments\\Reducers\\StdClassArgumentReducer', + 3 => 'Spatie\\Backtrace\\Arguments\\Reducers\\EnumArgumentReducer', + 4 => 'Spatie\\Backtrace\\Arguments\\Reducers\\ClosureArgumentReducer', + 5 => 'Spatie\\Backtrace\\Arguments\\Reducers\\DateTimeArgumentReducer', + 6 => 'Spatie\\Backtrace\\Arguments\\Reducers\\DateTimeZoneArgumentReducer', + 7 => 'Spatie\\Backtrace\\Arguments\\Reducers\\SymphonyRequestArgumentReducer', + 8 => 'Spatie\\LaravelIgnition\\ArgumentReducers\\ModelArgumentReducer', + 9 => 'Spatie\\LaravelIgnition\\ArgumentReducers\\CollectionArgumentReducer', + 10 => 'Spatie\\Backtrace\\Arguments\\Reducers\\StringableArgumentReducer', + ), + ), + 'sluggable' => + array ( + 'source' => NULL, + 'maxLength' => NULL, + 'maxLengthKeepWords' => true, + 'method' => NULL, + 'separator' => '-', + 'unique' => true, + 'uniqueSuffix' => NULL, + 'firstUniqueSuffix' => 2, + 'includeTrashed' => false, + 'reserved' => NULL, + 'onUpdate' => false, + 'slugEngineOptions' => + array ( + ), + ), +); diff --git a/config/app.php b/config/app.php index 4cf57f2..ab7a418 100755 --- a/config/app.php +++ b/config/app.php @@ -40,7 +40,6 @@ return [ */ 'debug' => env('APP_DEBUG', false), - 'exception_mail' => env('EXCEPTION_MAIL', 'exception@adametz.media'), /* |-------------------------------------------------------------------------- diff --git a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php b/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php deleted file mode 100644 index 17cd0b4..0000000 --- a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php +++ /dev/null @@ -1,30 +0,0 @@ -dropForeign('booking_lead_id_lead_id'); - }); - - DB::statement('ALTER TABLE `booking` RENAME COLUMN `lead_id` TO `inquiry_id`'); - - Schema::table('booking', function (Blueprint $table) { - // Neuen FK auf umbenannte Tabelle setzen - $table->foreign('inquiry_id', 'booking_inquiry_id_inquiries_id') - ->references('id')->on('inquiries') - ->onDelete('no action') - ->onUpdate('no action'); - }); - } - - public function down(): void - { - Schema::table('booking', function (Blueprint $table) { - $table->dropForeign('booking_inquiry_id_inquiries_id'); - }); - - DB::statement('ALTER TABLE `booking` RENAME COLUMN `inquiry_id` TO `lead_id`'); - - Schema::table('booking', function (Blueprint $table) { - $table->foreign('lead_id', 'booking_lead_id_lead_id') - ->references('id')->on('lead') - ->onDelete('no action') - ->onUpdate('no action'); - }); - } -}; diff --git a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2025_04_15_300001_phase3_create_participants_unified_table.php b/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2025_04_15_300001_phase3_create_participants_unified_table.php deleted file mode 100644 index 70a1c2f..0000000 --- a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2025_04_15_300001_phase3_create_participants_unified_table.php +++ /dev/null @@ -1,123 +0,0 @@ -id(); - - // Kontext-FKs — genau einer ist gesetzt, der andere NULL - $table->unsignedBigInteger('inquiry_id')->nullable(); - $table->unsignedBigInteger('booking_id')->nullable(); - - // Teilnehmerdaten - $table->string('participant_name')->nullable(); - $table->string('participant_firstname')->nullable(); - $table->date('participant_birthdate')->nullable(); - $table->unsignedBigInteger('participant_salutation_id')->nullable(); - $table->boolean('participant_child')->default(false); - $table->integer('nationality_id')->nullable(); - - // Nur bei Buchungs-Teilnehmern - $table->boolean('participant_pass')->default(false); - $table->boolean('participant_storno')->default(false); - - // Markiert den Hauptreisenden aus lead.participant_name - $table->boolean('is_lead_contact')->default(false); - - $table->timestamps(); - - $table->index('inquiry_id'); - $table->index('booking_id'); - - $table->foreign('inquiry_id', 'pu_inquiry_id_fk') - ->references('id')->on('inquiries') - ->onDelete('cascade'); - - $table->foreign('booking_id', 'pu_booking_id_fk') - ->references('id')->on('booking') - ->onDelete('cascade'); - - $table->foreign('participant_salutation_id', 'pu_salutation_id_fk') - ->references('id')->on('salutation') - ->onDelete('set null'); - }); - - // ── Daten migrieren ───────────────────────────────────────────────── - - // 1. Aus lead_participant → inquiry_id gesetzt - DB::statement(" - INSERT INTO participants_unified - (inquiry_id, booking_id, participant_name, participant_firstname, - participant_birthdate, participant_salutation_id, participant_child, - nationality_id, is_lead_contact, created_at, updated_at) - SELECT - lead_id, NULL, - participant_name, participant_firstname, - participant_birthdate, participant_salutation_id, participant_child, - nationality_id, 0, - NOW(), NOW() - FROM lead_participant - "); - - // 2. Aus lead.participant_name → Hauptreisende-Datensätze (is_lead_contact = 1) - DB::statement(" - INSERT INTO participants_unified - (inquiry_id, booking_id, participant_name, participant_firstname, - participant_birthdate, participant_salutation_id, participant_child, - nationality_id, is_lead_contact, created_at, updated_at) - SELECT - id, NULL, - participant_name, participant_firstname, - participant_birthdate, participant_salutation_id, 0, - NULL, 1, - NOW(), NOW() - FROM inquiries - WHERE participant_name IS NOT NULL - AND participant_name != '' - "); - - // 3. Aus participant → booking_id gesetzt - DB::statement(" - INSERT INTO participants_unified - (inquiry_id, booking_id, participant_name, participant_firstname, - participant_birthdate, participant_salutation_id, participant_child, - nationality_id, participant_pass, participant_storno, - is_lead_contact, created_at, updated_at) - SELECT - NULL, booking_id, - participant_name, participant_firstname, - participant_birthdate, participant_salutation_id, participant_child, - nationality_id, participant_pass, participant_storno, - 0, NOW(), NOW() - FROM participant - "); - } - - public function down(): void - { - Schema::dropIfExists('participants_unified'); - } -}; diff --git a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2025_04_15_300002_phase3_drop_old_participant_tables.php b/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2025_04_15_300002_phase3_drop_old_participant_tables.php deleted file mode 100644 index 3f2cbec..0000000 --- a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2025_04_15_300002_phase3_drop_old_participant_tables.php +++ /dev/null @@ -1,40 +0,0 @@ -id(); - - // Kontext-FKs — bei customer_mails können beide gesetzt sein - $table->unsignedBigInteger('inquiry_id')->nullable(); - $table->unsignedBigInteger('booking_id')->nullable(); - $table->unsignedBigInteger('contact_id')->nullable(); - - // Reply-Chain — kein FK-Constraint (cross-table Remapping) - $table->unsignedBigInteger('reply_id')->nullable(); - - // Herkunft für Remapping und spätere Analyse - $table->enum('legacy_source', ['lead_mail', 'customer_mail']); - $table->unsignedBigInteger('legacy_id'); - - // Mail-Felder (identisch in beiden Quell-Tabellen) - $table->boolean('is_answer')->default(false); - $table->string('email')->nullable(); - $table->text('recipient')->nullable(); - $table->text('cc')->nullable(); - $table->text('bcc')->nullable(); - $table->string('subject')->nullable(); - $table->text('message')->nullable(); - $table->boolean('dir')->default(false); - $table->unsignedBigInteger('subdir')->nullable(); - - // Nur in customer_mails vorhanden - $table->unsignedBigInteger('travel_country_id')->nullable(); - - $table->boolean('draft')->default(false); - $table->boolean('important')->default(false); - $table->boolean('send')->default(false); - $table->boolean('fail')->default(false); - $table->text('error')->nullable(); - $table->text('forward')->nullable(); - - $table->dateTime('sent_at')->nullable(); - $table->dateTime('scheduled_at')->nullable(); - $table->dateTime('delivered_at')->nullable(); - - $table->timestamps(); - - $table->index('inquiry_id'); - $table->index('booking_id'); - $table->index('contact_id'); - $table->index('reply_id'); - $table->index(['legacy_source', 'legacy_id'], 'comm_legacy_idx'); - - $table->foreign('inquiry_id', 'comm_inquiry_id_fk') - ->references('id')->on('inquiries') - ->onDelete('cascade'); - - $table->foreign('booking_id', 'comm_booking_id_fk') - ->references('id')->on('booking') - ->onDelete('cascade'); - - $table->foreign('contact_id', 'comm_contact_id_fk') - ->references('id')->on('contacts') - ->onDelete('set null'); - }); - - // ── Daten migrieren ───────────────────────────────────────────────── - - // 1. Aus lead_mails → inquiry_id gesetzt - DB::statement(" - INSERT INTO communications - (inquiry_id, booking_id, contact_id, is_answer, reply_id, - legacy_source, legacy_id, - email, recipient, cc, bcc, subject, message, - dir, subdir, travel_country_id, - draft, important, send, fail, error, forward, - sent_at, scheduled_at, delivered_at, created_at, updated_at) - SELECT - lead_id, NULL, customer_id, is_answer, reply_id, - 'lead_mail', id, - email, recipient, cc, bcc, subject, message, - dir, subdir, NULL, - draft, important, send, fail, error, forward, - sent_at, scheduled_at, delivered_at, created_at, updated_at - FROM lead_mails - "); - - // 2. Aus customer_mails → booking_id gesetzt (kann zusätzlich lead_id haben) - DB::statement(" - INSERT INTO communications - (inquiry_id, booking_id, contact_id, is_answer, reply_id, - legacy_source, legacy_id, - email, recipient, cc, bcc, subject, message, - dir, subdir, travel_country_id, - draft, important, send, fail, error, forward, - sent_at, scheduled_at, delivered_at, created_at, updated_at) - SELECT - lead_id, booking_id, customer_id, is_answer, reply_id, - 'customer_mail', id, - email, recipient, cc, bcc, subject, message, - dir, subdir, travel_country_id, - draft, important, send, fail, error, forward, - sent_at, scheduled_at, delivered_at, created_at, updated_at - FROM customer_mails - "); - - // 3. Reply-IDs remappen: alte source-table-IDs auf neue communications-IDs umstellen - // Funktioniert weil reply_id immer auf eine Mail derselben Quell-Tabelle zeigt. - DB::statement(" - UPDATE communications c1 - INNER JOIN communications c2 - ON c1.legacy_source = c2.legacy_source - AND c2.legacy_id = c1.reply_id - SET c1.reply_id = c2.id - WHERE c1.reply_id IS NOT NULL - "); - } - - public function down(): void - { - Schema::dropIfExists('communications'); - } -}; diff --git a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2025_04_15_400002_phase4_create_notices_table.php b/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2025_04_15_400002_phase4_create_notices_table.php deleted file mode 100644 index 624c891..0000000 --- a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2025_04_15_400002_phase4_create_notices_table.php +++ /dev/null @@ -1,94 +0,0 @@ -id(); - - // Kontext-FKs — genau einer ist gesetzt - $table->unsignedBigInteger('inquiry_id')->nullable(); - $table->unsignedBigInteger('booking_id')->nullable(); - - // Benutzer-Referenzen (integer wie in den Quell-Tabellen) - $table->unsignedInteger('from_user_id'); - $table->unsignedInteger('to_user_id')->nullable(); - - $table->text('message')->nullable(); - $table->boolean('show')->default(false); - $table->boolean('important')->default(false); - $table->dateTime('edit_at')->nullable(); - - $table->timestamps(); - - $table->index('inquiry_id'); - $table->index('booking_id'); - $table->index('from_user_id'); - $table->index('to_user_id'); - - $table->foreign('inquiry_id', 'notices_inquiry_id_fk') - ->references('id')->on('inquiries') - ->onDelete('cascade'); - - $table->foreign('booking_id', 'notices_booking_id_fk') - ->references('id')->on('booking') - ->onDelete('cascade'); - - $table->foreign('from_user_id', 'notices_from_user_id_fk') - ->references('id')->on('users') - ->onDelete('cascade'); - - $table->foreign('to_user_id', 'notices_to_user_id_fk') - ->references('id')->on('users') - ->onDelete('cascade'); - }); - - // ── Daten migrieren ───────────────────────────────────────────────── - - // 1. Aus lead_notices → inquiry_id gesetzt - DB::statement(" - INSERT INTO notices - (inquiry_id, booking_id, from_user_id, to_user_id, - message, show, important, edit_at, created_at, updated_at) - SELECT - lead_id, NULL, from_user_id, to_user_id, - message, show, important, edit_at, created_at, updated_at - FROM lead_notices - "); - - // 2. Aus booking_notices → booking_id gesetzt - DB::statement(" - INSERT INTO notices - (inquiry_id, booking_id, from_user_id, to_user_id, - message, show, important, edit_at, created_at, updated_at) - SELECT - NULL, booking_id, from_user_id, to_user_id, - message, show, important, edit_at, created_at, updated_at - FROM booking_notices - "); - } - - public function down(): void - { - Schema::dropIfExists('notices'); - } -}; diff --git a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2025_04_15_400003_phase4_create_attachments_table.php b/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2025_04_15_400003_phase4_create_attachments_table.php deleted file mode 100644 index 7f44feb..0000000 --- a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2025_04_15_400003_phase4_create_attachments_table.php +++ /dev/null @@ -1,118 +0,0 @@ -id(); - - // Kontext-FKs - $table->unsignedBigInteger('inquiry_id')->nullable(); - $table->unsignedBigInteger('booking_id')->nullable(); - - // Verknüpfung zur dazugehörigen Mail (nur bei Anfrage-Anhängen) - $table->unsignedBigInteger('communication_id')->nullable(); - - // Datei-Metadaten - $table->string('identifier')->nullable(); - $table->string('filename'); - $table->string('dir')->nullable(); - $table->string('original_name')->nullable(); - $table->string('ext')->nullable(); - $table->string('mime_type')->nullable(); - $table->unsignedInteger('size')->nullable(); - - // Herkunft für spätere Analyse / Rollback - $table->enum('legacy_source', ['lead_file', 'booking_file']); - $table->unsignedBigInteger('legacy_id'); - - $table->timestamps(); - - $table->index('inquiry_id'); - $table->index('booking_id'); - $table->index('communication_id'); - $table->index('identifier', 'attachments_identifier_idx'); - $table->index(['legacy_source', 'legacy_id'], 'attachments_legacy_idx'); - - $table->foreign('inquiry_id', 'att_inquiry_id_fk') - ->references('id')->on('inquiries') - ->onDelete('cascade'); - - $table->foreign('booking_id', 'att_booking_id_fk') - ->references('id')->on('booking') - ->onDelete('cascade'); - - $table->foreign('communication_id', 'att_communication_id_fk') - ->references('id')->on('communications') - ->onDelete('set null'); - }); - - // ── Daten migrieren ───────────────────────────────────────────────── - - // 1. Aus lead_files → inquiry_id gesetzt; communication_id wird unten gesetzt - DB::statement(" - INSERT INTO attachments - (inquiry_id, booking_id, communication_id, - identifier, filename, dir, original_name, ext, mime_type, size, - legacy_source, legacy_id, created_at, updated_at) - SELECT - lead_id, NULL, NULL, - identifier, filename, dir, original_name, ext, mine, size, - 'lead_file', id, created_at, updated_at - FROM lead_files - "); - - // 2. Aus booking_files → booking_id gesetzt - DB::statement(" - INSERT INTO attachments - (inquiry_id, booking_id, communication_id, - identifier, filename, dir, original_name, ext, mime_type, size, - legacy_source, legacy_id, created_at, updated_at) - SELECT - NULL, booking_id, NULL, - identifier, filename, dir, original_name, ext, mine, size, - 'booking_file', id, created_at, updated_at - FROM booking_files - "); - - // 3. communication_id für lead_files setzen (sofern lead_mail_id gesetzt war) - // Nutzt legacy_source + legacy_id der communications-Tabelle. - DB::statement(" - UPDATE attachments a - INNER JOIN lead_files lf ON lf.id = a.legacy_id - INNER JOIN communications c - ON c.legacy_source = 'lead_mail' - AND c.legacy_id = lf.lead_mail_id - SET a.communication_id = c.id - WHERE a.legacy_source = 'lead_file' - AND lf.lead_mail_id IS NOT NULL - "); - } - - public function down(): void - { - Schema::dropIfExists('attachments'); - } -}; diff --git a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2025_04_15_400004_phase4_drop_old_communication_tables.php b/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2025_04_15_400004_phase4_drop_old_communication_tables.php deleted file mode 100644 index bbdff3f..0000000 --- a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2025_04_15_400004_phase4_drop_old_communication_tables.php +++ /dev/null @@ -1,45 +0,0 @@ -dropForeign('lead_files_lead_mail_id_foreign'); - }); - } - - Schema::dropIfExists('lead_mails'); - Schema::dropIfExists('customer_mails'); - } - - public function down(): void - { - throw new \RuntimeException( - 'Phase 4 Schritt 2a kann nicht automatisch zurückgerollt werden. ' . - 'Bitte Datenbank-Backup einspielen.' - ); - } -}; diff --git a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2025_04_15_400005_phase4_drop_old_notice_tables.php b/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2025_04_15_400005_phase4_drop_old_notice_tables.php deleted file mode 100644 index d50006d..0000000 --- a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2025_04_15_400005_phase4_drop_old_notice_tables.php +++ /dev/null @@ -1,35 +0,0 @@ -id(); - $t->string('offer_number', 32)->unique(); - - $t->foreignId('contact_id') - ->constrained('contacts') - ->restrictOnDelete(); - - $t->foreignId('inquiry_id') - ->nullable() - ->constrained('inquiries') - ->nullOnDelete(); - - $t->foreignId('booking_id') - ->nullable() - ->constrained('booking') - ->nullOnDelete(); - - $t->enum('status', [ - 'draft', - 'sent', - 'accepted', - 'declined', - 'expired', - 'withdrawn', - ])->default('draft'); - - // FK wird in 2026_04_17_100007 nachträglich gesetzt - $t->unsignedBigInteger('current_version_id')->nullable(); - - $t->foreignId('created_by')->constrained('users'); - - $t->timestamps(); - $t->softDeletes(); - - $t->index(['status', 'contact_id']); - $t->index('inquiry_id'); - }); - } - - public function down(): void - { - Schema::dropIfExists('offers'); - } -}; diff --git a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2026_04_17_100002_create_offer_versions_table.php b/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2026_04_17_100002_create_offer_versions_table.php deleted file mode 100644 index 2cc6e30..0000000 --- a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2026_04_17_100002_create_offer_versions_table.php +++ /dev/null @@ -1,81 +0,0 @@ -id(); - - $t->foreignId('offer_id') - ->constrained('offers') - ->cascadeOnDelete(); - - $t->unsignedInteger('version_no'); - - $t->enum('status', [ - 'draft', - 'sent', - 'accepted', - 'declined', - 'expired', - 'superseded', - ])->default('draft'); - - $t->date('valid_until')->nullable(); - $t->decimal('total_price', 10, 2)->default(0); - - $t->string('headline')->nullable(); - $t->text('intro_text')->nullable(); - $t->longText('itinerary_text')->nullable(); - $t->text('closing_text')->nullable(); - - // offer_templates wird in Migration 4 erzeugt — FK dort, - // hier zunächst nullable + FK wird über Migration 4 nachgeholt - $t->unsignedBigInteger('template_id')->nullable(); - - $t->string('pdf_path')->nullable(); - $t->boolean('pdf_archived')->default(false); - - $t->dateTime('sent_at')->nullable(); - $t->dateTime('accepted_at')->nullable(); - - $t->enum('accepted_via', [ - 'customer_link', - 'admin', - 'email', - ])->nullable(); - - // Referenz auf zentral hinterlegte Dokument-Vorlagen, - // die mit dieser Version (als Anhang) verknüpft sind - $t->json('template_document_ids')->nullable(); - - $t->foreignId('created_by')->constrained('users'); - - $t->timestamps(); - - $t->unique(['offer_id', 'version_no']); - $t->index(['offer_id', 'status']); - }); - } - - public function down(): void - { - Schema::dropIfExists('offer_versions'); - } -}; diff --git a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2026_04_17_100003_create_offer_items_table.php b/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2026_04_17_100003_create_offer_items_table.php deleted file mode 100644 index c540a9b..0000000 --- a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2026_04_17_100003_create_offer_items_table.php +++ /dev/null @@ -1,68 +0,0 @@ -id(); - - $t->foreignId('offer_version_id') - ->constrained('offer_versions') - ->cascadeOnDelete(); - - $t->unsignedInteger('position')->default(0); - - $t->enum('type', [ - 'travel', - 'service', - 'option', - 'discount', - 'insurance', - 'custom', - ]); - - $t->string('title'); - $t->text('description')->nullable(); - - $t->unsignedInteger('quantity')->default(1); - $t->decimal('price_per_unit', 10, 2)->default(0); - $t->decimal('total_price', 10, 2)->default(0); - - // Bewusst OHNE FK-Constraint — siehe Risiko R4 - $t->unsignedBigInteger('travel_program_id')->nullable(); - $t->unsignedBigInteger('fewo_lodging_id')->nullable(); - - $t->json('metadata')->nullable(); - - $t->timestamps(); - - $t->index(['offer_version_id', 'position']); - }); - } - - public function down(): void - { - Schema::dropIfExists('offer_items'); - } -}; diff --git a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2026_04_17_100004_create_offer_templates_table.php b/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2026_04_17_100004_create_offer_templates_table.php deleted file mode 100644 index 67d4bab..0000000 --- a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2026_04_17_100004_create_offer_templates_table.php +++ /dev/null @@ -1,70 +0,0 @@ -id(); - - // `branch` existiert schon im CRM — Vorlagen können so pro - // Filiale gepflegt werden. Eine spätere Erweiterung auf - // `organization_id` (Modul 5) erfolgt additiv. - $t->foreignId('branch_id') - ->nullable() - ->constrained('branch') - ->nullOnDelete(); - - $t->string('name'); - $t->text('description')->nullable(); - - $t->string('default_headline')->nullable(); - $t->text('default_intro')->nullable(); - $t->longText('default_itinerary')->nullable(); - $t->text('default_closing')->nullable(); - - // Array aus [{title, description, type, price_per_unit, quantity}, …] - $t->json('default_items')->nullable(); - - $t->boolean('is_active')->default(true); - $t->foreignId('created_by')->constrained('users'); - - $t->timestamps(); - $t->softDeletes(); - - $t->index(['branch_id', 'is_active']); - }); - - Schema::table('offer_versions', function (Blueprint $t) { - $t->foreign('template_id') - ->references('id') - ->on('offer_templates') - ->nullOnDelete(); - }); - } - - public function down(): void - { - Schema::table('offer_versions', function (Blueprint $t) { - $t->dropForeign(['template_id']); - }); - - Schema::dropIfExists('offer_templates'); - } -}; diff --git a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2026_04_17_100005_create_offer_files_table.php b/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2026_04_17_100005_create_offer_files_table.php deleted file mode 100644 index 2e8442f..0000000 --- a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2026_04_17_100005_create_offer_files_table.php +++ /dev/null @@ -1,48 +0,0 @@ -id(); - - $t->foreignId('offer_version_id') - ->constrained('offer_versions') - ->cascadeOnDelete(); - - $t->string('identifier', 64)->nullable(); - $t->string('filename'); - $t->string('dir'); - $t->string('original_name'); - $t->string('ext', 16); - $t->string('mine', 128); - $t->unsignedBigInteger('size')->default(0); - - $t->boolean('include_in_pdf')->default(true); - - $t->timestamps(); - - $t->index(['offer_version_id', 'identifier']); - }); - } - - public function down(): void - { - Schema::dropIfExists('offer_files'); - } -}; diff --git a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2026_04_17_100006_create_offer_access_tokens_table.php b/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2026_04_17_100006_create_offer_access_tokens_table.php deleted file mode 100644 index cd10b9c..0000000 --- a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2026_04_17_100006_create_offer_access_tokens_table.php +++ /dev/null @@ -1,48 +0,0 @@ -id(); - - $t->foreignId('offer_id') - ->constrained('offers') - ->cascadeOnDelete(); - - $t->foreignId('offer_version_id') - ->constrained('offer_versions') - ->cascadeOnDelete(); - - $t->string('token_hash', 64)->unique(); - $t->dateTime('expires_at')->nullable(); - $t->dateTime('first_opened_at')->nullable(); - $t->dateTime('revoked_at')->nullable(); - - $t->timestamps(); - - $t->index(['offer_id', 'revoked_at']); - }); - } - - public function down(): void - { - Schema::dropIfExists('offer_access_tokens'); - } -}; diff --git a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2026_04_17_100007_add_offer_refs_to_offers_and_bookings.php b/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2026_04_17_100007_add_offer_refs_to_offers_and_bookings.php deleted file mode 100644 index 32681ab..0000000 --- a/dev/backups/phase2-offers-2026-04-17/FILES/migrations/2026_04_17_100007_add_offer_refs_to_offers_and_bookings.php +++ /dev/null @@ -1,57 +0,0 @@ -foreign('current_version_id') - ->references('id') - ->on('offer_versions') - ->nullOnDelete(); - }); - - Schema::table('booking', function (Blueprint $t) { - // `inquiry_id` kommt aus Modul-3 Phase 2 (war vorher `lead_id`) - $t->unsignedBigInteger('offer_id')->nullable()->after('inquiry_id'); - - $t->foreign('offer_id') - ->references('id') - ->on('offers') - ->nullOnDelete(); - - $t->index('offer_id'); - }); - } - - public function down(): void - { - Schema::table('booking', function (Blueprint $t) { - $t->dropForeign(['offer_id']); - $t->dropIndex(['offer_id']); - $t->dropColumn('offer_id'); - }); - - Schema::table('offers', function (Blueprint $t) { - $t->dropForeign(['current_version_id']); - }); - } -}; diff --git a/dev/backups/phase2-offers-2026-04-17/FILES/models/Offer.php b/dev/backups/phase2-offers-2026-04-17/FILES/models/Offer.php deleted file mode 100644 index 02b503c..0000000 --- a/dev/backups/phase2-offers-2026-04-17/FILES/models/Offer.php +++ /dev/null @@ -1,132 +0,0 @@ - 'int', - 'inquiry_id' => 'int', - 'booking_id' => 'int', - 'current_version_id' => 'int', - 'created_by' => 'int', - ]; - - public function contact(): BelongsTo - { - return $this->belongsTo(Contact::class); - } - - public function inquiry(): BelongsTo - { - // Nach Modul 3 Phase 2: `Lead`-Model bildet die `inquiries`-Tabelle ab - return $this->belongsTo(Lead::class, 'inquiry_id'); - } - - public function booking(): BelongsTo - { - return $this->belongsTo(Booking::class); - } - - public function currentVersion(): BelongsTo - { - return $this->belongsTo(OfferVersion::class, 'current_version_id'); - } - - public function versions(): HasMany - { - return $this->hasMany(OfferVersion::class)->orderBy('version_no'); - } - - public function accessTokens(): HasMany - { - return $this->hasMany(OfferAccessToken::class); - } - - public function creator(): BelongsTo - { - return $this->belongsTo(User::class, 'created_by'); - } - - public function scopeStatus(Builder $q, string $status): Builder - { - return $q->where('status', $status); - } - - public function scopeOpen(Builder $q): Builder - { - return $q->whereIn('status', [self::STATUS_DRAFT, self::STATUS_SENT]); - } - - public function isEditable(): bool - { - return in_array($this->status, [self::STATUS_DRAFT, self::STATUS_SENT], true); - } -} diff --git a/dev/backups/phase2-offers-2026-04-17/FILES/models/OfferAccessToken.php b/dev/backups/phase2-offers-2026-04-17/FILES/models/OfferAccessToken.php deleted file mode 100644 index 54fb1c3..0000000 --- a/dev/backups/phase2-offers-2026-04-17/FILES/models/OfferAccessToken.php +++ /dev/null @@ -1,120 +0,0 @@ - 'int', - 'offer_version_id' => 'int', - 'expires_at' => 'datetime', - 'first_opened_at' => 'datetime', - 'revoked_at' => 'datetime', - ]; - - public function offer(): BelongsTo - { - return $this->belongsTo(Offer::class); - } - - public function version(): BelongsTo - { - return $this->belongsTo(OfferVersion::class, 'offer_version_id'); - } - - public function scopeActive(Builder $q): Builder - { - return $q->whereNull('revoked_at') - ->where(function (Builder $q) { - $q->whereNull('expires_at')->orWhere('expires_at', '>', now()); - }); - } - - /** - * Erzeugt ein neues Token für die angegebene Version und liefert - * den Klartext-Token zurück (nur einmalig abrufbar). In der - * Datenbank wird nur der Hash persistiert. - */ - public static function generate( - Offer $offer, - OfferVersion $version, - ?Carbon $expiresAt = null - ): array { - $plain = Str::random(48); - $hash = hash('sha256', $plain); - - /** @var self $token */ - $token = self::create([ - 'offer_id' => $offer->id, - 'offer_version_id' => $version->id, - 'token_hash' => $hash, - 'expires_at' => $expiresAt, - ]); - - return ['plain' => $plain, 'token' => $token]; - } - - /** - * Lookup per Klartext-Token (konstantzeitig via DB-Unique-Index). - */ - public static function findByPlainToken(string $plain): ?self - { - return self::where('token_hash', hash('sha256', $plain))->first(); - } - - public function isActive(): bool - { - if ($this->revoked_at !== null) { - return false; - } - if ($this->expires_at !== null && $this->expires_at->isPast()) { - return false; - } - - return true; - } -} diff --git a/dev/backups/phase2-offers-2026-04-17/FILES/models/OfferFile.php b/dev/backups/phase2-offers-2026-04-17/FILES/models/OfferFile.php deleted file mode 100644 index be9332c..0000000 --- a/dev/backups/phase2-offers-2026-04-17/FILES/models/OfferFile.php +++ /dev/null @@ -1,99 +0,0 @@ - 'int', - 'size' => 'int', - 'include_in_pdf' => 'bool', - ]; - - public static array $iconExt = [ - 'default' => 'fa fa-file', - 'pdf' => 'fa fa-file-pdf', - 'jpg' => 'fa fa-file-image', - 'jpeg' => 'fa fa-file-image', - 'png' => 'fa fa-file-image', - 'doc' => 'fa fa-file-word', - 'docx' => 'fa fa-file-word', - ]; - - public function version(): BelongsTo - { - return $this->belongsTo(OfferVersion::class, 'offer_version_id'); - } - - public function getIconExt(): string - { - return self::$iconExt[$this->ext] ?? self::$iconExt['default']; - } - - public function getURL(bool|string $do = false): string - { - return route('storage_file', [$this->id, 'offer', $do]); - } - - public function getPath(): string - { - return \Storage::disk('offer')->path($this->dir . $this->filename); - } - - public function formatBytes(int $precision = 2): string - { - $size = $this->size; - if ($size <= 0) { - return (string) $size; - } - - $base = log($size) / log(1024); - $suffixes = [' bytes', ' KB', ' MB', ' GB', ' TB']; - - return round(1024 ** ($base - floor($base)), $precision) . $suffixes[floor($base)]; - } -} diff --git a/dev/backups/phase2-offers-2026-04-17/FILES/models/OfferItem.php b/dev/backups/phase2-offers-2026-04-17/FILES/models/OfferItem.php deleted file mode 100644 index 9baf8f8..0000000 --- a/dev/backups/phase2-offers-2026-04-17/FILES/models/OfferItem.php +++ /dev/null @@ -1,86 +0,0 @@ - 'int', - 'position' => 'int', - 'quantity' => 'int', - 'price_per_unit' => 'decimal:2', - 'total_price' => 'decimal:2', - 'travel_program_id' => 'int', - 'fewo_lodging_id' => 'int', - 'metadata' => 'array', - ]; - - public function version(): BelongsTo - { - return $this->belongsTo(OfferVersion::class, 'offer_version_id'); - } - - /** - * Aus Menge × Einzelpreis den Positions-Gesamtpreis berechnen - * (Rabatte negativ — gehört in den Service-Layer zur Summierung). - */ - public function calculateTotal(): float - { - return round($this->quantity * (float) $this->price_per_unit, 2); - } -} diff --git a/dev/backups/phase2-offers-2026-04-17/FILES/models/OfferTemplate.php b/dev/backups/phase2-offers-2026-04-17/FILES/models/OfferTemplate.php deleted file mode 100644 index 2f49ec6..0000000 --- a/dev/backups/phase2-offers-2026-04-17/FILES/models/OfferTemplate.php +++ /dev/null @@ -1,77 +0,0 @@ - 'int', - 'default_items' => 'array', - 'is_active' => 'bool', - 'created_by' => 'int', - ]; - - public function branch(): BelongsTo - { - return $this->belongsTo(Branch::class); - } - - public function creator(): BelongsTo - { - return $this->belongsTo(User::class, 'created_by'); - } - - public function scopeActive(Builder $q): Builder - { - return $q->where('is_active', true); - } -} diff --git a/dev/backups/phase2-offers-2026-04-17/FILES/models/OfferVersion.php b/dev/backups/phase2-offers-2026-04-17/FILES/models/OfferVersion.php deleted file mode 100644 index d85b1a5..0000000 --- a/dev/backups/phase2-offers-2026-04-17/FILES/models/OfferVersion.php +++ /dev/null @@ -1,126 +0,0 @@ - 'int', - 'version_no' => 'int', - 'valid_until' => 'date', - 'total_price' => 'decimal:2', - 'template_id' => 'int', - 'pdf_archived' => 'bool', - 'sent_at' => 'datetime', - 'accepted_at' => 'datetime', - 'template_document_ids' => 'array', - 'created_by' => 'int', - ]; - - public function offer(): BelongsTo - { - return $this->belongsTo(Offer::class); - } - - public function template(): BelongsTo - { - return $this->belongsTo(OfferTemplate::class, 'template_id'); - } - - public function items(): HasMany - { - return $this->hasMany(OfferItem::class)->orderBy('position'); - } - - public function files(): HasMany - { - return $this->hasMany(OfferFile::class); - } - - public function creator(): BelongsTo - { - return $this->belongsTo(User::class, 'created_by'); - } - - public function isEditable(): bool - { - return $this->status === self::STATUS_DRAFT; - } -} diff --git a/dev/backups/phase2-offers-2026-04-17/MANIFEST.md b/dev/backups/phase2-offers-2026-04-17/MANIFEST.md deleted file mode 100644 index 93962ee..0000000 --- a/dev/backups/phase2-offers-2026-04-17/MANIFEST.md +++ /dev/null @@ -1,158 +0,0 @@ -# Backup: Phase-2 + Offers (Rückbau für Phase-1-Live-Deploy) - -**Erstellt:** 2026-04-17 -**Zweck:** Der Workspace enthielt gemischt Phase 1 + Phase 2 (Code-Umstellung `inquiry_id`, `$table='contacts'/'inquiries'`) + Offers-Modul. Phase 1 muss zuerst auf Live, dann erst Phase 2 + Offers. Dieses Backup sichert alle **Phase-2- und Offers-Artefakte**, damit sie nach erfolgreichem Phase-1-Live-Deploy per `restore.sh` wieder in den Workspace eingespielt werden können. - -Parallel dazu existiert ein **Tarball-Backup** des gesamten Workspace-Zustands vor dem Rückbau unter `../../../../../backups-safety/workspace-pre-phase1-rollback-2026-04-17.tar.gz` (182 MB) und ein **Git-Commit** `e3dc1af` (lokal auf `master`, nicht gepusht) als doppeltes Sicherheitsnetz. - ---- - -## Was im Backup liegt - -### `FILES/migrations/` (18 Migrations-Dateien) - -**Phase 2 (3 Dateien):** -- `2025_04_15_200001_phase2_rename_customer_to_contacts.php` — RENAME TABLE customer → contacts -- `2025_04_15_200002_phase2_rename_lead_to_inquiries.php` — RENAME TABLE lead → inquiries -- `2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php` — Spalte `booking.lead_id` → `booking.inquiry_id` - -**Phase 3 (2 Dateien):** -- `2025_04_15_300001_phase3_create_participants_unified_table.php` -- `2025_04_15_300002_phase3_drop_old_participant_tables.php` - -**Phase 4 (6 Dateien):** -- `2025_04_15_400001_phase4_create_communications_table.php` -- `2025_04_15_400002_phase4_create_notices_table.php` -- `2025_04_15_400003_phase4_create_attachments_table.php` -- `2025_04_15_400004_phase4_drop_old_communication_tables.php` -- `2025_04_15_400005_phase4_drop_old_notice_tables.php` -- `2025_04_15_400006_phase4_drop_old_attachment_tables.php` - -**Offers (7 Dateien):** -- `2026_04_17_100001_create_offers_table.php` -- `2026_04_17_100002_create_offer_versions_table.php` -- `2026_04_17_100003_create_offer_items_table.php` -- `2026_04_17_100004_create_offer_templates_table.php` -- `2026_04_17_100005_create_offer_files_table.php` -- `2026_04_17_100006_create_offer_access_tokens_table.php` -- `2026_04_17_100007_add_offer_refs_to_offers_and_bookings.php` - -### `FILES/models/` (6 Offer-Models) - -- `Offer.php` — **neues** Offer-Model des Offers-Moduls (überschreibt das Legacy-Reliese-Model in `app/Models/Offer.php`; Legacy-Version ist in `HEAD^` bzw. Commit `389d5d1`) -- `OfferVersion.php` -- `OfferItem.php` -- `OfferTemplate.php` -- `OfferFile.php` -- `OfferAccessToken.php` - -### `PATCHES/` (Diffs für Phase-2/Offers-Rückbau) - -Jede Datei enthält den **vollständigen** oder **Phase-1-only**-Diff gegen `HEAD^` (= `389d5d1`, Basis-Live-Stand): - -- `Booking.php.full.diff` — kompletter Vorher-Diff (inkl. Phase-2 + Offers-Änderungen, die zurückgebaut wurden) -- `Booking.php.phase1-only.diff` — was nach Rückbau übrig blieb: **nur** `HasFactory` + `$dates`→`$casts datetime` (Laravel-10-Upgrade) -- `Customer.php.phase1-only.diff` — Phase-1-Arbeit (SoftDeletes, Merge-Fields, Relations, Global Scope); `$table = 'contacts'` zurück auf `'customer'` -- `Lead.php.phase1-only.diff` — `$table = 'inquiries'` zurück auf `'lead'` (+ Phase-1-$casts-Refactoring) -- `Contact.php.phase1-only.diff` — komplett neue Datei (Contacts-Modul); `$table = 'contacts'` zurück auf `'customer'` -- `filesystems.php.full.diff` — entfernte `offer`-Disk (komplett zurückgerollt; Datei jetzt identisch mit HEAD^) -- `Repositories.full.diff` / `Repositories.phase1-only.diff` — BookingPDFRepository (Laravel-10-Upgrade `Storage::disk()->path()`), LeadRepository, CustomerMailRepository -- `Controllers.phase1-only.diff` — RequestController (Phase-1-$casts), API/BookingController, Admin/Report*, LeadController, CustomerController, ContactController (komplett neu) -- `Services-Commands.phase1-only.diff` — BookingImport, SyncNewsletterKulturreisen (neu), Contacts*Duplicates (neu) -- `Views.phase1-only.diff` — (leer, reine Phase-2-Views wurden zurückgerollt) - ---- - -## Was im Workspace VERBLEIBT (Phase 1 + Laravel 10) - -**Deploy-ready auf Live** — alle Phase-1-Artefakte, die auf Test bereits laufen: - -### Neue Dateien (Phase 1) -- `database/migrations/2025_04_15_100001_phase1_add_merge_fields_to_customer_table.php` -- `database/migrations/2025_04_15_100002_phase1_add_soft_delete_to_customer_table.php` -- `app/Models/Contact.php` (mit `$table = 'customer'`) -- `app/Repositories/ContactRepository.php` -- `app/Http/Controllers/ContactController.php` -- `app/Console/Commands/ContactsFindDuplicates.php` -- `app/Console/Commands/ContactsMergeDuplicates.php` -- `resources/views/contact/*.blade.php` (5 Dateien) - -### Modifizierte Dateien (Phase 1 + Laravel 10 Upgrade) -- `app/Models/Customer.php` — SoftDeletes, Merge-Fields, Global Scope, Relations -- `app/Models/Booking.php` — HasFactory, `$dates`→`$casts datetime` -- `app/Models/Lead.php` — Phase-1-$casts-Refactoring -- Viele weitere Models mit Laravel-10-Upgrade-Änderungen -- `app/Repositories/BookingPDFRepository.php` — `Storage::disk()->path()` statt deprecated `getAdapter()->getPathPrefix()` -- `routes/web.php` — `/contacts`, `/contact/*` Routen -- `resources/views/layouts/includes/layout-sidenav.blade.php` — Contacts-Menüpunkt -- Weitere modifizierte Dateien, u.a. diverse Views, Tests (`tests/Feature/*`, `tests/Unit/*`), Konfiguration (`phpunit.xml`, `config/trustedproxy.php`), Composer/Package-Dateien -- `app/Services/MailDirService.php` (neu) -- `database/factories/BookingFactory.php`, `CustomerFactory.php`, `LeadFactory.php` (neu) - -**Alle diese Dateien gehen mit dem Phase-1-Live-Deploy mit.** - ---- - -## Restore-Anleitung - -### Automatisch (empfohlen) - -```bash -bash dev/backups/phase2-offers-2026-04-17/restore.sh -``` - -Das Script: -1. Prüft, ob Phase 2 auf Live eingespielt wurde (erfordert bewusste Bestätigung) -2. Spielt die 18 Migrations-Dateien zurück nach `database/migrations/` -3. Spielt die 6 Offer-Models zurück nach `app/Models/` (inkl. Überschreiben der Legacy-`Offer.php`) -4. Wendet die Phase-2/Offers-Änderungen wieder an (Booking.php inquiry_id + offer()-Relation, Customer/Lead/Contact $table, Repositories, Controllers, Services, Commands, Views, filesystems.php) -5. Zeigt einen abschließenden `git status` zur Verifikation - -### Manuell - -Falls das Script fehlschlägt oder man einzelne Teile prüfen möchte: - -**Migrations zurück:** -```bash -cp dev/backups/phase2-offers-2026-04-17/FILES/migrations/*.php database/migrations/ -``` - -**Models zurück (WARNUNG: überschreibt Legacy-Offer.php):** -```bash -cp dev/backups/phase2-offers-2026-04-17/FILES/models/*.php app/Models/ -``` - -**Code-Änderungen aus dem WIP-Sicherheits-Commit (`e3dc1af`) zurückholen:** -```bash -git checkout e3dc1af -- app/Models/Booking.php app/Models/Customer.php app/Models/Contact.php app/Models/Lead.php -git checkout e3dc1af -- app/Repositories/BookingPDFRepository.php app/Repositories/LeadRepository.php app/Repositories/CustomerMailRepository.php -git checkout e3dc1af -- app/Http/Controllers/RequestController.php app/Http/Controllers/API/BookingController.php -git checkout e3dc1af -- app/Http/Controllers/Admin/ReportController.php app/Http/Controllers/Admin/ReportProviderController.php app/Http/Controllers/Admin/ReportLeadsController.php -git checkout e3dc1af -- app/Http/Controllers/LeadController.php app/Http/Controllers/CustomerController.php app/Http/Controllers/ContactController.php -git checkout e3dc1af -- app/Services/BookingImport.php -git checkout e3dc1af -- app/Console/Commands/SyncNewsletterKulturreisen.php app/Console/Commands/ContactsFindDuplicates.php app/Console/Commands/ContactsMergeDuplicates.php -git checkout e3dc1af -- resources/views/customer/mail/modal-show-mail-inner.blade.php resources/views/pdf/components/booking_head.blade.php resources/views/pdf/components/booking_header.blade.php -git checkout e3dc1af -- config/filesystems.php -``` - -**Alternative — kompletter Restore aus dem Git-Commit:** -```bash -git checkout e3dc1af -- . -``` -(Aber dann sind auch die Phase-2-Migrationen in `database/migrations/` wieder da, was richtig ist.) - ---- - -## Notfall-Rollback (alles rückgängig, auch Phase-1-Rückbau) - -Falls der Phase-1-Rückbau komplett falsch war und man zum Ausgangszustand zurück will: - -```bash -# Option A: Git -git reset --hard e3dc1af # setzt Workspace auf den WIP-Sicherheits-Commit - -# Option B: Tarball (falls Git nicht funktioniert) -cd /workspace/mein.sterntours.de -rm -rf * .[a-z]* # Vorsicht! Löscht alles außer dem übergeordneten Dir -tar -xzf ../backups-safety/workspace-pre-phase1-rollback-2026-04-17.tar.gz -``` diff --git a/dev/backups/phase2-offers-2026-04-17/PATCHES/Booking.php.full.diff b/dev/backups/phase2-offers-2026-04-17/PATCHES/Booking.php.full.diff deleted file mode 100644 index 2a28d0a..0000000 --- a/dev/backups/phase2-offers-2026-04-17/PATCHES/Booking.php.full.diff +++ /dev/null @@ -1,110 +0,0 @@ -diff --git a/app/Models/Booking.php b/app/Models/Booking.php -index 79a91ba..f3bf11c 100644 ---- a/app/Models/Booking.php -+++ b/app/Models/Booking.php -@@ -9,6 +9,7 @@ namespace App\Models; - use Carbon\Carbon; - use App\Services\Util; - use App\Services\Passolution; -+use Illuminate\Database\Eloquent\Factories\HasFactory; - use Illuminate\Database\Eloquent\Model; - use Illuminate\Database\Eloquent\Collection; - -@@ -18,7 +19,7 @@ use Illuminate\Database\Eloquent\Collection; - * @property int $id - * @property Carbon $booking_date - * @property int $customer_id -- * @property int $lead_id -+ * @property int $inquiry_id - * @property bool $new_drafts - * @property int $sf_guard_user_id - * @property int $branch_id -@@ -203,13 +204,16 @@ use Illuminate\Database\Eloquent\Collection; - */ - class Booking extends Model - { -+ use HasFactory; -+ - protected $connection = 'mysql'; - - protected $table = 'booking'; - - protected $casts = [ - 'customer_id' => 'int', -- 'lead_id' => 'int', -+ 'inquiry_id' => 'int', -+ 'offer_id' => 'int', - 'new_drafts' => 'bool', - 'sf_guard_user_id' => 'int', - 'branch_id' => 'int', -@@ -237,25 +241,24 @@ class Booking extends Model - 'is_rail_fly' => 'bool', - 'comfort' => 'bool', - 'airline_ids' => 'array', -- 'participant_pass' => 'bool' -- ]; -- -- protected $dates = [ -- 'booking_date', -- 'start_date', -- 'end_date', -- 'participant_birthdate', -- 'final_payment_date', -- 'refund_date', -- 'lawyer_date', -- 'xx_tkt_date' -- -- ]; -+ 'participant_pass' => 'bool', -+ 'booking_date' => 'datetime', -+ 'start_date' => 'datetime', -+ 'end_date' => 'datetime', -+ 'participant_birthdate' => 'datetime', -+ 'final_payment_date' => 'datetime', -+ 'refund_date' => 'datetime', -+ 'lawyer_date' => 'datetime', -+ 'xx_tkt_date' => 'datetime', -+ ]; -+ -+ - - protected $fillable = [ - 'booking_date', - 'customer_id', -- 'lead_id', -+ 'inquiry_id', -+ 'offer_id', - 'new_drafts', - 'sf_guard_user_id', - 'branch_id', -@@ -392,9 +395,29 @@ class Booking extends Model - return $this->belongsTo(Customer::class); - } - -+ /** -+ * Lead/Inquiry der Buchung. -+ * FK-Spalte `inquiry_id` (vormals `lead_id` — Modul 3 Phase 2 Rename). -+ * Methodenname bleibt `lead()` für Legacy-Kompatibilität; {@see self::inquiry()} -+ * ist der fachlich korrekte Alias und sollte in neuem Code verwendet werden. -+ */ - public function lead() - { -- return $this->belongsTo(Lead::class); -+ return $this->belongsTo(Lead::class, 'inquiry_id'); -+ } -+ -+ public function inquiry() -+ { -+ return $this->belongsTo(Lead::class, 'inquiry_id'); -+ } -+ -+ /** -+ * Angebot, aus dem diese Buchung entstanden ist (Modul 6, Ticket B8). -+ * Nullable — nicht jede Buchung hat einen Angebots-Vorlauf. -+ */ -+ public function offer() -+ { -+ return $this->belongsTo(\App\Models\Offer::class); - } - - public function sf_guard_user() diff --git a/dev/backups/phase2-offers-2026-04-17/PATCHES/Booking.php.phase1-only.diff b/dev/backups/phase2-offers-2026-04-17/PATCHES/Booking.php.phase1-only.diff deleted file mode 100644 index 90acf25..0000000 --- a/dev/backups/phase2-offers-2026-04-17/PATCHES/Booking.php.phase1-only.diff +++ /dev/null @@ -1,54 +0,0 @@ -diff --git a/app/Models/Booking.php b/app/Models/Booking.php -index 79a91ba..3d61b89 100644 ---- a/app/Models/Booking.php -+++ b/app/Models/Booking.php -@@ -9,6 +9,7 @@ namespace App\Models; - use Carbon\Carbon; - use App\Services\Util; - use App\Services\Passolution; -+use Illuminate\Database\Eloquent\Factories\HasFactory; - use Illuminate\Database\Eloquent\Model; - use Illuminate\Database\Eloquent\Collection; - -@@ -203,6 +204,8 @@ use Illuminate\Database\Eloquent\Collection; - */ - class Booking extends Model - { -+ use HasFactory; -+ - protected $connection = 'mysql'; - - protected $table = 'booking'; -@@ -237,20 +240,18 @@ class Booking extends Model - 'is_rail_fly' => 'bool', - 'comfort' => 'bool', - 'airline_ids' => 'array', -- 'participant_pass' => 'bool' -- ]; -- -- protected $dates = [ -- 'booking_date', -- 'start_date', -- 'end_date', -- 'participant_birthdate', -- 'final_payment_date', -- 'refund_date', -- 'lawyer_date', -- 'xx_tkt_date' -- -- ]; -+ 'participant_pass' => 'bool', -+ 'booking_date' => 'datetime', -+ 'start_date' => 'datetime', -+ 'end_date' => 'datetime', -+ 'participant_birthdate' => 'datetime', -+ 'final_payment_date' => 'datetime', -+ 'refund_date' => 'datetime', -+ 'lawyer_date' => 'datetime', -+ 'xx_tkt_date' => 'datetime', -+ ]; -+ -+ - - protected $fillable = [ - 'booking_date', diff --git a/dev/backups/phase2-offers-2026-04-17/PATCHES/Contact.php.phase1-only.diff b/dev/backups/phase2-offers-2026-04-17/PATCHES/Contact.php.phase1-only.diff deleted file mode 100644 index 5252efa..0000000 --- a/dev/backups/phase2-offers-2026-04-17/PATCHES/Contact.php.phase1-only.diff +++ /dev/null @@ -1,170 +0,0 @@ -diff --git a/app/Models/Contact.php b/app/Models/Contact.php -new file mode 100644 -index 0000000..6d941e8 ---- /dev/null -+++ b/app/Models/Contact.php -@@ -0,0 +1,164 @@ -+ 'int', -+ 'credit_card_type_id' => 'int', -+ 'country_id' => 'int', -+ 'merged_into_id' => 'int', -+ 'birthdate' => 'datetime', -+ 'credit_card_expiration_date' => 'datetime', -+ 'merged_at' => 'datetime', -+ ]; -+ -+ protected $fillable = [ -+ 'salutation_id', -+ 'title', -+ 'name', -+ 'firstname', -+ 'birthdate', -+ 'company', -+ 'street', -+ 'zip', -+ 'city', -+ 'email', -+ 'phone', -+ 'phonebusiness', -+ 'phonemobile', -+ 'fax', -+ 'bank', -+ 'bank_code', -+ 'bank_account_number', -+ 'credit_card_type_id', -+ 'credit_card_number', -+ 'credit_card_expiration_date', -+ 'participants_remarks', -+ 'miscellaneous_remarks', -+ 'country_id', -+ 'merged_into_id', -+ 'merged_at', -+ ]; -+ -+ /** -+ * Globaler Scope: zusammengeführte Duplikate werden standardmäßig ausgeblendet. -+ * Für Zugriff auf alle inkl. Duplikate: Contact::withoutGlobalScope('not_merged') -+ */ -+ protected static function booted(): void -+ { -+ static::addGlobalScope('not_merged', function (Builder $query) { -+ $query->whereNull('merged_into_id'); -+ }); -+ } -+ -+ // ── Beziehungen ────────────────────────────────────────────────────────── -+ -+ public function mergedInto(): \Illuminate\Database\Eloquent\Relations\BelongsTo -+ { -+ return $this->belongsTo(Contact::class, 'merged_into_id') -+ ->withoutGlobalScope('not_merged'); -+ } -+ -+ public function mergedContacts(): \Illuminate\Database\Eloquent\Relations\HasMany -+ { -+ return $this->hasMany(Contact::class, 'merged_into_id') -+ ->withoutGlobalScope('not_merged'); -+ } -+ -+ public function leads(): \Illuminate\Database\Eloquent\Relations\HasMany -+ { -+ return $this->hasMany(Lead::class, 'customer_id')->orderByDesc('created_at'); -+ } -+ -+ public function bookings(): \Illuminate\Database\Eloquent\Relations\HasMany -+ { -+ return $this->hasMany(Booking::class, 'customer_id')->orderByDesc('created_at'); -+ } -+ -+ public function salutation(): \Illuminate\Database\Eloquent\Relations\BelongsTo -+ { -+ return $this->belongsTo(Salutation::class); -+ } -+ -+ public function travel_country(): \Illuminate\Database\Eloquent\Relations\BelongsTo -+ { -+ return $this->belongsTo(TravelCountry::class, 'country_id'); -+ } -+ -+ // ── Hilfsmethoden ──────────────────────────────────────────────────────── -+ -+ public function fullName(): string -+ { -+ if ($this->firstname) { -+ return $this->firstname . ' ' . $this->name; -+ } -+ return (string) $this->name; -+ } -+ -+ public function isMerged(): bool -+ { -+ return $this->merged_into_id !== null; -+ } -+ -+ public static function getCountriesArray(): \Illuminate\Support\Collection -+ { -+ return TravelCountry::where('is_customer_country', 1)->get()->pluck('name', 'id'); -+ } -+ -+ public static $salutationType = [ -+ 1 => 'Herr', -+ 2 => 'Frau', -+ 3 => 'Divers/keine Anrede', -+ 4 => 'Firma', -+ ]; -+} diff --git a/dev/backups/phase2-offers-2026-04-17/PATCHES/Controllers.phase1-only.diff b/dev/backups/phase2-offers-2026-04-17/PATCHES/Controllers.phase1-only.diff deleted file mode 100644 index 65c2c26..0000000 --- a/dev/backups/phase2-offers-2026-04-17/PATCHES/Controllers.phase1-only.diff +++ /dev/null @@ -1,372 +0,0 @@ -diff --git a/app/Http/Controllers/API/BookingController.php b/app/Http/Controllers/API/BookingController.php -index e47cd07..a020ad4 100755 ---- a/app/Http/Controllers/API/BookingController.php -+++ b/app/Http/Controllers/API/BookingController.php -@@ -8,42 +8,35 @@ use App\Services\BookingImport; - - class BookingController extends Controller - { -- private $successStatus = 200; -- private $successKey = 'f6077389c9ce710e554763a5de02c8ec'; -- -- protected $draftRepo; -+ private int $successStatus = 200; -+ private string $successKey; - - public function __construct() - { -- -+ $this->successKey = config('app.success_key'); - } - - public function import() - { - - $request = \Request::all(); -- if(!isset($request['key']) || $request['key'] !== $this->successKey){ -+ if (!isset($request['key']) || $request['key'] !== $this->successKey) { - return response()->json(['error' => "key"], 401); - } - $travel_booking = TravelBooking::find($request['travel_booking_id']); - -- //# vor testing -- //$travel_booking = TravelBooking::find(2922); -- if(!isset($travel_booking) || !$travel_booking){ -+ if (!$travel_booking) { - return response()->json(['error' => 'no-booking-found'], $this->successStatus); - } - - $booking = BookingImport::importFrom($travel_booking); - -- $ret= [ -- 'url_v1' => make_old_url('/index.php/booking/'.$booking->id.'/edit'), -+ $ret = [ -+ 'url_v1' => make_old_url('/index.php/booking/' . $booking->id . '/edit'), - 'url_v3' => route('booking_detail', $booking->id), - 'lead_id' => $booking->lead_id - ]; - return response()->json(['success' => "import", "ret" => $ret], $this->successStatus); -- //return response()->json(['error' => 'no-node'], $this->successStatus); - - } -- -- - } -diff --git a/app/Http/Controllers/ContactController.php b/app/Http/Controllers/ContactController.php -new file mode 100644 -index 0000000..08ef241 ---- /dev/null -+++ b/app/Http/Controllers/ContactController.php -@@ -0,0 +1,312 @@ -+middleware(['admin', '2fa']); -+ } -+ -+ public function index(): \Illuminate\View\View -+ { -+ return view('contact.index'); -+ } -+ -+ public function detail(string $id): \Illuminate\View\View -+ { -+ if ($id === 'new') { -+ $contact = new Contact(); -+ } else { -+ $contact = Contact::with(['salutation', 'leads', 'bookings', 'mergedContacts']) -+ ->findOrFail((int) $id); -+ } -+ -+ return view('contact.detail', [ -+ 'contact' => $contact, -+ 'id' => $id, -+ ]); -+ } -+ -+ public function store(Request $request, string $id): \Illuminate\Http\RedirectResponse -+ { -+ $data = $request->except('_token'); -+ -+ if (!isset($data['action'])) { -+ abort(403, 'keine Action'); -+ } -+ -+ if ($id === 'new') { -+ $contact = $this->contactRepo->createContact($data); -+ \Session()->flash('alert-save', '1'); -+ return redirect(route('contact_detail', [$contact->id]) . '#collapseContactDetail'); -+ } -+ -+ $this->contactRepo->updateContact($id, $data); -+ \Session()->flash('alert-save', '1'); -+ -+ return redirect(route('contact_detail', [$id]) . '#collapseContactDetail'); -+ } -+ -+ public function history(int $id): \Illuminate\View\View -+ { -+ $contact = Contact::with([ -+ 'leads.sf_guard_user', -+ 'bookings.travel_country', -+ 'bookings.travel_agenda', -+ 'bookings.sf_guard_user', -+ 'bookings.lead', -+ ])->findOrFail($id); -+ -+ return view('contact._detail_history', [ -+ 'contact' => $contact, -+ 'modal' => true, -+ ]); -+ } -+ -+ public function destroy(int $id): \Illuminate\Http\JsonResponse -+ { -+ $contact = Contact::withoutGlobalScope('not_merged')->findOrFail($id); -+ -+ $leadsCount = $contact->leads()->count(); -+ $bookingCount = $contact->bookings()->count(); -+ -+ if ($leadsCount > 0 || $bookingCount > 0) { -+ return response()->json([ -+ 'success' => false, -+ 'message' => sprintf( -+ 'Kontakt kann nicht gelöscht werden: %s%s vorhanden.', -+ $leadsCount > 0 ? $leadsCount . ' Anfrage(n)' : '', -+ $bookingCount > 0 ? ($leadsCount > 0 ? ' und ' : '') . $bookingCount . ' Buchung(en)' : '' -+ ), -+ ], 422); -+ } -+ -+ $contact->delete(); -+ -+ return response()->json(['success' => true]); -+ } -+ -+ public function duplicates(): \Illuminate\View\View -+ { -+ $counts = [ -+ 'HIGH' => $this->countDuplicateGroups('email'), -+ 'MEDIUM' => $this->countDuplicateGroups('name_birthdate'), -+ 'LOW' => $this->countDuplicateGroups('name_zip'), -+ ]; -+ -+ return view('contact.duplicates', compact('counts')); -+ } -+ -+ public function getDuplicateGroups(Request $request): \Illuminate\Http\JsonResponse -+ { -+ $confidence = strtoupper($request->input('confidence', 'HIGH')); -+ -+ $groups = match ($confidence) { -+ 'HIGH' => $this->findByEmail(), -+ 'MEDIUM' => $this->findByNameBirthdate(), -+ 'LOW' => $this->findByNameZip(), -+ default => [], -+ }; -+ -+ // Für jede Gruppe die vollständigen Kontakt-Daten laden -+ $result = []; -+ foreach ($groups as $ids) { -+ $contacts = Contact::withoutGlobalScopes() -+ ->withCount(['leads', 'bookings']) -+ ->whereIn('id', $ids) -+ ->whereNull('merged_into_id') -+ ->whereNull('deleted_at') -+ ->orderByRaw('FIELD(id, ' . implode(',', $ids) . ')') -+ ->get(['id', 'firstname', 'name', 'email', 'zip', 'city', 'phone', 'phonemobile', 'birthdate', 'created_at', 'updated_at']); -+ -+ if ($contacts->count() < 2) { -+ continue; -+ } -+ -+ $result[] = [ -+ 'master' => $contacts->first(), -+ 'duplicates' => $contacts->skip(1)->values(), -+ ]; -+ } -+ -+ return response()->json($result); -+ } -+ -+ public function merge(Request $request): \Illuminate\Http\JsonResponse -+ { -+ $masterId = (int) $request->input('master_id'); -+ $dupeId = (int) $request->input('duplicate_id'); -+ -+ if (!$masterId || !$dupeId || $masterId === $dupeId) { -+ return response()->json(['success' => false, 'message' => 'Ungültige IDs.'], 422); -+ } -+ -+ $master = Contact::withoutGlobalScopes()->find($masterId); -+ $dupe = Contact::withoutGlobalScopes()->find($dupeId); -+ -+ if (!$master || !$dupe) { -+ return response()->json(['success' => false, 'message' => 'Kontakt nicht gefunden.'], 404); -+ } -+ -+ DB::transaction(function () use ($masterId, $dupeId) { -+ DB::table('lead')->where('customer_id', $dupeId)->update(['customer_id' => $masterId]); -+ DB::table('booking')->where('customer_id', $dupeId)->update(['customer_id' => $masterId]); -+ DB::table('customer_mails')->where('customer_id', $dupeId)->update(['customer_id' => $masterId]); -+ DB::table('lead_mails')->where('customer_id', $dupeId)->update(['customer_id' => $masterId]); -+ DB::table('customer')->where('id', $dupeId)->update([ -+ 'merged_into_id' => $masterId, -+ 'merged_at' => now(), -+ ]); -+ }); -+ -+ return response()->json(['success' => true]); -+ } -+ -+ // ── Duplikat-Hilfs-Queries ──────────────────────────────────────────────── -+ -+ private function countDuplicateGroups(string $type): int -+ { -+ return match ($type) { -+ 'email' => DB::table('customer')->selectRaw('COUNT(*) as cnt')->whereNotNull('email')->where('email', '!=', '')->whereNull('merged_into_id')->whereNull('deleted_at')->groupBy('email')->havingRaw('COUNT(*) > 1')->get()->count(), -+ 'name_birthdate' => DB::table('customer')->selectRaw('COUNT(*) as cnt')->whereNotNull('name')->whereNotNull('firstname')->whereNotNull('birthdate')->whereNull('merged_into_id')->whereNull('deleted_at')->groupBy('name', 'firstname', 'birthdate')->havingRaw('COUNT(*) > 1')->get()->count(), -+ 'name_zip' => DB::table('customer')->selectRaw('COUNT(*) as cnt')->whereNotNull('name')->whereNotNull('firstname')->whereNotNull('zip')->where('zip', '!=', '')->whereNull('merged_into_id')->whereNull('deleted_at')->groupBy('name', 'firstname', 'zip')->havingRaw('COUNT(*) > 1')->get()->count(), -+ default => 0, -+ }; -+ } -+ -+ private function findByEmail(): array -+ { -+ return DB::table('customer') -+ ->selectRaw('GROUP_CONCAT(id ORDER BY updated_at DESC, id DESC) as ids') -+ ->whereNotNull('email')->where('email', '!=', '') -+ ->whereNull('merged_into_id')->whereNull('deleted_at') -+ ->groupBy('email')->havingRaw('COUNT(*) > 1') -+ ->pluck('ids')->map(fn ($s) => array_map('intval', explode(',', $s)))->all(); -+ } -+ -+ private function findByNameBirthdate(): array -+ { -+ return DB::table('customer') -+ ->selectRaw('GROUP_CONCAT(id ORDER BY updated_at DESC, id DESC) as ids') -+ ->whereNotNull('name')->whereNotNull('firstname')->whereNotNull('birthdate') -+ ->whereNull('merged_into_id')->whereNull('deleted_at') -+ ->groupBy('name', 'firstname', 'birthdate')->havingRaw('COUNT(*) > 1') -+ ->pluck('ids')->map(fn ($s) => array_map('intval', explode(',', $s)))->all(); -+ } -+ -+ private function findByNameZip(): array -+ { -+ return DB::table('customer') -+ ->selectRaw('GROUP_CONCAT(id ORDER BY updated_at DESC, id DESC) as ids') -+ ->whereNotNull('name')->whereNotNull('firstname') -+ ->whereNotNull('zip')->where('zip', '!=', '') -+ ->whereNull('merged_into_id')->whereNull('deleted_at') -+ ->groupBy('name', 'firstname', 'zip')->havingRaw('COUNT(*) > 1') -+ ->pluck('ids')->map(fn ($s) => array_map('intval', explode(',', $s)))->all(); -+ } -+ -+ public function restore(int $id): \Illuminate\Http\JsonResponse -+ { -+ $contact = Contact::onlyTrashed()->withoutGlobalScope('not_merged')->findOrFail($id); -+ $contact->restore(); -+ -+ return response()->json(['success' => true]); -+ } -+ -+ public function getContacts(Request $request): \Illuminate\Http\JsonResponse -+ { -+ $showDeleted = $request->filled('filter_deleted'); -+ -+ // Reihenfolge wichtig: select() zuerst, dann withCount() — sonst überschreibt -+ // select() die COUNT-Subqueries die withCount() per addSelect() eingetragen hat. -+ $query = $showDeleted -+ ? Contact::onlyTrashed()->withoutGlobalScope('not_merged')->select('customer.*')->withCount(['leads', 'bookings']) -+ : Contact::select('customer.*')->withCount(['leads', 'bookings']); -+ -+ // Zusatzfilter aus der UI (werden als extra GET-Parameter gesendet) -+ if ($request->filled('filter_has_leads')) { -+ $query->has('leads'); -+ } -+ if ($request->filled('filter_has_bookings')) { -+ $query->has('bookings'); -+ } -+ if ($request->filled('filter_has_email')) { -+ $query->whereNotNull('email')->where('email', '!=', ''); -+ } -+ -+ return DataTables::eloquent($query) -+ ->addColumn('action_edit', function (Contact $contact) use ($showDeleted) { -+ if ($showDeleted) { -+ return ''; -+ } -+ return ''; -+ }) -+ ->addColumn('action_delete', function (Contact $contact) use ($showDeleted) { -+ if ($showDeleted) { -+ return ''; -+ } -+ return ''; -+ }) -+ ->addColumn('id', function (Contact $contact) use ($showDeleted) { -+ if ($showDeleted) { -+ return '' . $contact->id . ''; -+ } -+ return '' . $contact->id . ''; -+ }) -+ ->addColumn('raw_id', fn(Contact $contact) => $contact->id) -+ ->addColumn('leads_count', fn(Contact $contact) => $contact->leads_count) -+ ->addColumn('bookings_count', fn(Contact $contact) => $contact->bookings_count) -+ ->addColumn('deleted_at', fn(Contact $contact) => $contact->deleted_at?->format('d.m.Y H:i') ?? '') -+ ->orderColumn('id', 'customer.id $1') -+ ->orderColumn('deleted_at', 'customer.deleted_at $1') -+ ->filterColumn('id', function ($query, $keyword) { -+ if ($keyword !== '') { -+ $query->where('customer.id', 'LIKE', '%' . $keyword . '%'); -+ } -+ }) -+ ->filterColumn('name', function ($query, $keyword) { -+ if ($keyword !== '') { -+ $query->where(function ($q) use ($keyword) { -+ $q->where('name', 'LIKE', '%' . $keyword . '%') -+ ->orWhere('firstname', 'LIKE', '%' . $keyword . '%'); -+ }); -+ } -+ }) -+ ->filter(function ($query) use ($request) { -+ $location = $request->input('filter_location'); -+ if ($location && $location !== '') { -+ $query->where(function ($q) use ($location) { -+ $q->where('zip', 'LIKE', '%' . $location . '%') -+ ->orWhere('city', 'LIKE', '%' . $location . '%'); -+ }); -+ } -+ -+ $search = $request->input('search.value'); -+ if ($search && $search !== '') { -+ $query->where(function ($q) use ($search) { -+ $q->where('name', 'LIKE', '%' . $search . '%') -+ ->orWhere('firstname', 'LIKE', '%' . $search . '%') -+ ->orWhere('email', 'LIKE', '%' . $search . '%') -+ ->orWhere('phone', 'LIKE', '%' . $search . '%') -+ ->orWhere('phonemobile', 'LIKE', '%' . $search . '%'); -+ }); -+ } -+ }, true) -+ ->rawColumns(['action_edit', 'action_delete', 'id']) -+ ->make(true); -+ } -+} diff --git a/dev/backups/phase2-offers-2026-04-17/PATCHES/Customer.php.phase1-only.diff b/dev/backups/phase2-offers-2026-04-17/PATCHES/Customer.php.phase1-only.diff deleted file mode 100644 index 10750a2..0000000 --- a/dev/backups/phase2-offers-2026-04-17/PATCHES/Customer.php.phase1-only.diff +++ /dev/null @@ -1,40 +0,0 @@ -diff --git a/app/Models/Customer.php b/app/Models/Customer.php -index 94e618e..436b3ee 100644 ---- a/app/Models/Customer.php -+++ b/app/Models/Customer.php -@@ -9,6 +9,7 @@ namespace App\Models; - use App\Models\Sym\TravelCountry; - use Carbon\Carbon; - use Illuminate\Database\Eloquent\Collection; -+use Illuminate\Database\Eloquent\Factories\HasFactory; - use Illuminate\Database\Eloquent\Model; - - /** -@@ -83,6 +84,8 @@ use Illuminate\Database\Eloquent\Model; - */ - class Customer extends Model - { -+ use HasFactory; -+ - protected $connection = 'mysql'; - - protected $table = 'customer'; -@@ -90,13 +93,12 @@ class Customer extends Model - protected $casts = [ - 'salutation_id' => 'int', - 'credit_card_type_id' => 'int', -- 'country_id' => 'int' -- ]; -+ 'country_id' => 'int', -+ 'birthdate' => 'datetime', -+ 'credit_card_expiration_date' => 'datetime', -+ ]; - -- protected $dates = [ -- 'birthdate', -- 'credit_card_expiration_date' -- ]; -+ - - protected $fillable = [ - 'salutation_id', diff --git a/dev/backups/phase2-offers-2026-04-17/PATCHES/Lead.php.phase1-only.diff b/dev/backups/phase2-offers-2026-04-17/PATCHES/Lead.php.phase1-only.diff deleted file mode 100644 index 3f81f4e..0000000 --- a/dev/backups/phase2-offers-2026-04-17/PATCHES/Lead.php.phase1-only.diff +++ /dev/null @@ -1,44 +0,0 @@ -diff --git a/app/Models/Lead.php b/app/Models/Lead.php -index 8b84b9e..227bd38 100644 ---- a/app/Models/Lead.php -+++ b/app/Models/Lead.php -@@ -9,6 +9,7 @@ namespace App\Models; - use Carbon\Carbon; - use App\Services\Passolution; - use App\Models\Lead as ModelsLead; -+use Illuminate\Database\Eloquent\Factories\HasFactory; - use Illuminate\Database\Eloquent\Model; - use Illuminate\Database\Eloquent\Collection; - -@@ -107,6 +108,8 @@ use Illuminate\Database\Eloquent\Collection; - */ - class Lead extends Model - { -+ use HasFactory; -+ - protected $connection = 'mysql'; - - protected $table = 'lead'; -@@ -126,16 +129,14 @@ class Lead extends Model - 'travelcategory_id' => 'int', - 'price' => 'float', - 'pax' => 'int', -- 'participant_salutation_id' => 'int' -+ 'participant_salutation_id' => 'int', -+ 'request_date' => 'datetime', -+ 'travelperiod_start' => 'datetime', -+ 'travelperiod_end' => 'datetime', -+ 'next_due_date' => 'datetime', -+ 'participant_birthdate' => 'datetime', - ]; - -- protected $dates = [ -- 'request_date', -- 'travelperiod_start', -- 'travelperiod_end', -- 'next_due_date', -- 'participant_birthdate' -- ]; - - protected $fillable = [ - 'customer_id', diff --git a/dev/backups/phase2-offers-2026-04-17/PATCHES/Repositories.full.diff b/dev/backups/phase2-offers-2026-04-17/PATCHES/Repositories.full.diff deleted file mode 100644 index e4e519b..0000000 --- a/dev/backups/phase2-offers-2026-04-17/PATCHES/Repositories.full.diff +++ /dev/null @@ -1,159 +0,0 @@ -diff --git a/app/Repositories/BookingPDFRepository.php b/app/Repositories/BookingPDFRepository.php -index 505ddaf..e5ba139 100644 ---- a/app/Repositories/BookingPDFRepository.php -+++ b/app/Repositories/BookingPDFRepository.php -@@ -22,7 +22,7 @@ class BookingPDFRepository extends BaseRepository - public function __construct(Booking $model) - { - $this->model = $model; -- $this->prepath = Storage::disk('public')->getAdapter()->getPathPrefix(); -+ $this->prepath = Storage::disk('public')->path(''); - } - - public function update($data) -@@ -63,13 +63,13 @@ class BookingPDFRepository extends BaseRepository - { - $document = new stdClass(); - $document->name = 'registration'; -- $document->number = $this->model->lead_id; -+ $document->number = $this->model->inquiry_id; - $document->title = 'BUCHUNGSAUFTRAG'; - $document->voucher = null; - $document->date = now(); - $document->total = $this->model->getPriceRaw(); - $dir = $this->getDirPath('pdf', 'booking', $document->date->format('Y')); -- $filename = "Buchnungsauftrag-" . $this->model->lead_id . ".pdf"; -+ $filename = "Buchnungsauftrag-" . $this->model->inquiry_id . ".pdf"; - $pdf_file = new CreatePDF('pdf.booking_registration'); - $data = [ - 'booking' => $this->model, -@@ -85,7 +85,7 @@ class BookingPDFRepository extends BaseRepository - { - $document = new stdClass(); - $document->name = 'confirmation'; -- $document->number = $this->model->lead_id; -+ $document->number = $this->model->inquiry_id; - $document->title = 'REISEBESTÄTIGUNG'; - $document->voucher = null; - $document->date = now(); -@@ -104,7 +104,7 @@ class BookingPDFRepository extends BaseRepository - $document->final_payment_date = date('Y-m-d'); - } - $dir = $this->getDirPath('pdf', 'booking', $document->date->format('Y')); -- $filename = "Reisebestätigung-" . $this->model->lead_id . ".pdf"; -+ $filename = "Reisebestätigung-" . $this->model->inquiry_id . ".pdf"; - - $pdf_file = new CreatePDF('pdf.booking_confirmation'); - $data = [ -@@ -160,14 +160,14 @@ class BookingPDFRepository extends BaseRepository - { - $document = new stdClass(); - $document->name = 'voucher'; -- $document->number = $this->model->lead_id; -+ $document->number = $this->model->inquiry_id; - $document->name = 'voucher'; - $document->title = $agency ? 'VOUCHER Agentur' : 'VOUCHER'; - $document->voucher = $agency ? 'agency' : 'client'; - $document->date = now(); - - $dir = $this->getDirPath('pdf', 'voucher', $document->date->format('Y')); -- $filename = ($agency ? 'VoucherAgentur' : 'Voucher') . "-" . $this->model->lead_id . ".pdf"; -+ $filename = ($agency ? 'VoucherAgentur' : 'Voucher') . "-" . $this->model->inquiry_id . ".pdf"; - - $pdf_file = new CreatePDF('pdf.booking_voucher'); - $data = [ -@@ -224,7 +224,7 @@ class BookingPDFRepository extends BaseRepository - //init document - $document = new stdClass(); - $document->name = $identifier; -- $document->number = $this->model->lead_id; -+ $document->number = $this->model->inquiry_id; - $document->title = 'STORNOBESTÄTIGUNG'; - $document->voucher = null; - $document->date = Carbon::parse($data['storno_print']); -@@ -253,7 +253,7 @@ class BookingPDFRepository extends BaseRepository - - - $dir = $this->getDirPath('pdf', 'storno', $document->date->format('Y')); -- $filename = "Reisestornierung -" . $this->model->lead_id . ".pdf"; -+ $filename = "Reisestornierung -" . $this->model->inquiry_id . ".pdf"; - - $pdf_file = new CreatePDF('pdf.booking_storno'); - $data = [ -@@ -288,7 +288,9 @@ class BookingPDFRepository extends BaseRepository - $fill = [ - 'booking_id' => $this->model->id, - 'customer_id' => $this->model->customer_id, -- 'lead_id' => $this->model->lead_id, -+ // booking_documents.lead_id ist ein Shadow-Feld von booking.inquiry_id; -+ // die Spalte selbst wird von Phase 2 nicht umbenannt. -+ 'lead_id' => $this->model->inquiry_id, - 'identifier' => $identifier, - 'filename' => $filename, - 'dir' => $dir, -diff --git a/app/Repositories/CustomerMailRepository.php b/app/Repositories/CustomerMailRepository.php -index c1b1226..6397f0c 100644 ---- a/app/Repositories/CustomerMailRepository.php -+++ b/app/Repositories/CustomerMailRepository.php -@@ -135,7 +135,8 @@ class CustomerMailRepository extends BaseRepository { - $customer_mail->fill([ - 'booking_id' => $booking->id, - 'customer_id' => $booking->customer_id, -- 'lead_id' => $booking->lead_id, -+ // customer_mails.lead_id-Spalte bleibt unverändert; Wert kommt aus booking.inquiry_id -+ 'lead_id' => $booking->inquiry_id, - 'is_answer' => $is_answer, - 'reply_id' => $reply_id, - 'email' => $mail_from, -@@ -153,7 +154,8 @@ class CustomerMailRepository extends BaseRepository { - $customer_mail = CustomerMail::create([ - 'booking_id' => $booking->id, - 'customer_id' => $booking->customer_id, -- 'lead_id' => $booking->lead_id, -+ // customer_mails.lead_id-Spalte bleibt unverändert; Wert kommt aus booking.inquiry_id -+ 'lead_id' => $booking->inquiry_id, - 'is_answer' => $is_answer, - 'reply_id' => $reply_id, - 'email' => $mail_from, -@@ -300,7 +302,7 @@ class CustomerMailRepository extends BaseRepository { - $value->id = $customer_mail->booking_id; - $value->booking = $booking; - $value->show = 'single'; -- $value->lead_title_id = " - (".$value->booking->lead_id.")"; -+ $value->lead_title_id = " - (".$value->booking->inquiry_id.")"; - - - $tmp = []; -@@ -342,7 +344,7 @@ class CustomerMailRepository extends BaseRepository { - $value->booking = $booking; - $value->show = 'single'; - $value->draft = true; -- $value->lead_title_id = " - (".$value->booking->lead_id.")"; -+ $value->lead_title_id = " - (".$value->booking->inquiry_id.")"; - - }else{ - //multi -@@ -379,8 +381,8 @@ class CustomerMailRepository extends BaseRepository { - $value->draft = false; - $value->booking = $booking; - $value->message = ""; -- $value->subject = " - (".$value->booking->lead_id.")"; -- $value->lead_title_id = " - (".$value->booking->lead_id.")"; -+ $value->subject = " - (".$value->booking->inquiry_id.")"; -+ $value->lead_title_id = " - (".$value->booking->inquiry_id.")"; - $value->s_placeholder = "Betreff des Kunden"; - $value->m_placeholder = "Nachricht des Kunden"; - if(isset($data['customer_mail_id']) && $customer_mail = CustomerMail::find($data['customer_mail_id'])){ -diff --git a/app/Repositories/LeadRepository.php b/app/Repositories/LeadRepository.php -index 929426d..ad0ac87 100644 ---- a/app/Repositories/LeadRepository.php -+++ b/app/Repositories/LeadRepository.php -@@ -134,7 +134,7 @@ class LeadRepository extends BaseRepository { - $data = [ - 'booking_date' => date('Y-m-d'), //now - 'customer_id' => $this->model->customer->id, -- 'lead_id' => $this->model->id, -+ 'inquiry_id' => $this->model->id, - 'new_drafts' => 1, - 'sf_guard_user_id' => $this->model->sf_guard_user_id, - 'branch_id' => 4, diff --git a/dev/backups/phase2-offers-2026-04-17/PATCHES/Repositories.phase1-only.diff b/dev/backups/phase2-offers-2026-04-17/PATCHES/Repositories.phase1-only.diff deleted file mode 100644 index 932b124..0000000 --- a/dev/backups/phase2-offers-2026-04-17/PATCHES/Repositories.phase1-only.diff +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/app/Repositories/BookingPDFRepository.php b/app/Repositories/BookingPDFRepository.php -index 505ddaf..dc4d313 100644 ---- a/app/Repositories/BookingPDFRepository.php -+++ b/app/Repositories/BookingPDFRepository.php -@@ -22,7 +22,7 @@ class BookingPDFRepository extends BaseRepository - public function __construct(Booking $model) - { - $this->model = $model; -- $this->prepath = Storage::disk('public')->getAdapter()->getPathPrefix(); -+ $this->prepath = Storage::disk('public')->path(''); - } - - public function update($data) diff --git a/dev/backups/phase2-offers-2026-04-17/PATCHES/Services-Commands.phase1-only.diff b/dev/backups/phase2-offers-2026-04-17/PATCHES/Services-Commands.phase1-only.diff deleted file mode 100644 index 88694fe..0000000 --- a/dev/backups/phase2-offers-2026-04-17/PATCHES/Services-Commands.phase1-only.diff +++ /dev/null @@ -1,405 +0,0 @@ -diff --git a/app/Console/Commands/ContactsFindDuplicates.php b/app/Console/Commands/ContactsFindDuplicates.php -new file mode 100644 -index 0000000..cef982d ---- /dev/null -+++ b/app/Console/Commands/ContactsFindDuplicates.php -@@ -0,0 +1,160 @@ -+info('Suche nach Duplikaten in der customer-Tabelle...'); -+ $this->newLine(); -+ -+ $groups = collect(); -+ -+ // ── HIGH: gleiche E-Mail ────────────────────────────────────────── -+ if ($this->shouldCheck('HIGH')) { -+ $emailDupes = DB::table('customer') -+ ->select('email', DB::raw('COUNT(*) as cnt'), DB::raw('GROUP_CONCAT(id ORDER BY updated_at DESC) as ids')) -+ ->whereNotNull('email') -+ ->where('email', '!=', '') -+ ->whereNull('merged_into_id') -+ ->groupBy('email') -+ ->having('cnt', '>', 1) -+ ->get(); -+ -+ foreach ($emailDupes as $row) { -+ $groups->push([ -+ 'confidence' => 'HIGH', -+ 'reason' => 'E-Mail: ' . $row->email, -+ 'ids' => $row->ids, -+ 'count' => $row->cnt, -+ ]); -+ } -+ -+ $this->line(sprintf('HIGH (gleiche E-Mail): %d Gruppen', $emailDupes->count())); -+ } -+ -+ // ── MEDIUM: Name + Vorname + Geburtsdatum ──────────────────────── -+ if ($this->shouldCheck('MEDIUM')) { -+ $nameBdDupes = DB::table('customer') -+ ->select( -+ 'name', 'firstname', 'birthdate', -+ DB::raw('COUNT(*) as cnt'), -+ DB::raw('GROUP_CONCAT(id ORDER BY updated_at DESC) as ids') -+ ) -+ ->whereNotNull('name') -+ ->whereNotNull('firstname') -+ ->whereNotNull('birthdate') -+ ->whereNull('merged_into_id') -+ ->groupBy('name', 'firstname', 'birthdate') -+ ->having('cnt', '>', 1) -+ ->get(); -+ -+ foreach ($nameBdDupes as $row) { -+ $groups->push([ -+ 'confidence' => 'MEDIUM', -+ 'reason' => "Name: {$row->firstname} {$row->name}, GD: {$row->birthdate}", -+ 'ids' => $row->ids, -+ 'count' => $row->cnt, -+ ]); -+ } -+ -+ $this->line(sprintf('MEDIUM (Name+GD): %d Gruppen', $nameBdDupes->count())); -+ } -+ -+ // ── LOW: Name + Vorname + PLZ ───────────────────────────────────── -+ if ($this->shouldCheck('LOW')) { -+ $nameZipDupes = DB::table('customer') -+ ->select( -+ 'name', 'firstname', 'zip', -+ DB::raw('COUNT(*) as cnt'), -+ DB::raw('GROUP_CONCAT(id ORDER BY updated_at DESC) as ids') -+ ) -+ ->whereNotNull('name') -+ ->whereNotNull('firstname') -+ ->whereNotNull('zip') -+ ->where('zip', '!=', '') -+ ->whereNull('merged_into_id') -+ ->groupBy('name', 'firstname', 'zip') -+ ->having('cnt', '>', 1) -+ ->get(); -+ -+ foreach ($nameZipDupes as $row) { -+ $groups->push([ -+ 'confidence' => 'LOW', -+ 'reason' => "Name: {$row->firstname} {$row->name}, PLZ: {$row->zip}", -+ 'ids' => $row->ids, -+ 'count' => $row->cnt, -+ ]); -+ } -+ -+ $this->line(sprintf('LOW (Name+PLZ): %d Gruppen', $nameZipDupes->count())); -+ } -+ -+ $this->newLine(); -+ $this->info(sprintf('Gesamt: %d Duplikat-Gruppen gefunden', $groups->count())); -+ -+ if ($groups->isEmpty()) { -+ $this->info('Keine Duplikate — nichts zu tun.'); -+ return self::SUCCESS; -+ } -+ -+ // Tabellen-Ausgabe -+ $this->table( -+ ['Konfidenz', 'Grund', 'IDs (neueste zuerst)', 'Anzahl'], -+ $groups->map(fn ($g) => [$g['confidence'], $g['reason'], $g['ids'], $g['count']])->all() -+ ); -+ -+ // CSV-Export -+ if ($export = $this->option('export')) { -+ $this->exportCsv($groups->all(), $export); -+ $this->info("CSV gespeichert: {$export}"); -+ } else { -+ $this->newLine(); -+ $this->line('Tipp: --export=duplicates.csv für CSV-Export'); -+ $this->line('Tipp: php artisan contacts:merge-duplicates --dry-run zum Prüfen'); -+ } -+ -+ return self::SUCCESS; -+ } -+ -+ private function shouldCheck(string $level): bool -+ { -+ $filter = strtoupper((string) $this->option('confidence')); -+ return $filter === '' || $filter === $level; -+ } -+ -+ private function exportCsv(array $groups, string $path): void -+ { -+ $handle = fopen($path, 'w'); -+ fputcsv($handle, ['Konfidenz', 'Grund', 'IDs (neueste zuerst)', 'Anzahl']); -+ foreach ($groups as $group) { -+ fputcsv($handle, [$group['confidence'], $group['reason'], $group['ids'], $group['count']]); -+ } -+ fclose($handle); -+ } -+} -diff --git a/app/Console/Commands/ContactsMergeDuplicates.php b/app/Console/Commands/ContactsMergeDuplicates.php -new file mode 100644 -index 0000000..e51ca46 ---- /dev/null -+++ b/app/Console/Commands/ContactsMergeDuplicates.php -@@ -0,0 +1,233 @@ -+dryRun = (bool) $this->option('dry-run'); -+ -+ if ($this->dryRun) { -+ $this->warn('DRY-RUN Modus — keine Daten werden verändert'); -+ } else { -+ $this->warn('ACHTUNG: Diese Operation verändert Produktionsdaten.'); -+ if (!$this->option('force') && !$this->confirm('Fortfahren?')) { -+ $this->info('Abgebrochen.'); -+ return self::SUCCESS; -+ } -+ } -+ -+ $this->newLine(); -+ -+ DB::transaction(function () { -+ $this->processLevel( -+ 'HIGH', -+ fn () => $this->findByEmail() -+ ); -+ -+ $this->processLevel( -+ 'MEDIUM', -+ fn () => $this->findByNameBirthdate() -+ ); -+ -+ $this->processLevel( -+ 'LOW', -+ fn () => $this->findByNameZip() -+ ); -+ }); -+ -+ $this->newLine(); -+ $this->info(sprintf( -+ '%s %d Duplikate zusammengeführt | %d leads aktualisiert | %d bookings aktualisiert', -+ $this->dryRun ? '[DRY-RUN]' : '', -+ $this->mergedCount, -+ $this->updatedLeads, -+ $this->updatedBookings -+ )); -+ -+ if ($this->dryRun) { -+ $this->newLine(); -+ $this->line('Zum Ausführen: php artisan contacts:merge-duplicates'); -+ } -+ -+ return self::SUCCESS; -+ } -+ -+ // ───────────────────────────────────────────────────────────────────────── -+ // Duplikat-Gruppen ermitteln -+ // ───────────────────────────────────────────────────────────────────────── -+ -+ private function findByEmail(): array -+ { -+ return DB::table('customer') -+ ->select('email', DB::raw('GROUP_CONCAT(id ORDER BY updated_at DESC, id DESC) as ids')) -+ ->whereNotNull('email') -+ ->where('email', '!=', '') -+ ->whereNull('merged_into_id') -+ ->groupBy('email') -+ ->having(DB::raw('COUNT(*)'), '>', 1) -+ ->pluck('ids') -+ ->map(fn ($ids) => explode(',', $ids)) -+ ->all(); -+ } -+ -+ private function findByNameBirthdate(): array -+ { -+ return DB::table('customer') -+ ->select(DB::raw('GROUP_CONCAT(id ORDER BY updated_at DESC, id DESC) as ids')) -+ ->whereNotNull('name') -+ ->whereNotNull('firstname') -+ ->whereNotNull('birthdate') -+ ->whereNull('merged_into_id') -+ ->groupBy('name', 'firstname', 'birthdate') -+ ->having(DB::raw('COUNT(*)'), '>', 1) -+ ->pluck('ids') -+ ->map(fn ($ids) => explode(',', $ids)) -+ ->all(); -+ } -+ -+ private function findByNameZip(): array -+ { -+ return DB::table('customer') -+ ->select(DB::raw('GROUP_CONCAT(id ORDER BY updated_at DESC, id DESC) as ids')) -+ ->whereNotNull('name') -+ ->whereNotNull('firstname') -+ ->whereNotNull('zip') -+ ->where('zip', '!=', '') -+ ->whereNull('merged_into_id') -+ ->groupBy('name', 'firstname', 'zip') -+ ->having(DB::raw('COUNT(*)'), '>', 1) -+ ->pluck('ids') -+ ->map(fn ($ids) => explode(',', $ids)) -+ ->all(); -+ } -+ -+ // ───────────────────────────────────────────────────────────────────────── -+ // Verarbeitung -+ // ───────────────────────────────────────────────────────────────────────── -+ -+ private function processLevel(string $level, callable $finder): void -+ { -+ if ($filter = $this->option('confidence')) { -+ if (strtoupper($filter) !== $level) { -+ return; -+ } -+ } -+ -+ $groups = $finder(); -+ -+ if (empty($groups)) { -+ $this->line("[{$level}] Keine Duplikate."); -+ return; -+ } -+ -+ $this->line(sprintf('[%s] %d Gruppe(n) gefunden', $level, count($groups))); -+ -+ foreach ($groups as $ids) { -+ $masterId = (int) $ids[0]; // erster = neuester -+ $duplicateIds = array_map('intval', array_slice($ids, 1)); -+ -+ $this->line(sprintf( -+ ' Master: #%d ← Duplikate: %s', -+ $masterId, -+ implode(', ', array_map(fn ($id) => '#' . $id, $duplicateIds)) -+ )); -+ -+ foreach ($duplicateIds as $dupeId) { -+ $this->mergeInto($masterId, $dupeId); -+ } -+ } -+ } -+ -+ private function mergeInto(int $masterId, int $dupeId): void -+ { -+ // 1. Leads umhängen -+ $leadCount = DB::table('lead')->where('customer_id', $dupeId)->count(); -+ if ($leadCount > 0) { -+ $this->line(" lead.customer_id: {$leadCount} Zeile(n) → #{$masterId}"); -+ if (!$this->dryRun) { -+ DB::table('lead') -+ ->where('customer_id', $dupeId) -+ ->update(['customer_id' => $masterId]); -+ } -+ $this->updatedLeads += $leadCount; -+ } -+ -+ // 2. Bookings umhängen -+ $bookingCount = DB::table('booking')->where('customer_id', $dupeId)->count(); -+ if ($bookingCount > 0) { -+ $this->line(" booking.customer_id: {$bookingCount} Zeile(n) → #{$masterId}"); -+ if (!$this->dryRun) { -+ DB::table('booking') -+ ->where('customer_id', $dupeId) -+ ->update(['customer_id' => $masterId]); -+ } -+ $this->updatedBookings += $bookingCount; -+ } -+ -+ // 3. customer_mails umhängen -+ $mailCount = DB::table('customer_mails')->where('customer_id', $dupeId)->count(); -+ if ($mailCount > 0) { -+ $this->line(" customer_mails.customer_id: {$mailCount} Zeile(n) → #{$masterId}"); -+ if (!$this->dryRun) { -+ DB::table('customer_mails') -+ ->where('customer_id', $dupeId) -+ ->update(['customer_id' => $masterId]); -+ } -+ } -+ -+ // 4. lead_mails umhängen -+ $leadMailCount = DB::table('lead_mails')->where('customer_id', $dupeId)->count(); -+ if ($leadMailCount > 0) { -+ $this->line(" lead_mails.customer_id: {$leadMailCount} Zeile(n) → #{$masterId}"); -+ if (!$this->dryRun) { -+ DB::table('lead_mails') -+ ->where('customer_id', $dupeId) -+ ->update(['customer_id' => $masterId]); -+ } -+ } -+ -+ // 5. Duplikat als zusammengeführt markieren -+ if (!$this->dryRun) { -+ DB::table('customer') -+ ->where('id', $dupeId) -+ ->update([ -+ 'merged_into_id' => $masterId, -+ 'merged_at' => now(), -+ ]); -+ } -+ -+ $this->mergedCount++; -+ } -+} diff --git a/dev/backups/phase2-offers-2026-04-17/PATCHES/Views.phase1-only.diff b/dev/backups/phase2-offers-2026-04-17/PATCHES/Views.phase1-only.diff deleted file mode 100644 index e69de29..0000000 diff --git a/dev/backups/phase2-offers-2026-04-17/PATCHES/filesystems.php.full.diff b/dev/backups/phase2-offers-2026-04-17/PATCHES/filesystems.php.full.diff deleted file mode 100644 index 75218f9..0000000 --- a/dev/backups/phase2-offers-2026-04-17/PATCHES/filesystems.php.full.diff +++ /dev/null @@ -1,17 +0,0 @@ -diff --git a/config/filesystems.php b/config/filesystems.php -index 2318ca2..97762c8 100755 ---- a/config/filesystems.php -+++ b/config/filesystems.php -@@ -72,6 +72,12 @@ return [ - 'url' => env('APP_URL').'/storage/booking', - 'visibility' => 'public', - ], -+ 'offer' => [ -+ 'driver' => 'local', -+ 'root' => storage_path('app/offer'), -+ 'url' => env('APP_URL').'/storage/offer', -+ 'visibility' => 'public', -+ ], - 'general' => [ - 'driver' => 'local', - 'root' => storage_path('app/general'), diff --git a/dev/backups/phase2-offers-2026-04-17/restore.sh b/dev/backups/phase2-offers-2026-04-17/restore.sh deleted file mode 100755 index 8c3afda..0000000 --- a/dev/backups/phase2-offers-2026-04-17/restore.sh +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env bash -# -# Restore-Script: spielt Phase 2 + Offers-Änderungen zurück in den Workspace. -# Voraussetzung: Phase 1 wurde erfolgreich auf Live deployed und migriert. -# -# Nutzung: -# bash dev/backups/phase2-offers-2026-04-17/restore.sh # interaktiv -# bash dev/backups/phase2-offers-2026-04-17/restore.sh --force # ohne Nachfrage -# -# Details siehe MANIFEST.md im selben Verzeichnis. - -set -euo pipefail - -BACKUP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "${BACKUP_DIR}/../../../" && pwd)" -FORCE=false - -for arg in "$@"; do - case "$arg" in - --force|-f) FORCE=true ;; - *) echo "Unbekannte Option: $arg"; exit 1 ;; - esac -done - -cd "${PROJECT_ROOT}" - -echo "==========================================" -echo " Phase-2 + Offers Restore" -echo "==========================================" -echo -echo " Projekt-Root: ${PROJECT_ROOT}" -echo " Backup-Ordner: ${BACKUP_DIR}" -echo -echo " Folgende Änderungen werden zurückgespielt:" -echo " - 18 Migrationen (Phase 2, 3, 4, Offers) nach database/migrations/" -echo " - 6 Offer-Models nach app/Models/ (inkl. Überschreiben von Legacy-Offer.php)" -echo " - Code-Reverts via git checkout \${WIP_COMMIT} -- " -echo - -WIP_COMMIT="e3dc1af" -if ! git cat-file -e "${WIP_COMMIT}" 2>/dev/null; then - echo "FEHLER: Commit ${WIP_COMMIT} existiert nicht in diesem Repo." - echo " Wenn das Repo neu geklont wurde, musst du stattdessen manuell" - echo " die Dateien aus dem Tarball wiederherstellen." - echo " Siehe MANIFEST.md → Abschnitt 'Manuell'." - exit 2 -fi - -if ! ${FORCE}; then - echo - read -r -p " Wurde Phase 1 auf Live erfolgreich deployed und migriert? (yes/NO): " answer - if [[ "${answer}" != "yes" ]]; then - echo " Abbruch. Bitte erst Phase 1 auf Live einspielen." - exit 3 - fi - echo - read -r -p " Trotzdem fortfahren? (yes/NO): " confirm - if [[ "${confirm}" != "yes" ]]; then - echo " Abbruch durch User." - exit 3 - fi -fi - -echo -echo "--- 1/4 --- Migrationen zurückspielen" -cp -v "${BACKUP_DIR}/FILES/migrations/"*.php database/migrations/ -echo " → $(ls "${BACKUP_DIR}/FILES/migrations/" | wc -l) Migrationen kopiert" - -echo -echo "--- 2/4 --- Offer-Models zurückspielen" -cp -v "${BACKUP_DIR}/FILES/models/"*.php app/Models/ -echo " → 6 Models kopiert (inkl. Überschreiben Legacy-Offer.php)" - -echo -echo "--- 3/4 --- Code-Reverts aus Git-Commit ${WIP_COMMIT}" -FILES_TO_RESTORE=( - "app/Models/Booking.php" - "app/Models/Customer.php" - "app/Models/Contact.php" - "app/Models/Lead.php" - "app/Repositories/BookingPDFRepository.php" - "app/Repositories/LeadRepository.php" - "app/Repositories/CustomerMailRepository.php" - "app/Http/Controllers/RequestController.php" - "app/Http/Controllers/API/BookingController.php" - "app/Http/Controllers/Admin/ReportController.php" - "app/Http/Controllers/Admin/ReportProviderController.php" - "app/Http/Controllers/Admin/ReportLeadsController.php" - "app/Http/Controllers/LeadController.php" - "app/Http/Controllers/CustomerController.php" - "app/Http/Controllers/ContactController.php" - "app/Services/BookingImport.php" - "app/Console/Commands/SyncNewsletterKulturreisen.php" - "app/Console/Commands/ContactsFindDuplicates.php" - "app/Console/Commands/ContactsMergeDuplicates.php" - "resources/views/customer/mail/modal-show-mail-inner.blade.php" - "resources/views/pdf/components/booking_head.blade.php" - "resources/views/pdf/components/booking_header.blade.php" - "config/filesystems.php" -) -for f in "${FILES_TO_RESTORE[@]}"; do - git checkout "${WIP_COMMIT}" -- "${f}" - echo " ↺ ${f}" -done - -echo -echo "--- 4/4 --- Verifikation" -echo -echo " git status (sollte Phase-2 + Offers-Dateien als modifiziert zeigen):" -git status --short | head -40 -echo -echo " Verbliebene Phase-2-Marker im Code (sollten nun wieder da sein):" -PHASE2_HITS=$(grep -rn "inquiry_id" app/ 2>/dev/null | head -5 || true) -if [[ -n "${PHASE2_HITS}" ]]; then - echo "${PHASE2_HITS}" - echo " → OK, Phase-2-Code ist wieder aktiv." -else - echo " (keine gefunden — Restore eventuell nicht vollständig)" -fi -echo - -echo "==========================================" -echo " Restore abgeschlossen." -echo "==========================================" -echo -echo " Nächste Schritte:" -echo " 1. php -l auf allen geänderten Dateien (Syntax-Check)" -echo " 2. composer dump-autoload (neue Models registrieren)" -echo " 3. Weiterarbeit an Phase 2 + Offers nach Plan" -echo " 4. Phase-2-Migrationen ausführen:" -echo " php artisan migrate --path=database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php" -echo " php artisan migrate --path=database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php" -echo " php artisan migrate --path=database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php" -echo diff --git a/dev/customer-bookings/phase-1-live-deploy.md b/dev/customer-bookings/phase-1-live-deploy.md deleted file mode 100644 index fabc883..0000000 --- a/dev/customer-bookings/phase-1-live-deploy.md +++ /dev/null @@ -1,241 +0,0 @@ -# Phase-1-Live-Deploy — Anleitung - -**Ausgangssituation:** -- Test-System läuft mit Phase 1 (Laravel-10-Upgrade, Contacts-Modul, Duplicats-Commands, Soft-Delete- & Merge-Fields auf `customer`-Tabelle) -- Live-Server ist bei Git-Commit `389d5d1` (Januar 2026) — noch ohne Phase 1 -- Der Workspace wurde per `dev/backups/phase2-offers-2026-04-17/` um Phase 2 + Offers bereinigt und ist jetzt **exakt auf Phase-1-Stand** wie Test - ---- - -## Vorbereitung (einmalig) - -1. **Backup auf Live-Server** - - Vollständiges DB-Backup (mysqldump oder Hoster-Tool) — speichere mit Zeitstempel - - Vollständiges Dateisystem-Backup (zumindest `/app`, `/config`, `/routes`, `/resources`, `/database/migrations`, `/composer.json`, `/composer.lock`) - -2. **Maintenance-Mode aktivieren** (empfohlen für 15–30 min) - ```bash - php artisan down --render="errors::503" --secret="dein-geheimer-preview-token" - ``` - Mit `--secret` kannst du über `https://domain/dein-geheimer-preview-token` weiter auf Live zugreifen, während alle anderen die 503-Seite sehen. - -3. **Abhängigkeiten abklären** - - Läuft auf Live mindestens **PHP 8.1** (Laravel-10-Anforderung)? Prüfen: `php -v` - - Ist **Composer** auf dem Live-Server installiert? Falls Managed Hosting: `composer install` vor dem Deploy auf einem Staging-Server ausführen und das `vendor/`-Verzeichnis mitsyncen - ---- - -## Upload (rsync / scp / SFTP) - -### Was **muss** hochgeladen werden - -**Neue Dateien:** -``` -database/migrations/2025_04_15_100001_phase1_add_merge_fields_to_customer_table.php -database/migrations/2025_04_15_100002_phase1_add_soft_delete_to_customer_table.php -app/Models/Contact.php -app/Repositories/ContactRepository.php -app/Http/Controllers/ContactController.php -app/Console/Commands/ContactsFindDuplicates.php -app/Console/Commands/ContactsMergeDuplicates.php -app/Services/MailDirService.php -resources/views/contact/_detail_contact.blade.php -resources/views/contact/_detail_history.blade.php -resources/views/contact/detail.blade.php -resources/views/contact/duplicates.blade.php -resources/views/contact/index.blade.php -database/factories/BookingFactory.php -database/factories/CustomerFactory.php -database/factories/LeadFactory.php -tests/Feature/Api/BookingImportTest.php -tests/Feature/Auth/LoginTest.php -tests/Feature/BookingControllerTest.php -tests/Unit/Services/UtilTest.php -``` - -**Modifizierte Dateien** — vollständige Upload-Liste generieren: - -```bash -cd /workspace/mein.sterntours.de -git diff --name-only HEAD^ \ - | grep -Ev "^(dev/|tests/|\.env|\.mcp\.json$|CLAUDE\.md$|boost\.json$|_ide_helper|\.devcontainer/|bootstrap/cache/|mein\.sterntours\.de\.code-workspace$|docker-compose\.yml$|public/storage$)" \ - > /tmp/phase1-files-to-upload.txt -cat /tmp/phase1-files-to-upload.txt -wc -l /tmp/phase1-files-to-upload.txt -``` - -Das filtert die unerwünschten Dateien automatisch raus. - -Die wichtigsten modifizierten Dateien (komplette Liste in der generierten Textdatei): -- `app/Models/Customer.php`, `Booking.php`, `Lead.php` (und viele weitere Models — Laravel-10-Upgrade) -- `app/Http/Controllers/*.php` (Controllers mit Laravel-10-Anpassungen) -- `app/Http/Kernel.php`, `app/Console/Kernel.php`, `app/Providers/*.php`, `app/Http/Middleware/TrustProxies.php` (Laravel-10-Upgrade) -- `app/Repositories/*.php` (Laravel-10: `Storage::disk()->path()` statt `getAdapter()->getPathPrefix()`) -- `app/Libraries/CreatePDF.php` -- `routes/web.php` (neue `/contacts`-Routen) -- `resources/views/layouts/application.blade.php`, `resources/views/layouts/includes/layout-sidenav.blade.php` -- `config/trustedproxy.php` -- `packages/digital-bird/shoppingcart/**`, `packages/iqcontent/laravel-filemanager/**` (Package-Updates) -- `composer.json`, `composer.lock` -- `phpunit.xml` - -### Was **nicht** hochgeladen werden darf - -- **Alles unter `dev/`** — dort liegt Entwicklungsdokumentation und die Backups (Phase 2, Offers) -- `.env`, `.env.*` — enthält produktive Credentials, wird separat verwaltet -- `_ide_helper.php`, `_ide_helper_models.php` — lokale IDE-Hilfen (von barryvdh/laravel-ide-helper generiert) -- `bootstrap/cache/config.php` — wird auf Live per `php artisan config:cache` frisch generiert -- `.mcp.json`, `CLAUDE.md`, `boost.json` — Dev-Tools-Konfiguration -- `.devcontainer/` — Entwicklungsumgebung (VSCode-Devcontainer) -- `storage/logs/*`, `storage/framework/cache/*`, `storage/framework/views/*`, `storage/framework/sessions/*` — Runtime-Daten -- `vendor/` — wird auf Live per `composer install` generiert (außer Managed Hosting, siehe oben) -- `node_modules/` — genauso -- `.git/` — Repo-Metadaten -- `tests/` — nur auf Test relevant; auf Live-Production nicht nötig (aber nicht schädlich, falls mit rauf kommt) -- `mein.sterntours.de.code-workspace` — IDE-Workspace-Datei -- `docker-compose.yml` — lokale Dev-Orchestrierung - -### Beispiel rsync-Kommando (als Orientierung) - -```bash -# Von lokal → Live -# Achtung: --dry-run zuerst, dann ohne --dry-run wiederholen! -rsync -av --dry-run \ - --exclude='.git/' \ - --exclude='dev/' \ - --exclude='node_modules/' \ - --exclude='vendor/' \ - --exclude='storage/logs/' \ - --exclude='storage/framework/cache/' \ - --exclude='storage/framework/views/' \ - --exclude='storage/framework/sessions/' \ - --exclude='storage/app/public/temp/' \ - --exclude='public/storage' \ - --exclude='.env*' \ - --exclude='bootstrap/cache/' \ - --exclude='_ide_helper*' \ - --exclude='.mcp.json' \ - --exclude='CLAUDE.md' \ - --exclude='boost.json' \ - --exclude='.devcontainer/' \ - /workspace/mein.sterntours.de/ \ - user@live-server:/pfad/zu/mein.sterntours.de/ -``` - -Falls du **nur geänderte Dateien** syncen möchtest (sicherer bei großer Codebase): -```bash -git diff --name-only HEAD^ | grep -v "^dev/" > /tmp/phase1-files.txt -rsync -av --files-from=/tmp/phase1-files.txt . user@live-server:/pfad/zu/mein.sterntours.de/ -``` - ---- - -## Ausführung auf Live-Server - -Nach dem Upload, per SSH auf dem Live-Server: - -```bash -cd /pfad/zu/mein.sterntours.de - -# 1. Composer Dependencies aktualisieren (Laravel 10 Upgrade!) -composer install --no-dev --optimize-autoloader - -# 2. Alle Caches leeren (wichtig wegen Struktur-Änderungen) -php artisan config:clear -php artisan route:clear -php artisan view:clear -php artisan cache:clear - -# 3. DB-Migrationen ausführen (nur die 2 Phase-1-Migrationen) -php artisan migrate --force -# Sollte zeigen: -# Running: 2025_04_15_100001_phase1_add_merge_fields_to_customer_table -# Running: 2025_04_15_100002_phase1_add_soft_delete_to_customer_table - -# 4. Autoload neu generieren (neue Klassen: Contact, ContactRepository, MailDirService) -composer dump-autoload --optimize - -# 5. Duplicats-Analyse (read-only, erzeugt Reports) -php artisan contacts:find-duplicates -# → Reports unter storage/app/contacts/duplicates/*.csv - -# 6. Duplicats-Merging (DRY-RUN zuerst!) -php artisan contacts:merge-duplicates --dry-run -# Review der Ausgabe. Wenn OK: - -# 7. Duplicats-Merging (echt) -php artisan contacts:merge-duplicates --confidence=HIGH --force -# Merged nur hochkonfidente Duplikate automatisch. -# Mittlere/niedrige Konfidenz bleibt für manuelle Review. - -# 8. Production-Caches wieder aufbauen (für Performance) -php artisan config:cache -php artisan route:cache -php artisan view:cache -# Optional: php artisan event:cache -# NICHT php artisan optimize (das cached auch, aber einige Setups haben damit Probleme) - -# 9. Maintenance-Mode deaktivieren -php artisan up -``` - ---- - -## Smoke-Tests nach dem Deploy - -Nach dem `php artisan up` diese Kernfunktionen manuell testen: - -- [ ] Login funktioniert (`/login`) -- [ ] Buchungsliste lädt (`/booking`) -- [ ] Einzelne Buchung öffnen, Detailseite lädt -- [ ] Anfragenliste lädt (`/lead`) -- [ ] Einzelne Anfrage öffnen, Detailseite lädt -- [ ] **NEU**: Kontakte-Liste lädt (`/contacts` oder `/contact` je nach Route) -- [ ] **NEU**: Kontakte-Duplikats-Übersicht lädt (`/contacts/duplicates` oder ähnlich) -- [ ] PDF-Erzeugung funktioniert (Buchungsbestätigung, Voucher, Storno) — testet `Storage::disk()->path()`-Änderung -- [ ] Mail-Versand über CustomerMail funktioniert -- [ ] Admin-Report läuft durch (`/admin/report`) - ---- - -## Rollback-Plan (falls etwas schiefgeht) - -1. **Maintenance-Mode aktivieren:** `php artisan down` -2. **DB-Rollback:** - ```bash - php artisan migrate:rollback --step=2 - ``` - oder — falls das fehlschlägt — das zuvor erstellte DB-Backup einspielen. -3. **Dateisystem-Rollback:** das zuvor erstellte Dateisystem-Backup zurückspielen (überschreiben). -4. **Caches leeren + Up:** - ```bash - php artisan config:clear && php artisan cache:clear && php artisan view:clear && php artisan up - ``` - ---- - -## Nach erfolgreichem Live-Deploy - -1. **Lokal** im Workspace: Restore-Script laufen lassen, um Phase 2 + Offers zurückzuholen: - ```bash - cd /workspace/mein.sterntours.de - bash dev/backups/phase2-offers-2026-04-17/restore.sh - ``` -2. Der Workspace enthält dann wieder Phase 2 Code + Offers-Modul. Weitermachen nach Plan in: - - `dev/customer-bookings/umsetzung.md` (Phase 2–4) - - `dev/offers/umsetzung.md` (Offers-Modul) -3. **Master-Branch** im Git kann bei Gelegenheit mit dem WIP-Commit `e3dc1af` aktualisiert werden (push), damit der Remote-Stand den aktuellen Arbeitsstand abbildet. - ---- - -## Dateien in diesem Backup - -- `dev/backups/phase2-offers-2026-04-17/MANIFEST.md` — detaillierte Übersicht -- `dev/backups/phase2-offers-2026-04-17/FILES/migrations/` — 18 Migrations-Dateien -- `dev/backups/phase2-offers-2026-04-17/FILES/models/` — 6 Offer-Models -- `dev/backups/phase2-offers-2026-04-17/PATCHES/` — 11 Diff-Dateien zur Dokumentation -- `dev/backups/phase2-offers-2026-04-17/restore.sh` — automatisches Restore - -**Zusätzliche Sicherheitsnetze:** -- Tarball: `../backups-safety/workspace-pre-phase1-rollback-2026-04-17.tar.gz` (182 MB, kompletter Workspace-State vor dem Rückbau) -- Git-Commit: `e3dc1af` auf lokalem `master` (nicht gepusht) — enthält Phase 1 + 2 + Offers gemischt diff --git a/dev/customer-bookings/phase-2-live-deploy.md b/dev/customer-bookings/phase-2-live-deploy.md deleted file mode 100644 index fe367a1..0000000 --- a/dev/customer-bookings/phase-2-live-deploy.md +++ /dev/null @@ -1,305 +0,0 @@ -# Phase-2-Live-Deploy — Anleitung - -**Ausgangssituation (nach Phase-1-Live-Deploy am 2026-04-17):** -- Live: Phase 1 ✅ (Code + DB-Migrationen 25, 26) -- Test: Phase 1 ✅ + Phase 2 ✅ (Code + DB-Migrationen 25–29, verifiziert) -- Workspace (lokal): Phase 1 + Phase 2 + Offers-Code ready - -Dieses Handbuch beschreibt, wie Phase 2 vom Test-Stand auf Live übertragen wird. Phase 2 umfasst zwei atomar zusammengehörige Teile plus eine kleine Ergänzung aus Phase 1: - -1. **DB-Schema-Änderungen** (3 Migrationen, breaking): - - `RENAME TABLE customer → contacts` - - `RENAME TABLE lead → inquiries` - - Spalte `booking.lead_id → booking.inquiry_id` (inkl. FK-Index-Neuaufbau) -2. **Application-Code-Änderungen** (23 modifizierte Dateien): - - Models `Customer`, `Contact`, `Lead`, `Booking` mit neuen `$table`-Werten bzw. Spaltennamen - - Repositories, Controllers, Services, Commands, Views — alle Referenzen auf `booking.lead_id` → `inquiry_id` umgestellt; Raw-SQL auf `contacts`/`inquiries` umgestellt - - Smoke-Test-Fixes (2026-04-17): `Lead::bookings()` mit explizitem FK `inquiry_id`; `orderColumn`-SQL in `ContactController`/`ReportController`/`ReportBookingController`/`ReportLeadsController` auf `contacts.*` gezogen. -3. **Phase-1-Hotfixes — nullable Parameter in den Mail-Dir-Services** (3 modifizierte Dateien): - - `app/Services/Booking.php`, `app/Services/Lead.php`, `app/Services/MailDirService.php` - - `int $mailDirId` → `?int $mailDirId` bei `getCustomerMailName()` / `getCustomerMailEmails()` / `resolveModel()` (behebt Mail-Dialog-Crash wenn `customer_mails.mail_dir_id` / `lead_mails.mail_dir_id` NULL ist). - - `string $subdir` → `?string $subdir` bei `setOutputDirs()` / `setOutputDir()` (behebt Mail-Dialog-Crash wenn `$mail_sdir_id` NULL ist, z.B. bei Entwürfen oder Top-Level-Ordnern). - - Fachlich eigentlich Phase-1-Bugs (Laravel-10/PHP-8-strict-typing vs. `null`-Werte aus nullable DB-Spalten), die aber erst durch die Test-Durchläufe am 2026-04-17 sichtbar wurden. Wir liefern die Fixes zusammen mit Phase 2 aus, weil ein zweites separates Wartungsfenster für drei Service-Dateien nicht sinnvoll wäre und die Änderungen keinerlei DB-Abhängigkeit haben (also auch außerhalb der DB-Migration risikoarm sind). - -> **Wichtig:** DB und Phase-2-Code müssen atomar gemeinsam live gehen. Zwischen den beiden gibt es kein kompatibles Fenster, deswegen ist ein **Maintenance-Window von ~10–20 min Pflicht**. Die 3 Phase-1-Hotfix-Services werden im selben rsync-Schritt mit hochgeladen. - ---- - -## Vorbereitung (am Tag vor dem Deploy) - -1. **Timing**: Wartungsfenster einplanen, idealerweise außerhalb der Geschäftszeiten. ~20 Minuten reichen erfahrungsgemäß. -2. **Deploy-Freigabe**: Phase 1 auf Live läuft stabil (mindestens 24–48 h), keine Regressions-Meldungen. -3. **Team-Kommunikation**: Mitarbeiter:innen informieren — während des Wartungsfensters keine Mails, keine Buchungen anlegen, keine PDFs drucken. - -### Vorab-Kontrolle auf Live (lesend, keine Änderungen) - -```bash -# Live-Server: aktueller Stand? -php artisan --version -# → sollte 10.50+ sein (Phase 1 hat Laravel 10 gebracht) - -php artisan migrate:status | grep phase1 -# → sollte beide phase1-Migrationen mit "Ran" zeigen - -mysql -e "SHOW TABLES LIKE 'customer';" -mysql -e "SHOW TABLES LIKE 'lead';" -# → beide müssen noch existieren (Phase 2 NICHT ausgeführt) - -mysql -e "SHOW COLUMNS FROM booking LIKE 'lead_id';" -# → lead_id muss existieren -``` - -Wenn einer dieser Checks nicht das erwartete Ergebnis liefert: **STOP**, klären bevor weitergemacht wird. - -### Smoke-Tests auf Test (am Deploy-Tag, vor dem Live-Deploy) - -Diese Liste einmal komplett auf `https://mein.sterntours.test` durchgehen. Wenn einer fehlschlägt → Fehler fixen, Live-Deploy verschieben. - -- [ ] Login funktioniert, Dashboard lädt -- [ ] **Buchungen**: `/booking` lädt, Filter- und Sortier-Funktionen arbeiten, Anfrage-Nr-Spalte zeigt korrekte Werte -- [ ] **Buchungsdetail** öffnen, alle Tabs laden (Buchung, Mails, Notizen, Dokumente) -- [ ] **PDF-Generierung** aus einer Buchung: Buchungsauftrag, Reisebestätigung, Voucher (Kunde + Agentur), Storno -- [ ] **Anfragen**: `/lead` lädt, einzelne Anfrage öffnen -- [ ] **Anfrage → Buchung erzeugen** (legt `booking.inquiry_id` korrekt an) -- [ ] **Kontakte**: `/contacts` lädt, einzelner Kontakt öffnet, `leads_count` + `bookings_count` zeigen korrekte Werte (testet die Beziehungen über die neuen Tabellennamen) -- [ ] **Kontakt-Duplikate**: `/contact/duplicates` lädt, Zähler stimmen -- [ ] **Mail-Versand** aus einer Buchung (Eintrag in `customer_mails.lead_id` muss identisch zur `booking.inquiry_id` sein) -- [ ] **Mail-Dialog öffnen** bei Buchung und Anfrage (Modal "Neue Mail schreiben" / "Mail anzeigen") — testet den nullable-`mail_dir_id`-Fix in `Services\Booking::getCustomerMailName()` / `Services\Lead::getCustomerMailName()`. -- [ ] **Admin-Reports**: `/admin/report`, `/admin/report-bookings`, `/admin/report-provider`, `/admin/report-leads` laden UND jeweils einmal nach Kundenname sortieren (testet die `orderColumn`-Fixes für `contacts.firstname/name`) + CSV-Export -- [ ] **BookingImport über API** (falls testbar — feuert gegen `/api/booking/import`) legt Datensatz mit korrekter `inquiry_id` an -- [ ] Newsletter-Sync-Command als Dry-Run: `php artisan newsletter:sync-kulturreisen --dry-run` (falls implementiert) - ---- - -## Deploy-Ablauf Live - -### Schritt 1 — Upload-Liste generieren (lokal) - -Der Phase-2-Delta gegen Phase 1: **3 neue Migrationen + 24 modifizierte Dateien (Phase 2) + 3 Phase-1-Hotfix-Services = 30 Dateien.** Exakte Liste: - -```bash -cd /workspace/mein.sterntours.de -cat > /tmp/phase2-files-to-upload.txt <<'EOF' -database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php -database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php -database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php -app/Models/Booking.php -app/Models/Customer.php -app/Models/Contact.php -app/Models/Lead.php -app/Repositories/BookingPDFRepository.php -app/Repositories/LeadRepository.php -app/Repositories/CustomerMailRepository.php -app/Http/Controllers/RequestController.php -app/Http/Controllers/API/BookingController.php -app/Http/Controllers/Admin/ReportController.php -app/Http/Controllers/Admin/ReportBookingController.php -app/Http/Controllers/Admin/ReportProviderController.php -app/Http/Controllers/Admin/ReportLeadsController.php -app/Http/Controllers/LeadController.php -app/Http/Controllers/CustomerController.php -app/Http/Controllers/ContactController.php -app/Services/BookingImport.php -app/Services/Booking.php -app/Services/Lead.php -app/Services/MailDirService.php -app/Console/Commands/SyncNewsletterKulturreisen.php -app/Console/Commands/ContactsFindDuplicates.php -app/Console/Commands/ContactsMergeDuplicates.php -resources/views/customer/mail/modal-show-mail-inner.blade.php -resources/views/contact/index.blade.php -resources/views/pdf/components/booking_head.blade.php -resources/views/pdf/components/booking_header.blade.php -EOF - -wc -l /tmp/phase2-files-to-upload.txt -# → sollte 30 zeigen (3 Migrationen + 24 Phase-2-Code + 3 Phase-1-Hotfix-Services) -``` - -**Erklärung zu den zusätzlichen Dateien gegenüber einer rein strukturellen Phase-2-Liste:** -- `ReportBookingController.php` — Smoke-Test-Fix am 2026-04-17: `orderColumn('customer.firstname/name', …)` → `contacts.*`. War schon vor dem Fix nicht perfekt, fiele aber beim ersten Sortier-Klick nach Phase-2-Rename hart auf. -- `resources/views/contact/index.blade.php` — Smoke-Test-Fix am 2026-04-17: DataTables-Column-Definition `name: 'customer.id'` → `name: 'contacts.id'` (+ passender `orderColumn('contacts.id', …)` / `filterColumn('contacts.id', …)` im `ContactController`). Yajra verwendet `name:` als raw SQL-Spalte, wenn kein `orderColumn` matched — deswegen muss der Identifier zwischen Blade und Controller konsistent sein und dem neuen Tabellennamen entsprechen. -- `app/Services/Booking.php`, `Lead.php`, `MailDirService.php` — Phase-1-Hotfix: nullable-Parameter (`?int $mailDirId`, `?string $subdir`). Behebt Laravel-10/PHP-8-strict-typing-Fehler bei Mail-Dialogen mit NULL-Werten aus `customer_mails` / `lead_mails`. - -> **Ausdrücklich NICHT mit hochladen:** Phase-3-/Phase-4-Migrationen (5+6 Stück), Offers-Migrationen (7 Stück), Offer-Models (6 Stück), `config/filesystems.php` (offer-Disk). Diese kommen erst mit späteren Modulen und würden auf Live ungewollt Tabellen anlegen oder Code-Erwartungen brechen. - -### Schritt 1b — Upload-Liste gegen den Workspace verifizieren (Sanity-Check) - -Damit sich hier kein Tippfehler einschleicht, einmal prüfen, dass alle 29 Einträge tatsächlich im Workspace existieren: - -```bash -cd /workspace/mein.sterntours.de -MISSING=0 -while read f; do - if [[ ! -f "$f" ]]; then - echo "FEHLT: $f" - MISSING=$((MISSING+1)) - fi -done < /tmp/phase2-files-to-upload.txt -echo "---" -echo "Fehlend: $MISSING (sollte 0 sein)" -echo "Gesamt: $(wc -l < /tmp/phase2-files-to-upload.txt)" -``` - -### Schritt 2 — DB-Backup auf Live - -```bash -# Auf dem Live-Server, vor dem Deploy -TIMESTAMP=$(date +%Y-%m-%d_%H%M) -mysqldump --single-transaction --routines --triggers \ - --databases DEIN_DB_NAME \ - | gzip > /backup/live-db-before-phase2-${TIMESTAMP}.sql.gz - -# Größe prüfen: -ls -lh /backup/live-db-before-phase2-${TIMESTAMP}.sql.gz -``` - -Falls `mysqldump` nicht zur Hand: das Hoster-Tool nutzen (phpMyAdmin-Export, Plesk/cPanel-Backup-Feature, etc.). - -### Schritt 3 — Maintenance-Mode an - -```bash -cd /pfad/zu/mein.sterntours.de -php artisan down --render="errors::503" --secret="phase-2-deploy-$(date +%s)" -# → Secret-URL wird ausgegeben, z.B.: -# https://domain/phase-2-deploy-1746123456 -# Damit kannst du während der Wartung selbst noch als Admin auf die Seite. -``` - -### Schritt 4 — Upload per rsync - -```bash -# Vom lokalen Workspace (oder Test-Server) auf Live -rsync -av --files-from=/tmp/phase2-files-to-upload.txt \ - /workspace/mein.sterntours.de/ \ - user@live-server:/pfad/zu/mein.sterntours.de/ -``` - -Alternative mit scp, falls rsync nicht verfügbar: -```bash -while read f; do - scp "/workspace/mein.sterntours.de/$f" "user@live-server:/pfad/zu/mein.sterntours.de/$f" -done < /tmp/phase2-files-to-upload.txt -``` - -### Schritt 5 — Code-Caches leeren - -Auf dem Live-Server: - -```bash -cd /pfad/zu/mein.sterntours.de -php artisan cache:clear -php artisan config:clear -php artisan route:clear -php artisan view:clear -composer dump-autoload --optimize -``` - -### Schritt 6 — DB-Migrationen gezielt ausführen - -> **Wichtig: KEIN `php artisan migrate` ohne `--path`!** -> Wenn man nacktes `migrate` aufruft, werden auch die im Workspace liegenden Phase-3-, Phase-4- und Offer-Migrationen ausgeführt, die noch nicht deployreif sind. Deswegen gezielt per `--path`: - -```bash -php artisan migrate --path=database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php --force -php artisan migrate --path=database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php --force -php artisan migrate --path=database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php --force - -# Verifikation -php artisan migrate:status | grep phase2 -# → alle drei sollten jetzt "Ran" zeigen -``` - -Erwartete Dauer: wenige Sekunden. `RENAME TABLE` ist in MySQL eine Metadaten-Operation und läuft unabhängig von der Tabellengröße fast instantan. Der Spaltenrename `booking.lead_id → inquiry_id` ist je nach MySQL-Version ebenfalls schnell (MySQL 8+ mit `ALGORITHM=INSTANT`) oder dauert ein paar Sekunden (mit Fallback). - -### Schritt 7 — Production-Caches neu aufbauen - -```bash -php artisan config:cache -php artisan route:cache -php artisan view:cache -``` - -### Schritt 8 — Maintenance-Mode aus - -```bash -php artisan up -``` - -### Schritt 9 — Live-Smoke-Tests - -Dieselbe Checkliste wie bei Test (oben). Besonders aufpassen: -- PDF-Erzeugung (Voucher einer aktuellen Buchung) -- Mail-Versand aus einer Buchung -- Anfrage-Erstellung + daraus Buchung ableiten -- `/contacts/duplicates` öffnen — zeigt korrekte Counts - -Bei Problemen sofort zu **Rollback-Plan** (unten) wechseln. - ---- - -## Rollback-Plan (falls Phase 2 auf Live scheitert) - -Der Rollback muss ebenfalls atomar passieren: **Code und DB zusammen zurück.** - -### Schritt A — Maintenance-Mode an - -```bash -php artisan down --render="errors::503" -``` - -### Schritt B — DB zurückrollen - -Option 1 (sauber, per Migration): -```bash -php artisan migrate:rollback --path=database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php --force -php artisan migrate:rollback --path=database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php --force -php artisan migrate:rollback --path=database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php --force -``` -(Die Migrationen haben symmetrische `down()`-Methoden, die die Renames rückgängig machen.) - -Option 2 (Notfall, falls `migrate:rollback` fehlschlägt): -```bash -gunzip < /backup/live-db-before-phase2-${TIMESTAMP}.sql.gz | mysql DEIN_DB_NAME -``` - -### Schritt C — Code zurück - -Entweder die pre-Phase-2-Versionen der 22 Dateien zurückspielen (z.B. aus dem DB-Backup-Zeitpunkt-Dateisystem-Backup), oder mit git Revert-Commit: -```bash -# Wenn Live-Server ein Git-Checkout ist: -git checkout -- app/ resources/ config/ -``` - -### Schritt D — Caches + Up - -```bash -php artisan cache:clear && php artisan config:clear && php artisan view:clear && php artisan route:clear -composer dump-autoload -php artisan config:cache && php artisan route:cache && php artisan view:cache -php artisan up -``` - ---- - -## Dokumentation nach erfolgreichem Live-Deploy - -1. `dev/customer-bookings/umsetzung.md` updaten: Phase 2 auf Live per 2026-MM-DD. -2. Dieses Handbuch als "durchgeführt" markieren, ggf. Learnings ergänzen. -3. Git-Commit auf Live-Server (falls Git-basiert): mit Tag `deploy/phase-2-live-2026-MM-DD`. -4. Nächster Schritt: Entscheidung, wann Phase 3 (Participants-Unification), Phase 4 (Communications-Unification) oder das Offers-Modul an die Reihe kommen. - ---- - -## Hintergrund: warum das Maintenance-Window nötig ist - -Zwischen DB-Rename und Code-Deploy ist der Zustand inkompatibel: - -| Zustand | DB hat | Code erwartet | Resultat | -|---|---|---|---| -| Vor Deploy | `customer`, `lead`, `booking.lead_id` | `customer`, `lead`, `booking.lead_id` | OK | -| DB-migriert, Code alt | `contacts`, `inquiries`, `booking.inquiry_id` | `customer`, `lead`, `booking.lead_id` | **Fehler: Table doesn't exist** | -| DB alt, Code neu | `customer`, `lead`, `booking.lead_id` | `contacts`, `inquiries`, `booking.inquiry_id` | **Fehler: Table doesn't exist** | -| Nach Deploy | `contacts`, `inquiries`, `booking.inquiry_id` | `contacts`, `inquiries`, `booking.inquiry_id` | OK | - -Deswegen: Maintenance-Mode an → Code + DB gleichzeitig → Maintenance-Mode aus. Alles innerhalb eines Fensters, in dem keine Requests an die App laufen. diff --git a/dev/customer-bookings/umsetzung.md b/dev/customer-bookings/umsetzung.md index 0b88e59..eccfa6d 100644 --- a/dev/customer-bookings/umsetzung.md +++ b/dev/customer-bookings/umsetzung.md @@ -1,10 +1,8 @@ # Umsetzung: Neustrukturierung Customer / Lead / Booking -**Status:** Phase 1 auf **Live** abgeschlossen (2026-04-17); Phase 2 auf **Test** erfolgreich migriert und verifiziert — bereit für Live-Deploy. +**Status:** Phase 1 auf Testsystem abgeschlossen — Contacts-Modul live; **Phase-2-App-Code vorbereitet (deploy-bereit)** **Erstellt:** April 2025 -**Letzte Aktualisierung:** 2026-04-17 -**Konzept:** [konzept.md](konzept.md) -**Deploy-Handbücher:** [phase-1-live-deploy.md](phase-1-live-deploy.md) · [phase-2-live-deploy.md](phase-2-live-deploy.md) +**Konzept:** [konzept.md](konzept.md) --- @@ -12,70 +10,12 @@ | Phase | Status | Deployed auf Test? | Deployed auf Live? | |-------|--------|-------------------|-------------------| -| Phase 1 — Contact-Deduplizierung | ✅ Abgeschlossen | ✅ Ja | ✅ **2026-04-17** | -| Phase 1 — Contacts-Modul (neuer Code) | ✅ Abgeschlossen | ✅ Ja | ✅ **2026-04-17** | -| Phase 2 — App-Code (Models/Repos/Controller/Views auf `contacts`/`inquiries`/`inquiry_id`) | ✅ Abgeschlossen | ✅ Ja | ⬜ Nein (Deploy-Handbuch fertig) | -| Phase 2 — Tabellen-Migrationen (3 Migrationsdateien) | ✅ Abgeschlossen | ✅ **Ja, Batch 27–29** | ⬜ Nein (Deploy-Handbuch fertig) | -| Phase 2 — Smoke-Test-Fixes 2026-04-17 (`Lead::bookings()` FK; `orderColumn`-SQL in 4 Controllern; Blade-Column-`name:` in `contact/index.blade.php`) | ✅ Abgeschlossen | ✅ Ja | ⬜ Nein (in Phase-2-Deploy gebündelt) | -| Phase-1-Hotfix — nullable Parameter (`?int $mailDirId`, `?string $subdir`) in 3 Service-Klassen (Mail-Dialoge mit NULL-Werten) | ✅ Abgeschlossen | ✅ Ja | ⬜ Nein (in Phase-2-Deploy gebündelt, siehe Handbuch) | -| Phase 3 — Participants konsolidieren | ⬜ Ausstehend (Migrationen geschrieben, Code noch nicht) | ⬜ Nein | ⬜ Nein | -| Phase 4 — Communications / Notices / Attachments | ⬜ Ausstehend (Migrationen geschrieben, Code noch nicht) | ⬜ Nein | ⬜ Nein | - -### Verifikation Phase 2 auf Test (2026-04-17) - -Smoke-Queries nach erfolgter Migration: - -``` -Tabellen: - customer NOT FOUND ← umbenannt ✓ - contacts EXISTS ← neu ✓ - lead NOT FOUND ← umbenannt ✓ - inquiries EXISTS ← neu ✓ - booking EXISTS ✓ - -booking-Spalten: - lead_id NOT FOUND ← umbenannt ✓ - inquiry_id EXISTS ← neu ✓ - -Eloquent-Queries: - Contact::count() = 19.156 (ohne merged) - Customer::count() = 22.283 (alle, inkl. merged — Legacy-Model) - Lead::count() = 19.543 - Booking::count() = 10.648 - Booking->inquiry_id funktioniert - Booking->lead() Relation mit expl. FK inquiry_id funktioniert -``` - -### Smoke-Test-Findings 2026-04-17 (Test-Durchlauf nach Phase-2-Restore) - -Während der UI-Smoke-Tests auf `mein.sterntours.test` traten drei unterschiedliche Bug-Klassen auf, alle auf Test gefixt und in die Phase-2-Deploy-Liste aufgenommen. - -**1. Eloquent-Relation ohne expliziten FK — `Lead::bookings()`** -- Fehler: `/leads` → `Column not found: 'booking.lead_id'`. -- Ursache: `$this->hasMany(Booking::class)` ohne 2. Argument ⇒ Laravel leitet den FK aus dem Model-Namen ab (`Lead` → `lead_id`), der nach dem Rename nicht mehr existiert. -- Fix: `app/Models/Lead.php` Zeile 249–253: `$this->hasMany(Booking::class, 'inquiry_id')`. - -**2. Hartcodierte Tabellenqualifier in `orderColumn`-SQL-Strings + Blade-Column-`name:`** -- Fehler A: `/contacts` → `Column not found: 'customer.id' in 'order clause'`. Behoben durch SQL-Umstellung im Controller — blieb aber bestehen, weil Yajra bei fehlendem `orderColumn`-Match den `name:`-String aus der Blade-Column-Definition als raw SQL verwendet. -- Fehler B (gleiche Ursache, 2. Runde): `/contacts` → derselbe Fehler trotz Controller-Fix. Root Cause: In `resources/views/contact/index.blade.php` Zeile 188 stand `name: 'customer.id'`. Yajra matched `orderColumn()` auf diesen `name:`-String — mein erster Fix mit `orderColumn('id', …)` hat also nicht getroffen. -- Ursache zusammengefasst: Yajra-DataTables-`orderColumn()` bekommt als 2. Argument eine Raw-SQL-Expression; **und** der `name:`-Wert einer DataTable-Column muss exakt zum 1. Argument von `orderColumn()` / `filterColumn()` passen — sonst fällt Yajra zurück auf "Name wörtlich als SQL einsetzen". -- Fix in 5 Dateien: - - `app/Http/Controllers/ContactController.php` — `orderColumn('contacts.id' / 'deleted_at', …)`, `filterColumn('contacts.id', …)` - - `resources/views/contact/index.blade.php` — Column-Name `customer.id` → `contacts.id` - - `app/Http/Controllers/Admin/ReportController.php` — 2 Blöcke (`customer.firstname $1` → `contacts.firstname $1` etc.) - - `app/Http/Controllers/Admin/ReportBookingController.php` — 2 Blöcke - - `app/Http/Controllers/Admin/ReportLeadsController.php` — `$orderByNum`-Array -- Bewusst nicht angefasst: die 1. Argumente von `addColumn` / `rawColumns` (`customer.fullName`, `lead.status_id`) — das sind reine DataTables-Frontend-Identifier, die Yajra über den Eloquent-Relation-Mechanismus auflöst (Model `Customer`/`Lead` → `$table = 'contacts'`/`'inquiries'`), und die 3-teiligen `booking.customer.firstname`-Strings in `ReportController.php` und `ReportProviderController.php` (Yajra-Relation-Notation bzw. seit jeher kein gültiges SQL, durch Phase 2 nicht schlimmer). - -**3. Strict-Type-Regression aus Phase 1 (Laravel-10/PHP-8) — zwei Varianten** -- Fehler A: `App\Services\Booking::getCustomerMailName(): Argument #2 ($mailDirId) must be of type int, null given` in `modal-show-mail-inner.blade.php` Zeile 92. -- Fehler B: `App\Services\Booking::setOutputDirs(): Argument #2 ($subdir) must be of type string, null given` in derselben kompilierten View, Zeile 101. -- Ursache: Blade-Views übergeben `$mail_dir_id` bzw. `$mail_sdir_id` direkt aus DB-Feldern, die nullable sein können (Entwurfs-Mails, Top-Level-Ordner, Mails ohne Subdir). Unter Laravel 8 / PHP 7 wurde `null` stillschweigend zu `0` / `""` gecastet; PHP 8 strict types verweigern das. -- Fix in drei Dateien: - - `app/Services/Booking.php` — `getCustomerMailName()`, `getCustomerMailEmails()`: `int $mailDirId` → `?int`; `setOutputDirs()`: `string $subdir` → `?string`. - - `app/Services/Lead.php` — identische Änderungen (symmetrische API). - - `app/Services/MailDirService.php` — `getCustomerMailName()`, `getCustomerMailEmails()`, `resolveModel()` auf `?int`; `setOutputDir()` auf `?string`; in `resolveModel()` zusätzlich ein früher `return null`-Guard. -- Das ist **fachlich ein Phase-1-Bug**, der nichts mit Customer→Contacts zu tun hat. Wir deployen ihn trotzdem zusammen mit Phase 2, weil die Services keine DB-Abhängigkeit haben und ein separates Wartungsfenster unnötig wäre. +| Phase 1 — Contact-Deduplizierung | ✅ Abgeschlossen | ✅ Ja | ⬜ Nein | +| Phase 1 — Contacts-Modul (neuer Code) | ✅ Abgeschlossen | ✅ Ja | ⬜ Nein | +| Phase 2 — App-Code (Models/Repos/Controller/Views auf `contacts`/`inquiries`/`inquiry_id`) | ✅ Abgeschlossen | ⬜ Nein | ⬜ Nein | +| Phase 2 — Tabellen-Migrationen (3 Migrationsdateien) | ⬜ Ausstehend (Code ist deploy-ready) | ⬜ Nein | ⬜ Nein | +| Phase 3 — Participants konsolidieren | ⬜ Ausstehend | ⬜ Nein | ⬜ Nein | +| Phase 4 — Communications / Notices / Attachments | ⬜ Ausstehend | ⬜ Nein | ⬜ Nein | ### Was in Phase 1 umgesetzt wurde @@ -238,46 +178,33 @@ php artisan migrate:rollback --path=database/migrations/2025_04_15_100001_phase1 - Routen-Namen (`lead_detail`, `lead_index`) — bleiben als Aliase, um Links in Views/Mails/Logs nicht zu brechen. ### Schritt 1: Migrationen einspielen - -**Auf Test (2026-04-17 erfolgreich durchgeführt, Batch 27–29):** ```bash -php artisan migrate --path=database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php --force -php artisan migrate --path=database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php --force -php artisan migrate --path=database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php --force +# Backup erstellen! +php artisan migrate --path=database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php +php artisan migrate --path=database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php +php artisan migrate --path=database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php ``` -> **Wichtig — `--path` verwenden!** Ohne `--path` würde `php artisan migrate` auch die noch im Workspace liegenden Phase-3-, Phase-4- und Offers-Migrationen ausführen, die noch nicht deployreif sind. Einzeln per `--path` hält den Deploy auf exakt diese drei Dateien beschränkt. - -**Auf Live:** Ablauf siehe [phase-2-live-deploy.md](phase-2-live-deploy.md). Der Live-Deploy muss innerhalb eines Wartungsfensters passieren, weil DB und Code atomar zusammengehen müssen. - ### Ergebnis-Prüfung ```sql +-- Tabellen vorhanden? SHOW TABLES LIKE 'contacts'; SHOW TABLES LIKE 'inquiries'; +-- Spalte umbenannt? SHOW COLUMNS FROM booking LIKE 'inquiry_id'; +-- FK vorhanden? SELECT * FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_NAME = 'booking' AND COLUMN_NAME = 'inquiry_id'; ``` -Oder via Laravel-Tinker: -```bash -php artisan tinker --execute=" -echo Schema::hasTable('contacts') ? 'contacts OK' : 'contacts FEHLT'; echo PHP_EOL; -echo Schema::hasTable('inquiries') ? 'inquiries OK' : 'inquiries FEHLT'; echo PHP_EOL; -echo Schema::hasColumn('booking', 'inquiry_id') ? 'booking.inquiry_id OK' : 'FEHLT'; echo PHP_EOL; -" -``` - ### Rollback Phase 2 ```bash # In umgekehrter Reihenfolge -php artisan migrate:rollback --path=database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php --force -php artisan migrate:rollback --path=database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php --force -php artisan migrate:rollback --path=database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php --force +php artisan migrate:rollback --path=database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php +php artisan migrate:rollback --path=database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php +php artisan migrate:rollback --path=database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php ``` -> **Wichtig:** Rollback MUSS zusammen mit Code-Revert passieren (Code erwartet sonst die falschen Tabellennamen). Details im [phase-2-live-deploy.md](phase-2-live-deploy.md#rollback-plan-falls-phase-2-auf-live-scheitert). - --- ## Phase 3 — Participants konsolidieren diff --git a/resources/views/booking/_detail_price.blade.php b/resources/views/booking/_detail_price.blade.php index cd48819..869e6e2 100755 --- a/resources/views/booking/_detail_price.blade.php +++ b/resources/views/booking/_detail_price.blade.php @@ -121,25 +121,6 @@ - @php - $priceTotalStored = $booking->getPriceTotalRaw(); - $priceTotalComputed = round((float) $booking->getPriceRaw() + (float) $booking->getServiceTotal(true), 2); - @endphp -
-

- price_total (gespeichert in DB): - {{ \App\Services\Util::_number_format($priceTotalStored ?? 0) }} € - · - rechnerisch (Organisation + Vermittlung): - {{ \App\Services\Util::_number_format($priceTotalComputed) }} € - @if($booking->isCanceled() && $booking->getPriceCanceledRaw() !== null) - · - laut Storno-Logik (= Storno-Betrag price_canceled): - {{ \App\Services\Util::_number_format((float) $booking->getPriceCanceledRaw()) }} € - @endif -

-
-

diff --git a/resources/views/contact/index.blade.php b/resources/views/contact/index.blade.php index e02edb4..b327d3d 100644 --- a/resources/views/contact/index.blade.php +++ b/resources/views/contact/index.blade.php @@ -185,7 +185,7 @@ }, { data: 'id', - name: 'contacts.id' + name: 'customer.id' }, { data: 'firstname', diff --git a/resources/views/emails/exception.blade.php b/resources/views/emails/exception.blade.php deleted file mode 100644 index 4eef74b..0000000 --- a/resources/views/emails/exception.blade.php +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - Exception — {{ config('app.name') }} - - - - - {!! $content !!} - - - diff --git a/vendor.tar b/vendor.tar deleted file mode 100644 index eba74bf..0000000 Binary files a/vendor.tar and /dev/null differ