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:
1 parent
299b72483c
commit
fba8cfc215
8 files changed
+167
-23
No files matched your search
@@ -22,6 +22,7 @@ from app.core.constants import (
|
||||
CUSTOMER_TYPE_ENTERPRISE,
|
||||
CUSTOMER_TYPE_PERSONAL,
|
||||
MODULE_CUSTOMER,
|
||||
PRODUCT_CATEGORY_MUNICIPAL_WATER,
|
||||
)
|
||||
from app.core.db import get_session_factory
|
||||
from app.models import BusinessRole, OperationLog, OwnershipChangeLog
|
||||
@@ -31,6 +32,7 @@ pytestmark = pytest.mark.integration
|
||||
|
||||
_PERSONNEL = "/api/v1/base/personnel"
|
||||
_CUSTOMERS = "/api/v1/base/customers"
|
||||
_BUSINESSES = "/api/v1/business/businesses"
|
||||
|
||||
|
||||
def _company() -> str:
|
||||
@@ -149,6 +151,32 @@ class TestCrudFlow:
|
||||
assert dup.status_code == 400, dup.text
|
||||
assert "该公司名称与负责人的客户已存在" in dup.json()["message"]
|
||||
|
||||
def test_删除被业务引用的客户_400引导处理单据(
|
||||
self, client: TestClient, normal: dict
|
||||
) -> None:
|
||||
"""业务表就位后删除保护补齐:FK RESTRICT 不能以 500 暴露。"""
|
||||
headers = normal["headers"]
|
||||
customer = _create_customer(client, headers)
|
||||
resp = client.post(
|
||||
_BUSINESSES,
|
||||
json={
|
||||
"customer_id": customer["id"],
|
||||
"items": [
|
||||
{
|
||||
"product_name": "t_被引用产品",
|
||||
"product_category": PRODUCT_CATEGORY_MUNICIPAL_WATER,
|
||||
"quantity": "1",
|
||||
"sale_price": "1",
|
||||
}
|
||||
],
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
blocked = client.delete(f"{_CUSTOMERS}/{customer['id']}", headers=headers)
|
||||
assert blocked.status_code == 400, blocked.text
|
||||
assert "已存在关联业务" in blocked.json()["message"]
|
||||
|
||||
def test_客户分类非法_400(self, client: TestClient, normal: dict) -> None:
|
||||
resp = client.post(
|
||||
_CUSTOMERS,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user