feat: add party-size queueing and call modes

This commit is contained in:
wangxuming
2026-07-16 11:40:34 +08:00
parent 7f751bebae
commit 66951b4dc3
53 changed files with 3348 additions and 533 deletions

View File

@@ -153,13 +153,36 @@ def main() -> None:
"smoke requires a freshly rebuilt, dedicated database with no waiting "
"tickets; rebuild and seed the smoke database before running it"
)
project_config = snapshot["project"]
assert project_config["call_mode"] == "BOTH", "smoke project must expose both call modes"
party_size = min(
int(project_config["max_party_size"]),
max(int(project_config["min_party_size"]), 3),
)
phone = "139" + str(int(time.time() * 1000))[-8:]
last_name = "烟测"
honorific = "游客"
missing_party = client.request(
"POST",
f"/api/staff/projects/{project_id}/tickets",
{"phone": phone},
key=str(uuid.uuid4()),
expected=422,
)
assert error_code(missing_party) == "INVALID_PARTY_SIZE"
oversized_party = client.request(
"POST",
f"/api/staff/projects/{project_id}/tickets",
{"phone": phone, "party_size": int(project_config["max_party_size"]) + 1},
key=str(uuid.uuid4()),
expected=422,
)
assert error_code(oversized_party) == "INVALID_PARTY_SIZE"
ticket_body = {
"phone": phone,
"last_name": last_name,
"honorific": honorific,
"party_size": party_size,
"allow_duplicate": False,
}
first_key = str(uuid.uuid4())
@@ -173,6 +196,7 @@ def main() -> None:
first_ticket = first["ticket"]
assert first_ticket["ticket_number"]
assert first_ticket["public_token"]
assert first_ticket["party_size"] == party_size
assert_personal_continuity(
first_ticket,
phone=phone,
@@ -217,6 +241,9 @@ def main() -> None:
"GET", f"/api/public/status/{first_ticket['public_token']}"
)
assert public["ticket_number"] == first_ticket["ticket_number"]
assert public["party_size"] == party_size
assert public["people_ahead"] == 0
assert public["estimated_wait"]["estimate_minutes"] == 0
assert_public_phone_projection(public, phone, "public status")
phone_lookup = client.request(
@@ -308,7 +335,19 @@ def main() -> None:
assert waiting_ids == {ticket["id"] for ticket in created_tickets}, (
"dedicated smoke queue contains unexpected tickets"
)
expected_waiting_people = sum(ticket["party_size"] for ticket in created_tickets)
assert snapshot["metrics"]["waiting_ticket_count"] == len(created_tickets)
assert snapshot["metrics"]["waiting_people_count"] == expected_waiting_people
display_before_call = client.request(
"GET", f"/api/display/{args.display_token}/snapshot"
)
assert display_before_call["waiting_ticket_count"] == len(created_tickets)
assert display_before_call["waiting_people_count"] == expected_waiting_people
assert display_before_call["experienced_people"] == max(
int(project.get("experienced_people_start") or 0), expected_waiting_people
)
for ticket in waiting_before_call:
assert ticket["party_size"] == party_size
assert_personal_continuity(
ticket,
phone=phone,
@@ -319,7 +358,7 @@ def main() -> None:
stale_call = client.request(
"POST",
f"/api/staff/projects/{project_id}/call-next",
{"expected_revision": max(0, int(snapshot["revision"]) - 1), "count": batch_size},
{"expected_revision": max(0, int(snapshot["revision"]) - 1), "mode": "PEOPLE", "count": batch_size * party_size + party_size - 1},
key=str(uuid.uuid4()),
expected=409,
)
@@ -329,7 +368,8 @@ def main() -> None:
ticket["id"] for ticket in waiting_before_call[:batch_size]
]
call_key = str(uuid.uuid4())
call_body = {"expected_revision": int(snapshot["revision"]), "count": batch_size}
people_target = batch_size * party_size + party_size - 1
call_body = {"expected_revision": int(snapshot["revision"]), "mode": "PEOPLE", "count": people_target}
called = client.request(
"POST",
f"/api/staff/projects/{project_id}/call-next",
@@ -340,6 +380,12 @@ def main() -> None:
assert [ticket["id"] for ticket in called_tickets] == expected_called_ids
assert len(called_tickets) == min(batch_size, len(waiting_before_call))
assert called_tickets, "call-next must call at least one waiting ticket"
assert called["batch"]["call_mode"] == "PEOPLE"
assert called["batch"]["requested_count"] == people_target
assert called["batch"]["ticket_count"] == batch_size
assert called["batch"]["people_count"] == batch_size * party_size
assert called["batch"]["people_count"] <= people_target
assert called["batch"]["people_count"] + waiting_before_call[batch_size]["party_size"] > people_target
for ticket in called_tickets:
assert_personal_continuity(
ticket,
@@ -375,6 +421,8 @@ def main() -> None:
assert [ticket["id"] for ticket in current_batch["tickets"]] == (
expected_called_ids
)
assert current_batch["call_mode"] == "PEOPLE"
assert current_batch["people_count"] == batch_size * party_size
for ticket in current_batch["tickets"]:
assert_personal_continuity(
ticket,
@@ -387,16 +435,21 @@ def main() -> None:
second_called = client.request(
"POST",
f"/api/staff/projects/{project_id}/call-next",
{"expected_revision": int(called["revision"]), "count": 1},
{"expected_revision": int(called["revision"]), "mode": "TICKET", "count": 1},
key=str(uuid.uuid4()),
)
assert second_called["batch"]["id"] != called["batch"]["id"]
assert len(second_called["batch"]["tickets"]) == 1
assert second_called["batch"]["call_mode"] == "TICKET"
assert second_called["batch"]["ticket_count"] == 1
assert second_called["batch"]["people_count"] == party_size
final_staff_snapshot = client.request(
"GET", f"/api/staff/projects/{project_id}/queue"
)
assert final_staff_snapshot["current_batch"]["id"] == second_called["batch"]["id"]
assert final_staff_snapshot["metrics"]["waiting_count"] == 0
assert final_staff_snapshot["metrics"]["waiting_ticket_count"] == 0
assert final_staff_snapshot["metrics"]["waiting_people_count"] == 0
completed_public = client.request(
"GET", f"/api/public/status/{first_ticket['public_token']}"
)
@@ -405,6 +458,9 @@ def main() -> None:
display = client.request(
"GET", f"/api/display/{args.display_token}/snapshot"
)
assert display["experienced_people"] == max(
int(project_config.get("experienced_people_start") or 0), expected_waiting_people
)
serialized_display = json.dumps(display, ensure_ascii=False)
display_keys = mapping_keys(display)
assert "honorific" not in display_keys
@@ -426,6 +482,11 @@ def main() -> None:
overview = client.request("GET", "/api/admin/overview")
project = next(item for item in overview["projects"] if item["id"] == project_id)
assert project["waiting_count"] == 0
assert project["waiting_ticket_count"] == 0
assert project["waiting_people_count"] == 0
assert project["experienced_people"] == max(
int(project_config.get("experienced_people_start") or 0), expected_waiting_people
)
current_admin_ticket = ticket_with_number(
overview.get("active_tickets"),
second_called["batch"]["tickets"][0]["ticket_number"],
@@ -440,6 +501,36 @@ def main() -> None:
source="admin active_tickets after automatic rotation",
)
public_projects = client.request("GET", "/api/public/projects")
assert any(item.get("id") == project_id for item in public_projects.get("projects", []))
public_phone = "137" + str(int(time.time() * 1000))[-8:]
public_party_size = min(int(project_config["max_party_size"]), party_size + 1)
public_body = {"phone": public_phone, "last_name": "游客", "honorific": "游客", "party_size": public_party_size}
public_key = str(uuid.uuid4())
public_create = client.request(
"POST",
f"/api/public/projects/{project_id}/tickets",
public_body,
key=public_key,
expected=201,
)
public_ticket = public_create["ticket"]
assert public_ticket["ticket_number"]
assert public_ticket["party_size"] == public_party_size
assert public_ticket["phone_last4"] == public_phone[-4:]
public_keys = mapping_keys(public_ticket)
assert "phone" not in public_keys
assert "last_name" not in public_keys
assert "honorific" not in public_keys
public_replay = client.request(
"POST",
f"/api/public/projects/{project_id}/tickets",
public_body,
key=public_key,
expected=201,
)
assert public_replay["ticket"]["id"] == public_ticket["id"]
print(
json.dumps(
{