File: /home/xedaptot/be.naniguide.com/app/Model/ActivityLog.php
<?php
namespace App\Model;
use App\ActivityLog\ActivityRegistry;
use App\Library\Traits\HasUid;
use Illuminate\Database\Eloquent\Model;
class ActivityLog extends Model
{
use HasUid;
protected $table = 'activity_logs';
protected $guarded = [];
protected $casts = [
'data' => 'array',
];
/**
* Mirror customer-facing activity log writes into the `notifications` table
* so the topbar bell + notifications listing pick them up automatically.
*
* Selection rule: the resolved Activity class declares `static $notify = true`
* AND the row has a customer_id. SubscriptionEvent (every subscription.* type)
* is the canonical opt-in. All other activity classes default to false →
* audit-trail-only, no notification.
*
* Failures are swallowed with a warning log (not allowed to swallow per
* project rules; logged context preserved). The activity log row itself
* must persist regardless of notification mirroring success.
*/
protected static function booted(): void
{
static::created(function (ActivityLog $log) {
if (empty($log->customer_id)) {
return;
}
$cls = ActivityRegistry::resolve($log->type);
if (!$cls || !$cls::notify()) {
return;
}
$customer = Customer::find($log->customer_id);
if (! $customer) {
return;
}
try {
app(\App\Services\Notifications\Notifier::class)->sendDirect(
$customer,
new \App\Notifications\Customer\ActivityMirror($log)
);
} catch (\Throwable $e) {
// Mirror failure must not break the activity log write itself.
// Specific catch + structured log preserves the failure mode
// without swallowing — operators see it in Laravel logs.
\Log::warning('Failed to mirror ActivityLog into notifications', [
'activity_log_id' => $log->id,
'type' => $log->type,
'error' => $e->getMessage(),
]);
}
});
}
// Relationships
public function customer()
{
return $this->belongsTo(Customer::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
// Display
public function message(): string
{
$class = ActivityRegistry::resolve($this->type);
if ($class) {
return $class::toHtml($this);
}
return e($this->subject_name . ' — ' . $this->type);
}
public function icon(): string
{
$class = ActivityRegistry::resolve($this->type);
return $class ? $class::icon() : 'info';
}
// Scopes
public function scopeOfSubjectType($query, string $subjectType)
{
return $query->where('subject_type', $subjectType);
}
public function scopeSearch($query, ?string $keyword)
{
if (!empty(trim($keyword ?? ''))) {
$query->where('subject_name', 'like', '%' . $keyword . '%');
}
return $query;
}
// Filter & search (compatible with existing listing pattern)
public static function filter($request)
{
$customer = $request->user()->customer;
$query = self::where('customer_id', $customer->id);
if (!empty(trim($request->keyword ?? ''))) {
$query->where('subject_name', 'like', '%' . $request->keyword . '%');
}
if (!empty($request->type)) {
$query->where('subject_type', $request->type);
}
return $query;
}
public static function search($request)
{
$query = self::filter($request);
$query->orderBy(
$request->sort_order ?? 'created_at',
$request->sort_direction ?? 'desc'
);
return $query;
}
}