HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
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';
    }
}