README.md

# CampaignFlow Client

An Elixir client library for the [Campaign Flow API](https://campaignflow.com.au), built with the [Req](https://hexdocs.pm/req) HTTP client.

## ⚠️ AI Generated! ⚠️
Full disclosure, this repo was almost completely generated by Claude Code. Use at your own risk.

## Features

- **Shared Token Manager** - OAuth2 tokens managed by GenServer processes and shared across your application
- **Multi-Environment Support** - Use both production and test environments simultaneously
- **Automatic Token Refresh** - Tokens refreshed automatically with race-condition-free coordination
- **Simple API** - No need to thread updated client through your code
- **Full API Coverage** - Complete coverage for all Campaign Flow endpoints
- **Type-Safe** - Function signatures with `@spec` for compile-time checking
- **Comprehensive Error Handling** - Structured error types for robust error handling
- **Embedded Mock Server** - Optional Plug-based mock API you can mount in your app for local development and end-to-end tests

## Installation

Add `campaign_flow` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:campaign_flow, "~> 2.1.0"}
  ]
end
```

## Configuration

The client uses **application-level configuration** where credentials are configured globally and shared across your entire application. Each environment (production, test, etc.) has its own dedicated TokenManager process.

### Basic Configuration

Configure environments in your `config/config.exs`:

```elixir
config :campaign_flow,
  environments: [
    prod: [
      base_url: "https://app.campaignflow.com.au/api/v2",
      client_id: System.get_env("CAMPAIGNFLOW_PROD_CLIENT_ID"),
      client_secret: System.get_env("CAMPAIGNFLOW_PROD_CLIENT_SECRET")
    ],
    test: [
      base_url: "https://test.campaignflow.com.au/api/v2",
      client_id: System.get_env("CAMPAIGNFLOW_TEST_CLIENT_ID"),
      client_secret: System.get_env("CAMPAIGNFLOW_TEST_CLIENT_SECRET")
    ]
  ]
```

### Runtime Configuration

For production deployments, use `config/runtime.exs`:

```elixir
import Config

if config_env() == :prod do
  config :campaign_flow,
    environments: [
      prod: [
        base_url: "https://app.campaignflow.com.au/api/v2",
        client_id: System.fetch_env!("CAMPAIGNFLOW_PROD_CLIENT_ID"),
        client_secret: System.fetch_env!("CAMPAIGNFLOW_PROD_CLIENT_SECRET")
      ]
    ]
end
```

### Environment Variables

Set the following environment variables:

```bash
# Production credentials
export CAMPAIGNFLOW_PROD_CLIENT_ID="your_prod_client_id"
export CAMPAIGNFLOW_PROD_CLIENT_SECRET="your_prod_client_secret"

# Test environment credentials (if needed)
export CAMPAIGNFLOW_TEST_CLIENT_ID="your_test_client_id"
export CAMPAIGNFLOW_TEST_CLIENT_SECRET="your_test_client_secret"
```

### Multiple Environments

You can configure and use multiple environments simultaneously:

```elixir
# Configure both prod and test
config :campaign_flow,
  environments: [
    prod: [
      base_url: "https://app.campaignflow.com.au/api/v2",
      client_id: System.get_env("CAMPAIGNFLOW_PROD_CLIENT_ID"),
      client_secret: System.get_env("CAMPAIGNFLOW_PROD_CLIENT_SECRET")
    ],
    test: [
      base_url: "https://test.campaignflow.com.au/api/v2",
      client_id: System.get_env("CAMPAIGNFLOW_TEST_CLIENT_ID"),
      client_secret: System.get_env("CAMPAIGNFLOW_TEST_CLIENT_SECRET")
    ]
  ]

# Use them simultaneously in your application
prod_client = CampaignFlow.Client.new(environment: :prod)
test_client = CampaignFlow.Client.new(environment: :test)

{:ok, prod_campaigns} = CampaignFlow.Client.Campaigns.list(prod_client)
{:ok, test_campaigns} = CampaignFlow.Client.Campaigns.list(test_client)
```

## Usage

### Creating a Client

Clients are lightweight structs that reference a configured environment. Credentials are stored in the application config, not in the client:

```elixir
# Create a client for the production environment
client = CampaignFlow.Client.new(environment: :prod)

