defmodule AccessGrid.AccessPasses do @moduledoc """ Manages access-pass lifecycle operations against `/v1/key-cards`. All functions accept an optional `:client` in opts. If not provided, credentials are loaded from application config via Gestalt. ## Examples # Using explicit client client = AccessGrid.Client.new(account_id: "...", api_secret: "...") AccessGrid.AccessPasses.issue(%{card_template_id: "tmpl_123"}, client: client) # Using config (client resolved automatically) AccessGrid.AccessPasses.get("card_abc123") """ alias AccessGrid.AccessPass alias AccessGrid.Client alias AccessGrid.HttpFailure alias AccessGrid.HttpResponse alias AccessGrid.Params alias AccessGrid.Types @base_path "/v1/key-cards" @type result :: {:ok, AccessPass.t()} | {:error, Types.api_error_reason(), HttpFailure.t() | [atom(), ...]} @type list_result :: {:ok, [AccessPass.t()]} | {:error, Types.api_error_reason(), HttpFailure.t() | [atom(), ...]} @doc """ Issues a new key card. ## Parameters * `params` - Map with card parameters (card_template_id, full_name, etc.) * `opts` - Options including `:client` ## Examples AccessGrid.AccessPasses.issue(%{ card_template_id: "tmpl_123", full_name: "John Doe" }) """ @spec issue(map(), keyword()) :: result() def issue(params, opts \\ []) do with :ok <- Params.require(params, [:card_template_id]) do opts[:client] |> Client.request(:post, @base_path, body: params) |> handle_response() end end @doc """ Retrieves a key card by ID. ## Examples AccessGrid.AccessPasses.get("card_abc123") """ @spec get(String.t(), keyword()) :: result() def get(card_id, opts \\ []) do with :ok <- Params.require_present(card_id, :card_id) do opts[:client] |> Client.request(:get, "#{@base_path}/#{card_id}") |> handle_response() end end @doc """ Updates a key card. ## Examples AccessGrid.AccessPasses.update("card_abc123", %{full_name: "New Name"}) """ @spec update(String.t(), map(), keyword()) :: result() def update(card_id, params, opts \\ []) do with :ok <- Params.require_present(card_id, :card_id) do opts[:client] |> Client.request(:patch, "#{@base_path}/#{card_id}", body: params) |> handle_response() end end @doc """ Lists key cards for a template. ## Options * `:state` - Filter by card state (e.g., "active", "suspended") * `:client` - Client for authentication ## Examples AccessGrid.AccessPasses.list("tmpl_123") AccessGrid.AccessPasses.list("tmpl_123", state: "active") """ @spec list(String.t(), keyword()) :: list_result() def list(template_id, opts \\ []) do with :ok <- Params.require_present(template_id, :template_id) do params = %{"template_id" => template_id} |> maybe_add_param("state", opts[:state]) opts[:client] |> Client.request(:get, @base_path, params: params) |> handle_list_response() end end @doc """ Suspends a key card. ## Examples AccessGrid.AccessPasses.suspend("card_abc123") """ @spec suspend(String.t(), keyword()) :: result() def suspend(card_id, opts \\ []), do: manage_state(card_id, "suspend", opts) @doc """ Resumes a suspended key card. ## Examples AccessGrid.AccessPasses.resume("card_abc123") """ @spec resume(String.t(), keyword()) :: result() def resume(card_id, opts \\ []), do: manage_state(card_id, "resume", opts) @doc """ Unlinks a key card from its device. ## Examples AccessGrid.AccessPasses.unlink("card_abc123") """ @spec unlink(String.t(), keyword()) :: result() def unlink(card_id, opts \\ []), do: manage_state(card_id, "unlink", opts) @doc """ Deletes a key card. ## Examples AccessGrid.AccessPasses.delete("card_abc123") """ @spec delete(String.t(), keyword()) :: result() def delete(card_id, opts \\ []), do: manage_state(card_id, "delete", opts) # --- Private helpers --- defp manage_state(card_id, action, opts) do with :ok <- Params.require_present(card_id, :card_id) do opts[:client] |> Client.request(:post, "#{@base_path}/#{card_id}/#{action}") |> handle_response() end end defp handle_response({:ok, %HttpResponse{body_decoded: body}}) do {:ok, AccessPass.from_response(body)} end defp handle_response({:error, %HttpFailure{} = failure}) do {:error, reason_from_failure(failure), failure} end defp handle_list_response({:ok, %HttpResponse{body_decoded: body}}) do cards = body |> Map.get("keys", []) |> Enum.map(&AccessPass.from_response/1) {:ok, cards} end defp handle_list_response({:error, %HttpFailure{} = failure}) do {:error, reason_from_failure(failure), failure} end defp reason_from_failure(%HttpFailure{reason: :unauthorized}), do: :unauthorized defp reason_from_failure(%HttpFailure{reason: :forbidden}), do: :forbidden defp reason_from_failure(%HttpFailure{reason: :not_found}), do: :not_found defp reason_from_failure(%HttpFailure{reason: :unprocessable_entity}), do: :validation_failed defp reason_from_failure(%HttpFailure{reason: :too_many_requests}), do: :rate_limited defp reason_from_failure(%HttpFailure{reason: :timeout}), do: :timeout defp reason_from_failure(%HttpFailure{reason: reason}) when reason in [:internal_server_error, :bad_gateway, :service_unavailable, :gateway_timeout, :server_error], do: :server_error defp reason_from_failure(%HttpFailure{}), do: :request_failed defp maybe_add_param(params, _key, nil), do: params defp maybe_add_param(params, key, value), do: Map.put(params, key, value) end