3. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
Visited Leipzig back in 2005
for a conference
Took the high speed train
to Paris
Hello Hamburg!
4. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
B.A.Sc in software engineering
M.Sc. and PhD in computer science
Worked with Deloitte, IBM, Canadian Federal Government
PHP, C#, Java, Ruby, Clojure
Elixir part-time only,
but since the beginning
(3 tiny PRs in the elixir core)
Part-time professor at the University of Ottawa
Two kids, August (4) and Hayden (6)
Married 13 years to Ayana
Currently working at CrossFit
(remotely, HQ located in California, USA)
Andrew Forward
aforward@gmail.com
@a4word
a4word.com
github.com/aforward
13. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
Enter user information
Share directly with Stripe
Returns unguessable token
Send payment
token to your server
Apply charge
to Stripe
account
14. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
https://stripe.com/docs/security
Keep CC information off your computer*
22. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
Returns unguessable token
{
"id": "tok_1A3QMREz7Te5wka71D40vfk1",
"card": {
"id": "card_1A3QMREz7Te5wka7XSzH1osR",
"name": "aforward@gmail.com",
"brand": "Visa",
"last4": "1111",
"object": "card",
"country": "US",
"funding": "unknown",
"exp_year": "2019",
"cvc_check": "pass",
"exp_month": "12",
"address_zip": "",
"address_city": "",
"address_line1": "",
"address_line2": "",
"address_state": "",
"dynamic_last4": "",
"address_country": "",
"address_zip_check": "",
"address_line1_check": "",
"tokenization_method": ""
},
"type": "card",
"used": "false",
"email": "aforward@gmail.com",
"object": "token",
"created": "1490969059",
"livemode": "false",
"client_ip": "69.159.206.244"
}
No sensitive CC information
The unguessable token
Amount is not provided, as
we have only requested authority
to make the charge
23. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
Send payment
token to your server
token: function(stripeToken) {
$.ajax({
type: "POST",
url: "api/tokens",
data: {token: {"stripe": stripeToken,
"invoice": entity.invoiceData}},
});
}
Forward on the
token to your server
for processing
That unguessable token
For this example, we are also passing
along an invoice full of details about
the payment
24. {
"stripe": {
"id": "tok_1A3QMREz7Te5wka71D40vfk1",
},
"invoice": {
"name": “Payment Gateway
"currency": "cad",
"description": "3 woggles"
}
}
SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
Append invoice information as well
to more easily apply the payment
Send payment
token to your server
Pass along the stripe token (other
fields omitted here for brevity)
Anything client side is manipulable
from, well, the client. So beware!
25. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
Apply charge to
Stripe account
iex(1)> StripePost.post(
...(1)> "https://api.stripe.com/v1/charges",
...(1)> "amount=10000¤cy=cad&description=3+wozzle&source=pk_abc_123",
...(1)> [{"Authorization", "Bearer abc123"},
...(1)> {"Content-Type", "application/x-www-form-urlencoded"}])
Another REST call to Stripe APIs, but
now done in Elixir on the server
Endpoint for making charges
URL encoded data
Your stripe “sensitive” key
(only for server side transactions)
29. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
https://github.com/aforward/stripe-post
30. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
@doc"""
Post a message to the Stripe API by providing all the necessary
information. The answer will be
If successful
{status_code, body}
Under error
{:error, reason}
"""
def post(url, body, headers) do
case HTTPoison.post(url, body, headers) do
{:ok, %HTTPoison.Response{status_code: status_code, body: body}} ->
{status_code, Poison.decode!(body)}
{:error, %HTTPoison.Error{reason: reason}} ->
{:error, reason}
end
end
Stripe endpoint
URL encoded data
Authentication + other headers
Delegate to HTTPoison (I prefer its API to
alternatives like HTTPotion)
Similar to {:ok, object} tuple response, except here it’s the
HTTP status code response (e.g. 200 for OK)
31. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
iex(1)> StripePost.post(
...(1)> "https://api.stripe.com/v1/charges",
...(1)> "amount=10000¤cy=cad&description=3+wozzle&source=pk_abc_123",
...(1)> [{"Authorization", "Bearer abc123"},
...(1)> {"Content-Type", "application/x-www-form-urlencoded"}])
16:41:29.215 [error] SSL: :certify: tls_connection.erl:704:Fatal error: handshake
failure - malformed_handshake_data
{:error, {:tls_alert, 'handshake failure'}}
Underlying error with HTTPosion
(mix deps.update --all)
Not a legit authentication token
Not a legit payment token
And too much “raw” code
here…
{401,
%{"error" => %{"message" => "Invalid API Key provided: **c123",
"type" => "invalid_request_error"}}}
Contrived example, but showing
that indeed we are hitting the API
32. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
@doc"""
Charge an account with the following body configurations
body = %{amount: 10000, currency: "cad", description: "3 wozzle", source: "pk_abc_123"}
configs = %{secret_key: "sk_test_abc123"}
"""
def charge(body, configs) do
post(Api.url <> "/charges", encode_body(body), headers(configs))
end
Constant (which
could/should be configurable)
Helper to convert maps to
URL encoded bodies
Helper to convert maps
to valid HTTPosion headers
33. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
iex(1)> StripePost.charge(
...(1)> %{amount: 10000,
...(1)> currency: "cad",
...(1)> description: "3 wozzle",
...(1)> source: "pk_abc_123"},
...(1)> %{secret_key: "sk_test_abc123"})
{401,
%{"error" => %{"message" => "Invalid API Key provided: **c123",
"type" => "invalid_request_error"}}}
Still contrived
But at least now more elixir like
Passing in the secret key directly
is OK, but not practical
As you will have one for testing
and one for production, so
it’s really more a config
34. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
use Mix.Config
config :stripe_post,
secret_key: "sk_test_abc123",
public_key: "pk_test_def456"
iex(1)> StripePost.charge(
...(1)> %{amount: 10000,
...(1)> currency: "cad",
...(1)> description: "3 wozzle",
...(1)> source: "pk_abc_123"})
So, let’s leverage Elixir’s config instead
No we can charge without
providing the configs
directly
Note, that is the payment token
not the sensitive secret key from above
so it’s still part of the API
35. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
Not real keys
Sensitive keys
are for the server
(“s” for secret)
Public keys are
for the client
(i.e. JavaScript)
Production keys
separate from
testing
37. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
https://github.com/aforward/stripe-callbacks
38. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
iex(1)> StripeCallbacks.process(%{
...(1)> "stripe" => %{"id" => "pk_abc_123"},
...(1)> "invoice" => %{
...(1)> "amount" => 2000,
...(1)> "currency" => "cad",
...(1)> "description" => "3 wozzle"}})
Still contrived
But we log to a database
Split between expected
response from stripe
and the “charge” invoice info
10:22:01.870 [debug] QUERY OK db=3.1ms
UPDATE "tokens" SET "token_status" = $1, "updated_at" = $2 WHERE "id" = $3 ["invalid", {{2017, 4, 25}, {14, 22, 1, 867165}}, 1]
{:ok,
%StripeCallbacks.Token{__meta__: #Ecto.Schema.Metadata<:loaded, "tokens">,
data: %{"invoice" => %{"amount" => 2000, "currency" => "cad",
"description" => "3 wozzle"}, "stripe" => %{"id" => "pk_abc_123"}}, id: 1,
inserted_at: ~N[2017-04-25 14:22:01.075526], token_status: "invalid",
updated_at: ~N[2017-04-25 14:22:01.867165]}}
39. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
Store tokens from the client in the database
Also store the response from Stripe trying to make the charge
Contrived example,
so we are still in “error”
but not for long
stripe_callbacks_dev=# select data, token_status from tokens;
data | token_status
-----------------------------------------------------------------------------+--------------
{ "stripe": {"id": "pk_abc_123"}, | invalid
"invoice": {"amount": 2000, "currency": "cad", "description": "3 wozzle"}} |
stripe_callbacks_dev=# select data, response_status, status_code, token_id from responses;
data | response_status | status_code | token_id
-----------------------------------------------------------------+-----------------+-------------+----------
{“error”. : {"type": "invalid_request_error", | failure | 400 | 1
"message": "Invalid token id: pk_abc_123"}} | | |
43. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
mix new <my-project> --sup --module MyProject --app my_project
11:32 /tmp $ mix new stripe-callbacks --sup --module StripeCallbacks --app stripe_callbacks
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/stripe_callbacks.ex
* creating lib/stripe_callbacks/application.ex
* creating test
* creating test/test_helper.exs
* creating test/stripe_callbacks_test.exs
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
cd stripe-callbacks
mix test
Run "mix help" for more commands.
45. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
@doc"""
Encode the provided hash map for the URL.
## Examples
iex> StripePost.Api.encode_body(%{a: "one", b: "two"})
"a=one&b=two"
iex> StripePost.Api.encode_body(%{a: "o ne"})
"a=o+ne"
"""
def encode_body(map), do: URI.encode_query(map)
Simply delegates to available
core Elixir function
But documentation is a
first class property of Elixir
And testing too!
46. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
https://hexdocs.pm/stripe_post/StripePost.Api.html
47. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
@doc"""
Build the headers for your API
## Examples
iex> StripePost.Api.headers(%{content_type: "application/json", secret_key: "abc123"})
[{"Authorization", "Bearer abc123"}, {"Content-Type", "application/json"}]
iex> StripePost.Api.headers(%{secret_key: "abc123"})
[{"Authorization", "Bearer abc123"}, {"Content-Type", "application/x-www-form-urlencoded"}]
iex> StripePost.Api.headers(%{})
[{"Authorization", "Bearer sk_test_abc123"}, {"Content-Type", "application/x-www-form-urlencoded"}]
iex> StripePost.Api.headers()
[{"Authorization", "Bearer sk_test_abc123"}, {"Content-Type", "application/x-www-form-urlencoded"}]
"""
def headers(), do: headers(%{})
def headers(nil), do: headers(%{})
def headers(data) do
h = %{content_type: "application/x-www-form-urlencoded"}
|> Map.merge(app_headers())
|> Map.merge(reject_nil(data))
[{"Authorization", "Bearer #{h[:secret_key]}"},
{"Content-Type", h[:content_type]}]
end
Pattern matching to avoid “if”ing out
bad, missing, defaulted data
Generating headers in a structure
like HTTPosion wants
Examples, examples, examples
49. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
use Mix.Config
config :stripe_post,
secret_key: "sk_test_abc123",
public_key: "pk_test_def456"
iex(1)> StripePost.charge(
...(1)> %{amount: 10000,
...(1)> currency: "cad",
...(1)> description: "3 wozzle",
...(1)> source: "pk_abc_123"})
So, let’s leverage Elixir’s config instead
No we can charge without
providing the configs
directly
Note, that is the payment token
not the sensitive secret key from above
so it’s still part of the API
50. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
use Mix.Config
# You will need to configure your public and private keys in Stripe.
# optionally you can also set the default content type
# https://dashboard.stripe.com/account/apikeys
# config :stripe_post,
# secret_key: "sk_test_abc123"
# public_key: "pk_test_abc123"
# content_type: "application/x-www-form-urlencoded"
#
# Within the application we will reference these using
# Application.get_env(:stripe_post, :secret_key)
# Application.get_env(:stripe_post, :public_key)
# Application.get_env(:stripe_post, :content_type)
#
import_config "#{Mix.env}.exs"
Elixir convention for
loading MIX_ENV specific
configs
And then each env, typically
dev, test and prod can be
separately configured
51. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
DO NOT commit sensitive
information to your repo
If some prod configs are not sensitive
then split them between prod.exs and
prod.secret.exs
Make sure you .gitignore the file
But, DO add a .example file, to help others (and your future self)
remember what production configs need to be set
53. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
mix test.watch
https://hex.pm/packages/mix_test_watch
54. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
autotesting rocks!
11:23 ~/sin/projects/current/stripe-callbacks (master)$ mix test.watch
===> Fetching pc ({pkg,<<"pc">>,<<"1.4.0">>})
===> Downloaded package, caching at /Users/aforward/.cache/rebar3/hex/default/packages/pc-1.4.0.tar
===> Compiling pc
===> Fetching rebar3_hex ({pkg,<<"rebar3_hex">>,<<"3.0.0">>})
===> Downloaded package, caching at /Users/aforward/.cache/rebar3/hex/default/packages/
rebar3_hex-3.0.0.tar
===> Compiling rebar3_hex
===> Compiling fs
===> Compiling /Users/aforward/sin/projects/current/stripe-callbacks/deps/fs/c_src/mac/cli.c
===> Compiling /Users/aforward/sin/projects/current/stripe-callbacks/deps/fs/c_src/mac/compat.c
===> Compiling /Users/aforward/sin/projects/current/stripe-callbacks/deps/fs/c_src/mac/main.c
===> Linking /Users/aforward/sin/projects/current/stripe-callbacks/deps/fs/priv/mac_listener
==> mix_test_watch
Compiling 12 files (.ex)
Generated mix_test_watch app
Running tests...
Compiling 2 files (.ex)
Generated stripe_callbacks app
..
Finished in 0.03 seconds
2 tests, 0 failures
Randomized with seed 587946
Each time you save a file in your project
the tests are re-run.
Unlike some other languages, your tests will be so fast
mix_test_watch does not need to be smart about
which tests to run
55. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
https://medium.com/@a4word/continuous-testing-with-elixir-ddc1107c5cc0
Learn more about continuous testing
56. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
Learn more A LOT MORE
about continuous testing
58. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
Grab ecto as well as the appropriate
underlying DB manager (e.g. postgrex
for PostgreSQL)
Create your Repo (thank you
macros… just 3 LOC
Make sure your application
supervises your database
Tell you application which
ecto_repos you care about
59. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
Support different DBs
for different environments
Uncomment this line in your config.exs
Sample configuration
for your dev database
60. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
So add this if you want your
`mix ecto.migrate` to work
I really like small commits, so I usually setup
and commit an empty schema
61. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
/stripe-callbacks (master)$ mix ecto.gen.migration add_tokens
* creating priv/repo/migrations
* creating priv/repo/migrations/20170325160151_add_tokens.exs
Your database is nothing more
than a bunch of schema migrations
Ecto provides a create
language to easily create
tables, indexes, etc.
62. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
Ecto is NOT an ORM
Describe your DB
schema
Configure your entity
as a JSON response
for RESTful APIs
Changesets are confusing,
until they aren’t
64. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
This allows for asynchronous
testing
Ecto is chatty, so this will
disable much of the logging
But now each test needs it’s own
DB connection
All set, go forth and test
65. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
1) test create (default status) (StripeCallbacks.TokenTest)
test/token_test.exs:22
** (MatchError) no match of right hand side value: {:error, i
%DBConnection.ConnectionError{message:
"connection not available because of disconnection"}}
stacktrace:
test/token_test.exs:8: StripeCallbacks.TokenTest.__ex_unit_setup_0/1
test/token_test.exs:1: StripeCallbacks.TokenTest.__ex_unit__/2
Oopses, no database
Override test* to ensure your database is properly up and running
66. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
$ mix test.watch
Running tests...
Compiling 6 files (.ex)
Generated stripe_callbacks app
** (Mix) The database for StripeCallbacks.Repo couldn't be dropped:
ERROR 55006 (object_in_use): database "stripe_callbacks_test"
is being accessed by other users
There are 10 other sessions using the database.
But now we broke `mix test.watch`
Safe way to support a “one off” testing versus test.watch
but you need to call `MIX_ENV=test mix test.once`
67. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
https://github.com/lpil/mix-test.watch/pull/70
https://github.com/lpil/mix-test.watch/pull/71
Support for just running something “once”
Circumventing (unnecessary?) internal
call that break testing that includes
database setup
But, we can do better
68. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
Fix feature-bug for creating / dropping
databases as part of your testing
To avoid re-creating the database
on each test run (renamed from
just “test”)
Enable creating your
database just once when
start `mix test.run`
(instead of each run)
But, you have to live on the edge
70. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
Schema to describe
your data
Key/Value inputs from
a user (very raw)
Data transformations
(cleaned, defaulted,
derived, validated)
+
Insert into database
Or, present
errors for correction
to the user
This IS a changeset
(just a data structure)
71. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
iex(1)> schema = %{first_name: :string, last_name: :string, email: :string}
%{email: :string, first_name: :string, last_name: :string}
iex(2)> params = %{"first_name" => "James", "last_name" => "Url"}
%{"first_name" => "James", "last_name" => "Url"}
iex(3)> changeset = Ecto.Changeset.cast({%{}, schema}, params, Map.keys(schema))
iex(4)> changeset.changes
%{first_name: "James", last_name: "Url"}
iex(5)> changeset.valid?
true
A changeset is just a tuple
with starting data, and the schema
#Ecto.Changeset<action: nil, changes: %{first_name: "James", last_name: "Url"},
errors: [], data: %{}, valid?: true>
The params are the user input
so usually strings as keys
What are the
valid fields
And it’s really just data structure coming back
That you can access like any struct
72. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
defmodule StripeCallbacks.Token do
use Ecto.Schema
import Ecto.Changeset
schema "tokens" do
field :data, :map
field :token_status, :string
timestamps()
end
def changeset(model, params %{}) do
model
|> cast(params, [:data, :token_status])
end
end
The ecto struct (e.g. %Token{})
Based on the schema
Params and valid fields still apply
%Token{}
|> Token.changeset(
%{"data" => %{"apples" => "red"},
"token_status" => “processed"})
|> Repo.insert
Using changes
to insert data
73. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
@doc """
Creates a changeset based on the `model` and `params`.
If no params are provided, an invalid changeset is returned
with no validation performed.
"""
def changeset(model, params %{}) do
model
|> cast(params, [:pubid, :name, :slug, :owner, :status])
|> ChangesetMerger.defaulted(:name, "LiveCode")
|> ChangesetMerger.defaulted(:owner, "Teacher")
|> ChangesetMerger.Slug.derive_if_missing
|> ChangesetMerger.Token.defaulted(:pubid, 4)
|> ChangesetMerger.defaulted(:status, "created")
end
A much more involved changes
that includes default values,
slug (aka URL safe) generation
and tokens
74. SIMPLE PAYMENT PAGE USING ELIXIR AND STRIPE
https://hex.pm/packages/changeset_merger