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

## Installation

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

```elixir
def deps do
  [
    {:campaign_flow, "~> 2.0.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

## 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.