├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── config └── config.exs ├── lib ├── stripe.ex └── stripe │ ├── api.ex │ ├── balance.ex │ ├── balance_transaction.ex │ ├── bitcoin_receiver.ex │ ├── charge.ex │ ├── connect.ex │ ├── connect │ ├── account.ex │ ├── application_fee.ex │ ├── country_spec.ex │ └── recipient.ex │ ├── customer.ex │ ├── dispute.ex │ ├── errors │ ├── api_connection_error.ex │ ├── api_error.ex │ ├── authentication_error.ex │ ├── card_error.ex │ ├── invalid_request_error.ex │ ├── rate_limit_error.ex │ └── signature_verification_error.ex │ ├── event.ex │ ├── file.ex │ ├── refund.ex │ ├── subscriptions │ ├── coupon.ex │ ├── invoice.ex │ ├── invoice_item.ex │ ├── plan.ex │ ├── subscription.ex │ └── subscription_item.ex │ ├── token.ex │ ├── transfer.ex │ ├── utils.ex │ └── webhook.ex ├── mix.exs ├── mix.lock └── test ├── balance_test.exs ├── balance_transaction_test.exs ├── charge_test.exs ├── connect ├── account_test.exs ├── application_fee_test.exs ├── country_spec_test.exs └── recipient_test.exs ├── connect_test.exs ├── customer_test.exs ├── event_test.exs ├── refund_test.exs ├── subscriptions ├── coupon_test.exs ├── invoice_item_test.exs ├── invoice_test.exs ├── plan_test.exs ├── subscription_item_test.exs └── subscription_test.exs ├── support └── token_fixture.ex ├── test_helper.exs ├── token_test.exs ├── transfer_test.exs └── webhook_test.exs /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | 19 | # Mac artifact 20 | .DS_Store 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.8.0 2 | - [Feature] Added support for SubscriptionItem 3 | - [Deps] Added support for elixir 1.5 4 | ## 0.7.1 5 | - [Dependancy] Updated Poison dep to support 3.0 6 | ## 0.7.0 7 | - [Enhancement] Added ability to verify signed Stripe Webhook payloads via `Stripe.Webhook.construct_event/4`. (Thank you @jayjun) 8 | - [Enhancement] Added ability to generate a Connect oauth url via `Stripe.Connect.authorize_url/1`. 9 | - [Modification] Missing secret_key will now raise `Stripe.AuthenticationError` error instead of `MissingSecretKeyError`. -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-present, Sikan He 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stripe [![Build Status](https://semaphoreci.com/api/v1/sikanhe/stripe-elixir/branches/master/badge.svg)](https://semaphoreci.com/sikanhe/stripe-elixir) # 2 | 3 | Stripe API client for Elixir. [Documentation](https://hexdocs.pm/stripe_elixir/api-reference.html) 4 | - Everything except for Relay features are complete and tested. 5 | - Looking for more contributors/maintainers for this project, currently need help with documentation. 6 | 7 | ## Installation 8 | 9 | 1. Add `stripe` to your list of dependencies in `mix.exs`: 10 | 11 | ```elixir 12 | def deps do 13 | [{:stripe, "~> 0.8.0", hex: :stripe_elixir}] 14 | end 15 | ``` 16 | 17 | 2. (Pre-Elixir 1.4) Ensure `stripe` is started before your application: 18 | 19 | ```elixir 20 | def application do 21 | [applications: [:stripe]] 22 | end 23 | ``` 24 | 25 | 3. Make sure your stripe secret_key is added to your config file: 26 | 27 | ```elixir 28 | config :stripe, :secret_key, 29 | ``` 30 | 31 | 4. Alternatively, you can also set the secret key as an environment variable: 32 | 33 | ```bash 34 | export STRIPE_SECRET_KEY= 35 | ``` 36 | 37 | ## Basic Usage 38 | 39 | This lib closely follows the official Ruby Client API. 40 | 41 | * `Stripe.{RESOURCE}.create` 42 | * `Stripe.{RESOURCE}.retrieve` 43 | * `Stripe.{RESOURCE}.update` 44 | * `Stripe.{RESOURCE}.list` 45 | 46 | Returns `{:ok, RESPONSE_BODY}` when the request is successful. 47 | 48 | `{:error, %ERROR_STRUCT{}}` tuples are returned when there is a request/api error. 49 | See all error types at https://stripe.com/docs/api/ruby#errors 50 | 51 | ## Some Basic Examples 52 | 53 | Create a customer: 54 | 55 | ```elixir 56 | {:ok, %{"id" => "cus_asdfghjkl"} = 57 | Stripe.Customer.create(email: "example@gmail.com") 58 | ``` 59 | 60 | Note that either KeywordLists or Maps with either String or Atom keys are acceptable for arguments and options. So all of the following would also work: 61 | 62 | ```elixir 63 | Stripe.Customer.create(%{email: "example@gmail.com"}) 64 | Stripe.Customer.create(%{"email" => "example@gmail.com"}) 65 | Stripe.Customer.create([{"email", "example@gmail.com"}]) 66 | ``` 67 | 68 | Retrieve that customer: 69 | 70 | ```elixir 71 | {:ok, customer} = Stripe.Customer.retrieve("cus_asdfghjkl") 72 | ``` 73 | 74 | Update the customer: 75 | 76 | ```elixir 77 | {:ok, %{"metadata" => %{"somedata" => "somevalue"}}} = 78 | Stripe.Customer.update("cus_asdfghjkl", metadata: [somedata: "somevalue"]) 79 | ``` 80 | 81 | Delete the customer: 82 | 83 | ```elixir 84 | {:ok, %{"deleted" => true}} = Stripe.Customer.delete("cus_asdfghjkl") 85 | ``` 86 | 87 | ## Stripe Connect 88 | 89 | To perform a Direct Charge on a connected stripe account, simply pass :stripe_account as an option 90 | 91 | ```elixir 92 | Stripe.Charge.create([customer: "cus_asdfghjkl", amount: 400], stripe_account: "acct_sOMeAcCountId") 93 | ``` 94 | 95 | #### Generate a Connect authorization url via `Stripe.Connect.authorize_url/1`. 96 | 97 | ```elixir 98 | Stripe.Connect.authorize_url([ 99 | redirect_uri: , 100 | state: , 101 | client_id: 102 | ]) 103 | ``` 104 | 105 | #### Options: 106 | - `redirect_uri`: An optional callback url after authorization succeeds. 107 | - `state`: You can protect your request from CSRF attacks by passing a csrf token. 108 | - `client_id`: You can pass in an optional client_id to be used for this url. Defaults to `STRIPE_CLIENT_ID` environment variable or `config :stripe, :client_id` config value. 109 | 110 | 111 | ## Handling Webhooks 112 | 113 | Stripe uses webhooks to notify your web app with events. `Stripe.Webhook` provides `construct_event/3` to authenticate the requests, which can be useful in plugs. 114 | 115 | ```elixir 116 | payload = # HTTP content body (e.g. from Plug.Conn.read_body/3) 117 | signature = # 'Stripe-Signature' HTTP header (e.g. from Plug.Conn.get_req_header/2) 118 | secret = # Provided by Stripe 119 | 120 | case Stripe.Webhook.construct_event(payload, signature, secret) do 121 | {:ok, event} -> 122 | # Return 2XX 123 | {:error, %Stripe.SignatureVerificationError{}} -> 124 | # Return non-2XX and handle error 125 | end 126 | ``` 127 | 128 | The default tolerance is 5 minutes (300 seconds as per official libraries). If your app is rejecting requests because the tolerance is too low, consider passing a higher number to `construct_event/4`. 129 | 130 | ```elixir 131 | Stripe.Webhook.construct_event(payload, signature, secret, 600) 132 | ``` 133 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /lib/stripe.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe do 2 | @moduledoc """ 3 | Main module for handling sending/receiving requests to Stripe's API 4 | """ 5 | 6 | @default_api_endpoint "https://api.stripe.com/v1/" 7 | @client_version Mix.Project.config[:version] 8 | 9 | def version do 10 | @client_version 11 | end 12 | 13 | alias Stripe.{APIConnectionError, 14 | APIError, 15 | AuthenticationError, 16 | CardError, 17 | InvalidRequestError, 18 | RateLimitError} 19 | 20 | @missing_secret_key_error_message""" 21 | The secret_key settings is required to use stripe. Please include your 22 | stripe secret api key in your application config file like so: 23 | 24 | config :stripe, secret_key: YOUR_SECRET_KEY 25 | 26 | Alternatively, you can also set the secret key as an environment variable: 27 | 28 | STRIPE_SECRET_KEY=YOUR_SECRET_KEY 29 | """ 30 | 31 | defp get_secret_key do 32 | System.get_env("STRIPE_SECRET_KEY") || 33 | Application.get_env(:stripe, :secret_key) || 34 | raise AuthenticationError, message: @missing_secret_key_error_message 35 | end 36 | 37 | defp get_api_endpoint do 38 | System.get_env("STRIPE_API_ENDPOINT") || 39 | Application.get_env(:stripe, :api_endpoint) || 40 | @default_api_endpoint 41 | end 42 | 43 | defp request_url(endpoint) do 44 | Path.join(get_api_endpoint(), endpoint) 45 | end 46 | 47 | defp request_url(endpoint, []) do 48 | Path.join(get_api_endpoint(), endpoint) 49 | end 50 | 51 | defp request_url(endpoint, data) do 52 | base_url = request_url(endpoint) 53 | query_params = Stripe.Utils.encode_data(data) 54 | "#{base_url}?#{query_params}" 55 | end 56 | 57 | defp create_headers(opts) do 58 | headers = 59 | [{"Authorization", "Bearer #{get_secret_key()}"}, 60 | {"User-Agent", "Stripe/v1 stripe-elixir/#{@client_version}"}, 61 | {"Content-Type", "application/x-www-form-urlencoded"}] 62 | 63 | case Keyword.get(opts, :stripe_account) do 64 | nil -> headers 65 | account_id -> [{"Stripe-Account", account_id} | headers] 66 | end 67 | end 68 | 69 | def request(action, endpoint, data, opts) when action in [:get, :post, :delete] do 70 | HTTPoison.request(action, request_url(endpoint, data), "", create_headers(opts)) 71 | |> handle_response 72 | end 73 | 74 | defp handle_response({:ok, %{body: body, status_code: 200}}) do 75 | {:ok, process_response_body(body)} 76 | end 77 | 78 | defp handle_response({:ok, %{body: body, status_code: code}}) do 79 | %{"message" => message} = error = 80 | body 81 | |> process_response_body 82 | |> Map.fetch!("error") 83 | 84 | error_struct = 85 | case code do 86 | code when code in [400, 404] -> 87 | %InvalidRequestError{message: message, param: error["param"]} 88 | 401 -> 89 | %AuthenticationError{message: message} 90 | 402 -> 91 | %CardError{message: message, code: error["code"], param: error["param"]} 92 | 429 -> 93 | %RateLimitError{message: message} 94 | _ -> 95 | %APIError{message: message} 96 | end 97 | 98 | {:error, error_struct} 99 | end 100 | 101 | defp handle_response({:error, %HTTPoison.Error{reason: reason}}) do 102 | %APIConnectionError{message: "Network Error: #{reason}"} 103 | end 104 | 105 | defp process_response_body(body) do 106 | Poison.decode! body 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/stripe/api.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.API do 2 | defmacro __using__(opts) do 3 | quote do 4 | if :create in unquote(opts) do 5 | @doc """ 6 | Create a(n) #{__MODULE__ |> to_string |> String.split(".") |> List.last} 7 | """ 8 | def create(data, opts \\ []) do 9 | Stripe.request(:post, endpoint(), data, opts) 10 | end 11 | end 12 | 13 | if :retrieve in unquote(opts) do 14 | @doc """ 15 | Retrive a(n) #{__MODULE__ |> to_string |> String.split(".") |> List.last} by its ID 16 | """ 17 | def retrieve(id, opts \\ []) when is_bitstring(id) do 18 | resource_url = Path.join(endpoint(), id) 19 | Stripe.request(:get, resource_url, [], opts) 20 | end 21 | end 22 | 23 | if :update in unquote(opts) do 24 | @doc """ 25 | Update a(n) #{__MODULE__ |> to_string |> String.split(".") |> List.last} 26 | """ 27 | def update(id, data, opts \\ []) when is_bitstring(id) do 28 | resource_url = Path.join(endpoint(), id) 29 | Stripe.request(:post, resource_url, data, opts) 30 | end 31 | end 32 | 33 | if :list in unquote(opts) do 34 | @doc """ 35 | List all #{__MODULE__ |> to_string |> String.split(".") |> List.last}s 36 | """ 37 | def list(pagination_opts \\ [], opts \\ []) when is_list(pagination_opts) do 38 | Stripe.request(:get, endpoint(), pagination_opts, opts) 39 | end 40 | end 41 | 42 | if :delete in unquote(opts) do 43 | @doc """ 44 | Delete a(n) #{__MODULE__ |> to_string |> String.split(".") |> List.last} 45 | """ 46 | def delete(id, data \\ [], opts \\ []) when is_bitstring(id) do 47 | resource_url = Path.join(endpoint(), id) 48 | Stripe.request(:delete, resource_url, data, opts) 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/stripe/balance.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.Balance do 2 | use Stripe.API, [:list] 3 | 4 | def endpoint do 5 | "balance" 6 | end 7 | 8 | def retrieve do 9 | list 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/stripe/balance_transaction.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.BalanceTransaction do 2 | use Stripe.API, [:retrieve, :list] 3 | 4 | def endpoint do 5 | "balance/history" 6 | end 7 | 8 | def all(opts \\ []) do 9 | list([], opts) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/stripe/bitcoin_receiver.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.BitcoinReceiver do 2 | use Stripe.API, [:create, :retrieve, :list] 3 | 4 | def endpoint do 5 | "bitcoin/receivers" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/stripe/charge.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.Charge do 2 | use Stripe.API, [:list, :retrieve, :create, :update] 3 | 4 | def endpoint do 5 | "charges" 6 | end 7 | 8 | def capture_endpoint(id) do 9 | "#{endpoint()}/#{id}/capture" 10 | end 11 | 12 | def capture(id, opts \\ []) do 13 | Stripe.request(:post, capture_endpoint(id), [], opts) 14 | end 15 | 16 | def capture(id, data, opts) do 17 | Stripe.request(:post, capture_endpoint(id), data, opts) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/stripe/connect.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.Connect do 2 | @moduledoc""" 3 | Functions related to Connect 4 | """ 5 | 6 | @oauth_url "https://connect.stripe.com/oauth/authorize" 7 | 8 | @spec authorize_url(Keyword.t) :: binary 9 | def authorize_url(opts \\ []) do 10 | client_id = Keyword.get(opts, :client_id) || get_client_id() 11 | query_params = 12 | opts 13 | |> Keyword.put_new(:client_id, client_id) 14 | |> Keyword.put_new(:scope, "read_write") 15 | |> Keyword.put_new(:response_type, "code") 16 | |> Stripe.Utils.encode_data() 17 | 18 | @oauth_url <> "?" <> query_params 19 | end 20 | 21 | @missing_client_id_error_message """ 22 | A Connect Client ID is required to use stripe. Please include your 23 | connect Client ID in your application config file like so: 24 | 25 | config :stripe, client_id: 26 | 27 | Alternatively, you can also set the secret key as an environment variable: 28 | 29 | STRIPE_CLIENT_ID= 30 | 31 | You can also pass in client_id option for Stripe.Connect.generate_authorize_url/1 32 | Stripe.Connect.authorize_url(client_id: ) 33 | """ 34 | 35 | defp get_client_id do 36 | System.get_env("STRIPE_CLIENT_ID") || 37 | Application.get_env(:stripe, :client_id) || 38 | raise Stripe.AuthenticationError, message: @missing_client_id_error_message 39 | end 40 | end -------------------------------------------------------------------------------- /lib/stripe/connect/account.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.Account do 2 | use Stripe.API, [:create, :retrieve, :update, :list, :delete] 3 | 4 | def endpoint, do: "accounts" 5 | 6 | def reject(account_id, [reason: reason], opts \\ []) do 7 | Stripe.request(:post, "#{endpoint()}/#{account_id}/reject", [reason: reason], opts) 8 | end 9 | 10 | def external_account_endpoint(account_id, external_account_id \\ "") do 11 | "#{endpoint()}/#{account_id}/external_accounts/#{external_account_id}" 12 | end 13 | 14 | def create_external_account(account_id, [external_account: token_id], opts \\ []) do 15 | url = external_account_endpoint(account_id) 16 | Stripe.request(:post, url, [external_account: token_id], opts) 17 | end 18 | 19 | def retrieve_external_account(account_id, external_account_id, opts \\ []) do 20 | url = external_account_endpoint(account_id, external_account_id) 21 | Stripe.request(:get, url, [], opts) 22 | end 23 | 24 | def update_external_account(account_id, external_account_id, updates, opts \\ []) do 25 | url = external_account_endpoint(account_id, external_account_id) 26 | Stripe.request(:post, url, updates, opts) 27 | end 28 | 29 | def delete_external_account(account_id, external_account_id, opts \\ []) do 30 | url = external_account_endpoint(account_id, external_account_id) 31 | Stripe.request(:delete, url, [], opts) 32 | end 33 | 34 | def list_external_account(account_id, pagination_opts \\ [], opts \\ []) do 35 | url = external_account_endpoint(account_id) 36 | Stripe.request(:get, url, pagination_opts, opts) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/stripe/connect/application_fee.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.ApplicationFee do 2 | use Stripe.API, [:create, :retrieve, :update, :list] 3 | 4 | def endpoint do 5 | "application_fees" 6 | end 7 | 8 | def refund(fee_id, opts \\ []) do 9 | Stripe.request(:post, "#{endpoint()}/#{fee_id}/refund", [], opts) 10 | end 11 | 12 | def retrieve_refund(fee_id, refund_id, opts \\ []) do 13 | Stripe.request(:get, "#{endpoint()}/#{fee_id}/refund/#{refund_id}", [], opts) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/stripe/connect/country_spec.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.CountrySpec do 2 | use Stripe.API, [:retrieve, :list] 3 | 4 | def endpoint do 5 | "country_specs" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/stripe/connect/recipient.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.Recipient do 2 | use Stripe.API, [:create, :retrieve, :update, :delete, :list] 3 | 4 | def endpoint do 5 | "recipients" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/stripe/customer.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.Customer do 2 | use Stripe.API, [:retrieve, :update, :create, :list, :delete] 3 | 4 | def endpoint do 5 | "/customers" 6 | end 7 | 8 | # discount 9 | 10 | def delete_discount(customer_id, opts \\ []) do 11 | Stripe.request(:delete, "#{endpoint()}/#{customer_id}/discount", [], opts) 12 | end 13 | 14 | # sources 15 | 16 | def retrieve_source(customer_id, source_id, opts \\ []) do 17 | Stripe.request(:get, "#{endpoint()}/#{customer_id}/sources/#{source_id}", [], opts) 18 | end 19 | 20 | def update_source(customer_id, source_id, updates, opts \\ []) do 21 | Stripe.request(:post, "#{endpoint()}/#{customer_id}/sources/#{source_id}", updates, opts) 22 | end 23 | 24 | def create_source(customer_id, data, opts \\ []) do 25 | Stripe.request(:post, "#{endpoint()}/#{customer_id}/sources", data, opts) 26 | end 27 | 28 | def delete_source(customer_id, data, opts \\ []) do 29 | Stripe.request(:delete, "#{endpoint()}/#{customer_id}/sources/#{data[:source]}", [], opts) 30 | end 31 | 32 | # card 33 | 34 | def create_card(customer_id, card_id, opts \\ []) do 35 | create_source(customer_id, [source: card_id], opts) 36 | end 37 | 38 | def update_card(customer_id, card_id, updates) do 39 | update_source(customer_id, card_id, updates) 40 | end 41 | 42 | def delete_card(customer_id, card_id, opts \\ []) do 43 | delete_source(customer_id, [source: card_id], opts) 44 | end 45 | 46 | def list_cards(customer_id, pagination_opts \\ [], opts \\ []) do 47 | pagination_opts = put_in(pagination_opts, [:object], "card") 48 | Stripe.request(:get, "#{endpoint()}/#{customer_id}/sources", pagination_opts, opts) 49 | end 50 | 51 | # bank_account 52 | 53 | def create_bank_account(customer_id, bank_acct_id, opts \\ []) do 54 | create_source(customer_id, [source: bank_acct_id], opts) 55 | end 56 | 57 | def update_bank_account(customer_id, bank_acct_id, updates, opts \\ []) do 58 | update_source(customer_id, bank_acct_id, updates, opts) 59 | end 60 | 61 | def delete_bank_account(customer_id, bank_acct_id, opts \\ []) do 62 | delete_source(customer_id, [source: bank_acct_id], opts) 63 | end 64 | 65 | def list_bank_accounts(customer_id, pagination_opts \\ [], opts \\ []) do 66 | pagination_opts = put_in(pagination_opts, [:object], "bank_account") 67 | Stripe.request(:get, "#{endpoint()}/#{customer_id}/sources", pagination_opts, opts) 68 | end 69 | 70 | def verify_bank_account(customer_id, bank_acct_id, amounts, opts \\ []) do 71 | Stripe.request(:post, "#{endpoint()}/#{customer_id}/sources/#{bank_acct_id}/verify", [amounts: amounts], opts) 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/stripe/dispute.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.Dispute do 2 | use Stripe.API, [:list, :create, :update] 3 | 4 | def endpoint do 5 | "disputes" 6 | end 7 | 8 | def close_dispute_endpoint(id) do 9 | "#{endpoint()}/#{id}/close" 10 | end 11 | 12 | def close(id, opts \\ []) do 13 | Stripe.request(:post, close_dispute_endpoint(id), [], opts) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/stripe/errors/api_connection_error.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.APIConnectionError do 2 | @moduledoc """ 3 | Failure to connect to Stripe's API. 4 | """ 5 | defexception type: "api_connection_error", message: nil 6 | end 7 | -------------------------------------------------------------------------------- /lib/stripe/errors/api_error.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.APIError do 2 | @moduledoc """ 3 | API errors cover any other type of problem (e.g., a temporary problem with 4 | Stripe's servers) and are extremely uncommon. 5 | """ 6 | defexception type: "api_error", message: nil 7 | end 8 | -------------------------------------------------------------------------------- /lib/stripe/errors/authentication_error.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.AuthenticationError do 2 | @moduledoc """ 3 | Failure to properly authenticate yourself in the request. 4 | """ 5 | defexception type: "authentication_error", message: nil 6 | end 7 | -------------------------------------------------------------------------------- /lib/stripe/errors/card_error.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.CardError do 2 | @moduledoc """ 3 | Card errors are the most common type of error you should expect to handle. 4 | They result when the user enters a card that can't be charged for some reason. 5 | """ 6 | defexception type: "card_error", message: nil, param: nil, code: nil 7 | end 8 | -------------------------------------------------------------------------------- /lib/stripe/errors/invalid_request_error.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.InvalidRequestError do 2 | @moduledoc """ 3 | Invalid request errors arise when your request has invalid parameters. 4 | """ 5 | defexception type: "invalid_request_error", message: nil, param: nil 6 | end 7 | -------------------------------------------------------------------------------- /lib/stripe/errors/rate_limit_error.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.RateLimitError do 2 | @moduledoc """ 3 | Too many requests hit the API too quickly. 4 | """ 5 | defexception type: "rate_limit_error", message: nil 6 | end 7 | -------------------------------------------------------------------------------- /lib/stripe/errors/signature_verification_error.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.SignatureVerificationError do 2 | @moduledoc """ 3 | Failure to verify payload with signature attached to webhook requests. 4 | """ 5 | defexception type: "signature_verification_error", message: nil 6 | end 7 | -------------------------------------------------------------------------------- /lib/stripe/event.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.Event do 2 | use Stripe.API, [:retrieve, :list] 3 | 4 | def endpoint do 5 | "events" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/stripe/file.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.File do 2 | use Stripe.API, [:create, :retrieve, :list] 3 | 4 | def endpoint do 5 | "files" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/stripe/refund.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.Refund do 2 | use Stripe.API, [:create, :retrieve, :update, :list] 3 | 4 | def endpoint do 5 | "refunds" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/stripe/subscriptions/coupon.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.Coupon do 2 | use Stripe.API, [:create, :retrieve, :update, :delete, :list] 3 | 4 | def endpoint do 5 | "coupons" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/stripe/subscriptions/invoice.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.Invoice do 2 | use Stripe.API, [:create, :retrieve, :update, :list] 3 | 4 | def endpoint do 5 | "invoices" 6 | end 7 | 8 | def line_items(%{"id" => invoice_id}) do 9 | line_items(invoice_id) 10 | end 11 | 12 | def line_items(invoice_id, pagination_opts \\ [], opts \\ []) do 13 | Stripe.request(:get, "#{endpoint()}/#{invoice_id}/lines", pagination_opts, opts) 14 | end 15 | 16 | def upcoming(data, opts \\ []) do 17 | Stripe.request(:get, "#{endpoint()}/upcoming", data, opts) 18 | end 19 | 20 | def pay(invoice_id, opts \\ []) do 21 | Stripe.request(:post, "#{endpoint()}/#{invoice_id}/pay", [], opts) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/stripe/subscriptions/invoice_item.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.InvoiceItem do 2 | use Stripe.API, [:create, :retrieve, :update, :delete, :list] 3 | 4 | def endpoint do 5 | "invoiceitems" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/stripe/subscriptions/plan.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.Plan do 2 | use Stripe.API, [:create, :retrieve, :update, :delete, :list] 3 | 4 | def endpoint do 5 | "plans" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/stripe/subscriptions/subscription.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.Subscription do 2 | use Stripe.API, [:create, :retrieve, :update, :delete, :list] 3 | 4 | def endpoint do 5 | "subscriptions" 6 | end 7 | 8 | def delete_discount(subscription_id, opts \\ []) do 9 | Stripe.request(:delete, "#{endpoint()}/#{subscription_id}/discount", [], opts) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/stripe/subscriptions/subscription_item.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.SubscriptionItem do 2 | use Stripe.API, [:create, :retrieve, :update, :delete, :list] 3 | 4 | def endpoint do 5 | "subscription_items" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/stripe/token.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.Token do 2 | use Stripe.API, [:create, :retrieve] 3 | 4 | def endpoint do 5 | "tokens" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/stripe/transfer.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.Transfer do 2 | use Stripe.API, [:create, :retrieve, :update, :list] 3 | 4 | def endpoint do 5 | "transfers" 6 | end 7 | 8 | def transfer_reversal_endpoint(transfer_id, reversal_id \\ "") do 9 | "#{endpoint()}/#{transfer_id}/reversals/#{reversal_id}" 10 | end 11 | 12 | def create_reversal(transfer_id, data \\ [], opts \\ []) do 13 | Stripe.request(:post, transfer_reversal_endpoint(transfer_id), data, opts) 14 | end 15 | 16 | def retrieve_reversal(transfer_id, reversal_id, opts \\ []) do 17 | Stripe.request(:get, transfer_reversal_endpoint(transfer_id, reversal_id), [], opts) 18 | end 19 | 20 | def update_reversal(transfer_id, reversal_id, data, opts \\ []) do 21 | Stripe.request(:post, transfer_reversal_endpoint(transfer_id, reversal_id), data, opts) 22 | end 23 | 24 | def list_reversals(transfer_id, pagination_opts, opts \\ []) do 25 | Stripe.request(:get, transfer_reversal_endpoint(transfer_id), pagination_opts, opts) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/stripe/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.Utils do 2 | @moduledoc """ 3 | Utility functions for stripe 4 | """ 5 | 6 | @doc """ 7 | Encodes the given map or list of tuples. 8 | Borrowed from "https://github.com/elixir-lang/plug/blob/master/lib/plug/conn/query.ex" 9 | """ 10 | def encode_data(kv, encoder \\ &to_string/1) do 11 | IO.iodata_to_binary encode_pair("", kv, encoder) 12 | end 13 | 14 | # covers structs 15 | defp encode_pair(field, %{__struct__: struct} = map, encoder) when is_atom(struct) do 16 | [field, ?=|encode_value(map, encoder)] 17 | end 18 | 19 | # covers maps 20 | defp encode_pair(parent_field, %{} = map, encoder) do 21 | encode_kv(map, parent_field, encoder) 22 | end 23 | 24 | # covers keyword lists 25 | defp encode_pair(parent_field, list, encoder) when is_list(list) and is_tuple(hd(list)) do 26 | encode_kv(Enum.uniq_by(list, &elem(&1, 0)), parent_field, encoder) 27 | end 28 | 29 | # covers non-keyword lists 30 | defp encode_pair(parent_field, list, encoder) when is_list(list) do 31 | prune Enum.flat_map list, fn value -> 32 | [?&, encode_pair(parent_field <> "[]", value, encoder)] 33 | end 34 | end 35 | 36 | # covers nil 37 | defp encode_pair(field, nil, _encoder) do 38 | [field, ?=] 39 | end 40 | 41 | # encoder fallback 42 | defp encode_pair(field, value, encoder) do 43 | [field, ?=|encode_value(value, encoder)] 44 | end 45 | 46 | defp encode_kv(kv, parent_field, encoder) do 47 | prune Enum.flat_map kv, fn 48 | {_, value} when value in [%{}, []] -> 49 | [] 50 | {field, value} -> 51 | field = 52 | if parent_field == "" do 53 | encode_key(field) 54 | else 55 | parent_field <> "[" <> encode_key(field) <> "]" 56 | end 57 | [?&, encode_pair(field, value, encoder)] 58 | end 59 | end 60 | 61 | defp encode_key(item) do 62 | item |> to_string |> URI.encode_www_form 63 | end 64 | 65 | 66 | defp encode_value(item, encoder) do 67 | item |> encoder.() |> URI.encode_www_form 68 | end 69 | 70 | defp prune([?&|t]), do: t 71 | defp prune([]), do: [] 72 | end 73 | -------------------------------------------------------------------------------- /lib/stripe/webhook.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.Webhook do 2 | alias Stripe.SignatureVerificationError 3 | 4 | @default_tolerance 300 5 | @expected_scheme "v1" 6 | 7 | def construct_event(payload, signature_header, secret, tolerance \\ @default_tolerance) do 8 | case verify_header(payload, signature_header, secret, tolerance) do 9 | :ok -> 10 | {:ok, Poison.decode!(payload)} 11 | error -> 12 | error 13 | end 14 | end 15 | 16 | defp verify_header(payload, signature_header, secret, tolerance) do 17 | case get_timestamp_and_signatures(signature_header, @expected_scheme) do 18 | {nil, _} -> 19 | {:error, %SignatureVerificationError{message: "Unable to extract timestamp and signatures from header"}} 20 | 21 | {_, []} -> 22 | {:error, %SignatureVerificationError{message: "No signatures found with expected scheme #{@expected_scheme}"}} 23 | 24 | {timestamp, signatures} -> 25 | with {:ok, timestamp} <- check_timestamp(timestamp, tolerance), 26 | {:ok, _signatures} <- check_signatures(signatures, timestamp, payload, secret) do 27 | :ok 28 | else 29 | {:error, error} -> {:error, error} 30 | end 31 | end 32 | end 33 | 34 | defp get_timestamp_and_signatures(signature_header, scheme) do 35 | signature_header 36 | |> String.split(",") 37 | |> Enum.map(& String.split(&1, "=")) 38 | |> Enum.reduce({nil, []}, fn 39 | ["t", timestamp], {nil, signatures} -> 40 | {to_integer(timestamp), signatures} 41 | 42 | [^scheme, signature], {timestamp, signatures} -> 43 | {timestamp, [signature | signatures]} 44 | 45 | _, acc -> 46 | acc 47 | end) 48 | end 49 | 50 | defp to_integer(timestamp) do 51 | case Integer.parse(timestamp) do 52 | {timestamp, _} -> 53 | timestamp 54 | :error -> 55 | nil 56 | end 57 | end 58 | 59 | defp check_timestamp(timestamp, tolerance) do 60 | now = System.system_time(:seconds) 61 | if timestamp < (now - tolerance) do 62 | {:error, %SignatureVerificationError{message: "Timestamp outside the tolerance zone (#{now})"}} 63 | else 64 | {:ok, timestamp} 65 | end 66 | end 67 | 68 | defp check_signatures(signatures, timestamp, payload, secret) do 69 | signed_payload = "#{timestamp}.#{payload}" 70 | expected_signature = compute_signature(signed_payload, secret) 71 | if Enum.any?(signatures, & secure_equals?(&1, expected_signature)) do 72 | {:ok, signatures} 73 | else 74 | {:error, %SignatureVerificationError{message: "No signatures found matching the expected signature for payload"}} 75 | end 76 | end 77 | 78 | defp compute_signature(payload, secret) do 79 | :crypto.hmac(:sha256, secret, payload) 80 | |> Base.encode16(case: :lower) 81 | end 82 | 83 | defp secure_equals?(input, expected) when byte_size(input) == byte_size(expected) do 84 | input = String.to_charlist(input) 85 | expected = String.to_charlist(expected) 86 | secure_compare(input, expected) 87 | end 88 | defp secure_equals?(_, _), do: false 89 | 90 | defp secure_compare(acc \\ 0, input, expected) 91 | defp secure_compare(acc, [], []), do: acc == 0 92 | defp secure_compare(acc, [input_codepoint | input], [expected_codepoint | expected]) do 93 | import Bitwise 94 | acc 95 | |> bor(input_codepoint ^^^ expected_codepoint) 96 | |> secure_compare(input, expected) 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Stripe.Mixfile do 2 | use Mix.Project 3 | 4 | @version "0.8.0" 5 | 6 | def project do 7 | [app: :stripe, 8 | version: @version, 9 | elixir: "~> 1.5", 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | elixirc_paths: elixirc_paths(Mix.env), 13 | deps: deps(), 14 | package: package(), 15 | description: description()] 16 | end 17 | 18 | def description do 19 | "Stripe API Client for Elixir" 20 | end 21 | 22 | def package do 23 | [ 24 | name: "stripe_elixir", 25 | maintainers: ["Sikan He"], 26 | licenses: ["Apache 2.0"], 27 | links: %{"GitHub" => "https://github.com/sikanhe/stripe-elixir"} 28 | ] 29 | end 30 | 31 | defp elixirc_paths(:test), do: ["lib", "test/support"] 32 | defp elixirc_paths(_), do: ["lib"] 33 | 34 | def application do 35 | [extra_applications: [:logger]] 36 | end 37 | 38 | defp deps do 39 | [ 40 | {:httpoison, "~> 0.11"}, 41 | {:poison, "~> 2.2 or ~> 3.0"}, 42 | 43 | # Docs 44 | {:ex_doc, "~> 0.18.0", only: :dev}, 45 | {:earmark, "~> 1.2.0", only: :dev}, 46 | {:inch_ex, ">= 0.0.0", only: :dev} 47 | ] 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], [], "hexpm"}, 2 | "earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [:mix], [], "hexpm"}, 3 | "ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "hackney": {:hex, :hackney, "1.10.1", "c38d0ca52ea80254936a32c45bb7eb414e7a96a521b4ce76d00a69753b157f21", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "httpoison": {:hex, :httpoison, "0.13.0", "bfaf44d9f133a6599886720f3937a7699466d23bb0cd7a88b6ba011f53c6f562", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, 7 | "inch_ex": {:hex, :inch_ex, "0.5.6", "418357418a553baa6d04eccd1b44171936817db61f4c0840112b420b8e378e67", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, 8 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, 9 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, 10 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, 11 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, 12 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}} 13 | -------------------------------------------------------------------------------- /test/balance_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Stripe.BalanceTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Stripe.Balance 5 | 6 | test "retrieve balance" do 7 | assert {:ok, %{"object" => "balance"}} = Balance.retrieve() 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/balance_transaction_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Stripe.BalanceTransactionTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Stripe.BalanceTransaction 5 | alias Stripe.InvalidRequestError 6 | 7 | test "retrieve a balance transaction" do 8 | assert {:error, %InvalidRequestError{message: "No such balance transaction: not exist"}} 9 | = BalanceTransaction.retrieve("not exist") 10 | end 11 | 12 | test "list all balance transaction" do 13 | assert {:ok, %{"object" => "list", "url" => "/v1/balance/history"}} 14 | = BalanceTransaction.all 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/charge_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Stripe.ChargeTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Stripe.Charge 5 | alias Stripe.InvalidRequestError 6 | 7 | test "create a charge" do 8 | assert {:error, %InvalidRequestError{message: "Must provide source or customer."}} 9 | = Charge.create([]) 10 | end 11 | 12 | test "retrieve a charge" do 13 | assert {:error, %InvalidRequestError{message: "No such charge: not exist"}} 14 | = Charge.retrieve("not exist") 15 | end 16 | 17 | test "list all charges" do 18 | assert {:ok, %{"object" => "list", "url" => "/v1/charges"}} 19 | = Charge.list 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/connect/account_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Stripe.AccountTest do 2 | use ExUnit.Case 3 | 4 | test "create/update/retrieve/delete an account" do 5 | assert {:ok, account} = Stripe.Account.create(type: "custom") 6 | assert {:ok, %{"email" => _email}} = Stripe.Account.retrieve(account["id"]) 7 | assert {:ok, %{"metadata" => %{"test" => "data"}}} = 8 | Stripe.Account.update(account["id"], metadata: [test: "data"]) 9 | assert {:ok, _} = Stripe.Account.delete(account["id"]) 10 | assert {:error, _} = Stripe.Account.retrieve(account["id"]) 11 | end 12 | 13 | test "list all accounts" do 14 | assert {:ok, %{"object" => "list", "url" => "/v1/accounts"}} = Stripe.Account.list 15 | end 16 | 17 | test "create/update/retrieve/delete/list an external_account" do 18 | {:ok, account} = Stripe.Account.create(type: "custom") 19 | 20 | {:ok, token} = Stripe.Token.create( 21 | card: [ 22 | number: "4000056655665556", 23 | exp_month: 7, 24 | exp_year: 2018, 25 | cvc: "314", 26 | currency: "usd" 27 | ] 28 | ) 29 | 30 | {:ok, token2} = Stripe.Token.create( 31 | card: [ 32 | number: "5200828282828210", 33 | exp_month: 7, 34 | exp_year: 2018, 35 | cvc: "314", 36 | currency: "usd" 37 | ] 38 | ) 39 | 40 | account_id = account["id"] 41 | 42 | assert {:ok, external_account} = 43 | Stripe.Account.create_external_account(account_id, external_account: token["id"]) 44 | assert {:ok, external_account2} = 45 | Stripe.Account.create_external_account(account_id, external_account: token2["id"]) 46 | 47 | url = "/v1/accounts/#{account_id}/external_accounts" 48 | 49 | assert {:ok, %{"object" => "list", "url" => ^url}} 50 | = Stripe.Account.list_external_account(account_id) 51 | 52 | external_account_id = external_account["id"] 53 | external_account_id2 = external_account2["id"] 54 | 55 | assert {:ok, ^external_account} = Stripe.Account.retrieve_external_account(account_id, external_account_id) 56 | assert {:ok, %{"metadata" => %{"test" => "data"}}} = 57 | Stripe.Account.update_external_account(account_id, external_account_id, metadata: [test: "data"]) 58 | assert {:ok, _} = Stripe.Account.delete_external_account(account_id, external_account_id2) 59 | assert {:error, _} = Stripe.Account.retrieve_external_account(account_id, external_account_id2) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/connect/application_fee_test.exs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sikanhe/stripe-elixir/dd5589854f608ede4fa0e54ac9cf959b698602a4/test/connect/application_fee_test.exs -------------------------------------------------------------------------------- /test/connect/country_spec_test.exs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sikanhe/stripe-elixir/dd5589854f608ede4fa0e54ac9cf959b698602a4/test/connect/country_spec_test.exs -------------------------------------------------------------------------------- /test/connect/recipient_test.exs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sikanhe/stripe-elixir/dd5589854f608ede4fa0e54ac9cf959b698602a4/test/connect/recipient_test.exs -------------------------------------------------------------------------------- /test/connect_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Stripe.ConnectTest do 2 | use ExUnit.Case 3 | 4 | test "Stripe.Connect.authorize_url/1 should generate urls with correct query params" do 5 | url = Stripe.Connect.authorize_url( 6 | client_id: "TEST_CLIENT_ID", 7 | state: "TEST_CSRF_TOKEN", 8 | redirect_uri: "TEST_REDIRECT_URL" 9 | ) 10 | 11 | assert url =~ "client_id=TEST_CLIENT_ID" 12 | assert url =~ "state=TEST_CSRF_TOKEN" 13 | assert url =~ "redirect_uri=TEST_REDIRECT_URL" 14 | assert url =~ "scope=read_write" 15 | assert url =~ "response_type=code" 16 | end 17 | 18 | test "Stripe.Connect.authorize_url/1 should raise AuthenticationError when client_id is not set" do 19 | assert_raise Stripe.AuthenticationError, fn -> Stripe.Connect.authorize_url() end 20 | end 21 | 22 | test "retrive method should be able to recognize stripe_account option" do 23 | assert {:error, api_error} = 24 | Stripe.Customer.retrieve("abc", stripe_account: "acct_19tNirFUbEJstA5") 25 | 26 | assert api_error.message =~ "does not have access to account" 27 | end 28 | 29 | test "create method should be able to recognize stripe_account option" do 30 | assert {:error, api_error} = 31 | Stripe.Customer.create([], stripe_account: "acct_19tNirFUbEJstA5") 32 | 33 | assert api_error.message =~ "does not have access to account" 34 | end 35 | 36 | test "update method should be able to recognize stripe_account option" do 37 | assert {:error, api_error} = 38 | Stripe.Customer.update("abc", [], stripe_account: "acct_19tNirFUbEJstA5") 39 | 40 | assert api_error.message =~ "does not have access to account" 41 | end 42 | 43 | test "delete method should be able to recognize stripe_account option" do 44 | assert {:error, api_error} = 45 | Stripe.Customer.delete("abc", [], stripe_account: "acct_19tNirFUbEJstA5") 46 | 47 | assert api_error.message =~ "does not have access to account" 48 | end 49 | end -------------------------------------------------------------------------------- /test/customer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Stripe.CustomerTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Stripe.Fixture.Token, as: TokenFixture 5 | alias Stripe.{Customer, Token} 6 | alias Stripe.InvalidRequestError 7 | 8 | setup do 9 | assert {:ok, customer} = Customer.create([]) 10 | 11 | {:ok, customer: customer} 12 | end 13 | 14 | test "create a customer", %{customer: customer} do 15 | assert %{"id" => _} = customer 16 | end 17 | 18 | test "retrieve a customer", %{customer: customer} do 19 | assert {:ok, ^customer} = Customer.retrieve(customer["id"]) 20 | end 21 | 22 | test "update a customer", %{customer: customer} do 23 | assert {:ok, %{"email" => "hello@gmail.com"}} = 24 | Customer.update(customer["id"], email: "hello@gmail.com") 25 | end 26 | 27 | test "list all customers" do 28 | assert {:ok, %{"object" => "list", "url" => "/v1/customers"}} 29 | = Customer.list 30 | end 31 | 32 | test "list cards", %{customer: customer} do 33 | {:ok, token} = TokenFixture.valid_card() |> Token.create() 34 | 35 | assert {:ok, %{"id" => _source_id, "object" => "card"}} = 36 | Customer.create_card(customer["id"], token["id"]) 37 | 38 | customer_sources_url = "/v1/customers/#{customer["id"]}/sources" 39 | 40 | assert {:ok, %{"data" => data, 41 | "object" => "list", 42 | "url" => ^customer_sources_url}} = 43 | Customer.list_cards(customer["id"]) 44 | 45 | assert length(data) > 0 46 | end 47 | 48 | test "add/update/delete a card to a customer", %{customer: customer} do 49 | {:ok, token} = TokenFixture.valid_card() |> Token.create() 50 | 51 | assert {:ok, %{"id" => source_id, "object" => "card"}} = 52 | Customer.create_card(customer["id"], token["id"]) 53 | 54 | assert {:ok, %{"name" => "new name"}} = 55 | Customer.update_card(customer["id"], source_id, name: "new name") 56 | 57 | assert {:ok, %{"deleted" => true}} = 58 | Customer.delete_card(customer["id"], source_id) 59 | end 60 | 61 | test "remove discount from customer", %{customer: customer} do 62 | assert {:error, %InvalidRequestError{message: "No active discount" <> _}} = 63 | Customer.delete_discount(customer["id"]) 64 | end 65 | 66 | test "delete a customer", %{customer: customer} do 67 | assert {:ok, %{"deleted" => true}} = Customer.delete(customer["id"]) 68 | end 69 | 70 | test "create and verify a bank account", %{customer: customer} do 71 | {:ok, %{"id" => token}} = Token.create( 72 | bank_account: %{ 73 | account_number: "000123456789", 74 | routing_number: "110000000", 75 | country: "US", 76 | currency: "usd", 77 | account_holder_name: "Sikan He", 78 | account_holder_type: "individual" 79 | } 80 | ) 81 | {:ok, %{"id" => source_id}} = Customer.create_source(customer["id"], source: token) 82 | {:ok, %{"status" => "verified"}} = Customer.verify_bank_account(customer["id"], source_id, [32, 45]) 83 | end 84 | 85 | test "list bank accounts", %{customer: customer} do 86 | {:ok, %{"id" => token}} = Token.create( 87 | bank_account: %{ 88 | account_number: "000123456789", 89 | routing_number: "110000000", 90 | country: "US", 91 | currency: "usd", 92 | account_holder_name: "Sikan He", 93 | account_holder_type: "individual" 94 | } 95 | ) 96 | 97 | {:ok, %{"id" => _source_id}} = Customer.create_source(customer["id"], source: token) 98 | 99 | customer_sources_url = "/v1/customers/#{customer["id"]}/sources" 100 | 101 | assert {:ok, %{"data" => data, 102 | "object" => "list", 103 | "url" => ^customer_sources_url}} = 104 | Customer.list_bank_accounts(customer["id"]) 105 | 106 | assert length(data) > 0 107 | end 108 | 109 | end 110 | -------------------------------------------------------------------------------- /test/event_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Stripe.EventTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Stripe.Event 5 | alias Stripe.InvalidRequestError 6 | 7 | test "retrieve an event" do 8 | assert {:error, %InvalidRequestError{message: "No such event: not exist"}} 9 | = Event.retrieve("not exist") 10 | end 11 | 12 | test "list all events" do 13 | assert {:ok, %{"object" => "list", "url" => "/v1/events"}} 14 | = Event.list 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/refund_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Stripe.RefundTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Stripe.{Token, Charge, Refund} 5 | alias Stripe.InvalidRequestError 6 | 7 | test "create/update a refund" do 8 | {:ok, token} = Token.create( 9 | card: [ 10 | number: "4242424242424242", 11 | exp_month: 7, 12 | exp_year: 2018, 13 | cvc: "314" 14 | ] 15 | ) 16 | 17 | {:ok, charge} = Charge.create(amount: 1000, source: token["id"], currency: "usd") 18 | 19 | assert {:ok, %{"amount" => 1000} = refund} = Refund.create(charge: charge["id"]) 20 | assert {:ok, %{"metadata" => %{"key" => "value"}}} = Refund.update(refund["id"], metadata: [key: "value"]) 21 | end 22 | 23 | test "retrieve a refund" do 24 | assert {:error, %InvalidRequestError{message: "No such refund: not exist"}} 25 | = Refund.retrieve("not exist") 26 | end 27 | 28 | test "list all refunds" do 29 | assert {:ok, %{"object" => "list", "url" => "/v1/refunds"}} 30 | = Refund.list 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/subscriptions/coupon_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Stripe.CouponTest do 2 | use ExUnit.Case, async: true 3 | alias Stripe.Coupon 4 | 5 | test "create/update/delete a coupon" do 6 | Coupon.delete("COUPONTEST") 7 | assert {:ok, coupon} = Coupon.create(id: "COUPONTEST", 8 | duration: "forever", 9 | percent_off: 50) 10 | 11 | assert {:ok, ^coupon} = Coupon.retrieve(coupon["id"]) 12 | assert {:ok, %{"metadata" => %{"key" => "value"}}} = 13 | Coupon.update(coupon["id"], metadata: [key: "value"]) 14 | assert {:ok, %{"deleted" => true}} = Coupon.delete(coupon["id"]) 15 | end 16 | 17 | test "list all coupons" do 18 | assert {:ok, %{"object" => "list"}} = Coupon.list 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/subscriptions/invoice_item_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Stripe.InvoiceItemTest do 2 | use ExUnit.Case 3 | alias Stripe.{Customer, InvoiceItem} 4 | 5 | setup do 6 | {:ok, customer} = Customer.create([]) 7 | 8 | {:ok, invoice_item} = InvoiceItem.create(customer: customer["id"], 9 | amount: 100, 10 | currency: "usd", 11 | description: "invoice item test") 12 | 13 | {:ok, invoice_item: invoice_item} 14 | end 15 | 16 | test "create invoice item", %{invoice_item: invoice_item} do 17 | assert %{"object" => "invoiceitem"} = invoice_item 18 | end 19 | 20 | test "update invoice item", %{invoice_item: invoice_item} do 21 | assert {:ok, %{"amount" => 99}} = InvoiceItem.update(invoice_item["id"], 22 | amount: 99) 23 | end 24 | 25 | test "retrieve an invoice item", %{invoice_item: invoice_item} do 26 | assert {:ok, ^invoice_item} = InvoiceItem.retrieve(invoice_item["id"]) 27 | end 28 | 29 | test "delete an invoice item", %{invoice_item: invoice_item} do 30 | assert {:ok, %{"deleted" => true}} = InvoiceItem.delete(invoice_item["id"]) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/subscriptions/invoice_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Stripe.InvoiceTest do 2 | use ExUnit.Case, async: true 3 | alias Stripe.Fixture.Token, as: TokenFixture 4 | alias Stripe.{Customer, Token, Invoice, InvoiceItem} 5 | 6 | setup_all do 7 | {:ok, customer} = Customer.create([]) 8 | 9 | {:ok, card} = TokenFixture.valid_card() |> Token.create() 10 | 11 | Customer.create_card(customer["id"], card["id"]) 12 | 13 | {:ok, _invoice_item} = InvoiceItem.create(customer: customer["id"], 14 | amount: 100, 15 | currency: "usd", 16 | description: "invoice test") 17 | 18 | {:ok, invoice} = Invoice.create(customer: customer["id"]) 19 | 20 | {:ok, invoice: invoice, customer: customer} 21 | end 22 | 23 | test "create an invoice", %{invoice: invoice} do 24 | assert %{"amount_due" => _} = invoice 25 | end 26 | # 27 | test "retrive an invoice", %{invoice: %{"id" => invoice_id}} do 28 | assert {:ok, %{"id" => ^invoice_id}} = Invoice.retrieve(invoice_id) 29 | end 30 | 31 | test "update an invoice", %{invoice: invoice} do 32 | assert {:ok, %{"metadata" => %{"key" => "value"}}} = 33 | Invoice.update(invoice["id"], metadata: [key: "value"]) 34 | end 35 | 36 | test "pay an invoice", %{invoice: invoice} do 37 | assert {:ok, %{"paid" => true}} = Invoice.pay(invoice["id"]) 38 | end 39 | 40 | test "list all invoices" do 41 | assert {:ok, %{"object" => "list", "url" => "/v1/invoices"}} = Invoice.list 42 | end 43 | 44 | test "retrieve upcoming invoices for a customer", %{customer: %{"id" => cus_id}} do 45 | assert {:error, %Stripe.InvalidRequestError{message: "No upcoming invoices for customer: " <> ^cus_id}} = 46 | Invoice.upcoming(customer: cus_id) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/subscriptions/plan_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Stripe.PlanTest do 2 | use ExUnit.Case, async: true 3 | alias Stripe.Plan 4 | alias Stripe.InvalidRequestError 5 | 6 | test "create/update/delete a plan" do 7 | Plan.delete("plan_test") 8 | assert {:ok, plan} = Plan.create(name: "plan_test", 9 | id: "plan_test", 10 | amount: 1, 11 | currency: "usd", 12 | interval: "month") 13 | 14 | assert {:ok, ^plan} = Plan.retrieve("plan_test") 15 | assert {:ok, %{"name" => "premium"}} = Plan.update("plan_test", name: "premium") 16 | assert {:ok, %{"deleted" => true}} = Plan.delete("plan_test") 17 | assert {:error, %InvalidRequestError{message: "No such plan: plan_test"}} = 18 | Plan.retrieve("plan_test") 19 | end 20 | 21 | test "list all plans" do 22 | assert {:ok, %{"object" => "list", "url" => "/v1/plans"}} = Plan.list 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/subscriptions/subscription_item_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Stripe.SubscriptionItemTest do 2 | use ExUnit.Case, async: true 3 | alias Stripe.{Subscription, SubscriptionItem, Plan, Customer, Token} 4 | alias Stripe.InvalidRequestError 5 | alias Stripe.Fixture.Token, as: TokenFixture 6 | 7 | @addon_id "addon_plan" 8 | 9 | setup do 10 | {:ok, customer} = Customer.create([]) 11 | 12 | {:ok, card} = TokenFixture.valid_card() |> Token.create() 13 | 14 | Customer.create_card(customer["id"], card["id"]) 15 | 16 | Plan.delete(@addon_id) 17 | Plan.delete("sub_item_test_plan") 18 | {:ok, plan} = Plan.create(name: "sub_item_test_plan", 19 | id: "sub_item_test_plan", 20 | amount: 999, 21 | currency: "usd", 22 | interval: "month") 23 | 24 | {:ok, _addon_plan} = Plan.create(name: "addon_plan", 25 | id: @addon_id, 26 | amount: 1, 27 | currency: "usd", 28 | interval: "month") 29 | 30 | {:ok, subscription} = Subscription.create(customer: customer["id"], 31 | plan: plan["id"]) 32 | 33 | {:ok, subscription: subscription} 34 | end 35 | 36 | test "create/update/delete a subscription_item", %{subscription: subscription} do 37 | SubscriptionItem.delete("subscription_item_test") 38 | assert {:ok, subscription_item} = SubscriptionItem.create(subscription: subscription["id"], 39 | plan: @addon_id, 40 | quantity: 1) 41 | 42 | assert {:ok, ^subscription_item} = SubscriptionItem.retrieve(subscription_item["id"]) 43 | assert {:ok, %{"quantity" => 2}} = SubscriptionItem.update(subscription_item["id"], quantity: 2) 44 | assert {:ok, %{"deleted" => true}} = SubscriptionItem.delete(subscription_item["id"]) 45 | assert {:error, %InvalidRequestError{message: "Invalid subscription_item id" <> _}} = 46 | SubscriptionItem.retrieve(subscription_item["id"]) 47 | end 48 | 49 | test "list all subscription_items", %{subscription: subscription} do 50 | assert {:ok, %{"data" => list}} = SubscriptionItem.list(subscription: subscription["id"]) 51 | assert length(list) > 0 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/subscriptions/subscription_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Stripe.SubscriptionTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Stripe.Fixture.Token, as: TokenFixture 5 | alias Stripe.{Subscription, Plan, Customer, Token} 6 | alias Stripe.InvalidRequestError 7 | 8 | setup do 9 | {:ok, customer} = Customer.create([]) 10 | 11 | {:ok, card} = TokenFixture.valid_card() |> Token.create() 12 | 13 | Customer.create_card(customer["id"], card["id"]) 14 | 15 | Plan.delete("sub_test_plan") 16 | {:ok, plan} = Plan.create(name: "sub_test_plan", 17 | id: "sub_test_plan", 18 | amount: 999, 19 | currency: "usd", 20 | interval: "month") 21 | 22 | {:ok, subscription} = Subscription.create(customer: customer["id"], 23 | plan: plan["id"]) 24 | 25 | on_exit fn -> 26 | Plan.delete("sub_test_plan") 27 | end 28 | 29 | {:ok, subscription: subscription} 30 | end 31 | 32 | test "delete discount for a subscription", %{subscription: subscription} do 33 | assert {:error, %InvalidRequestError{message: "No active discount for subscription" <> _}} = 34 | Subscription.delete_discount(subscription["id"]) 35 | end 36 | 37 | test "create/update/delete a subscription", %{subscription: subscription} do 38 | assert {:ok, ^subscription} = Subscription.retrieve(subscription["id"]) 39 | 40 | assert {:ok, %{"metadata" => %{"key" => "value"}}} = 41 | Subscription.update(subscription["id"], metadata: [key: "value"]) 42 | 43 | assert {:ok, %{"status" => "active", "cancel_at_period_end" => true}} = 44 | Subscription.delete(subscription["id"], at_period_end: true) 45 | end 46 | 47 | test "list all subscriptions" do 48 | assert {:ok, %{"object" => "list", "url" => "/v1/subscriptions"}} = Subscription.list 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/support/token_fixture.ex: -------------------------------------------------------------------------------- 1 | defmodule Stripe.Fixture.Token do 2 | 3 | def valid_card() do 4 | %{card: [ 5 | number: "4242424242424242", 6 | exp_month: exp_month(), 7 | exp_year: exp_year(), 8 | cvc: "314" 9 | ]} 10 | end 11 | 12 | def invalid_card() do 13 | %{card: [ 14 | number: "invalid card number", 15 | exp_month: exp_month(), 16 | exp_year: exp_year(), 17 | cvc: "314" 18 | ]} 19 | end 20 | 21 | defp exp_month() do 22 | 7 23 | end 24 | 25 | defp exp_year() do 26 | DateTime.utc_now.year + 1 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/token_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Stripe.TokenTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Stripe.Fixture.Token, as: TokenFixture 5 | alias Stripe.Token 6 | alias Stripe.{InvalidRequestError, CardError} 7 | 8 | test "create a token" do 9 | assert {:ok, token} = TokenFixture.valid_card() |> Token.create() 10 | 11 | assert {:ok, ^token} = Token.retrieve(token["id"]) 12 | end 13 | 14 | test "card error" do 15 | assert {:error, %CardError{param: "number", code: "incorrect_number"}} = TokenFixture.invalid_card() |> Token.create() 16 | end 17 | 18 | test "retrieve a token" do 19 | assert {:error, %InvalidRequestError{message: "No such token: not exist"}} 20 | = Token.retrieve("not exist") 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/transfer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Stripe.TransferTest do 2 | use ExUnit.Case, async: true 3 | alias Stripe.Transfer 4 | alias Stripe.InvalidRequestError 5 | 6 | test "create a transfer" do 7 | assert {:error, %InvalidRequestError{param: "destination"}} = Transfer.create(amount: 100, 8 | currency: "usd", 9 | destination: "transfer_test") 10 | end 11 | 12 | test "retrieve a transfer" do 13 | assert {:error, %InvalidRequestError{message: "No such transfer: test"}} = 14 | Transfer.retrieve("test") 15 | end 16 | 17 | test "update a transfer" do 18 | assert {:error, %InvalidRequestError{message: "No such transfer: test"}} = 19 | Transfer.update("test", metadata: [key: "value"]) 20 | end 21 | 22 | test "list all transfers" do 23 | assert {:ok, %{"object" => "list", "url" => "/v1/transfers"}} = Transfer.list 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/webhook_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Stripe.WebhookTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Stripe.Webhook 5 | 6 | @payload ~S({"object": "event"}) 7 | 8 | @valid_scheme "v1" 9 | @invalid_scheme "v0" 10 | 11 | @secret "secret" 12 | 13 | defp generate_signature(timestamp, payload, secret \\ @secret) do 14 | :crypto.hmac(:sha256, secret, "#{timestamp}.#{payload}") 15 | |> Base.encode16(case: :lower) 16 | end 17 | 18 | defp create_signature_header(timestamp, scheme, signature) do 19 | "t=#{timestamp},#{scheme}=#{signature}" 20 | end 21 | 22 | test "payload with a valid signature should return event" do 23 | timestamp = System.system_time(:seconds) 24 | payload = @payload 25 | signature = generate_signature(timestamp, payload) 26 | signature_header = create_signature_header(timestamp, @valid_scheme, signature) 27 | 28 | assert {:ok, _} = Webhook.construct_event(payload, signature_header, @secret) 29 | end 30 | 31 | test "payload with invalid timestamp should fail" do 32 | timestamp = "abc" 33 | payload = @payload 34 | signature = generate_signature(timestamp, payload) 35 | signature_header = create_signature_header(timestamp, @valid_scheme, signature) 36 | 37 | assert {:error, %Stripe.SignatureVerificationError{}} = Webhook.construct_event(payload, signature_header, @secret) 38 | end 39 | 40 | test "payload with expired timestamp should fail" do 41 | timestamp = 1 42 | payload = @payload 43 | signature = generate_signature(timestamp, payload) 44 | signature_header = create_signature_header(timestamp, @valid_scheme, signature) 45 | 46 | assert {:error, %Stripe.SignatureVerificationError{}} = Webhook.construct_event(payload, signature_header, @secret) 47 | end 48 | 49 | test "payload with an invalid signature should fail" do 50 | timestamp = System.system_time(:seconds) 51 | payload = @payload 52 | signature = generate_signature(timestamp, "random") 53 | signature_header = create_signature_header(timestamp, @valid_scheme, signature) 54 | 55 | assert {:error, %Stripe.SignatureVerificationError{}} = Webhook.construct_event(payload, signature_header, @secret) 56 | end 57 | 58 | test "payload with wrong secret should fail" do 59 | timestamp = System.system_time(:seconds) 60 | payload = @payload 61 | signature = generate_signature(timestamp, payload, "wrong") 62 | signature_header = create_signature_header(timestamp, @valid_scheme, signature) 63 | 64 | assert {:error, %Stripe.SignatureVerificationError{}} = Webhook.construct_event(payload, signature_header, @secret) 65 | end 66 | 67 | test "payload with missing signature scheme should fail" do 68 | timestamp = System.system_time(:seconds) 69 | payload = @payload 70 | signature = generate_signature(timestamp, payload) 71 | signature_header = create_signature_header(timestamp, @invalid_scheme, signature) 72 | 73 | assert {:error, %Stripe.SignatureVerificationError{}} = Webhook.construct_event(payload, signature_header, @secret) 74 | end 75 | end 76 | --------------------------------------------------------------------------------