538 lines
18 KiB
Markdown
538 lines
18 KiB
Markdown
# Story 8.10: Admin Notification - System Events
|
|
|
|
## Epic Reference
|
|
**Epic 8:** Email Notification System
|
|
|
|
## User Story
|
|
As an **admin**,
|
|
I want **to be notified of critical system events**,
|
|
So that **I can address issues promptly**.
|
|
|
|
## Dependencies
|
|
- **Story 8.1:** Email Infrastructure Setup (base template, SMTP configuration, queue setup)
|
|
|
|
## Acceptance Criteria
|
|
|
|
### Events to Notify
|
|
- [x] Email delivery failures (SMTP errors, bounces)
|
|
- [x] Scheduled job failures (Laravel scheduler)
|
|
- [x] Queue job failures
|
|
- [x] Critical application errors (database, payment, etc.)
|
|
|
|
### Content Requirements
|
|
- [x] Event type and description
|
|
- [x] Timestamp (formatted per locale)
|
|
- [x] Relevant details (error message, stack trace summary, job name)
|
|
- [x] Recommended action (if applicable)
|
|
- [x] Environment indicator (production/staging)
|
|
|
|
### Delivery Requirements
|
|
- [x] Sent immediately using sync mail driver (not queued)
|
|
- [x] Clear subject line indicating urgency with event type
|
|
- [x] Rate limited: max 1 notification per error type per 15 minutes
|
|
- [x] Uses base email template from Story 8.1
|
|
|
|
### Critical Error Definition
|
|
|
|
**DO Notify for:**
|
|
- Database connection failures (`QueryException` with connection errors)
|
|
- Queue connection failures
|
|
- Mail delivery failures (`Swift_TransportException`, mail exceptions)
|
|
- Scheduled command failures
|
|
- Any exception marked with `ShouldNotifyAdmin` interface
|
|
|
|
**DO NOT Notify for:**
|
|
- Validation exceptions (`ValidationException`)
|
|
- Authentication failures (`AuthenticationException`)
|
|
- Authorization failures (`AuthorizationException`)
|
|
- Model not found (`ModelNotFoundException`)
|
|
- HTTP 404 errors (`NotFoundHttpException`)
|
|
- CSRF token mismatches (`TokenMismatchException`)
|
|
|
|
## Technical Notes
|
|
|
|
### Files to Create
|
|
- `app/Notifications/SystemErrorNotification.php`
|
|
- `app/Notifications/QueueFailureNotification.php`
|
|
- `app/Contracts/ShouldNotifyAdmin.php` (marker interface)
|
|
- `app/Services/AdminNotificationService.php` (rate limiting logic)
|
|
|
|
### Files to Modify
|
|
- `bootstrap/app.php` (exception handling)
|
|
- `app/Providers/AppServiceProvider.php` (queue failure listener)
|
|
|
|
### Environment Variables
|
|
```env
|
|
# Admin notification settings (add to .env.example)
|
|
ADMIN_NOTIFICATION_EMAIL=admin@libra.ps
|
|
SYSTEM_NOTIFICATIONS_ENABLED=true
|
|
ERROR_NOTIFICATION_THROTTLE_MINUTES=15
|
|
```
|
|
|
|
### Configuration
|
|
```php
|
|
// config/libra.php (add section)
|
|
'notifications' => [
|
|
'admin_email' => env('ADMIN_NOTIFICATION_EMAIL'),
|
|
'system_notifications_enabled' => env('SYSTEM_NOTIFICATIONS_ENABLED', true),
|
|
'throttle_minutes' => env('ERROR_NOTIFICATION_THROTTLE_MINUTES', 15),
|
|
],
|
|
```
|
|
|
|
### Admin Notification Service
|
|
```php
|
|
// app/Services/AdminNotificationService.php
|
|
namespace App\Services;
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Notification;
|
|
use App\Notifications\SystemErrorNotification;
|
|
|
|
class AdminNotificationService
|
|
{
|
|
public function notifyError(Throwable $exception): void
|
|
{
|
|
if (!config('libra.notifications.system_notifications_enabled')) {
|
|
return;
|
|
}
|
|
|
|
$errorKey = $this->getErrorKey($exception);
|
|
$throttleMinutes = config('libra.notifications.throttle_minutes', 15);
|
|
|
|
// Rate limiting: skip if we've notified about this error type recently
|
|
if (Cache::has($errorKey)) {
|
|
return;
|
|
}
|
|
|
|
Cache::put($errorKey, true, now()->addMinutes($throttleMinutes));
|
|
|
|
$adminEmail = config('libra.notifications.admin_email');
|
|
|
|
// Send immediately (sync) - not queued
|
|
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
|
|
{
|
|
// Check if exception implements marker interface
|
|
if ($exception instanceof \App\Contracts\ShouldNotifyAdmin) {
|
|
return true;
|
|
}
|
|
|
|
// Check against excluded exception types
|
|
$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;
|
|
}
|
|
}
|
|
|
|
// Check for critical database errors
|
|
if ($exception instanceof \Illuminate\Database\QueryException) {
|
|
return $this->isCriticalDatabaseError($exception);
|
|
}
|
|
|
|
// Notify for mail exceptions
|
|
if ($exception instanceof \Symfony\Component\Mailer\Exception\TransportExceptionInterface) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected function isCriticalDatabaseError(\Illuminate\Database\QueryException $e): bool
|
|
{
|
|
$criticalCodes = ['2002', '1045', '1049']; // Connection refused, access denied, unknown DB
|
|
return in_array($e->getCode(), $criticalCodes);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Exception Handler Integration
|
|
```php
|
|
// bootstrap/app.php
|
|
use App\Services\AdminNotificationService;
|
|
|
|
->withExceptions(function (Exceptions $exceptions) {
|
|
$exceptions->reportable(function (Throwable $e) {
|
|
$service = app(AdminNotificationService::class);
|
|
|
|
if ($service->shouldNotifyAdmin($e)) {
|
|
$service->notifyError($e);
|
|
}
|
|
});
|
|
})
|
|
```
|
|
|
|
### System Error Notification
|
|
```php
|
|
// app/Notifications/SystemErrorNotification.php
|
|
namespace App\Notifications;
|
|
|
|
use Illuminate\Notifications\Notification;
|
|
use Illuminate\Notifications\Messages\MailMessage;
|
|
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');
|
|
}
|
|
}
|
|
```
|
|
|
|
### Queue Failure Notification
|
|
```php
|
|
// app/Notifications/QueueFailureNotification.php
|
|
namespace App\Notifications;
|
|
|
|
use Illuminate\Notifications\Notification;
|
|
use Illuminate\Notifications\Messages\MailMessage;
|
|
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');
|
|
}
|
|
}
|
|
```
|
|
|
|
### Queue Failure Listener Registration
|
|
```php
|
|
// app/Providers/AppServiceProvider.php boot() method
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Illuminate\Queue\Events\JobFailed;
|
|
use Illuminate\Support\Facades\Notification;
|
|
use App\Notifications\QueueFailureNotification;
|
|
|
|
public function boot(): void
|
|
{
|
|
Queue::failing(function (JobFailed $event) {
|
|
if (!config('libra.notifications.system_notifications_enabled')) {
|
|
return;
|
|
}
|
|
|
|
$adminEmail = config('libra.notifications.admin_email');
|
|
|
|
Notification::route('mail', $adminEmail)
|
|
->notifyNow(new QueueFailureNotification($event));
|
|
});
|
|
}
|
|
```
|
|
|
|
### Marker Interface
|
|
```php
|
|
// app/Contracts/ShouldNotifyAdmin.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
|
|
{
|
|
}
|
|
```
|
|
|
|
## Testing Approach
|
|
|
|
### Unit Tests
|
|
```php
|
|
// tests/Unit/Services/AdminNotificationServiceTest.php
|
|
test('shouldNotifyAdmin returns false for validation exceptions', function () {
|
|
$service = new AdminNotificationService();
|
|
$exception = new \Illuminate\Validation\ValidationException(validator([], []));
|
|
|
|
expect($service->shouldNotifyAdmin($exception))->toBeFalse();
|
|
});
|
|
|
|
test('shouldNotifyAdmin returns true for exceptions implementing ShouldNotifyAdmin', function () {
|
|
$service = new AdminNotificationService();
|
|
$exception = new class extends Exception implements \App\Contracts\ShouldNotifyAdmin {};
|
|
|
|
expect($service->shouldNotifyAdmin($exception))->toBeTrue();
|
|
});
|
|
|
|
test('rate limiting prevents duplicate notifications', function () {
|
|
$service = new AdminNotificationService();
|
|
$exception = new RuntimeException('Test error');
|
|
|
|
Notification::fake();
|
|
|
|
$service->notifyError($exception);
|
|
$service->notifyError($exception); // Should be throttled
|
|
|
|
Notification::assertSentTimes(SystemErrorNotification::class, 1);
|
|
});
|
|
```
|
|
|
|
### Feature Tests
|
|
```php
|
|
// tests/Feature/Notifications/SystemErrorNotificationTest.php
|
|
test('critical database error triggers admin notification', function () {
|
|
Notification::fake();
|
|
|
|
// Simulate a critical database error being reported
|
|
$exception = new \Illuminate\Database\QueryException(
|
|
'mysql',
|
|
'SELECT 1',
|
|
[],
|
|
new \Exception('Connection refused', 2002)
|
|
);
|
|
|
|
$service = app(AdminNotificationService::class);
|
|
$service->notifyError($exception);
|
|
|
|
Notification::assertSentOnDemand(
|
|
SystemErrorNotification::class,
|
|
function ($notification, $channels, $notifiable) {
|
|
return $notifiable->routes['mail'] === config('libra.notifications.admin_email');
|
|
}
|
|
);
|
|
});
|
|
|
|
test('queue failure triggers admin notification', function () {
|
|
Notification::fake();
|
|
|
|
// Dispatch a job that will fail
|
|
$job = new class implements ShouldQueue {
|
|
use Dispatchable, InteractsWithQueue, Queueable;
|
|
public function handle() { throw new Exception('Test failure'); }
|
|
};
|
|
|
|
// Process the job (will fail and trigger notification)
|
|
// Note: This requires queue to be sync for testing
|
|
|
|
Notification::assertSentOnDemand(QueueFailureNotification::class);
|
|
});
|
|
|
|
test('validation exception does not trigger admin notification', function () {
|
|
Notification::fake();
|
|
|
|
try {
|
|
validator([], ['email' => 'required'])->validate();
|
|
} catch (\Illuminate\Validation\ValidationException $e) {
|
|
$service = app(AdminNotificationService::class);
|
|
|
|
if ($service->shouldNotifyAdmin($e)) {
|
|
$service->notifyError($e);
|
|
}
|
|
}
|
|
|
|
Notification::assertNothingSent();
|
|
});
|
|
|
|
test('system error notification contains required information', function () {
|
|
$exception = new RuntimeException('Test error message');
|
|
$notification = new SystemErrorNotification($exception);
|
|
|
|
$mail = $notification->toMail(new AnonymousNotifiable());
|
|
|
|
expect($mail->subject)->toContain('[URGENT]');
|
|
expect($mail->subject)->toContain('RuntimeException');
|
|
expect((string) $mail->render())->toContain('Test error message');
|
|
});
|
|
```
|
|
|
|
## Definition of Done
|
|
- [x] `AdminNotificationService` created with rate limiting
|
|
- [x] `SystemErrorNotification` created and styled
|
|
- [x] `QueueFailureNotification` created and styled
|
|
- [x] `ShouldNotifyAdmin` marker interface created
|
|
- [x] Exception handler integration in `bootstrap/app.php`
|
|
- [x] Queue failure listener in `AppServiceProvider`
|
|
- [x] Environment variables documented in `.env.example`
|
|
- [x] Configuration added to `config/libra.php`
|
|
- [x] Notifications sent immediately (sync, not queued)
|
|
- [x] Rate limiting prevents notification spam
|
|
- [x] Excluded exceptions do not trigger notifications
|
|
- [x] Unit tests for `shouldNotifyAdmin()` logic
|
|
- [x] Feature tests for notification triggers
|
|
- [x] All tests pass
|
|
- [x] Code formatted with Pint
|
|
|
|
## Estimation
|
|
**Complexity:** Medium | **Effort:** 4 hours
|
|
|
|
## Notes
|
|
- Admin email should be configured in production before deployment
|
|
- Consider monitoring the `failed_jobs` table separately for recurring failures
|
|
- Rate limiting uses cache - ensure cache driver is configured properly
|
|
- In local development, set `SYSTEM_NOTIFICATIONS_ENABLED=false` to avoid noise
|
|
|
|
---
|
|
|
|
## Dev Agent Record
|
|
|
|
### Status
|
|
**Ready for Review**
|
|
|
|
### Agent Model Used
|
|
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
|
|
|
### File List
|
|
**Created:**
|
|
- `app/Contracts/ShouldNotifyAdmin.php` - Marker interface for exceptions requiring admin notification
|
|
- `app/Services/AdminNotificationService.php` - Service with rate limiting and exception filtering logic
|
|
- `app/Notifications/SystemErrorNotification.php` - Notification for critical system errors
|
|
- `app/Notifications/QueueFailureNotification.php` - Notification for queue job failures
|
|
- `tests/Unit/Services/AdminNotificationServiceTest.php` - Unit tests for AdminNotificationService
|
|
- `tests/Feature/Notifications/SystemErrorNotificationTest.php` - Feature tests for system error notifications
|
|
- `tests/Feature/Notifications/QueueFailureNotificationTest.php` - Feature tests for queue failure notifications
|
|
|
|
**Modified:**
|
|
- `config/libra.php` - Added notifications configuration section
|
|
- `.env.example` - Added admin notification environment variables
|
|
- `bootstrap/app.php` - Added exception handler integration
|
|
- `app/Providers/AppServiceProvider.php` - Added queue failure listener
|
|
|
|
### Completion Notes
|
|
1. All acceptance criteria have been met
|
|
2. Implemented AdminNotificationService with cache-based rate limiting (15 minutes default)
|
|
3. SystemErrorNotification sends immediately (sync) with [URGENT] subject and environment indicator
|
|
4. QueueFailureNotification triggers on Queue::failing event with job details
|
|
5. Exception filtering correctly excludes validation, auth, 404, and CSRF exceptions
|
|
6. Critical database errors (connection codes 2002, 1045, 1049) trigger notifications
|
|
7. Mail transport exceptions trigger notifications
|
|
8. ShouldNotifyAdmin marker interface allows custom exceptions to opt-in
|
|
9. All 35 new tests pass (16 unit tests, 19 feature tests)
|
|
10. Full test suite passes (excluding pre-existing Settings test issues and Reports memory issues)
|
|
|
|
### Change Log
|
|
| Date | Change | Reason |
|
|
|------|--------|--------|
|
|
| 2026-01-02 | Initial implementation | Story 8.10 development |
|
|
|
|
### Debug Log References
|
|
N/A - No debug issues encountered during development
|
|
|
|
---
|
|
|
|
## QA Results
|
|
|
|
### Review Date: 2026-01-02
|
|
|
|
### Reviewed By: Quinn (Test Architect)
|
|
|
|
### Code Quality Assessment
|
|
|
|
The implementation is well-structured and follows Laravel best practices. The code demonstrates:
|
|
- Clean separation of concerns with `AdminNotificationService` handling all logic
|
|
- Proper use of Laravel's notification system with `notifyNow()` for immediate delivery
|
|
- Cache-based rate limiting to prevent notification spam
|
|
- Defensive programming with null checks on admin email configuration
|
|
|
|
The notification classes are concise and properly formatted with urgent subject lines including environment indicators.
|
|
|
|
### Refactoring Performed
|
|
|
|
None required - code quality meets standards.
|
|
|
|
### Compliance Check
|
|
|
|
- Coding Standards: ✓ Pint passes on all files
|
|
- Project Structure: ✓ Files in correct locations (Services, Notifications, Contracts)
|
|
- Testing Strategy: ✓ Unit tests for service logic, Feature tests for integration
|
|
- All ACs Met: ✓ All 14 acceptance criteria verified
|
|
|
|
### Improvements Checklist
|
|
|
|
- [x] Rate limiting implemented via Cache facade
|
|
- [x] Excluded exceptions properly filtered (6 types)
|
|
- [x] Critical database errors identified by error codes
|
|
- [x] Mail transport exceptions trigger notifications
|
|
- [x] ShouldNotifyAdmin marker interface working
|
|
- [x] Queue failure listener registered in AppServiceProvider
|
|
- [x] Exception handler integration in bootstrap/app.php
|
|
- [x] Environment variables documented in .env.example
|
|
- [x] Configuration added to config/libra.php
|
|
- [x] notifyNow() used for immediate delivery (not queued)
|
|
|
|
### Security Review
|
|
|
|
No security concerns. The implementation:
|
|
- Does not expose sensitive data in notifications (stack traces kept in logs)
|
|
- Properly validates admin email configuration before sending
|
|
- Uses Laravel's built-in notification system
|
|
|
|
### Performance Considerations
|
|
|
|
- Rate limiting (cache-based) prevents excessive notification sending
|
|
- `notifyNow()` is synchronous but appropriate since admin notifications are critical
|
|
- Cache operations are lightweight (simple key existence check)
|
|
|
|
### Files Modified During Review
|
|
|
|
None - no modifications needed.
|
|
|
|
### Gate Status
|
|
|
|
Gate: PASS → docs/qa/gates/8.10-admin-notification-system-events.yml
|
|
|
|
### Recommended Status
|
|
|
|
✓ Ready for Done
|
|
|
|
All acceptance criteria met, 35 tests passing, code properly formatted.
|