"""订单流转节点逐点验证测试。 逐一测试订单生命周期中的每个状态转换节点, 确保每个节点都能正常工作。 流程图(有司机): 创建 -> pending_approve -> approved -> pending_driver -> accepted -> picked_up -> pending_logistics -> in_transit -> shipped -> completed -> settled 流程图(无司机): 创建 -> pending_approve -> approved -> pending_logistics -> (快递100) -> in_transit -> shipped -> completed -> settled 取消流: pending_approve -> canceled (直接取消) approved -> cancel_pending -> canceled (审批取消) pending_driver -> cancel_fulfillment_pending -> canceled (履约取消) pending_driver -> cancel_fulfillment_pending -> approved (拒绝取消恢复) """ from __future__ import annotations import pytest @pytest.mark.order @pytest.mark.p0 class TestOrderFlowNode1_Create: """节点1: 创建订单 -> pending_approve""" def test_create_order_goes_to_pending_approve(self, client, salesman_headers, make_product): """创建订单后状态应为 pending_approve。""" product = make_product() resp = client.post("/api/orders", headers=salesman_headers, json={ "customer_name": "节点测试客户", "customer_mobile": "13800001001", "items": [{ "product_id": product.id, "product_name": product.product_name, "specification": product.specification, "unit": product.unit, "quantity": 10, "sale_price": 100, "cost_price": 60, }], }) assert resp.status_code == 200 data = resp.json()["data"] assert data["order_status"] == "pending_approve" assert data["order_no"] is not None return data["order_id"] @pytest.mark.order @pytest.mark.p0 class TestOrderFlowNode2_Approve: """节点2: pending_approve -> pending_logistics (审批通过,无司机)""" def test_approve_passes_order(self, client, salesman_headers, manager_headers, make_product): """审批通过后(无司机)状态应为 pending_logistics。""" product = make_product() # 创建 resp = client.post("/api/orders", headers=salesman_headers, json={ "customer_name": "审批测试客户", "customer_mobile": "13800001002", "items": [{ "product_id": product.id, "product_name": product.product_name, "specification": product.specification, "unit": product.unit, "quantity": 10, "sale_price": 100, "cost_price": 60, }], }) order_id = resp.json()["data"]["order_id"] # 审批通过(无司机) resp = client.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={ "approve_result": "pass", }) assert resp.status_code == 200 assert resp.json()["data"]["order_status"] == "pending_logistics" @pytest.mark.order @pytest.mark.p0 class TestOrderFlowNode2b_Reject: """节点2b: pending_approve -> rejected (审批拒绝)""" def test_reject_order(self, client, salesman_headers, manager_headers, make_product): """审批拒绝后状态应为 rejected。""" product = make_product() resp = client.post("/api/orders", headers=salesman_headers, json={ "customer_name": "拒绝测试客户", "customer_mobile": "13800001003", "items": [{ "product_id": product.id, "product_name": product.product_name, "specification": product.specification, "unit": product.unit, "quantity": 10, "sale_price": 100, "cost_price": 60, }], }) order_id = resp.json()["data"]["order_id"] resp = client.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={ "approve_result": "reject", "approve_opinion": "价格不合理", }) assert resp.status_code == 200 assert resp.json()["data"]["order_status"] == "rejected" @pytest.mark.order @pytest.mark.p0 class TestOrderFlowNode2c_RejectResubmit: """节点2c: rejected -> pending_approve (退回后重新提交)""" def test_reject_then_resubmit(self, client, salesman_headers, manager_headers, make_product): """退回后重新提交应恢复为 pending_approve。""" product = make_product() resp = client.post("/api/orders", headers=salesman_headers, json={ "customer_name": "重提测试客户", "customer_mobile": "13800001004", "items": [{ "product_id": product.id, "product_name": product.product_name, "specification": product.specification, "unit": product.unit, "quantity": 10, "sale_price": 100, "cost_price": 60, }], }) order_id = resp.json()["data"]["order_id"] # 拒绝 client.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={ "approve_result": "reject", "approve_opinion": "需修改", }) # 重新提交 resp = client.post(f"/api/orders/{order_id}/submit", headers=salesman_headers) assert resp.status_code == 200 assert resp.json()["data"]["order_status"] == "pending_approve" @pytest.mark.order @pytest.mark.p0 class TestOrderFlowNode3_ApproveWithoutDriver: """节点3: approved -> pending_logistics (无司机审批)""" def test_approve_without_driver_goes_to_pending_logistics(self, client, salesman_headers, manager_headers, make_product): """审批通过但不分配司机时,状态应为 pending_logistics。""" product = make_product() # 创建订单 resp = client.post("/api/orders", headers=salesman_headers, json={ "customer_name": "无司机测试客户", "customer_mobile": "13800001010", "items": [{ "product_id": product.id, "product_name": product.product_name, "specification": product.specification, "unit": product.unit, "quantity": 10, "sale_price": 100, "cost_price": 60, }], }) order_id = resp.json()["data"]["order_id"] # 审批通过但不分配司机 resp = client.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={ "approve_result": "pass", # 不提供 driver_id }) assert resp.status_code == 200 assert resp.json()["data"]["order_status"] == "pending_logistics" @pytest.mark.order @pytest.mark.p0 class TestOrderFlowNode3b_SupplierText: """节点3b: approved -> pending_logistics (确认供应商文本,仅当有司机时)""" def test_confirm_supplier_text_with_driver(self, client, salesman_headers, manager_headers, make_product, make_supplier): """有司机审批通过后,确认供应商文本后状态应为 pending_logistics。""" product = make_product() supplier = make_supplier() resp = client.post("/api/orders", headers=salesman_headers, json={ "customer_name": "下发测试客户", "customer_mobile": "13800001005", "factory_id": supplier.id, "items": [{ "product_id": product.id, "product_name": product.product_name, "specification": product.specification, "unit": product.unit, "quantity": 10, "sale_price": 100, "cost_price": 60, }], }) order_id = resp.json()["data"]["order_id"] # 审批通过(无司机,直接进入 pending_logistics) client.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={ "approve_result": "pass", }) # 确认供应商文本(此时订单已在 pending_logistics 状态) resp = client.post(f"/api/orders/{order_id}/supplier-text/confirm", headers=manager_headers, json={ "supplier_id": supplier.id, "text_content": "请安排生产10张产品", }) # 由于订单已在 pending_logistics 状态,确认供应商文本应失败 assert resp.status_code == 400 @pytest.mark.order @pytest.mark.p0 class TestOrderFlowNode4_ManualStatusChanges: """节点4-7: approved -> pending_driver -> accepted -> picked_up -> pending_logistics -> completed -> settled""" def test_full_fulfillment_pipeline(self, client, manager_headers, make_order): """完整履约流程: approved -> pending_driver -> accepted -> picked_up -> pending_logistics -> completed -> settled。 注意: shipped 状态可由快递100自动判断,不通过手动状态变更。 """ order = make_order(order_status="approved") order_id = order.id transitions = [ ("pending_driver", "分配司机"), ("accepted", "司机接单"), ("picked_up", "司机提货"), ("pending_logistics", "绑定物流单号"), # shipped 状态由快递100自动判断,不在此测试 ("completed", "订单完成"), ("settled", "订单结算"), ] for target_status, remark in transitions: resp = client.post(f"/api/orders/{order_id}/status", headers=manager_headers, json={ "target_status": target_status, "remark": remark, }) assert resp.status_code == 200, f"转换到 {target_status} 失败: {resp.text}" assert resp.json()["data"]["order_status"] == target_status, \ f"期望 {target_status}, 实际 {resp.json()['data']['order_status']}" @pytest.mark.cancel @pytest.mark.p0 class TestCancelNode1_DirectCancel: """取消节点1: pending_approve -> canceled (直接取消)""" def test_direct_cancel_from_pending_approve(self, client, salesman_headers, make_order): """待审批订单可直接取消。""" order = make_order(order_status="pending_approve", salesman_id=2) resp = client.post(f"/api/orders/{order.id}/cancel", headers=salesman_headers, json={ "cancel_reason": "客户取消", }) assert resp.status_code == 200 assert resp.json()["data"]["order_status"] == "canceled" @pytest.mark.cancel @pytest.mark.p0 class TestCancelNode2_ApprovedCancel: """取消节点2: approved -> cancel_pending -> canceled (审批取消)""" def test_approved_cancel_flow(self, client, salesman_headers, manager_headers, make_order): """已审批订单取消需要经理批准。""" order = make_order(order_status="approved", salesman_id=2) # 申请取消 resp = client.post(f"/api/orders/{order.id}/cancel", headers=salesman_headers, json={ "cancel_reason": "客户变更需求", }) assert resp.status_code == 200 assert resp.json()["data"]["order_status"] == "cancel_pending" # 经理批准取消 resp = client.post(f"/api/orders/{order.id}/cancel-approve", headers=manager_headers, json={ "approve_result": "pass", }) assert resp.status_code == 200 assert resp.json()["data"]["order_status"] == "canceled" def test_approved_cancel_refuse_restores(self, client, salesman_headers, manager_headers, make_order): """拒绝取消应恢复到原状态。""" order = make_order(order_status="approved", salesman_id=2) # 申请取消 client.post(f"/api/orders/{order.id}/cancel", headers=salesman_headers, json={ "cancel_reason": "客户变更需求", }) # 经理拒绝取消 resp = client.post(f"/api/orders/{order.id}/cancel-approve", headers=manager_headers, json={ "approve_result": "refuse", "approve_opinion": "不同意取消", }) assert resp.status_code == 200 assert resp.json()["data"]["order_status"] == "approved" @pytest.mark.cancel @pytest.mark.p0 class TestCancelNode3_FulfillmentCancel: """取消节点3: pending_driver -> cancel_fulfillment_pending -> canceled (履约取消)""" def test_fulfillment_cancel_flow(self, client, salesman_headers, manager_headers, make_order): """履约阶段取消需要经理批准。""" order = make_order(order_status="pending_driver", salesman_id=2) # 申请取消 resp = client.post(f"/api/orders/{order.id}/cancel", headers=salesman_headers, json={ "cancel_reason": "客户取消订单", }) assert resp.status_code == 200 assert resp.json()["data"]["order_status"] == "cancel_fulfillment_pending" # 经理批准 resp = client.post(f"/api/orders/{order.id}/cancel-approve", headers=manager_headers, json={ "approve_result": "pass", }) assert resp.status_code == 200 assert resp.json()["data"]["order_status"] == "canceled" def test_fulfillment_cancel_refuse_restores(self, client, salesman_headers, manager_headers, make_order): """拒绝履约取消应恢复到原状态。""" order = make_order(order_status="pending_driver", salesman_id=2) # 申请取消 client.post(f"/api/orders/{order.id}/cancel", headers=salesman_headers, json={ "cancel_reason": "客户取消订单", }) # 经理拒绝 resp = client.post(f"/api/orders/{order.id}/cancel-approve", headers=manager_headers, json={ "approve_result": "refuse", "approve_opinion": "已安排生产", }) assert resp.status_code == 200 assert resp.json()["data"]["order_status"] == "pending_driver" @pytest.mark.order @pytest.mark.p0 class TestInvalidTransitions: """非法状态转换应被拒绝。""" def test_cannot_approve_draft(self, client, manager_headers, make_order): """草稿订单不能审批。""" order = make_order(order_status="draft") resp = client.post(f"/api/orders/{order.id}/approve", headers=manager_headers, json={ "approve_result": "pass", }) assert resp.status_code in (400, 409) def test_cannot_approve_canceled(self, client, manager_headers, make_order): """已取消订单不能审批。""" order = make_order(order_status="canceled") resp = client.post(f"/api/orders/{order.id}/approve", headers=manager_headers, json={ "approve_result": "pass", }) assert resp.status_code in (400, 409) def test_cannot_submit_approved(self, client, salesman_headers, make_order): """已审批订单不能再次提交。""" order = make_order(order_status="approved", salesman_id=2) resp = client.post(f"/api/orders/{order.id}/submit", headers=salesman_headers) assert resp.status_code in (400, 409) def test_cannot_skip_to_unsupported_status(self, client, manager_headers, make_order): """approved 不能直接跳到未知状态。""" order = make_order(order_status="approved") resp = client.post(f"/api/orders/{order.id}/status", headers=manager_headers, json={ "target_status": "archived", }) assert resp.status_code in (400, 409) def test_cannot_cancel_settled(self, client, salesman_headers, make_order): """已结算订单不能取消。""" order = make_order(order_status="settled", salesman_id=2) resp = client.post(f"/api/orders/{order.id}/cancel", headers=salesman_headers, json={ "cancel_reason": "测试", }) assert resp.status_code in (400, 409) @pytest.mark.order @pytest.mark.p0 class TestOrderEditNodes: """编辑节点测试。""" def test_edit_pending_approve_order(self, client, salesman_headers, make_product): """pending_approve 状态可编辑。""" product = make_product() resp = client.post("/api/orders", headers=salesman_headers, json={ "customer_name": "编辑测试客户", "customer_mobile": "13800001010", "items": [{ "product_id": product.id, "product_name": product.product_name, "specification": product.specification, "unit": product.unit, "quantity": 10, "sale_price": 100, "cost_price": 60, }], }) order_id = resp.json()["data"]["order_id"] # 编辑 resp = client.put(f"/api/orders/{order_id}", headers=salesman_headers, json={ "customer_name": "编辑后的客户", "customer_mobile": "13800001010", "items": [{ "product_id": product.id, "product_name": product.product_name, "specification": product.specification, "unit": product.unit, "quantity": 20, "sale_price": 100, "cost_price": 60, }], }) assert resp.status_code == 200 assert resp.json()["data"]["order_status"] == "pending_approve" def test_edit_rejected_order(self, client, salesman_headers, manager_headers, make_product): """rejected 状态可编辑。""" product = make_product() resp = client.post("/api/orders", headers=salesman_headers, json={ "customer_name": "退回编辑客户", "customer_mobile": "13800001011", "items": [{ "product_id": product.id, "product_name": product.product_name, "specification": product.specification, "unit": product.unit, "quantity": 10, "sale_price": 100, "cost_price": 60, }], }) order_id = resp.json()["data"]["order_id"] # 拒绝 client.post(f"/api/orders/{order_id}/approve", headers=manager_headers, json={ "approve_result": "reject", "approve_opinion": "需修改", }) # 编辑 resp = client.put(f"/api/orders/{order_id}", headers=salesman_headers, json={ "customer_name": "退回编辑后的客户", "customer_mobile": "13800001011", "items": [{ "product_id": product.id, "product_name": product.product_name, "specification": product.specification, "unit": product.unit, "quantity": 15, "sale_price": 120, "cost_price": 70, }], }) assert resp.status_code == 200 assert resp.json()["data"]["order_status"] == "rejected" @pytest.mark.order @pytest.mark.p0 class TestOrderSupplierTextNode: """供应商文本节点测试。""" def test_generate_supplier_text(self, client, manager_headers, make_order, make_supplier): """生成供应商文本(不改变状态)。""" supplier = make_supplier() order = make_order(order_status="approved") order.factory_id = supplier.id resp = client.post(f"/api/orders/{order.id}/supplier-text", headers=manager_headers, json={ "supplier_id": supplier.id, }) assert resp.status_code == 200 # 生成文本不改变状态 detail = client.get(f"/api/orders/{order.id}", headers=manager_headers) assert detail.json()["data"]["order_status"] == "approved" @pytest.mark.order @pytest.mark.p0 class TestOrderPermissionNodes: """权限节点测试。""" def test_salesman_cannot_approve(self, client, salesman_headers, make_order): """业务员不能审批。""" order = make_order(order_status="pending_approve", salesman_id=2) resp = client.post(f"/api/orders/{order.id}/approve", headers=salesman_headers, json={ "approve_result": "pass", }) assert resp.status_code == 403 def test_salesman_cannot_change_status(self, client, salesman_headers, make_order): """业务员不能手动变更状态。""" order = make_order(order_status="approved", salesman_id=2) resp = client.post(f"/api/orders/{order.id}/status", headers=salesman_headers, json={ "target_status": "pending_driver", }) assert resp.status_code == 403 def test_salesman_cannot_cancel_approve(self, client, salesman_headers, make_order): """业务员不能审批取消。""" order = make_order(order_status="cancel_pending", salesman_id=2) resp = client.post(f"/api/orders/{order.id}/cancel-approve", headers=salesman_headers, json={ "approve_result": "pass", }) assert resp.status_code == 403 def test_manager_can_approve(self, client, manager_headers, make_order): """经理可以审批。""" order = make_order(order_status="pending_approve") resp = client.post(f"/api/orders/{order.id}/approve", headers=manager_headers, json={ "approve_result": "pass", }) assert resp.status_code == 200 def test_manager_can_change_status(self, client, manager_headers, make_order): """经理可以变更状态。""" order = make_order(order_status="approved") resp = client.post(f"/api/orders/{order.id}/status", headers=manager_headers, json={ "target_status": "pending_driver", }) assert resp.status_code == 200