recurly v0.2.2 Recurly

Getting Started

This documentation is a work in progress. Please consider contributing.

For now, Read each section on this page. After reading these sections, these modules may be a good place to start digging.

Resources

Resources are structs that represent a server object at a given point in time. Like all elixir variables, they are immutable values. A resource consists of 3 parts:

  • Fields
  • Actions
  • Associations
subscription
# %Recurly.Subscription{
#   __meta__: %{
#     actions: %{
#       cancel: [:put, "https://api.recurly.com/v2/subscriptions/37e3a404f2c9b0edde8bbf4c7aa6a561/cancel"],
#       notes: [:put,"https://api.recurly.com/v2/subscriptions/37e3a404f2c9b0edde8bbf4c7aa6a561/notes"],
#       postpone: [:put,"https://api.recurly.com/v2/subscriptions/37e3a404f2c9b0edde8bbf4c7aa6a561/postpone"],
#       terminate: [:put,"https://api.recurly.com/v2/subscriptions/37e3a404f2c9b0edde8bbf4c7aa6a561/terminate"]
#     },
#     href: "https://api.recurly.com/v2/subscriptions/37e3a404f2c9b0edde8bbf4c7aa6a561"
#   },
#   account: %Recurly.Association{href: "https://api.recurly.com/v2/accounts/my_account_code",
#     paginate: false, resource_type: Recurly.Account},
#   currency: "USD",
#   plan: %Recurly.Plan{
#     __meta__: %{
#       href: "https://api.recurly.com/v2/plans/myplancode"
#     },
#     accounting_code: nil,
#     cancel_url: nil,
#     display_quantity: nil,
#     name: "A plan",
#     plan_code: "myplancode",
#     plan_interval_length: nil,
#     plan_interval_unit: nil,
#     revenue_schedule_type: nil,
#     setup_fee_accounting_code: nil,
#     setup_fee_in_cents: nil,
#     setup_fee_revenue_schedule_type: nil,
#     success_url: nil,
#     tax_code: nil,
#     tax_exempt: nil,
#     total_billing_cycles: nil,
#     trial_interval_length: nil,
#     trial_interval_unit: nil,
#     unit_amount_in_cents: nil,
#     unit_name: nil
#   },
#   plan_code: nil,
#   quantity: 1,
#   state: "active",
#   subscription_add_ons: [],
#   tax_in_cents: nil,
#   tax_rate: nil,
#   tax_region: nil,
#   tax_type: nil,
#   unit_amount_in_cents: 100,
#   uuid: "37e3a404f2c9b0edde8bbf4c7aa6a561"
# }

The schema for each resource is defined in a module with the resource’s name. As an example: Recurly.Account.

Every resource has a reference to it’s module and thus it’s schema. But how schemas work internally can largely be ignored by the programmer.

  Recurly.XML.Schema.get(Recurly.Account)
  # %Recurly.XML.Schema{fields: [%Recurly.XML.Field{name: :accept_language,
  #    opts: [], type: :string},
  #   %Recurly.XML.Field{name: :account_code, opts: [], type: :string},
  #   %Recurly.XML.Field{name: :address, opts: [], type: Recurly.Address},
  #   %Recurly.XML.Field{name: :billing_info, opts: [], type: Recurly.BillingInfo},
  #   %Recurly.XML.Field{name: :cc_emails, opts: [], type: :string},
  #   %Recurly.XML.Field{name: :company_name, opts: [], type: :string},
  #   %Recurly.XML.Field{name: :email, opts: [], type: :string},
  #   %Recurly.XML.Field{name: :entity_use_code, opts: [], type: :string},
  #   %Recurly.XML.Field{name: :first_name, opts: [], type: :string},
  #   %Recurly.XML.Field{name: :last_name, opts: [], type: :string},
  #   %Recurly.XML.Field{name: :state, opts: [read_only: true], type: :string},
  #   %Recurly.XML.Field{name: :tax_exempt, opts: [], type: :boolean},
  #   %Recurly.XML.Field{name: :transactions, opts: [paginate: true],
  #    type: Recurly.Transaction},
  #   %Recurly.XML.Field{name: :username, opts: [], type: :string},
  #   %Recurly.XML.Field{name: :vat_number, opts: [], type: :string}],
  #  resource_type: Recurly.Account}

Changesets

At no point should you need to modify the resource structs in memory. In order to create or modify a resource, you must create a changeset and send that to the server.

A changeset is represented as a nested Keyword list. You must use the changeset to create or modify data. Consider the simplest case:

{:ok, account} = Recurly.Account.create(account_code: "myaccountcode")
# {:ok,
#   %Recurly.Account{__meta__: %{actions: %{},
#           href: "https://subdomain.recurly.com/v2/accounts/myaccountcode"},
#           account_code: "myaccountcode", billing_info: nil, cc_emails: nil,
#             company_name: nil, email: nil, first_name: nil, last_name: nil,
#               state: "active", tax_exempt: nil, username: nil, vat_number: nil}}

The Recurly.Account.create/1 function takes only a changeset as an argument and returns the created account. Only the account_code is needed in this case.

Consider a more complicated case with nested data:

changeset_data = [
  plan_code: "myplancode",
  currency: "USD",
   account: [
    account_code: "myotheraccountcode",
    billing_info: [
      address1: " 400 Alabama St",
      city: " San Francisco",
      state: "CA",
      zip: "94110",
      number: "4111-1111-1111-1111",
      verification_value: "123",
      first_name: "Benjamin",
      last_name: "Person",
      month: "05",
      year: 2019,
      country: "US"
    ]
   ],
   subscription_add_ons: [
     subscription_add_on: [add_on_code: "myaddon", quantity: 1]
   ]
]

