- Add database migrations for plans, subscriptions, notification preferences, and notification log tables - Implement DispatchUserNotificationJob to handle channel resolution, daily limits, and logging (sent/tier_restricted/daily_limit) - Add SendScheduledWhatsAppJob for scheduled notification delivery - Create PlanFeatures service to resolve tier capabilities, check daily limits, and validate fuel
226 lines
7.6 KiB
PHP
226 lines
7.6 KiB
PHP
<?php
|
|
|
|
use App\Enums\FuelType;
|
|
use App\Jobs\DispatchUserNotificationJob;
|
|
use App\Jobs\SendScheduledWhatsAppJob;
|
|
use App\Models\NotificationLog;
|
|
use App\Models\Plan;
|
|
use App\Models\User;
|
|
use App\Models\UserNotificationPreference;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Queue;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function (): void {
|
|
$this->artisan('db:seed', ['--class' => 'PlanSeeder']);
|
|
});
|
|
|
|
// ─── DispatchUserNotificationJob — sent logging ───────────────────────────────
|
|
|
|
it('logs a sent entry for each allowed channel', function (): void {
|
|
// Free tier allows email (weekly_digest). User has email pref enabled.
|
|
$user = User::factory()->create();
|
|
|
|
UserNotificationPreference::factory()->create([
|
|
'user_id' => $user->id,
|
|
'channel' => 'email',
|
|
'fuel_type' => FuelType::E10->value,
|
|
'enabled' => true,
|
|
]);
|
|
|
|
(new DispatchUserNotificationJob($user, 'price_threshold', FuelType::E10->value, price: 143.9))->handle();
|
|
|
|
$log = NotificationLog::where('user_id', $user->id)->where('channel', 'email')->first();
|
|
|
|
expect($log)->not->toBeNull()
|
|
->and($log->sent)->toBeTrue()
|
|
->and($log->trigger_type)->toBe('price_threshold')
|
|
->and($log->fuel_type)->toBe(FuelType::E10->value);
|
|
});
|
|
|
|
// ─── DispatchUserNotificationJob — tier_restricted logging ───────────────────
|
|
|
|
it('logs tier_restricted for channels the user wants but their tier forbids', function (): void {
|
|
// Free tier: sms is disabled. User has sms pref enabled.
|
|
$user = User::factory()->create();
|
|
|
|
UserNotificationPreference::factory()->create([
|
|
'user_id' => $user->id,
|
|
'channel' => 'sms',
|
|
'fuel_type' => FuelType::E10->value,
|
|
'enabled' => true,
|
|
]);
|
|
|
|
(new DispatchUserNotificationJob($user, 'price_threshold', FuelType::E10->value))->handle();
|
|
|
|
$log = NotificationLog::where('user_id', $user->id)
|
|
->where('channel', 'sms')
|
|
->first();
|
|
|
|
expect($log)->not->toBeNull()
|
|
->and($log->sent)->toBeFalse()
|
|
->and($log->missed_reason)->toBe('tier_restricted');
|
|
});
|
|
|
|
// ─── DispatchUserNotificationJob — daily_limit logging ───────────────────────
|
|
|
|
it('logs daily_limit when the channel is allowed but the limit is exhausted', function (): void {
|
|
$user = User::factory()->create();
|
|
|
|
// Patch the free plan to allow sms with limit 1
|
|
$freePlan = Plan::where('name', 'free')->first();
|
|
$features = $freePlan->features;
|
|
$features['sms'] = ['enabled' => true, 'daily_limit' => 1];
|
|
$freePlan->features = $features;
|
|
$freePlan->save();
|
|
|
|
UserNotificationPreference::factory()->create([
|
|
'user_id' => $user->id,
|
|
'channel' => 'sms',
|
|
'fuel_type' => FuelType::E10->value,
|
|
'enabled' => true,
|
|
]);
|
|
|
|
// Pre-log one sent SMS to hit the daily limit
|
|
NotificationLog::factory()->create([
|
|
'user_id' => $user->id,
|
|
'channel' => 'sms',
|
|
'sent' => true,
|
|
'created_at' => now(),
|
|
]);
|
|
|
|
(new DispatchUserNotificationJob($user, 'price_threshold', FuelType::E10->value))->handle();
|
|
|
|
$missed = NotificationLog::where('user_id', $user->id)
|
|
->where('channel', 'sms')
|
|
->where('sent', false)
|
|
->first();
|
|
|
|
expect($missed)->not->toBeNull()
|
|
->and($missed->missed_reason)->toBe('daily_limit');
|
|
});
|
|
|
|
// ─── DispatchUserNotificationJob — does not log user-disabled channels ────────
|
|
|
|
it('does not log channels the user has explicitly disabled', function (): void {
|
|
$user = User::factory()->create();
|
|
|
|
// Patch free plan to allow sms
|
|
$freePlan = Plan::where('name', 'free')->first();
|
|
$features = $freePlan->features;
|
|
$features['sms'] = ['enabled' => true, 'daily_limit' => 3];
|
|
$freePlan->features = $features;
|
|
$freePlan->save();
|
|
|
|
// User has sms pref but it is disabled
|
|
UserNotificationPreference::factory()->create([
|
|
'user_id' => $user->id,
|
|
'channel' => 'sms',
|
|
'fuel_type' => FuelType::E10->value,
|
|
'enabled' => false,
|
|
]);
|
|
|
|
(new DispatchUserNotificationJob($user, 'price_threshold', FuelType::E10->value))->handle();
|
|
|
|
expect(NotificationLog::where('user_id', $user->id)->where('channel', 'sms')->count())->toBe(0);
|
|
});
|
|
|
|
// ─── DispatchUserNotificationJob — queued on notifications queue ──────────────
|
|
|
|
it('is dispatched on the notifications queue', function (): void {
|
|
Queue::fake();
|
|
|
|
$user = User::factory()->create();
|
|
|
|
DispatchUserNotificationJob::dispatch($user, 'price_threshold', FuelType::E10->value);
|
|
|
|
Queue::assertPushedOn('notifications', DispatchUserNotificationJob::class);
|
|
});
|
|
|
|
// ─── SendScheduledWhatsAppJob — dispatches per eligible user ─────────────────
|
|
|
|
it('dispatches DispatchUserNotificationJob for eligible whatsapp users', function (): void {
|
|
Queue::fake();
|
|
|
|
$user = User::factory()->create();
|
|
|
|
// Patch free plan to allow whatsapp with scheduled updates
|
|
$freePlan = Plan::where('name', 'free')->first();
|
|
$features = $freePlan->features;
|
|
$features['whatsapp'] = ['enabled' => true, 'daily_limit' => 5, 'scheduled_updates' => 2];
|
|
$freePlan->features = $features;
|
|
$freePlan->save();
|
|
|
|
UserNotificationPreference::factory()->create([
|
|
'user_id' => $user->id,
|
|
'channel' => 'whatsapp',
|
|
'fuel_type' => FuelType::E10->value,
|
|
'enabled' => true,
|
|
]);
|
|
|
|
(new SendScheduledWhatsAppJob('morning'))->handle();
|
|
|
|
Queue::assertPushedOn('notifications', DispatchUserNotificationJob::class);
|
|
});
|
|
|
|
// ─── SendScheduledWhatsAppJob — skips users over daily limit ─────────────────
|
|
|
|
it('skips users who have hit their whatsapp daily limit', function (): void {
|
|
Queue::fake();
|
|
|
|
$user = User::factory()->create();
|
|
|
|
$freePlan = Plan::where('name', 'free')->first();
|
|
$features = $freePlan->features;
|
|
$features['whatsapp'] = ['enabled' => true, 'daily_limit' => 1, 'scheduled_updates' => 2];
|
|
$freePlan->features = $features;
|
|
$freePlan->save();
|
|
|
|
UserNotificationPreference::factory()->create([
|
|
'user_id' => $user->id,
|
|
'channel' => 'whatsapp',
|
|
'fuel_type' => FuelType::E10->value,
|
|
'enabled' => true,
|
|
]);
|
|
|
|
// Exhaust the daily limit
|
|
NotificationLog::factory()->create([
|
|
'user_id' => $user->id,
|
|
'channel' => 'whatsapp',
|
|
'sent' => true,
|
|
'created_at' => now(),
|
|
]);
|
|
|
|
(new SendScheduledWhatsAppJob('evening'))->handle();
|
|
|
|
Queue::assertNothingPushed();
|
|
});
|
|
|
|
// ─── SendScheduledWhatsAppJob — correct trigger type per period ───────────────
|
|
|
|
it('passes scheduled_morning trigger for morning period', function (): void {
|
|
Queue::fake();
|
|
|
|
$user = User::factory()->create();
|
|
|
|
$freePlan = Plan::where('name', 'free')->first();
|
|
$features = $freePlan->features;
|
|
$features['whatsapp'] = ['enabled' => true, 'daily_limit' => 5, 'scheduled_updates' => 2];
|
|
$freePlan->features = $features;
|
|
$freePlan->save();
|
|
|
|
UserNotificationPreference::factory()->create([
|
|
'user_id' => $user->id,
|
|
'channel' => 'whatsapp',
|
|
'fuel_type' => FuelType::E10->value,
|
|
'enabled' => true,
|
|
]);
|
|
|
|
(new SendScheduledWhatsAppJob('morning'))->handle();
|
|
|
|
Queue::assertPushed(DispatchUserNotificationJob::class, function (DispatchUserNotificationJob $job): bool {
|
|
return $job->triggerType === 'scheduled_morning';
|
|
});
|
|
});
|