fix: enforce atomic presign status and action transitions.
This commit is contained in:
1 parent
337d31bbe8
commit
3f40caa1cd
4 files changed
+194
-5
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user