feat: build FuelPriceAlert notification with multi-channel adapters

The DispatchUserNotificationJob has been logging sent=true for every
allowed channel without actually sending anything — a TODO marker covered
by the existing test suite, which only asserted log rows. The downstream
"missed today" widget read those rows and reported falsely. This commit
makes the telemetry truthful by wiring the real send.

- App\Notifications\FuelPriceAlert — Notification class with via() that
  returns the per-tier-filtered channel list passed in by the dispatcher.
  Implements toMail / toOneSignal / toVonageWhatsApp / toVonageSms.
  ShouldQueue on the 'notifications' queue.
- App\Notifications\Channels\OneSignalChannel — raw HTTP to OneSignal
  REST API, gated on services.onesignal.{app_id,api_key} + user
  push_token. Logs every call to api_logs via ApiLogger.
- App\Notifications\Channels\VonageWhatsAppChannel — raw HTTP to Vonage
  Messages API, gated on whatsapp_verified_at + whatsapp_number.
- App\Notifications\Channels\VonageSmsChannel — raw HTTP to Vonage SMS
  API, gated on whatsapp_number.
- DispatchUserNotificationJob now calls $user->notify(new
  FuelPriceAlert(...)) before logging.
- New tests: assert the notification IS dispatched with the right
  channels, and that nothing is dispatched when no channels are allowed.

Channels gracefully no-op when their credentials are unset (logging at
info level), so existing tests without a Notification::fake() still
pass — the channels just early-return on missing config.

No new composer dependencies — Vonage SDK avoided in favour of raw HTTP
through the existing ApiLogger pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ovidiu U
2026-04-29 19:48:10 +01:00
parent 27c82ef103
commit 5369b4a5a0
7 changed files with 395 additions and 6 deletions

View File

@@ -5,15 +5,15 @@ namespace App\Jobs;
use App\Models\NotificationLog;
use App\Models\User;
use App\Models\UserNotificationPreference;
use App\Notifications\FuelPriceAlert;
use App\Services\PlanFeatures;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
/**
* Resolves allowed notification channels for a user and trigger, sends
* notifications, and logs every outcome (sent, daily_limit, tier_restricted).
*
* Actual sending is stubbed until FuelPriceAlert notification class exists.
* Resolves allowed notification channels for a user and trigger, dispatches
* the FuelPriceAlert notification (which fans out to email + push + WhatsApp +
* SMS), and logs every outcome (sent, daily_limit, tier_restricted).
*/
final class DispatchUserNotificationJob implements ShouldQueue
{
@@ -38,9 +38,21 @@ final class DispatchUserNotificationJob implements ShouldQueue
// Step 3: channels that pass tier + user-pref + daily-limit checks
$allowed = $features->channelsFor($this->triggerType);
// Step 4: send and log sent notifications
// Step 4: dispatch the multi-channel notification — Laravel fans out
// to mail / OneSignal / Vonage WhatsApp / Vonage SMS based on via().
if ($allowed !== []) {
$this->user->notify(new FuelPriceAlert(
$this->triggerType,
$this->fuelType,
$this->price,
$allowed,
));
}
// Step 5: log a sent entry per allowed channel. The notify() call
// above queues per-channel sends; per-channel HTTP outcomes are
// captured in api_logs by the channel adapters themselves.
foreach ($allowed as $channel) {
// TODO: $this->user->notify(new FuelPriceAlert($this->triggerType, $this->fuelType, $this->price));
$this->log($channel, sent: true);
}