fix: enforce atomic presign status and action transitions.

This commit is contained in:
zlwu committed 2026-10-11 12:06:22 +08:00
1 parent 337d31bbe8
commit 3f40caa1cd
4 files changed
+194 -5

No files matched your search

+11 -1
View File
@@ -37,6 +37,11 @@ def _dump_transfer_body(body: MarketPresignTransferBody) -> dict:
return body.model_dump(by_alias=False, exclude_unset=True)
def _reject_direct_conversion(fields: dict) -> None:
if str(fields.get('order_status') or '').strip() == 'converted':
raise ValueError('预签约订单必须通过转正式操作进入已转正式状态')
def _assert_transfer_order_writable(order: BizPresignOrder) -> None:
if (order.order_status or '').strip() not in ('', 'draft', 'active'):
raise ValueError('当前状态的预签约订单不可操作转账记录')
@@ -266,6 +271,7 @@ def create_presign_order(
user_info: dict,
) -> dict:
fields = _dump_body(body)
_reject_direct_conversion(fields)
sample_type_ids = fields.pop("sample_type_ids", None)
transfers = fields.pop("transfers", None)
if transfers and (
@@ -361,6 +367,7 @@ def update_presign_order(
if (current.order_status or '').strip() == 'converted':
raise ValueError('当前状态的预签约订单不可修改')
fields = _dump_body(body)
_reject_direct_conversion(fields)
sample_type_ids = fields.pop("sample_type_ids", None)
fields.pop("transfers", None)
resulting_region_id = fields.get("region_id", current.region_id)
@@ -402,13 +409,16 @@ def update_presign_order(
fields=fields,
sample_type_ids=sample_type_ids,
create_dept_id=_current_dept_id(user_info),
commit=False,
)
row = assert_presign_access(
db, row.id, ctx, user_info=user_info
)
return presign_crud.order_to_dict(
result = presign_crud.order_to_dict(
db, row, ctx.tenant_no, with_children=True
)
db.commit()
return result
except Exception:
db.rollback()
raise
+14 -4
View File
@@ -200,7 +200,6 @@ def get_form_options(
selected_region = str(region_id or '').strip()
if selected_region and selected_region not in region_ids:
raise PermissionError('所选地区不在当前用户权限范围内')
hospital_regions = [selected_region] if selected_region else region_ids
regions = []
if region_ids:
regions = (
@@ -213,6 +212,12 @@ def get_form_options(
.order_by(BizCollectionRegion.order_num.asc(), BizCollectionRegion.region_name.asc())
.all()
)
active_region_ids = [str(row.id) for row in regions]
if selected_region and selected_region not in active_region_ids:
raise PermissionError('所选地区不存在或已停用')
hospital_regions = (
[selected_region] if selected_region else active_region_ids
)
hospitals = []
staff_rows = []
if hospital_regions:
@@ -702,12 +707,17 @@ def decorate_allowed_actions(
)
return data
if module == 'presign' and detail:
parent_writable = status.lower() in {'', 'draft', 'active'}
transfers = []
for transfer in data.get('transfers') or []:
transfer_data = dict(transfer)
transfer_data['allowedActions'] = allowed_actions(
'presign-transfer',
status=str(transfer_data.get('payStatus') or ''),
transfer_data['allowedActions'] = (
allowed_actions(
'presign-transfer',
status=str(transfer_data.get('payStatus') or ''),
)
if parent_writable
else []
)
transfers.append(transfer_data)
data['transfers'] = transfers
+90
View File
@@ -539,6 +539,96 @@ def test_specialist_create_forces_current_sales_user_and_accepts_order_status():
assert row.order_status == "void"
def test_create_rejects_client_requested_converted_status_without_writing():
from app.schemas.market_mobile import MarketPresignOrderBody
from app.services.market_presign import create_presign_order
db = _session()
_add_master_data(db)
with pytest.raises(ValueError, match='转正式|converted'):
create_presign_order(
db,
body=MarketPresignOrderBody.model_validate(
{
'motherName': '不可直转客户',
'regionId': 'region-a',
'hospitalId': 'hospital-a',
'orderStatus': 'converted',
}
),
ctx=_ctx('specialist'),
user_info=_user(),
)
assert db.query(BizPresignOrder).count() == 0
@pytest.mark.parametrize('current_status', ['active', 'draft', 'void'])
def test_update_rejects_client_requested_converted_status_without_writing(
current_status,
):
from app.schemas.market_mobile import MarketPresignOrderBody
from app.services.market_presign import update_presign_order
db = _session()
_add_master_data(db)
order = _order('own', sales_user_id='u1')
order.order_status = current_status
order.remark = 'before'
db.add(order)
db.commit()
with pytest.raises(ValueError, match='转正式|converted'):
update_presign_order(
db,
order_id='own',
body=MarketPresignOrderBody.model_validate(
{'orderStatus': 'converted', 'remark': 'after'}
),
ctx=_ctx('specialist'),
user_info=_user(),
)
db.expire_all()
unchanged = db.query(BizPresignOrder).filter_by(id='own').one()
assert unchanged.order_status == current_status
assert unchanged.remark == 'before'
def test_update_rolls_back_when_response_serialization_fails(monkeypatch):
from app.crud import presign_order as presign_crud
from app.schemas.market_mobile import MarketPresignOrderBody
from app.services.market_presign import update_presign_order
db = _session()
_add_master_data(db)
order = _order('own', sales_user_id='u1')
order.remark = 'before'
db.add(order)
db.commit()
monkeypatch.setattr(
presign_crud,
'order_to_dict',
lambda *args, **kwargs: (_ for _ in ()).throw(
RuntimeError('serialize failed')
),
)
with pytest.raises(RuntimeError, match='serialize failed'):
update_presign_order(
db,
order_id='own',
body=MarketPresignOrderBody(remark='after'),
ctx=_ctx('specialist'),
user_info=_user(),
)
db.expire_all()
assert db.query(BizPresignOrder).filter_by(id='own').one().remark == 'before'
def test_create_rolls_back_order_children_and_transfers_when_data_scope_rejects():
from app.schemas.market_mobile import MarketPresignOrderBody
from app.services.market_presign import create_presign_order
+79
View File
@@ -712,6 +712,85 @@ def test_presign_actions_include_delete_and_nested_transfer_actions():
[],
]
for parent_status, parent_actions in (
('converted', ['edit']),
('void', ['edit', 'delete']),
):
locked = decorate_allowed_actions(
'presign',
{
'orderStatus': parent_status,
'transfers': [
{'id': 't1', 'payStatus': 'dfj'},
{'id': 't2', 'payStatus': 'jfqrz'},
],
},
scope_kind='specialist',
detail=True,
)
assert locked['allowedActions'] == parent_actions
assert [row['allowedActions'] for row in locked['transfers']] == [[], []]
unknown = decorate_allowed_actions(
'presign',
{
'orderStatus': 'legacy-locked',
'transfers': [{'id': 't1', 'payStatus': 'dfj'}],
},
scope_kind='specialist',
detail=True,
)
assert unknown['transfers'][0]['allowedActions'] == []
def test_form_options_do_not_leak_children_of_soft_deleted_regions():
from app.services.market_support import get_form_options
db = _session()
_seed_options(db)
db.add_all(
[
BizCollectionRegion(
id='deleted-region',
region_name='已删除地区',
tenant_no='tenant-a',
create_dept_id='',
del_flag=1,
),
BizCollectionHospital(
id='deleted-region-hospital',
region_id='deleted-region',
hospital_name='不应泄漏医院',
tenant_no='tenant-a',
create_dept_id='',
del_flag=0,
),
BizCollectionRegionStaff(
id='deleted-region-staff',
region_id='deleted-region',
user_id='user-a',
is_leader=0,
tenant_no='tenant-a',
create_dept_id='',
del_flag=0,
),
]
)
db.commit()
out = get_form_options(db, module='presign', ctx=_ctx(), user_info=_user())
assert '已删除地区' not in str(out)
assert '不应泄漏医院' not in str(out)
with pytest.raises(PermissionError, match='地区'):
get_form_options(
db,
module='presign',
ctx=_ctx(),
user_info=_user(),
region_id='deleted-region',
)
def test_decorate_allowed_actions_covers_nested_payment_transfers():
from app.services.market_support import decorate_allowed_actions