defmodule CampaignFlow.Client do @moduledoc """ Main client module for the Campaign Flow API. This module provides a convenient interface for interacting with the Campaign Flow API using OAuth2 authentication and the Req HTTP client library. ## Configuration You can configure the client in your config files: config :campaign_flow, base_url: "https://app.campaignflow.com.au/api/v2", client_id: "your_client_id", client_secret: "your_client_secret" Or pass configuration directly when creating a client: client = CampaignFlow.Client.new( base_url: "https://app.campaignflow.com.au/api/v2", client_id: "your_client_id", client_secret: "your_client_secret" ) ## Usage # Create a new client client = CampaignFlow.Client.new( client_id: "your_client_id", client_secret: "your_client_secret" ) # Make API calls using resource modules {:ok, campaigns} = CampaignFlow.Client.Campaigns.list(client) {:ok, campaign} = CampaignFlow.Client.Campaigns.get(client, campaign_id) """ alias CampaignFlow.Client.Auth @type t :: %__MODULE__{ base_url: String.t(), client_id: String.t(), client_secret: String.t(), access_token: String.t() | nil, token_expires_at: DateTime.t() | nil, req_options: keyword() } defstruct [ :base_url, :client_id, :client_secret, :access_token, :token_expires_at, req_options: [] ] # Private functions defp default_base_url(opts, config) do environment = opts[:environment] || config[:environment] || :prod case environment do env when env in [:test, "test"] -> "https://test.campaignflow.com.au/api/v2" env when env in [:prod, "prod"] -> "https://app.campaignflow.com.au/api/v2" _ -> raise "Invalid environment: #{inspect(environment)}. Must be :prod or :test" end end # Public API @doc """ Creates a new Campaign Flow API client. ## Options * `:base_url` - The base URL for the API (overrides environment setting) * `:environment` - The environment to use (`:prod` or `:test`, defaults to `:prod`) * `:client_id` - OAuth2 client ID (required) * `:client_secret` - OAuth2 client secret (required) * `:req_options` - Additional options to pass to Req ## Examples client = CampaignFlow.Client.new(client_id: "id", client_secret: "secret") #=> %CampaignFlow.Client{...} client = CampaignFlow.Client.new(client_id: "id", client_secret: "secret", environment: :test) #=> %CampaignFlow.Client{base_url: "https://test.campaignflow.com.au/api/v2", ...} """ @spec new(keyword()) :: t() def new(opts \\ []) do config = Application.get_all_env(:campaign_flow) %__MODULE__{ base_url: opts[:base_url] || config[:base_url] || default_base_url(opts, config), client_id: opts[:client_id] || config[:client_id] || raise("client_id is required"), client_secret: opts[:client_secret] || config[:client_secret] || raise("client_secret is required"), req_options: opts[:req_options] || [] } end @doc """ Creates a new client for the test environment. ## Examples client = CampaignFlow.Client.test(client_id: "id", client_secret: "secret") #=> %CampaignFlow.Client{base_url: "https://test.campaignflow.com.au/api/v2", ...} """ @spec test(keyword()) :: t() def test(opts \\ []) do opts |> Keyword.put(:base_url, "https://test.campaignflow.com.au/api/v2") |> new() end @doc """ Ensures the client has a valid access token, refreshing if necessary. ## Examples client = CampaignFlow.Client.new(client_id: "id", client_secret: "secret") {:ok, authenticated_client} = CampaignFlow.Client.authenticate(client) """ @spec authenticate(t()) :: {:ok, t()} | {:error, term()} def authenticate(%__MODULE__{} = client) do if token_valid?(client) do {:ok, client} else Auth.get_access_token(client) end end @doc """ Checks if the client's current access token is valid. """ @spec token_valid?(t()) :: boolean() def token_valid?(%__MODULE__{access_token: nil}), do: false def token_valid?(%__MODULE__{token_expires_at: nil}), do: false def token_valid?(%__MODULE__{token_expires_at: expires_at}) do DateTime.compare(DateTime.utc_now(), expires_at) == :lt end end