"""移动市场预签单服务:通用数据权限与现场范围始终组合生效。""" from __future__ import annotations from datetime import datetime from decimal import Decimal from typing import List, Optional, Tuple from fastapi import HTTPException from sqlalchemy import func, or_ from sqlalchemy.orm import Session from app.services.project_assignment import ( create_assignment, guard_project_edit, filter_assignment, ) from app.crud import presign_order as presign_crud from app.models.models import ( BizCellSampleType, BizCollectionHospital, BizCollectionRegion, BizCollectionRegionStaff, BizPresignOrder, BizPresignOrderSampleType, BizPresignTransfer, ) from app.schemas.market_mobile import ( MarketPresignOrderBody, MarketPresignTransferBody, ) from app.services.market_field_scope import ( MarketFieldContext, apply_presign_scope, assert_presign_access, ) from app.services.market_support import validate_attachment_refs _MONEY_QUANTUM = Decimal('0.01') def _money_text(value: Optional[Decimal]) -> Optional[str]: if value is None: return None return format(Decimal(str(value)).quantize(_MONEY_QUANTUM), 'f') def _mobile_transfer_to_dict( db: Session, row: BizPresignTransfer, tenant_no: str ) -> dict: data = presign_crud._transfer_to_dict(db, row, tenant_no) data['transferAmount'] = _money_text(row.transfer_amount) return data def _reference_data( db: Session, rows: List[BizPresignOrder], tenant_no: str ) -> dict: order_ids = [row.id for row in rows] region_ids = {row.region_id for row in rows if row.region_id} hospital_ids = {row.hospital_id for row in rows if row.hospital_id} region_names = {} if region_ids: region_names = { str(row.id): str(row.region_name or '').strip() for row in db.query(BizCollectionRegion).filter( BizCollectionRegion.id.in_(region_ids), BizCollectionRegion.tenant_no == tenant_no, ) } hospital_names = {} if hospital_ids: hospital_names = { str(row.id): str(row.hospital_name or '').strip() for row in db.query(BizCollectionHospital).filter( BizCollectionHospital.id.in_(hospital_ids), BizCollectionHospital.tenant_no == tenant_no, ) } type_ids_by_order = {order_id: [] for order_id in order_ids} if order_ids: links = ( db.query(BizPresignOrderSampleType) .filter( BizPresignOrderSampleType.presign_order_id.in_(order_ids), BizPresignOrderSampleType.tenant_no == tenant_no, BizPresignOrderSampleType.del_flag == 0, ) .order_by( BizPresignOrderSampleType.order_num.asc(), BizPresignOrderSampleType.create_time.asc(), BizPresignOrderSampleType.id.asc(), ) .all() ) for link in links: if link.sample_type_id: type_ids_by_order.setdefault(link.presign_order_id, []).append( str(link.sample_type_id) ) type_ids = { type_id for order_type_ids in type_ids_by_order.values() for type_id in order_type_ids } type_names = {} if type_ids: type_names = { str(row.id): str(row.type_name or '').strip() for row in db.query(BizCellSampleType).filter( BizCellSampleType.id.in_(type_ids), BizCellSampleType.tenant_no == tenant_no, ) } return { 'regionNames': region_names, 'hospitalNames': hospital_names, 'sampleTypeIds': type_ids_by_order, 'sampleTypeNames': { order_id: [type_names.get(type_id, '') for type_id in order_type_ids] for order_id, order_type_ids in type_ids_by_order.items() }, } def _mobile_order_to_dict( db: Session, row: BizPresignOrder, tenant_no: str, *, with_children: bool, deposit_amount: Optional[Decimal] = None, user_name_map: Optional[dict] = None, reference_data: Optional[dict] = None, ) -> dict: deposit = ( deposit_amount if deposit_amount is not None else presign_crud.confirmed_deposit_amount( db, presign_order_id=row.id, tenant_no=tenant_no ) ) data = presign_crud.order_to_dict( db, row, tenant_no, with_children=False, deposit_amount=deposit, user_name_map=user_name_map, ) data['contractAmount'] = _money_text(row.contract_amount) data['confirmedDepositAmount'] = _money_text(deposit) references = reference_data or _reference_data(db, [row], tenant_no) data['regionName'] = references['regionNames'].get(row.region_id or '', '') data['hospitalName'] = references['hospitalNames'].get( row.hospital_id or '', '' ) data['sampleTypeIds'] = references['sampleTypeIds'].get(row.id, []) data['sampleTypeNames'] = references['sampleTypeNames'].get(row.id, []) if with_children: transfer_rows = ( db.query(BizPresignTransfer) .filter( BizPresignTransfer.presign_order_id == row.id, BizPresignTransfer.tenant_no == tenant_no, BizPresignTransfer.del_flag == 0, ) .order_by(BizPresignTransfer.create_time.asc()) .all() ) data['transfers'] = [ _mobile_transfer_to_dict(db, transfer, tenant_no) for transfer in transfer_rows ] return data def _dump_body(body: MarketPresignOrderBody) -> dict: return body.model_dump(by_alias=False, exclude_unset=True) 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('当前状态的预签约订单不可操作转账记录') def _scoped_order_for_update( db: Session, order_id: str, ctx: MarketFieldContext, user_info: dict, ) -> BizPresignOrder: row = ( apply_presign_scope( db.query(BizPresignOrder), db, ctx, user_info=user_info ) .filter(BizPresignOrder.id == order_id) .populate_existing() .with_for_update() .first() ) if row is None: raise HTTPException(status_code=404, detail='数据不存在或无权访问') return row def _current_dept_id(user_info: dict) -> str: """兼容 API 用户上下文与测试/内部调用的部门字段命名。""" if not isinstance(user_info, dict): return "" return str( user_info.get("dept_id") or user_info.get("deptId") or user_info.get("region_id") or "" ).strip() def _manager_assignment_allowed( db: Session, ctx: MarketFieldContext, *, region_id: Optional[str], region_manager_user_id: Optional[str], ) -> bool: region = (region_id or "").strip() if not region: return False return ( db.query(BizCollectionRegionStaff.id) .filter( BizCollectionRegionStaff.tenant_no == ctx.tenant_no, BizCollectionRegionStaff.del_flag == 0, BizCollectionRegionStaff.user_id == ctx.user_id, BizCollectionRegionStaff.is_leader == 1, BizCollectionRegionStaff.region_id == region, ) .first() is not None ) def _ensure_manager_assignment( db: Session, ctx: MarketFieldContext, *, region_id: Optional[str], region_manager_user_id: Optional[str], ) -> None: if not _manager_assignment_allowed( db, ctx, region_id=region_id, region_manager_user_id=region_manager_user_id, ): raise ValueError("预签约订单归属不在当前地区经理范围内") def _validate_master_data( db: Session, ctx: MarketFieldContext, *, region_id: Optional[str], hospital_id: Optional[str], sample_type_ids: Optional[List[str]], ) -> None: region = (region_id or "").strip() hospital = (hospital_id or "").strip() if region: region_exists = ( db.query(BizCollectionRegion.id) .filter( BizCollectionRegion.id == region, BizCollectionRegion.tenant_no == ctx.tenant_no, BizCollectionRegion.del_flag == 0, ) .first() is not None ) if not region_exists: raise ValueError("采集地区不存在或不属于当前租户") if hospital: hospital_exists = ( db.query(BizCollectionHospital.id) .filter( BizCollectionHospital.id == hospital, BizCollectionHospital.region_id == region, BizCollectionHospital.tenant_no == ctx.tenant_no, BizCollectionHospital.del_flag == 0, ) .first() is not None ) if not hospital_exists: raise ValueError("医院不存在、不属于当前租户或与采集地区不匹配") if sample_type_ids is not None: requested = { str(sample_type_id).strip() for sample_type_id in sample_type_ids if str(sample_type_id or "").strip() } if requested: existing = { str(row[0]) for row in db.query(BizCellSampleType.id) .filter( BizCellSampleType.id.in_(requested), BizCellSampleType.tenant_no == ctx.tenant_no, BizCellSampleType.del_flag == 0, ) .all() } if existing != requested: raise ValueError("样本类型不存在或不属于当前租户") def list_presign_orders( db: Session, *, ctx: MarketFieldContext, user_info: dict, page: int = 1, page_size: int = 10, keyword: Optional[str] = None, order_status: Optional[str] = None, project_id: Optional[str] = None, team_id: Optional[str] = None, ) -> Tuple[List[dict], int]: query = apply_presign_scope( db.query(BizPresignOrder), db, ctx, user_info=user_info ) query = filter_assignment( query, BizPresignOrder, db, ctx.tenant_no, project_id, team_id ) status = (order_status or "").strip() if status: query = query.filter(BizPresignOrder.order_status == status) search = (keyword or "").strip() if search: like = f"%{search}%" query = query.filter( or_( BizPresignOrder.protocol_no.like(like), BizPresignOrder.mother_name.like(like), ) ) total = ( query.order_by(None) .with_entities(func.count(BizPresignOrder.id)) .scalar() or 0 ) safe_page = max(1, page) safe_page_size = min(max(1, page_size), 100) rows = ( query.order_by(BizPresignOrder.create_time.desc()) .offset((safe_page - 1) * safe_page_size) .limit(safe_page_size) .all() ) order_ids = [row.id for row in rows] deposit_map = presign_crud.confirmed_deposit_amount_map( db, presign_order_ids=order_ids, tenant_no=ctx.tenant_no ) person_ids = [ user_id for row in rows for user_id in ( row.collector_user_id, row.sales_user_id, row.region_manager_user_id, ) if user_id ] user_name_map = presign_crud._user_nick_name_map( db, person_ids, ctx.tenant_no ) references = _reference_data(db, rows, ctx.tenant_no) return [ _mobile_order_to_dict( db, row, ctx.tenant_no, with_children=False, deposit_amount=deposit_map.get(row.id, 0), user_name_map=user_name_map, reference_data=references, ) for row in rows ], int(total) def get_presign_order( db: Session, *, order_id: str, ctx: MarketFieldContext, user_info: dict, ) -> dict: row = assert_presign_access( db, order_id, ctx, user_info=user_info ) references = _reference_data(db, [row], ctx.tenant_no) return _mobile_order_to_dict( db, row, ctx.tenant_no, with_children=True, reference_data=references, ) def create_presign_order( db: Session, *, body: MarketPresignOrderBody, ctx: MarketFieldContext, user_info: dict, ) -> dict: fields = _dump_body(body) fields["project_id"] = create_assignment( db, ctx.tenant_no, user_info, body.project_id, "presign" ) _reject_direct_conversion(fields) sample_type_ids = fields.pop("sample_type_ids", None) transfers = fields.pop("transfers", None) if transfers and ( fields.get('order_status') or 'active' ).strip() not in ('', 'draft', 'active'): raise ValueError('当前状态的预签约订单不可操作转账记录') _validate_master_data( db, ctx, region_id=fields.get("region_id"), hospital_id=fields.get("hospital_id"), sample_type_ids=sample_type_ids, ) if ctx.scope_kind == "specialist": fields["sales_user_id"] = ctx.user_id else: _ensure_manager_assignment( db, ctx, region_id=fields.get("region_id"), region_manager_user_id=fields.get("region_manager_user_id"), ) presign_crud.validate_presign_person_ids( db, tenant_no=ctx.tenant_no, collector_user_id=fields.get("collector_user_id") or ctx.user_id, sales_user_id=fields.get("sales_user_id"), region_manager_user_id=fields.get("region_manager_user_id"), ) transfer_fields = [ item.model_dump(by_alias=False, exclude_unset=True) if hasattr(item, "model_dump") else item for item in (transfers or []) ] try: row = presign_crud.create_presign_order( db, tenant_no=ctx.tenant_no, user_id=ctx.user_id, fields=fields, sample_type_ids=sample_type_ids, transfers=transfer_fields, create_dept_id=_current_dept_id(user_info), commit=False, ) created_transfers = ( db.query(BizPresignTransfer) .filter( BizPresignTransfer.presign_order_id == row.id, BizPresignTransfer.tenant_no == ctx.tenant_no, BizPresignTransfer.del_flag == 0, ) .all() ) for transfer in created_transfers: if transfer.transfer_attachments: transfer.transfer_attachments = validate_attachment_refs( db, transfer.transfer_attachments, module='presign', ctx=ctx, creating=True, business_id=row.id, ) db.add(transfer) db.flush() row = assert_presign_access( db, row.id, ctx, user_info=user_info ) db.commit() db.refresh(row) except Exception: db.rollback() raise return _mobile_order_to_dict( db, row, ctx.tenant_no, with_children=True ) def update_presign_order( db: Session, *, order_id: str, body: MarketPresignOrderBody, ctx: MarketFieldContext, user_info: dict, ) -> dict: try: current = _scoped_order_for_update( db, order_id, ctx, user_info ) if (current.order_status or '').strip() == 'converted': raise ValueError('当前状态的预签约订单不可修改') fields = _dump_body(body) guard_project_edit(current, fields) fields.pop("project_id", None) _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) resulting_hospital_id = fields.get("hospital_id", current.hospital_id) _validate_master_data( db, ctx, region_id=resulting_region_id, hospital_id=resulting_hospital_id, sample_type_ids=sample_type_ids, ) if ctx.scope_kind == "specialist": fields["sales_user_id"] = ctx.user_id elif str(resulting_region_id or '').strip() != str(current.region_id or '').strip(): _ensure_manager_assignment( db, ctx, region_id=resulting_region_id, region_manager_user_id=fields.get( "region_manager_user_id", current.region_manager_user_id ), ) presign_crud.validate_presign_person_ids( db, tenant_no=ctx.tenant_no, collector_user_id=fields.get( "collector_user_id", current.collector_user_id ), sales_user_id=fields.get("sales_user_id", current.sales_user_id), region_manager_user_id=fields.get( "region_manager_user_id", current.region_manager_user_id ), ) row = presign_crud.update_presign_order( db, order_id=order_id, tenant_no=ctx.tenant_no, user_id=ctx.user_id, 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 ) result = _mobile_order_to_dict( db, row, ctx.tenant_no, with_children=True ) db.commit() return result except Exception: db.rollback() raise def delete_presign_order( db: Session, order_id: str, ctx: MarketFieldContext, user_info: dict, ) -> None: try: row = _scoped_order_for_update(db, order_id, ctx, user_info) if (row.order_status or '').strip() == 'converted': raise ValueError('已转正式的预签约订单不可删除') presign_crud.soft_delete_presign_order( db, order_id=order_id, tenant_no=ctx.tenant_no, user_id=ctx.user_id, ) except Exception: db.rollback() raise def _scoped_transfer( db: Session, transfer_id: str, ctx: MarketFieldContext, user_info: dict, ) -> BizPresignTransfer: probe = ( db.query(BizPresignTransfer) .filter( BizPresignTransfer.id == transfer_id, BizPresignTransfer.tenant_no == ctx.tenant_no, BizPresignTransfer.del_flag == 0, ) .first() ) if probe is None: raise HTTPException(status_code=404, detail='转账记录不存在或无权访问') order = _scoped_order_for_update( db, probe.presign_order_id, ctx, user_info ) _assert_transfer_order_writable(order) row = ( db.query(BizPresignTransfer) .filter( BizPresignTransfer.id == transfer_id, BizPresignTransfer.tenant_no == ctx.tenant_no, BizPresignTransfer.del_flag == 0, ) .populate_existing() .with_for_update() .first() ) if row is None: raise HTTPException(status_code=404, detail='转账记录不存在或无权访问') return row def _update_transfer_if_status( db: Session, *, transfer_id: str, tenant_no: str, expected_status: str, values: dict, extra_conditions: tuple = (), ) -> None: updates = dict(values) changed = ( db.query(BizPresignTransfer) .filter( BizPresignTransfer.id == transfer_id, BizPresignTransfer.tenant_no == tenant_no, BizPresignTransfer.del_flag == 0, BizPresignTransfer.pay_status == expected_status, *extra_conditions, ) .update(updates, synchronize_session=False) ) if changed != 1: raise ValueError('转账状态已变化,请刷新后重试') def create_presign_transfer( db: Session, order_id: str, body: MarketPresignTransferBody, ctx: MarketFieldContext, user_info: dict, ) -> dict: try: order = _scoped_order_for_update( db, order_id, ctx, user_info ) _assert_transfer_order_writable(order) fields = _dump_transfer_body(body) if 'transfer_attachments' in fields: fields['transfer_attachments'] = validate_attachment_refs( db, fields['transfer_attachments'], module='presign', ctx=ctx, creating=True, business_id=order.id, ) row = presign_crud.add_transfer( db, presign_order_id=order.id, tenant_no=ctx.tenant_no, user_id=ctx.user_id, fields=fields, create_dept_id=_current_dept_id(user_info), commit=False, ) result = _mobile_transfer_to_dict(db, row, ctx.tenant_no) db.commit() return result except Exception: db.rollback() raise def update_presign_transfer( db: Session, transfer_id: str, body: MarketPresignTransferBody, ctx: MarketFieldContext, user_info: dict, ) -> dict: try: current = _scoped_transfer(db, transfer_id, ctx, user_info) if (current.pay_status or '').strip() != 'dfj': raise ValueError('仅待缴费状态可编辑意向金转账笔') fields = _dump_transfer_body(body) if 'transfer_attachments' in fields: fields['transfer_attachments'] = validate_attachment_refs( db, fields['transfer_attachments'], module='presign', ctx=ctx, creating=False, existing_refs=current.transfer_attachments, business_id=current.presign_order_id, ) row = presign_crud.update_transfer_draft( db, transfer_id=transfer_id, tenant_no=ctx.tenant_no, user_id=ctx.user_id, fields=fields, commit=False, ) result = _mobile_transfer_to_dict(db, row, ctx.tenant_no) db.commit() return result except Exception: db.rollback() raise def delete_presign_transfer( db: Session, transfer_id: str, ctx: MarketFieldContext, user_info: dict, ) -> None: try: row = _scoped_transfer(db, transfer_id, ctx, user_info) if (row.pay_status or '').strip() != 'dfj': raise ValueError('仅待缴费状态可删除意向金转账笔') _update_transfer_if_status( db, transfer_id=transfer_id, tenant_no=ctx.tenant_no, expected_status='dfj', values={ BizPresignTransfer.del_flag: 1, BizPresignTransfer.update_by: ctx.user_id, BizPresignTransfer.update_time: datetime.now(), }, ) db.commit() except Exception: db.rollback() raise def submit_presign_transfer( db: Session, transfer_id: str, ctx: MarketFieldContext, user_info: dict, ) -> dict: try: _scoped_transfer(db, transfer_id, ctx, user_info) row = presign_crud.submit_receipt_finance( db, transfer_id=transfer_id, tenant_no=ctx.tenant_no, user_id=ctx.user_id, commit=False, ) result = _mobile_transfer_to_dict(db, row, ctx.tenant_no) db.commit() return result except Exception: db.rollback() raise def withdraw_presign_transfer( db: Session, transfer_id: str, ctx: MarketFieldContext, user_info: dict, ) -> dict: try: _scoped_transfer(db, transfer_id, ctx, user_info) row = presign_crud.withdraw_receipt( db, transfer_id=transfer_id, tenant_no=ctx.tenant_no, user_id=ctx.user_id, commit=False, ) result = _mobile_transfer_to_dict(db, row, ctx.tenant_no) db.commit() return result except Exception: db.rollback() raise def convert_presign_order( db: Session, *, order_id: str, ctx: MarketFieldContext, user_info: dict, ) -> dict: try: row = _scoped_order_for_update(db, order_id, ctx, user_info) if (row.order_status or '').strip() not in ('', 'draft', 'active'): raise ValueError('当前状态的预签约订单不可转正式协议') presign_crud.validate_presign_person_ids( db, tenant_no=ctx.tenant_no, collector_user_id=row.collector_user_id or ctx.user_id, sales_user_id=row.sales_user_id, region_manager_user_id=row.region_manager_user_id, ) result = presign_crud.convert_to_protocol( db, order_id=order_id, tenant_no=ctx.tenant_no, user_id=ctx.user_id, create_dept_id=_current_dept_id(user_info), ) if isinstance(result.get('presignOrder'), dict): result = dict(result) result['presignOrder'] = _mobile_order_to_dict( db, row, ctx.tenant_no, with_children=True ) return result except Exception: db.rollback() raise