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:
@@ -11,23 +11,25 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
*/
|
||||
class PlanFactory extends Factory
|
||||
{
|
||||
private static array $defaultFeatures = [
|
||||
'fuel_types' => ['max' => 1],
|
||||
'email' => ['enabled' => false, 'frequency' => 'weekly_digest'],
|
||||
'push' => ['enabled' => false, 'frequency' => 'none'],
|
||||
'whatsapp' => ['enabled' => false, 'daily_limit' => 0, 'scheduled_updates' => 0],
|
||||
'sms' => ['enabled' => false, 'daily_limit' => 0],
|
||||
'ai_predictions' => false,
|
||||
'price_threshold' => false,
|
||||
'score_alerts' => false,
|
||||
];
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => PlanTier::Free->value,
|
||||
'stripe_price_id' => null,
|
||||
'features' => self::$defaultFeatures,
|
||||
'stripe_price_id_monthly' => null,
|
||||
'stripe_price_id_annual' => null,
|
||||
'max_fuel_types' => 1,
|
||||
'email_enabled' => true,
|
||||
'email_frequency' => 'weekly_digest',
|
||||
'push_enabled' => false,
|
||||
'push_frequency' => 'none',
|
||||
'whatsapp_enabled' => false,
|
||||
'whatsapp_daily_limit' => 0,
|
||||
'whatsapp_scheduled_updates' => 0,
|
||||
'sms_enabled' => false,
|
||||
'sms_daily_limit' => 0,
|
||||
'ai_predictions' => false,
|
||||
'price_threshold' => false,
|
||||
'score_alerts' => false,
|
||||
'active' => true,
|
||||
];
|
||||
}
|
||||
@@ -36,8 +38,8 @@ class PlanFactory extends Factory
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'name' => PlanTier::Free->value,
|
||||
'stripe_price_id' => null,
|
||||
'features' => self::$defaultFeatures,
|
||||
'stripe_price_id_monthly' => null,
|
||||
'stripe_price_id_annual' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -45,17 +47,21 @@ class PlanFactory extends Factory
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'name' => PlanTier::Basic->value,
|
||||
'stripe_price_id' => 'price_basic_test',
|
||||
'features' => [
|
||||
'fuel_types' => ['max' => 1],
|
||||
'email' => ['enabled' => true, 'frequency' => 'daily'],
|
||||
'push' => ['enabled' => true, 'frequency' => 'daily'],
|
||||
'whatsapp' => ['enabled' => true, 'daily_limit' => 5, 'scheduled_updates' => 2],
|
||||
'sms' => ['enabled' => false, 'daily_limit' => 0],
|
||||
'ai_predictions' => false,
|
||||
'price_threshold' => true,
|
||||
'score_alerts' => true,
|
||||
],
|
||||
'stripe_price_id_monthly' => 'price_basic_monthly_test',
|
||||
'stripe_price_id_annual' => 'price_basic_annual_test',
|
||||
'max_fuel_types' => 1,
|
||||
'email_enabled' => true,
|
||||
'email_frequency' => 'daily',
|
||||
'push_enabled' => true,
|
||||
'push_frequency' => 'daily',
|
||||
'whatsapp_enabled' => true,
|
||||
'whatsapp_daily_limit' => 5,
|
||||
'whatsapp_scheduled_updates' => 2,
|
||||
'sms_enabled' => false,
|
||||
'sms_daily_limit' => 0,
|
||||
'ai_predictions' => false,
|
||||
'price_threshold' => true,
|
||||
'score_alerts' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -63,17 +69,21 @@ class PlanFactory extends Factory
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'name' => PlanTier::Plus->value,
|
||||
'stripe_price_id' => 'price_plus_test',
|
||||
'features' => [
|
||||
'fuel_types' => ['max' => 1],
|
||||
'email' => ['enabled' => true, 'frequency' => 'triggered'],
|
||||
'push' => ['enabled' => true, 'frequency' => 'triggered'],
|
||||
'whatsapp' => ['enabled' => true, 'daily_limit' => 5, 'scheduled_updates' => 2],
|
||||
'sms' => ['enabled' => true, 'daily_limit' => 1],
|
||||
'ai_predictions' => true,
|
||||
'price_threshold' => true,
|
||||
'score_alerts' => true,
|
||||
],
|
||||
'stripe_price_id_monthly' => 'price_plus_monthly_test',
|
||||
'stripe_price_id_annual' => 'price_plus_annual_test',
|
||||
'max_fuel_types' => 1,
|
||||
'email_enabled' => true,
|
||||
'email_frequency' => 'triggered',
|
||||
'push_enabled' => true,
|
||||
'push_frequency' => 'triggered',
|
||||
'whatsapp_enabled' => true,
|
||||
'whatsapp_daily_limit' => 5,
|
||||
'whatsapp_scheduled_updates' => 2,
|
||||
'sms_enabled' => true,
|
||||
'sms_daily_limit' => 1,
|
||||
'ai_predictions' => true,
|
||||
'price_threshold' => true,
|
||||
'score_alerts' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -81,17 +91,21 @@ class PlanFactory extends Factory
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'name' => PlanTier::Pro->value,
|
||||
'stripe_price_id' => 'price_pro_test',
|
||||
'features' => [
|
||||
'fuel_types' => ['max' => null],
|
||||
'email' => ['enabled' => true, 'frequency' => 'triggered'],
|
||||
'push' => ['enabled' => true, 'frequency' => 'triggered'],
|
||||
'whatsapp' => ['enabled' => true, 'daily_limit' => 5, 'scheduled_updates' => 2],
|
||||
'sms' => ['enabled' => true, 'daily_limit' => 3],
|
||||
'ai_predictions' => true,
|
||||
'price_threshold' => true,
|
||||
'score_alerts' => true,
|
||||
],
|
||||
'stripe_price_id_monthly' => 'price_pro_monthly_test',
|
||||
'stripe_price_id_annual' => 'price_pro_annual_test',
|
||||
'max_fuel_types' => null,
|
||||
'email_enabled' => true,
|
||||
'email_frequency' => 'triggered',
|
||||
'push_enabled' => true,
|
||||
'push_frequency' => 'triggered',
|
||||
'whatsapp_enabled' => true,
|
||||
'whatsapp_daily_limit' => 5,
|
||||
'whatsapp_scheduled_updates' => 2,
|
||||
'sms_enabled' => true,
|
||||
'sms_daily_limit' => 3,
|
||||
'ai_predictions' => true,
|
||||
'price_threshold' => true,
|
||||
'score_alerts' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user