diff --git a/config/config.exs b/config/config.exs index 589bc42..3ad0083 100644 --- a/config/config.exs +++ b/config/config.exs @@ -15,6 +15,7 @@ config :stripe_mock, StripeMockWeb.Endpoint, render_errors: [view: StripeMockWeb.ErrorView, accepts: ~w(json)] config :stripe_mock, ecto_repos: [StripeMock.Repo] +config :stripe_mock, StripeMock.Repo, migration_primary_key: [type: :binary_id] # Configures Elixir's Logger config :logger, :console, diff --git a/lib/stripe_mock/api/card.ex b/lib/stripe_mock/api/card.ex index 8359837..54e76df 100644 --- a/lib/stripe_mock/api/card.ex +++ b/lib/stripe_mock/api/card.ex @@ -4,7 +4,6 @@ defmodule StripeMock.API.Card do schema "cards" do field :brand, :string field :deleted, :boolean - field :metadata, :map, default: %{} field :last4, :string field :source, :string @@ -15,6 +14,7 @@ defmodule StripeMock.API.Card do belongs_to :customer, API.Customer + common_fields() timestamps() end @@ -23,6 +23,7 @@ defmodule StripeMock.API.Card do card |> cast(attrs, [:source, :metadata]) |> validate_required([:source, :metadata]) + |> put_common_fields() end @doc false @@ -39,6 +40,7 @@ defmodule StripeMock.API.Card do |> validate_required([:number, :exp_month, :exp_year, :cvc]) |> set_brand() |> set_last4() + |> put_common_fields() end defp set_brand(changeset) do diff --git a/lib/stripe_mock/api/charge.ex b/lib/stripe_mock/api/charge.ex index da78851..f727fb9 100644 --- a/lib/stripe_mock/api/charge.ex +++ b/lib/stripe_mock/api/charge.ex @@ -53,6 +53,7 @@ defmodule StripeMock.API.Charge do def capture_changeset(charge, payment_intent) do charge |> change(%{payment_intent_id: payment_intent.id}) + |> put_common_fields() end defp set_customer_and_source(changeset) do diff --git a/lib/stripe_mock/api/customer.ex b/lib/stripe_mock/api/customer.ex index c57d655..97ae798 100644 --- a/lib/stripe_mock/api/customer.ex +++ b/lib/stripe_mock/api/customer.ex @@ -4,12 +4,11 @@ defmodule StripeMock.API.Customer do schema "customers" do field :currency, :string, default: "usd" field :deleted, :boolean, default: false - field :description, :string field :email, :string - field :metadata, :map, default: %{} field :name, :string field :phone, :string + common_fields() timestamps() end @@ -18,6 +17,7 @@ defmodule StripeMock.API.Customer do customer |> cast(attrs, [:created, :currency, :description, :email, :name, :phone]) |> validate_email() + |> put_common_fields() end defp validate_email(changeset) do diff --git a/lib/stripe_mock/api/refund.ex b/lib/stripe_mock/api/refund.ex index 5ff58bf..1780d96 100644 --- a/lib/stripe_mock/api/refund.ex +++ b/lib/stripe_mock/api/refund.ex @@ -4,11 +4,11 @@ defmodule StripeMock.API.Refund do schema "refunds" do field :amount, :integer - field :metadata, :map, default: %{} field :reason, :string belongs_to :charge, API.Charge + common_fields() timestamps() end @@ -21,6 +21,7 @@ defmodule StripeMock.API.Refund do |> validate_required(:amount) |> validate_amount() |> validate_reason() + |> put_common_fields() end @doc false diff --git a/lib/stripe_mock/api/source.ex b/lib/stripe_mock/api/source.ex index dffbd2d..31ca88c 100644 --- a/lib/stripe_mock/api/source.ex +++ b/lib/stripe_mock/api/source.ex @@ -4,11 +4,14 @@ defmodule StripeMock.API.Source do schema "sources" do belongs_to :card, API.Card belongs_to :token, API.Card + common_fields() + timestamps() end @doc false def changeset(token, attrs) do token |> cast(attrs, [:card_id, :token_id]) + |> put_common_fields() end end diff --git a/lib/stripe_mock/api/token.ex b/lib/stripe_mock/api/token.ex index a3cced2..ee147a2 100644 --- a/lib/stripe_mock/api/token.ex +++ b/lib/stripe_mock/api/token.ex @@ -8,6 +8,7 @@ defmodule StripeMock.API.Token do belongs_to :card, API.Card + common_fields() timestamps() end @@ -18,6 +19,7 @@ defmodule StripeMock.API.Token do |> cast_assoc(:card, with: &API.Card.token_changeset/2) |> set_type() |> validate_required([:type]) + |> put_common_fields() end defp set_type(changeset) do diff --git a/lib/stripe_mock/id.ex b/lib/stripe_mock/id.ex index 84f6e4e..1cda74a 100644 --- a/lib/stripe_mock/id.ex +++ b/lib/stripe_mock/id.ex @@ -2,11 +2,42 @@ defmodule StripeMock.ID do @moduledoc """ Generates a somewhat Stripe-esque random ID string. Pretty much a base52-encoded UUID. """ + alias StripeMock.API + @spec generate() :: String.t() def generate() do - :crypto.strong_rand_bytes(16) + from_bytes(:crypto.strong_rand_bytes(16)) + end + + @spec from_uuid(String.t()) :: String.t() | none() + def from_uuid(uuid) do + {:ok, bytes} = Ecto.UUID.dump(uuid) + from_bytes(bytes) + end + + defp from_bytes(bytes) do + bytes |> :erlang.binary_to_list() - |> Enum.reduce(1, &(&1 * &2)) + |> Enum.reduce(1, &if(&1 > 0, do: &1 * &2, else: &2)) |> StripeMock.Base52.encode() end + + @spec type(module()) :: atom() + 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.PaymentMethod), do: :payment_method + def type(API.PaymentIntent), do: :payment_intent + def type(API.Refund), do: :refund + def type(API.Token), do: :token + + @spec type(map()) :: String.t() + def prefix(%API.Card{}), do: "card" + def prefix(%API.Charge{}), do: "ch" + def prefix(%API.Customer{}), do: "cus" + def prefix(%API.PaymentMethod{}), do: "pm" + def prefix(%API.PaymentIntent{}), do: "pi" + def prefix(%API.Refund{}), do: "re" + def prefix(%API.Token{}), do: "tok" end diff --git a/lib/stripe_mock/migrator.ex b/lib/stripe_mock/migrator.ex index 52bb8f7..8bad7a6 100644 --- a/lib/stripe_mock/migrator.ex +++ b/lib/stripe_mock/migrator.ex @@ -36,12 +36,7 @@ defmodule StripeMock.Migrator do IO.puts("Starting dependencies") Enum.each(@start_apps, &Application.ensure_all_started/1) - Application.load(:basic_space) - - # Start the Repo(s) for app - # Switch pool_size to 2 for ecto > 3.0 - IO.puts("Starting repos") - Enum.each(@repos, & &1.start_link(pool_size: 2)) + Application.load(:stripe_mock) # Run migrations Enum.each(@repos, &run_migrations_for/1) diff --git a/lib/stripe_mock/repo.ex b/lib/stripe_mock/repo.ex index 3a3e264..7f8b593 100644 --- a/lib/stripe_mock/repo.ex +++ b/lib/stripe_mock/repo.ex @@ -4,7 +4,7 @@ defmodule StripeMock.Repo do alias StripeMock.API @impl true - def init(_type, config) do + def init(:supervisor, config) do {uri, _} = System.cmd("pg_tmp", ["-t"]) [[username, host, port, database]] = @@ -24,16 +24,23 @@ defmodule StripeMock.Repo do {:ok, config} end + def init(_type, config) do + {:ok, config} + end + def fetch(schema, id) do - with true <- valid_id?(id), - %{} = object <- get(schema, id) do - {:ok, object} + if valid_uuid?(id) do + get(schema, id) else - _ -> {:error, :not_found} + get_by(schema, stripe_id: id) + end + |> case do + nil -> {:error, :not_found} + object -> {:ok, object} end end - defp valid_id?(id) do + defp valid_uuid?(id) do case Ecto.UUID.cast(id) do {:ok, _} -> true _ -> false @@ -61,23 +68,4 @@ defmodule StripeMock.Repo do changeset end end - - defp generate_id(schema) do - prefix(schema) <> "_" <> StripeMock.ID.generate() - end - - 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.PaymentIntent), do: :payment_intent - def type(API.Refund), do: :refund - def type(API.Token), do: :token - - def prefix(%API.Card{}), do: "card" - def prefix(%API.Charge{}), do: "ch" - def prefix(%API.Customer{}), do: "cus" - def prefix(%API.PaymentIntent{}), do: "pi" - def prefix(%API.Refund{}), do: "re" - def prefix(%API.Token{}), do: "tok" end diff --git a/lib/stripe_mock/schema.ex b/lib/stripe_mock/schema.ex index 688b7e0..9160edc 100644 --- a/lib/stripe_mock/schema.ex +++ b/lib/stripe_mock/schema.ex @@ -1,4 +1,7 @@ defmodule StripeMock.Schema do + import Ecto.Changeset + alias StripeMock.ID + defmacro __using__(_) do quote do use Ecto.Schema @@ -7,7 +10,7 @@ defmodule StripeMock.Schema do import unquote(__MODULE__) alias StripeMock.{API, Repo} - Module.put_attribute(__MODULE__, :primary_key, {:id, :binary_id, autogenerate: true}) + Module.put_attribute(__MODULE__, :primary_key, {:id, :binary_id, autogenerate: false}) Module.put_attribute(__MODULE__, :foreign_key_type, :binary_id) Module.put_attribute(__MODULE__, :timestamps_opts, inserted_at: :created, updated_at: false) end @@ -15,14 +18,34 @@ defmodule StripeMock.Schema do defmacro common_fields() do quote do + field :stripe_id, :string field :description, :string field :metadata, :map, default: %{} end end - import Ecto.Changeset - def put_common_fields(changeset) do - validate_required(changeset, [:metadata]) + changeset + |> validate_required(required_fields(changeset)) + |> put_ids() + end + + defp required_fields(changeset) do + Enum.filter([:metadata], &Map.has_key?(changeset.data, &1)) + end + + defp put_ids(changeset) do + case get_field(changeset, :id) do + nil -> do_put_stripe_id(changeset) + _ -> changeset + end + end + + defp do_put_stripe_id(changeset) do + uuid = Ecto.UUID.generate() + prefix = ID.prefix(changeset.data) + stripe_id = prefix <> "_" <> StripeMock.ID.from_uuid(uuid) + + change(changeset, %{id: uuid, stripe_id: stripe_id}) end end diff --git a/lib/stripe_mock_web/view_helpers.ex b/lib/stripe_mock_web/view_helpers.ex index 236c4d5..f15a4bc 100644 --- a/lib/stripe_mock_web/view_helpers.ex +++ b/lib/stripe_mock_web/view_helpers.ex @@ -1,9 +1,14 @@ defmodule StripeMockWeb.ViewHelpers do - def as_map(struct) do - struct + def as_map(struct, object \\ nil) do + if is_bitstring(object) do + Map.put(struct, :object, object) + else + struct + end |> update_created() |> Map.delete(:__struct__) |> Map.delete(:__meta__) + |> Map.put(:id, struct.stripe_id) end defp update_created(%{created: %{} = timestamp} = struct) do diff --git a/lib/stripe_mock_web/views/card_view.ex b/lib/stripe_mock_web/views/card_view.ex index dfcfe01..940cb3f 100644 --- a/lib/stripe_mock_web/views/card_view.ex +++ b/lib/stripe_mock_web/views/card_view.ex @@ -12,10 +12,9 @@ defmodule StripeMockWeb.CardView do def render("card.json", %{card: card}) do card - |> Map.take(~w(id created deleted exp_month exp_year metadata last4 brand)a) - |> Map.put("object", "card") + |> as_map("card") + |> Map.take(~w(id object created deleted exp_month exp_year metadata last4 brand)a) |> Map.put("customer", card.customer_id) - |> as_map() end def render("delete.json", %{card: card}) do diff --git a/lib/stripe_mock_web/views/charge_view.ex b/lib/stripe_mock_web/views/charge_view.ex index 705ce20..1905de1 100644 --- a/lib/stripe_mock_web/views/charge_view.ex +++ b/lib/stripe_mock_web/views/charge_view.ex @@ -11,21 +11,16 @@ defmodule StripeMockWeb.ChargeView do end def render("charge.json", %{charge: charge}) do - %{ - id: charge.id, - amount: charge.amount, - currency: charge.currency, - capture: charge.capture, + charge + |> as_map("charge") + |> Map.take( + ~w(id object amount currency capture description metadata statement_descriptor transfer_group)a + ) + |> Map.merge(%{ customer: charge.customer_id, - description: charge.description, - metadata: charge.metadata, - object: "charge", outcome: render_outcome(charge), - payment_method: render(StripeMockWeb.CardView, "card.json", card: charge.card), - statement_descriptor: charge.statement_descriptor, - transfer_group: charge.transfer_group - } - |> as_map() + payment_method: render(StripeMockWeb.CardView, "card.json", card: charge.card) + }) end defp render_outcome(_charge) do diff --git a/lib/stripe_mock_web/views/customer_view.ex b/lib/stripe_mock_web/views/customer_view.ex index a155246..781d163 100644 --- a/lib/stripe_mock_web/views/customer_view.ex +++ b/lib/stripe_mock_web/views/customer_view.ex @@ -15,7 +15,7 @@ defmodule StripeMockWeb.CustomerView do sources = API.list_cards(customer) |> Pagination.paginate() customer - |> as_map() + |> as_map("customer") |> Map.put(:sources, render_page(conn, sources, CardView, "card.json")) end end diff --git a/lib/stripe_mock_web/views/payment_intent_view.ex b/lib/stripe_mock_web/views/payment_intent_view.ex index eaa599f..11e1be3 100644 --- a/lib/stripe_mock_web/views/payment_intent_view.ex +++ b/lib/stripe_mock_web/views/payment_intent_view.ex @@ -12,9 +12,9 @@ defmodule StripeMockWeb.PaymentIntentView do def render("payment_intent.json", %{payment_intent: payment_intent}) do payment_intent - |> as_map() + |> as_map("payment_intent") |> Map.take( - ~w(amount capture_method confirmation_method currency description id metadata payment_method_types statement_descriptor status transfer_group)a + ~w(id object amount capture_method confirmation_method currency description metadata payment_method_types statement_descriptor status transfer_group)a ) |> Map.put("customer", payment_intent.customer_id) |> Map.merge(%{ diff --git a/lib/stripe_mock_web/views/payment_method_view.ex b/lib/stripe_mock_web/views/payment_method_view.ex index 1461900..82713df 100644 --- a/lib/stripe_mock_web/views/payment_method_view.ex +++ b/lib/stripe_mock_web/views/payment_method_view.ex @@ -15,8 +15,8 @@ defmodule StripeMockWeb.PaymentMethodView do payment_object = render(StripeMockWeb.CardView, "card.json", card: card) payment_method - |> Map.take(~w(id created description metadata)a) - |> as_map() + |> as_map("payment_method") + |> Map.take(~w(id object created description metadata)a) |> Map.put(:card, payment_object) end diff --git a/lib/stripe_mock_web/views/refund_view.ex b/lib/stripe_mock_web/views/refund_view.ex index 0540f29..79e989b 100644 --- a/lib/stripe_mock_web/views/refund_view.ex +++ b/lib/stripe_mock_web/views/refund_view.ex @@ -12,7 +12,8 @@ defmodule StripeMockWeb.RefundView do def render("refund.json", %{refund: refund}) do refund - |> Map.take(~w(id created amount metadata reason)a) + |> as_map("refund") + |> Map.take(~w(id object created amount metadata reason)a) |> Map.put(:charge, refund.charge_id) end end diff --git a/lib/stripe_mock_web/views/token_view.ex b/lib/stripe_mock_web/views/token_view.ex index 24cf890..0f86c0b 100644 --- a/lib/stripe_mock_web/views/token_view.ex +++ b/lib/stripe_mock_web/views/token_view.ex @@ -11,21 +11,16 @@ defmodule StripeMockWeb.TokenView do end def render("token.json", %{token: token}) do - object = %{ - id: token.id, - object: "token", - client_ip: token.client_ip, - created: token.created, - type: token.type, - used: token.used - } + token + |> as_map("token") + |> Map.take(~w(id object card client_ip created type used)a) + |> case do + %{card: %API.Card{}} = token -> + card = render(CardView, "card.json", card: token.card) + %{token | card: card} - if token.card do - card = render(CardView, "card.json", card: token.card) - Map.put(object, :card, card) - else - object + token -> + token end - |> as_map() end end diff --git a/priv/repo/migrations/20191015221147_create_tables.exs b/priv/repo/migrations/20191015221147_create_tables.exs index f936e0f..1b7297a 100644 --- a/priv/repo/migrations/20191015221147_create_tables.exs +++ b/priv/repo/migrations/20191015221147_create_tables.exs @@ -3,6 +3,7 @@ defmodule StripeMock.Repo.Migrations.CreateTables do defmacro common_fields() do quote do + add(:stripe_id, :string, null: false) add(:deleted, :boolean, null: false, default: false) add(:description, :string, null: true) add(:metadata, :map, null: false, default: %{}) diff --git a/test/stripe_mock_web/controllers/charge_controller_test.exs b/test/stripe_mock_web/controllers/charge_controller_test.exs index 900e0de..0a1e445 100644 --- a/test/stripe_mock_web/controllers/charge_controller_test.exs +++ b/test/stripe_mock_web/controllers/charge_controller_test.exs @@ -2,8 +2,6 @@ defmodule StripeMockWeb.ChargeControllerTest do use StripeMockWeb.ConnCase @moduletag :charge - alias StripeMock.API.Charge - setup :create_customer setup :create_card @@ -32,7 +30,7 @@ defmodule StripeMockWeb.ChargeControllerTest do conn = get(conn, Routes.charge_path(conn, :show, id)) assert %{ - "id" => id, + "id" => "ch_" <> id, "amount" => 5000, "capture" => true, "currency" => "some currency", @@ -76,9 +74,9 @@ defmodule StripeMockWeb.ChargeControllerTest do describe "update charge" do setup [:create_charge] - test "renders charge when data is valid", %{conn: conn, charge: %Charge{id: id} = charge} do + test "renders charge when data is valid", %{conn: conn, charge: charge} do conn = put(conn, Routes.charge_path(conn, :update, charge), update_attrs()) - assert %{"id" => ^id} = json_response(conn, 200) + assert %{"id" => "ch_" <> _ = id} = json_response(conn, 200) conn = get(conn, Routes.charge_path(conn, :show, id)) diff --git a/test/stripe_mock_web/controllers/customer_controller_test.exs b/test/stripe_mock_web/controllers/customer_controller_test.exs index e592bb4..ad72dae 100644 --- a/test/stripe_mock_web/controllers/customer_controller_test.exs +++ b/test/stripe_mock_web/controllers/customer_controller_test.exs @@ -29,7 +29,7 @@ defmodule StripeMockWeb.CustomerControllerTest do test "renders customer data", %{conn: conn, customer: customer} do conn = get(conn, Routes.customer_path(conn, :show, customer.id)) - assert %{"id" => id} = json_response(conn, 200) + assert %{"id" => "cus_" <> id} = json_response(conn, 200) end test "renders 404 on not found", %{conn: conn} do @@ -45,7 +45,7 @@ defmodule StripeMockWeb.CustomerControllerTest do conn = get(conn, Routes.customer_path(conn, :show, id)) - assert %{"id" => id} = json_response(conn, 200) + assert %{"id" => "cus_" <> id} = json_response(conn, 200) end test "renders errors when data is invalid", %{conn: conn} do @@ -57,16 +57,13 @@ defmodule StripeMockWeb.CustomerControllerTest do describe "update customer" do setup [:create_customer] - test "renders customer when data is valid", %{ - conn: conn, - customer: %Customer{id: id} = customer - } do + test "renders customer when data is valid", %{conn: conn, customer: customer} do conn = put(conn, Routes.customer_path(conn, :update, customer), @update_attrs) - assert %{"id" => ^id} = json_response(conn, 200) + assert %{"id" => "cus_" <> _ = id} = json_response(conn, 200) conn = get(conn, Routes.customer_path(conn, :show, id)) - assert %{"id" => id} = json_response(conn, 200) + assert %{"id" => "cus_" <> _} = json_response(conn, 200) end test "renders errors when data is invalid", %{conn: conn, customer: customer} do diff --git a/test/stripe_mock_web/controllers/payment_intent_controller_test.exs b/test/stripe_mock_web/controllers/payment_intent_controller_test.exs index 38a271e..8e9e53f 100644 --- a/test/stripe_mock_web/controllers/payment_intent_controller_test.exs +++ b/test/stripe_mock_web/controllers/payment_intent_controller_test.exs @@ -2,8 +2,6 @@ defmodule StripeMockWeb.PaymentIntentControllerTest do use StripeMockWeb.ConnCase @moduletag :payment_intent - alias StripeMock.API.PaymentIntent - setup :create_customer setup :create_card @@ -78,9 +76,8 @@ defmodule StripeMockWeb.PaymentIntentControllerTest do conn: conn, payment_intent: payment_intent } do - %PaymentIntent{id: id} = payment_intent conn = put(conn, Routes.payment_intent_path(conn, :update, payment_intent), update_attrs()) - assert %{"id" => ^id} = json_response(conn, 200) + assert %{"id" => "pi_" <> _ = id} = json_response(conn, 200) conn = get(conn, Routes.payment_intent_path(conn, :show, id)) diff --git a/test/stripe_mock_web/controllers/refund_controller_test.exs b/test/stripe_mock_web/controllers/refund_controller_test.exs index d29daf0..b9dfe28 100644 --- a/test/stripe_mock_web/controllers/refund_controller_test.exs +++ b/test/stripe_mock_web/controllers/refund_controller_test.exs @@ -2,8 +2,6 @@ defmodule StripeMockWeb.RefundControllerTest do use StripeMockWeb.ConnCase @moduletag :refund - alias StripeMock.API.Refund - setup :create_customer setup :create_charge @@ -71,11 +69,11 @@ defmodule StripeMockWeb.RefundControllerTest do describe "update refund" do setup [:create_refund] - test "renders refund when data is valid", %{conn: conn, refund: %Refund{id: id} = refund} do + test "renders refund when data is valid", %{conn: conn, refund: refund} do assert refund.metadata == %{} conn = put(conn, Routes.refund_path(conn, :update, refund), %{metadata: %{"key" => "val"}}) - assert %{"id" => ^id} = json_response(conn, 200) + assert %{"id" => "re_" <> _ = id} = json_response(conn, 200) conn = get(conn, Routes.refund_path(conn, :show, id)) diff --git a/test/stripe_mock_web/controllers/source_controller_test.exs b/test/stripe_mock_web/controllers/source_controller_test.exs index 8cd8a0f..45b5553 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.Card alias StripeMock.CardFixture @card_attrs CardFixture.valid_card() @@ -56,13 +55,13 @@ defmodule StripeMockWeb.SourceControllerTest do describe "update card" do setup [:create_card] - test "renders card when data is valid", %{conn: conn, card: %Card{id: id} = card} do + test "renders card when data is valid", %{conn: conn, card: card} do conn = put(conn, Routes.customer_source_path(conn, :update, card.customer_id, card), card: @update_attrs ) - assert %{"id" => ^id, "object" => "card"} = json_response(conn, 200) + assert %{"id" => "card_" <> _ = id, "object" => "card"} = json_response(conn, 200) conn = get(conn, Routes.customer_source_path(conn, :show, card.customer_id, card)) @@ -91,7 +90,7 @@ defmodule StripeMockWeb.SourceControllerTest do |> get(Routes.customer_source_path(conn, :index, card.customer_id, object: "card")) |> json_response(200) - assert Enum.find(data, &(&1["id"] == card.id)) + assert Enum.find(data, &(&1["id"] == card.stripe_id)) delete(conn, Routes.customer_source_path(conn, :delete, card.customer_id, card)) @@ -100,7 +99,7 @@ defmodule StripeMockWeb.SourceControllerTest do |> get(Routes.customer_source_path(conn, :index, card.customer_id, object: "card")) |> json_response(200) - refute Enum.find(data, &(&1["id"] == card.id)) + refute Enum.find(data, &(&1["id"] == card.stripe_id)) end end end