Fix: Prevent cancellation of past appointments
Add check to both user and admin cancel endpoints to reject cancellation of appointments whose slot_start is in the past. This matches the spec requirement that cancellations can only happen 'before the appointment'. Added tests for both user and admin cancel endpoints. Also includes frontend styling updates.
This commit is contained in:
parent
89eec1e9c4
commit
63cf46c230
5 changed files with 679 additions and 291 deletions
|
|
@ -266,6 +266,13 @@ async def cancel_my_appointment(
|
|||
detail=f"Cannot cancel appointment with status '{appointment.status.value}'"
|
||||
)
|
||||
|
||||
# Check if appointment is in the past
|
||||
if appointment.slot_start <= datetime.now(timezone.utc):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cannot cancel a past appointment"
|
||||
)
|
||||
|
||||
# Cancel the appointment
|
||||
appointment.status = AppointmentStatus.CANCELLED_BY_USER
|
||||
appointment.cancelled_at = datetime.now(timezone.utc)
|
||||
|
|
@ -346,6 +353,13 @@ async def admin_cancel_appointment(
|
|||
detail=f"Cannot cancel appointment with status '{appointment.status.value}'"
|
||||
)
|
||||
|
||||
# Check if appointment is in the past
|
||||
if appointment.slot_start <= datetime.now(timezone.utc):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cannot cancel a past appointment"
|
||||
)
|
||||
|
||||
# Cancel the appointment
|
||||
appointment.status = AppointmentStatus.CANCELLED_BY_ADMIN
|
||||
appointment.cancelled_at = datetime.now(timezone.utc)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ Booking API Tests
|
|||
|
||||
Tests for the user booking endpoints.
|
||||
"""
|
||||
from datetime import date, timedelta
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
import pytest
|
||||
|
||||
from models import Appointment, AppointmentStatus
|
||||
|
||||
|
||||
def tomorrow() -> date:
|
||||
return date.today() + timedelta(days=1)
|
||||
|
|
@ -656,6 +658,30 @@ class TestCancelAppointment:
|
|||
)
|
||||
assert len(slots_response.json()["slots"]) == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cannot_cancel_past_appointment(self, client_factory, regular_user):
|
||||
"""User cannot cancel a past appointment."""
|
||||
# Create a past appointment directly in DB
|
||||
async with client_factory.get_db_session() as db:
|
||||
past_time = datetime.now(timezone.utc) - timedelta(hours=1)
|
||||
appointment = Appointment(
|
||||
user_id=regular_user["user"]["id"],
|
||||
slot_start=past_time,
|
||||
slot_end=past_time + timedelta(minutes=15),
|
||||
status=AppointmentStatus.BOOKED,
|
||||
)
|
||||
db.add(appointment)
|
||||
await db.commit()
|
||||
await db.refresh(appointment)
|
||||
apt_id = appointment.id
|
||||
|
||||
# Try to cancel
|
||||
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
||||
response = await client.post(f"/api/appointments/{apt_id}/cancel")
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "past" in response.json()["detail"].lower()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Admin Appointments Tests
|
||||
|
|
@ -806,3 +832,27 @@ class TestAdminCancelAppointment:
|
|||
assert response.status_code == 400
|
||||
assert "cancelled_by_user" in response.json()["detail"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_cannot_cancel_past_appointment(self, client_factory, regular_user, admin_user):
|
||||
"""Admin cannot cancel a past appointment."""
|
||||
# Create a past appointment directly in DB
|
||||
async with client_factory.get_db_session() as db:
|
||||
past_time = datetime.now(timezone.utc) - timedelta(hours=1)
|
||||
appointment = Appointment(
|
||||
user_id=regular_user["user"]["id"],
|
||||
slot_start=past_time,
|
||||
slot_end=past_time + timedelta(minutes=15),
|
||||
status=AppointmentStatus.BOOKED,
|
||||
)
|
||||
db.add(appointment)
|
||||
await db.commit()
|
||||
await db.refresh(appointment)
|
||||
apt_id = appointment.id
|
||||
|
||||
# Admin tries to cancel
|
||||
async with client_factory.create(cookies=admin_user["cookies"]) as admin_client:
|
||||
response = await admin_client.post(f"/api/admin/appointments/{apt_id}/cancel")
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "past" in response.json()["detail"].lower()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue