ExBooking. Availability
(ExBooking v0.1.0)
View Source
Availability assembly and eligibility checks.
This module expands each resource's offerable wall time, subtracts busy time with buffers, generates candidate slots, applies policy predicates, and combines results according to the meeting participant mode.
Resources and rules are paired by position. :one offers slots for any free
resource, :collective requires every resource to be free, and :pool
offers slots while enough seats remain across capacity-aware resources.
Example
iex> meeting_type = %ExBooking.MeetingType{id: "intro", duration_min: 30}
...> resource = %ExBooking.Resource{id: "resource_1", timezone: "Etc/UTC"}
...>
...> rule = %ExBooking.AvailabilityRule{
...> timezone: "Etc/UTC",
...> windows: [%{weekday: 1, start_time: ~T[09:00:00], end_time: ~T[10:00:00]}]
...> }
...>
...> {:ok, [slot, _]} =
...> ExBooking.Availability.assemble(meeting_type, [resource], [rule],
...> now: ~U[2026-07-08 12:00:00Z],
...> from: ~U[2026-07-13 00:00:00Z],
...> until: ~U[2026-07-13 23:59:59Z]
...> )
...>
...> slot.start_at
~U[2026-07-13 09:00:00Z]
Summary
Functions
Assembles bookable slots for a meeting type over a search horizon.
Returns the resources eligible to take the request's slot — free of conflict
and policy violations, and satisfying the participant mode — or
{:error, reasons} aggregating every failing reason. Drives both
validate/5 and ExBooking.decide/5.
Checks a specific requested slot against conflict and policy for a meeting
type, without committing to an assignment. Returns :ok, or
{:error, reasons} with every failing reason.
Validates meeting, resource, and rule inputs used by availability operations.
Validates a meeting type's complete kernel-facing shape.
Validates the structural relationship between a request and meeting type.
Functions
@spec assemble( ExBooking.MeetingType.t(), [ExBooking.Resource.t()], [ExBooking.AvailabilityRule.t()], keyword() ) :: {:ok, [ExBooking.Interval.t()]} | {:error, term()}
Assembles bookable slots for a meeting type over a search horizon.
Requires :now, :from, and :until in opts. Returns slots sorted
ascending by start_at, deduplicated across resources.
Example
iex> meeting_type = %ExBooking.MeetingType{id: "intro", duration_min: 30}
...>
...> ExBooking.Availability.assemble(meeting_type, [], [],
...> now: ~U[2026-07-08 12:00:00Z],
...> from: ~U[2026-07-13 00:00:00Z],
...> until: ~U[2026-07-14 00:00:00Z]
...> )
{:ok, []}
@spec eligible( ExBooking.Request.t(), ExBooking.MeetingType.t(), [ExBooking.Resource.t()], [ExBooking.AvailabilityRule.t()], DateTime.t() ) :: {:ok, [ExBooking.Resource.t()]} | {:error, [term()] | {:invalid, atom(), term()}}
Returns the resources eligible to take the request's slot — free of conflict
and policy violations, and satisfying the participant mode — or
{:error, reasons} aggregating every failing reason. Drives both
validate/5 and ExBooking.decide/5.
Example
iex> slot =
...> ExBooking.Interval.new!(
...> ~U[2026-07-13 09:00:00Z],
...> ~U[2026-07-13 09:30:00Z]
...> )
...>
...> meeting_type = %ExBooking.MeetingType{id: "intro", duration_min: 30}
...>
...> request = %ExBooking.Request{
...> meeting_type_id: "intro",
...> invitee_timezone: "Etc/UTC",
...> slot: slot
...> }
...>
...> resource = %ExBooking.Resource{id: "resource_1", timezone: "Etc/UTC"}
...>
...> rule = %ExBooking.AvailabilityRule{
...> timezone: "Etc/UTC",
...> windows: [%{weekday: 1, start_time: ~T[09:00:00], end_time: ~T[10:00:00]}]
...> }
...>
...> {:ok, [eligible]} =
...> ExBooking.Availability.eligible(
...> request,
...> meeting_type,
...> [resource],
...> [rule],
...> ~U[2026-07-08 12:00:00Z]
...> )
...>
...> eligible.id
"resource_1"
@spec validate( ExBooking.Request.t(), ExBooking.MeetingType.t(), [ExBooking.Resource.t()], [ExBooking.AvailabilityRule.t()], keyword() ) :: :ok | {:error, [term()] | {:invalid, atom(), term()}}
Checks a specific requested slot against conflict and policy for a meeting
type, without committing to an assignment. Returns :ok, or
{:error, reasons} with every failing reason.
Example
iex> slot =
...> ExBooking.Interval.new!(
...> ~U[2026-07-13 09:00:00Z],
...> ~U[2026-07-13 09:30:00Z]
...> )
...>
...> meeting_type = %ExBooking.MeetingType{id: "intro", duration_min: 30}
...>
...> request = %ExBooking.Request{
...> meeting_type_id: "intro",
...> invitee_timezone: "Etc/UTC",
...> slot: slot
...> }
...>
...> resource = %ExBooking.Resource{id: "resource_1", timezone: "Etc/UTC"}
...>
...> rule = %ExBooking.AvailabilityRule{
...> timezone: "Etc/UTC",
...> windows: [%{weekday: 1, start_time: ~T[09:00:00], end_time: ~T[10:00:00]}]
...> }
...>
...> ExBooking.Availability.validate(
...> request,
...> meeting_type,
...> [resource],
...> [rule],
...> now: ~U[2026-07-08 12:00:00Z]
...> )
:ok
@spec validate_inputs(ExBooking.MeetingType.t(), [ExBooking.Resource.t()], [ ExBooking.AvailabilityRule.t() ]) :: :ok | {:error, {:invalid, atom(), term()}}
Validates meeting, resource, and rule inputs used by availability operations.
This preflight keeps hand-built malformed structs away from temporal arithmetic, timezone conversion, assignment, and slot generation.
Example
iex> meeting_type = %ExBooking.MeetingType{id: "intro", duration_min: 30}
...> resource = %ExBooking.Resource{id: "resource_1", timezone: "Etc/UTC"}
...> rule = %ExBooking.AvailabilityRule{timezone: "Etc/UTC", windows: []}
...> ExBooking.Availability.validate_inputs(meeting_type, [resource], [rule])
:ok
@spec validate_meeting_type(ExBooking.MeetingType.t()) :: :ok | {:error, {:invalid, atom(), term()}}
Validates a meeting type's complete kernel-facing shape.
Examples
iex> ExBooking.Availability.validate_meeting_type(%ExBooking.MeetingType{
...> id: "intro",
...> duration_min: 30
...> })
:ok
@spec validate_request_shape(ExBooking.Request.t(), ExBooking.MeetingType.t()) :: :ok | {:error, {:invalid, atom(), term()}}
Validates the structural relationship between a request and meeting type.
This check is independent of resource availability and returns a tagged malformed-input error rather than booking rejection reasons.
Example
iex> slot = ExBooking.Interval.new!(~U[2026-07-13 09:00:00Z], ~U[2026-07-13 09:30:00Z])
...>
...> request = %ExBooking.Request{
...> meeting_type_id: "intro",
...> invitee_timezone: "Etc/UTC",
...> slot: slot
...> }
...>
...> meeting_type = %ExBooking.MeetingType{id: "intro", duration_min: 30}
...> ExBooking.Availability.validate_request_shape(request, meeting_type)
:ok