# 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