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
=================
[](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()
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
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[ 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.