diff --git a/AGENTS.md b/AGENTS.md index a0e3142..14887dc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,6 +53,7 @@ Build a reliable, maintainable salon booking platform with Django (backend) and - Python is invoked as `python3`. - A virtualenv is in use. - DB: PostgreSQL in production, SQLite allowed for local dev. +- Backend tests must run with the venv active and `pytest-django` installed; run from `backend/` so `backend/pytest.ini` is picked up and `DJANGO_SETTINGS_MODULE` resolves. ## Collaboration Rules for Agents - Don’t delete or rewrite unrelated work. @@ -62,4 +63,4 @@ Build a reliable, maintainable salon booking platform with Django (backend) and # ExecPlans -When writing complex features or significant refactors, use an ExecPlan (as described in PLANS.md) from design to implementation. The active ExecPlan is `docs/execplans/arabic-localization.md`. +When writing complex features or significant refactors, use an ExecPlan (as described in PLANS.md) from design to implementation. The active ExecPlan is `docs/execplans/booking-integrity.md`. diff --git a/backend/apps/bookings/services.py b/backend/apps/bookings/services.py index e8f8ae9..614a22c 100644 --- a/backend/apps/bookings/services.py +++ b/backend/apps/bookings/services.py @@ -1,5 +1,6 @@ from datetime import timedelta +from django.utils import timezone from django.utils.translation import gettext_lazy as _ from rest_framework import serializers @@ -21,16 +22,19 @@ def validate_booking_request(service, staff, start_time, end_time): if expected_end != end_time: raise serializers.ValidationError({"end_time": _("End time must match service duration")}) + start_local = timezone.localtime(start_time) + end_local = timezone.localtime(end_time) + availability_qs = StaffAvailability.objects.filter( staff=staff, - day_of_week=start_time.weekday(), + day_of_week=start_local.weekday(), is_active=True, ) if availability_qs.exists(): within_window = availability_qs.filter( - start_time__lte=start_time.time(), - end_time__gte=end_time.time(), + start_time__lte=start_local.time(), + end_time__gte=end_local.time(), ).exists() if not within_window: raise serializers.ValidationError({"start_time": _("Booking is outside staff availability")}) diff --git a/backend/apps/bookings/tests/test_booking_integrity.py b/backend/apps/bookings/tests/test_booking_integrity.py index 2234171..d34618e 100644 --- a/backend/apps/bookings/tests/test_booking_integrity.py +++ b/backend/apps/bookings/tests/test_booking_integrity.py @@ -3,6 +3,7 @@ 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 @@ -46,15 +47,34 @@ def booking_payload(service, staff, start_time, end_time, notes=""): def next_day_at(hour, minute=0): - now = timezone.now() - target = now + timedelta(days=1) + 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_staff_assignment(client, base_entities): +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.force_login(customer) + client = APIClient() + client.force_authenticate(user=customer) start_time = next_day_at(9) end_time = start_time + timedelta(minutes=service.duration_minutes) @@ -71,9 +91,10 @@ def test_requires_staff_assignment(client, base_entities): @pytest.mark.django_db -def test_rejects_end_before_start(client, base_entities): +def test_rejects_end_before_start(base_entities): customer, staff, service = base_entities - client.force_login(customer) + client = APIClient() + client.force_authenticate(user=customer) start_time = next_day_at(9) end_time = start_time - timedelta(minutes=service.duration_minutes) @@ -90,9 +111,10 @@ def test_rejects_end_before_start(client, base_entities): @pytest.mark.django_db -def test_rejects_duration_mismatch(client, base_entities): +def test_rejects_duration_mismatch(base_entities): customer, staff, service = base_entities - client.force_login(customer) + client = APIClient() + client.force_authenticate(user=customer) start_time = next_day_at(9) end_time = start_time + timedelta(minutes=30) @@ -109,16 +131,18 @@ def test_rejects_duration_mismatch(client, base_entities): @pytest.mark.django_db -def test_rejects_outside_availability_when_defined(client, base_entities): +def test_rejects_outside_availability_when_defined(base_entities): customer, staff, service = base_entities - client.force_login(customer) + 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_time.weekday(), + 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(), ) @@ -135,9 +159,10 @@ def test_rejects_outside_availability_when_defined(client, base_entities): @pytest.mark.django_db -def test_allows_booking_without_availability_records(client, base_entities): +def test_allows_booking_without_availability_records(base_entities): customer, staff, service = base_entities - client.force_login(customer) + client = APIClient() + client.force_authenticate(user=customer) start_time = next_day_at(10) end_time = start_time + timedelta(minutes=service.duration_minutes) @@ -153,9 +178,10 @@ def test_allows_booking_without_availability_records(client, base_entities): @pytest.mark.django_db -def test_rejects_overlapping_pending_or_confirmed_bookings(client, base_entities): +def test_rejects_overlapping_pending_or_confirmed_bookings(base_entities): customer, staff, service = base_entities - client.force_login(customer) + client = APIClient() + client.force_authenticate(user=customer) start_time = next_day_at(9) end_time = start_time + timedelta(minutes=service.duration_minutes) @@ -188,9 +214,10 @@ def test_rejects_overlapping_pending_or_confirmed_bookings(client, base_entities @pytest.mark.django_db -def test_allows_overlap_with_cancelled_or_completed(client, base_entities): +def test_allows_overlap_with_cancelled_or_completed(base_entities): customer, staff, service = base_entities - client.force_login(customer) + client = APIClient() + client.force_authenticate(user=customer) start_time = next_day_at(9) end_time = start_time + timedelta(minutes=service.duration_minutes)