diff --git a/README.md b/README.md new file mode 100644 index 0000000..c8d27ee --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# stripe-elixir + +a Stripe wrapper for Elixir + +## Usage + +> ##### Note +> stripe-elixir requires the `STRIPE_SECRET_KEY` environment variable set to a +> valid API key associated with your Stripe account. + +```elixir +iex> Stripe.start +:ok +iex> Stripe.InvoiceItems.list +[Stripe.InvoiceItem[id: "ii_103lSZ2eZvKYlo2C6Zz1aFHv", object: "invoiceitem", + livemode: false, amount: 1000, currency: "usd", + customer: "cus_3lPWbj9wX1KqP6", date: {{2014, 3, 30}, {3, 0, 11}}, + proration: false, description: "One-time setup fee", invoice: nil, + metadata: [{}], subscription: nil], + Stripe.InvoiceItem[id: "ii_103kXf2eZvKYlo2CkRlaXEN6", object: "invoiceitem", + livemode: false, amount: 350, currency: "usd", customer: "cus_3kXfWSyHPMZOan", + date: {{2014, 3, 27}, {16, 11, 35}}, proration: false, description: nil, + invoice: "in_103kXf2eZvKYlo2CgUV8Vctw", metadata: [{}], subscription: nil], + ...] +``` + +## Reference + +See [Stripe's API docs](https://stripe.com/docs/api/). + +## Dependencies + +- [HTTPoison](https://github.com/edgurgel/httpoison) +- [JSEX](https://github.com/talentdeficit/jsex) + +## License + +See [LICENSE](https://github.com/slogsdon/stripe-elixir/blob/master/LICENSE) diff --git a/lib/stripe.ex b/lib/stripe.ex new file mode 100644 index 0000000..78488f5 --- /dev/null +++ b/lib/stripe.ex @@ -0,0 +1,83 @@ +defmodule Stripe do + @moduledoc """ + A HTTP client for Stripe. + """ + + # Let's build on top of HTTPoison + use Application.Behaviour + use HTTPoison.Base + + def start(_type, _args) do + Stripe.Supervisor.start_link + end + + @doc """ + Creates the URL for our endpoint. + Args: + * endpoint - part of the API we're hitting + Returns string + """ + def process_url(endpoint) do + "https://api.stripe.com/v1/" <> endpoint + end + + @doc """ + Set our request headers for every request. + """ + def req_headers do + HashDict.new + |> Dict.put("Authorization", "Bearer #{key}") + |> Dict.put("User-Agent", "Stripe/v1 stripe-elixir/0.1.0") + |> Dict.put("Content-Type", "application/x-www-form-urlencoded") + end + + @doc """ + Converts the binary keys in our response to atoms. + Args: + * body - string binary response + Returns Record or ArgumentError + """ + def process_response_body(body) do + JSEX.decode! body, [{:labels, :atom}] + end + + @doc """ + Boilerplate code to make requests. + Args: + * endpoint - string requested API endpoint + * body - request body + Returns dict + """ + def make_request(method, endpoint, body // [], headers // [], options // []) do + rb = Enum.map(body, &url_encode_keyvalue(&1)) + |> Enum.join("&") + rh = req_headers + |> Dict.merge(headers) + |> Dict.to_list + + response = case method do + :get -> get( endpoint, rh, options) + :put -> put( endpoint, rb, rh, options) + :head -> head( endpoint, rh, options) + :post -> post( endpoint, rb, rh, options) + :patch -> patch( endpoint, rb, rh, options) + :delete -> delete( endpoint, rh, options) + :options -> options( endpoint, rh, options) + end + + response.body + end + + @doc """ + Grabs STRIPE_SECRET_KEY from system ENV + Returns binary + """ + def key do + System.get_env "STRIPE_SECRET_KEY" + end + + defp url_encode_keyvalue({k, v}) do + key = atom_to_binary(k) + "#{key}=#{v}" + end +end diff --git a/lib/stripe/invoice_item.ex b/lib/stripe/invoice_item.ex new file mode 100644 index 0000000..90de168 --- /dev/null +++ b/lib/stripe/invoice_item.ex @@ -0,0 +1,80 @@ +defrecord Stripe.InvoiceItem, + id: nil, + object: "invoiceitem", + livemode: nil, + amount: nil, + currency: nil, + customer: nil, + date: nil, + proration: nil, + description: nil, + invoice: nil, + metadata: nil, + subscription: nil do + + @type id :: binary + @type object :: binary + @type livemode :: nil + @type amount :: nil + @type currency :: binary + @type customer :: binary + @type date :: {{1970..10000, 1..12, 1..31}, {0..23, 0..59, 0..59}} + @type proration :: nil + @type description :: binary + @type invoice :: binary + @type metadata :: Keyword.t + @type subscription :: binary + + record_type id: id, + object: object, + livemode: livemode, + amount: amount, + currency: currency, + customer: customer, + date: date, + proration: proration, + description: description, + invoice: invoice, + metadata: metadata, + subscription: subscription + + @moduledoc """ + ## Attributes + + - `id` - `String` + - `object` - `String`, value is "invoiceitem" + - `livemode` - `Boolean` + - `amount` - `Integer` + - `currency` - `String` + - `customer` - `String` + - `date` - `Tuple` + - `proration` - `Boolean` - Whether or not the invoice item was created + automatically as a proration adjustment when the customer + switched plans + - `description` - `String` + - `invoice` - `String` + - `metadata` - `Keyword` - A set of key/value pairs that you can + attach to an invoice item object. It can be useful for storing + additional information about the invoice item in a structured format. + - `subscription` - `String` - The subscription that this invoice item + has been created for, if any. + """ + + def from_keyword(data) do + datetime = Stripe.Util.datetime_from_timestamp data[:date] + Stripe.InvoiceItem.new( + id: data[:id], + object: data[:object], + livemode: data[:livemode], + amount: data[:amount], + currency: data[:currency], + customer: data[:customer], + date: datetime, + proration: data[:proration], + description: data[:description], + invoice: data[:invoice], + metadata: data[:metadata], + subscription: data[:subscription] + ) + end +end \ No newline at end of file diff --git a/lib/stripe/invoice_items.ex b/lib/stripe/invoice_items.ex new file mode 100644 index 0000000..ebf378c --- /dev/null +++ b/lib/stripe/invoice_items.ex @@ -0,0 +1,172 @@ +defmodule Stripe.InvoiceItems do + @moduledoc """ + Invoice Items + + Sometimes you want to add a charge or credit to a customer but only + actually charge the customer's card at the end of a regular billing + cycle. This is useful for combining several charges to minimize + per-transaction fees or having Stripe tabulate your usage-based + billing totals. + """ + + @endpoint "invoiceitems" + + @doc """ + Returns a list of your invoice items. Invoice Items are returned sorted + by creation date, with the most recently created invoice items appearing + first. + + ## Arguments + + - `created` - `String` | `Keyword` - (optional) - A filter on the list + based on the object created field. The value can be a string with + an exact UTC timestamp, or it can be a dictionary with the + following options: + - `gt` - `String` - (optional) - Return values where the created + field is after this timestamp. + - `gte` - `String` - (optional) - Return values where the created + field is after or equal to this timestamp. + - `lt` - `String` - (optional) - Return values where the created + field is before this timestamp. + - `lte` - `String` - (optional) - Return values where the created + field is before or equal to this timestamp. + - `customer` - `String` - (optional) - The identifier of the customer + whose invoice items to return. If none is provided, all invoice + items will be returned. + - `limit` - `Integer` - (optional), default is 10 - A limit on the number + of objects to be returned. Limit can range between 1 and 100 items. + - `offset` - `Integer` - (optional), default is 0 - An offset into the + list of returned items. The API will return the requested number of + items starting at that offset. + - `starting_after` - `String` - (optional) - A "cursor" for use in + pagination. starting_after is an object id that defines your place + in the list. For instance, if you make a list request and receive + 100 objects, ending with obj_foo, your subsequent call can include + `starting_after=obj_foo` in order to fetch the next page of the list. + + ## Returns + + A dictionary with a data property that contains an array of up to limit + invoice items, starting after invoice item starting_after. Each entry in + the array is a separate invoice item object. If no more invoice items + are available, the resulting array will be empty. This request should + never return an error. + + You can optionally request that the response include the total count of + all invoice items that match your filters. To do so, specify + `include[]=total_count` in your request. + """ + def list do + obj = Stripe.make_request :get, @endpoint + if obj[:data] do + Enum.map obj[:data], &Stripe.InvoiceItem.from_keyword(&1) + else + [] + end + end + + @doc """ + Adds an arbitrary charge or credit to the customer's upcoming invoice. + + ## Arguments + + - `customer` - `String` - (required) - The ID of the customer who will + be billed when this invoice item is billed. + - `amount` - `Integer` - (required) - The integer amount in cents of + the charge to be applied to the upcoming invoice. If you want to + apply a credit to the customer's account, pass a negative amount. + - `currency` - `String` - (required) - 3-letter ISO code for currency. + - `invoice` - `String` - (optional) - The ID of an existing invoice to + add this invoice item to. When left blank, the invoice item will be + added to the next upcoming scheduled invoice. Use this when adding + invoice items in response to an invoice.created webhook. You + cannot add an invoice item to an invoice that has already been + paid or closed. + - `subscription` - `String` - (optional) - The ID of a subscription to + add this invoice item to. When left blank, the invoice item will be + added to the next upcoming scheduled invoice. When set, scheduled + invoices for subscriptions other than the specified subscription + will ignore the invoice item. Use this when you want to express + that an invoice item has been accrued within the context of a + particular subscription. + - `description` - `String` - (optional), default is `null` - An arbitrary + string which you can attach to the invoice item. The description is + displayed in the invoice for easy tracking. + - `metadata` - `Keyword` - (optional), default is `[]` - A set of + key/value pairs that you can attach to an invoice item object. It can + be useful for storing additional information about the invoice item in + a structured format. + + ## Returns + + The created invoice item object is returned if successful. Otherwise, + this call returns an error. + """ + def create(params) do + obj = Stripe.make_request :post, @endpoint, params + Stripe.InvoiceItem.from_keyword obj + end + + @doc """ + Retrieves the invoice item with the given ID. + + ## Arguments + + - `id` - `String` - (required) - The ID of the desired invoice item. + + ## Returns + + Returns an invoice item if a valid invoice item ID was provided. Returns + an error otherwise. + """ + def retrieve(id) do + obj = Stripe.make_request :get, @endpoint <> "/#{id}" + Stripe.InvoiceItem.from_keyword obj + end + + @doc """ + Updates the amount or description of an invoice item on an upcoming invoice. + Updating an invoice item is only possible before the invoice it's attached + to is closed. + + ## Arguments + + - `amount` - `Integer` - (required) - The integer amount in cents of + the charge to be applied to the upcoming invoice. If you want to + apply a credit to the customer's account, pass a negative amount. + - `description` - `String` - (optional), default is `null` - An arbitrary + string which you can attach to the invoice item. The description is + displayed in the invoice for easy tracking. + - `metadata` - `Keyword` - (optional), default is `[]` - A set of + key/value pairs that you can attach to an invoice item object. It can + be useful for storing additional information about the invoice item in + a structured format. + + ## Returns + + The updated invoice item object is returned upon success. Otherwise, this + call returns an error. + """ + def update(params) do + obj = Stripe.make_request :post, @endpoint <> "/#{params[:id]}", params + Stripe.InvoiceItem.from_keyword obj + end + + @doc """ + Removes an invoice item from the upcoming invoice. Removing an invoice + item is only possible before the invoice it's attached to is closed. + + ## Arguments + + - `id` - `String` - (required) - The ID of the desired invoice item. + + ## Returns + + An object with the deleted invoice item's ID and a deleted flag upon + success. This call returns an error otherwise, such as when the invoice + item has already been deleted. + """ + def delete(id) do + Stripe.make_request :delete, @endpoint <> "/#{id}" + end +end diff --git a/lib/stripe/supervisor.ex b/lib/stripe/supervisor.ex new file mode 100644 index 0000000..6f76690 --- /dev/null +++ b/lib/stripe/supervisor.ex @@ -0,0 +1,18 @@ +defmodule Stripe.Supervisor do + use Supervisor.Behaviour + + def start_link do + :supervisor.start_link(__MODULE__, []) + end + + def init([]) do + children = [ + # Define workers and child supervisors to be supervised + # worker(Stripe.Worker, []) + ] + + # See http://elixir-lang.org/docs/stable/Supervisor.Behaviour.html + # for other strategies and supported options + supervise(children, strategy: :one_for_one) + end +end \ No newline at end of file diff --git a/lib/stripe/util.ex b/lib/stripe/util.ex new file mode 100644 index 0000000..9661f7f --- /dev/null +++ b/lib/stripe/util.ex @@ -0,0 +1,6 @@ +defmodule Stripe.Util do + def datetime_from_timestamp(ts) do + {{year, month, day}, {hour, minutes, seconds}} = :calendar.gregorian_seconds_to_datetime ts + {{year + 1970, month, day}, {hour, minutes, seconds}} + end +end \ No newline at end of file diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..fc8679a --- /dev/null +++ b/mix.exs @@ -0,0 +1,28 @@ +defmodule Stripe.Mixfile do + use Mix.Project + + def project do + [ app: :stripe, + version: "0.1.0", + elixir: "~> 0.12.1", + deps: deps ] + end + + # Configuration for the OTP application + def application do + [mod: { Stripe, [] }] + end + + # Returns the list of dependencies in the format: + # { :foobar, git: "https://github.com/elixir-lang/foobar.git", tag: "0.1" } + # + # To specify particular versions, regardless of the tag, do: + # { :barbat, "~> 0.1", github: "elixir-lang/barbat" } + defp deps do + [ + { :hackney_lib, github: "benoitc/hackney_lib", tag: "0.2.2", override: true }, + { :httpoison, github: "edgurgel/httpoison", tag: "0.0.1" }, + { :jsex, github: "talentdeficit/jsex"} + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..fa1627a --- /dev/null +++ b/mix.lock @@ -0,0 +1,6 @@ +[ "hackney": {:git, "git://github.com/benoitc/hackney.git", "7f2a0b5996313d930277786e7ff5d538e9346547", [{:tag, "0.10.1"}]}, + "hackney_lib": {:git, "git://github.com/benoitc/hackney_lib.git", "eecdb481f1d38bfb194896bffdf9bdcdd967ae96", [{:tag, "0.2.2"}]}, + "httpoison": {:git, "git://github.com/edgurgel/httpoison.git", "376bf1f254e260205dd573e4788db364b2c34b12", [{:tag, "0.0.1"}]}, + "jsex": {:git, "git://github.com/talentdeficit/jsex.git", "c9df36f07b2089a73ab6b32074c01728f1e5a2e1", []}, + "jsx": {:git, "git://github.com/talentdeficit/jsx.git", "e50af6e109cb03bd26acf715cbc77de746507d1d", [{:tag, "v1.4.3"}]}, + "mimetypes": {:git, "git://github.com/spawngrid/mimetypes.git", "47d37a977a7d633199822bf6b08353007483d00f", [{:ref, "master"}]} ] diff --git a/test/stripe_test.exs b/test/stripe_test.exs new file mode 100644 index 0000000..d5f2ee0 --- /dev/null +++ b/test/stripe_test.exs @@ -0,0 +1,7 @@ +defmodule StripeTest do + use ExUnit.Case + + test "the truth" do + assert(true) + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..4b8b246 --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start