feat: refactor space booking view to be more efficient

This commit is contained in:
JasterV 2026-02-02 17:03:01 +01:00
commit 7ff3adf730
14 changed files with 657 additions and 1555 deletions

View file

@ -13,10 +13,6 @@ defmodule SpazioSolazzo.BookingSystem do
define :create_space, define :create_space,
action: :create, action: :create,
args: [:name, :slug, :description, :capacity] args: [:name, :slug, :description, :capacity]
define :check_availability,
action: :check_availability,
args: [:space_id, :date, :start_time, :end_time]
end end
resource SpazioSolazzo.BookingSystem.TimeSlotTemplate do resource SpazioSolazzo.BookingSystem.TimeSlotTemplate do
@ -40,9 +36,9 @@ defmodule SpazioSolazzo.BookingSystem do
define :count_pending_requests, action: :count_pending_requests define :count_pending_requests, action: :count_pending_requests
define :get_slot_booking_counts, define :list_bookings_by_datetime_range,
action: :get_slot_booking_counts, action: :by_datetime_range_and_status,
args: [:space_id, :date, :start_time, :end_time] args: [:space_id, :user_id, :start_datetime, :end_datetime, :states]
define :create_booking, define :create_booking,
action: :create, action: :create,

View file

@ -26,6 +26,18 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
references do references do
reference :user, on_delete: :nilify, index?: true reference :user, on_delete: :nilify, index?: true
end end
custom_indexes do
# Composite index for space + datetime range queries (most common pattern)
index [:space_id, :start_datetime, :end_datetime]
# Composite index for space + state queries (filtering by status)
index [:space_id, :state]
# Single indexes for datetime overlap queries
index [:start_datetime]
index [:end_datetime]
end
end end
state_machine do state_machine do
@ -97,6 +109,49 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
filter expr(state == :requested) filter expr(state == :requested)
end end
read :by_datetime_range_and_status do
description "Fetch bookings within a date/time range with optional filters"
argument :space_id, :uuid, allow_nil?: true
argument :user_id, :uuid, allow_nil?: true
argument :start_datetime, :datetime, allow_nil?: false
argument :end_datetime, :datetime, allow_nil?: false
argument :states, {:array, :atom}, allow_nil?: true
prepare fn query, _ctx ->
start_dt = Ash.Query.get_argument(query, :start_datetime)
end_dt = Ash.Query.get_argument(query, :end_datetime)
# Base datetime overlap filter
query =
Ash.Query.filter(
query,
start_datetime < ^end_dt and end_datetime > ^start_dt
)
# Optional space filter
query =
case Ash.Query.get_argument(query, :space_id) do
nil -> query
space_id -> Ash.Query.filter(query, space_id == ^space_id)
end
# Optional user filter
query =
case Ash.Query.get_argument(query, :user_id) do
nil -> query
user_id -> Ash.Query.filter(query, user_id == ^user_id)
end
# Optional states filter
case Ash.Query.get_argument(query, :states) do
nil -> query
[] -> query
states -> Ash.Query.filter(query, state in ^states)
end
end
end
create :create do create :create do
argument :space_id, :uuid, allow_nil?: false argument :space_id, :uuid, allow_nil?: false
argument :user_id, :uuid, allow_nil?: true argument :user_id, :uuid, allow_nil?: true
@ -366,52 +421,10 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
description "Delete a booking record" description "Delete a booking record"
primary? true primary? true
end end
action :get_slot_booking_counts, :map do
argument :space_id, :uuid, allow_nil?: true
argument :date, :date, allow_nil?: false
argument :start_time, :time, allow_nil?: false
argument :end_time, :time, allow_nil?: false
run fn input, _context ->
space_id = input.arguments.space_id
date = input.arguments.date
start_time = input.arguments.start_time
end_time = input.arguments.end_time
start_datetime = DateTime.new!(date, start_time, "Etc/UTC")
end_datetime = DateTime.new!(date, end_time, "Etc/UTC")
query =
__MODULE__
|> Ash.Query.filter(
start_datetime < ^end_datetime and end_datetime > ^start_datetime and
(state == :requested or state == :accepted)
)
query =
if space_id do
Ash.Query.filter(query, space_id == ^space_id)
else
query
end
case Ash.read(query) do
{:ok, bookings} ->
pending_count = Enum.count(bookings, &(&1.state == :requested))
approved_count = Enum.count(bookings, &(&1.state == :accepted))
{:ok, %{pending: pending_count, approved: approved_count}}
error ->
error
end
end
end
end end
policies do policies do
policy action([:cancel, :approve, :reject, :get_slot_booking_counts]) do policy action([:cancel, :approve, :reject]) do
authorize_if always() authorize_if always()
end end

View file

@ -30,66 +30,9 @@ defmodule SpazioSolazzo.BookingSystem.Space do
end end
end end
end end
action :check_availability, :atom do
argument :space_id, :uuid, allow_nil?: false
argument :date, :date, allow_nil?: false
argument :start_time, :time, allow_nil?: false
argument :end_time, :time, allow_nil?: false
run fn input, _context ->
require Ash.Query
space_id = input.arguments.space_id
date_arg = input.arguments.date
start_time_arg = input.arguments.start_time
end_time_arg = input.arguments.end_time
start_datetime = DateTime.new!(date_arg, start_time_arg, "Etc/UTC")
end_datetime = DateTime.new!(date_arg, end_time_arg, "Etc/UTC")
# Load the space
case Ash.get(__MODULE__, space_id) do
{:ok, space} ->
# Get accepted bookings for this space that overlap with the requested time slot
query =
SpazioSolazzo.BookingSystem.Booking
|> Ash.Query.filter(
expr(
space_id == ^space_id and state == :accepted and start_datetime < ^end_datetime and
end_datetime > ^start_datetime
)
)
case Ash.read(query) do
{:ok, overlapping_bookings} ->
current_count = length(overlapping_bookings)
availability =
if current_count >= space.capacity do
:over_capacity
else
:available
end
{:ok, availability}
error ->
error
end
error ->
error
end
end
end
end end
policies do policies do
policy action(:check_availability) do
authorize_if always()
end
policy action_type(:read) do policy action_type(:read) do
authorize_if always() authorize_if always()
end end

View file

@ -67,4 +67,17 @@ defmodule SpazioSolazzo.BookingSystem.TimeSlotTemplate do
public? true public? true
end end
end end
calculations do
calculate :booking_stats,
:map,
{SpazioSolazzo.BookingSystem.TimeSlotTemplate.Calculations.SlotBookingStats, []} do
description "Calculates booking counts and availability for this time slot on a specific date"
argument :date, :date, allow_nil?: false
argument :space_id, :uuid, allow_nil?: false
argument :capacity, :integer, allow_nil?: false
argument :user_id, :uuid, allow_nil?: true
end
end
end end

View file