# Create a client for the test environment
client = CampaignFlow.Client.new(environment: :test)

# Or use the convenience function for test environment
client = CampaignFlow.Client.test()

# You can also override the base URL if needed
client = CampaignFlow.Client.new(
  environment: :prod,
  base_url: "https://custom.example.com/api/v2"
)
```

### Campaigns

```elixir
# List campaigns
{:ok, campaigns} = CampaignFlow.Client.Campaigns.list(client)

# Get a specific campaign
{:ok, campaign} = CampaignFlow.Client.Campaigns.get(client, 123)

# Create a new campaign
{:ok, campaign} = CampaignFlow.Client.Campaigns.create(client, %{
  name: "Summer Campaign 2024",
  agency_id: 1,
  property_id: 2
})

# Update a campaign
{:ok, campaign} = CampaignFlow.Client.Campaigns.update(client, 123, %{
  name: "Updated Campaign Name"
})

# Add a comment to a campaign
{:ok, response} = CampaignFlow.Client.Campaigns.add_comment(client, 123, %{
  comment: "Campaign approved by client"
})

# Set campaign status
{:ok, response} = CampaignFlow.Client.Campaigns.set_status(client, 123, %{
  status: "approved"
})

# Send approved email
{:ok, response} = CampaignFlow.Client.Campaigns.send_approved_email(client, 123)
```

### Campaign Vendors

```elixir
# List campaign vendors
{:ok, vendors} = CampaignFlow.Client.Campaigns.list_vendors(client, 123)

# Get a specific vendor
{:ok, vendor} = CampaignFlow.Client.Campaigns.get_vendor(client, 123, 456)

# Add a vendor to a campaign
{:ok, vendor} = CampaignFlow.Client.Campaigns.add_vendor(client, 123, %{
  vendor_id: 456
})

# Update a campaign vendor
{:ok, vendor} = CampaignFlow.Client.Campaigns.update_vendor(client, 123, 456, %{
  status: "approved"
})

# Remove a vendor from a campaign
{:ok, response} = CampaignFlow.Client.Campaigns.remove_vendor(client, 123, 456)

# Send campaign to vendor
{:ok, response} = CampaignFlow.Client.Campaigns.send_campaign_to_vendor(client, 123, 456)

# Verify vendor contact
{:ok, response} = CampaignFlow.Client.Campaigns.verify_vendor_contact(client, 123, 456)
```

### Agencies

```elixir
# List agencies
{:ok, agencies} = CampaignFlow.Client.Agencies.list(client)

# Get a specific agency
{:ok, agency} = CampaignFlow.Client.Agencies.get(client, 123)
```

### Invoices

```elixir
# List invoices
{:ok, invoices} = CampaignFlow.Client.Invoices.list(client)

# Get a specific invoice
{:ok, invoice} = CampaignFlow.Client.Invoices.get(client, 123)

# Create an invoice
{:ok, invoice} = CampaignFlow.Client.Invoices.create(client, %{
  campaign_id: 123,
  amount: 1000.00
})

# Update an invoice
{:ok, invoice} = CampaignFlow.Client.Invoices.update(client, 123, %{
  amount: 1200.00
})
```

### Campaign Budgets

```elixir
# List campaign budgets
{:ok, budgets} = CampaignFlow.Client.CampaignBudgets.list(client)

# Get a specific budget
{:ok, budget} = CampaignFlow.Client.CampaignBudgets.get(client, 123)

# Create a campaign budget
{:ok, budget} = CampaignFlow.Client.CampaignBudgets.create(client, %{
  campaign_id: 123,
  amount: 10000.00
})

# Update a campaign budget
{:ok, budget} = CampaignFlow.Client.CampaignBudgets.update(client, 123, %{
  amount: 12000.00
})

# List finance options
{:ok, options} = CampaignFlow.Client.CampaignBudgets.list_finance_options(client, 123)

# Get a finance quote
{:ok, quote} = CampaignFlow.Client.CampaignBudgets.get_finance_quote(client, 123, "OPTION_CODE")
```

### Finance Applications

```elixir
# List finance applications
{:ok, applications} = CampaignFlow.Client.FinanceApplications.list(client)

