fix(business): 删除保护补齐与单价小数位口径修复(e2e 发现)

- 客户删除保护:被业务引用 → 400(T4 占位注释在业务表就位后漏补,FK 500 暴露)
- 人员删除保护:业务归属(负责/协助)引用纳入检查(同样漏检 500)
- schema 单价类列(DECIMAL 14,4)小数位放行至 4 位:回显 '5000.0000'
  原样提交被两位校验永久拒收(编辑任何带精度单价的历史行都失败)
- 补 2 个集成用例锁定;文案同步「客户归属或业务归属引用」

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
liuwenxinandClaude Code committed 2026-10-13 17:06:46 +08:00
1 parent 299b72483c
commit fba8cfc215
8 files changed
+167 -23

No files matched your search

+52 -2
View File
@@ -29,6 +29,7 @@ pytestmark = pytest.mark.integration
_PERSONNEL = "/api/v1/base/personnel"
_CUSTOMERS = "/api/v1/base/customers"
_BUSINESSES = "/api/v1/business/businesses"
def _uname() -> str:
@@ -234,7 +235,7 @@ class TestDeleteProtection:
member = _create(client, headers, manager_id=manager["id"]) # 团队成员占用
blocked = client.delete(f"{_PERSONNEL}/{manager['id']}", headers=headers)
assert blocked.status_code == 400, blocked.text
assert "存在团队成员或客户归属引用,请改为离职而非删除" in blocked.json()["message"]
assert "存在团队成员、客户归属或业务归属引用,请改为离职而非删除" in blocked.json()["message"]
client.delete(f"{_PERSONNEL}/{member['id']}", headers=headers)
freed = client.delete(f"{_PERSONNEL}/{manager['id']}", headers=headers)
@@ -260,7 +261,7 @@ class TestDeleteProtection:
blocked = client.delete(f"{_PERSONNEL}/{owner['id']}", headers=headers)
assert blocked.status_code == 400, blocked.text
assert "存在团队成员或客户归属引用,请改为离职而非删除" in blocked.json()["message"]
assert "存在团队成员、客户归属或业务归属引用,请改为离职而非删除" in blocked.json()["message"]
# 保护与在职状态无关:离职并不能让物理删除放行
client.put(f"{_PERSONNEL}/{owner['id']}/status", json={"status": 0}, headers=headers)
@@ -269,6 +270,55 @@ class TestDeleteProtection:
# 删客户解除引用后方可删人
assert client.delete(f"{_CUSTOMERS}/{customer['id']}", headers=headers).status_code == 200
freed = client.delete(f"{_PERSONNEL}/{owner['id']}", headers=headers)
assert freed.status_code == 200, freed.text
def test_被业务归属引用禁删_负责或协助任一即拦(
self, client: TestClient, normal: dict
) -> None:
"""业务表就位后删除保护补齐:负责/协助引用任一即拦,FK 不以 500 暴露。"""
headers = normal["headers"]
owner = _create(client, headers)
assistant = _create(client, headers)
resp_customer = client.post(
_CUSTOMERS,
json={
"company_name": f"t_公司_{uuid.uuid4().hex[:8]}",
"owner_name": "张老板",
"customer_type": "personal",
},
headers=headers,
)
assert resp_customer.status_code == 200, resp_customer.text
customer_id = resp_customer.json()["data"]["id"]
resp = client.post(
_BUSINESSES,
json={
"customer_id": customer_id,
"owner_personnel_id": owner["id"],
"assistant_personnel_id": assistant["id"],
"items": [
{
"product_name": "t_引用产品",
"product_category": "municipal_water",
"quantity": "1",
"sale_price": "1",
}
],
},
headers=headers,
)
assert resp.status_code == 200, resp.text
for target, label in ((owner, "负责"), (assistant, "协助")):
blocked = client.delete(f"{_PERSONNEL}/{target['id']}", headers=headers)
assert blocked.status_code == 400, blocked.text
assert "业务归属" in blocked.json()["message"], f"{label}引用未拦截"
# 删除业务解除引用后,人员方可删除(owner 仍被客户归属引用,先删客户)
business_id = resp.json()["data"]["id"]
assert client.delete(f"{_BUSINESSES}/{business_id}", headers=headers).status_code == 200
assert client.delete(f"{_CUSTOMERS}/{customer_id}", headers=headers).status_code == 200
assert client.delete(f"{_PERSONNEL}/{owner['id']}", headers=headers).status_code == 200