Hex Version Hex Docs

An Elixir client for the Deputy API, organized by resource type.

Installation

This package can be installed by adding deputy to your list of dependencies in mix.exs:

def deps do
  [
    {:deputy, "~> 0.2.1"}
  ]
end

Getting Started

First, create a new Deputy client with your API credentials:

client = Deputy.new(
  base_url: "https://your-subdomain.deputy.com",
  api_key: "your-api-key"
)

Then, use the client to interact with the Deputy API:

# Get locations
{:ok, locations} = Deputy.Locations.list(client)

# Get employees
{:ok, employees} = Deputy.Employees.list(client)

API Modules

The library is organized into modules by resource type:

Each module provides functions for interacting with the corresponding Deputy API endpoints.

Error Handling

All API functions return either {:ok, result} or {:error, error} tuples. The error can be one of several types:

  • %Deputy.Error.API{} - API error with details from Deputy (status, code, message)
  • %Deputy.Error.HTTP{} - HTTP-level error (network issues, server errors)
  • %Deputy.Error.ParseError{} - Error parsing the API response
  • %Deputy.Error.ValidationError{} - Request validation failed
  • %Deputy.Error.RateLimitError{} - Rate limit exceeded

Example of handling different error types:

case Deputy.Locations.get(client, 12345) do
  {:ok, location} ->
    # Process location data
    IO.inspect(location)

  {:error, %Deputy.Error.API{status: 404}} ->
    # Handle not found error
    IO.puts("Location not found")

  {:error, %Deputy.Error.HTTP{reason: reason}} ->
    # Handle HTTP error
    IO.puts("HTTP error: #{inspect(reason)}")

  {:error, %Deputy.Error.RateLimitError{retry_after: seconds}} ->
    # Handle rate limit
    IO.puts("Rate limited. Try again in #{seconds} seconds")
end

Bang Functions

Each API function has a corresponding bang (!) version that raises an exception instead of returning an error tuple. This is useful when you want to fail fast if an error occurs.

# Using regular function with error tuple
{:ok, locations} = Deputy.Locations.list(client)

# Using bang version that raises an exception on error
locations = Deputy.Locations.list!(client)

Examples

Working with Locations

# List all locations
{:ok, locations} = Deputy.Locations.list(client)
# Or using the bang version
locations = Deputy.Locations.list!(client)

# Get a specific location
{:ok, location} = Deputy.Locations.get(client, 123)
# Or using the bang version
location = Deputy.Locations.get!(client, 123)

# Create a new location
attrs = %{
  strWorkplaceName: "New Location",
  strWorkplaceCode: "NLC",
  strAddress: "123 Test St",
  intIsWorkplace: 1,
  intIsPayrollEntity: 1,
  strTimezone: "America/New_York"
}
{:ok, new_location} = Deputy.Locations.create(client, attrs)

# Update a location with error handling
case Deputy.Locations.update(client, 123, %{strWorkplaceCode: "UPD"}) do
  {:ok, updated} ->
    IO.puts("Location updated successfully")

  {:error, %Deputy.Error.API{status: 404}} ->
    IO.puts("Location not found")

  {:error, %Deputy.Error.ValidationError{message: message}} ->
    IO.puts("Validation error: #{message}")
end

Working with Employees

# List all employees
{:ok, employees} = Deputy.Employees.list(client)
# Or using the bang version
employees = Deputy.Employees.list!(client)

# Get a specific employee with error handling for rate limits
case Deputy.Employees.get(client, 123) do
  {:ok, employee} ->
    IO.inspect(employee)

  {:error, %Deputy.Error.RateLimitError{retry_after: seconds}} ->
    Process.sleep(seconds * 1000)
    # Retry after waiting
    {:ok, employee} = Deputy.Employees.get(client, 123)
end

# Create a new employee
attrs = %{
  strFirstName: "John",
  strLastName: "Doe",
  intCompanyId: 1,
  intGender: 1,
  strCountryCode: "US",
  strDob: "1980-01-01",
  strStartDate: "2023-01-01",
  strMobilePhone: "5551234567"
}

