Enth\u00e4lt gemischt: Laravel-10-Upgrade + Phase 1 (Contacts-Modul, Duplicats-Commands, Soft-Delete+Merge-Fields) + Phase 2 Code-Umstellungen (inquiry_id, $table='contacts'/'inquiries') + Offers-Modul (Migrationen, Models, offer_id in Booking, offer-Disk in filesystems.php). Phase 2 + Offers werden im folgenden Commit nach dev/backups/phase2-offers-2026-04-17/ verschoben, damit der Workspace auf Phase-1-only (= Test-System-Stand) reduziert ist und direkt auf Live deploybar wird. Tarball-Backup zus\u00e4tzlich unter: ../backups-safety/workspace-pre-phase1-rollback-2026-04-17.tar.gz Made-with: Cursor
369 lines
12 KiB
PHP
369 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Carbon\Carbon;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
|
|
/**
|
|
* Class NewsletterContact
|
|
*
|
|
* @property int $id
|
|
* @property string $email
|
|
* @property string|null $firstname
|
|
* @property string|null $lastname
|
|
* @property bool $group_kulturreisen
|
|
* @property bool $group_ferienwohnungen
|
|
* @property string $source
|
|
* @property string $status
|
|
* @property Carbon|null $subscribed_at
|
|
* @property Carbon|null $unsubscribed_at
|
|
* @property Carbon|null $last_booking_at
|
|
* @property Carbon|null $last_travel_end_date
|
|
* @property int $total_bookings_kulturreisen
|
|
* @property int $total_bookings_ferienwohnungen
|
|
* @property int|null $customer_id
|
|
* @property int|null $travel_user_id
|
|
* @property Carbon|null $last_synced_at
|
|
* @property string|null $sync_hash
|
|
* @property string|null $notes
|
|
* @property Carbon $created_at
|
|
* @property Carbon $updated_at
|
|
* @property Carbon|null $deleted_at
|
|
* @property-read Customer|null $customer
|
|
* @property-read TravelUser|null $travel_user
|
|
* @property-read Collection|NewsletterLog[] $logs
|
|
* @package App\Models
|
|
* @property-read mixed $full_name
|
|
* @property-read mixed $groups
|
|
* @property-read mixed $groups_string
|
|
* @property-read mixed $source_label
|
|
* @property-read mixed $status_badge
|
|
* @property-read mixed $status_color
|
|
* @property-read mixed $status_label
|
|
* @property-read mixed $total_bookings
|
|
* @property-read int|null $logs_count
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact active()
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact ferienwohnungen()
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact kulturreisen()
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact multipleBookers()
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact newModelQuery()
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact newQuery()
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact onlyTrashed()
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact query()
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereCreatedAt($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereCustomerId($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereDeletedAt($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereEmail($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereFirstname($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereGroupFerienwohnungen($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereGroupKulturreisen($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereId($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereLastBookingAt($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereLastSyncedAt($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereLastTravelEndDate($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereLastname($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereNotes($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereSource($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereStatus($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereSubscribedAt($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereSyncHash($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereTotalBookingsFerienwohnungen($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereTotalBookingsKulturreisen($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereTravelUserId($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereUnsubscribedAt($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact whereUpdatedAt($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact withBookings()
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact withTrashed()
|
|
* @method static \Illuminate\Database\Eloquent\Builder|NewsletterContact withoutTrashed()
|
|
* @mixin \Eloquent
|
|
*/
|
|
class NewsletterContact extends Model
|
|
{
|
|
use SoftDeletes;
|
|
|
|
protected $connection = 'mysql';
|
|
|
|
protected $table = 'newsletter_contacts';
|
|
|
|
protected $casts = [
|
|
'group_kulturreisen' => 'boolean',
|
|
'group_ferienwohnungen' => 'boolean',
|
|
'subscribed_at' => 'datetime',
|
|
'unsubscribed_at' => 'datetime',
|
|
'last_booking_at' => 'datetime',
|
|
'last_travel_end_date' => 'datetime',
|
|
'last_synced_at' => 'datetime',
|
|
'total_bookings_kulturreisen' => 'int',
|
|
'total_bookings_ferienwohnungen' => 'int',
|
|
'customer_id' => 'int',
|
|
'travel_user_id' => 'int',
|
|
];
|
|
|
|
protected $fillable = [
|
|
'email',
|
|
'firstname',
|
|
'lastname',
|
|
'group_kulturreisen',
|
|
'group_ferienwohnungen',
|
|
'source',
|
|
'status',
|
|
'subscribed_at',
|
|
'unsubscribed_at',
|
|
'last_booking_at',
|
|
'last_travel_end_date',
|
|
'total_bookings_kulturreisen',
|
|
'total_bookings_ferienwohnungen',
|
|
'customer_id',
|
|
'travel_user_id',
|
|
'last_synced_at',
|
|
'sync_hash',
|
|
'notes',
|
|
];
|
|
|
|
// Konstanten für Source
|
|
const SOURCE_BOOKING_KULTURREISEN = 'booking_kulturreisen';
|
|
const SOURCE_BOOKING_FERIENWOHNUNGEN = 'booking_ferienwohnungen';
|
|
const SOURCE_NEWSLETTER_SIGNUP = 'newsletter_signup';
|
|
const SOURCE_MANUAL = 'manual';
|
|
const SOURCE_IMPORT = 'import';
|
|
|
|
// Konstanten für Status
|
|
const STATUS_ACTIVE = 'active';
|
|
const STATUS_INACTIVE = 'inactive';
|
|
const STATUS_UNSUBSCRIBED = 'unsubscribed';
|
|
const STATUS_BOUNCED = 'bounced';
|
|
|
|
public static $sourceLabels = [
|
|
self::SOURCE_BOOKING_KULTURREISEN => 'Buchung Kulturreisen',
|
|
self::SOURCE_BOOKING_FERIENWOHNUNGEN => 'Buchung Ferienwohnungen',
|
|
self::SOURCE_NEWSLETTER_SIGNUP => 'Newsletter-Anmeldung',
|
|
self::SOURCE_MANUAL => 'Manuell',
|
|
self::SOURCE_IMPORT => 'Import',
|
|
];
|
|
|
|
public static $statusLabels = [
|
|
self::STATUS_ACTIVE => 'Aktiv',
|
|
self::STATUS_INACTIVE => 'Inaktiv',
|
|
self::STATUS_UNSUBSCRIBED => 'Abgemeldet',
|
|
self::STATUS_BOUNCED => 'Bounced',
|
|
];
|
|
|
|
public static $statusColors = [
|
|
self::STATUS_ACTIVE => 'success',
|
|
self::STATUS_INACTIVE => 'secondary',
|
|
self::STATUS_UNSUBSCRIBED => 'warning',
|
|
self::STATUS_BOUNCED => 'danger',
|
|
];
|
|
|
|
/**
|
|
* Beziehung zum Customer (Kulturreisen)
|
|
*/
|
|
public function customer()
|
|
{
|
|
return $this->belongsTo(Customer::class, 'customer_id');
|
|
}
|
|
|
|
/**
|
|
* Beziehung zum TravelUser (Ferienwohnungen)
|
|
*/
|
|
public function travel_user()
|
|
{
|
|
return $this->belongsTo(TravelUser::class, 'travel_user_id');
|
|
}
|
|
|
|
/**
|
|
* Logs zu diesem Kontakt
|
|
*/
|
|
public function logs()
|
|
{
|
|
return $this->hasMany(NewsletterLog::class, 'newsletter_contact_id')->orderBy('created_at', 'DESC');
|
|
}
|
|
|
|
/**
|
|
* Vollständiger Name
|
|
*/
|
|
public function getFullNameAttribute()
|
|
{
|
|
return trim($this->firstname . ' ' . $this->lastname);
|
|
}
|
|
|
|
/**
|
|
* Gruppenzugehörigkeit als Array
|
|
*/
|
|
public function getGroupsAttribute()
|
|
{
|
|
$groups = [];
|
|
if ($this->group_kulturreisen) {
|
|
$groups[] = 'Kulturreisen';
|
|
}
|
|
if ($this->group_ferienwohnungen) {
|
|
$groups[] = 'Ferienwohnungen';
|
|
}
|
|
return $groups;
|
|
}
|
|
|
|
/**
|
|
* Gruppenzugehörigkeit als String
|
|
*/
|
|
public function getGroupsStringAttribute()
|
|
{
|
|
return implode(', ', $this->groups);
|
|
}
|
|
|
|
/**
|
|
* Status-Label
|
|
*/
|
|
public function getStatusLabelAttribute()
|
|
{
|
|
return self::$statusLabels[$this->status] ?? $this->status;
|
|
}
|
|
|
|
/**
|
|
* Status-Color
|
|
*/
|
|
public function getStatusColorAttribute()
|
|
{
|
|
return self::$statusColors[$this->status] ?? 'secondary';
|
|
}
|
|
|
|
/**
|
|
* Source-Label
|
|
*/
|
|
public function getSourceLabelAttribute()
|
|
{
|
|
return self::$sourceLabels[$this->source] ?? $this->source;
|
|
}
|
|
|
|
/**
|
|
* Status-Badge HTML
|
|
*/
|
|
public function getStatusBadgeAttribute()
|
|
{
|
|
return '<span class="badge badge-' . $this->status_color . '">' . $this->status_label . '</span>';
|
|
}
|
|
|
|
/**
|
|
* Gesamtzahl Buchungen
|
|
*/
|
|
public function getTotalBookingsAttribute()
|
|
{
|
|
return $this->total_bookings_kulturreisen + $this->total_bookings_ferienwohnungen;
|
|
}
|
|
|
|
/**
|
|
* Ist Kontakt aktiv?
|
|
*/
|
|
public function isActive()
|
|
{
|
|
return $this->status === self::STATUS_ACTIVE;
|
|
}
|
|
|
|
/**
|
|
* Ist Kontakt abgemeldet?
|
|
*/
|
|
public function isUnsubscribed()
|
|
{
|
|
return $this->status === self::STATUS_UNSUBSCRIBED;
|
|
}
|
|
|
|
/**
|
|
* Hat Kontakt mindestens eine Buchung?
|
|
*/
|
|
public function hasBookings()
|
|
{
|
|
return $this->total_bookings > 0;
|
|
}
|
|
|
|
/**
|
|
* Kontakt abmelden
|
|
*/
|
|
public function unsubscribe($reason = null)
|
|
{
|
|
$this->status = self::STATUS_UNSUBSCRIBED;
|
|
$this->unsubscribed_at = now();
|
|
$this->save();
|
|
|
|
// Log erstellen
|
|
$this->logs()->create([
|
|
'action' => 'unsubscribed',
|
|
'description' => $reason ?? 'Kontakt abgemeldet',
|
|
]);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Kontakt wieder aktivieren
|
|
*/
|
|
public function resubscribe()
|
|
{
|
|
$this->status = self::STATUS_ACTIVE;
|
|
$this->unsubscribed_at = null;
|
|
$this->save();
|
|
|
|
// Log erstellen
|
|
$this->logs()->create([
|
|
'action' => 'subscribed',
|
|
'description' => 'Kontakt wieder aktiviert',
|
|
]);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Hash für Duplikat-Erkennung generieren
|
|
*/
|
|
public static function generateSyncHash($email, $source)
|
|
{
|
|
return md5(strtolower(trim($email)) . '_' . $source);
|
|
}
|
|
|
|
/**
|
|
* Scope: Nur aktive Kontakte
|
|
*/
|
|
public function scopeActive($query)
|
|
{
|
|
return $query->where('status', self::STATUS_ACTIVE);
|
|
}
|
|
|
|
/**
|
|
* Scope: Nur Kulturreisen
|
|
*/
|
|
public function scopeKulturreisen($query)
|
|
{
|
|
return $query->where('group_kulturreisen', true);
|
|
}
|
|
|
|
/**
|
|
* Scope: Nur Ferienwohnungen
|
|
*/
|
|
public function scopeFerienwohnungen($query)
|
|
{
|
|
return $query->where('group_ferienwohnungen', true);
|
|
}
|
|
|
|
/**
|
|
* Scope: Mit Buchungen
|
|
*/
|
|
public function scopeWithBookings($query)
|
|
{
|
|
return $query->where(function ($q) {
|
|
$q->where('total_bookings_kulturreisen', '>', 0)
|
|
->orWhere('total_bookings_ferienwohnungen', '>', 0);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Scope: Mehrfachbucher
|
|
*/
|
|
public function scopeMultipleBookers($query)
|
|
{
|
|
return $query->where(function ($q) {
|
|
$q->where('total_bookings_kulturreisen', '>', 1)
|
|
->orWhere('total_bookings_ferienwohnungen', '>', 1);
|
|
});
|
|
}
|
|
}
|