@ -0,0 +1,66 @@
defmodule SpazioSolazzo.BookingSystem.TimeSlotTemplate.Calculations.SlotBookingStats do
@moduledoc """
Calculates booking statistics for time slots by fetching all bookings for the day once,
then filtering in memory. This eliminates N+1 query problems.
"""
use Ash.Resource.Calculation
@impl true
def load(_query, _opts, _context) do
[:start_time, :end_time, :space_id]
end
@impl true
def calculate(records, _opts, %{arguments: arguments}) do
date = Map.get(arguments, :date)
space_id = Map.get(arguments, :space_id)
capacity = Map.get(arguments, :capacity)
user_id = Map.get(arguments, :user_id)
# Fetch all bookings for the entire day ONCE
day_start = DateTime.new!(date, ~T[00:00:00], "Etc/UTC")
day_end = DateTime.new!(date, ~T[23:59:59], "Etc/UTC")
{:ok, all_bookings} =
SpazioSolazzo.BookingSystem.list_bookings_by_datetime_range(
space_id,
nil,
day_start,
day_end,
[:requested, :accepted]
)
# Calculate stats for each slot using the cached bookings
Enum.map(records, fn slot ->
slot_start = DateTime.new!(date, slot.start_time, "Etc/UTC")
slot_end = DateTime.new!(date, slot.end_time, "Etc/UTC")
# Filter bookings that overlap with this slot
overlapping =
Enum.filter(all_bookings, fn booking ->
DateTime.compare(booking.start_datetime, slot_end) == :lt and
DateTime.compare(booking.end_datetime, slot_start) == :gt
end)
requested_count = Enum.count(overlapping, &(&1.state == :requested))
accepted_count = Enum.count(overlapping, &(&1.state == :accepted))
user_has_booking =
if user_id do
Enum.any?(overlapping, &(&1.user_id == user_id))
else
false
end
availability = if accepted_count >= capacity, do: :over_capacity, else: :available
%{
requested_count: requested_count,
accepted_count: accepted_count,
user_has_booking: user_has_booking,
availability_status: availability
}
end)
end
end

View file

@ -153,20 +153,31 @@ defmodule SpazioSolazzoWeb.Admin.WalkInLive do
assign(socket, time_slot_warning: nil) assign(socket, time_slot_warning: nil)
else else
date = socket.assigns.selected_date date = socket.assigns.selected_date
start_time = socket.assigns.start_time
end_time = socket.assigns.end_time
space_id = socket.assigns.coworking_space.id
capacity = socket.assigns.coworking_space.capacity
case BookingSystem.check_availability( start_datetime = DateTime.new!(date, start_time, "Etc/UTC")
socket.assigns.coworking_space.id, end_datetime = DateTime.new!(date, end_time, "Etc/UTC")
date,
socket.assigns.start_time,
socket.assigns.end_time
) do
{:ok, :over_capacity} ->
assign(socket,
time_slot_warning: "This time slot is currently overbooked. Proceed with caution."
)
_ -> {:ok, bookings} =
assign(socket, time_slot_warning: nil) BookingSystem.list_bookings_by_datetime_range(
space_id,
nil,
start_datetime,
end_datetime,
[:accepted]
)
accepted_count = length(bookings)
if accepted_count >= capacity do
assign(socket,
time_slot_warning: "This time slot is currently overbooked. Proceed with caution."
)
else
assign(socket, time_slot_warning: nil)
end end
end end
end end

View file

