File: /home/xedaptot/be.naniguide.com/app/Model/AppNotification.php
<?php
namespace App\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* Eloquent model for the redesigned `notifications` table.
*
* Two row shapes coexist by `audience`:
* - direct: notifiable_type/id set to a specific Admin or Customer; per-row
* read_at / dismissed_at apply to that one recipient
* - admins / customers / everyone: notifiable_type/id NULL; the row is
* shared across the entire audience (any admin can mark read / dismiss
* and the change is visible to everyone)
*
* Dispatch lives in App\Services\Notifications\Notifier (the only entry
* point for creating rows). This model only exposes query helpers + the
* action lifecycle (markAsRead / markAsUnread / dismiss).
*/
class AppNotification extends Model
{
protected $table = 'notifications';
public $incrementing = false;
protected $keyType = 'string';
protected $fillable = [
'id', 'type',
'audience',
'notifiable_type', 'notifiable_id',
'subject_type', 'subject_id',
'category', 'severity',
'group_key', 'data',
'read_at', 'read_by_type', 'read_by_id',
'dismissed_at', 'dismissed_by_type', 'dismissed_by_id',
];
protected $casts = [
'data' => 'array',
'read_at' => 'datetime',
'dismissed_at' => 'datetime',
];
// Relationships
public function notifiable(): MorphTo
{
return $this->morphTo();
}
public function subject(): MorphTo
{
return $this->morphTo();
}
public function readBy(): MorphTo
{
return $this->morphTo('read_by');
}
public function dismissedBy(): MorphTo
{
return $this->morphTo('dismissed_by');
}
// Scopes
public function scopeUnread(Builder $q): Builder
{
return $q->whereNull('read_at');
}
public function scopeActive(Builder $q): Builder
{
return $q->whereNull('dismissed_at');
}
public function scopeForAudience(Builder $q, string $audience): Builder
{
return $q->where('audience', $audience);
}
public function scopeDirectFor(Builder $q, Model $recipient): Builder
{
return $q->where('audience', 'direct')
->where('notifiable_type', $recipient->getMorphClass())
->where('notifiable_id', $recipient->getKey());
}
// Actions
/**
* Mark this notification read by the given actor (any admin can act on a
* shared 'admins' row; only the recipient can act on a 'direct' row —
* authorization enforced by NotificationPolicy at the controller layer).
*/
public function markAsRead(?Model $by = null): void
{
if ($this->read_at !== null) {
return;
}
$this->forceFill([
'read_at' => now(),
'read_by_type' => $by?->getMorphClass(),
'read_by_id' => $by?->getKey(),
])->save();
}
public function markAsUnread(): void
{
$this->forceFill([
'read_at' => null,
'read_by_type' => null,
'read_by_id' => null,
])->save();
}
/**
* Dismiss preserves history (no DELETE) but clears group_key so that the
* unique (notifiable_type, notifiable_id, group_key) index frees up — a
* future event with the same key can land cleanly.
*/
public function dismiss(?Model $by = null): void
{
if ($this->dismissed_at !== null) {
return;
}
$this->forceFill([
'dismissed_at' => now(),
'dismissed_by_type' => $by?->getMorphClass(),
'dismissed_by_id' => $by?->getKey(),
'group_key' => null,
])->save();
}
// Convenience accessors for views
public function title(): ?string
{
return $this->data['title'] ?? null;
}
public function body(): ?string
{
return $this->data['body'] ?? null;
}
public function actionLabel(): ?string
{
return $this->data['action']['label'] ?? null;
}
/**
* Resolve the CTA URL from the route name + params stored in data.action.
* Returns null when the event has no action, OR when the action's route
* no longer exists — the latter happens legitimately when a plugin that
* registered the route is uninstalled while its historical notifications
* still live in the DB. Title/body still render; only the CTA button is
* dropped (handled by the @if($actionUrl) check in _item.blade.php).
*/
public function actionUrl(): ?string
{
$action = $this->data['action'] ?? null;
if (! $action || empty($action['route'])) {
return null;
}
if (! \Route::has($action['route'])) {
\Log::warning('AppNotification CTA route missing — plugin uninstalled?', [
'notification_id' => $this->id,
'type' => $this->type,
'route' => $action['route'],
]);
return null;
}
return route($action['route'], $action['params'] ?? []);
}
public function isRead(): bool
{
return $this->read_at !== null;
}
public function isUnread(): bool
{
return $this->read_at === null;
}
public function isShared(): bool
{
return $this->audience !== 'direct';
}
}