defmodule ArcadiaCloud.Subscriptions do @moduledoc """ Subscriptions bind a deployment to a plan version. One subscription per deployment. Addons attach with their price + qty SNAPSHOTTED, so a later catalog price change doesn't retroactively reprice an existing subscriber. """ import Ecto.Query, warn: false alias ArcadiaCloud.Repo alias ArcadiaCloud.Catalog alias ArcadiaCloud.Billing.{Subscription, SubscriptionAddon} @doc """ Create a subscription for a deployment on a plan version. Period defaults to the current calendar month. """ def create_subscription(deployment_id, plan_version_id, opts \\ []) do {period_start, period_end} = opts[:period] || current_month() %Subscription{} |> Subscription.changeset(%{ deployment_id: deployment_id, plan_version_id: plan_version_id, status: opts[:status] || "active", current_period_start: period_start, current_period_end: period_end, trial_ends_at: opts[:trial_ends_at] }) |> Repo.insert() end def get_subscription(id) do case Repo.get(Subscription, id) do nil -> nil sub -> Repo.preload(sub, [:addons, :plan_version]) end end def get_subscription_for_deployment(deployment_id) do case Repo.get_by(Subscription, deployment_id: deployment_id) do nil -> nil sub -> Repo.preload(sub, [:addons, :plan_version]) end end def list_active_subscriptions do from(s in Subscription, where: s.status == "active", preload: [:addons, :plan_version]) |> Repo.all() end def update_subscription(%Subscription{} = sub, attrs) do sub |> Subscription.changeset(attrs) |> Repo.update() end @doc """ Migrate a subscription to a different plan version (e.g. a price change, or an up/downgrade). The change takes effect from the next period unless `:immediate` is set. """ def change_plan_version(%Subscription{} = sub, new_plan_version_id) do update_subscription(sub, %{plan_version_id: new_plan_version_id}) end # ---- addons --------------------------------------------------------------- @doc """ Attach an addon to a subscription, snapshotting its current price + qty. `addon` may be an Addon struct or an addon code string. """ def attach_addon(%Subscription{id: sub_id}, addon) do addon = resolve_addon(addon) %SubscriptionAddon{} |> SubscriptionAddon.changeset(%{ subscription_id: sub_id, addon_id: addon.id, resource_kind: addon.resource_kind, qty: addon.qty, price_cents: addon.price_cents, currency: addon.currency, attached_at: DateTime.utc_now() |> DateTime.truncate(:second) }) |> Repo.insert() end def detach_addon(%SubscriptionAddon{} = sa), do: Repo.delete(sa) def list_addons(subscription_id) do from(sa in SubscriptionAddon, where: sa.subscription_id == ^subscription_id, preload: [:addon]) |> Repo.all() end defp resolve_addon(%ArcadiaCloud.Catalog.Addon{} = addon), do: addon defp resolve_addon(code) when is_binary(code) do [addon] = Catalog.get_addons([code]) addon end # ---- helpers -------------------------------------------------------------- defp current_month do today = Date.utc_today() {Date.beginning_of_month(today), Date.end_of_month(today)} end end