Files
Salon/backend/apps/bookings/tests/test_booking_integrity.py
T

249 lines
7.2 KiB
Python

from datetime import timedelta
import pytest
from django.urls import reverse
from django.utils import timezone
from rest_framework.test import APIClient
from apps.accounts.models import User, UserRole
from apps.bookings.models import Booking, BookingStatus
from apps.salons.models import Salon, Service, StaffAvailability, StaffProfile
@pytest.fixture
def base_entities():
owner = User.objects.create_user(email="owner@example.com", password="pass", role=UserRole.MANAGER)
customer = User.objects.create_user(email="customer@example.com", password="pass")
staff_user = User.objects.create_user(email="staff@example.com", password="pass", role=UserRole.STAFF)
salon = Salon.objects.create(
owner=owner,
name="Main Salon",
description="",
address="123 King Rd",
city="Riyadh",
phone_number="0512345678",
)
service = Service.objects.create(
salon=salon,
name="Haircut",
description="",
duration_minutes=60,
price_amount=120,
currency="SAR",
)
staff = StaffProfile.objects.create(user=staff_user, salon=salon)
return customer, staff, service
def booking_payload(service, staff, start_time, end_time, notes=""):
return {
"service": service.id,
"staff": staff.id if staff else None,
"start_time": start_time.isoformat(),
"end_time": end_time.isoformat(),
"notes": notes,
}
def next_day_at(hour, minute=0):
now_local = timezone.localtime(timezone.now())
target = now_local + timedelta(days=1)
return target.replace(hour=hour, minute=minute, second=0, microsecond=0)
@pytest.mark.django_db
def test_requires_authentication(base_entities):
_, staff, service = base_entities
client = APIClient()
start_time = next_day_at(9)
end_time = start_time + timedelta(minutes=service.duration_minutes)
payload = booking_payload(service, staff, start_time, end_time)
response = client.post(
reverse("booking-list"),
payload,
content_type="application/json",
)
assert response.status_code == 401
@pytest.mark.django_db
def test_requires_staff_assignment(base_entities):
customer, staff, service = base_entities
client = APIClient()
client.force_authenticate(user=customer)
start_time = next_day_at(9)
end_time = start_time + timedelta(minutes=service.duration_minutes)
payload = booking_payload(service, None, start_time, end_time)
response = client.post(
reverse("booking-list"),
payload,
content_type="application/json",
)
assert response.status_code == 400
assert "staff" in response.json()
@pytest.mark.django_db
def test_rejects_end_before_start(base_entities):
customer, staff, service = base_entities
client = APIClient()
client.force_authenticate(user=customer)
start_time = next_day_at(9)
end_time = start_time - timedelta(minutes=service.duration_minutes)
payload = booking_payload(service, staff, start_time, end_time)
response = client.post(
reverse("booking-list"),
payload,
content_type="application/json",
)
assert response.status_code == 400
assert "end_time" in response.json()
@pytest.mark.django_db
def test_rejects_duration_mismatch(base_entities):
customer, staff, service = base_entities
client = APIClient()
client.force_authenticate(user=customer)
start_time = next_day_at(9)
end_time = start_time + timedelta(minutes=30)
payload = booking_payload(service, staff, start_time, end_time)
response = client.post(
reverse("booking-list"),
payload,
content_type="application/json",
)
assert response.status_code == 400
assert "end_time" in response.json()
@pytest.mark.django_db
def test_rejects_outside_availability_when_defined(base_entities):
customer, staff, service = base_entities
client = APIClient()
client.force_authenticate(user=customer)
start_time = next_day_at(8)
end_time = start_time + timedelta(minutes=service.duration_minutes)
start_local = timezone.localtime(start_time)
StaffAvailability.objects.create(
staff=staff,
day_of_week=start_local.weekday(),
start_time=timezone.datetime(2000, 1, 1, 9, 0).time(),
end_time=timezone.datetime(2000, 1, 1, 17, 0).time(),
)
payload = booking_payload(service, staff, start_time, end_time)
response = client.post(
reverse("booking-list"),
payload,
content_type="application/json",
)
assert response.status_code == 400
assert "start_time" in response.json()
@pytest.mark.django_db
def test_allows_booking_without_availability_records(base_entities):
customer, staff, service = base_entities
client = APIClient()
client.force_authenticate(user=customer)
start_time = next_day_at(10)
end_time = start_time + timedelta(minutes=service.duration_minutes)
payload = booking_payload(service, staff, start_time, end_time)
response = client.post(
reverse("booking-list"),
payload,
content_type="application/json",
)
assert response.status_code == 201
@pytest.mark.django_db
def test_rejects_overlapping_pending_or_confirmed_bookings(base_entities):
customer, staff, service = base_entities
client = APIClient()
client.force_authenticate(user=customer)
start_time = next_day_at(9)
end_time = start_time + timedelta(minutes=service.duration_minutes)
Booking.objects.create(
salon=service.salon,
customer=customer,
service=service,
staff=staff,
start_time=start_time,
end_time=end_time,
status=BookingStatus.CONFIRMED,
price_amount=service.price_amount,
currency=service.currency,
notes="",
)
overlap_start = start_time + timedelta(minutes=30)
overlap_end = overlap_start + timedelta(minutes=service.duration_minutes)
payload = booking_payload(service, staff, overlap_start, overlap_end)
response = client.post(
reverse("booking-list"),
payload,
content_type="application/json",
)
assert response.status_code == 400
assert "start_time" in response.json()
@pytest.mark.django_db
def test_allows_overlap_with_cancelled_or_completed(base_entities):
customer, staff, service = base_entities
client = APIClient()
client.force_authenticate(user=customer)
start_time = next_day_at(9)
end_time = start_time + timedelta(minutes=service.duration_minutes)
Booking.objects.create(
salon=service.salon,
customer=customer,
service=service,
staff=staff,
start_time=start_time,
end_time=end_time,
status=BookingStatus.CANCELLED,
price_amount=service.price_amount,
currency=service.currency,
notes="",
)
overlap_start = start_time + timedelta(minutes=30)
overlap_end = overlap_start + timedelta(minutes=service.duration_minutes)
payload = booking_payload(service, staff, overlap_start, overlap_end)
response = client.post(
reverse("booking-list"),
payload,
content_type="application/json",
)
assert response.status_code == 201