Skip to main content

lib/marqeta/resources/cards.ex

defmodule Marqeta.Cards do
  @moduledoc """
  Create and manage physical and virtual payment cards.

  Cards are always associated with a user and derived from a card product.

  ## Card states

    * `UNACTIVATED`  — newly issued, requires explicit activation
    * `ACTIVE`       — active and usable for transactions
    * `SUSPENDED`    — temporarily frozen; no transactions
    * `TERMINATED`   — permanently closed; cannot be reactivated
    * `LIMITED`      — restricted to certain transaction types
    * `UNSUPPORTED`  — card type not supported in current context

  ## Attribute precedence

  `card` → `bulkissuance` → `cardproduct` (higher overrides lower,
  does not overwrite lower-precedence values).

  ## Examples

      # Virtual card
      {:ok, card} = Marqeta.Cards.create(%{
        user_token: "user_01",
        card_product_token: "cp_01"
      })

      # Physical card with shipping
      {:ok, card} = Marqeta.Cards.create(%{
        user_token: "user_01",
        card_product_token: "physical_cp_01",
        fulfillment: %{
          card_personalization: %{text: %{name_line_1: %{value: "Jane Doe"}}},
          shipping: %{
            method: "TWO_DAY",
            recipient_address: %{
              first_name: "Jane",
              last_name: "Doe",
              address1: "123 Main St",
              city: "San Francisco",
              state: "CA",
              zip: "94105",
              country: "USA"
            }
          }
        }
      })

      # Reissue with same PAN (lost / damaged replacement)
      {:ok, card} = Marqeta.Cards.create(%{
        user_token: "user_01",
        card_product_token: "cp_01",
        reissue_pan_from_card_token: "old_card_token"
      })
  """

  use Marqeta.Resource, path: "/cards", resource: "card", create: false, get: false

  alias Marqeta.Client
  alias Marqeta.Error
  alias Marqeta.Stream, as: MStream

  # ---------------------------------------------------------------------------
  # Overridden create — supports show_pan / show_cvv_number query params
  # ---------------------------------------------------------------------------

  @doc """
  Creates a new card.

  ## Options

    * `:show_pan`        — include full PAN in response (PCI DSS required)
    * `:show_cvv_number` — include CVV2 in response (PCI DSS required)
  """
  @spec create(map(), keyword()) :: {:ok, map()} | {:error, Error.t()}
  def create(params \\ %{}, opts \\ []) do
    {sensitive, req_opts} = Keyword.split(opts, [:show_cvv_number, :show_pan])
    Client.post("/cards" <> sensitive_query(sensitive), params, req_opts)
  end

  # ---------------------------------------------------------------------------
  # Overridden get — supports show_pan / show_cvv_number query params
  # ---------------------------------------------------------------------------

  @doc """
  Retrieves a card by token.

  ## Options

    * `:show_pan`        — include full PAN in response (PCI DSS required)
    * `:show_cvv_number` — include CVV2 in response (PCI DSS required)
  """
  @spec get(String.t(), keyword()) :: {:ok, map()} | {:error, Error.t()}
  def get(token, opts \\ []) do
    {sensitive, req_opts} = Keyword.split(opts, [:show_cvv_number, :show_pan])
    Client.get("/cards/#{token}" <> sensitive_query(sensitive), req_opts)
  end

  @doc """
  Retrieves a card with the full PAN and CVV2 included.

  Requires PCI DSS compliance. Sets `fulfillment_status` to `DIGITALLY_PRESENTED`.
  """
  @spec show_pan(String.t(), keyword()) :: {:ok, map()} | {:error, Error.t()}
  def show_pan(token, opts \\ []) do
    get(token, Keyword.merge(opts, show_cvv_number: true, show_pan: true))
  end

  @doc "Lists cards matching the last 4 digits of their PAN. Returns up to 10 per page."
  @spec list_by_last_four(String.t(), map(), keyword()) :: {:ok, map()} | {:error, Error.t()}
  def list_by_last_four(last_four, params \\ %{}, opts \\ []) do
    Client.get("/cards", Keyword.put(opts, :params, Map.put(params, :last_four, last_four)))
  end

  @doc "Lists all cards for a user."
  @spec list_by_user(String.t(), map(), keyword()) :: {:ok, map()} | {:error, Error.t()}
  def list_by_user(user_token, params \\ %{}, opts \\ []) do
    Client.get("/cards/user/#{user_token}", Keyword.put(opts, :params, params))
  end

  @doc "Returns a lazy stream of all cards for a user."
  @spec stream_by_user(String.t(), map()) :: Enumerable.t()
  def stream_by_user(user_token, params \\ %{}) do
    MStream.stream(fn p -> list_by_user(user_token, p) end, params)
  end

  @doc "Lists all cards for a business."
  @spec list_by_business(String.t(), map(), keyword()) :: {:ok, map()} | {:error, Error.t()}
  def list_by_business(token, params \\ %{}, opts \\ []) do
    Client.get("/cards/business/#{token}", Keyword.put(opts, :params, params))
  end

  @doc "Returns a lazy stream of all cards for a business."
  @spec stream_by_business(String.t(), map()) :: Enumerable.t()
  def stream_by_business(token, params \\ %{}) do
    MStream.stream(fn p -> list_by_business(token, p) end, params)
  end

  @doc "Lists transactions for a card."
  @spec transactions(String.t(), map(), keyword()) :: {:ok, map()} | {:error, Error.t()}
  def transactions(token, params \\ %{}, opts \\ []) do
    Client.get("/transactions/card/#{token}", Keyword.put(opts, :params, params))
  end

  @doc "Returns a lazy stream of transactions for a card."
  @spec stream_transactions(String.t(), map()) :: Enumerable.t()
  def stream_transactions(token, params \\ %{}) do
    MStream.stream(fn p -> transactions(token, p) end, params)
  end

  @doc "Returns merchant-specific data about where a card has been used."
  @spec merchant_scope(String.t(), map(), keyword()) :: {:ok, map()} | {:error, Error.t()}
  def merchant_scope(token, params \\ %{}, opts \\ []) do
    Client.get("/cards/#{token}/merchantscope", Keyword.put(opts, :params, params))
  end

  # ---------------------------------------------------------------------------
  # Private
  # ---------------------------------------------------------------------------

  @spec sensitive_query(keyword()) :: String.t()
  defp sensitive_query([]), do: ""

  defp sensitive_query(opts) do
    parts =
      Enum.flat_map(opts, fn
        {:show_pan, true} -> [{"show_pan", "true"}]
        {:show_cvv_number, true} -> [{"show_cvv_number", "true"}]
        _ -> []
      end)

    if parts == [], do: "", else: "?" <> URI.encode_query(parts)
  end
end