defmodule Money.Subscription.Plan do @moduledoc """ Defines a standard subscription plan data structure. """ @typedoc "A plan interval type." @type interval :: :day | :week | :month | :year @typedoc "A integer interval count for a plan." @type interval_count :: non_neg_integer @typedoc "A Subscription Plan" @type t :: %__MODULE__{ price: Money.t() | nil, interval: interval, interval_count: interval_count } @doc """ Defines the structure of a subscription plan. """ defstruct price: nil, interval: nil, interval_count: nil @interval [:day, :week, :month, :year] @doc """ Returns `{:ok, Money.Subscription.Plan.t}` or an `{:error, reason}` tuple. ### Arguments * `:price` is any `Money.t` * `:interval` is the period of the plan. The valid intervals are ` `:day`, `:week`, `:month` or ':year`. * `:interval_count` is an integer count of the number of `:interval`s of the plan. The default is `1` ### Returns A `Money.Subscription.Plan.t` ### Examples iex> Money.Subscription.Plan.new Money.new(:USD, 100), :month, 1 {:ok, %Money.Subscription.Plan{ interval: :month, interval_count: 1, price: Money.new(:USD, 100) }} iex> Money.Subscription.Plan.new Money.new(:USD, 100), :month {:ok, %Money.Subscription.Plan{ interval: :month, interval_count: 1, price: Money.new(:USD, 100) }} iex> Money.Subscription.Plan.new Money.new(:USD, 100), :day, 30 {:ok, %Money.Subscription.Plan{ interval: :day, interval_count: 30, price: Money.new(:USD, 100) }} iex> Money.Subscription.Plan.new 23, :day, 30 {:error, {Money.Invalid, "Invalid subscription plan definition"}} """ @spec new(Money.t(), interval(), interval_count()) :: {:ok, t()} | {:error, {module(), String.t()}} def new(price, interval, interval_count \\ 1) def new(%Money{} = price, interval, interval_count) when interval in @interval and is_integer(interval_count) do {:ok, %__MODULE__{price: price, interval: interval, interval_count: interval_count}} end def new(_price, _interval, _interval_count) do {:error, {Money.Invalid, "Invalid subscription plan definition"}} end @doc """ Returns `{:ok, Money.Subscription.Plan.t}` or raises an exception. Takes the same arguments as `Money.Subscription.Plan.new/3`. ##@ Example iex> Money.Subscription.Plan.new! Money.new(:USD, 100), :day, 30 %Money.Subscription.Plan{ interval: :day, interval_count: 30, price: Money.new(:USD, 100) } """ @spec new!(Money.t(), interval(), interval_count()) :: t() | no_return() def new!(price, interval, interval_count \\ 1) def new!(price, interval, interval_count) do case new(price, interval, interval_count) do {:ok, plan} -> plan {:error, {exception, reason}} -> raise exception, reason end end if Code.ensure_loaded?(Cldr.Unit) do import Kernel, except: [to_string: 1] @doc """ Return a localised string representation of a subscription plan. ### Arguments * Any `t:Money.Subscription.Plan.t/0` as returned from `Money.Subscription.Plan.new/3`. * `options` is a keyword list of options. ### Options * See `Cldr.Unit.to_string/1` for available options. ### Returns * `{:ok, localized_string}` or * `{:error, reason}` ### Examples iex> {:ok, plan} = Money.Subscription.Plan.new(Money.new(:USD, 10), :year) iex> Money.Subscription.Plan.to_string(plan) {:ok, "$10.00 per year"} iex> Money.Subscription.Plan.to_string(plan, locale: :ja) {:ok, "$10.00毎年"} iex> Money.Subscription.Plan.to_string(plan, locale: :de, style: :narrow) {:ok, "10,00\u00A0$/J"} iex> {:ok, plan} = Money.Subscription.Plan.new(Money.new(:USD, 10), :day, 30) iex> Money.Subscription.Plan.to_string(plan) {:ok, "$10.00 per 30 days"} iex> Money.Subscription.Plan.to_string(plan, locale: :de) {:ok, "10,00\u00A0$ pro 30 Tage"} iex> Money.Subscription.Plan.to_string(plan, locale: :de, style: :short) {:ok, "10,00\u00A0$/30 Tg."} """ @doc since: "5.22.0" def to_string(%__MODULE__{} = plan, options \\ []) do backend = Keyword.get_lazy(options, :backend, &Money.default_backend/0) plan |> unit_from_plan() |> Cldr.Unit.new!(plan.price.amount) |> Cldr.Unit.to_string(backend, options) end def to_string!(%__MODULE__{} = plan, options \\ []) do case to_string(plan, options) do {:ok, string} -> string {:error, {exception, reason}} -> raise exception, reason end end defp unit_from_plan(%__MODULE__{interval_count: 1} = plan) do %{price: amount, interval: interval} = plan "curr-#{amount.currency}-per-#{interval}" end defp unit_from_plan(%__MODULE__{interval_count: interval_count} = plan) do %{price: amount, interval: interval} = plan "curr-#{amount.currency}-per-#{interval_count}-#{interval}" end end end