# Using the bang version with exception rescue
try do
  new_employee = Deputy.Employees.create!(client, attrs)
  IO.puts("Employee created successfully")
rescue
  e in Deputy.Error.ValidationError ->
    IO.puts("Validation error: #{e.message}")
  e in Deputy.Error.API ->
    IO.puts("API error (#{e.status}): #{e.message}")
end

Working with Timesheets

# Start a timesheet using the bang version
timesheet = Deputy.Timesheets.start!(client, %{
  intEmployeeId: 123,
  intCompanyId: 456
})

# Stop a timesheet with error handling
case Deputy.Timesheets.stop(client, %{intTimesheetId: 789}) do
  {:ok, result} ->
    IO.puts("Timesheet stopped successfully")

  {:error, %Deputy.Error.API{status: 404}} ->
    IO.puts("Timesheet not found")

  {:error, %Deputy.Error.API{status: 400, message: message}} ->
    IO.puts("Bad request: #{message}")

  {:error, %Deputy.Error.HTTP{reason: reason}} ->
    IO.puts("HTTP error: #{inspect(reason)}")
end

# Get timesheet details
{:ok, details} = Deputy.Timesheets.get_details(client, 789)
# Or using the bang version
details = Deputy.Timesheets.get_details!(client, 789)

Working with Authenticated User Data

# Get information about the authenticated user
{:ok, user} = Deputy.My.me(client)
# Or using the bang version
user = Deputy.My.me!(client)

# Get locations where the authenticated user can work
{:ok, locations} = Deputy.My.locations(client)

# Get the authenticated user's rosters with error handling
case Deputy.My.rosters(client) do
  {:ok, rosters} ->
    IO.inspect(rosters)

  {:error, %Deputy.Error.API{status: status, message: message}} ->
    IO.puts("API error (#{status}): #{message}")

  {:error, %Deputy.Error.HTTP{reason: :timeout}} ->
    IO.puts("Request timed out, try again later")

  {:error, error} ->
    IO.puts("Unexpected error: #{inspect(error)}")
end

# Get the authenticated user's timesheets
timesheets = Deputy.My.timesheets!(client)

Testing

You can test your code that uses this library by leveraging the Deputy.HTTPClient.Mock provided for testing. This allows you to mock the API responses in your tests.

# In your test setup
Mox.defmock(Deputy.HTTPClient.Mock, for: Deputy.HTTPClient.Behaviour)

test "list employees success" do
  client = Deputy.new(
    base_url: "https://test.deputy.com",
    api_key: "test-key",
    http_client: Deputy.HTTPClient.Mock
  )

  # Set up expectations for success
  Deputy.HTTPClient.Mock
  |> expect(:request, fn opts ->
    assert Keyword.get(opts, :method) == :get
    assert Keyword.get(opts, :url) == "https://test.deputy.com/api/v1/supervise/employee"

    {:ok, [%{"Id" => 1, "FirstName" => "John", "LastName" => "Doe"}]}
  end)

  # Call the function
  {:ok, employees} = Deputy.Employees.list(client)

  # Assertions
  assert length(employees) == 1
  assert hd(employees)["FirstName"] == "John"
end

test "list employees error handling" do
  client = Deputy.new(
    base_url: "https://test.deputy.com",
    api_key: "test-key",
    http_client: Deputy.HTTPClient.Mock
  )

  # Set up expectations for error
  Deputy.HTTPClient.Mock
  |> expect(:request, fn _opts ->
    error = Deputy.Error.from_response(%{
      status: 403,
      body: %{"error" => %{"code" => "permission_denied", "message" => "Permission denied"}}
    })
    {:error, error}
  end)

  # Call the function and test error handling
  assert {:error, %Deputy.Error.API{status: 403, code: "permission_denied"}} =
    Deputy.Employees.list(client)

  # Test bang version raises exception
  assert_raise Deputy.Error.API, fn ->
    Deputy.Employees.list!(client)
  end
end

Documentation

Detailed documentation can be found at https://hexdocs.pm/deputy.

License

Deputy is released under the MIT license.