In the last episode, we made it possible to complete orders. As an e-commerce site, it does us very little good to complete an order if we don't also charge the customer for it. So let's add Stripe support in.

Project

We've seen stripity-stripe before. Let's add it to our project:

vim mix.exs
def applications(_) do
  #...
  :stripity_stripe
  #...
end

def deps do
  #...
  {:stripity_stripe, "~> 1.2.0"}
end
mix deps.get

Alright, the next thing to do is to configure it. I've got my Stripe test environment variable set up already, we'll use it in the config:

vim config/config.exs
config :stripity_stripe, secret_key: System.get_env("STRIPE_SECRET_KEY")

Alright, so to charge a card is pretty easy. We'll set up a test that actually charges the customer, verify it works, and then set out to TDD the building of the arguments that we send to stripity-stripe ultimately.

We'll let the register take care of charging for the order as well as turning the cart into the order, since those are essentially two parts of the same process. Consequently, we'll put this test in the RegisterTest module:

vim test/models/register_test.exs
  # We're going to tag this test with `:external` since it talks to an external
  # service, and for now we're just going to use the Stripity Stripe API directly.
  @tag :external
  test "charging for an order at Stripe" do
    guid = Ecto.UUID.generate
    params = [
      source: [
        object: "card",
        number: "4111111111111111",
        exp_month: 10,
        exp_year: 2020,
        country: "US",
        name: "Phoenix Commerce",
        cvc: 123
      ],
      metadata: [
        guid: guid
      ]
    ]
    amount = 2_520 # in cents
    assert {:ok, charge} = Stripe.Charges.create amount, params
    {:ok, fetched_charge} = Stripe.Charges.get(charge.id)
    assert fetched_charge.metadata["guid"] == guid
    assert fetched_charge.amount == 2_520
  end

OK, so when we run this it hits the Stripe API directly. This is why we've tagged it with external. We can modify our test suite to not run the external tags by default.

vim test/test_helper.exs
ExUnit.configure(exclude: [external: true])

Now if we specify a test directly, it will still run it, but if we just run the suite it will skip all tests tagged external.

Back to the task at hand though. Right now we've got some tests in our RegisterTest module but we're not testing anything in the Register. That's because I just wanted to get something stubbed out that was talking to Stripe. Let's move some of this around.

First, we'll assume we're given the card information in the register function, but that it knows the cost based on the Cart it's checking out:

  @tag :external
  test "charging for an order at Stripe", %{cart: cart} do
    guid = Ecto.UUID.generate
    params = [
      source: [
        object: "card",
        number: "4111111111111111",
        exp_month: 10,
        exp_year: 2020,
        country: "US",
        name: "Phoenix Commerce",
        cvc: 123
      ],
      metadata: [
        guid: guid
      ]
    ]
    # We'll introduce a Register.charge function
    assert {:ok, charge} = Register.charge(cart, params)
    {:ok, fetched_charge} = Stripe.Charges.get(charge.id)
    assert fetched_charge.metadata["guid"] == guid
    assert fetched_charge.amount == 2_520
  end

Now let's open up the register and define this charge function.

  @spec charge(%Cart{}, %{}) :: {:ok, map()}
  def charge(cart=%Cart{}, params) do
    # We'll derive the amount from the cart.  We'll get to this function in a
    # bit.
    amount = cart_amount(cart)

    # We'll turn this into an integer representing the amount in cents.
    # I wish I knew a better way to do this, but so far I do not.
    amount_in_cents_d = Decimal.mult(amount, Decimal.new(100))
    {amount_in_cents, _} = Decimal.to_string(amount_in_cents_d) |> Integer.parse

    # Finally, we'll make the call to stripe to create the charge.
    Stripe.Charges.create amount_in_cents, params
  end

  # As far as actually deriving the amount, here's the first naive attempt at
  # doing this.  We'll just select the price and quantity for each line item,
  # then multiply and sum them later.
  defp cart_amount(cart=%Cart{}) do
    line_items_query =
      from li in LineItem,
      join: product in assoc(li, :product),
      where: li.cart_id == ^cart.id,
      select: [product.price, li.quantity]

    line_items = Repo.all(line_items_query)

    line_items
    |> Enum.reduce(Decimal.new("0"), fn([price, quantity], acc) ->
      quantity = Decimal.new(quantity)
      Decimal.add(acc, Decimal.mult(price, quantity))
    end)
  end
  # Honestly, I could do this in SQL with a fragment but it's not like it's an
  # amazingly concise SQL statement, and this is not likely to be very costly.
  # Consequently, I'm going to stick with this until there's a compelling reason
  # to do something more intelligent.

OK, so let's run the tests...they pass, but this isn't what we want to be passing in as the second argument - we have to know the parameters that Stripe is expecting. Let's move some of that knowledge into the register:

  @spec charge(%Cart{}, map()) :: {:ok, map()}
  def charge(cart=%Cart{}, card_params) do
    amount = cart_amount(cart)

    amount_in_cents_d = Decimal.mult(amount, Decimal.new(100))
    {amount_in_cents, _} = Decimal.to_string(amount_in_cents_d) |> Integer.parse

    params = [
      source: put_in(card_params[:object], "card")
    ]

    Stripe.Charges.create amount_in_cents, params
  end

And we'll update the test. We'll also remove the metadata piece, though I'd like to ultimately add the order id as metadata.

  @tag :external
  test "charging for an order at Stripe", %{cart: cart} do
    params = [
      number: "4111111111111111",
      exp_month: 10,
      exp_year: 2020,
      country: "US",
      name: "Phoenix Commerce",
      cvc: 123
    ]
    assert {:ok, charge} = Register.charge(cart, params)
    {:ok, fetched_charge} = Stripe.Charges.get(charge.id)
    assert fetched_charge.amount == 2_520
  end

Run the tests, and they pass.

Summary

So this is sufficient to charge for an order at Stripe, though in its current state your application will need to pass PCI compliance as your server will be handling credit cards directly. That's kind of a pain in the butt, so in the next episode we'll introduce stripe.js to avoid having any credit card information pass through our servers at all. I hope you enjoyed it. See you soon!

Resources