From 1bc93d78b3f6bc68aa02e2a62bbd70ec6a6fcdb5 Mon Sep 17 00:00:00 2001 From: Nick Kezhaya Date: Sun, 9 Jun 2019 17:38:05 -0500 Subject: [PATCH 1/3] Charge with customer/token data --- lib/stripe_mock/api/card.ex | 10 +-- lib/stripe_mock/api/charge.ex | 83 +++++++++++++++++-- lib/stripe_mock/api/customer.ex | 5 +- lib/stripe_mock/api/operations/card.ex | 1 + lib/stripe_mock/api/operations/charge.ex | 39 ++++++++- lib/stripe_mock/api/refund.ex | 8 +- lib/stripe_mock/api/token.ex | 8 +- lib/stripe_mock/repo.ex | 37 ++++++--- lib/stripe_mock/schema.ex | 9 ++ lib/stripe_mock/{ => type}/metadata.ex | 2 +- .../controllers/charge_controller.ex | 4 +- lib/stripe_mock_web/router.ex | 5 ++ lib/stripe_mock_web/views/charge_view.ex | 1 + .../controllers/charge_controller_test.exs | 42 +++++++++- .../controllers/source_controller_test.exs | 15 ---- test/test_helper.exs | 17 +++- 16 files changed, 225 insertions(+), 61 deletions(-) create mode 100644 lib/stripe_mock/schema.ex rename lib/stripe_mock/{ => type}/metadata.ex (93%) diff --git a/lib/stripe_mock/api/card.ex b/lib/stripe_mock/api/card.ex index 2be5fca..0e2e67a 100644 --- a/lib/stripe_mock/api/card.ex +++ b/lib/stripe_mock/api/card.ex @@ -1,13 +1,11 @@ defmodule StripeMock.API.Card do - use Ecto.Schema - import Ecto.Changeset - alias StripeMock.API + use StripeMock.Schema schema "cards" do field :brand, :string field :created, :integer field :deleted, :boolean - field :metadata, StripeMock.Metadata, default: %{} + field :metadata, StripeMock.Type.Metadata, default: %{} field :last4, :string field :source, :string @@ -24,8 +22,6 @@ defmodule StripeMock.API.Card do card |> cast(attrs, [:source, :metadata]) |> validate_required([:source, :metadata]) - |> set_brand() - |> set_last4() end @doc false @@ -69,7 +65,7 @@ defmodule StripeMock.API.Card do defp set_last4(changeset) do case get_field(changeset, :number) do number when is_bitstring(number) -> - last4 = String.split_at(number, -6) |> elem(1) + last4 = String.split_at(number, -4) |> elem(1) put_change(changeset, :last4, last4) _ -> diff --git a/lib/stripe_mock/api/charge.ex b/lib/stripe_mock/api/charge.ex index dbe2c42..0dc45f8 100644 --- a/lib/stripe_mock/api/charge.ex +++ b/lib/stripe_mock/api/charge.ex @@ -1,8 +1,6 @@ defmodule StripeMock.API.Charge do - use Ecto.Schema - import Ecto.Changeset - - alias StripeMock.API + use StripeMock.Schema + alias StripeMock.Repo @foreign_key_type :binary_id schema "charges" do @@ -10,11 +8,12 @@ defmodule StripeMock.API.Charge do field :capture, :boolean, default: false field :currency, :string field :description, :string - field :metadata, StripeMock.Metadata, default: %{} + field :metadata, StripeMock.Type.Metadata, default: %{} field :statement_descriptor, :string field :transfer_group, :string belongs_to :customer, API.Customer + belongs_to :source, API.Card end @doc false @@ -27,10 +26,13 @@ defmodule StripeMock.API.Charge do :customer_id, :description, :metadata, + :source_id, :statement_descriptor, :transfer_group ]) |> validate_required([:amount, :currency]) + |> set_customer_and_source() + |> validate_required(:source_id) end @doc false @@ -44,4 +46,75 @@ defmodule StripeMock.API.Charge do ]) |> validate_required([:amount, :currency]) end + + defp set_customer_and_source(changeset) do + customer = + with customer_id when not is_nil(customer_id) <- get_field(changeset, :customer_id) do + case Repo.fetch(API.Customer, customer_id) do + {:ok, customer} -> customer + _ -> throw({:not_found, :customer_id}) + end + else + _ -> nil + end + + source = + case fetch_source(get_field(changeset, :source_id)) do + :invalid -> throw({:not_found, :source_id}) + source -> source + end + + case {get_field(changeset, :customer_id), get_field(changeset, :source_id)} do + {nil, nil} -> + validate_required(changeset, :source_id) + + {nil, _source_id} -> + case source do + %{card: %{customer_id: nil}} -> changeset + _ -> validate_required(changeset, :customer_id) + end + + {customer_id, nil} -> + # TODO: Get the default payment method. + API.Card + |> Repo.all() + |> Enum.filter(&(&1.customer_id == customer_id)) + |> case do + [source | _] -> put_change(changeset, :source_id, source.id) + [] -> throw({:not_found, :source_id}) + end + + {nil, "card_" <> _} -> + throw({:not_found, :source_id}) + + {_customer_id, "card_" <> _} -> + if source.customer_id != customer.id do + throw({:not_found, :source_id}) + else + changeset + end + + {_customer_id, "tok_" <> _} -> + changeset + + _ -> + add_error(changeset, :base, "either source or customer is required") + end + |> ensure_source() + catch + {:not_found, field} -> add_error(changeset, field, "not found") + end + + defp ensure_source(changeset) do + with source when is_map(source) <- fetch_source(get_field(changeset, :source_id)) do + changeset + else + _ -> add_error(changeset, :source, "is invalid") + end + end + + defp fetch_source("tok_" <> _ = token_id), do: Repo.get(API.Token, token_id) + defp fetch_source("card_" <> _ = card_id), do: Repo.get(API.Card, card_id) + defp fetch_source(nil), do: nil + defp fetch_source(_), do: :invalid end diff --git a/lib/stripe_mock/api/customer.ex b/lib/stripe_mock/api/customer.ex index a8b3eb3..29685c1 100644 --- a/lib/stripe_mock/api/customer.ex +++ b/lib/stripe_mock/api/customer.ex @@ -1,6 +1,5 @@ defmodule StripeMock.API.Customer do - use Ecto.Schema - import Ecto.Changeset + use StripeMock.Schema @primary_key {:id, :binary_id, autogenerate: false} schema "customers" do @@ -9,7 +8,7 @@ defmodule StripeMock.API.Customer do field :deleted, :boolean, default: false field :description, :string field :email, :string - field :metadata, StripeMock.Metadata, default: %{} + field :metadata, StripeMock.Type.Metadata, default: %{} field :name, :string field :phone, :string end diff --git a/lib/stripe_mock/api/operations/card.ex b/lib/stripe_mock/api/operations/card.ex index ff47a2b..e608189 100644 --- a/lib/stripe_mock/api/operations/card.ex +++ b/lib/stripe_mock/api/operations/card.ex @@ -8,6 +8,7 @@ defmodule StripeMock.API.Operations.Card do |> Enum.filter(&(&1.customer_id == customer.id)) end + def get_card(id), do: Repo.get(Card, id) def get_card!(id), do: Repo.get!(Card, id) def create_card(customer, attrs) do diff --git a/lib/stripe_mock/api/operations/charge.ex b/lib/stripe_mock/api/operations/charge.ex index bb8a44b..18faa66 100644 --- a/lib/stripe_mock/api/operations/charge.ex +++ b/lib/stripe_mock/api/operations/charge.ex @@ -1,24 +1,55 @@ defmodule StripeMock.API.Operations.Charge do alias StripeMock.Repo - alias StripeMock.API.Charge + alias StripeMock.API.{Card, Charge, Token} - def list_charges do - Repo.all(Charge) + def list_charges() do + Charge + |> Repo.all() + |> preload_source() end def get_charge(id) do - Repo.fetch(Charge, id) + with {:ok, charge} <- Repo.fetch(Charge, id) do + {:ok, preload_source(charge)} + end end + defp preload_source(charges) when is_list(charges) do + Enum.map(charges, &preload_source/1) + end + + defp preload_source({:ok, charge}) do + {:ok, preload_source(charge)} + end + + defp preload_source(%Charge{} = charge) do + %{charge | source: fetch_source(charge.source_id)} + end + + defp preload_source(any), do: any + def create_charge(attrs \\ %{}) do %Charge{} |> Charge.create_changeset(attrs) |> Repo.insert() + |> preload_source() end def update_charge(%Charge{} = charge, attrs) do charge |> Charge.create_changeset(attrs) |> Repo.update() + |> preload_source() end + + defp fetch_source("card_" <> _ = card_id), do: Repo.get(Card, card_id) + + defp fetch_source("tok_" <> _ = token_id) do + case Repo.get(Token, token_id) do + %{card_id: card_id} -> fetch_source(card_id) + _ -> nil + end + end + + defp fetch_source(nil), do: nil end diff --git a/lib/stripe_mock/api/refund.ex b/lib/stripe_mock/api/refund.ex index f645644..8aa5d7f 100644 --- a/lib/stripe_mock/api/refund.ex +++ b/lib/stripe_mock/api/refund.ex @@ -1,14 +1,12 @@ defmodule StripeMock.API.Refund do - use Ecto.Schema - import Ecto.Changeset - - alias StripeMock.{API, Repo} + use StripeMock.Schema + alias StripeMock.Repo @foreign_key_type :binary_id schema "refunds" do field :amount, :integer field :created, :integer - field :metadata, StripeMock.Metadata, default: %{} + field :metadata, StripeMock.Type.Metadata, default: %{} field :reason, :string belongs_to :charge, API.Charge diff --git a/lib/stripe_mock/api/token.ex b/lib/stripe_mock/api/token.ex index 3cae8e1..f37e7c3 100644 --- a/lib/stripe_mock/api/token.ex +++ b/lib/stripe_mock/api/token.ex @@ -1,7 +1,5 @@ defmodule StripeMock.API.Token do - use Ecto.Schema - import Ecto.Changeset - alias StripeMock.API + use StripeMock.Schema @foreign_key_type :binary_id schema "tokens" do @@ -16,10 +14,10 @@ defmodule StripeMock.API.Token do @doc false def changeset(token, attrs) do token - |> cast(attrs, [:client_ip]) + |> cast(attrs, [:client_ip, :type]) |> cast_assoc(:card, with: &API.Card.token_changeset/2) |> set_type() - |> validate_required([:client_ip, :type]) + |> validate_required([:type]) end defp set_type(changeset) do diff --git a/lib/stripe_mock/repo.ex b/lib/stripe_mock/repo.ex index 29b537f..8dd86c3 100644 --- a/lib/stripe_mock/repo.ex +++ b/lib/stripe_mock/repo.ex @@ -6,7 +6,7 @@ defmodule StripeMock.Repo do State structure is: %{ - customers: %{ + customer: %{ "cus_123123" => %Customer{} } } @@ -27,7 +27,7 @@ defmodule StripeMock.Repo do def get!(schema, id) do case GenServer.call(pid(), {:get, schema, id}) do - nil -> raise "Not found." + nil -> raise "No #{schema} found with id #{id}." record -> record end end @@ -55,7 +55,7 @@ defmodule StripeMock.Repo do @impl true def handle_call({:all, schema}, _from, state) do - case Map.get(state, schema) do + case Map.get(state, type(schema)) do schemas when is_map(schemas) -> {:reply, Map.values(schemas), state} _ -> {:reply, [], state} end @@ -72,7 +72,7 @@ defmodule StripeMock.Repo do @impl true def handle_call({:get, schema, id}, _from, state) do object = - with schemas when is_map(schemas) <- Map.get(state, schema), + with schemas when is_map(schemas) <- Map.get(state, type(schema)), object when is_map(object) <- Map.get(schemas, id) do object else @@ -111,6 +111,8 @@ defmodule StripeMock.Repo do changeset end + changeset = set_client_ip(changeset) + # Run through changed assocs and save {changeset, state} = Enum.reduce_while(changeset.changes, {changeset, state}, fn @@ -140,7 +142,7 @@ defmodule StripeMock.Repo do object = apply_changes(changeset) # Put the object into the new state - type = object.__struct__ + type = type(object) type_map = case Map.get(state, type) do @@ -157,15 +159,30 @@ defmodule StripeMock.Repo do {:error, changeset} end + if Mix.env() == :test do + defp set_client_ip(changeset) do + if Map.has_key?(changeset.data, :client_ip) and is_nil(get_field(changeset, :client_ip)) do + put_change(changeset, :client_ip, "0.0.0.0") + else + changeset + end + end + else + defp set_client_ip(changeset) do + changeset + end + end + defp generate_id(schema) do prefix(schema) <> "_" <> StripeMock.ID.generate() end - def type(%API.Card{}), do: :card - def type(%API.Charge{}), do: :charge - def type(%API.Customer{}), do: :customer - def type(%API.Refund{}), do: :refund - def type(%API.Token{}), do: :token + def type(%{} = map), do: type(map.__struct__) + def type(API.Card), do: :card + def type(API.Charge), do: :charge + def type(API.Customer), do: :customer + def type(API.Refund), do: :refund + def type(API.Token), do: :token def prefix(%API.Card{}), do: "card" def prefix(%API.Charge{}), do: "ch" diff --git a/lib/stripe_mock/schema.ex b/lib/stripe_mock/schema.ex new file mode 100644 index 0000000..aec9302 --- /dev/null +++ b/lib/stripe_mock/schema.ex @@ -0,0 +1,9 @@ +defmodule StripeMock.Schema do + defmacro __using__(_) do + quote do + use Ecto.Schema + import Ecto.Changeset, warn: false + alias StripeMock.API + end + end +end diff --git a/lib/stripe_mock/metadata.ex b/lib/stripe_mock/type/metadata.ex similarity index 93% rename from lib/stripe_mock/metadata.ex rename to lib/stripe_mock/type/metadata.ex index 681ba9d..a903918 100644 --- a/lib/stripe_mock/metadata.ex +++ b/lib/stripe_mock/type/metadata.ex @@ -1,4 +1,4 @@ -defmodule StripeMock.Metadata do +defmodule StripeMock.Type.Metadata do @behaviour Ecto.Type def type(), do: :map diff --git a/lib/stripe_mock_web/controllers/charge_controller.ex b/lib/stripe_mock_web/controllers/charge_controller.ex index 1fda950..9de5c85 100644 --- a/lib/stripe_mock_web/controllers/charge_controller.ex +++ b/lib/stripe_mock_web/controllers/charge_controller.ex @@ -4,7 +4,9 @@ defmodule StripeMockWeb.ChargeController do alias StripeMock.API alias StripeMock.API.Charge - plug SMPlug.ConvertParams, %{"customer" => "customer_id"} when action in [:create, :update] + plug SMPlug.ConvertParams, + %{"customer" => "customer_id", "source" => "source_id"} when action in [:create, :update] + action_fallback StripeMockWeb.FallbackController def index(conn, params) do diff --git a/lib/stripe_mock_web/router.ex b/lib/stripe_mock_web/router.ex index b5ec4ce..13f2d9f 100644 --- a/lib/stripe_mock_web/router.ex +++ b/lib/stripe_mock_web/router.ex @@ -11,11 +11,16 @@ defmodule StripeMockWeb.Router do resources "/customers", CustomerController do resources "/sources", SourceController + post "/sources/:id", SourceController, :update end resources "/charges", ChargeController, except: [:delete] resources "/refunds", RefundController, except: [:delete] resources "/sources", SourceController, only: [:show] resources "/tokens", TokenController, only: [:create, :show] + + post "/charges/:id", ChargeController, :update + post "/customers/:id", CustomerController, :update + post "/refunds/:id", RefundController, :update end end diff --git a/lib/stripe_mock_web/views/charge_view.ex b/lib/stripe_mock_web/views/charge_view.ex index 4679e97..5c560e5 100644 --- a/lib/stripe_mock_web/views/charge_view.ex +++ b/lib/stripe_mock_web/views/charge_view.ex @@ -20,6 +20,7 @@ defmodule StripeMockWeb.ChargeView do description: charge.description, metadata: charge.metadata, object: "charge", + source: render(StripeMockWeb.CardView, "card.json", card: charge.source), statement_descriptor: charge.statement_descriptor, transfer_group: charge.transfer_group } diff --git a/test/stripe_mock_web/controllers/charge_controller_test.exs b/test/stripe_mock_web/controllers/charge_controller_test.exs index 3dd9ff4..7e79b31 100644 --- a/test/stripe_mock_web/controllers/charge_controller_test.exs +++ b/test/stripe_mock_web/controllers/charge_controller_test.exs @@ -5,8 +5,11 @@ defmodule StripeMockWeb.ChargeControllerTest do alias StripeMock.API.Charge setup :create_customer + setup :create_card describe "index" do + setup :create_charge + test "lists all charges", %{conn: conn} do conn = get(conn, Routes.charge_path(conn, :index)) assert is_list(json_response(conn, 200)["data"]) @@ -14,8 +17,16 @@ defmodule StripeMockWeb.ChargeControllerTest do end describe "create charge" do - test "renders charge when data is valid", %{conn: conn, customer: customer} do - conn = post(conn, Routes.charge_path(conn, :create), create_attrs(customer.id)) + setup :create_token + + test "renders charge when the customer and card are valid", %{ + conn: conn, + customer: customer, + token: token + } do + params = create_attrs() |> Map.merge(%{customer_id: customer.id, source: token.id}) + + conn = post(conn, Routes.charge_path(conn, :create), params) assert %{"id" => id} = json_response(conn, 201) conn = get(conn, Routes.charge_path(conn, :show, id)) @@ -33,6 +44,29 @@ defmodule StripeMockWeb.ChargeControllerTest do } = json_response(conn, 200) end + test "renders charge when the token is valid and no customer is provided", %{ + conn: conn, + token: token + } do + params = create_attrs() |> Map.merge(%{source: token.id}) + conn = post(conn, Routes.charge_path(conn, :create), params) + assert %{"id" => id} = json_response(conn, 201) + + conn = get(conn, Routes.charge_path(conn, :show, id)) + + assert %{ + "id" => id, + "amount" => 5000, + "capture" => true, + "currency" => "some currency", + "customer" => nil, + "description" => "some description", + "metadata" => %{}, + "statement_descriptor" => "some statement_descriptor", + "transfer_group" => "some transfer_group" + } = json_response(conn, 200) + end + test "renders errors when data is invalid", %{conn: conn} do conn = post(conn, Routes.charge_path(conn, :create), invalid_attrs()) assert json_response(conn, 422)["errors"] != %{} @@ -60,12 +94,11 @@ defmodule StripeMockWeb.ChargeControllerTest do end end - def create_attrs(customer_id) do + def create_attrs() do %{ amount: 5000, capture: true, currency: "some currency", - customer: customer_id, description: "some description", metadata: %{}, statement_descriptor: "some statement_descriptor", @@ -88,6 +121,7 @@ defmodule StripeMockWeb.ChargeControllerTest do customer: nil, description: nil, metadata: nil, + source: nil, statement_descriptor: nil, transfer_group: nil } diff --git a/test/stripe_mock_web/controllers/source_controller_test.exs b/test/stripe_mock_web/controllers/source_controller_test.exs index 11c7496..43ad622 100644 --- a/test/stripe_mock_web/controllers/source_controller_test.exs +++ b/test/stripe_mock_web/controllers/source_controller_test.exs @@ -2,7 +2,6 @@ defmodule StripeMockWeb.SourceControllerTest do use StripeMockWeb.ConnCase @moduletag :source - alias StripeMock.API alias StripeMock.API.Card alias StripeMock.CardFixture @@ -86,18 +85,4 @@ defmodule StripeMockWeb.SourceControllerTest do assert response["deleted"] end end - - defp create_card(%{conn: conn, customer: customer}) do - %{"id" => token} = - conn - |> post(Routes.token_path(conn, :create), card: @card_attrs) - |> json_response(201) - - %{"id" => id, "object" => "card"} = - conn - |> post(Routes.customer_source_path(conn, :create, customer.id), source: token) - |> json_response(201) - - [card: API.get_card!(id)] - end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 08b5316..984ed98 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -7,6 +7,12 @@ defmodule StripeMock.TestHelper do name: "Foo" } + def create_card(%{customer: customer} = ctx) do + [token: token] = create_token(ctx) + {:ok, card} = API.create_card(customer, %{source: token.id}) + [card: card] + end + def create_customer() do {:ok, customer} = API.create_customer(@create_attrs) customer @@ -21,14 +27,17 @@ defmodule StripeMock.TestHelper do end def create_charge(%API.Customer{} = customer) do + [token: token] = create_token() + {:ok, charge} = API.create_charge(%{ amount: 5000, capture: true, currency: "some currency", - customer: customer.id, + customer_id: customer.id, description: "some description", metadata: %{}, + source_id: token.id, statement_descriptor: "some statement_descriptor", transfer_group: "some transfer_group" }) @@ -47,4 +56,10 @@ defmodule StripeMock.TestHelper do {:ok, refund} = API.create_refund(params) {:ok, refund: refund} end + + def create_token(_ctx \\ %{}) do + params = %{card: StripeMock.CardFixture.valid_card(), client_ip: "0.0.0.0"} + {:ok, token} = API.create_token(params) + [token: token] + end end From 51441ecb57920e344a959dfd9b8845049174bf0f Mon Sep 17 00:00:00 2001 From: Nick Kezhaya Date: Sun, 9 Jun 2019 17:46:36 -0500 Subject: [PATCH 2/3] Add outcome to charge view --- lib/stripe_mock/api/charge.ex | 2 +- lib/stripe_mock_web/views/charge_view.ex | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/stripe_mock/api/charge.ex b/lib/stripe_mock/api/charge.ex index 0dc45f8..0ee120b 100644 --- a/lib/stripe_mock/api/charge.ex +++ b/lib/stripe_mock/api/charge.ex @@ -69,7 +69,7 @@ defmodule StripeMock.API.Charge do validate_required(changeset, :source_id) {nil, _source_id} -> - case source do + case source |> IO.inspect() do %{card: %{customer_id: nil}} -> changeset _ -> validate_required(changeset, :customer_id) end diff --git a/lib/stripe_mock_web/views/charge_view.ex b/lib/stripe_mock_web/views/charge_view.ex index 5c560e5..92826f3 100644 --- a/lib/stripe_mock_web/views/charge_view.ex +++ b/lib/stripe_mock_web/views/charge_view.ex @@ -20,9 +20,21 @@ defmodule StripeMockWeb.ChargeView do description: charge.description, metadata: charge.metadata, object: "charge", + outcome: render_outcome(charge), source: render(StripeMockWeb.CardView, "card.json", card: charge.source), statement_descriptor: charge.statement_descriptor, transfer_group: charge.transfer_group } end + + defp render_outcome(_charge) do + %{ + network_status: "approved_by_network", + reason: nil, + risk_level: "normal", + risk_score: 0, + seller_message: "Approved by network.", + type: "authorized" + } + end end From 42b343d1b299202d3f0baa3ccb6a82e72ed7826b Mon Sep 17 00:00:00 2001 From: Nick Kezhaya Date: Sun, 9 Jun 2019 17:47:28 -0500 Subject: [PATCH 3/3] Remove inspect call --- lib/stripe_mock/api/charge.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/stripe_mock/api/charge.ex b/lib/stripe_mock/api/charge.ex index 0ee120b..0dc45f8 100644 --- a/lib/stripe_mock/api/charge.ex +++ b/lib/stripe_mock/api/charge.ex @@ -69,7 +69,7 @@ defmodule StripeMock.API.Charge do validate_required(changeset, :source_id) {nil, _source_id} -> - case source |> IO.inspect() do + case source do %{card: %{customer_id: nil}} -> changeset _ -> validate_required(changeset, :customer_id) end