# Get a specific application
{:ok, application} = CampaignFlow.Client.FinanceApplications.get(client, 123)

# Set application status
{:ok, response} = CampaignFlow.Client.FinanceApplications.set_status(client, 123, %{
  status: "approved"
})

# Submit an application
{:ok, response} = CampaignFlow.Client.FinanceApplications.submit(client, 123)
```

### Users, Tenants, Properties

```elixir
# Users
{:ok, users} = CampaignFlow.Client.Users.list(client)
{:ok, user} = CampaignFlow.Client.Users.get(client, 123)

# Tenants
{:ok, tenants} = CampaignFlow.Client.Tenants.list(client)
{:ok, tenant} = CampaignFlow.Client.Tenants.get(client, 123)

# Properties
{:ok, properties} = CampaignFlow.Client.Properties.list(client)
{:ok, property} = CampaignFlow.Client.Properties.get(client, 123)
```

### Error Handling

The client returns tuples with `{:ok, result}` or `{:error, reason}`:

```elixir
case CampaignFlow.Client.Campaigns.get(client, 123) do
  {:ok, campaign} ->
    # Process campaign
    IO.inspect(campaign)

  {:error, :not_found} ->
    # Handle not found
    IO.puts("Campaign not found")

  {:error, :unauthorized} ->
    # Handle authentication error
    IO.puts("Authentication failed")

  {:error, {:validation_error, details}} ->
    # Handle validation errors
    IO.inspect(details)

  {:error, reason} ->
    # Handle other errors
    IO.inspect(reason)
end
```

### Pagination

Most list endpoints support pagination:

```elixir
# Get page 2 with 50 items per page
{:ok, campaigns} = CampaignFlow.Client.Campaigns.list(client,
  page: 2,
  per_page: 50
)
```

## Authentication

The client uses a **shared token manager** architecture for OAuth2 authentication:

1. **Application Startup**: When your application starts, a `TokenManager` GenServer process is started for each configured environment (`:prod`, `:test`, etc.)
2. **Automatic Token Acquisition**: When you make your first API request, the TokenManager automatically obtains an access token using the OAuth2 client credentials flow
3. **Token Caching**: The token is cached in the TokenManager process and shared across all processes in your application
4. **Automatic Refresh**: When a token expires, it's automatically refreshed. Only one refresh occurs even with concurrent requests
5. **Race-Condition Free**: GenServer's message queue ensures only one token refresh happens at a time, preventing duplicate refresh requests

### How It Works

```elixir
# Multiple processes can safely use the same client
client = CampaignFlow.Client.new(environment: :prod)

# These calls all share the same token from TokenManager
Task.async(fn -> CampaignFlow.Client.Campaigns.list(client) end)
Task.async(fn -> CampaignFlow.Client.Agencies.list(client) end)
Task.async(fn -> CampaignFlow.Client.Users.list(client) end)

# When the token expires, only ONE refresh request is made
# All concurrent calls automatically receive the new token
```

### Benefits

- **Shared State**: One token per environment, not one per client instance
- **Thread-Safe**: Race-condition-free token refresh coordination
- **Memory Efficient**: Reduced memory usage compared to per-client tokens
- **Secure**: Credentials never stored in client structs, only in TokenManager processes
- **Simple**: No need to manually manage or refresh tokens

## Available Resources

The following resource modules are available:

- `CampaignFlow.Client.Campaigns` - Campaign management
- `CampaignFlow.Client.Agencies` - Agency operations
- `CampaignFlow.Client.Invoices` - Invoice management
- `CampaignFlow.Client.CampaignBudgets` - Campaign budget operations
- `CampaignFlow.Client.FinanceApplications` - Finance application management
- `CampaignFlow.Client.Users` - User operations
- `CampaignFlow.Client.Tenants` - Tenant management
- `CampaignFlow.Client.Properties` - Property operations
- `CampaignFlow.Client.Referrals` - Referral management
- `CampaignFlow.Client.Signatories` - Signatory operations

## Mock Server (Embedded Mode)

The library ships with an optional embedded mock Campaign Flow server that you can run inside your own application. It's intended for:

- Local development without real API credentials
- Integration and end-to-end tests that exercise the full client → HTTP → server flow without hitting production
- CI environments where you want deterministic, sandboxed referral data

The mock server is implemented as a Plug router backed by your application's existing `Ecto.Repo` (PostgreSQL). It is opt-in and only loads when the optional dependencies are present.

### Optional Dependencies

To use the mock server, add the following optional dependencies to your application's `mix.exs`:

```elixir
def deps do
  [
    {:campaign_flow, "~> 2.1.0"},
    # Required only if you use the embedded mock server:
    {:bandit, "~> 1.0"},
    {:plug, "~> 1.14"},
    {:ecto_sql, "~> 3.10"},
    {:postgrex, "~> 0.17"}
  ]