@ -8,12 +8,9 @@ defmodule SpazioSolazzoWeb.SpaceBookingLive do
case BookingSystem.get_space_by_slug(space_slug) do case BookingSystem.get_space_by_slug(space_slug) do
{:ok, space} -> {:ok, space} ->
selected_date = Date.utc_today() selected_date = Date.utc_today()
current_user = socket.assigns[:current_user]
{:ok, time_slots} = time_slots = load_time_slots_with_stats(space, selected_date, current_user)
BookingSystem.get_space_time_slots_by_date(space.id, selected_date)
{:ok, bookings} =
BookingSystem.list_accepted_space_bookings_by_date(space.id, selected_date)
if connected?(socket) do if connected?(socket) do
Phoenix.PubSub.subscribe(SpazioSolazzo.PubSub, "booking:created") Phoenix.PubSub.subscribe(SpazioSolazzo.PubSub, "booking:created")
@ -26,20 +23,13 @@ defmodule SpazioSolazzoWeb.SpaceBookingLive do
socket socket
|> assign( |> assign(
space: space, space: space,
bookings: bookings,
selected_date: selected_date, selected_date: selected_date,
selected_time_slot: nil, selected_time_slot: nil,
show_booking_modal: false, show_booking_modal: false,
show_success_modal: false, show_success_modal: false,
time_slots: time_slots, time_slots: time_slots,
current_scope: nil, current_scope: nil
slot_availability: %{}, )}
slot_booking_counts: %{},
user_booked_slots: %{}
)
|> compute_slot_availability()
|> compute_slot_booking_counts()
|> compute_user_booked_slots()}
{:error, _error} -> {:error, _error} ->
{:ok, {:ok,
@ -53,7 +43,7 @@ defmodule SpazioSolazzoWeb.SpaceBookingLive do
time_slot = Enum.find(socket.assigns.time_slots, &(&1.id == time_slot_id)) time_slot = Enum.find(socket.assigns.time_slots, &(&1.id == time_slot_id))
# Prevent opening modal if user already has a booking for this slot # Prevent opening modal if user already has a booking for this slot
if socket.assigns.user_booked_slots[time_slot_id] do if time_slot && time_slot.booking_stats.user_has_booking do
{:noreply, socket} {:noreply, socket}
else else
{:noreply, {:noreply,
@ -99,13 +89,11 @@ defmodule SpazioSolazzoWeb.SpaceBookingLive do
{:error, %Ash.Error.Invalid{errors: errors}} -> {:error, %Ash.Error.Invalid{errors: errors}} ->
error_message = error_message =
errors Enum.map_join(errors, ", ", fn
|> Enum.map(fn
%{field: :date, message: msg} -> msg %{field: :date, message: msg} -> msg
%{message: msg} -> msg %{message: msg} -> msg
_error -> "Invalid booking request" _error -> "Invalid booking request"
end) end)
|> Enum.join(", ")
{:noreply, {:noreply,
socket socket
@ -121,111 +109,43 @@ defmodule SpazioSolazzoWeb.SpaceBookingLive do
end end
def handle_info({:date_selected, date}, socket) do def handle_info({:date_selected, date}, socket) do
{:ok, time_slots} = current_user = socket.assigns[:current_user]
BookingSystem.get_space_time_slots_by_date(socket.assigns.space.id, date) time_slots = load_time_slots_with_stats(socket.assigns.space, date, current_user)
{:ok, bookings} =
BookingSystem.list_accepted_space_bookings_by_date(socket.assigns.space.id, date)
{:noreply, {:noreply,
socket socket
|> assign( |> assign(
selected_date: date, selected_date: date,
time_slots: time_slots, time_slots: time_slots
bookings: bookings )}
)
|> compute_slot_availability()
|> compute_slot_booking_counts()
|> compute_user_booked_slots()}
end end
def handle_info( def handle_info(
%{topic: "booking:" <> _event, payload: %{data: %{space_id: space_id, date: date}}}, %{topic: "booking:" <> _event, payload: %{data: %{space_id: space_id, date: date}}},
%{assigns: %{space: %{id: space_id}, selected_date: date}} = socket %{assigns: %{space: %{id: space_id}, selected_date: date}} = socket
) do ) do
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space_id, date) current_user = socket.assigns[:current_user]
time_slots = load_time_slots_with_stats(socket.assigns.space, date, current_user)
{:noreply, {:noreply,
socket socket
|> assign(bookings: bookings) |> assign(time_slots: time_slots)}
|> compute_slot_availability()
|> compute_slot_booking_counts()
|> compute_user_booked_slots()}
end end
def handle_info(_msg, socket) do def handle_info(_msg, socket) do
{:noreply, socket} {:noreply, socket}
end end
defp compute_slot_availability(socket) do defp load_time_slots_with_stats(space, date, current_user) do
slot_availability = {:ok, time_slots} = BookingSystem.get_space_time_slots_by_date(space.id, date)
socket.assigns.time_slots
|> Enum.map(fn time_slot ->
{:ok, status} =
BookingSystem.check_availability(
socket.assigns.space.id,
socket.assigns.selected_date,
time_slot.start_time,
time_slot.end_time
)
{time_slot.id, status} Ash.load!(time_slots,
end) booking_stats: %{
|> Map.new() date: date,
space_id: space.id,
assign(socket, slot_availability: slot_availability) capacity: space.capacity,
end user_id: current_user && current_user.id
}
defp compute_slot_booking_counts(socket) do )
slot_booking_counts =
socket.assigns.time_slots
|> Enum.map(fn time_slot ->
{:ok, counts} =
BookingSystem.get_slot_booking_counts(
socket.assigns.space.id,
socket.assigns.selected_date,
time_slot.start_time,
time_slot.end_time
)
{time_slot.id, counts}
end)
|> Map.new()
assign(socket, slot_booking_counts: slot_booking_counts)
end
defp compute_user_booked_slots(socket) do
current_user = socket.assigns.current_user
user_booked_slots =
if current_user do
socket.assigns.time_slots
|> Enum.map(fn time_slot ->
start_datetime =
DateTime.new!(socket.assigns.selected_date, time_slot.start_time, "Etc/UTC")
end_datetime =
DateTime.new!(socket.assigns.selected_date, time_slot.end_time, "Etc/UTC")
existing_bookings =
SpazioSolazzo.BookingSystem.Booking
|> Ash.Query.filter(
user_id == ^current_user.id and
space_id == ^socket.assigns.space.id and
(state == :requested or state == :accepted) and
start_datetime < ^end_datetime and
end_datetime > ^start_datetime
)
|> Ash.read!()
{time_slot.id, existing_bookings != []}
end)
|> Map.new()
else
%{}
end
assign(socket, user_booked_slots: user_booked_slots)
end end
end end

View file

@ -45,83 +45,83 @@
</div> </div>
<% else %> <% else %>
<%= for time_slot <- @time_slots do %> <%= for time_slot <- @time_slots do %>
<% availability = Map.get(@slot_availability, time_slot.id, :available) %> <% availability = time_slot.booking_stats.availability_status %>
<% counts = <% requested_count = time_slot.booking_stats.requested_count %>
Map.get(@slot_booking_counts, time_slot.id, %{pending: 0, approved: 0}) %> <% accepted_count = time_slot.booking_stats.accepted_count %>
<% user_has_booking = Map.get(@user_booked_slots, time_slot.id, false) %> <% user_has_booking = time_slot.booking_stats.user_has_booking %>
<button <button
phx-click={if user_has_booking, do: nil, else: "select_slot"} phx-click={if user_has_booking, do: nil, else: "select_slot"}
phx-value-time_slot_id={time_slot.id} phx-value-time_slot_id={time_slot.id}
disabled={user_has_booking} disabled={user_has_booking}
class={[ class={[
"w-full p-4 rounded-xl border-2 transition-all duration-200 text-left", "w-full p-4 rounded-xl border-2 transition-all duration-200 text-left",
if(user_has_booking, if(user_has_booking,
do: do:
"border-slate-200 bg-slate-100 cursor-not-allowed opacity-60 dark:bg-slate-700/50 dark:border-slate-600", "border-slate-200 bg-slate-100 cursor-not-allowed opacity-60 dark:bg-slate-700/50 dark:border-slate-600",
else: else:
if(availability == :available, if(availability == :available,
do: do:
"border-green-200 bg-green-50 hover:border-green-500 hover:shadow-lg cursor-pointer dark:bg-green-900/20 dark:border-green-800 dark:hover:border-green-600", "border-green-200 bg-green-50 hover:border-green-500 hover:shadow-lg cursor-pointer dark:bg-green-900/20 dark:border-green-800 dark:hover:border-green-600",
else: else:
"border-yellow-200 bg-yellow-50 hover:border-yellow-500 hover:shadow-lg cursor-pointer dark:bg-yellow-900/20 dark:border-yellow-800 dark:hover:border-yellow-600" "border-yellow-200 bg-yellow-50 hover:border-yellow-500 hover:shadow-lg cursor-pointer dark:bg-yellow-900/20 dark:border-yellow-800 dark:hover:border-yellow-600"
) )
) )
]} ]}
> >
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex-1"> <div class="flex-1">
<div class="text-lg font-semibold text-slate-900 dark:text-white"> <div class="text-lg font-semibold text-slate-900 dark:text-white">
{Calendar.strftime(time_slot.start_time, "%H:%M")} - {Calendar.strftime( {Calendar.strftime(time_slot.start_time, "%H:%M")} - {Calendar.strftime(
time_slot.end_time, time_slot.end_time,
"%H:%M" "%H:%M"
)} )}
</div>
<%= if user_has_booking do %>
<div class="text-sm text-slate-500 dark:text-slate-400 font-medium mt-1">
Already Requested
</div> </div>
<%= if user_has_booking do %> <% else %>
<div class="text-sm text-slate-500 dark:text-slate-400 font-medium mt-1"> <%= if availability == :available do %>
Already Requested <div class="text-sm text-green-600 dark:text-green-400 font-medium mt-1">
Available - Request Booking
</div> </div>
<% else %> <% else %>
<%= if availability == :available do %> <div class="text-sm text-yellow-600 dark:text-yellow-400 font-medium mt-1">
<div class="text-sm text-green-600 dark:text-green-400 font-medium mt-1"> High Demand - Join Waitlist
Available - Request Booking </div>
</div> <% end %>
<% else %> <% end %>
<div class="text-sm text-yellow-600 dark:text-yellow-400 font-medium mt-1"> <div class="flex gap-3 mt-2 text-xs text-slate-600 dark:text-slate-400">
High Demand - Join Waitlist <%= if requested_count > 0 do %>
</div> <span class="flex items-center gap-1">
<% end %> <.icon name="hero-clock" class="w-3.5 h-3.5" />
{requested_count} pending
</span>
<% end %>
<%= if accepted_count > 0 do %>
<span class="flex items-center gap-1">
<.icon name="hero-check-circle" class="w-3.5 h-3.5" />
{accepted_count} booked
</span>
<% end %> <% end %>
<div class="flex gap-3 mt-2 text-xs text-slate-600 dark:text-slate-400">
<%= if counts.pending > 0 do %>
<span class="flex items-center gap-1">
<.icon name="hero-clock" class="w-3.5 h-3.5" />
{counts.pending} pending
</span>
<% end %>
<%= if counts.approved > 0 do %>
<span class="flex items-center gap-1">
<.icon name="hero-check-circle" class="w-3.5 h-3.5" />
{counts.approved} booked
</span>
<% end %>
</div>
</div> </div>
<.icon
name={if user_has_booking, do: "hero-check", else: "hero-arrow-right"}
class={[
"w-5 h-5",
if(user_has_booking,
do: "text-slate-400 dark:text-slate-500",
else:
if(availability == :available,
do: "text-green-500 dark:text-green-400",
else: "text-yellow-500 dark:text-yellow-400"
)
)
]}
/>
</div> </div>
</button> <.icon
name={if user_has_booking, do: "hero-check", else: "hero-arrow-right"}
class={[
"w-5 h-5",
if(user_has_booking,
do: "text-slate-400 dark:text-slate-500",
else:
if(availability == :available,
do: "text-green-500 dark:text-green-400",
else: "text-yellow-500 dark:text-yellow-400"
)
)
]}
/>
</div>
</button>
<% end %> <% end %>
<% end %> <% end %>
</div> </div>
@ -146,7 +146,7 @@
current_user={@current_user} current_user={@current_user}
slot_availability={ slot_availability={
if @selected_time_slot do if @selected_time_slot do
Map.get(@slot_availability, @selected_time_slot.id, :available) @selected_time_slot.booking_stats.availability_status
else else
:available :available
end end

View file

@ -0,0 +1,29 @@
defmodule SpazioSolazzo.Repo.Migrations.AddBookingIndexes do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
create index(:bookings, [:end_datetime])
create index(:bookings, [:start_datetime])
create index(:bookings, [:space_id, :state])
create index(:bookings, [:space_id, :start_datetime, :end_datetime])
end
def down do
drop_if_exists index(:bookings, [:space_id, :start_datetime, :end_datetime])
drop_if_exists index(:bookings, [:space_id, :state])
drop_if_exists index(:bookings, [:start_datetime])
drop_if_exists index(:bookings, [:end_datetime])
end
end

View file

@ -0,0 +1,365 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"precision": null,
"primary_key?": true,
"references": null,
"scale": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "start_datetime",
"type": "utc_datetime"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "end_datetime",
"type": "utc_datetime"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "date",
"type": "date"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "customer_name",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "customer_email",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "start_time",
"type": "time"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "end_time",
"type": "time"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "customer_phone",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "customer_comment",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "cancellation_reason",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "rejection_reason",
"type": "text"
},
{
"allow_nil?": false,
"default": "\"requested\"",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "state",
"type": "text"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "inserted_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "updated_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "bookings_space_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "spaces"
},
"scale": null,
"size": null,
"source": "space_id",
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": true,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "bookings_user_id_fkey",
"on_delete": "nilify",
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "users"
},
"scale": null,
"size": null,
"source": "user_id",
"type": "uuid"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [
{
"all_tenants?": false,
"concurrently": false,
"error_fields": [
"space_id",
"start_datetime",
"end_datetime"
],
"fields": [
{
"type": "atom",
"value": "space_id"
},
{
"type": "atom",
"value": "start_datetime"
},
{
"type": "atom",
"value": "end_datetime"
}
],
"include": null,
"message": null,
"name": null,
"nulls_distinct": true,
"prefix": null,
"table": null,
"unique": false,
"using": null,
"where": null
},
{
"all_tenants?": false,
"concurrently": false,
"error_fields": [
"space_id",
"state"
],
"fields": [
{
"type": "atom",
"value": "space_id"
},
{
"type": "atom",
"value": "state"
}
],
"include": null,
"message": null,
"name": null,
"nulls_distinct": true,
"prefix": null,
"table": null,
"unique": false,
"using": null,
"where": null
},
{
"all_tenants?": false,
"concurrently": false,
"error_fields": [
"start_datetime"
],
"fields": [
{
"type": "atom",
"value": "start_datetime"
}
],
"include": null,
"message": null,
"name": null,
"nulls_distinct": true,
"prefix": null,
"table": null,
"unique": false,
"using": null,
"where": null
},
{
"all_tenants?": false,
"concurrently": false,
"error_fields": [
"end_datetime"
],
"fields": [
{
"type": "atom",
"value": "end_datetime"
}
],
"include": null,
"message": null,
"name": null,
"nulls_distinct": true,
"prefix": null,
"table": null,
"unique": false,
"using": null,
"where": null
}
],
"custom_statements": [],
"has_create_action": true,
"hash": "5DB2211C98FA47BC6D744D8FD763CC8920F067DFD092EC120CBD2115E26810CF",
"identities": [],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.SpazioSolazzo.Repo",
"schema": null,
"table": "bookings"
}

View file

@ -576,195 +576,6 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
end end
end end
describe "check_availability/4" do
test "returns :available when under public capacity", %{space: space, date: date} do
{:ok, booking} =
request_booking(
space.id,
nil,
date,
~T[09:00:00],
~T[10:00:00],
"User 1",
"user1@example.com",
"",
""
)
{:ok, _} = BookingSystem.approve_booking(booking.id)
{:ok, status} =
BookingSystem.check_availability(
space.id,
date,
~T[09:00:00],
~T[10:00:00]
)
assert status == :available
end
test "returns :over_capacity when at or over capacity", %{
space: space,
date: date
} do
for i <- 1..2 do
{:ok, booking} =
request_booking(
space.id,
nil,
date,
~T[09:00:00],
~T[10:00:00],
"User #{i}",
"user#{i}@example.com",
"",
""
)
{:ok, _} = BookingSystem.approve_booking(booking.id)
end
{:ok, status} =
BookingSystem.check_availability(
space.id,
date,
~T[09:00:00],
~T[10:00:00]
)
assert status == :over_capacity
end
test "only counts overlapping bookings", %{space: space, date: date} do
{:ok, booking1} =
request_booking(
space.id,
nil,
date,
~T[09:00:00],
~T[10:00:00],
"User 1",
"user1@example.com",
"",
""
)
{:ok, _} = BookingSystem.approve_booking(booking1.id)
{:ok, booking2} =
request_booking(
space.id,
nil,
date,
~T[10:00:00],
~T[11:00:00],
"User 2",
"user2@example.com",
"",
""
)
{:ok, _} = BookingSystem.approve_booking(booking2.id)
{:ok, status} =
BookingSystem.check_availability(
space.id,
date,
~T[10:00:00],
~T[11:00:00]
)
assert status == :available
end
test "counts partial overlaps", %{space: space, date: date} do
for i <- 1..2 do
{:ok, booking} =
request_booking(
space.id,
nil,
date,
~T[09:00:00],
~T[11:00:00],
"User #{i}",
"user#{i}@example.com",
"",
""
)
{:ok, _} = BookingSystem.approve_booking(booking.id)
end
{:ok, status} =
BookingSystem.check_availability(
space.id,
date,
~T[10:00:00],
~T[12:00:00]
)
assert status == :over_capacity
end
test "does not count pending bookings", %{space: space, date: date} do
for i <- 1..3 do
{:ok, _booking} =
request_booking(
space.id,
nil,
date,
~T[09:00:00],
~T[10:00:00],
"User #{i}",
"user#{i}@example.com",
"",
""
)
end
{:ok, status} =
BookingSystem.check_availability(
space.id,
date,
~T[09:00:00],
~T[10:00:00]
)
assert status == :available
end
test "does not count cancelled bookings", %{space: space, date: date} do
for i <- 1..3 do
{:ok, booking} =
request_booking(
space.id,
nil,
date,
~T[09:00:00],
~T[10:00:00],
"User #{i}",
"user#{i}@example.com",
"",
""
)
{:ok, _} = BookingSystem.approve_booking(booking.id)
{:ok, _} = BookingSystem.cancel_booking(booking.id, "Test cancellation")
end
{:ok, status} =
BookingSystem.check_availability(
space.id,
date,
~T[09:00:00],
~T[10:00:00]
)
assert status == :available
end
end
describe "count_pending_requests/0" do describe "count_pending_requests/0" do
test "returns only pending bookings", %{space: space, date: date} do test "returns only pending bookings", %{space: space, date: date} do
{:ok, _pending1} = {:ok, _pending1} =
@ -1014,290 +825,5 @@ defmodule SpazioSolazzo.BookingSystem.BookingTest do
assert booking.start_time == start_time assert booking.start_time == start_time
assert booking.end_time == end_time assert booking.end_time == end_time
end end
test "walk-in bookings are counted in availability check", %{space: space} do
date = Date.add(Date.utc_today(), 1)
start_time = ~T[09:00:00]
end_time = ~T[10:00:00]
for i <- 1..3 do
BookingSystem.create_walk_in(
space.id,
DateTime.new!(date, start_time, "Etc/UTC"),
DateTime.new!(date, end_time, "Etc/UTC"),
"Customer #{i}",
"customer#{i}@example.com",
nil,
nil
)
end
{:ok, status} = BookingSystem.check_availability(space.id, date, start_time, end_time)
assert status == :over_capacity
end
end
describe "get_slot_booking_counts/4" do
test "counts pending and approved bookings in time slot", %{space: space, date: date} do
{:ok, _pending1} =
request_booking(
space.id,
nil,
date,
~T[09:00:00],
~T[10:00:00],
"User 1",
"user1@example.com",
"",
""
)
{:ok, approved1} =
request_booking(
space.id,
nil,
date,
~T[09:00:00],
~T[10:00:00],
"User 2",
"user2@example.com",
"",
""
)
{:ok, _} = BookingSystem.approve_booking(approved1.id)
{:ok, approved2} =
request_booking(
space.id,
nil,
date,
~T[09:00:00],
~T[10:00:00],
"User 3",
"user3@example.com",
"",
""
)
{:ok, _} = BookingSystem.approve_booking(approved2.id)
{:ok, counts} =
BookingSystem.get_slot_booking_counts(space.id, date, ~T[09:00:00], ~T[10:00:00])
assert counts.pending == 1
assert counts.approved == 2
end
test "only counts overlapping bookings", %{space: space, date: date} do
{:ok, _pending1} =
request_booking(
space.id,
nil,
date,
~T[08:00:00],
~T[09:00:00],
"User 1",
"user1@example.com",
"",
""
)
{:ok, _pending2} =
request_booking(
space.id,
nil,
date,
~T[10:00:00],
~T[11:00:00],
"User 2",
"user2@example.com",
"",
""
)
{:ok, counts} =
BookingSystem.get_slot_booking_counts(space.id, date, ~T[09:00:00], ~T[10:00:00])
assert counts.pending == 0
assert counts.approved == 0
end
test "counts partial overlaps", %{space: space, date: date} do
{:ok, _pending1} =
request_booking(
space.id,
nil,
date,
~T[08:30:00],
~T[09:30:00],
"User 1",
"user1@example.com",
"",
""
)
{:ok, approved1} =
request_booking(
space.id,
nil,
date,
~T[09:30:00],
~T[10:30:00],
"User 2",
"user2@example.com",
"",
""
)
{:ok, _} = BookingSystem.approve_booking(approved1.id)
{:ok, counts} =
BookingSystem.get_slot_booking_counts(space.id, date, ~T[09:00:00], ~T[10:00:00])
assert counts.pending == 1
assert counts.approved == 1
end
test "does not count cancelled bookings", %{space: space, date: date} do
{:ok, cancelled} =
request_booking(
space.id,
nil,
date,
~T[09:00:00],
~T[10:00:00],
"User 1",
"user1@example.com",
"",
""
)
{:ok, _} = BookingSystem.cancel_booking(cancelled.id, "Test cancellation")
{:ok, counts} =
BookingSystem.get_slot_booking_counts(space.id, date, ~T[09:00:00], ~T[10:00:00])
assert counts.pending == 0
assert counts.approved == 0
end
test "does not count rejected bookings", %{space: space, date: date} do
{:ok, rejected} =
request_booking(
space.id,
nil,
date,
~T[09:00:00],
~T[10:00:00],
"User 1",
"user1@example.com",
"",
""
)
{:ok, _} = BookingSystem.reject_booking(rejected.id, "Space not available")
{:ok, counts} =
BookingSystem.get_slot_booking_counts(space.id, date, ~T[09:00:00], ~T[10:00:00])
assert counts.pending == 0
assert counts.approved == 0
end
test "filters by date correctly", %{space: space, date: date} do
other_date = Date.add(date, 1)
{:ok, _pending1} =
request_booking(
space.id,
nil,
other_date,
~T[09:00:00],
~T[10:00:00],
"User 1",
"user1@example.com",
"",
""
)
{:ok, counts} =
BookingSystem.get_slot_booking_counts(space.id, date, ~T[09:00:00], ~T[10:00:00])
assert counts.pending == 0
assert counts.approved == 0
end
test "filters by space correctly", %{space: space, date: date} do
{:ok, other_space} =
BookingSystem.create_space(
"Other Space",
"other-space-counts",
"Other description",
5
)
{:ok, _pending1} =
request_booking(
other_space.id,
nil,
date,
~T[09:00:00],
~T[10:00:00],
"User 1",
"user1@example.com",
"",
""
)
{:ok, counts} =
BookingSystem.get_slot_booking_counts(space.id, date, ~T[09:00:00], ~T[10:00:00])
assert counts.pending == 0
assert counts.approved == 0
end
test "works with nil space_id to count all spaces", %{space: space, date: date} do
{:ok, other_space} =
BookingSystem.create_space(
"Other Space",
"other-space-all",
"Other description",
5
)
{:ok, _pending1} =
request_booking(
space.id,
nil,
date,
~T[09:00:00],
~T[10:00:00],
"User 1",
"user1@example.com",
"",
""
)
{:ok, _pending2} =
request_booking(
other_space.id,
nil,
date,
~T[09:00:00],
~T[10:00:00],
"User 2",
"user2@example.com",
"",
""
)
{:ok, counts} =
BookingSystem.get_slot_booking_counts(nil, date, ~T[09:00:00], ~T[10:00:00])
assert counts.pending == 2
assert counts.approved == 0
end
end end
end end

View file

@ -1,501 +0,0 @@
defmodule SpazioSolazzo.BookingSystem.MultiDayBookingTest do
@moduledoc """
Tests for multi-day booking functionality using datetime fields.
Verifies that bookings can span multiple days and that datetime range
queries work correctly for availability checking and listing.
"""
use SpazioSolazzo.DataCase, async: true
alias SpazioSolazzo.BookingSystem
setup do
{:ok, space} =
BookingSystem.create_space(
"Coworking",
"coworking",
"Coworking space for testing",
5
)
%{space: space}
end
describe "multi-day walk-in bookings" do
test "can create a multi-day booking spanning 3 days", %{space: space} do
start_date = Date.add(Date.utc_today(), 1)
end_date = Date.add(Date.utc_today(), 3)
start_datetime = DateTime.new!(start_date, ~T[09:00:00], "Etc/UTC")
end_datetime = DateTime.new!(end_date, ~T[18:00:00], "Etc/UTC")
{:ok, booking} =
BookingSystem.create_walk_in(
space.id,
start_datetime,
end_datetime,
"John Doe",
"john@example.com",
nil,
nil
)
assert booking.start_datetime == start_datetime
assert booking.end_datetime == end_datetime
assert booking.state == :accepted
assert booking.customer_name == "John Doe"
end
test "multi-day booking appears in queries for all days it spans", %{space: space} do
start_date = Date.add(Date.utc_today(), 1)
end_date = Date.add(Date.utc_today(), 4)
start_datetime = DateTime.new!(start_date, ~T[10:00:00], "Etc/UTC")
end_datetime = DateTime.new!(end_date, ~T[17:00:00], "Etc/UTC")
{:ok, _booking} =
BookingSystem.create_walk_in(
space.id,
start_datetime,
end_datetime,
"Jane Smith",
"jane@example.com",
nil,
nil
)
# Should appear on start date
{:ok, day1_bookings} =
BookingSystem.list_accepted_space_bookings_by_date(space.id, start_date)
assert length(day1_bookings) == 1
# Should appear on middle date
middle_date = Date.add(start_date, 1)
{:ok, day2_bookings} =
BookingSystem.list_accepted_space_bookings_by_date(space.id, middle_date)
assert length(day2_bookings) == 1
# Should appear on end date
{:ok, day4_bookings} =
BookingSystem.list_accepted_space_bookings_by_date(space.id, end_date)
assert length(day4_bookings) == 1
# Should not appear on day after end date
day_after = Date.add(end_date, 1)
{:ok, day_after_bookings} =
BookingSystem.list_accepted_space_bookings_by_date(space.id, day_after)
assert length(day_after_bookings) == 0
end
test "multi-day booking correctly counts toward availability on all days", %{space: space} do
start_date = Date.add(Date.utc_today(), 1)
end_date = Date.add(Date.utc_today(), 3)
start_datetime = DateTime.new!(start_date, ~T[09:00:00], "Etc/UTC")
end_datetime = DateTime.new!(end_date, ~T[18:00:00], "Etc/UTC")
{:ok, _booking} =
BookingSystem.create_walk_in(
space.id,
start_datetime,
end_datetime,
"Test User",
"test@example.com",
nil,
nil
)
# Check availability on start date
{:ok, availability_day1} =
BookingSystem.check_availability(space.id, start_date, ~T[10:00:00], ~T[16:00:00])
# Should show reduced availability due to the multi-day booking
assert availability_day1 in [:available, :over_public_capacity]
# Check availability on middle date
middle_date = Date.add(start_date, 1)
{:ok, availability_day2} =
BookingSystem.check_availability(space.id, middle_date, ~T[10:00:00], ~T[16:00:00])
assert availability_day2 in [:available, :over_public_capacity]
# Check availability on end date
{:ok, availability_day3} =
BookingSystem.check_availability(space.id, end_date, ~T[10:00:00], ~T[16:00:00])
assert availability_day3 in [:available, :over_public_capacity]
end
test "multiple overlapping multi-day bookings correctly fill capacity", %{space: space} do
start_date = Date.add(Date.utc_today(), 1)
end_date = Date.add(Date.utc_today(), 3)
# Create 5 multi-day bookings (public capacity)
for i <- 1..5 do
start_datetime = DateTime.new!(start_date, ~T[09:00:00], "Etc/UTC")
end_datetime = DateTime.new!(end_date, ~T[18:00:00], "Etc/UTC")
{:ok, _booking} =
BookingSystem.create_walk_in(
space.id,
start_datetime,
end_datetime,
"User #{i}",
"user#{i}@example.com",
nil,
nil
)
end
# Check that public capacity is reached on middle day
middle_date = Date.add(start_date, 1)
{:ok, availability} =
BookingSystem.check_availability(space.id, middle_date, ~T[10:00:00], ~T[16:00:00])
assert availability == :over_capacity
end
test "can have both single-day and multi-day bookings on the same day", %{space: space} do
date = Date.add(Date.utc_today(), 1)
# Create a multi-day booking
multi_start = DateTime.new!(date, ~T[09:00:00], "Etc/UTC")
multi_end = DateTime.new!(Date.add(date, 2), ~T[18:00:00], "Etc/UTC")
{:ok, _multi_booking} =
BookingSystem.create_walk_in(
space.id,
multi_start,
multi_end,
"Multi Day User",
"multi@example.com",
nil,
nil
)
# Create a single-day booking on the same date
single_start = DateTime.new!(date, ~T[10:00:00], "Etc/UTC")
single_end = DateTime.new!(date, ~T[16:00:00], "Etc/UTC")
{:ok, _single_booking} =
BookingSystem.create_walk_in(
space.id,
single_start,
single_end,
"Single Day User",
"single@example.com",
nil,
nil
)
# Both should appear in the query for that date
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, date)
assert length(bookings) == 2
customer_names = Enum.map(bookings, & &1.customer_name)
assert "Multi Day User" in customer_names
assert "Single Day User" in customer_names
end
test "slot booking counts correctly include multi-day bookings", %{space: space} do
date = Date.add(Date.utc_today(), 1)
# Create a multi-day booking that includes this date
multi_start = DateTime.new!(Date.add(date, -1), ~T[09:00:00], "Etc/UTC")
multi_end = DateTime.new!(Date.add(date, 1), ~T[18:00:00], "Etc/UTC")
{:ok, _multi_booking} =
BookingSystem.create_walk_in(
space.id,
multi_start,
multi_end,
"Multi Day User",
"multi@example.com",
nil,
nil
)
# Create a single-day booking on the same date
single_start = DateTime.new!(date, ~T[10:00:00], "Etc/UTC")
single_end = DateTime.new!(date, ~T[16:00:00], "Etc/UTC")
{:ok, _single_booking} =
BookingSystem.create_walk_in(
space.id,
single_start,
single_end,
"Single Day User",
"single@example.com",
nil,
nil
)
# Get slot counts for a time range on that date
{:ok, counts} =
BookingSystem.get_slot_booking_counts(space.id, date, ~T[11:00:00], ~T[15:00:00])
# Should count both bookings
assert counts.approved == 2
assert counts.pending == 0
end
test "multi-day booking with different start and end times", %{space: space} do
start_date = Date.add(Date.utc_today(), 1)
end_date = Date.add(Date.utc_today(), 5)
# Booking starts at 2 PM on day 1 and ends at 11 AM on day 5
start_datetime = DateTime.new!(start_date, ~T[14:00:00], "Etc/UTC")
end_datetime = DateTime.new!(end_date, ~T[11:00:00], "Etc/UTC")
{:ok, booking} =
BookingSystem.create_walk_in(
space.id,
start_datetime,
end_datetime,
"Extended Stay User",
"extended@example.com",
"+1234567890",
"Long term booking"
)
assert booking.start_datetime == start_datetime
assert booking.end_datetime == end_datetime
assert booking.customer_phone == "+1234567890"
assert booking.customer_comment == "Long term booking"
# Verify it appears on all days
for day_offset <- 0..4 do
check_date = Date.add(start_date, day_offset)
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, check_date)
assert length(bookings) == 1
assert hd(bookings).customer_name == "Extended Stay User"
end
end
test "multi-day booking does not appear on days outside its range", %{space: space} do
start_date = Date.add(Date.utc_today(), 5)
end_date = Date.add(Date.utc_today(), 7)
start_datetime = DateTime.new!(start_date, ~T[09:00:00], "Etc/UTC")
end_datetime = DateTime.new!(end_date, ~T[18:00:00], "Etc/UTC")
{:ok, _booking} =
BookingSystem.create_walk_in(
space.id,
start_datetime,
end_datetime,
"Range Test User",
"range@example.com",
nil,
nil
)
# Should not appear on day before start
day_before = Date.add(start_date, -1)
{:ok, bookings_before} =
BookingSystem.list_accepted_space_bookings_by_date(space.id, day_before)
assert length(bookings_before) == 0
# Should appear on start date
{:ok, bookings_start} =
BookingSystem.list_accepted_space_bookings_by_date(space.id, start_date)
assert length(bookings_start) == 1
# Should appear on end date
{:ok, bookings_end} = BookingSystem.list_accepted_space_bookings_by_date(space.id, end_date)
assert length(bookings_end) == 1
# Should not appear on day after end
day_after = Date.add(end_date, 1)
{:ok, bookings_after} =
BookingSystem.list_accepted_space_bookings_by_date(space.id, day_after)
assert length(bookings_after) == 0
end
test "very long multi-day booking (30 days)", %{space: space} do
start_date = Date.add(Date.utc_today(), 1)
end_date = Date.add(Date.utc_today(), 30)
start_datetime = DateTime.new!(start_date, ~T[09:00:00], "Etc/UTC")
end_datetime = DateTime.new!(end_date, ~T[18:00:00], "Etc/UTC")
{:ok, booking} =
BookingSystem.create_walk_in(
space.id,
start_datetime,
end_datetime,
"Long Term User",
"longterm@example.com",
nil,
"Monthly booking"
)
assert booking.start_datetime == start_datetime
assert booking.end_datetime == end_datetime
# Spot check a few days
for day_offset <- [0, 10, 20, 29] do
check_date = Date.add(start_date, day_offset)
{:ok, bookings} = BookingSystem.list_accepted_space_bookings_by_date(space.id, check_date)
assert length(bookings) == 1
end
# Verify it doesn't appear the day after
day_after = Date.add(end_date, 1)
{:ok, bookings_after} =
BookingSystem.list_accepted_space_bookings_by_date(space.id, day_after)
assert length(bookings_after) == 0
end
end
describe "datetime range overlaps" do
test "detects overlap when new booking starts during existing booking", %{space: space} do
# Existing booking: Day 1-3
day1 = Date.add(Date.utc_today(), 1)
day2 = Date.add(Date.utc_today(), 2)
day3 = Date.add(Date.utc_today(), 3)
existing_start = DateTime.new!(day1, ~T[09:00:00], "Etc/UTC")
existing_end = DateTime.new!(day3, ~T[18:00:00], "Etc/UTC")
{:ok, _existing} =
BookingSystem.create_walk_in(
space.id,
existing_start,
existing_end,
"Existing User",
"existing@example.com",
nil,
nil
)
# Check overlap on day 2
{:ok, counts} =
BookingSystem.get_slot_booking_counts(space.id, day2, ~T[10:00:00], ~T[16:00:00])
assert counts.approved == 1
end
test "detects overlap when new booking ends during existing booking", %{space: space} do
# Existing booking: Day 3-5
day3 = Date.add(Date.utc_today(), 3)
day4 = Date.add(Date.utc_today(), 4)
day5 = Date.add(Date.utc_today(), 5)
existing_start = DateTime.new!(day3, ~T[09:00:00], "Etc/UTC")
existing_end = DateTime.new!(day5, ~T[18:00:00], "Etc/UTC")
{:ok, _existing} =
BookingSystem.create_walk_in(
space.id,
existing_start,
existing_end,
"Existing User",
"existing@example.com",
nil,
nil
)
# Check availability on day 4
{:ok, counts} =
BookingSystem.get_slot_booking_counts(space.id, day4, ~T[10:00:00], ~T[16:00:00])
assert counts.approved == 1
end
test "detects overlap when new booking completely contains existing booking", %{space: space} do
# Existing booking: Day 3-5
day3 = Date.add(Date.utc_today(), 3)
day4 = Date.add(Date.utc_today(), 4)
day5 = Date.add(Date.utc_today(), 5)
existing_start = DateTime.new!(day3, ~T[09:00:00], "Etc/UTC")
existing_end = DateTime.new!(day5, ~T[18:00:00], "Etc/UTC")
{:ok, _existing} =
BookingSystem.create_walk_in(
space.id,
existing_start,
existing_end,
"Existing User",
"existing@example.com",
nil,
nil
)
# Check if overlaps on day 4 (middle day)
{:ok, counts} =
BookingSystem.get_slot_booking_counts(space.id, day4, ~T[10:00:00], ~T[16:00:00])
assert counts.approved == 1
end
test "detects overlap when new booking is contained within existing booking", %{space: space} do
# Existing booking: Day 1-10
day1 = Date.add(Date.utc_today(), 1)
day5 = Date.add(Date.utc_today(), 5)
day10 = Date.add(Date.utc_today(), 10)
existing_start = DateTime.new!(day1, ~T[09:00:00], "Etc/UTC")
existing_end = DateTime.new!(day10, ~T[18:00:00], "Etc/UTC")
{:ok, _existing} =
BookingSystem.create_walk_in(
space.id,
existing_start,
existing_end,
"Existing User",
"existing@example.com",
nil,
nil
)
# Check availability on day 5 (middle day within long booking)
{:ok, counts} =
BookingSystem.get_slot_booking_counts(space.id, day5, ~T[10:00:00], ~T[16:00:00])
assert counts.approved == 1
end
test "no overlap when bookings are on consecutive days with no time overlap", %{space: space} do
# First booking: Day 1-2, ending at 12 PM on day 2
day1 = Date.add(Date.utc_today(), 1)
day2 = Date.add(Date.utc_today(), 2)
first_start = DateTime.new!(day1, ~T[09:00:00], "Etc/UTC")
first_end = DateTime.new!(day2, ~T[12:00:00], "Etc/UTC")
{:ok, _first} =
BookingSystem.create_walk_in(
space.id,
first_start,
first_end,
"First User",
"first@example.com",
nil,
nil
)
# Check availability on day 2 afternoon (after first booking ends)
{:ok, counts} =
BookingSystem.get_slot_booking_counts(space.id, day2, ~T[13:00:00], ~T[18:00:00])
assert counts.approved == 0
end
end
end

View file

@ -85,287 +85,4 @@ defmodule SpazioSolazzo.BookingSystem.SpaceTest do
assert space1.id != space2.id assert space1.id != space2.id
end end
end end
describe "check_availability/4" do
setup do
{:ok, space} =
BookingSystem.create_space(
"Availability Test Space",
"availability-test",
"Test description",
2
)
%{space: space}
end
test "returns :available when no bookings exist", %{space: space} do
date = Date.utc_today()
start_time = ~T[09:00:00]
end_time = ~T[10:00:00]
assert {:ok, :available} =
BookingSystem.check_availability(space.id, date, start_time, end_time)
end
test "returns :available when under capacity", %{space: space} do
date = Date.add(Date.utc_today(), 1)
start_time = ~T[09:00:00]
end_time = ~T[10:00:00]
BookingSystem.create_walk_in(
space.id,
DateTime.new!(date, start_time, "Etc/UTC"),
DateTime.new!(date, end_time, "Etc/UTC"),
"Customer 1",
"customer1@example.com",
nil,
nil
)
assert {:ok, :available} =
BookingSystem.check_availability(space.id, date, start_time, end_time)
end
test "returns :over_capacity when at or over capacity", %{space: space} do
date = Date.add(Date.utc_today(), 1)
start_time = ~T[09:00:00]
end_time = ~T[10:00:00]
BookingSystem.create_walk_in(
space.id,
DateTime.new!(date, start_time, "Etc/UTC"),
DateTime.new!(date, end_time, "Etc/UTC"),
"Customer 1",
"customer1@example.com",
nil,
nil
)
BookingSystem.create_walk_in(
space.id,
DateTime.new!(date, start_time, "Etc/UTC"),
DateTime.new!(date, end_time, "Etc/UTC"),
"Customer 2",
"customer2@example.com",
nil,
nil
)
assert {:ok, :over_capacity} =
BookingSystem.check_availability(space.id, date, start_time, end_time)
end
test "only counts overlapping bookings", %{space: space} do
date = Date.utc_today()
start_time = ~T[09:00:00]
end_time = ~T[10:00:00]
BookingSystem.create_walk_in(
space.id,
DateTime.new!(date, ~T[08:00:00], "Etc/UTC"),
DateTime.new!(date, ~T[09:00:00], "Etc/UTC"),
"Customer 1",
"customer1@example.com",
nil,
nil
)
BookingSystem.create_walk_in(
space.id,
DateTime.new!(date, ~T[10:00:00], "Etc/UTC"),
DateTime.new!(date, ~T[11:00:00], "Etc/UTC"),
"Customer 2",
"customer2@example.com",
nil,
nil
)
assert {:ok, :available} =
BookingSystem.check_availability(space.id, date, start_time, end_time)
end
test "counts partial overlaps", %{space: space} do
date = Date.add(Date.utc_today(), 1)
start_time = ~T[09:00:00]
end_time = ~T[10:00:00]
BookingSystem.create_walk_in(
space.id,
DateTime.new!(date, ~T[08:30:00], "Etc/UTC"),
DateTime.new!(date, ~T[09:30:00], "Etc/UTC"),
"Customer 1",
"customer1@example.com",
nil,
nil
)
BookingSystem.create_walk_in(
space.id,
DateTime.new!(date, ~T[09:30:00], "Etc/UTC"),
DateTime.new!(date, ~T[10:30:00], "Etc/UTC"),
"Customer 2",
"customer2@example.com",
nil,
nil
)
assert {:ok, :over_capacity} =
BookingSystem.check_availability(space.id, date, start_time, end_time)
end
test "does not count pending bookings", %{space: space} do
date = Date.add(Date.utc_today(), 1)
start_time = ~T[09:00:00]
end_time = ~T[10:00:00]
BookingSystem.create_booking(
space.id,
nil,
date,
start_time,
end_time,
"Customer 1",
"customer1@example.com",
nil,
nil
)
BookingSystem.create_booking(
space.id,
nil,
date,
start_time,
end_time,
"Customer 2",
"customer2@example.com",
nil,
nil
)
assert {:ok, :available} =
BookingSystem.check_availability(space.id, date, start_time, end_time)
end
test "does not count cancelled bookings", %{space: space} do
date = Date.add(Date.utc_today(), 1)
start_time = ~T[09:00:00]
end_time = ~T[10:00:00]
{:ok, booking1} =
BookingSystem.create_walk_in(
space.id,
DateTime.new!(date, start_time, "Etc/UTC"),
DateTime.new!(date, end_time, "Etc/UTC"),
"Customer 1",
"customer1@example.com",
nil,
nil
)
{:ok, _} =
BookingSystem.create_walk_in(
space.id,
DateTime.new!(date, start_time, "Etc/UTC"),
DateTime.new!(date, end_time, "Etc/UTC"),
"Customer 2",
"customer2@example.com",
nil,
nil
)
BookingSystem.cancel_booking(booking1, "User requested cancellation")
assert {:ok, :available} =
BookingSystem.check_availability(space.id, date, start_time, end_time)
end
test "does not count rejected bookings", %{space: space} do
date = Date.add(Date.utc_today(), 1)
start_time = ~T[09:00:00]
end_time = ~T[10:00:00]
{:ok, booking1} =
BookingSystem.create_booking(
space.id,
nil,
date,
start_time,
end_time,
"Customer 1",
"customer1@example.com",
nil,
nil
)
{:ok, _} =
BookingSystem.create_booking(
space.id,
nil,
date,
start_time,
end_time,
"Customer 2",
"customer2@example.com",
nil,
nil
)
BookingSystem.reject_booking(booking1, "Space not available")
assert {:ok, :available} =
BookingSystem.check_availability(space.id, date, start_time, end_time)
end
test "filters by date correctly", %{space: space} do
date1 = Date.utc_today()
date2 = Date.add(Date.utc_today(), 1)
start_time = ~T[09:00:00]
end_time = ~T[10:00:00]
for i <- 1..3 do
BookingSystem.create_walk_in(
space.id,
DateTime.new!(date1, start_time, "Etc/UTC"),
DateTime.new!(date1, end_time, "Etc/UTC"),
"Customer #{i}",
"customer#{i}@example.com",
nil,
nil
)
end
assert {:ok, :available} =
BookingSystem.check_availability(space.id, date2, start_time, end_time)
end
test "filters by space correctly", %{space: space} do
{:ok, other_space} =
BookingSystem.create_space(
"Other Space",
"other-space",
"Another test space",
2
)
date = Date.add(Date.utc_today(), 1)
start_time = ~T[09:00:00]
end_time = ~T[10:00:00]
for i <- 1..3 do
BookingSystem.create_walk_in(
other_space.id,
DateTime.new!(date, start_time, "Etc/UTC"),
DateTime.new!(date, end_time, "Etc/UTC"),
"Customer #{i}",
"customer#{i}@example.com",
nil,
nil
)
end
assert {:ok, :available} =
BookingSystem.check_availability(space.id, date, start_time, end_time)
end
end
end end

View file

@ -251,7 +251,11 @@ defmodule SpazioSolazzoWeb.BookingLive.SpaceBookingTest do
assert html =~ "High Demand - Join Waitlist" assert html =~ "High Demand - Join Waitlist"
end end
test "shows slots over capacity with high demand warning", %{conn: conn, space: space, today: today} do test "shows slots over capacity with high demand warning", %{
conn: conn,
space: space,
today: today
} do
for i <- 1..3 do for i <- 1..3 do
{:ok, booking} = {:ok, booking} =
request_booking( request_booking(