544 lines
21 KiB
Python
544 lines
21 KiB
Python
"""订单流转节点逐点验证测试。
|
||
|
||
逐一测试订单生命周期中的每个状态转换节点,
|
||
确保每个节点都能正常工作。
|
||
|
||
流程图(有司机):
|
||
创建 -> pending_approve -> approved -> pending_driver -> accepted
|
||
-> picked_up -> pending_logistics -> (快递100) -> delivered -> completed -> settled
|
||
|
||
流程图(无司机):
|
||
创建 -> pending_approve -> approved -> pending_logistics -> (快递100)
|
||
-> delivered -> 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。
|
||
|
||
注意: delivered 状态由快递100自动判断,不通过手动状态变更。
|
||
"""
|
||
order = make_order(order_status="approved")
|
||
order_id = order.id
|
||
|
||
transitions = [
|
||
("pending_driver", "分配司机"),
|
||
("accepted", "司机接单"),
|
||
("picked_up", "司机提货"),
|
||
("pending_logistics", "绑定物流单号"),
|
||
# delivered 状态由快递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_delivered(self, client, manager_headers, make_order):
|
||
"""approved 不能直接跳到 delivered。"""
|
||
order = make_order(order_status="approved")
|
||
resp = client.post(f"/api/orders/{order.id}/status", headers=manager_headers, json={
|
||
"target_status": "delivered",
|
||
})
|
||
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
|