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

assemble(meeting_type, resources, rules, opts)

@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, []}

eligible(request, meeting_type, resources, rules, now)

@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"

validate(request, meeting_type, resources, rules, opts)

@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

validate_inputs(meeting_type, resources, rules)

@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

validate_meeting_type(meeting_type)

@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

validate_request_shape(request, meeting_type)

@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