lib/subscription.ex

defmodule Braintree.Subscription do
  @moduledoc """
  Manage customer subscriptions to recurring billing plans.

  For additional reference see:
  https://developers.braintreepayments.com/reference/request/subscription/create/ruby
  """

  use Braintree.Construction

  alias Braintree.ErrorResponse, as: Error
  alias Braintree.{AddOn, HTTP, Search, Transaction}

  @type t :: %__MODULE__{
          id: String.t(),
          plan_id: String.t(),
          balance: String.t(),
          billing_day_of_month: String.t(),
          billing_period_end_date: String.t(),
          billing_period_start_date: String.t(),
          created_at: String.t(),
          current_billing_cycle: String.t(),
          days_past_due: String.t(),
          descriptor: String.t(),
          failure_count: String.t(),
          first_billing_date: String.t(),
          merchant_account_id: String.t(),
          never_expires: String.t(),
          next_bill_amount: String.t(),
          next_billing_date: String.t(),
          next_billing_period_amount: String.t(),
          number_of_billing_cycles: String.t(),
          paid_through_date: String.t(),
          payment_method_token: String.t(),
          price: String.t(),
          status: String.t(),
          trial_duration: String.t(),
          trial_duration_unit: String.t(),
          trial_period: String.t(),
          updated_at: String.t(),
          add_ons: [AddOn.t()],
          discounts: [any],
          transactions: [Transaction.t()],
          status_history: [any]
        }

  defstruct id: nil,
            plan_id: nil,
            balance: nil,
            billing_day_of_month: nil,
            billing_period_end_date: nil,
            billing_period_start_date: nil,
            created_at: nil,
            current_billing_cycle: nil,
            days_past_due: nil,
            descriptor: nil,
            failure_count: nil,
            first_billing_date: nil,
            merchant_account_id: nil,
            never_expires: nil,
            next_bill_amount: nil,
            next_billing_date: nil,
            next_billing_period_amount: nil,
            number_of_billing_cycles: nil,
            paid_through_date: nil,
            payment_method_token: nil,
            price: nil,
            status: nil,
            trial_duration: nil,
            trial_duration_unit: nil,
            trial_period: nil,
            updated_at: nil,
            add_ons: [],
            discounts: [],
            transactions: [],
            status_history: []

  @doc """
  Create a subscription, or return an error response with after failed
  validation.

  ## Example

      {:ok, sub} = Braintree.Subscription.create(%{
        payment_method_token: card.token,
        plan_id: "starter"
      })
  """
  @spec create(map, Keyword.t()) :: {:ok, t} | {:error, Error.t()}
  def create(params \\ %{}, opts \\ []) do
    with {:ok, payload} <- HTTP.post("subscriptions", %{subscription: params}, opts) do
      {:ok, new(payload)}
    end
  end

  @doc """
  Find an existing subscription by `subscription_id`

  ## Example

      {:ok, subscription} = Subscription.find("123")
  """
  @spec find(String.t(), Keyword.t()) :: {:ok, t} | {:error, Error.t()}
  def find(subscription_id, opts \\ []) do
    with {:ok, payload} <- HTTP.get("subscriptions/#{subscription_id}", opts) do
      {:ok, new(payload)}
    end
  end

  @doc """
  Cancel an existing subscription by `subscription_id`. A cancelled subscription
  cannot be reactivated, you would need to create a new one.

  ## Example

      {:ok, subscription} = Subscription.cancel("123")
  """
  @spec cancel(String.t(), Keyword.t()) :: {:ok, t} | {:error, Error.t()}
  def cancel(subscription_id, opts \\ []) do
    with {:ok, payload} <- HTTP.put("subscriptions/#{subscription_id}/cancel", opts) do
      {:ok, new(payload)}
    end
  end

  @doc """
  You can manually retry charging past due subscriptions.

  By default, we will use the subscription balance when retrying the
  transaction. If you would like to use a different amount you can optionally
  specify the amount for the transaction.

  A successful manual retry of a past due subscription will **always** reduce
  the balance of that subscription to $0, regardless of the amount of the
  retry.

  ## Example

      {:ok, transaction} = Braintree.Subscription.retry_charge(sub_id)
      {:ok, transaction} = Braintree.Subscription.retry_charge(sub_id, "24.00")
  """
  @spec retry_charge(String.t()) :: {:ok, Transaction.t()}
  @spec retry_charge(String.t(), String.t() | nil, Keyword.t()) ::
          {:ok, Transaction.t()} | {:error, Error.t()}
  def retry_charge(subscription_id, amount \\ nil, opts \\ []) do
    Transaction.sale(%{amount: amount, subscription_id: subscription_id}, opts)
  end

  @doc """
  To update a subscription, use its ID along with new attributes. The same
  validations apply as when creating a subscription. Any attribute not passed will
  remain unchanged.

  ## Example

      {:ok, subscription} = Braintree.Subscription.update("subscription_id", %{
        plan_id: "new_plan_id"
      })
      subscription.plan_id # "new_plan_id"
  """
  @spec update(binary, map, Keyword.t()) :: {:ok, t} | {:error, Error.t()}
  def update(id, params, opts \\ []) when is_binary(id) and is_map(params) do
    with {:ok, payload} <- HTTP.put("subscriptions/" <> id, %{subscription: params}, opts) do
      {:ok, new(payload)}
    end
  end

  @doc """
  To search for subscriptions, pass a map of search parameters.

  ## Example:

    {:ok, subscriptions} = Braintree.Subscription.search(%{plan_id: %{is: "starter"}})
  """
  @spec search(map, Keyword.t()) :: {:ok, t} | {:error, Error.t()}
  def search(params, opts \\ []) when is_map(params) do
    Search.perform(params, "subscriptions", &new/1, opts)
  end

  @doc """
  Convert a map into a Subscription struct. Add_ons and transactions
  are converted to a list of structs as well.

  ## Example

      subscripton = Braintree.Subscription.new(%{"plan_id" => "business",
                                                 "status" => "Active"})
  """
  def new(%{"subscription" => map}) do
    new(map)
  end

  def new(map) when is_map(map) do
    subscription = super(map)

    add_ons = AddOn.new(subscription.add_ons)
    transactions = Transaction.new(subscription.transactions)

    %{subscription | add_ons: add_ons, transactions: transactions}
  end

  def new(list) when is_list(list) do
    Enum.map(list, &new/1)
  end
end