README.md

# REA Pricing Client

An Elixir client library for the [REA Pricing API](https://developer.realestate.com.au/), built with the [Req](https://hexdocs.pm/req) HTTP client.

## 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
- **Type-Safe** - Function signatures with `@spec` for compile-time checking
- **HAL+JSON Support** - Handles REA's HAL+JSON response format
- **RFC-7807 Errors** - Structured error handling compatible with REA's error format

## Installation

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

```elixir
def deps do
  [
    {:rea, "~> 0.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 :rea,
  environments: [
    prod: [
      base_url: "https://api.realestate.com.au",
      client_id: System.get_env("REA_PROD_CLIENT_ID"),
      client_secret: System.get_env("REA_PROD_CLIENT_SECRET")
    ]
  ]
```

### Runtime Configuration

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

```elixir
import Config

if config_env() == :prod do
  config :rea,
    environments: [
      prod: [
        base_url: "https://api.realestate.com.au",
        client_id: System.fetch_env!("REA_PROD_CLIENT_ID"),
        client_secret: System.fetch_env!("REA_PROD_CLIENT_SECRET")
      ]
    ]
end
```

### Environment Variables

Set the following environment variables:

```bash
# Production credentials
export REA_PROD_CLIENT_ID="your_client_id"
export REA_PROD_CLIENT_SECRET="your_client_secret"
```

### Multiple Environments

You can configure and use multiple environments simultaneously:

```elixir
config :rea,
  environments: [
    prod: [
      base_url: "https://api.realestate.com.au",
      client_id: System.get_env("REA_PROD_CLIENT_ID"),
      client_secret: System.get_env("REA_PROD_CLIENT_SECRET")
    ],
    test: [
      base_url: "https://api.test.realestate.com.au",
      client_id: System.get_env("REA_TEST_CLIENT_ID"),
      client_secret: System.get_env("REA_TEST_CLIENT_SECRET")
    ]
  ]

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

## 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 = Rea.Client.new(environment: :prod)

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

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

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

### Realestate.com.au Prices

Get product prices for properties listed on realestate.com.au:

```elixir
# Get prices with required parameters
{:ok, prices} = Rea.Client.RealestateProperties.get_prices(client,
  agency_id: "FEXBNP",
  suburb: "Sydney",
  state: "NSW",
  postcode: "2000"
)

# With optional parameters
{:ok, prices} = Rea.Client.RealestateProperties.get_prices(client,
  agency_id: "FEXBNP",
  suburb: "Sydney",
  state: "NSW",
  postcode: "2000",
  section: "rent",           # "buy" (default), "rent", or "sold"
  listing_type: "residential", # "residential" (default), "land", or "rural"
  reupgrade: false,          # default: false
  date: "2024-06-30T10:00:00Z"
)
```

The response includes product categories:
- `"all"` - Products automatically purchased when listing
- `"elect"` - Optional products with minimum purchase commitments
- `"manual"` - Products that can be manually purchased
- `"flex"` - Products requiring Campaign flex points
- `"cappedProduct"` - Products with usage caps

### Realcommercial.com.au Prices

Get product prices for properties listed on realcommercial.com.au:

```elixir
{:ok, prices} = Rea.Client.CommercialProperties.get_prices(client,
  agency_id: "FEXBNP",
  suburb: "Sydney",
  state: "NSW",
  postcode: "2000"
)
```

The response includes:
- `"subscription"` - Subscription type (Diamond, Flexi, Access, Standard)
- `"productPrice"` - Price options per section (sale/lease)
- `"productOption"` - Available upgrade options
- Listing tiers: `"elite_plus"`, `"elite"`, `"enhanced"`, `"basic"`

### Error Handling

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

```elixir
case Rea.Client.RealestateProperties.get_prices(client, params) do
  {:ok, prices} ->
    # Process prices
    IO.inspect(prices)

  {:error, {:not_found, body}} ->
    # Handle not found - no products available
    IO.puts("No products found: #{body["title"]}")

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

  {:error, {:bad_request, body}} ->
    # Handle invalid parameters
    IO.puts("Invalid request: #{body["detail"]}")

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

You can use the `Rea.Client.Error` module for structured error handling:

```elixir
case Rea.Client.RealestateProperties.get_prices(client, params) do
  {:ok, prices} ->
    prices

  {:error, reason} ->
    error = Rea.Client.Error.new(reason)
    IO.puts(Rea.Client.Error.message(error))
    IO.puts(Rea.Client.Error.detail(error))
end
```

## 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 = Rea.Client.new(environment: :prod)

# These calls all share the same token from TokenManager
Task.async(fn -> Rea.Client.RealestateProperties.get_prices(client, params1) end)
Task.async(fn -> Rea.Client.RealestateProperties.get_prices(client, params2) end)
Task.async(fn -> Rea.Client.CommercialProperties.get_prices(client, params3) 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:

- `Rea.Client.RealestateProperties` - Pricing for realestate.com.au listings
- `Rea.Client.CommercialProperties` - Pricing for realcommercial.com.au listings

## Development

```bash
# Get dependencies
mix deps.get

# Run tests
mix test

# Generate documentation
mix docs

# Format code
mix format
```

## License

MIT