artisan('db:seed', ['--class' => 'PlanSeeder']); }); // ─── canUseChannel ──────────────────────────────────────────────────────────── it('canUseChannel returns false for sms on free tier', function (): void { $user = User::factory()->create(); expect(PlanFeatures::for($user)->canUseChannel('sms'))->toBeFalse(); }); it('canUseChannel returns false for sms on basic tier', function (): void { $plan = Plan::where('name', 'basic')->first(); expect($plan->sms_enabled)->toBeFalse(); }); it('canUseChannel returns true for sms on plus tier', function (): void { $plan = Plan::where('name', 'plus')->first(); expect($plan->sms_enabled)->toBeTrue(); }); it('canUseChannel returns true for sms on pro tier', function (): void { $plan = Plan::where('name', 'pro')->first(); expect($plan->sms_enabled)->toBeTrue(); }); // ─── canSendNow ─────────────────────────────────────────────────────────────── it('canSendNow returns false when tier does not allow the channel', function (): void { $user = User::factory()->create(); // free tier: push = false expect(PlanFeatures::for($user)->canSendNow('push'))->toBeFalse(); }); it('canSendNow returns false when daily limit is reached', function (): void { $plan = Plan::where('name', 'plus')->first(); // sms_daily_limit = 1 $user = User::factory()->create(); UserNotificationPreference::factory()->create([ 'user_id' => $user->id, 'channel' => 'sms', 'fuel_type' => FuelType::E10->value, 'enabled' => true, ]); NotificationLog::factory()->create([ 'user_id' => $user->id, 'channel' => 'sms', 'sent' => true, 'created_at' => now(), ]); expect($plan->sms_daily_limit)->toBe(1); $sentCount = NotificationLog::where('user_id', $user->id) ->where('channel', 'sms') ->where('sent', true) ->whereDate('created_at', today()) ->count(); expect($sentCount)->toBe(1); }); // ─── canTrackFuelType ───────────────────────────────────────────────────────── it('canTrackFuelType respects max limit for non-pro tiers', function (): void { $plan = Plan::where('name', 'basic')->first(); // max_fuel_types = 1 $user = User::factory()->create(); UserNotificationPreference::factory()->create([ 'user_id' => $user->id, 'channel' => 'email', 'fuel_type' => FuelType::E10->value, 'enabled' => true, ]); expect($plan->max_fuel_types)->toBe(1); $count = UserNotificationPreference::where('user_id', $user->id) ->distinct('fuel_type') ->count('fuel_type'); expect($count)->toBe(1); }); it('pro tier has null fuel type limit meaning unlimited', function (): void { $plan = Plan::where('name', 'pro')->first(); expect($plan->max_fuel_types)->toBeNull(); }); // ─── can() feature flags ────────────────────────────────────────────────────── it('can returns false for ai_predictions on free tier', function (): void { $plan = Plan::where('name', 'free')->first(); expect($plan->ai_predictions)->toBeFalse(); }); it('can returns true for ai_predictions on plus tier', function (): void { $plan = Plan::where('name', 'plus')->first(); expect($plan->ai_predictions)->toBeTrue(); }); // ─── PlanSeeder idempotency ─────────────────────────────────────────────────── it('PlanSeeder is idempotent', function (): void { $this->artisan('db:seed', ['--class' => 'PlanSeeder']); expect(Plan::count())->toBe(4); expect(Plan::where('name', 'free')->count())->toBe(1); expect(Plan::where('name', 'basic')->count())->toBe(1); expect(Plan::where('name', 'plus')->count())->toBe(1); expect(Plan::where('name', 'pro')->count())->toBe(1); }); // ─── RequiresFeature middleware ─────────────────────────────────────────────── it('RequiresFeature middleware returns 403 when feature is not available', function (): void { $user = User::factory()->create(); $request = Request::create('/test', 'GET'); $request->setUserResolver(fn () => $user); $middleware = new RequiresFeature; $response = $middleware->handle($request, fn () => response('ok'), 'ai_predictions'); expect($response->getStatusCode())->toBe(403); expect(json_decode((string) $response->getContent(), true))->toBe([ 'error' => 'upgrade_required', 'feature' => 'ai_predictions', ]); }); // ─── NotificationLog scopes ─────────────────────────────────────────────────── it('scopeMissed returns only unsent log entries', function (): void { $user = User::factory()->create(); NotificationLog::factory()->create(['user_id' => $user->id, 'sent' => true]); NotificationLog::factory()->create(['user_id' => $user->id, 'sent' => false, 'missed_reason' => 'daily_limit']); NotificationLog::factory()->create(['user_id' => $user->id, 'sent' => false, 'missed_reason' => 'tier_restricted']); expect(NotificationLog::missed()->where('user_id', $user->id)->count())->toBe(2); }); it('scopeSentToday returns only sent entries for that channel today', function (): void { $user = User::factory()->create(); NotificationLog::factory()->create(['user_id' => $user->id, 'channel' => 'sms', 'sent' => true, 'created_at' => now()]); NotificationLog::factory()->create(['user_id' => $user->id, 'channel' => 'sms', 'sent' => true, 'created_at' => now()->subDay()]); NotificationLog::factory()->create(['user_id' => $user->id, 'channel' => 'email', 'sent' => true, 'created_at' => now()]); expect(NotificationLog::sentToday('sms')->where('user_id', $user->id)->count())->toBe(1); }); // ─── UserNotificationPreference scopes ─────────────────────────────────────── it('scopeEnabled filters disabled preferences', function (): void { $user = User::factory()->create(); UserNotificationPreference::factory()->create(['user_id' => $user->id, 'channel' => 'email', 'fuel_type' => FuelType::E10->value, 'enabled' => true]); UserNotificationPreference::factory()->create(['user_id' => $user->id, 'channel' => 'sms', 'fuel_type' => FuelType::E10->value, 'enabled' => false]); expect(UserNotificationPreference::enabled()->where('user_id', $user->id)->count())->toBe(1); }); it('scopeForChannel filters by channel', function (): void { $user = User::factory()->create(); UserNotificationPreference::factory()->create(['user_id' => $user->id, 'channel' => 'sms']); UserNotificationPreference::factory()->create(['user_id' => $user->id, 'channel' => 'email']); expect(UserNotificationPreference::forChannel('sms')->where('user_id', $user->id)->count())->toBe(1); }); it('scopeForFuelType filters by fuel type', function (): void { $user = User::factory()->create(); UserNotificationPreference::factory()->create(['user_id' => $user->id, 'fuel_type' => FuelType::E10->value]); UserNotificationPreference::factory()->create(['user_id' => $user->id, 'fuel_type' => FuelType::E5->value]); expect(UserNotificationPreference::forFuelType(FuelType::E10->value)->where('user_id', $user->id)->count())->toBe(1); }); // ─── push frequency ─────────────────────────────────────────────────────────── it('seeds push frequency for every tier', function (): void { expect(Plan::where('name', 'free')->first()) ->push_enabled->toBeFalse()->push_frequency->toBe('none') ->and(Plan::where('name', 'basic')->first()) ->push_enabled->toBeTrue()->push_frequency->toBe('daily') ->and(Plan::where('name', 'plus')->first()) ->push_enabled->toBeTrue()->push_frequency->toBe('triggered') ->and(Plan::where('name', 'pro')->first()) ->push_enabled->toBeTrue()->push_frequency->toBe('triggered'); }); // ─── display name ───────────────────────────────────────────────────────────── it('Plan::displayName returns the user-facing label for each seeded tier', function (): void { expect(Plan::where('name', 'free')->first()->displayName())->toBe('Free') ->and(Plan::where('name', 'basic')->first()->displayName())->toBe('Daily') ->and(Plan::where('name', 'plus')->first()->displayName())->toBe('Smart') ->and(Plan::where('name', 'pro')->first()->displayName())->toBe('Pro'); }); it('PlanFeatures::displayName delegates to the resolved tier', function (): void { $user = User::factory()->create(); expect(PlanFeatures::for($user)->displayName())->toBe('Free'); });