lib/money/subscription/plan.ex

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
end