| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937 |
- from datetime import datetime, timedelta
- from unittest.mock import patch
- import pytest
- from fastapi import HTTPException
- from pydantic import ValidationError
- from sqlalchemy import create_engine, event
- from sqlalchemy.orm import sessionmaker
- from app.models.base import Base
- from app.models.models import (
- BizAttachment,
- BizCellSampleType,
- BizCollectionHospital,
- BizCollectionProtocol,
- BizCollectionRegion,
- BizCollectionRegionStaff,
- BizCustomer,
- BizItemUnit,
- BizProtocolCollectionInfo,
- BizProtocolPartyCustomer,
- BizProtocolPayment,
- BizProtocolSample,
- BizSampleCode,
- SysPost,
- SysPostRole,
- SysRole,
- SysRoleDataScopeDetail,
- SysUser,
- SysUserPost,
- SysUserRole,
- )
- from app.models.research_donation import (
- BizProtocolDonationConvert,
- BizResearchDonation,
- BizResearchDonationCollection,
- BizResearchDonationParty,
- BizResearchDonationSample,
- BizResearchDonationSampleTransport,
- )
- def _session():
- engine = create_engine('sqlite:///:memory:')
- Base.metadata.create_all(
- engine,
- tables=[
- BizCollectionProtocol.__table__,
- BizProtocolPartyCustomer.__table__,
- BizProtocolCollectionInfo.__table__,
- BizProtocolSample.__table__,
- BizProtocolPayment.__table__,
- BizSampleCode.__table__,
- BizResearchDonation.__table__,
- BizResearchDonationParty.__table__,
- BizResearchDonationCollection.__table__,
- BizResearchDonationSample.__table__,
- BizResearchDonationSampleTransport.__table__,
- BizProtocolDonationConvert.__table__,
- BizCollectionRegionStaff.__table__,
- BizCollectionRegion.__table__,
- BizCollectionHospital.__table__,
- BizCellSampleType.__table__,
- BizItemUnit.__table__,
- BizCustomer.__table__,
- BizAttachment.__table__,
- SysUser.__table__,
- SysRole.__table__,
- SysRoleDataScopeDetail.__table__,
- SysUserRole.__table__,
- SysPost.__table__,
- SysPostRole.__table__,
- SysUserPost.__table__,
- ],
- )
- return sessionmaker(bind=engine)()
- def _ctx(scope_kind, user_id='u1'):
- from app.services.market_field_scope import MarketFieldContext
- return MarketFieldContext('tenant-a', user_id, scope_kind)
- def _user(user_id='u1'):
- return {
- 'user_id': user_id,
- 'tenant_no': 'tenant-a',
- 'role': 'super_admin',
- 'region_id': 'dept-from-auth',
- }
- def _cell(db, row_id, *, sales='u1', manager='manager-a', region='region-a', created=None):
- row = BizCollectionProtocol(
- id=row_id,
- protocol_no=f'CELL-{row_id}',
- protocol_status='dsx',
- tenant_no='tenant-a',
- create_dept_id='dept-a',
- create_time=created or datetime.now(),
- del_flag=0,
- )
- collection = BizProtocolCollectionInfo(
- id=f'ci-{row_id}',
- collection_protocol_id=row_id,
- region_id=region,
- sales_user_id=sales,
- region_manager_user_id=manager,
- tenant_no='tenant-a',
- create_dept_id='dept-a',
- del_flag=0,
- )
- db.add_all([row, collection])
- return row
- def _donation(db, row_id, *, sales='u1', manager='manager-a', region='region-a', created=None):
- row = BizResearchDonation(
- id=row_id,
- donation_no=f'DON-{row_id}',
- donation_status='dsx',
- tenant_no='tenant-a',
- create_dept_id='dept-a',
- create_time=created or datetime.now(),
- del_flag=0,
- )
- collection = BizResearchDonationCollection(
- id=f'dci-{row_id}',
- research_donation_id=row_id,
- region_id=region,
- sales_user_id=sales,
- region_manager_user_id=manager,
- tenant_no='tenant-a',
- create_dept_id='dept-a',
- del_flag=0,
- )
- db.add_all([row, collection])
- return row
- def _masters(db):
- db.add_all(
- [
- BizCollectionRegion(
- id='region-a', region_name='A区', tenant_no='tenant-a', create_dept_id='', del_flag=0
- ),
- BizCollectionRegion(
- id='region-b', region_name='B区', tenant_no='tenant-a', create_dept_id='', del_flag=0
- ),
- BizCollectionHospital(
- id='hospital-a', region_id='region-a', hospital_name='A院', tenant_no='tenant-a', create_dept_id='', del_flag=0
- ),
- BizCellSampleType(
- id='type-a', type_code='A', type_name='样本A', tenant_no='tenant-a', create_dept_id='', del_flag=0
- ),
- BizSampleCode(
- id='code-a', sample_code='S-001', tenant_no='tenant-a', create_dept_id='', del_flag=0
- ),
- BizSampleCode(
- id='code-cross', sample_code='S-CROSS', tenant_no='tenant-b', create_dept_id='', del_flag=0
- ),
- BizSampleCode(
- id='code-deleted', sample_code='S-DELETED', tenant_no='tenant-a', create_dept_id='', del_flag=1
- ),
- BizItemUnit(
- id='unit-a', unit_code='ml', unit_name='毫升', tenant_no='tenant-a', create_dept_id='', del_flag=0
- ),
- SysUser(
- id='u1', user_no='u1', nick_name='当前用户', status='1', tenant_no='tenant-a', create_dept_id='', del_flag=0
- ),
- SysUser(
- id='manager-a', user_no='manager-a', nick_name='经理', status='1', tenant_no='tenant-a', create_dept_id='', del_flag=0
- ),
- SysUser(
- id='u2', user_no='u2', nick_name='其他专员', status='1', tenant_no='tenant-a', create_dept_id='', del_flag=0
- ),
- BizAttachment(
- id='att-protocol-a',
- business_type='market:specialist:protocol:staging',
- original_name='a.pdf',
- file_ext='pdf',
- file_url='protocol/a.pdf',
- file_size=10,
- file_type='application/pdf',
- tenant_no='tenant-a',
- create_by='u1',
- create_dept_id='',
- del_flag=0,
- ),
- ]
- )
- db.commit()
- def _body(source='cell_storage', **overrides):
- from app.schemas.market_mobile import MarketProtocolBody
- payload = {
- 'source': source,
- 'protocolNo': 'P-001',
- 'registerDate': '20261010',
- 'attachments': {'protocolAttachments': 'protocol/a.pdf'},
- 'customer': {'motherName': '张三', 'motherPhone': '13800000000'},
- 'collection': {
- 'regionId': 'region-a',
- 'hospitalId': 'hospital-a',
- 'salesUserId': 'someone-else',
- 'regionManagerUserId': 'manager-a',
- },
- 'samples': [
- {
- 'sampleTypeId': 'type-a',
- 'sampleCodeId': 'code-a',
- 'sampleCode': 'S-001',
- 'unitId': 'unit-a',
- 'collectAmount': 10,
- }
- ],
- 'remark': '移动端登记',
- }
- payload.update(overrides)
- return MarketProtocolBody.model_validate(payload)
- def test_protocol_create_rejects_unknown_attachment_reference():
- from app.services.market_protocol import create_protocol
- db = _session()
- _masters(db)
- body = _body(
- attachments={'protocolAttachments': 'https://evil.test/a.pdf'}
- )
- with pytest.raises(ValueError, match='附件'):
- create_protocol(
- db,
- body=body,
- ctx=_ctx('specialist'),
- user_info=_user(),
- )
- def test_terminal_protocol_rejects_hidden_update_action():
- from app.services.market_protocol import update_protocol
- db = _session()
- _masters(db)
- row = _cell(db, 'terminal')
- row.protocol_status = 'yzf'
- db.commit()
- with pytest.raises(ValueError, match='不可修改'):
- update_protocol(
- db,
- source='cell_storage',
- protocol_id=row.id,
- body=_body(protocolNo='CELL-terminal'),
- ctx=_ctx('specialist'),
- user_info=_user(),
- )
- def test_protocol_list_combines_cell_and_donation_without_scope_leak():
- from app.services.market_protocol import list_protocols
- db = _session()
- now = datetime.now()
- _cell(db, 'cell-own', created=now - timedelta(minutes=2))
- _cell(db, 'cell-other', sales='u2', created=now)
- _donation(db, 'don-own', created=now - timedelta(minutes=1))
- _donation(db, 'don-other', sales='u2', created=now + timedelta(minutes=1))
- db.commit()
- items, total = list_protocols(db, ctx=_ctx('specialist'), user_info=_user())
- assert total == 2
- assert [(item['source'], item['id']) for item in items] == [
- ('research_donation', 'don-own'),
- ('cell_storage', 'cell-own'),
- ]
- def test_protocol_list_large_page_uses_bounded_source_queries_and_global_order():
- from app.services.market_protocol import list_protocols
- db = _session()
- base = datetime(2026, 10, 10, 12, 0, 0)
- expected = []
- for index in range(24):
- created = base + timedelta(seconds=index)
- cell_id = f'cell-{index:02d}'
- donation_id = f'don-{index:02d}'
- _cell(db, cell_id, created=created)
- _donation(db, donation_id, created=created + timedelta(microseconds=1))
- expected.extend([(created, cell_id), (created + timedelta(microseconds=1), donation_id)])
- db.commit()
- statements = []
- def capture(_conn, _cursor, statement, _parameters, _context, _many):
- if 'FROM biz_collection_protocol ' in statement or 'FROM biz_research_donation ' in statement:
- statements.append(statement)
- event.listen(db.get_bind(), 'before_cursor_execute', capture)
- try:
- items, total = list_protocols(
- db,
- ctx=_ctx('specialist'),
- user_info=_user(),
- page=4,
- page_size=7,
- )
- finally:
- event.remove(db.get_bind(), 'before_cursor_execute', capture)
- ordered_ids = [item[1] for item in sorted(expected, key=lambda item: (item[0], item[1]), reverse=True)]
- assert total == 48
- assert [item['id'] for item in items] == ordered_ids[21:28]
- page_selects = [sql for sql in statements if 'count(' not in sql.lower()]
- assert len(page_selects) == 2
- assert all(' LIMIT ' in sql.upper() for sql in page_selects)
- def test_protocol_list_stable_tie_breaker_prevents_page_overlap():
- from app.services.market_protocol import list_protocols
- db = _session()
- same_time = datetime(2026, 10, 10, 12, 0, 0)
- for row_id in ('a1', 'a3', 'a5', 'a7'):
- _cell(db, row_id, created=same_time)
- for row_id in ('a2', 'a4', 'a6', 'a8'):
- _donation(db, row_id, created=same_time)
- db.commit()
- page_one, total = list_protocols(
- db, ctx=_ctx('specialist'), user_info=_user(), page=1, page_size=3
- )
- page_two, _ = list_protocols(
- db, ctx=_ctx('specialist'), user_info=_user(), page=2, page_size=3
- )
- assert total == 8
- assert [item['id'] for item in page_one] == ['a8', 'a7', 'a6']
- assert [item['id'] for item in page_two] == ['a5', 'a4', 'a3']
- assert {item['id'] for item in page_one}.isdisjoint(
- {item['id'] for item in page_two}
- )
- def test_protocol_detail_rechecks_source_specific_idor():
- from app.services.market_protocol import get_protocol
- db = _session()
- _cell(db, 'cell-other', sales='u2')
- _donation(db, 'don-other', sales='u2')
- db.commit()
- with pytest.raises(HTTPException) as cell_error:
- get_protocol(
- db,
- source='cell_storage',
- protocol_id='cell-other',
- ctx=_ctx('specialist'),
- user_info=_user(),
- )
- with pytest.raises(HTTPException) as donation_error:
- get_protocol(
- db,
- source='research_donation',
- protocol_id='don-other',
- ctx=_ctx('specialist'),
- user_info=_user(),
- )
- assert cell_error.value.status_code == 404
- assert donation_error.value.status_code == 404
- def test_protocol_detail_can_resolve_source_from_scoped_record():
- from app.services.market_protocol import get_protocol
- db = _session()
- _donation(db, 'don-own')
- db.commit()
- result = get_protocol(
- db,
- source=None,
- protocol_id='don-own',
- ctx=_ctx('specialist'),
- user_info=_user(),
- )
- assert result['source'] == 'research_donation'
- assert result['basic']['id'] == 'don-own'
- def test_specialist_cell_create_forces_current_collection_and_collector_users():
- from app.services.market_protocol import create_protocol
- db = _session()
- _masters(db)
- result = create_protocol(
- db, body=_body(), ctx=_ctx('specialist'), user_info=_user()
- )
- main = db.get(BizCollectionProtocol, result['basic']['id'])
- collection = db.query(BizProtocolCollectionInfo).filter_by(collection_protocol_id=main.id, del_flag=0).one()
- party = db.query(BizProtocolPartyCustomer).filter_by(collection_protocol_id=main.id, del_flag=0).one()
- sample = db.query(BizProtocolSample).filter_by(collection_protocol_id=main.id, del_flag=0).one()
- assert main.collector_user_id == 'u1'
- assert main.create_dept_id == 'dept-from-auth'
- assert collection.sales_user_id == 'u1'
- assert collection.create_dept_id == 'dept-from-auth'
- assert party.collector_user_id == 'u1'
- assert party.customer_id is None
- assert db.query(BizCustomer).count() == 0
- assert sample.sample_code == 'S-001'
- def test_specialist_donation_create_forces_current_users_and_has_no_payment_section():
- from app.services.market_protocol import create_protocol
- db = _session()
- _masters(db)
- result = create_protocol(
- db,
- body=_body('research_donation', donationNo='D-001', protocolNo=None),
- ctx=_ctx('specialist'),
- user_info=_user(),
- )
- donation = db.get(BizResearchDonation, result['basic']['id'])
- collection = db.query(BizResearchDonationCollection).filter_by(research_donation_id=donation.id, del_flag=0).one()
- party = db.query(BizResearchDonationParty).filter_by(research_donation_id=donation.id, del_flag=0).one()
- sample = db.query(BizResearchDonationSample).filter_by(research_donation_id=donation.id, del_flag=0).one()
- assert donation.collector_user_id == 'u1'
- assert donation.create_dept_id == 'dept-from-auth'
- assert collection.sales_user_id == 'u1'
- assert collection.create_dept_id == 'dept-from-auth'
- assert party.collector_user_id == 'u1'
- assert party.create_dept_id == 'dept-from-auth'
- assert sample.create_dept_id == 'dept-from-auth'
- assert party.customer_id is None
- assert db.query(BizCustomer).count() == 0
- assert 'payment' not in result
- def test_manager_update_cannot_move_document_out_of_scope_and_rolls_back():
- from app.services.market_protocol import update_protocol
- db = _session()
- _masters(db)
- _cell(db, 'cell-manager', manager='u1')
- db.commit()
- with pytest.raises(ValueError, match='范围'):
- update_protocol(
- db,
- source='cell_storage',
- protocol_id='cell-manager',
- body=_body(collection={'regionId': 'region-b', 'regionManagerUserId': 'manager-a'}),
- ctx=_ctx('manager'),
- user_info=_user(),
- )
- db.expire_all()
- collection = db.query(BizProtocolCollectionInfo).filter_by(collection_protocol_id='cell-manager', del_flag=0).one()
- assert collection.region_id == 'region-a'
- assert collection.region_manager_user_id == 'u1'
- def test_partial_collection_update_validates_against_resulting_region():
- from app.schemas.market_mobile import MarketProtocolBody
- from app.services.market_protocol import update_protocol
- db = _session()
- _masters(db)
- _cell(db, 'cell-partial', sales='u1')
- db.commit()
- update_protocol(
- db,
- source='cell_storage',
- protocol_id='cell-partial',
- body=MarketProtocolBody.model_validate(
- {
- 'source': 'cell_storage',
- 'collection': {'hospitalId': 'hospital-a'},
- }
- ),
- ctx=_ctx('specialist'),
- user_info=_user(),
- )
- collection = db.query(BizProtocolCollectionInfo).filter_by(
- collection_protocol_id='cell-partial', del_flag=0
- ).one()
- assert collection.region_id == 'region-a'
- assert collection.hospital_id == 'hospital-a'
- def test_cell_partial_customer_update_preserves_ids_and_unspecified_fields():
- from app.schemas.market_mobile import MarketProtocolBody
- from app.services.market_protocol import update_protocol
- db = _session()
- _masters(db)
- _cell(db, 'cell-party')
- db.add_all(
- [
- BizCustomer(id='mother-c', customer_name='母客户', sex='1', tenant_no='tenant-a', create_dept_id='', del_flag=0),
- BizCustomer(id='father-c', customer_name='父客户', sex='0', tenant_no='tenant-a', create_dept_id='', del_flag=0),
- BizCustomer(id='newborn-c', customer_name='子客户', sex='2', tenant_no='tenant-a', create_dept_id='', del_flag=0),
- BizProtocolPartyCustomer(
- id='party-cell', collection_protocol_id='cell-party',
- customer_id='mother-c', father_customer_id='father-c', newborn_customer_id='newborn-c',
- mother_name='母亲', mother_phone='旧电话', mother_id_card='ID-M',
- father_name='父亲', father_phone='父电话', newborn_name='宝宝',
- tenant_no='tenant-a', create_dept_id='', del_flag=0,
- ),
- ]
- )
- db.commit()
- update_protocol(
- db,
- source='cell_storage',
- protocol_id='cell-party',
- body=MarketProtocolBody.model_validate(
- {
- 'source': 'cell_storage',
- 'customer': {'motherPhone': '新电话', 'remark': '只改电话'},
- }
- ),
- ctx=_ctx('specialist'),
- user_info=_user(),
- )
- party = db.get(BizProtocolPartyCustomer, 'party-cell')
- assert (party.customer_id, party.father_customer_id, party.newborn_customer_id) == (
- 'mother-c', 'father-c', 'newborn-c'
- )
- assert party.mother_name == '母亲'
- assert party.mother_id_card == 'ID-M'
- assert party.father_name == '父亲'
- assert party.father_phone == '父电话'
- assert party.newborn_name == '宝宝'
- assert party.mother_phone == '新电话'
- assert party.remark == '只改电话'
- @pytest.mark.parametrize('source', ['cell_storage', 'research_donation'])
- def test_protocol_update_rejects_same_tenant_unlinked_customer_id(source):
- from app.schemas.market_mobile import MarketProtocolBody
- from app.services.market_protocol import update_protocol
- db = _session()
- _masters(db)
- db.add_all(
- [
- BizCustomer(id='linked-c', customer_name='已关联', sex='1', tenant_no='tenant-a', create_dept_id='', del_flag=0),
- BizCustomer(id='unrelated-c', customer_name='无关客户', sex='1', tenant_no='tenant-a', create_dept_id='', del_flag=0),
- ]
- )
- if source == 'cell_storage':
- _cell(db, 'customer-idor')
- db.add(BizProtocolPartyCustomer(id='linked-party', collection_protocol_id='customer-idor', customer_id='linked-c', mother_name='原姓名', tenant_no='tenant-a', create_dept_id='', del_flag=0))
- else:
- _donation(db, 'customer-idor')
- db.add(BizResearchDonationParty(id='linked-party', research_donation_id='customer-idor', customer_id='linked-c', mother_name='原姓名', tenant_no='tenant-a', create_dept_id='', del_flag=0))
- db.commit()
- with pytest.raises(ValueError, match='客户'):
- update_protocol(
- db,
- source=source,
- protocol_id='customer-idor',
- body=MarketProtocolBody.model_validate(
- {
- 'source': source,
- 'customer': {'customerId': 'unrelated-c', 'motherName': '被篡改'},
- }
- ),
- ctx=_ctx('specialist'),
- user_info=_user(),
- )
- db.expire_all()
- assert db.get(BizCustomer, 'unrelated-c').customer_name == '无关客户'
- assert db.get(BizCustomer, 'linked-c').customer_name == '已关联'
- @pytest.mark.parametrize('source', ['cell_storage', 'research_donation'])
- def test_protocol_update_does_not_mutate_unrelated_customer_by_id_card(source):
- from app.schemas.market_mobile import MarketProtocolBody
- from app.services.market_protocol import update_protocol
- db = _session()
- _masters(db)
- db.add(BizCustomer(id='unrelated-card', customer_name='身份证客户', sex='1', id_card_no='CARD-1', tenant_no='tenant-a', create_dept_id='', del_flag=0))
- if source == 'cell_storage':
- _cell(db, 'card-idor')
- db.add(BizProtocolPartyCustomer(id='card-party', collection_protocol_id='card-idor', mother_name='原姓名', tenant_no='tenant-a', create_dept_id='', del_flag=0))
- else:
- _donation(db, 'card-idor')
- db.add(BizResearchDonationParty(id='card-party', research_donation_id='card-idor', mother_name='原姓名', tenant_no='tenant-a', create_dept_id='', del_flag=0))
- db.commit()
- update_protocol(
- db,
- source=source,
- protocol_id='card-idor',
- body=MarketProtocolBody.model_validate(
- {
- 'source': source,
- 'customer': {'motherName': '协议姓名', 'motherIdCard': 'CARD-1'},
- }
- ),
- ctx=_ctx('specialist'),
- user_info=_user(),
- )
- db.expire_all()
- assert db.get(BizCustomer, 'unrelated-card').customer_name == '身份证客户'
- @pytest.mark.parametrize('source', ['cell_storage', 'research_donation'])
- def test_protocol_create_forbids_existing_customer_ids(source):
- from app.services.market_protocol import create_protocol
- db = _session()
- _masters(db)
- db.add(BizCustomer(id='existing-c', customer_name='已有客户', sex='1', tenant_no='tenant-a', create_dept_id='', del_flag=0))
- db.commit()
- with pytest.raises(ValueError, match='客户'):
- create_protocol(
- db,
- body=_body(
- source,
- **({'donationNo': 'D-CUSTOMER', 'protocolNo': None} if source == 'research_donation' else {}),
- customer={'customerId': 'existing-c', 'motherName': '已有客户'},
- ),
- ctx=_ctx('specialist'),
- user_info=_user(),
- )
- def test_protocol_body_rejects_status_finance_and_conversion_fields():
- from app.schemas.market_mobile import MarketProtocolBody
- forbidden = [
- {'auditStatus': 'shtg'},
- {'protocolStatus': 'yzf'},
- {'delFlag': 1},
- {'refundAmount': 100},
- {'financeRemark': '通过'},
- {'toResearchTime': '2026-10-10'},
- ]
- for field in forbidden:
- with pytest.raises(ValidationError):
- MarketProtocolBody.model_validate({'source': 'cell_storage', **field})
- def test_cross_tenant_or_disabled_referenced_user_is_rejected():
- from app.services.market_protocol import create_protocol
- db = _session()
- _masters(db)
- db.add(
- SysUser(
- id='disabled', user_no='disabled', nick_name='停用', status='0', tenant_no='tenant-a', create_dept_id='', del_flag=0
- )
- )
- db.commit()
- with pytest.raises(ValueError, match='用户'):
- create_protocol(
- db,
- body=_body(collection={'regionId': 'region-a', 'hospitalId': 'hospital-a', 'regionManagerUserId': 'disabled'}),
- ctx=_ctx('specialist'),
- user_info=_user(),
- )
- @pytest.mark.parametrize('source', ['cell_storage', 'research_donation'])
- @pytest.mark.parametrize(
- ('sample_code_id', 'sample_code'),
- [
- ('code-cross', 'S-CROSS'),
- ('code-deleted', 'S-DELETED'),
- ('code-a', 'S-WRONG'),
- ],
- )
- def test_protocol_rejects_invalid_or_inconsistent_sample_code_reference(
- source, sample_code_id, sample_code
- ):
- from app.services.market_protocol import create_protocol
- db = _session()
- _masters(db)
- payload = {
- 'samples': [
- {
- 'sampleTypeId': 'type-a',
- 'sampleCodeId': sample_code_id,
- 'sampleCode': sample_code,
- 'unitId': 'unit-a',
- }
- ]
- }
- if source == 'research_donation':
- payload.update({'donationNo': 'D-CODE', 'protocolNo': None})
- with pytest.raises(ValueError, match='样本编码'):
- create_protocol(
- db,
- body=_body(source, **payload),
- ctx=_ctx('specialist'),
- user_info=_user(),
- )
- def test_cell_update_rolls_back_main_and_existing_children_after_late_failure():
- from app.schemas.market_mobile import MarketProtocolBody
- from app.services.market_protocol import update_protocol
- db = _session()
- _masters(db)
- _cell(db, 'cell-update')
- db.add_all(
- [
- BizProtocolPartyCustomer(
- id='party-old', collection_protocol_id='cell-update', mother_name='旧姓名', tenant_no='tenant-a', create_dept_id='', del_flag=0
- ),
- BizProtocolSample(
- id='sample-old', collection_protocol_id='cell-update', sample_code='OLD-CODE', tenant_no='tenant-a', create_dept_id='', del_flag=0
- ),
- ]
- )
- db.commit()
- body = MarketProtocolBody.model_validate(
- {
- 'source': 'cell_storage',
- 'remark': '新备注',
- 'customer': {'motherName': '新姓名'},
- 'collection': {'medicalStaff': '新医护'},
- 'samples': [{'sampleTypeId': 'type-a', 'sampleCodeId': 'code-a', 'sampleCode': 'S-001'}],
- }
- )
- with patch(
- 'app.services.market_protocol.cell_crud.save_samples',
- side_effect=RuntimeError('late failure'),
- ):
- with pytest.raises(RuntimeError, match='late'):
- update_protocol(
- db,
- source='cell_storage',
- protocol_id='cell-update',
- body=body,
- ctx=_ctx('specialist'),
- user_info=_user(),
- )
- db.expire_all()
- assert db.get(BizCollectionProtocol, 'cell-update').remark is None
- assert db.get(BizProtocolPartyCustomer, 'party-old').mother_name == '旧姓名'
- assert db.get(BizProtocolPartyCustomer, 'party-old').del_flag == 0
- assert db.get(BizProtocolSample, 'sample-old').sample_code == 'OLD-CODE'
- assert db.get(BizProtocolCollectionInfo, 'ci-cell-update').medical_staff is None
- def test_donation_update_rolls_back_main_and_old_children_after_soft_delete():
- from app.schemas.market_mobile import MarketProtocolBody
- from app.services.market_protocol import update_protocol
- db = _session()
- _masters(db)
- _donation(db, 'don-update')
- db.add_all(
- [
- BizResearchDonationParty(
- id='dparty-old', research_donation_id='don-update', mother_name='旧姓名', tenant_no='tenant-a', create_dept_id='', del_flag=0
- ),
- BizResearchDonationSample(
- id='dsample-old', research_donation_id='don-update', sample_code='OLD-CODE', tenant_no='tenant-a', create_dept_id='', del_flag=0
- ),
- ]
- )
- db.commit()
- body = MarketProtocolBody.model_validate(
- {
- 'source': 'research_donation',
- 'donationNo': 'DON-don-update',
- 'remark': '新备注',
- 'customer': {'motherName': '新姓名'},
- 'collection': {'medicalStaff': '新医护'},
- 'samples': [{'sampleTypeId': 'type-a', 'sampleCodeId': 'code-a', 'sampleCode': 'S-001'}],
- }
- )
- with patch(
- 'app.crud.research_donation._create_samples',
- side_effect=RuntimeError('late failure'),
- ):
- with pytest.raises(RuntimeError, match='late'):
- update_protocol(
- db,
- source='research_donation',
- protocol_id='don-update',
- body=body,
- ctx=_ctx('specialist'),
- user_info=_user(),
- )
- db.expire_all()
- assert db.get(BizResearchDonation, 'don-update').remark is None
- assert db.get(BizResearchDonationParty, 'dparty-old').mother_name == '旧姓名'
- assert db.get(BizResearchDonationParty, 'dparty-old').del_flag == 0
- assert db.get(BizResearchDonationSample, 'dsample-old').del_flag == 0
- assert db.get(BizResearchDonationCollection, 'dci-don-update').medical_staff is None
- def test_manager_can_update_donation_in_scope_but_cannot_move_it_out():
- from app.schemas.market_mobile import MarketProtocolBody
- from app.services.market_protocol import update_protocol
- db = _session()
- _masters(db)
- _donation(db, 'don-manager', sales='u2', manager='u1')
- db.add(
- BizResearchDonationParty(
- id='dparty-manager', research_donation_id='don-manager', mother_name='旧姓名', tenant_no='tenant-a', create_dept_id='', del_flag=0
- )
- )
- db.commit()
- result = update_protocol(
- db,
- source='research_donation',
- protocol_id='don-manager',
- body=MarketProtocolBody.model_validate(
- {
- 'source': 'research_donation',
- 'donationNo': 'DON-don-manager',
- 'remark': '经理已修改',
- 'customer': {'motherName': '新姓名'},
- 'collection': {'medicalStaff': '新医护'},
- }
- ),
- ctx=_ctx('manager'),
- user_info=_user(),
- )
- assert result['basic']['remark'] == '经理已修改'
- assert result['customer']['motherName'] == '新姓名'
- assert result['collection']['medicalStaff'] == '新医护'
- with pytest.raises(ValueError, match='范围'):
- update_protocol(
- db,
- source='research_donation',
- protocol_id='don-manager',
- body=MarketProtocolBody.model_validate(
- {
- 'source': 'research_donation',
- 'collection': {
- 'regionId': 'region-b',
- 'regionManagerUserId': 'manager-a',
- },
- }
- ),
- ctx=_ctx('manager'),
- user_info=_user(),
- )
- db.expire_all()
- collection = db.query(BizResearchDonationCollection).filter_by(
- research_donation_id='don-manager', del_flag=0
- ).one()
- assert collection.region_id == 'region-a'
- assert collection.region_manager_user_id == 'u1'
- def test_protocol_routes_do_not_expose_audit_void_refund_or_conversion():
- from app.api.market_mobile import router
- paths = {route.path for route in router.routes}
- disallowed = ('audit', 'void', 'refund', 'convert', 'to-research', 'to-storage')
- protocol_paths = [path for path in paths if '/protocols' in path]
- assert protocol_paths
- assert all(not any(word in path for word in disallowed) for path in protocol_paths)
- def test_cell_create_rolls_back_main_customer_and_children_on_child_failure():
- from app.services.market_protocol import create_protocol
- db = _session()
- _masters(db)
- with patch(
- 'app.services.market_protocol.cell_crud.upsert_collection_info',
- side_effect=RuntimeError('injected child failure'),
- ):
- with pytest.raises(RuntimeError, match='injected'):
- create_protocol(
- db, body=_body(), ctx=_ctx('specialist'), user_info=_user()
- )
- assert db.query(BizCollectionProtocol).count() == 0
- assert db.query(BizProtocolPartyCustomer).count() == 0
- assert db.query(BizCustomer).count() == 0
- def test_donation_create_rolls_back_customer_and_children_on_child_failure():
- from app.services.market_protocol import create_protocol
- db = _session()
- _masters(db)
- with patch(
- 'app.crud.research_donation._create_collection',
- side_effect=RuntimeError('injected child failure'),
- ):
- with pytest.raises(RuntimeError, match='injected'):
- create_protocol(
- db,
- body=_body(
- 'research_donation', donationNo='D-ROLLBACK', protocolNo=None
- ),
- ctx=_ctx('specialist'),
- user_info=_user(),
- )
- assert db.query(BizResearchDonation).count() == 0
- assert db.query(BizResearchDonationParty).count() == 0
- assert db.query(BizCustomer).count() == 0
|