*/ use HasFactory; use MassPrunable; /** Tage, die Spam zur Nachprüfung in der Quarantäne bleibt. */ public const SPAM_RETENTION_DAYS = 30; protected $fillable = [ 'portal', 'topic', 'status', 'name', 'email', 'message', 'page_url', 'payload', 'requester_ip', 'user_id', 'resolved_by_user_id', 'admin_note', 'resolved_at', ]; protected function casts(): array { return [ 'topic' => ContactMessageTopic::class, 'status' => ContactMessageStatus::class, 'payload' => 'array', 'resolved_at' => 'datetime', ]; } /** * Spam-Quarantäne nach Ablauf der Prüffrist löschen (model:prune). * * @return Builder */ public function prunable(): Builder { return self::query() ->where('status', ContactMessageStatus::Spam->value) ->where('created_at', '<', now()->subDays(self::SPAM_RETENTION_DAYS)); } public function isSpam(): bool { return $this->status === ContactMessageStatus::Spam; } /** * In die Spam-Quarantäne legen (Admin, Sammelaktion oder Altbestand). * Ein vorhandener Filtergrund bleibt erhalten. */ public function markAsSpam(?User $by, string $reason = 'manual'): void { $this->update([ 'status' => ContactMessageStatus::Spam->value, 'payload' => [...($this->payload ?? []), 'spam_reason' => $this->payload['spam_reason'] ?? $reason], 'resolved_by_user_id' => $by?->id, 'resolved_at' => now(), ]); } /** * Fehlalarm: zurück in die offene Queue. Gibt zurück, ob die Team-Mail * noch aussteht: Hat der Filter die Anfrage beim Eingang abgefangen, ging * keine raus — beim ersten Zurückholen wird sie nachgeholt. Von Hand * markierter Spam hatte seine Mail schon. */ public function releaseFromSpam(): bool { $payload = $this->payload ?? []; $mailPending = isset($payload['spam_reason']) && $payload['spam_reason'] !== 'manual' && ! isset($payload['released_from_spam_at']); $this->update([ 'status' => ContactMessageStatus::Open->value, 'payload' => [...$payload, 'released_from_spam_at' => now()->toIso8601String()], 'resolved_by_user_id' => null, 'resolved_at' => null, ]); return $mailPending; } /** * Grund der Quarantäne für die Admin-Ansicht (ContactSpamFilter). */ public function spamReasonLabel(): ?string { $reason = $this->payload['spam_reason'] ?? null; return match ($reason) { null => null, 'link' => 'Link im Text', 'page_title' => 'Floskel mit Seitentitel', 'gibberish' => 'Zeichensalat', 'blacklist' => 'Begriff der Sperrliste', 'too_fast' => 'Zu schnell abgesendet', 'manual' => 'Von Hand markiert', default => (string) $reason, }; } /** * Angenommen, obwohl Cloudflare Turnstile nicht erreichbar war. */ public function skippedBotCheck(): bool { return ($this->payload['bot_check'] ?? null) === 'unavailable'; } /** * Öffentliche Vorgangsnummer — analog zu Legal-Requests ("M-{id}"). */ public function reference(): string { return 'KT-'.$this->id; } /** * @return BelongsTo */ public function user(): BelongsTo { return $this->belongsTo(User::class); } /** * @return BelongsTo */ public function resolver(): BelongsTo { return $this->belongsTo(User::class, 'resolved_by_user_id'); } /** * @param Builder $query * @return Builder */ public function scopeOpen(Builder $query): Builder { return $query->whereIn('status', [ ContactMessageStatus::Open->value, ContactMessageStatus::InProgress->value, ]); } }