mein-sterntours/app/Models/NewsletterContact.php
Phase-1-Rollback-Agent e3dc1afd8e WIP: Sicherheitsnetz vor Phase-1-R\u00fcckbau
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
2026-04-17 13:40:31 +00:00

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);
});
}
}