end
```

If you mount the mock router inside an existing Phoenix app, you likely already have `plug` and `ecto_sql`/`postgrex` and only need to ensure they are available.

### Configuration

Enable the mock server in the environment(s) where you want it to run (typically `config/dev.exs` and `config/test.exs`):

```elixir
# config/test.exs
config :campaign_flow, :mock_server,
  enabled: true,
  repo: MyApp.Repo
```

When `enabled: true`, the application supervisor automatically starts `CampaignFlow.MockServer` for you. The `:repo` option tells the mock server which `Ecto.Repo` to use for storage — it must be a Postgres repo owned by your application.

Then point a Campaign Flow client environment at the mock URL:

```elixir
config :campaign_flow,
  environments: [
    mock: [
      base_url: "http://localhost:4000/campaign-flow-mock/api/v2",
      client_id: "test_key",
      client_secret: "test_secret"
    ]
  ]

# In your code
client = CampaignFlow.Client.new(environment: :mock)
```

The mock server validates that an `Authorization: Bearer ...` header is present, but it does not verify the token value, so any non-empty `client_id`/`client_secret` will work.

### Migrations

The mock server stores data in normalized PostgreSQL tables (`properties`, `campaigns`, `campaign_budgets`, `agents`, `vendor_parties`, `referrals`). Generate a migration in your application and call into [`CampaignFlow.MockServer.Migrations`](lib/campaign_flow/mock_server/migrations.ex):

```bash
mix ecto.gen.migration add_campaign_flow_mock_server
```

```elixir
defmodule MyApp.Repo.Migrations.AddCampaignFlowMockServer do
  use Ecto.Migration

  def up, do: CampaignFlow.MockServer.Migrations.up(version: 1)
  def down, do: CampaignFlow.MockServer.Migrations.down(version: 1)
end
```

You can pass a `:prefix` option to install the tables in a non-`public` schema if you need to isolate them.

### Mounting in a Phoenix Router

Forward a path in your Phoenix router to [`CampaignFlow.MockServer.Router`](lib/campaign_flow/mock_server/router.ex):

```elixir
# lib/my_app_web/router.ex
forward "/campaign-flow-mock", CampaignFlow.MockServer.Router
```

This makes the mock available at `http://localhost:4000/campaign-flow-mock/api/v2/...` alongside the rest of your Phoenix app — no extra port, no separate process to manage.

### Implemented Endpoints

The mock server currently implements a focused subset of the API for end-to-end referral testing:

| Method | Path                       | Description                                       |
|--------|----------------------------|---------------------------------------------------|
| POST   | `/api/v2/referrals`        | Create a referral (atomic, normalized via `Ecto.Multi`) |
| GET    | `/api/v2/referrals/:id`    | Fetch a previously-created referral               |
| GET    | `/health`                  | Health check                                      |

Additional endpoints can be added over time — contributions welcome.

### Resetting State

Between tests you can clear all mock-server data with:

```elixir
CampaignFlow.MockServer.clear_all()
```

If your test suite uses `Ecto.Adapters.SQL.Sandbox`, transactional rollbacks will normally handle cleanup for you and you won't need to call this directly.

## Development

```bash
# Get dependencies
mix deps.get

# Run tests
mix test

# Generate documentation
mix docs

# Format code
mix format
```

## License

MIT

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.