336 lines
12 KiB
Python
336 lines
12 KiB
Python
from datetime import UTC, date, datetime, time, timedelta
|
|
|
|
import pytest
|
|
|
|
|
|
def next_weekday(target: int) -> date:
|
|
today = date.today()
|
|
delta = (target - today.weekday()) % 7
|
|
if delta == 0:
|
|
delta = 7
|
|
return today + timedelta(days=delta)
|
|
|
|
|
|
async def create_verified_center(client, owner_headers, admin_headers, internal_headers, *, city: str = "Seoul") -> dict:
|
|
center = (
|
|
await client.post(
|
|
"/api/service-centers",
|
|
headers=owner_headers,
|
|
json={
|
|
"display_name": f"Booking Service {city}",
|
|
"country": "KR",
|
|
"city": city,
|
|
"specializations": ["oil_change", "diagnostics"],
|
|
},
|
|
)
|
|
).json()
|
|
await client.post(
|
|
"/api/users",
|
|
headers=internal_headers,
|
|
json={"telegram_id": 9001, "platform_role": "admin"},
|
|
)
|
|
response = await client.post(f"/api/admin/service-centers/{center['id']}/verify", headers=admin_headers)
|
|
assert response.status_code == 200
|
|
return response.json()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sto_catalog_shows_only_approved_and_filters_city(
|
|
client, auth_headers, admin_auth_headers, internal_headers
|
|
) -> None:
|
|
approved = await create_verified_center(client, auth_headers, admin_auth_headers, internal_headers, city="Seoul")
|
|
await client.post(
|
|
"/api/service-centers",
|
|
headers=auth_headers,
|
|
json={"display_name": "Pending Booking Service", "country": "KR", "city": "Busan"},
|
|
)
|
|
|
|
response = await client.get("/api/sto/catalog?city=Seoul", headers=auth_headers)
|
|
|
|
assert response.status_code == 200
|
|
ids = [item["id"] for item in response.json()]
|
|
assert ids == [approved["id"]]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_available_slots_skip_weekend_lunch_and_holidays(
|
|
client, auth_headers, admin_auth_headers, internal_headers
|
|
) -> None:
|
|
center = await create_verified_center(client, auth_headers, admin_auth_headers, internal_headers)
|
|
monday = next_weekday(0)
|
|
saturday = next_weekday(5)
|
|
await client.post(
|
|
"/api/sto/settings/booking",
|
|
headers=auth_headers,
|
|
json={
|
|
"service_center_id": center["id"],
|
|
"working_days": [0],
|
|
"open_time": "09:00:00",
|
|
"close_time": "13:00:00",
|
|
"lunch_break_start": "11:00:00",
|
|
"lunch_break_end": "12:00:00",
|
|
"slot_duration_minutes": 60,
|
|
"max_parallel_bookings": 1,
|
|
"accepts_online_booking": True,
|
|
},
|
|
)
|
|
await client.post(
|
|
"/api/sto/settings/holidays",
|
|
headers=auth_headers,
|
|
json={"service_center_id": center["id"], "holiday_date": saturday.isoformat(), "reason": "Closed"},
|
|
)
|
|
|
|
monday_slots = (
|
|
await client.get(
|
|
f"/api/sto/{center['id']}/available-slots?date_from={monday}&date_to={monday}&duration_minutes=60",
|
|
headers=auth_headers,
|
|
)
|
|
).json()
|
|
saturday_slots = (
|
|
await client.get(
|
|
f"/api/sto/{center['id']}/available-slots?date_from={saturday}&date_to={saturday}&duration_minutes=60",
|
|
headers=auth_headers,
|
|
)
|
|
).json()
|
|
|
|
assert {datetime.fromisoformat(slot["start_at"]).time() for slot in monday_slots} == {time(9, 0), time(10, 0), time(12, 0)}
|
|
assert saturday_slots == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_customer_booking_lifecycle_capacity_calendar_work_order_and_notifications(
|
|
client, auth_headers, other_auth_headers, admin_auth_headers, internal_headers
|
|
) -> None:
|
|
center = await create_verified_center(client, auth_headers, admin_auth_headers, internal_headers)
|
|
workday = next_weekday(0)
|
|
start_at = datetime.combine(workday, time(10, 0), tzinfo=UTC)
|
|
proposed_at = datetime.combine(workday, time(14, 0), tzinfo=UTC)
|
|
await client.post(
|
|
"/api/sto/settings/booking",
|
|
headers=auth_headers,
|
|
json={
|
|
"service_center_id": center["id"],
|
|
"working_days": [0, 1, 2, 3, 4],
|
|
"open_time": "09:00:00",
|
|
"close_time": "18:00:00",
|
|
"slot_duration_minutes": 60,
|
|
"max_parallel_bookings": 1,
|
|
"accepts_online_booking": True,
|
|
},
|
|
)
|
|
vehicle = (
|
|
await client.post(
|
|
"/api/my/vehicles",
|
|
headers=other_auth_headers,
|
|
json={"name": "Client car", "current_odometer": 45000, "oil_change_interval_km": 10000},
|
|
)
|
|
).json()
|
|
|
|
past_response = await client.post(
|
|
"/api/appointments",
|
|
headers=other_auth_headers,
|
|
json={
|
|
"service_center_id": center["id"],
|
|
"vehicle_id": vehicle["id"],
|
|
"service_type": "oil_change",
|
|
"service_name": "Oil change",
|
|
"requested_start_at": (datetime.now(UTC) - timedelta(days=1)).isoformat(),
|
|
"estimated_duration_minutes": 60,
|
|
},
|
|
)
|
|
assert past_response.status_code == 409
|
|
|
|
created = await client.post(
|
|
"/api/appointments",
|
|
headers=other_auth_headers,
|
|
json={
|
|
"service_center_id": center["id"],
|
|
"vehicle_id": vehicle["id"],
|
|
"service_type": "oil_change",
|
|
"service_name": "Oil change",
|
|
"requested_start_at": start_at.isoformat(),
|
|
"estimated_duration_minutes": 60,
|
|
"customer_comment": "Need synthetic oil",
|
|
},
|
|
)
|
|
assert created.status_code == 201
|
|
appointment = created.json()
|
|
assert appointment["status"] == "requested"
|
|
|
|
duplicate = await client.post(
|
|
"/api/appointments",
|
|
headers=other_auth_headers,
|
|
json={
|
|
"service_center_id": center["id"],
|
|
"vehicle_id": vehicle["id"],
|
|
"service_type": "diagnostics",
|
|
"service_name": "Diagnostics",
|
|
"requested_start_at": start_at.isoformat(),
|
|
"estimated_duration_minutes": 60,
|
|
},
|
|
)
|
|
assert duplicate.status_code == 409
|
|
|
|
proposed = await client.post(
|
|
f"/api/sto/appointments/{appointment['id']}/propose-time",
|
|
headers=auth_headers,
|
|
json={"proposed_start_at": proposed_at.isoformat(), "comment": "Better window"},
|
|
)
|
|
assert proposed.status_code == 200
|
|
assert proposed.json()["status"] == "proposed_new_time"
|
|
|
|
accepted = await client.post(
|
|
f"/api/appointments/{appointment['id']}/accept-proposed-time",
|
|
headers=other_auth_headers,
|
|
)
|
|
assert accepted.status_code == 200
|
|
assert accepted.json()["status"] == "confirmed"
|
|
|
|
calendar = await client.get(
|
|
f"/api/sto/calendar?service_center_id={center['id']}&date_from={workday.isoformat()}T00:00:00Z&date_to={workday.isoformat()}T23:59:59Z",
|
|
headers=auth_headers,
|
|
)
|
|
assert calendar.status_code == 200
|
|
assert calendar.json()[0]["id"] == appointment["id"]
|
|
|
|
work_order = await client.post(
|
|
f"/api/sto/appointments/{appointment['id']}/create-work-order",
|
|
headers=auth_headers,
|
|
json={"odometer": 45100},
|
|
)
|
|
assert work_order.status_code == 201
|
|
assert work_order.json()["vehicle_id"] == vehicle["id"]
|
|
|
|
my_appointments = await client.get("/api/appointments/my", headers=other_auth_headers)
|
|
assert my_appointments.json()[0]["linked_work_order_id"] == work_order.json()["id"]
|
|
owner_delete_converted = await client.delete(f"/api/appointments/{appointment['id']}", headers=other_auth_headers)
|
|
sto_delete_converted = await client.delete(f"/api/sto/appointments/{appointment['id']}", headers=auth_headers)
|
|
assert owner_delete_converted.status_code == 409
|
|
assert sto_delete_converted.status_code == 409
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_appointments_can_be_deleted_by_owner_or_sto_before_work_order(
|
|
client, auth_headers, other_auth_headers, admin_auth_headers, internal_headers
|
|
) -> None:
|
|
center = await create_verified_center(client, auth_headers, admin_auth_headers, internal_headers, city="Delete Booking")
|
|
workday = next_weekday(2)
|
|
await client.post(
|
|
"/api/sto/settings/booking",
|
|
headers=auth_headers,
|
|
json={
|
|
"service_center_id": center["id"],
|
|
"working_days": [0, 1, 2, 3, 4],
|
|
"open_time": "09:00:00",
|
|
"close_time": "18:00:00",
|
|
"slot_duration_minutes": 60,
|
|
"max_parallel_bookings": 1,
|
|
"accepts_online_booking": True,
|
|
},
|
|
)
|
|
vehicle = (
|
|
await client.post(
|
|
"/api/my/vehicles",
|
|
headers=other_auth_headers,
|
|
json={"name": "Delete booking car", "current_odometer": 12000},
|
|
)
|
|
).json()
|
|
|
|
owner_deleted = (
|
|
await client.post(
|
|
"/api/appointments",
|
|
headers=other_auth_headers,
|
|
json={
|
|
"service_center_id": center["id"],
|
|
"vehicle_id": vehicle["id"],
|
|
"service_type": "diagnostics",
|
|
"service_name": "Diagnostics",
|
|
"requested_start_at": datetime.combine(workday, time(10, 0), tzinfo=UTC).isoformat(),
|
|
"estimated_duration_minutes": 60,
|
|
},
|
|
)
|
|
).json()
|
|
deleted_by_owner = await client.delete(f"/api/appointments/{owner_deleted['id']}", headers=other_auth_headers)
|
|
assert deleted_by_owner.status_code == 204
|
|
|
|
sto_deleted = (
|
|
await client.post(
|
|
"/api/appointments",
|
|
headers=other_auth_headers,
|
|
json={
|
|
"service_center_id": center["id"],
|
|
"vehicle_id": vehicle["id"],
|
|
"service_type": "diagnostics",
|
|
"service_name": "Diagnostics",
|
|
"requested_start_at": datetime.combine(workday, time(11, 0), tzinfo=UTC).isoformat(),
|
|
"estimated_duration_minutes": 60,
|
|
},
|
|
)
|
|
).json()
|
|
deleted_by_sto = await client.delete(f"/api/sto/appointments/{sto_deleted['id']}", headers=auth_headers)
|
|
assert deleted_by_sto.status_code == 204
|
|
|
|
my_appointments = await client.get("/api/appointments/my", headers=other_auth_headers)
|
|
assert {item["id"] for item in my_appointments.json()}.isdisjoint({owner_deleted["id"], sto_deleted["id"]})
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_recommendations_can_be_generated_dismissed_and_linked_to_booking(
|
|
client, auth_headers, other_auth_headers, admin_auth_headers, internal_headers
|
|
) -> None:
|
|
center = await create_verified_center(client, auth_headers, admin_auth_headers, internal_headers)
|
|
workday = next_weekday(1)
|
|
start_at = datetime.combine(workday, time(10, 0), tzinfo=UTC)
|
|
vehicle = (
|
|
await client.post(
|
|
"/api/my/vehicles",
|
|
headers=other_auth_headers,
|
|
json={"name": "Recommendation car", "current_odometer": 90000, "oil_change_interval_km": 10000},
|
|
)
|
|
).json()
|
|
|
|
recommendations = await client.get(
|
|
f"/api/vehicles/{vehicle['id']}/maintenance-recommendations",
|
|
headers=other_auth_headers,
|
|
)
|
|
assert recommendations.status_code == 200
|
|
oil_recommendation = recommendations.json()[0]
|
|
assert oil_recommendation["recommendation_type"] == "oil_change"
|
|
|
|
appointment = (
|
|
await client.post(
|
|
"/api/appointments",
|
|
headers=other_auth_headers,
|
|
json={
|
|
"service_center_id": center["id"],
|
|
"vehicle_id": vehicle["id"],
|
|
"service_type": "oil_change",
|
|
"service_name": "Oil change",
|
|
"requested_start_at": start_at.isoformat(),
|
|
"estimated_duration_minutes": 60,
|
|
"source_recommendation_id": oil_recommendation["id"],
|
|
},
|
|
)
|
|
).json()
|
|
booked = await client.post(
|
|
f"/api/maintenance-recommendations/{oil_recommendation['id']}/book",
|
|
headers=other_auth_headers,
|
|
json={"appointment_id": appointment["id"]},
|
|
)
|
|
assert booked.status_code == 200
|
|
assert booked.json()["status"] == "booked"
|
|
|
|
custom = (
|
|
await client.post(
|
|
f"/api/vehicles/{vehicle['id']}/maintenance-recommendations",
|
|
headers=other_auth_headers,
|
|
json={"recommendation_type": "brakes", "title": "Check brakes", "priority": "low"},
|
|
)
|
|
).json()
|
|
dismissed = await client.post(
|
|
f"/api/maintenance-recommendations/{custom['id']}/dismiss",
|
|
headers=other_auth_headers,
|
|
)
|
|
assert dismissed.status_code == 200
|
|
assert dismissed.json()["status"] == "dismissed"
|