diff --git a/backend/app/services/market_presign.py b/backend/app/services/market_presign.py index 7298221c..4eac38cd 100644 --- a/backend/app/services/market_presign.py +++ b/backend/app/services/market_presign.py @@ -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 diff --git a/backend/app/services/market_support.py b/backend/app/services/market_support.py index 1bfb4de5..e8d7c507 100644 --- a/backend/app/services/market_support.py +++ b/backend/app/services/market_support.py @@ -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 diff --git a/backend/tests/test_market_presign_api.py b/backend/tests/test_market_presign_api.py index 54bf4888..ec1ca55d 100644 --- a/backend/tests/test_market_presign_api.py +++ b/backend/tests/test_market_presign_api.py @@ -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 diff --git a/backend/tests/test_market_support_api.py b/backend/tests/test_market_support_api.py index ca5aa069..ed0e48bc 100644 --- a/backend/tests/test_market_support_api.py +++ b/backend/tests/test_market_support_api.py @@ -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