complete story 8.10 with qa test

This commit is contained in:
Naser Mansour
2026-01-02 23:14:53 +02:00
parent 78b3a01c4d
commit 97dca05562
13 changed files with 908 additions and 29 deletions
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace App\Contracts;
/**
* Marker interface for exceptions that should trigger admin notification.
* Implement this interface on custom exceptions that require admin attention.
*/
interface ShouldNotifyAdmin {}
@@ -0,0 +1,46 @@
<?php
namespace App\Notifications;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Queue\Events\JobFailed;
class QueueFailureNotification extends Notification
{
public function __construct(
public JobFailed $event
) {}
public function via(object $notifiable): array
{
return ['mail'];
}
public function toMail(object $notifiable): MailMessage
{
$jobName = $this->event->job->resolveName();
$environment = app()->environment();
return (new MailMessage)
->subject("[URGENT] [{$environment}] Queue Job Failed: {$jobName} - Libra Law Firm")
->greeting('Queue Failure Alert')
->line('A queued job has failed.')
->line("**Job:** {$jobName}")
->line('**Queue:** '.$this->event->job->getQueue())
->line('**Error:** '.$this->event->exception->getMessage())
->line('**Time:** '.now()->format('Y-m-d H:i:s'))
->line('**Attempts:** '.$this->event->job->attempts())
->line('Please check the failed_jobs table and application logs for details.')
->salutation('— Libra System Monitor');
}
public function toArray(object $notifiable): array
{
return [
'type' => 'queue_failure',
'job' => $this->event->job->resolveName(),
'queue' => $this->event->job->getQueue(),
];
}
}
@@ -0,0 +1,46 @@
<?php
namespace App\Notifications;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Throwable;
class SystemErrorNotification extends Notification
{
public function __construct(
public Throwable $exception
) {}
public function via(object $notifiable): array
{
return ['mail'];
}
public function toMail(object $notifiable): MailMessage
{
$environment = app()->environment();
$errorType = class_basename($this->exception);
return (new MailMessage)
->subject("[URGENT] [{$environment}] System Error: {$errorType} - Libra Law Firm")
->greeting('System Alert')
->line('A critical error occurred on the platform.')
->line("**Error Type:** {$errorType}")
->line('**Message:** '.$this->exception->getMessage())
->line('**File:** '.$this->exception->getFile().':'.$this->exception->getLine())
->line('**Time:** '.now()->format('Y-m-d H:i:s'))
->line("**Environment:** {$environment}")
->line('Please check the application logs for full stack trace and details.')
->salutation('— Libra System Monitor');
}
public function toArray(object $notifiable): array
{
return [
'type' => 'system_error',
'error_type' => class_basename($this->exception),
'message' => $this->exception->getMessage(),
];
}
}
+19
View File
@@ -5,10 +5,14 @@ namespace App\Providers;
use App\Listeners\LogFailedLoginAttempt;
use App\Models\Consultation;
use App\Models\TimelineUpdate;
use App\Notifications\QueueFailureNotification;
use App\Observers\ConsultationObserver;
use App\Observers\TimelineUpdateObserver;
use Illuminate\Auth\Events\Failed;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@@ -30,5 +34,20 @@ class AppServiceProvider extends ServiceProvider
Consultation::observe(ConsultationObserver::class);
TimelineUpdate::observe(TimelineUpdateObserver::class);
Queue::failing(function (JobFailed $event) {
if (! config('libra.notifications.system_notifications_enabled')) {
return;
}
$adminEmail = config('libra.notifications.admin_email');
if (empty($adminEmail)) {
return;
}
Notification::route('mail', $adminEmail)
->notifyNow(new QueueFailureNotification($event));
});
}
}
+81
View File
@@ -0,0 +1,81 @@
<?php
namespace App\Services;
use App\Contracts\ShouldNotifyAdmin;
use App\Notifications\SystemErrorNotification;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Notification;
use Throwable;
class AdminNotificationService
{
public function notifyError(Throwable $exception): void
{
if (! config('libra.notifications.system_notifications_enabled')) {
return;
}
$adminEmail = config('libra.notifications.admin_email');
if (empty($adminEmail)) {
return;
}
$errorKey = $this->getErrorKey($exception);
$throttleMinutes = config('libra.notifications.throttle_minutes', 15);
if (Cache::has($errorKey)) {
return;
}
Cache::put($errorKey, true, now()->addMinutes($throttleMinutes));
Notification::route('mail', $adminEmail)
->notifyNow(new SystemErrorNotification($exception));
}
protected function getErrorKey(Throwable $exception): string
{
return 'admin_notified:'.md5(get_class($exception).$exception->getMessage());
}
public function shouldNotifyAdmin(Throwable $exception): bool
{
if ($exception instanceof ShouldNotifyAdmin) {
return true;
}
$excludedTypes = [
\Illuminate\Validation\ValidationException::class,
\Illuminate\Auth\AuthenticationException::class,
\Illuminate\Auth\Access\AuthorizationException::class,
\Illuminate\Database\Eloquent\ModelNotFoundException::class,
\Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class,
\Illuminate\Session\TokenMismatchException::class,
];
foreach ($excludedTypes as $type) {
if ($exception instanceof $type) {
return false;
}
}
if ($exception instanceof \Illuminate\Database\QueryException) {
return $this->isCriticalDatabaseError($exception);
}
if ($exception instanceof \Symfony\Component\Mailer\Exception\TransportExceptionInterface) {
return true;
}
return false;
}
protected function isCriticalDatabaseError(\Illuminate\Database\QueryException $e): bool
{
$criticalCodes = ['2002', '1045', '1049'];
return in_array($e->getCode(), $criticalCodes);
}
}