defmodule DaProductApp.Mypinpad.MypinpadClient do require Logger defp config, do: Application.get_env(:da_product_app, :mypinpad, []) defp base_url, do: Keyword.get(config(), :base_url, "https://sandbox.mypinpad.io") defp token_url, do: Keyword.get(config(), :token_url) defp client_id, do: Keyword.get(config(), :client_id) defp client_secret, do: Keyword.get(config(), :client_secret) defp scope, do: Keyword.get(config(), :scope) @doc """ Fetches an OAuth2 Bearer token from Azure AD using client credentials, then calls GET /merchant/id to generate a new MyPinPad Merchant ID. Returns the parsed JSON body containing the new merchantId UUID. """ @spec generate_merchant_id() :: {:ok, map()} | {:error, any()} def generate_merchant_id do with {:ok, token} <- fetch_access_token() do call_generate_merchant_id(token) end end # Step 1: fetch Bearer token via Azure AD client credentials flow defp fetch_access_token do case validate_token_config() do {:error, {:missing_config, missing}} -> Logger.error("[MypinpadClient] Missing MyPinPad token configuration: #{inspect(missing)}") {:error, {:missing_config, missing}} {:error, {:invalid_config, reason}} -> Logger.error("[MypinpadClient] Invalid MyPinPad token configuration: #{inspect(reason)}") {:error, {:invalid_config, reason}} {:ok, _config} -> url = token_url() body = URI.encode_query(%{ "grant_type" => "client_credentials", "client_id" => client_id(), "client_secret" => client_secret(), "scope" => scope() }) headers = [{"Content-Type", "application/x-www-form-urlencoded"}] Logger.info("[MypinpadClient] Fetching access token from #{url}") case HTTPoison.post(url, body, headers, recv_timeout: 10000) do {:ok, %HTTPoison.Response{status_code: status, body: resp_body}} when status in 200..299 -> case Jason.decode(resp_body) do {:ok, %{"access_token" => token}} -> {:ok, token} {:ok, decoded} -> Logger.error("[MypinpadClient] Token response missing access_token: #{inspect(decoded)}") {:error, :missing_access_token} {:error, reason} -> Logger.error("[MypinpadClient] Failed to decode token response: #{inspect(reason)}") {:error, :invalid_token_response} end {:ok, %HTTPoison.Response{status_code: status, body: resp_body}} -> Logger.error("[MypinpadClient] Token endpoint non-2xx: #{status} body=#{resp_body}") {:error, {:token_error, status, resp_body}} {:error, %HTTPoison.Error{reason: reason}} -> Logger.error("[MypinpadClient] Token request failed: #{inspect(reason)}") {:error, {:request_failed, reason}} end end end defp validate_token_config do props = %{ token_url: token_url(), client_id: client_id(), client_secret: client_secret(), scope: scope() } missing = props |> Enum.filter(fn {_k, v} -> not (is_binary(v) and String.trim(v) != "") end) |> Enum.map(fn {k, _v} -> k end) cond do missing != [] -> {:error, {:missing_config, missing}} true -> # token_url should be a valid HTTP(S) URL case URI.parse(props.token_url).scheme do scheme when scheme in ["http", "https"] -> {:ok, props} _ -> {:error, {:invalid_config, :token_url_invalid}} end end end # Step 2: call MyPinPad with the Bearer token defp call_generate_merchant_id(token) do url = "#{base_url()}/merchant/id" headers = [ {"Authorization", "Bearer #{token}"}, {"Content-Type", "application/json"}, {"Accept", "application/json"} ] Logger.info("[MypinpadClient] GET #{url} - generating new merchant ID") case HTTPoison.get(url, headers, recv_timeout: 10000) do {:ok, %HTTPoison.Response{status_code: status, body: body}} when status in 200..299 -> case Jason.decode(body) do {:ok, decoded} when is_map(decoded) -> {:ok, decoded} {:ok, decoded} -> # Decoded to a non-map (e.g. a plain string), normalize into a map merchant_id = if is_binary(decoded), do: String.trim(decoded), else: to_string(decoded) {:ok, %{"merchantId" => merchant_id}} {:error, _} -> # Response is a plain string (e.g. bare UUID) — normalize into a map trimmed = String.trim(body) # Validate that the plain string matches a UUID format if String.match?(trimmed, ~r/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i) do Logger.info("[MypinpadClient] Received plain string UUID response: #{trimmed}") {:ok, %{"merchantId" => trimmed}} else Logger.error("[MypinpadClient] Received invalid plain string response: #{inspect(trimmed)}") {:error, :invalid_response_format} end end {:ok, %HTTPoison.Response{status_code: status, body: body}} -> Logger.error("[MypinpadClient] Non-2xx response: #{status} body=#{body}") {:error, {:http_error, status, body}} {:error, %HTTPoison.Error{reason: reason}} -> Logger.error("[MypinpadClient] HTTP request failed: #{inspect(reason)}") {:error, {:request_failed, reason}} end end end