Add STO booking and maintenance automation
This commit is contained in:
266
tests/test_sto_booking.py
Normal file
266
tests/test_sto_booking.py
Normal file
@@ -0,0 +1,266 @@
|
||||
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"]
|
||||
|
||||
|
||||
@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"
|
||||
Reference in New Issue
Block a user