Adds 7 new composable Query modules following the existing DeviceQuery/AlertQuery pattern. Each exposes base/0 and for_organization/1,2 plus filters used by 2+ callers. New modules: - Towerops.OnCall.ScheduleQuery - Towerops.OnCall.EscalationPolicyQuery - Towerops.Maintenance.MaintenanceWindowQuery - Towerops.Organizations.MembershipQuery - Towerops.Organizations.InvitationQuery - Towerops.Agents.AgentTokenQuery - Towerops.Gaiia.DeviceSubscriberLinkQuery Refactors callsites in alerts, agents, devices, gaiia, maintenance, on_call, organizations, search, and subscription_limits to use existing or new Query modules instead of inline 'where: x.organization_id == ^...' clauses.
155 lines
4.1 KiB
Elixir
155 lines
4.1 KiB
Elixir
defmodule Towerops.Organizations.SubscriptionLimits do
|
|
@moduledoc """
|
|
Subscription plan limits and quota enforcement.
|
|
|
|
Currently supports:
|
|
- Free plan: 10 devices max, 1 owned organization per user
|
|
- Future plans can be added here (per-device pricing, etc.)
|
|
"""
|
|
|
|
import Ecto.Query
|
|
|
|
alias Towerops.Devices.DeviceQuery
|
|
alias Towerops.Organizations.Membership
|
|
alias Towerops.Organizations.Organization
|
|
alias Towerops.Repo
|
|
|
|
@free_device_limit 10
|
|
|
|
@doc """
|
|
Returns the device limit for a given subscription plan.
|
|
|
|
## Examples
|
|
|
|
iex> device_limit("free")
|
|
10
|
|
|
|
iex> device_limit("paid")
|
|
:unlimited
|
|
"""
|
|
def device_limit("free"), do: @free_device_limit
|
|
def device_limit(_other), do: :unlimited
|
|
|
|
@doc """
|
|
Returns the effective device limit for an organization,
|
|
taking into account any custom billing override.
|
|
|
|
For paid plans, always returns `:unlimited` regardless of overrides.
|
|
For free plans, returns the custom limit if set, otherwise the default (10).
|
|
"""
|
|
@spec effective_device_limit(Organization.t()) :: integer() | :unlimited
|
|
def effective_device_limit(%Organization{} = org) do
|
|
case device_limit(org.subscription_plan) do
|
|
:unlimited -> :unlimited
|
|
default -> org.custom_free_device_limit || default
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Checks if an organization is within its device quota.
|
|
|
|
Returns `{:ok, :within_limit}` if the organization can add more devices.
|
|
Returns `{:error, :at_limit, current, max}` if at or over the limit.
|
|
|
|
## Examples
|
|
|
|
iex> check_device_limit(%Organization{subscription_plan: "free"})
|
|
{:ok, :within_limit}
|
|
|
|
iex> check_device_limit(%Organization{subscription_plan: "free"})
|
|
{:error, :at_limit, 10, 10}
|
|
"""
|
|
def check_device_limit(%Organization{} = organization) do
|
|
case effective_device_limit(organization) do
|
|
:unlimited ->
|
|
{:ok, :within_limit}
|
|
|
|
max_devices ->
|
|
current = count_organization_devices(organization.id)
|
|
|
|
if current >= max_devices do
|
|
{:error, :at_limit, current, max_devices}
|
|
else
|
|
{:ok, :within_limit}
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Returns the current device count and limit for an organization.
|
|
|
|
Returns `{current_count, limit}` where limit is either an integer or `:unlimited`.
|
|
|
|
## Examples
|
|
|
|
iex> device_quota(%Organization{id: "...", subscription_plan: "free"})
|
|
{5, 10}
|
|
|
|
iex> device_quota(%Organization{id: "...", subscription_plan: "paid"})
|
|
{45, :unlimited}
|
|
"""
|
|
def device_quota(%Organization{} = organization) do
|
|
current = count_organization_devices(organization.id)
|
|
limit = effective_device_limit(organization)
|
|
{current, limit}
|
|
end
|
|
|
|
@doc """
|
|
Checks if a user can create a free organization.
|
|
|
|
Users are limited to owning 1 free organization. They can create unlimited
|
|
paid organizations or be invited as members to unlimited organizations.
|
|
|
|
Returns `true` if the user can create another free organization, `false` otherwise.
|
|
|
|
## Examples
|
|
|
|
iex> can_create_free_organization?(user_id)
|
|
true
|
|
|
|
iex> can_create_free_organization?(user_id)
|
|
false
|
|
"""
|
|
def can_create_free_organization?(user_id) do
|
|
count_user_owned_free_organizations(user_id) < 1
|
|
end
|
|
|
|
@doc """
|
|
Counts how many free organizations a user owns.
|
|
|
|
Only counts organizations where the user has the `:owner` role.
|
|
Memberships as admin/member/viewer do not count.
|
|
|
|
## Examples
|
|
|
|
iex> count_user_owned_free_organizations(user_id)
|
|
1
|
|
|
|
iex> count_user_owned_free_organizations(user_id)
|
|
0
|
|
"""
|
|
def count_user_owned_free_organizations(user_id) do
|
|
Repo.aggregate(
|
|
from(o in Organization,
|
|
join: m in Membership,
|
|
on: m.organization_id == o.id,
|
|
where: m.user_id == ^user_id,
|
|
where: m.role == :owner,
|
|
where: o.subscription_plan == "free"
|
|
),
|
|
:count
|
|
)
|
|
end
|
|
|
|
@doc """
|
|
Counts total devices for an organization.
|
|
|
|
Counts all devices belonging to the organization, whether assigned to a site or not.
|
|
"""
|
|
def count_organization_devices(organization_id) do
|
|
Repo.aggregate(
|
|
DeviceQuery.for_organization(organization_id),
|
|
:count
|
|
)
|
|
end
|
|
end
|