plan = Plan::resolveForUser($user); } 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 { $allowed = []; foreach (self::CHANNELS 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->plan->{"{$channel}_enabled"}; } /** * 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->dailyLimit($channel); // null = unlimited; 0 = blocked even though enabled 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; } $alreadyTracking = UserNotificationPreference::where('user_id', $this->user->id) ->where('fuel_type', $fuelType) ->exists(); if ($alreadyTracking) { return true; } return $this->trackedFuelTypeCount() < $limit; } /** Maximum fuel types allowed, or null for unlimited. */ public function fuelTypeLimit(): ?int { return $this->plan->max_fuel_types; } /** 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 { return (bool) ($this->plan->{$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; } /** User-facing display label for the resolved tier (e.g. basic → "Daily"). */ public function displayName(): string { return $this->plan->displayName(); } /** 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(); } /** Per-channel daily limit. Null on email/push (no cap), int on whatsapp/sms. */ private function dailyLimit(string $channel): ?int { return match ($channel) { 'whatsapp' => $this->plan->whatsapp_daily_limit, 'sms' => $this->plan->sms_daily_limit, default => null, }; } }