Full Code of joshnuss/commerce_billing for AI

master 8e227b16a387 cached
18 files
28.5 KB
8.5k tokens
81 symbols
1 requests
Download .txt
Repository: joshnuss/commerce_billing
Branch: master
Commit: 8e227b16a387
Files: 18
Total size: 28.5 KB

Directory structure:
gitextract_ee89c674/

├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── config/
│   └── config.exs
├── lib/
│   ├── commerce_billing/
│   │   ├── address.ex
│   │   ├── credit_card.ex
│   │   ├── gateways/
│   │   │   ├── base.ex
│   │   │   ├── bogus.ex
│   │   │   └── stripe.ex
│   │   ├── response.ex
│   │   └── worker.ex
│   └── commerce_billing.ex
├── mix.exs
└── test/
    ├── commerce_billing_test.exs
    ├── gateways/
    │   ├── bogus_test.exs
    │   └── stripe_test.exs
    └── test_helper.exs

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
/_build
/deps
erl_crash.dump
*.ez
NOTES
docs


================================================
FILE: .travis.yml
================================================
language: elixir
elixir:
  - 1.3.0
env: MIX_ENV=test
otp_release:
  - 19.0


================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2015 Joshua Nussbaum

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
Commerce.Billing
=================
[![Build Status](https://secure.travis-ci.org/joshnuss/commerce_billing.svg?branch=master
"Build Status")](https://travis-ci.org/joshnuss/commerce_billing)

Payment processing library for Elixir. Based on [Shopify's](http://shopify.com) [ActiveMerchant](http://github.com/Shopify/active_merchant) ruby gem

## Supported Gateways

- Bogus
- Stripe

## Advantages of Elixir

- **Fault tolerant**: Each worker is supervised, so a new worker is started in the event of errors. Network errors are caught and payment is retried (not yet working).
- **Distributed**: Run workers on different machines.
- **Scalable**: Run multiple workers and adjust number of workers as needed.
- **Throughput**: Takes advantage of all cores. For example on my laptop with 4 cores (2 threads per core), I can do 100 authorizations with Stripe in 10 seconds. Thats 864,000 transactions per day. ebay does 1.4M/day.
- **Hot code swap**: Update code while the system is running

## Card processing example

```elixir
alias Commerce.Billing
alias Billing.{CreditCard, Address, Worker, Gateways}

config = %{credentials: {"sk_test_BQokikJOvBiI2HlWgH4olfQ2", ""},
           default_currency: "USD"}

Worker.start_link(Gateways.Stripe, config, name: :my_gateway)

card = %CreditCard{
  name: "John Smith",
  number: "4242424242424242",
  expiration: {2017, 12},
  cvc: "123"
}

address = %Address{
  street1: "123 Main",
  city: "New York",
  region: "NY",
  country: "US",
  postal_code: "11111"
}

case Billing.authorize(:my_gateway, 199.95, card, billing_address: address,
                                                   description: "Amazing T-Shirt") do
  {:ok,    %{authorization: authorization}} ->
    IO.puts("Payment authorized #{authorization}")

  {:error, %{code: :declined, reason: reason}} ->
    IO.puts("Payment declined #{reason}")

  {:error, %{code: error}} ->
    IO.puts("Payment error #{error}")
end
```

## Road Map

- Support multiple gateways (PayPal, Stripe, Authorize.net, Braintree etc..)
- Support gateways that bill directly and those that use html integrations.
- Support recurring billing
- Each gateway is hosted in a worker process and supervised.
- Workers can be pooled. (using poolboy)
- Workers can be spread on multiple nodes
- The gateway is selected by first calling the "Gateway Factory" process. The "Gateway Factory" decides which gateway to use. Usually it will just be one type based on configuration setting in mix.exs (i.e. Stripe), but the Factory can be replaced with something fancier. It will enable scenarios like:
    - Use one gateway for visa another for mastercard
    - Use primary gateway (i.e PayPal), but when PayPal is erroring switch to secondary/backup gateway (i.e. Authorize.net)
    - Currency specific gateway, i.e. use one gateway type for USD another for CAD
- Retry on network failure

## License

MIT

@joshnuss is a freelance software consultant. joshnuss@gmail.com


================================================
FILE: config/config.exs
================================================
# This file is responsible for configuring your application
# and its dependencies. The Mix.Config module provides functions
# to aid in doing so.
use Mix.Config

# Note this file is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project.

# Sample configuration:
#
#     config :my_dep,
#       key: :value,
#       limit: 42

# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
#     import_config "#{Mix.env}.exs"


================================================
FILE: lib/commerce_billing/address.ex
================================================
defmodule Commerce.Billing.Address do
  defstruct [:street1, :street2, :city, :region, :country, :postal_code, :phone]
end


================================================
FILE: lib/commerce_billing/credit_card.ex
================================================
defmodule Commerce.Billing.CreditCard do
  defstruct [:name, :number, :expiration, :cvc]
end


================================================
FILE: lib/commerce_billing/gateways/base.ex
================================================
defmodule Commerce.Billing.Gateways.Base do
  alias Commerce.Billing.Response

  @doc false
  defmacro __using__(_) do
    quote location: :keep do
      def purchase(_amount, _card_or_id, _opts)  do
        not_implemented
      end

      def authorize(_amount, _card_or_id, _opts)  do
        not_implemented
      end

      def capture(_id, _opts) do
        not_implemented
      end

      def void(_id, _opts) do
        not_implemented
      end

      def refund(_amount, _id, _opts) do
        not_implemented
      end

      def store(_card, _opts) do
        not_implemented
      end

      def unstore(_customer_id, _card_id, _opts) do
        not_implemented
      end

      defp http(method, path, params \\ [], opts \\ []) do
        credentials = Keyword.get(opts, :credentials)
        headers     = [{"Content-Type", "application/x-www-form-urlencoded"}]
        data        = params_to_string(params)

        HTTPoison.request(method, path, data, headers, [hackney: [basic_auth: credentials]])
      end

      defp money_to_cents(amount) when is_float(amount) do
        trunc(amount * 100)
      end

      defp money_to_cents(amount) do
        amount
      end

      defp params_to_string(params) do
        params |> Enum.filter(fn {_k, v} -> v != nil end)
               |> URI.encode_query
      end

      @doc false
      defp not_implemented do
        {:error, Response.error(code: :not_implemented)}
      end

      defoverridable [purchase: 3, authorize: 3, capture: 2, void: 2, refund: 3, store: 2, unstore: 3]
    end
  end
end


================================================
FILE: lib/commerce_billing/gateways/bogus.ex
================================================
defmodule Commerce.Billing.Gateways.Bogus do
  use Commerce.Billing.Gateways.Base

  alias Commerce.Billing.{
    CreditCard,
    Response
  }

  def authorize(_amount, _card_or_id, _opts),
    do: success

  def purchase(_amount, _card_or_id, _opts),
    do: success

  def capture(id, _opts),
    do: success(id)

  def void(id, _opts),
    do: success(id)

  def refund(_amount, id, _opts),
    do: success(id)

  def store(_card=%CreditCard{}, _opts),
    do: success

  def unstore(customer_id, nil, _opts),
    do: success(customer_id)

  def unstore(_customer_id, card_id, _opts),
    do: success(card_id)

  defp success,
    do: {:ok, Response.success(authorization: random_string)}

  defp success(id),
    do: {:ok, Response.success(authorization: id)}

  defp random_string(length \\ 10),
    do: 1..length |> Enum.map(&random_char/1) |> Enum.join

  defp random_char(_),
    do: to_string(:crypto.rand_uniform(0,9))
end


================================================
FILE: lib/commerce_billing/gateways/stripe.ex
================================================
defmodule Commerce.Billing.Gateways.Stripe do
  @base_url "https://api.stripe.com/v1"

  @cvc_code_translator %{
    "pass" => "M",
    "fail" => "N",
    "unchecked" => "P"
  }

  @avs_code_translator %{
    {"pass", "pass"} => "Y",
    {"pass", "fail"} => "A",
    {"pass", "unchecked"} => "B",
    {"fail", "pass"} => "Z",
    {"fail", "fail"} => "N",
    {"unchecked", "pass"} => "P",
    {"unchecked", "unchecked"} => "I"
  }

  use Commerce.Billing.Gateways.Base

  alias Commerce.Billing.{
    CreditCard,
    Address,
    Response
  }

  import Poison, only: [decode!: 1]

  def purchase(amount, card_or_id, opts),
    do: authorize(amount, card_or_id, [{:capture, true} | opts])

  def authorize(amount, card_or_id, opts) do
    config      = Keyword.fetch!(opts, :config)
    description = Keyword.get(opts, :description)
    address     = Keyword.get(opts, :billing_address)
    customer_id = Keyword.get(opts, :customer_id)
    currency    = Keyword.get(opts, :currency, config.default_currency)
    capture     = Keyword.get(opts, :capture, false)

    params = [capture: capture, description: description,
              currency: currency, customer: customer_id] ++
             amount_params(amount) ++
             card_params(card_or_id) ++
             address_params(address) ++
             connect_params(opts)

    commit(:post, "charges", params, opts)
  end

  def capture(id, opts) do
    params = opts
      |> Keyword.get(:amount)
      |> amount_params

    commit(:post, "charges/#{id}/capture", params, opts)
  end

  def void(id, opts),
    do: commit(:post, "charges/#{id}/refund", [], opts)

  def refund(amount, id, opts) do
    params = amount_params(amount)

    commit(:post, "charges/#{id}/refund", params, opts)
  end

  def store(card=%CreditCard{}, opts) do
    customer_id = Keyword.get(opts, :customer_id)
    params = card_params(card)

    path = if customer_id, do: "customers/#{customer_id}/card", else: "customers"

    commit(:post, path, params, opts)
  end

  def unstore(customer_id, nil, opts),
    do: commit(:delete, "customers/#{customer_id}", [], opts)

  def unstore(customer_id, card_id, opts),
    do: commit(:delete, "customers/#{customer_id}/#{card_id}", [], opts)

  defp amount_params(amount),
    do: [amount: money_to_cents(amount)]

  defp card_params(card=%CreditCard{}) do
    {expiration_year, expiration_month} = card.expiration

    ["card[number]":    card.number,
     "card[exp_year]":  expiration_year,
     "card[exp_month]": expiration_month,
     "card[cvc]":       card.cvc,
     "card[name]":      card.name]
  end

  defp card_params(id), do: [card: id]

  defp address_params(address=%Address{}) do
    ["card[address_line1]": address.street1,
     "card[address_line2]": address.street2,
     "card[address_city]":  address.city,
     "card[address_state]": address.region,
     "card[address_zip]":   address.postal_code,
     "card[address_country]": address.country]
  end

  defp address_params(_), do: []

  defp connect_params(opts),
    do: Keyword.take(opts, [:destination, :application_fee])

  defp commit(method, path, params, opts) do
    config = Keyword.fetch!(opts, :config)

    method
      |> http("#{@base_url}/#{path}", params, credentials: config.credentials)
      |> respond
  end

  defp respond({:ok, %{status_code: 200, body: body}}) do
    data = decode!(body)
    {cvc_result, avs_result} = verification_result(data)

    {:ok, Response.success(authorization: data["id"], raw: data, cvc_result: cvc_result, avs_result: avs_result)}
  end

  defp respond({:ok, %{body: body, status_code: status_code}}) do
    data = decode!(body)
    {code, reason} = error(status_code, data["error"])
    {cvc_result, avs_result} = verification_result(data)

    {:error, Response.error(code: code, reason: reason, raw: data, cvc_result: cvc_result, avs_result: avs_result)}
  end

  defp verification_result(%{"card" => card}) do
    cvc_result = @cvc_code_translator[card["cvc_check"]]
    avs_result = @avs_code_translator[{card["address_line1_check"], card["address_zip_check"]}]

    {cvc_result, avs_result}
  end

  defp verification_result(_), do: {"N","N"}

  defp error(status, _) when status >= 500,            do: {:server_error, nil}
  defp error(_, %{"type" => "invalid_request_error"}), do: {:invalid_request, nil}
  defp error(_, %{"code" => "incorrect_number"}),      do: {:declined, :invalid_number}
  defp error(_, %{"code" => "invalid_expiry_year"}),   do: {:declined, :invalid_expiration}
  defp error(_, %{"code" => "invalid_expiry_month"}),  do: {:declined, :invalid_expiration}
  defp error(_, %{"code" => "invalid_cvc"}),           do: {:declined, :invalid_cvc}
  defp error(_, %{"code" => "rate_limit"}),            do: {:rate_limit, nil}
  defp error(_, _), do: {:declined, :unknown}
end


================================================
FILE: lib/commerce_billing/response.ex
================================================
defmodule Commerce.Billing.Response do
  defstruct [:success, :authorization, :code, :reason, :avs_result, :cvc_result, :raw]

  def success(opts \\ []) do
    new(true, opts)
  end

  def error(opts \\ []) do
    new(false, opts)
  end

  defp new(success, opts) do
    Map.merge(%__MODULE__{success: success}, Enum.into(opts, %{}))
  end
end


================================================
FILE: lib/commerce_billing/worker.ex
================================================
defmodule Commerce.Billing.Worker do
  use GenServer

  def start_link(gateway, config, opts \\ []) do
    GenServer.start_link(__MODULE__, [gateway, config], opts)
  end

  def init([gateway, config]) do
    {:ok, %{config: config, gateway: gateway}}
  end

  def handle_call({:authorize, amount, card, opts}, _from, state) do
    response = state.gateway.authorize(amount, card, [{:config, state.config} | opts])
    {:reply, response, state}
  end

  def handle_call({:purchase, amount, card, opts}, _from, state) do
    response = state.gateway.purchase(amount, card, [{:config, state.config} | opts])
    {:reply, response, state}
  end

  def handle_call({:capture, id, opts}, _from, state) do
    response = state.gateway.capture(id, [{:config, state.config} | opts])
    {:reply, response, state}
  end

  def handle_call({:void, id, opts}, _from, state) do
    response = state.gateway.void(id, [{:config, state.config} | opts])
    {:reply, response, state}
  end

  def handle_call({:refund, amount, id, opts}, _from, state) do
    response = state.gateway.refund(amount, id, [{:config, state.config} | opts])
    {:reply, response, state}
  end

  def handle_call({:store, card, opts}, _from, state) do
    response = state.gateway.store(card, [{:config, state.config} | opts])
    {:reply, response, state}
  end

  def handle_call({:unstore, customer_id, card_id, opts}, _from, state) do
    response = state.gateway.unstore(customer_id, card_id, [{:config, state.config} | opts])
    {:reply, response, state}
  end
end


================================================
FILE: lib/commerce_billing.ex
================================================
defmodule Commerce.Billing do
  use Application

  import GenServer, only: [call: 2]

  # See http://elixir-lang.org/docs/stable/elixir/Application.html
  # for more information on OTP Applications
  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    children = [
      # Define workers and child supervisors to be supervised
      # worker(Commerce.Billing.Worker, [arg1, arg2, arg3])
    ]

    # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: Commerce.Billing.Supervisor]
    Supervisor.start_link(children, opts)
  end

  def authorize(worker, amount, card, opts \\ []),
    do: call(worker, {:authorize, amount, card, opts})

  def purchase(worker, amount, card, opts \\ []),
    do: call(worker, {:purchase, amount, card, opts})

  def capture(worker, id, opts \\ []),
    do: call(worker, {:capture, id, opts})

  def void(worker, id, opts \\ []),
    do: call(worker, {:void, id, opts})

  def refund(worker, amount, id, opts \\ []),
    do: call(worker, {:refund, amount, id, opts})

  def store(worker, card, opts \\ []),
    do: call(worker, {:store, card, opts})

  def unstore(worker, customer_id, card_id, opts \\ []),
    do: call(worker, {:unstore, customer_id, card_id, opts})
end


================================================
FILE: mix.exs
================================================
defmodule Commerce.Billing.Mixfile do
  use Mix.Project

  def project do
    [app: :commerce_billing,
     version: "0.0.2",
     description: "Credit card processing library",
     package: [
       contributors: ["Joshua Nussbaum"],
       licenses: ["MIT"],
       links: %{github: "https://github.com/joshnuss/commerce_billing"}
     ],
     elixir: ">= 1.2.0",
     deps: deps]
  end

  # Configuration for the OTP application
  #
  # Type `mix help compile.app` for more information
  def application do
    [applications: [:httpoison, :hackney],
     mod: {Commerce.Billing, []}]
  end

  # Dependencies can be hex.pm packages:
  #
  #   {:mydep, "~> 0.3.0"}
  #
  # Or git/path repositories:
  #
  #   {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1"}
  #
  # Type `mix help deps` for more examples and options
  defp deps do
    [{:poison, "~> 3.0"},
     {:httpoison, ">= 0.7.1"},
     {:ex_doc, ">= 0.6.0", only: :dev},
     {:mock, ">= 0.1.0", only: :test}]
  end
end


================================================
FILE: test/commerce_billing_test.exs
================================================
defmodule Commerce.BillingTest do
  use ExUnit.Case

  alias Commerce.Billing.Worker
  import Commerce.Billing

  defmodule FakeGateway do
    def authorize(100, :card, _) do
      :authorization_response
    end

    def purchase(100, :card, _) do
      :purchase_response
    end

    def capture(1234, _) do
      :capture_response
    end

    def void(1234, _) do
      :void_response
    end

    def refund(100, 1234, _) do
      :refund_response
    end

    def store(:card, _) do
      :store_response
    end

    def unstore(123, 456, _) do
      :unstore_response
    end
  end

  setup do
    {:ok, worker} = Worker.start_link(FakeGateway, :config)
    {:ok, worker: worker}
  end

  test "authorization", %{worker: worker} do
    assert authorize(worker, 100, :card, []) == :authorization_response
  end

  test "purchase", %{worker: worker} do
    assert purchase(worker, 100, :card, []) == :purchase_response
  end

  test "capture", %{worker: worker} do
    assert capture(worker, 1234, []) == :capture_response
  end

  test "void", %{worker: worker} do
    assert void(worker, 1234, []) == :void_response
  end

  test "refund", %{worker: worker} do
    assert refund(worker, 100, 1234, []) == :refund_response
  end

  test "store", %{worker: worker} do
    assert store(worker, :card, []) == :store_response
  end

  test "unstore", %{worker: worker} do
    assert unstore(worker, 123, 456, []) == :unstore_response
  end
end


================================================
FILE: test/gateways/bogus_test.exs
================================================
defmodule Commerce.Billing.Gateways.BogusTest do
  use ExUnit.Case

  alias Commerce.Billing.Response
  alias Commerce.Billing.Gateways.Bogus, as: Gateway

  test "authorize" do
    {:ok, %Response{authorization: authorization, success: success}} =
        Gateway.authorize(10.95, :card, [])

    assert success
    assert authorization != nil
  end

  test "purchase" do
    {:ok, %Response{authorization: authorization, success: success}} =
        Gateway.purchase(10.95, :card, [])

    assert success
    assert authorization != nil
  end

  test "capture" do
    {:ok, %Response{authorization: authorization, success: success}} =
        Gateway.capture(1234, [])

    assert success
    assert authorization != nil
  end

  test "void" do
    {:ok, %Response{authorization: authorization, success: success}} =
        Gateway.void(1234, [])

    assert success
    assert authorization != nil
  end

  test "store" do
    {:ok, %Response{success: success}} =
        Gateway.store(%Commerce.Billing.CreditCard{}, [])

    assert success
  end

  test "unstore with customer" do
    {:ok, %Response{success: success}} =
        Gateway.unstore(1234, nil, [])

    assert success
  end

  test "unstore with card" do
    {:ok, %Response{success: success}} =
        Gateway.unstore(nil, 456, [])

    assert success
  end
end


================================================
FILE: test/gateways/stripe_test.exs
================================================
defmodule Commerce.Billing.Gateways.StripeTest do
  use ExUnit.Case, async: false

  import Mock

  alias Commerce.Billing.{
    CreditCard,
    Address,
    Response
  }
  alias Commerce.Billing.Gateways.Stripe, as: Gateway

  defmacrop with_post(url, {status, response}, statement, do: block) do
    quote do
      {:ok, agent} = Agent.start_link(fn -> nil end)

      requestFn = fn(:post, unquote(url), params, [{"Content-Type", "application/x-www-form-urlencoded"}], [hackney: [basic_auth: {'user', 'pass'}]]) ->
        Agent.update(agent, fn(_) -> params end)
        {:ok, %{status_code: unquote(status), body: unquote(response)}}
      end

      with_mock HTTPoison, [request: requestFn] do
        unquote(statement)
        var!(params) = Agent.get(agent, &(URI.decode_query(&1)))

        unquote(block)

        Agent.stop(agent)
      end
    end
  end

  defmacrop with_delete(url, {status, response}, do: block) do
    quote do
      requestFn = fn(:delete, unquote(url), params, [{"Content-Type", "application/x-www-form-urlencoded"}], [hackney: [basic_auth: {'user', 'pass'}]]) ->
        {:ok, %{status_code: unquote(status), body: unquote(response)}}
      end

      with_mock HTTPoison, [request: requestFn], do: unquote(block)
    end
  end

  setup do
    config = %{credentials: {'user', 'pass'}, default_currency: "USD"}
    {:ok, config: config}
  end

  test "authorize success with credit card", %{config: config} do
    raw = ~S/
      {
        "id": "1234",
        "card": {
          "cvc_check": "pass",
          "address_line1_check": "unchecked",
          "address_zip_check": "pass"
        }
      }
    /
    card = %CreditCard{name: "John Smith", number: "123456", cvc: "123", expiration: {2015, 11}}
    address = %Address{street1: "123 Main", street2: "Suite 100", city: "New York", region: "NY", country: "US", postal_code: "11111"}

    with_post "https://api.stripe.com/v1/charges", {200, raw},
        response = Gateway.authorize(10.95, card, billing_address: address, config: config) do

      {:ok, %Response{authorization: authorization, success: success,
                      avs_result: avs_result, cvc_result: cvc_result}} = response

      assert success
      assert params["capture"] == "false"
      assert params["currency"] == "USD"
      assert params["amount"] == "1095"
      assert params["card[name]"] == "John Smith"
      assert params["card[number]"] == "123456"
      assert params["card[exp_month]"] == "11"
      assert params["card[exp_year]"] == "2015"
      assert params["card[cvc]"] == "123"
      assert params["card[address_line1]"] == "123 Main"
      assert params["card[address_line2]"] == "Suite 100"
      assert params["card[address_city]"] == "New York"
      assert params["card[address_state]"] == "NY"
      assert params["card[address_country]"] == "US"
      assert params["card[address_zip]"] == "11111"
      assert authorization == "1234"
      assert avs_result == "P"
      assert cvc_result == "M"
    end
  end

  test "purchase success with credit card", %{config: config} do
    raw = ~S/
      {
        "id": "1234",
        "card": {
          "cvc_check": "pass",
          "address_line1_check": "unchecked",
          "address_zip_check": "pass"
        }
      }
    /
    card = %CreditCard{name: "John Smith", number: "123456", cvc: "123", expiration: {2015, 11}}
    address = %Address{street1: "123 Main", street2: "Suite 100", city: "New York", region: "NY", country: "US", postal_code: "11111"}

    with_post "https://api.stripe.com/v1/charges", {200, raw},
        response = Gateway.purchase(10.95, card, billing_address: address, config: config) do

      {:ok, %Response{authorization: authorization, success: success,
                      avs_result: avs_result, cvc_result: cvc_result}} = response

      assert success
      assert params["capture"] == "true"
      assert params["currency"] == "USD"
      assert params["amount"] == "1095"
      assert params["card[name]"] == "John Smith"
      assert params["card[number]"] == "123456"
      assert params["card[exp_month]"] == "11"
      assert params["card[exp_year]"] == "2015"
      assert params["card[cvc]"] == "123"
      assert params["card[address_line1]"] == "123 Main"
      assert params["card[address_line2]"] == "Suite 100"
      assert params["card[address_city]"] == "New York"
      assert params["card[address_state]"] == "NY"
      assert params["card[address_country]"] == "US"
      assert params["card[address_zip]"] == "11111"
      assert authorization == "1234"
      assert avs_result == "P"
      assert cvc_result == "M"
    end
  end

  test "purchase success with credit card to a Connect account", %{config: config} do
    raw = ~S/
      {
        "id": "1234",
        "card": {
          "cvc_check": "pass",
          "address_line1_check": "unchecked",
          "address_zip_check": "pass"
        }
      }
    /
    card = %CreditCard{name: "John Smith", number: "123456", cvc: "123", expiration: {2015, 11}}
    address = %Address{street1: "123 Main", street2: "Suite 100", city: "New York", region: "NY", country: "US", postal_code: "11111"}
    destination = "stripe_id"
    application_fee = 123

    with_post "https://api.stripe.com/v1/charges", {200, raw},
        response = Gateway.purchase(10.95, card, billing_address: address, config: config, destination: destination, application_fee: application_fee) do

      {:ok, %Response{authorization: authorization, success: success,
                      avs_result: avs_result, cvc_result: cvc_result}} = response

      assert success
      assert params["capture"] == "true"
      assert params["currency"] == "USD"
      assert params["amount"] == "1095"
      assert params["card[name]"] == "John Smith"
      assert params["card[number]"] == "123456"
      assert params["card[exp_month]"] == "11"
      assert params["card[exp_year]"] == "2015"
      assert params["card[cvc]"] == "123"
      assert params["card[address_line1]"] == "123 Main"
      assert params["card[address_line2]"] == "Suite 100"
      assert params["card[address_city]"] == "New York"
      assert params["card[address_state]"] == "NY"
      assert params["card[address_country]"] == "US"
      assert params["card[address_zip]"] == "11111"
      assert params["destination"] == destination
      assert params["application_fee"] == "123"
      assert authorization == "1234"
      assert avs_result == "P"
      assert cvc_result == "M"
    end
  end

  test "capture success", %{config: config} do
    raw = ~S/{"id": "1234"}/

    with_post "https://api.stripe.com/v1/charges/1234/capture", {200, raw},
        response = Gateway.capture(1234, amount: 19.95, config: config) do

      {:ok, %Response{authorization: authorization, success: success}} = response

      assert success
      assert params["amount"] == "1995"
      assert authorization == "1234"
    end
  end

  test "void success", %{config: config} do
    raw = ~S/{"id": "1234"}/

    with_post "https://api.stripe.com/v1/charges/1234/refund", {200, raw},
        response = Gateway.void(1234, config: config) do

      {:ok, %Response{authorization: authorization, success: success}} = response

      assert success
      assert params["amount"] == nil
      assert authorization == "1234"
    end
  end

  test "refund success", %{config: config} do
    raw = ~S/{"id": "1234"}/

    with_post "https://api.stripe.com/v1/charges/1234/refund", {200, raw},
        response = Gateway.refund(19.95, 1234, config: config) do

      {:ok, %Response{authorization: authorization, success: success}} = response

      assert success
      assert params["amount"] == "1995"
      assert authorization == "1234"
    end
  end

  test "store credit card without customer", %{config: config} do
    raw = ~S/{"id": "1234"}/
    card = %CreditCard{name: "John Smith", number: "123456", cvc: "123", expiration: {2015, 11}}

    with_post "https://api.stripe.com/v1/customers", {200, raw},
        response = Gateway.store(card, config: config) do

      {:ok, %Response{authorization: authorization, success: success}} = response

      assert success
      assert params["card[name]"] == "John Smith"
      assert params["card[number]"] == "123456"
      assert params["card[exp_month]"] == "11"
      assert params["card[exp_year]"] == "2015"
      assert params["card[cvc]"] == "123"
      assert authorization == "1234"
    end
  end

  test "store credit card with customer", %{config: config} do
    raw = ~S/{"id": "1234"}/
    card = %CreditCard{name: "John Smith", number: "123456", cvc: "123", expiration: {2015, 11}}

    with_post "https://api.stripe.com/v1/customers/1234/card", {200, raw},
        response = Gateway.store(card, customer_id: 1234, config: config) do

      {:ok, %Response{authorization: authorization, success: success}} = response

      assert success
      assert params["card[name]"] == "John Smith"
      assert params["card[number]"] == "123456"
      assert params["card[exp_month]"] == "11"
      assert params["card[exp_year]"] == "2015"
      assert params["card[cvc]"] == "123"
      assert authorization == "1234"
    end
  end

  test "unstore credit card", %{config: config} do
    with_delete "https://api.stripe.com/v1/customers/123/456", {200, "{}"} do
      {:ok, %Response{success: success}} = Gateway.unstore(123, 456, config: config)

      assert success
    end
  end

  test "unstore customer", %{config: config} do
    with_delete "https://api.stripe.com/v1/customers/123", {200, "{}"} do
      {:ok, %Response{success: success}} = Gateway.unstore(123, nil, config: config)

      assert success
    end
  end
end


================================================
FILE: test/test_helper.exs
================================================
ExUnit.start()
Download .txt
gitextract_ee89c674/

├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── config/
│   └── config.exs
├── lib/
│   ├── commerce_billing/
│   │   ├── address.ex
│   │   ├── credit_card.ex
│   │   ├── gateways/
│   │   │   ├── base.ex
│   │   │   ├── bogus.ex
│   │   │   └── stripe.ex
│   │   ├── response.ex
│   │   └── worker.ex
│   └── commerce_billing.ex
├── mix.exs
└── test/
    ├── commerce_billing_test.exs
    ├── gateways/
    │   ├── bogus_test.exs
    │   └── stripe_test.exs
    └── test_helper.exs
Download .txt
SYMBOL INDEX (81 symbols across 12 files)

FILE: lib/commerce_billing.ex
  class Commerce.Billing (line 1) | defmodule Commerce.Billing
    method start (line 8) | def start(_type, _args) do
    method authorize (line 22) | def authorize(worker, amount, card, opts \\ []),
    method purchase (line 25) | def purchase(worker, amount, card, opts \\ []),
    method capture (line 28) | def capture(worker, id, opts \\ []),
    method void (line 31) | def void(worker, id, opts \\ []),
    method refund (line 34) | def refund(worker, amount, id, opts \\ []),
    method store (line 37) | def store(worker, card, opts \\ []),
    method unstore (line 40) | def unstore(worker, customer_id, card_id, opts \\ []),

FILE: lib/commerce_billing/address.ex
  class Commerce.Billing.Address (line 1) | defmodule Commerce.Billing.Address

FILE: lib/commerce_billing/credit_card.ex
  class Commerce.Billing.CreditCard (line 1) | defmodule Commerce.Billing.CreditCard

FILE: lib/commerce_billing/gateways/base.ex
  class Commerce.Billing.Gateways.Base (line 1) | defmodule Commerce.Billing.Gateways.Base

FILE: lib/commerce_billing/gateways/bogus.ex
  class Commerce.Billing.Gateways.Bogus (line 1) | defmodule Commerce.Billing.Gateways.Bogus
    method authorize (line 9) | def authorize(_amount, _card_or_id, _opts),
    method purchase (line 12) | def purchase(_amount, _card_or_id, _opts),
    method capture (line 15) | def capture(id, _opts),
    method void (line 18) | def void(id, _opts),
    method refund (line 21) | def refund(_amount, id, _opts),
    method store (line 24) | def store(_card=%CreditCard{}, _opts),
    method unstore (line 27) | def unstore(customer_id, nil, _opts),
    method unstore (line 30) | def unstore(_customer_id, card_id, _opts),
    method success (line 33) | defp success,
    method success (line 36) | defp success(id),
    method random_string (line 39) | defp random_string(length \\ 10),
    method random_char (line 42) | defp random_char(_),

FILE: lib/commerce_billing/gateways/stripe.ex
  class Commerce.Billing.Gateways.Stripe (line 1) | defmodule Commerce.Billing.Gateways.Stripe
    method purchase (line 30) | def purchase(amount, card_or_id, opts),
    method authorize (line 33) | def authorize(amount, card_or_id, opts) do
    method capture (line 51) | def capture(id, opts) do
    method void (line 59) | def void(id, opts),
    method refund (line 62) | def refund(amount, id, opts) do
    method store (line 68) | def store(card=%CreditCard{}, opts) do
    method unstore (line 77) | def unstore(customer_id, nil, opts),
    method unstore (line 80) | def unstore(customer_id, card_id, opts),
    method amount_params (line 83) | defp amount_params(amount),
    method card_params (line 86) | defp card_params(card=%CreditCard{}) do
    method card_params (line 96) | defp card_params(id), do: [card: id]
    method address_params (line 98) | defp address_params(address=%Address{}) do
    method address_params (line 107) | defp address_params(_), do: []
    method connect_params (line 109) | defp connect_params(opts),
    method commit (line 112) | defp commit(method, path, params, opts) do
    method respond (line 120) | defp respond({:ok, %{status_code: 200, body: body}}) do
    method respond (line 127) | defp respond({:ok, %{body: body, status_code: status_code}}) do
    method verification_result (line 135) | defp verification_result(%{"card" => card}) do
    method verification_result (line 142) | defp verification_result(_), do: {"N","N"}
    method error (line 145) | defp error(_, %{"type" => "invalid_request_error"}), do: {:invalid_req...
    method error (line 146) | defp error(_, %{"code" => "incorrect_number"}),      do: {:declined, :...
    method error (line 147) | defp error(_, %{"code" => "invalid_expiry_year"}),   do: {:declined, :...
    method error (line 148) | defp error(_, %{"code" => "invalid_expiry_month"}),  do: {:declined, :...
    method error (line 149) | defp error(_, %{"code" => "invalid_cvc"}),           do: {:declined, :...
    method error (line 150) | defp error(_, %{"code" => "rate_limit"}),            do: {:rate_limit,...
    method error (line 151) | defp error(_, _), do: {:declined, :unknown}

FILE: lib/commerce_billing/response.ex
  class Commerce.Billing.Response (line 1) | defmodule Commerce.Billing.Response
    method success (line 4) | def success(opts \\ []) do
    method error (line 8) | def error(opts \\ []) do
    method new (line 12) | defp new(success, opts) do

FILE: lib/commerce_billing/worker.ex
  class Commerce.Billing.Worker (line 1) | defmodule Commerce.Billing.Worker
    method start_link (line 4) | def start_link(gateway, config, opts \\ []) do
    method init (line 8) | def init([gateway, config]) do
    method handle_call (line 12) | def handle_call({:authorize, amount, card, opts}, _from, state) do
    method handle_call (line 17) | def handle_call({:purchase, amount, card, opts}, _from, state) do
    method handle_call (line 22) | def handle_call({:capture, id, opts}, _from, state) do
    method handle_call (line 27) | def handle_call({:void, id, opts}, _from, state) do
    method handle_call (line 32) | def handle_call({:refund, amount, id, opts}, _from, state) do
    method handle_call (line 37) | def handle_call({:store, card, opts}, _from, state) do
    method handle_call (line 42) | def handle_call({:unstore, customer_id, card_id, opts}, _from, state) do

FILE: mix.exs
  class Commerce.Billing.Mixfile (line 1) | defmodule Commerce.Billing.Mixfile
    method project (line 4) | def project do
    method application (line 20) | def application do
    method deps (line 34) | defp deps do

FILE: test/commerce_billing_test.exs
  class Commerce.BillingTest (line 1) | defmodule Commerce.BillingTest
  class FakeGateway (line 7) | defmodule FakeGateway
    method authorize (line 8) | def authorize(100, :card, _) do
    method purchase (line 12) | def purchase(100, :card, _) do
    method capture (line 16) | def capture(1234, _) do
    method void (line 20) | def void(1234, _) do
    method refund (line 24) | def refund(100, 1234, _) do
    method store (line 28) | def store(:card, _) do
    method unstore (line 32) | def unstore(123, 456, _) do

FILE: test/gateways/bogus_test.exs
  class Commerce.Billing.Gateways.BogusTest (line 1) | defmodule Commerce.Billing.Gateways.BogusTest

FILE: test/gateways/stripe_test.exs
  class Commerce.Billing.Gateways.StripeTest (line 1) | defmodule Commerce.Billing.Gateways.StripeTest
Condensed preview — 18 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (32K chars).
[
  {
    "path": ".gitignore",
    "chars": 45,
    "preview": "/_build\n/deps\nerl_crash.dump\n*.ez\nNOTES\ndocs\n"
  },
  {
    "path": ".travis.yml",
    "chars": 75,
    "preview": "language: elixir\nelixir:\n  - 1.3.0\nenv: MIX_ENV=test\notp_release:\n  - 19.0\n"
  },
  {
    "path": "LICENSE",
    "chars": 1081,
    "preview": "The MIT License (MIT)\nCopyright (c) 2015 Joshua Nussbaum\n\nPermission is hereby granted, free of charge, to any person ob"
  },
  {
    "path": "README.md",
    "chars": 2949,
    "preview": "Commerce.Billing\n=================\n[![Build Status](https://secure.travis-ci.org/joshnuss/commerce_billing.svg?branch=ma"
  },
  {
    "path": "config/config.exs",
    "chars": 825,
    "preview": "# This file is responsible for configuring your application\n# and its dependencies. The Mix.Config module provides funct"
  },
  {
    "path": "lib/commerce_billing/address.ex",
    "chars": 123,
    "preview": "defmodule Commerce.Billing.Address do\n  defstruct [:street1, :street2, :city, :region, :country, :postal_code, :phone]\ne"
  },
  {
    "path": "lib/commerce_billing/credit_card.ex",
    "chars": 93,
    "preview": "defmodule Commerce.Billing.CreditCard do\n  defstruct [:name, :number, :expiration, :cvc]\nend\n"
  },
  {
    "path": "lib/commerce_billing/gateways/base.ex",
    "chars": 1570,
    "preview": "defmodule Commerce.Billing.Gateways.Base do\n  alias Commerce.Billing.Response\n\n  @doc false\n  defmacro __using__(_) do\n "
  },
  {
    "path": "lib/commerce_billing/gateways/bogus.ex",
    "chars": 933,
    "preview": "defmodule Commerce.Billing.Gateways.Bogus do\n  use Commerce.Billing.Gateways.Base\n\n  alias Commerce.Billing.{\n    Credit"
  },
  {
    "path": "lib/commerce_billing/gateways/stripe.ex",
    "chars": 4811,
    "preview": "defmodule Commerce.Billing.Gateways.Stripe do\n  @base_url \"https://api.stripe.com/v1\"\n\n  @cvc_code_translator %{\n    \"pa"
  },
  {
    "path": "lib/commerce_billing/response.ex",
    "chars": 344,
    "preview": "defmodule Commerce.Billing.Response do\n  defstruct [:success, :authorization, :code, :reason, :avs_result, :cvc_result, "
  },
  {
    "path": "lib/commerce_billing/worker.ex",
    "chars": 1535,
    "preview": "defmodule Commerce.Billing.Worker do\n  use GenServer\n\n  def start_link(gateway, config, opts \\\\ []) do\n    GenServer.sta"
  },
  {
    "path": "lib/commerce_billing.ex",
    "chars": 1325,
    "preview": "defmodule Commerce.Billing do\n  use Application\n\n  import GenServer, only: [call: 2]\n\n  # See http://elixir-lang.org/doc"
  },
  {
    "path": "mix.exs",
    "chars": 1001,
    "preview": "defmodule Commerce.Billing.Mixfile do\n  use Mix.Project\n\n  def project do\n    [app: :commerce_billing,\n     version: \"0."
  },
  {
    "path": "test/commerce_billing_test.exs",
    "chars": 1448,
    "preview": "defmodule Commerce.BillingTest do\n  use ExUnit.Case\n\n  alias Commerce.Billing.Worker\n  import Commerce.Billing\n\n  defmod"
  },
  {
    "path": "test/gateways/bogus_test.exs",
    "chars": 1332,
    "preview": "defmodule Commerce.Billing.Gateways.BogusTest do\n  use ExUnit.Case\n\n  alias Commerce.Billing.Response\n  alias Commerce.B"
  },
  {
    "path": "test/gateways/stripe_test.exs",
    "chars": 9685,
    "preview": "defmodule Commerce.Billing.Gateways.StripeTest do\n  use ExUnit.Case, async: false\n\n  import Mock\n\n  alias Commerce.Billi"
  },
  {
    "path": "test/test_helper.exs",
    "chars": 15,
    "preview": "ExUnit.start()\n"
  }
]

About this extraction

This page contains the full source code of the joshnuss/commerce_billing GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 18 files (28.5 KB), approximately 8.5k tokens, and a symbol index with 81 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!