ExBooking.Assignment (ExBooking v0.2.1)

View Source

Deterministic resource assignment.

Assignment runs after availability has already established which resources can take the slot. Strategies use explicit fairness inputs from the caller, then fall back to resource id so ties are stable and replayable.

A scorer may rank resources before the strategy key. The scorer receives the request routing context as opaque data; the kernel never inspects CRM, GTM, territory, or enrichment semantics.

Example

iex> slot = ExBooking.Interval.new!(~U[2026-07-13 09:00:00Z], ~U[2026-07-13 09:30:00Z])
...>
...> resources = [
...>   %ExBooking.Resource{id: "b", timezone: "Etc/UTC"},
...>   %ExBooking.Resource{id: "a", timezone: "Etc/UTC"}
...> ]
...>
...> {:ok, [winner]} = ExBooking.Assignment.assign(resources, slot, [])
...> winner.id
"a"

Summary

Types

Assignment strategy selector.

Opaque scoring hook over routing context.

Functions

Picks the resource(s) that take a booking for slot.

Validates a strategy and any fairness inputs it consumes.

Validates strategy and scorer options independently of resource data.

Types

base_strategy()

@type base_strategy() ::
  :first_available
  | :round_robin
  | :least_recently_booked
  | :weighted
  | :priority

Assignment strategy selector.

scorer()

@type scorer() :: (ExBooking.Resource.t(), map() -> number())

Opaque scoring hook over routing context.

strategy()

@type strategy() ::
  base_strategy()
  | {:owner_first, owner_id: String.t(), fallback: base_strategy()}

Functions

assign(resources, slot, opts)

@spec assign([ExBooking.Resource.t()], ExBooking.Interval.t(), keyword()) ::
  {:ok, [ExBooking.Resource.t()]}
  | {:error, :no_eligible_resource | {:invalid, atom(), term()}}

Picks the resource(s) that take a booking for slot.

Options: :strategy (default :first_available), :scorer, :routing_context (passed to the scorer), :participants (default :one), and :capacity_required (for :pool).

Examples

iex> resources = [
...>   %ExBooking.Resource{id: "b", timezone: "Etc/UTC"},
...>   %ExBooking.Resource{id: "a", timezone: "Etc/UTC"}
...> ]
...>
...> slot = ExBooking.Interval.new!(~U[2026-07-13 09:00:00Z], ~U[2026-07-13 09:30:00Z])
...>
...> {:ok, [winner]} =
...>   ExBooking.Assignment.assign(resources, slot, strategy: :first_available)
...>
...> winner.id
"a"

validate(resources, opts)

@spec validate([ExBooking.Resource.t()], keyword()) ::
  :ok | {:error, {:invalid, atom(), term()}}

Validates a strategy and any fairness inputs it consumes.

Validation is separate from selection so callers can reject malformed input before performing availability work.

Example

iex> resource = %ExBooking.Resource{id: "resource_1", timezone: "Etc/UTC"}
...> ExBooking.Assignment.validate([resource], strategy: :round_robin)
:ok

validate_options(opts)

@spec validate_options(keyword()) :: :ok | {:error, {:invalid, atom(), term()}}

Validates strategy and scorer options independently of resource data.

Other keys belong to the calling operation and are checked by its option schema.

Examples

iex> ExBooking.Assignment.validate_options(strategy: :round_robin)
:ok
iex> ExBooking.Assignment.validate_options(strategy: :random)
{:error, {:invalid, :strategy, :random}}