refactor: flatten plans.features JSON to typed columns

The features JSON column required defensive fallback stubs in three
places (Plan::resolveForUser, PlanFeatures::__construct, PlanSeeder)
and silently swallowed misspelled keys. Typed columns give Eloquent
type-safe reads, simplify the Filament form (no more dotted JSON
paths), and let resolveForUser fail loud when the free row is
missing.

PlanFeatures public API is unchanged so consumers (jobs, middleware)
need no rewrites — one missed JSON read in SendScheduledWhatsAppJob
was caught and converted to a typed where() query.

tests/Pest.php seeds PlanSeeder in beforeEach so any feature test
that resolves a plan finds the free row, mirroring production where
plans always exist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ovidiu U
2026-04-29 18:13:26 +01:00
parent 088fd11058
commit 8695d5ec95
13 changed files with 304 additions and 260 deletions

View File

@@ -69,11 +69,10 @@ it('logs daily_limit when the channel is allowed but the limit is exhausted', fu
$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();
Plan::where('name', 'free')->first()->update([
'sms_enabled' => true,
'sms_daily_limit' => 1,
]);
UserNotificationPreference::factory()->create([
'user_id' => $user->id,
@@ -106,14 +105,11 @@ it('logs daily_limit when the channel is allowed but the limit is exhausted', fu
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();
Plan::where('name', 'free')->first()->update([
'sms_enabled' => true,
'sms_daily_limit' => 3,
]);
// User has sms pref but it is disabled
UserNotificationPreference::factory()->create([
'user_id' => $user->id,
'channel' => 'sms',
@@ -145,12 +141,11 @@ it('dispatches DispatchUserNotificationJob for eligible whatsapp users', functio
$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();
Plan::where('name', 'free')->first()->update([
'whatsapp_enabled' => true,
'whatsapp_daily_limit' => 5,
'whatsapp_scheduled_updates' => 2,
]);
UserNotificationPreference::factory()->create([
'user_id' => $user->id,
@@ -171,11 +166,11 @@ it('skips users who have hit their whatsapp daily limit', function (): void {
$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();
Plan::where('name', 'free')->first()->update([
'whatsapp_enabled' => true,
'whatsapp_daily_limit' => 1,
'whatsapp_scheduled_updates' => 2,
]);
UserNotificationPreference::factory()->create([
'user_id' => $user->id,
@@ -184,7 +179,6 @@ it('skips users who have hit their whatsapp daily limit', function (): void {
'enabled' => true,
]);
// Exhaust the daily limit
NotificationLog::factory()->create([
'user_id' => $user->id,
'channel' => 'whatsapp',
@@ -204,11 +198,11 @@ it('passes scheduled_morning trigger for morning period', function (): void {
$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();
Plan::where('name', 'free')->first()->update([
'whatsapp_enabled' => true,
'whatsapp_daily_limit' => 5,
'whatsapp_scheduled_updates' => 2,
]);
UserNotificationPreference::factory()->create([
'user_id' => $user->id,