plan = Plan::resolveForUser($user); } catch (Throwable) { // Never throw — fall back to a free-tier stub if resolution fails. $this->plan = new Plan([ 'name' => 'free', 'features' => [ 'fuel_types' => ['max' => 1], 'email' => ['enabled' => true, 'frequency' => 'weekly_digest'], 'push' => ['enabled' => false], '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 static function for(User $user): self { return new self($user); } /** * Channels allowed for a given trigger type, filtered by: * tier allows → user has enabled → daily limit not hit. * * @return string[] */ public function channelsFor(string $triggerType): array { $allChannels = ['email', 'push', 'whatsapp', 'sms']; $allowed = []; foreach ($allChannels as $channel) { if (! $this->canUseChannel($channel)) { continue; } if (! $this->userHasEnabledChannel($channel)) { continue; } if (! $this->canSendNow($channel)) { continue; } $allowed[] = $channel; } return $allowed; } /** Whether the plan allows this channel at all. */ public function canUseChannel(string $channel): bool { return (bool) ($this->feature($channel, 'enabled') ?? false); } /** Read a nested feature value, e.g. feature('sms', 'daily_limit'). */ private function feature(string $channel, string $key): mixed { $features = $this->plan->features ?? []; return $features[$channel][$key] ?? null; } /** Whether the user has opted in to this channel for at least one fuel type. */ private function userHasEnabledChannel(string $channel): bool { return UserNotificationPreference::where('user_id', $this->user->id) ->where('channel', $channel) ->where('enabled', true) ->exists(); } /** * Whether a notification can be sent right now on this channel. * Checks both the plan cap and today's live count in notification_log. */ public function canSendNow(string $channel): bool { if (! $this->canUseChannel($channel)) { return false; } $dailyLimit = $this->feature($channel, 'daily_limit'); // null or 0 in the feature means no SMS/unlimited — treat 0 as blocked, null as unlimited if ($dailyLimit === null) { return true; } if ($dailyLimit === 0) { return false; } $sentToday = NotificationLog::where('user_id', $this->user->id) ->where('channel', $channel) ->where('sent', true) ->whereDate('created_at', today()) ->count(); return $sentToday < $dailyLimit; } /** Whether the user can track an additional fuel type. */ public function canTrackFuelType(string $fuelType): bool { $limit = $this->fuelTypeLimit(); if ($limit === null) { return true; } $count = $this->trackedFuelTypeCount(); // Allow if already tracking this type (not adding a new one) $alreadyTracking = UserNotificationPreference::where('user_id', $this->user->id) ->where('fuel_type', $fuelType) ->exists(); if ($alreadyTracking) { return true; } return $count < $limit; } /** Maximum fuel types allowed, or null for unlimited. */ public function fuelTypeLimit(): ?int { $features = $this->plan->features ?? []; return $features['fuel_types']['max'] ?? 1; } /** Count of distinct fuel types the user has preferences for. */ public function trackedFuelTypeCount(): int { return UserNotificationPreference::where('user_id', $this->user->id) ->distinct('fuel_type') ->count('fuel_type'); } /** Generic boolean feature flag check. */ public function can(string $feature): bool { $features = $this->plan->features ?? []; return (bool) ($features[$feature] ?? false); } /** Count of notifications missed today on a channel. */ public function missedToday(string $channel): int { return NotificationLog::where('user_id', $this->user->id) ->where('channel', $channel) ->where('sent', false) ->whereDate('created_at', today()) ->count(); } /** Count of notifications missed this month on a channel. */ public function missedThisMonth(string $channel): int { return NotificationLog::where('user_id', $this->user->id) ->where('channel', $channel) ->where('sent', false) ->whereMonth('created_at', now()->month) ->whereYear('created_at', now()->year) ->count(); } /** The resolved plan tier name. */ public function tier(): string { return $this->plan->name ?? 'free'; } }