411180e312
Added a staff availability model and migration, a booking validation service, and serializer enforcement.
222 lines
6.4 KiB
Python
222 lines
6.4 KiB
Python
from datetime import timedelta
|
|
|
|
import pytest
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
|
|
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 = timezone.now()
|
|
target = now + timedelta(days=1)
|
|
return target.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_requires_staff_assignment(client, base_entities):
|
|
customer, staff, service = base_entities
|
|
client.force_login(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(client, base_entities):
|
|
customer, staff, service = base_entities
|
|
client.force_login(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(client, base_entities):
|
|
customer, staff, service = base_entities
|
|
client.force_login(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(client, base_entities):
|
|
customer, staff, service = base_entities
|
|
client.force_login(customer)
|
|
|
|
start_time = next_day_at(8)
|
|
end_time = start_time + timedelta(minutes=service.duration_minutes)
|
|
|
|
StaffAvailability.objects.create(
|
|
staff=staff,
|
|
day_of_week=start_time.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(client, base_entities):
|
|
customer, staff, service = base_entities
|
|
client.force_login(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(client, base_entities):
|
|
customer, staff, service = base_entities
|
|
client.force_login(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(client, base_entities):
|
|
customer, staff, service = base_entities
|
|
client.force_login(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
|