{:ok, subscription} = Recurly.Subscription.create(changeset_data)

# {:ok,
#  %Recurly.Subscription{__meta__: %{actions: %{cancel: [:put,
#        "https://subdomain.recurly.com/v2/subscriptions/37e068b0bc916763655db141b194e626/cancel"],
#       notes: [:put,
#        "https://subdomain.recurly.com/v2/subscriptions/37e068b0bc916763655db141b194e626/notes"],
#       postpone: [:put,
#        "https://subdomain.recurly.com/v2/subscriptions/37e068b0bc916763655db141b194e626/postpone"],
#       terminate: [:put,
#        "https://subdomain.recurly.com/v2/subscriptions/37e068b0bc916763655db141b194e626/terminate"]},
#     href: "https://subdomain.recurly.com/v2/subscriptions/37e068b0bc916763655db141b194e626"},
#   account: %Recurly.Association{href: "https://subdomain.recurly.com/v2/accounts/nsnsnsns",
#    paginate: false, resource_type: Recurly.Account}, currency: "USD",
#   plan: %Recurly.Plan{__meta__: %{href: "https://subdomain.recurly.com/v2/plans/myplancode"},
#    name: "A plan", plan_code: "myplancode", setup_fee_in_cents: nil,
#    unit_amount_in_cents: nil}, plan_code: nil, quantity: 1, state: "active",
#   subscription_add_ons: [%Recurly.SubscriptionAddOn{__meta__: %{href: nil},
#     add_on_code: "myaddon", quantity: 1,
#     unit_amount_in_cents: %Recurly.Money{AUD: nil, BRL: nil, CAD: nil, CHF: nil,
#     CZK: nil, DKK: nil, EUR: nil, GBP: nil, HUF: nil, ILS: nil, INR: nil,
#     MXN: nil, NOK: nil, NZD: nil, PLN: nil, SEK: nil, SGD: nil, USD: nil,
#     ZAR: nil, __meta__: %{href: nil}}}],
#   tax_in_cents: nil, tax_rate: nil, tax_region: nil,
#   tax_type: nil, unit_amount_in_cents: 100,
#   uuid: "37e068b0bc916763655db141b194e626"}}

As you can see, the billing_info is nested in the account which is nested in the subscription. This is nearly a 1x1 mapping of the XML request that is generated. Keep in mind that you must use keyword lists and never maps for changesets as maps do not support duplicate keys.

If any of the keys in your changeset data aren’t recognized, you will get an ArgumentError. This will prevent you from misspelling or sending incorrect data.

Actions

Actions are modifying actions that can be performed on a resource outside of the normal CRUD. Consider the subscription from above. It has a map of actions that can be performed:

subscription
#  %Recurly.Subscription{__meta__: %{actions: %{cancel: [:put,
#        "https://subdomain.recurly.com/v2/subscriptions/37e068b0bc916763655db141b194e626/cancel"],
#       notes: [:put,
#        "https://subdomain.recurly.com/v2/subscriptions/37e068b0bc916763655db141b194e626/notes"],
#       postpone: [:put,
#        "https://subdomain.recurly.com/v2/subscriptions/37e068b0bc916763655db141b194e626/postpone"],
#       terminate: [:put,
#        "https://subdomain.recurly.com/v2/subscriptions/37e068b0bc916763655db141b194e626/terminate"]},
#     href: "https://subdomain.recurly.com/v2/subscriptions/37e068b0bc916763655db141b194e626"},
#     ......

The Recurly.Subscription module allows us to perform these actions with helper functions:

{:ok, subscription} = Recurly.Subscription.cancel(subscription)

Or we can use Recurly.Resource.perform_action/2 with an atom and call the action directly:

{:ok, subscription} = Recurly.Resource.perform_action(subscription, :cancel)

Associations

See Recurly.Association for documentation on how to use associations.

Resource Streams

Instead of dealing with pagination, you can fetch resources as a stream (following elixir’s Stream protocol).

See Recurly.Resource.stream/3 for documentation on how to use streams.

Error Handling

All network bound calls which may result in an error follow a similar API:

case Recurly.Account.create(account_code: "myaccountcode") do
  {:ok, account} -> # *account* is the account created on the server
  {:error, error} -> # *error* is an error struct
end

The error value may be one of the following types:

A benefit of this API is that it allows fine grained pattern matching against errors cases. As an example, consider that you want to detect the case in which the account code is taken:

alias Recurly.ValidationError

case Recurly.Account.create(account_code: "myaccountcode") do
  {:ok, account} ->
    # account code was not taken and the account was created
  {:error, %ValidationError{errors: [%{symbol: :taken}]}} ->
    # the account code was taken
  {:error, error} ->
    # a fallback case
end

Pattern matching function arguments is also a nice way to exploit this property:

defmodule MyModule do
  def fetch(account_code) do
    account_code
    |> Recurly.Account.find
    |> handle_response
  end

  def handle_response({:ok, account}) do
    # account code was not used and the account was created
  end
  def handle_response({:error, %Recurly.NotFoundError{description: description}) do
    # description => Couldn't find Account with account_code = nonexistentcode
  end
  def handle_response(error) do
    # a fallback case
  end
end

MyModule.fetch("nonexistentcode")

It’s a good idea to always replace the value with any updated states from the server so you never have old state lying around:

{:ok, account} = Recurly.Account.create(account_code: "myaccountcode")
{:ok, account} = Recurly.Account.update(account, first_name: "Benjamin")

If you need to remove an attribute you can send nils to the server:

{:ok, account} = Recurly.Account.update(account, [
  username: nil,
  first_name: "Benjamin"
])

This generates the XML:

<acccount>
  <username nil="nil"/>
  <first_name>Benjamin</first_name>
</acccount>