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