towerops/lib/towerops/organizations/subscription_limits.ex
Graham McIntire 2528e78ff9 refactor: extract per-schema Query modules for organization_id filters
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.
2026-04-30 14:26:30 -05:00

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