"""补遗追加(FR-M2-10,addendum-plan):追加 → 解析扣 1 次 → 新条目标「补遗 1」、清单回到草稿、 截止时间进待确认、日期条目标「已被补遗 1 更新」;采用 / 不采用;采用截止时间时日期条目一起确认;检查预览提示。""" from __future__ import annotations from typing import Any from tests.conftest import FIXTURES, User, balance, project_with_tender, run_parse, upload async def add_addendum(u: User, project_id: str) -> tuple[str, dict[str, Any]]: done = await upload(u, "关于某市智慧政务系统集成项目的更正公告.pdf", (FIXTURES / "addendum-mini.pdf").read_bytes()) assert done.status_code == 200, done.text r = await u.post(f"/projects/{project_id}/documents", json={"upload_id": done.json()["id"], "role": "addendum"}) assert r.status_code == 201, r.text doc = r.json() assert doc["label"] == "补遗 1" job = await run_parse(u, doc["id"]) assert job["status"] == "succeeded", job return doc["id"], job def deadline_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]: return [i for i in items if ((i.get("spec") or {}).get("schedule") or {}).get("date_kind") == "bid_deadline"] async def parsed_project(u: User) -> str: project_id, tender_id = await project_with_tender(u, "tender-full.pdf") assert (await run_parse(u, tender_id))["status"] == "succeeded" return project_id async def test_addendum_merge_and_accept(user: User) -> None: project_id = await parsed_project(user) before = (await user.get(f"/projects/{project_id}")).json() assert before["deadline_at"].startswith("2026-12-01T01:30") await user.put(f"/projects/{project_id}/fields/deadline_at", json={"value": "2026-12-02T09:30:00+08:00"}) assert (await user.post(f"/projects/{project_id}/checklist/confirm")).status_code == 200 quota = await balance(user) _, job = await add_addendum(user, project_id) result = job["result"] assert result["dates_superseded"] == 1 and result["fields_pending"] >= 1 and result["items_added"] >= 2 assert await balance(user) == quota - 1 # 补遗单独扣 1 次解析 overview = (await user.get(f"/projects/{project_id}/checklist")).json() assert overview["status"] == "draft" and overview["counts"]["superseded_pending"] == 1 # 并入后回到草稿(AD1) items = (await user.get(f"/projects/{project_id}/checklist/items")).json()["items"] new_items = [i for i in items if i["origin_label"] == "补遗 1"] assert any("存储系统" in i["title"] for i in new_items) # 新条目直接进正式清单 assert all(i["origin_document"]["label"] == "补遗 1" for i in new_items) old, new = sorted(deadline_items(items), key=lambda i: i["origin_label"] != "补遗 1")[::-1] assert old["superseded_by"] == {"item_id": new["id"], "document_label": "补遗 1", "confirmed": False} assert new["superseded_by"] is None # 没确认之前项目用旧值(AD4),概览和检查预览都能看到待确认 detail = (await user.get(f"/projects/{project_id}")).json() assert detail["deadline_at"].startswith("2026-12-02T01:30") (f,) = [x for x in detail["fields"] if x["key"] == "deadline_at"] assert f["source"] == "human" and f["pending"]["value"].startswith("2026-12-08T09:30") assert f["pending"]["document_label"] == "补遗 1" assert any(d["superseded_pending"] for d in detail["key_dates"]) preview = (await user.get(f"/projects/{project_id}/check-preview")).json() assert preview["pending_updates"] >= 2 # 采用截止时间:清掉人工值、以补遗为准;清单里「投标截止」条目的更新一起确认(AD3) r = await user.post(f"/projects/{project_id}/fields/deadline_at/pending", json={"accept": True}) assert r.status_code == 200, r.text assert r.json()["source"] == "ai" and r.json()["pending"] is None and r.json()["value"].startswith("2026-12-08") detail = (await user.get(f"/projects/{project_id}")).json() assert detail["deadline_at"].startswith("2026-12-08T01:30") (bid,) = [d for d in detail["key_dates"] if d["label"] == "投标截止"] assert bid["replaces"]["document_label"] == "补遗 1" and bid["replaces"]["at"].startswith("2026-12-01") old_now = (await user.get(f"/checklist-items/{old['id']}")).json() assert old_now["deleted_at"] and old_now["superseded_by"]["confirmed"] assert (await user.get(f"/projects/{project_id}/checklist")).json()["counts"]["superseded_pending"] == 0 r = await user.post(f"/projects/{project_id}/fields/deadline_at/pending", json={"accept": True}) assert r.status_code == 409 and r.json()["code"] == "no_pending_value" async def test_addendum_reject_keeps_both(user: User) -> None: project_id = await parsed_project(user) await add_addendum(user, project_id) items = (await user.get(f"/projects/{project_id}/checklist/items")).json()["items"] (old,) = [i for i in deadline_items(items) if i["superseded_by"]] r = await user.post(f"/checklist-items/{old['id']}/supersession", json={"accept": False}) assert r.status_code == 200 and r.json()["superseded_by"] is None and r.json()["deleted_at"] is None assert len(deadline_items((await user.get(f"/projects/{project_id}/checklist/items")).json()["items"])) == 2 r = await user.post(f"/checklist-items/{old['id']}/supersession", json={"accept": True}) assert r.status_code == 409 and r.json()["code"] == "not_superseded" r = await user.post(f"/projects/{project_id}/fields/deadline_at/pending", json={"accept": False}) assert r.json()["pending"] is None and r.json()["value"].startswith("2026-12-01") assert (await user.get(f"/projects/{project_id}")).json()["deadline_at"].startswith("2026-12-01T01:30") assert (await user.get(f"/projects/{project_id}/check-preview")).json()["pending_updates"] == 0 # 已递交的项目只读 assert (await user.post(f"/projects/{project_id}/transitions", json={"action": "submit"})).status_code == 200 r = await user.post(f"/checklist-items/{old['id']}/supersession", json={"accept": True}) assert r.status_code == 409 and r.json()["code"] == "project_read_only" def test_same_time_ignores_missing_timezone() -> None: """补遗只是复述原时间时不关联;模型给的时间有时带时区、有时不带,按北京时间比。""" from bid_doctor.checklist.addendum import _same_time # pyright: ignore[reportPrivateUsage] assert _same_time("2026-12-01T09:30", "2026-12-01T09:30:00+08:00") assert not _same_time("2026-12-01T09:30", "2026-12-08T09:30:00+08:00")