1788 lines
58 KiB
Python
1788 lines
58 KiB
Python
import asyncio
|
|
from datetime import datetime, timedelta
|
|
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,
|
|
BizCollectionProtocol,
|
|
BizCollectionRegionStaff,
|
|
BizProtocolCollectionInfo,
|
|
BizProtocolFullmoonTodo,
|
|
BizProtocolPartyCustomer,
|
|
BizProtocolSample,
|
|
BizProtocolStorageCert,
|
|
BizQcWorkOrder,
|
|
BizSampleReception,
|
|
BizStorageReleaseReview,
|
|
SysPost,
|
|
SysPostRole,
|
|
SysRole,
|
|
SysRoleDataScopeDetail,
|
|
SysUser,
|
|
SysUserPost,
|
|
SysUserRole,
|
|
)
|
|
from app.models.research_donation import (
|
|
BizResearchDonation,
|
|
BizResearchDonationCollection,
|
|
BizResearchDonationParty,
|
|
BizResearchDonationSample,
|
|
)
|
|
|
|
|
|
TABLES = [
|
|
BizAttachment.__table__,
|
|
BizCollectionProtocol.__table__,
|
|
BizProtocolCollectionInfo.__table__,
|
|
BizProtocolPartyCustomer.__table__,
|
|
BizProtocolSample.__table__,
|
|
BizResearchDonation.__table__,
|
|
BizResearchDonationCollection.__table__,
|
|
BizResearchDonationParty.__table__,
|
|
BizResearchDonationSample.__table__,
|
|
BizCellSampleType.__table__,
|
|
BizSampleReception.__table__,
|
|
BizQcWorkOrder.__table__,
|
|
BizStorageReleaseReview.__table__,
|
|
BizProtocolStorageCert.__table__,
|
|
BizProtocolFullmoonTodo.__table__,
|
|
BizCollectionRegionStaff.__table__,
|
|
SysUser.__table__,
|
|
SysRole.__table__,
|
|
SysRoleDataScopeDetail.__table__,
|
|
SysUserRole.__table__,
|
|
SysPost.__table__,
|
|
SysPostRole.__table__,
|
|
SysUserPost.__table__,
|
|
]
|
|
|
|
|
|
def _session():
|
|
engine = create_engine('sqlite:///:memory:')
|
|
Base.metadata.create_all(engine, tables=TABLES)
|
|
db = sessionmaker(bind=engine)()
|
|
for index, name in enumerate(
|
|
(
|
|
'a.jpg',
|
|
'b.jpg',
|
|
'cert-new.jpg',
|
|
'cert-old.jpg',
|
|
'i.jpg',
|
|
'insurance-new.jpg',
|
|
'insurance-old.jpg',
|
|
'keep.jpg',
|
|
'keep-insurance.jpg',
|
|
'old.jpg',
|
|
)
|
|
):
|
|
db.add(
|
|
BizAttachment(
|
|
id=f'att-sample-{index}',
|
|
business_type='market:specialist:sample:staging',
|
|
original_name=name,
|
|
file_ext='jpg',
|
|
file_url=name,
|
|
file_size=10,
|
|
file_type='image/jpeg',
|
|
tenant_no='tenant-a',
|
|
create_by='u1',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
)
|
|
)
|
|
db.commit()
|
|
return db
|
|
|
|
|
|
def _ctx(kind='specialist', user_id='u1'):
|
|
from app.services.market_field_scope import MarketFieldContext
|
|
|
|
return MarketFieldContext('tenant-a', user_id, kind)
|
|
|
|
|
|
def _user(user_id='u1'):
|
|
return {
|
|
'user_id': user_id,
|
|
'tenant_no': 'tenant-a',
|
|
'role': 'super_admin',
|
|
'region_id': 'dept-a',
|
|
}
|
|
|
|
|
|
def _assign_region_staff(db, user_id: str, key: str, region_id: str = 'r1'):
|
|
db.add(
|
|
BizCollectionRegionStaff(
|
|
id=f'staff-{key}',
|
|
region_id=region_id,
|
|
user_id=user_id,
|
|
is_leader=0,
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
)
|
|
)
|
|
|
|
|
|
def test_sample_handle_rejects_external_attachment_reference():
|
|
from app.services.market_sample import update_storage_cert
|
|
|
|
db = _session()
|
|
_, _, sample = _cell_sample(db, 'attachment-reject')
|
|
with pytest.raises(ValueError, match='附件'):
|
|
update_storage_cert(
|
|
db,
|
|
sample_id=f"cell_storage:{sample.sample_code}",
|
|
fields={'attachments': 'https://evil.test/a.jpg'},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
|
|
def _cell_sample(
|
|
db,
|
|
key,
|
|
*,
|
|
sample_code=None,
|
|
sales='u1',
|
|
manager='m1',
|
|
region='r1',
|
|
tenant='tenant-a',
|
|
deleted=0,
|
|
created=None,
|
|
row_id=None,
|
|
):
|
|
created = created or datetime(2026, 10, 1, 8, 0)
|
|
code = f'C-{key}' if sample_code is None else sample_code
|
|
protocol = BizCollectionProtocol(
|
|
id=f'cp-{key}',
|
|
protocol_no=f'CP-{key}',
|
|
protocol_status='sxz',
|
|
tenant_no=tenant,
|
|
create_dept_id='dept-a',
|
|
create_time=created,
|
|
del_flag=deleted,
|
|
)
|
|
collection = BizProtocolCollectionInfo(
|
|
id=f'cc-{key}',
|
|
collection_protocol_id=protocol.id,
|
|
region_id=region,
|
|
sales_user_id=sales,
|
|
region_manager_user_id=manager,
|
|
sample_collect_time=created,
|
|
tenant_no=tenant,
|
|
create_dept_id='dept-a',
|
|
del_flag=deleted,
|
|
)
|
|
party = BizProtocolPartyCustomer(
|
|
id=f'pc-{key}',
|
|
collection_protocol_id=protocol.id,
|
|
mother_name=f'客户-{key}',
|
|
tenant_no=tenant,
|
|
create_dept_id='dept-a',
|
|
del_flag=deleted,
|
|
)
|
|
sample = BizProtocolSample(
|
|
id=row_id or f'cs-{key}',
|
|
collection_protocol_id=protocol.id,
|
|
sample_code=code,
|
|
sample_status='ycj',
|
|
tenant_no=tenant,
|
|
create_dept_id='dept-a',
|
|
create_time=created,
|
|
del_flag=deleted,
|
|
)
|
|
db.add_all([protocol, collection, party, sample])
|
|
db.commit()
|
|
return protocol, collection, sample
|
|
|
|
|
|
def _research_sample(
|
|
db,
|
|
key,
|
|
*,
|
|
sample_code=None,
|
|
sales='u1',
|
|
manager='m1',
|
|
region='r1',
|
|
tenant='tenant-a',
|
|
deleted=0,
|
|
created=None,
|
|
):
|
|
created = created or datetime(2026, 10, 1, 8, 0)
|
|
code = f'R-{key}' if sample_code is None else sample_code
|
|
donation = BizResearchDonation(
|
|
id=f'rd-{key}',
|
|
donation_no=f'RD-{key}',
|
|
donation_status='sxz',
|
|
tenant_no=tenant,
|
|
create_dept_id='dept-a',
|
|
create_time=created,
|
|
del_flag=deleted,
|
|
)
|
|
collection = BizResearchDonationCollection(
|
|
id=f'rc-{key}',
|
|
research_donation_id=donation.id,
|
|
region_id=region,
|
|
sales_user_id=sales,
|
|
region_manager_user_id=manager,
|
|
sample_collect_time=created,
|
|
tenant_no=tenant,
|
|
create_dept_id='dept-a',
|
|
del_flag=deleted,
|
|
)
|
|
party = BizResearchDonationParty(
|
|
id=f'rp-{key}',
|
|
research_donation_id=donation.id,
|
|
mother_name=f'科研客户-{key}',
|
|
tenant_no=tenant,
|
|
create_dept_id='dept-a',
|
|
del_flag=deleted,
|
|
)
|
|
sample = BizResearchDonationSample(
|
|
id=f'rs-{key}',
|
|
research_donation_id=donation.id,
|
|
sample_code=code,
|
|
sample_status='jcz',
|
|
tenant_no=tenant,
|
|
create_dept_id='dept-a',
|
|
create_time=created,
|
|
del_flag=deleted,
|
|
)
|
|
db.add_all([donation, collection, party, sample])
|
|
db.commit()
|
|
return donation, collection, sample
|
|
|
|
|
|
def _downstream(db, key, code, source, *, tenant='tenant-a', when=None, conclusion='tg'):
|
|
when = when or datetime(2026, 10, 2, 8, 0)
|
|
db.add_all(
|
|
[
|
|
BizSampleReception(
|
|
id=f'rec-{key}',
|
|
protocol_sample_id='stale-row-id',
|
|
collection_protocol_id='stale-parent-id',
|
|
reception_time=when,
|
|
accept_conclusion='js',
|
|
reception_source=source,
|
|
sample_code=code,
|
|
tenant_no=tenant,
|
|
create_dept_id='dept-a',
|
|
create_time=when,
|
|
del_flag=0,
|
|
),
|
|
BizQcWorkOrder(
|
|
id=f'qc-{key}',
|
|
work_order_no=f'QC-{key}',
|
|
sample_code=code,
|
|
reception_source=source,
|
|
trigger_source='reception',
|
|
work_order_status='ywc',
|
|
conclusion=conclusion,
|
|
tenant_no=tenant,
|
|
create_dept_id='dept-a',
|
|
create_time=when,
|
|
update_time=when,
|
|
del_flag=0,
|
|
),
|
|
BizStorageReleaseReview(
|
|
id=f'st-{key}',
|
|
sample_code=code,
|
|
reception_source=source,
|
|
storage_status='yrk',
|
|
storage_time=when,
|
|
tenant_no=tenant,
|
|
create_dept_id='dept-a',
|
|
create_time=when,
|
|
del_flag=0,
|
|
),
|
|
]
|
|
)
|
|
db.commit()
|
|
|
|
|
|
def test_sample_list_resolves_parent_scope_and_uses_global_stable_paging():
|
|
from app.services.market_sample import list_samples
|
|
|
|
db = _session()
|
|
base = datetime(2026, 10, 10, 8, 0)
|
|
_cell_sample(db, 'c-old', created=base)
|
|
_research_sample(db, 'r-new', created=base + timedelta(hours=3))
|
|
_cell_sample(db, 'c-mid', created=base + timedelta(hours=2))
|
|
_research_sample(db, 'r-mid', created=base + timedelta(hours=1))
|
|
_cell_sample(db, 'other-user', sales='u2', created=base + timedelta(hours=4))
|
|
_cell_sample(db, 'cross-tenant', tenant='tenant-b', created=base + timedelta(hours=5))
|
|
_research_sample(db, 'deleted', deleted=1, created=base + timedelta(hours=6))
|
|
|
|
page1, total = list_samples(db, ctx=_ctx(), user_info=_user(), page=1, page_size=2)
|
|
page2, total2 = list_samples(db, ctx=_ctx(), user_info=_user(), page=2, page_size=2)
|
|
|
|
assert total == total2 == 4
|
|
assert [item['sampleCode'] for item in page1] == ['R-r-new', 'C-c-mid']
|
|
assert [item['sampleCode'] for item in page2] == ['R-r-mid', 'C-c-old']
|
|
assert page1[0]['id'] == 'research_donation:R-r-new'
|
|
assert page1[1]['id'] == 'cell_storage:C-c-mid'
|
|
assert all('statusTone' in item and 'tagType' in item for item in page1)
|
|
|
|
|
|
def test_manager_and_dual_role_use_the_fixed_route_field_scope():
|
|
from app.services.market_sample import list_samples
|
|
|
|
db = _session()
|
|
db.add(
|
|
BizCollectionRegionStaff(
|
|
id='staff-m1',
|
|
region_id='r-managed',
|
|
user_id='m1',
|
|
is_leader=1,
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
)
|
|
)
|
|
db.commit()
|
|
_cell_sample(db, 'managed', sales='u2', manager='other', region='r-managed')
|
|
_cell_sample(db, 'assigned', sales='u2', manager='m1', region='r-other')
|
|
_cell_sample(db, 'hidden', sales='m1', manager='other', region='r-other')
|
|
|
|
manager_items, _ = list_samples(db, ctx=_ctx('manager', 'm1'), user_info=_user('m1'))
|
|
specialist_items, _ = list_samples(
|
|
db, ctx=_ctx('specialist', 'm1'), user_info=_user('m1')
|
|
)
|
|
|
|
assert {item['sampleCode'] for item in manager_items} == {'C-managed', 'C-assigned'}
|
|
assert {item['sampleCode'] for item in specialist_items} == {'C-hidden'}
|
|
|
|
|
|
def test_detail_matches_downstream_only_by_tenant_code_and_source_and_latest():
|
|
from app.services.market_sample import get_sample_detail
|
|
|
|
db = _session()
|
|
_cell_sample(db, 'same', sample_code='SAME', row_id='rebuilt-row-id')
|
|
_research_sample(db, 'same', sample_code='SAME')
|
|
old = datetime(2026, 10, 1, 8, 0)
|
|
new = datetime(2026, 10, 2, 8, 0)
|
|
_downstream(db, 'cell-old', 'SAME', 'cell_storage', when=old, conclusion='btg')
|
|
_downstream(db, 'cell-new', 'SAME', 'cell_storage', when=new, conclusion='tg')
|
|
_downstream(db, 'research', 'SAME', 'research_donation', when=new, conclusion='btg')
|
|
_downstream(db, 'cross', 'SAME', 'cell_storage', tenant='tenant-b', when=new + timedelta(days=1), conclusion='btg')
|
|
|
|
cell = get_sample_detail(
|
|
db, sample_id='cell_storage:SAME', ctx=_ctx(), user_info=_user()
|
|
)
|
|
research = get_sample_detail(
|
|
db, sample_id='research_donation:SAME', ctx=_ctx(), user_info=_user()
|
|
)
|
|
|
|
assert cell['card']['sampleRowId'] == 'rebuilt-row-id'
|
|
assert cell['progress']['qc']['code'] == 'tg'
|
|
assert research['progress']['qc']['code'] == 'btg'
|
|
assert cell['progress']['reception']['recordId'] == 'rec-cell-new'
|
|
assert cell['progress']['storage']['recordId'] == 'st-cell-new'
|
|
assert cell['sections']
|
|
|
|
|
|
def test_detail_idor_and_malformed_ids_are_not_exposed():
|
|
from app.services.market_sample import get_sample_detail
|
|
|
|
db = _session()
|
|
_cell_sample(db, 'other', sales='u2')
|
|
_cell_sample(db, 'cross', tenant='tenant-b')
|
|
|
|
for sample_id in ('cell_storage:C-other', 'cell_storage:C-cross', 'bad', 'evil:C-other'):
|
|
with pytest.raises(HTTPException) as exc:
|
|
get_sample_detail(db, sample_id=sample_id, ctx=_ctx(), user_info=_user())
|
|
assert exc.value.status_code == 404
|
|
|
|
|
|
def test_empty_sample_code_has_basic_read_only_detail_without_downstream():
|
|
from app.services.market_sample import get_sample_detail, list_samples
|
|
|
|
db = _session()
|
|
_cell_sample(db, 'blank', sample_code='')
|
|
_downstream(db, 'unrelated', 'C-other', 'cell_storage')
|
|
|
|
items, total = list_samples(db, ctx=_ctx(), user_info=_user())
|
|
assert total == 1
|
|
assert items[0]['id'] == 'cell_storage:@id:cs-blank'
|
|
detail = get_sample_detail(db, sample_id=items[0]['id'], ctx=_ctx(), user_info=_user())
|
|
assert detail['card']['sampleCode'] == ''
|
|
assert detail['actions'] == []
|
|
assert detail['progress']['reception']['code'] == 'none'
|
|
|
|
|
|
def test_cell_lightweight_storage_cert_and_insurance_writes_validate_handler():
|
|
from app.services.market_sample import update_insurance, update_storage_cert
|
|
|
|
db = _session()
|
|
protocol, _, _ = _cell_sample(db, 'write')
|
|
db.add_all(
|
|
[
|
|
SysUser(
|
|
id='handler',
|
|
user_no='handler',
|
|
nick_name='办理人',
|
|
email='',
|
|
phone_no='',
|
|
status='1',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
),
|
|
BizCollectionRegionStaff(
|
|
id='staff-handler-write',
|
|
region_id='r1',
|
|
user_id='handler',
|
|
is_leader=0,
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
),
|
|
SysUser(
|
|
id='disabled',
|
|
user_no='disabled',
|
|
nick_name='停用人',
|
|
email='',
|
|
phone_no='',
|
|
status='0',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
),
|
|
]
|
|
)
|
|
db.commit()
|
|
when = '2026-10-11 09:30:00'
|
|
cert = update_storage_cert(
|
|
db,
|
|
sample_id='cell_storage:C-write',
|
|
fields={
|
|
'handleTime': when,
|
|
'handlerUserId': 'handler',
|
|
'attachments': 'a.jpg,b.jpg',
|
|
'remark': '证书备注',
|
|
},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
insurance = update_insurance(
|
|
db,
|
|
sample_id='cell_storage:C-write',
|
|
fields={
|
|
'handleTime': when,
|
|
'handlerUserId': 'handler',
|
|
'attachments': 'i.jpg',
|
|
'remark': '保险备注',
|
|
},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
cert_row = db.query(BizProtocolStorageCert).filter_by(collection_protocol_id=protocol.id).one()
|
|
todo_row = db.query(BizProtocolFullmoonTodo).filter_by(collection_protocol_id=protocol.id).one()
|
|
assert cert['handlerName'] == insurance['handlerName'] == '办理人'
|
|
assert cert_row.cert_image_urls == 'a.jpg,b.jpg'
|
|
assert cert_row.remark == '证书备注'
|
|
assert todo_row.insurance_attachments == 'i.jpg'
|
|
assert todo_row.handle_remark == '保险备注'
|
|
|
|
with pytest.raises(ValueError, match='办理人员'):
|
|
update_storage_cert(
|
|
db,
|
|
sample_id='cell_storage:C-write',
|
|
fields={'handlerUserId': 'disabled'},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
|
|
def test_sample_form_options_return_only_enabled_staff_in_scoped_document_region():
|
|
from app.services.market_sample import get_sample_form_options
|
|
|
|
db = _session()
|
|
_cell_sample(db, 'handler-options', region='r1')
|
|
db.add_all(
|
|
[
|
|
SysUser(id='eligible', user_no='eligible', nick_name='可办理', email='', phone_no='', status='1', tenant_no='tenant-a', create_dept_id='', del_flag=0),
|
|
SysUser(id='other-region', user_no='other-region', nick_name='其他地区', email='', phone_no='', status='1', tenant_no='tenant-a', create_dept_id='', del_flag=0),
|
|
SysUser(id='disabled-staff', user_no='disabled-staff', nick_name='停用人员', email='', phone_no='', status='0', tenant_no='tenant-a', create_dept_id='', del_flag=0),
|
|
SysUser(id='cross-tenant-staff', user_no='cross-tenant-staff', nick_name='跨租户', email='', phone_no='', status='1', tenant_no='tenant-b', create_dept_id='', del_flag=0),
|
|
BizCollectionRegionStaff(id='region-eligible', region_id='r1', user_id='eligible', is_leader=0, tenant_no='tenant-a', create_dept_id='', del_flag=0),
|
|
BizCollectionRegionStaff(id='region-other', region_id='r2', user_id='other-region', is_leader=0, tenant_no='tenant-a', create_dept_id='', del_flag=0),
|
|
BizCollectionRegionStaff(id='region-disabled', region_id='r1', user_id='disabled-staff', is_leader=0, tenant_no='tenant-a', create_dept_id='', del_flag=0),
|
|
BizCollectionRegionStaff(id='region-cross', region_id='r1', user_id='cross-tenant-staff', is_leader=0, tenant_no='tenant-b', create_dept_id='', del_flag=0),
|
|
]
|
|
)
|
|
db.commit()
|
|
|
|
result = get_sample_form_options(
|
|
db,
|
|
sample_id='cell_storage:C-handler-options',
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
assert result == {'staff': [{'value': 'eligible', 'label': '可办理'}]}
|
|
|
|
|
|
def test_sample_detail_and_options_resolve_only_referenced_historical_handlers():
|
|
from app.services.market_sample import get_sample_detail, get_sample_form_options
|
|
|
|
db = _session()
|
|
protocol, _, sample = _cell_sample(db, 'historical-handlers', region='r1')
|
|
db.add_all(
|
|
[
|
|
SysUser(id='eligible', user_no='eligible-no', nick_name='当前可办理', email='', phone_no='', status='1', tenant_no='tenant-a', create_dept_id='', del_flag=0),
|
|
SysUser(id='disabled-history', user_no='disabled-no', nick_name='历史停用人员', email='', phone_no='', status='0', tenant_no='tenant-a', create_dept_id='', del_flag=0),
|
|
SysUser(id='deleted-history', user_no='deleted-no', nick_name='历史删除人员', email='', phone_no='', status='1', tenant_no='tenant-a', create_dept_id='', del_flag=1),
|
|
SysUser(id='unrelated-history', user_no='unrelated-no', nick_name='无关历史人员', email='', phone_no='', status='0', tenant_no='tenant-a', create_dept_id='', del_flag=1),
|
|
SysUser(id='cross-history', user_no='cross-no', nick_name='跨租户历史人员', email='', phone_no='', status='0', tenant_no='tenant-b', create_dept_id='', del_flag=1),
|
|
BizCollectionRegionStaff(id='region-eligible-history', region_id='r1', user_id='eligible', is_leader=0, tenant_no='tenant-a', create_dept_id='', del_flag=0),
|
|
BizProtocolStorageCert(id='cert-history', collection_protocol_id=protocol.id, handler_user_id='disabled-history', tenant_no='tenant-a', create_dept_id='', del_flag=0),
|
|
BizProtocolFullmoonTodo(id='todo-history', collection_protocol_id=protocol.id, handler_user_id='deleted-history', tenant_no='tenant-a', create_dept_id='', del_flag=0),
|
|
BizSampleReception(id='reception-history', protocol_sample_id=sample.id, collection_protocol_id=protocol.id, reception_time=datetime(2026, 10, 2, 8, 0), accept_conclusion='js', reception_source='cell_storage', sample_code=sample.sample_code, lab_storage_code='LAB-2026-001', tenant_no='tenant-a', create_dept_id='', del_flag=0),
|
|
]
|
|
)
|
|
db.commit()
|
|
|
|
detail = get_sample_detail(
|
|
db,
|
|
sample_id='cell_storage:C-historical-handlers',
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
handles = next(
|
|
section for section in detail['sections'] if section['key'] == 'market-handles'
|
|
)
|
|
basic = next(section for section in detail['sections'] if section['key'] == 'basic')
|
|
storage_options = get_sample_form_options(
|
|
db,
|
|
sample_id='cell_storage:C-historical-handlers',
|
|
action='storage-cert',
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
insurance_options = get_sample_form_options(
|
|
db,
|
|
sample_id='cell_storage:C-historical-handlers',
|
|
action='insurance',
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
default_options = get_sample_form_options(
|
|
db,
|
|
sample_id='cell_storage:C-historical-handlers',
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
assert handles['storageCert']['handlerUserName'] == '历史停用人员'
|
|
assert handles['insurance']['handlerUserName'] == '历史删除人员'
|
|
assert {'label': '实验室入库编码', 'value': 'LAB-2026-001'} in basic['fields']
|
|
assert {item['value']: item['label'] for item in storage_options['staff']} == {
|
|
'eligible': '当前可办理',
|
|
'disabled-history': '历史停用人员',
|
|
}
|
|
assert {item['value']: item['label'] for item in insurance_options['staff']} == {
|
|
'eligible': '当前可办理',
|
|
'deleted-history': '历史删除人员',
|
|
}
|
|
assert default_options == {
|
|
'staff': [{'value': 'eligible', 'label': '当前可办理'}]
|
|
}
|
|
|
|
|
|
def test_sample_handling_form_options_route_is_not_registered():
|
|
from app.api import market_mobile
|
|
|
|
paths = {route.path for route in market_mobile.router.routes}
|
|
assert '/mini/market/specialist/samples/form-options' not in paths
|
|
assert '/mini/market/manager/samples/form-options' not in paths
|
|
|
|
|
|
def test_sample_list_batches_referenced_handler_name_lookup():
|
|
from app.services.market_sample import list_samples
|
|
|
|
db = _session()
|
|
db.add(
|
|
SysUser(id='history-handler', user_no='history-no', nick_name='历史办理人', email='', phone_no='', status='0', tenant_no='tenant-a', create_dept_id='', del_flag=1)
|
|
)
|
|
for index in range(5):
|
|
protocol, _, _ = _cell_sample(db, f'handler-batch-{index}')
|
|
db.add(
|
|
BizProtocolStorageCert(
|
|
id=f'cert-handler-batch-{index}',
|
|
collection_protocol_id=protocol.id,
|
|
handler_user_id='history-handler',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
)
|
|
)
|
|
db.commit()
|
|
statements = []
|
|
|
|
def capture_statement(_conn, _cursor, statement, _parameters, _context, _many):
|
|
statements.append(statement)
|
|
|
|
event.listen(db.get_bind(), 'before_cursor_execute', capture_statement)
|
|
try:
|
|
items, total = list_samples(db, ctx=_ctx(), user_info=_user(), page_size=10)
|
|
finally:
|
|
event.remove(db.get_bind(), 'before_cursor_execute', capture_statement)
|
|
|
|
assert total == len(items) == 5
|
|
assert {item['storageCert']['handlerUserName'] for item in items} == {'历史办理人'}
|
|
assert sum('FROM sys_user' in statement for statement in statements) == 1
|
|
|
|
|
|
def test_sample_handler_must_be_enabled_staff_in_current_document_region():
|
|
from app.services.market_sample import update_storage_cert
|
|
|
|
db = _session()
|
|
_cell_sample(db, 'handler-region', region='r1')
|
|
db.add_all(
|
|
[
|
|
SysUser(id='wrong-region', user_no='wrong-region', nick_name='错区人员', email='', phone_no='', status='1', tenant_no='tenant-a', create_dept_id='', del_flag=0),
|
|
BizCollectionRegionStaff(id='wrong-region-link', region_id='r2', user_id='wrong-region', is_leader=0, tenant_no='tenant-a', create_dept_id='', del_flag=0),
|
|
]
|
|
)
|
|
db.commit()
|
|
|
|
with pytest.raises(ValueError, match='办理人员'):
|
|
update_storage_cert(
|
|
db,
|
|
sample_id='cell_storage:C-handler-region',
|
|
fields={'handleTime': '2026-10-11 09:30:00', 'handlerUserId': 'wrong-region'},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
assert db.query(BizProtocolStorageCert).count() == 0
|
|
|
|
|
|
def test_research_and_empty_code_samples_are_read_only_and_scope_is_rechecked():
|
|
from app.services.market_sample import update_storage_cert
|
|
|
|
db = _session()
|
|
_research_sample(db, 'readonly')
|
|
_, collection, _ = _cell_sample(db, 'moved')
|
|
_cell_sample(db, 'blank', sample_code='')
|
|
|
|
for sample_id in ('research_donation:R-readonly', 'cell_storage:@id:cs-blank'):
|
|
with pytest.raises(ValueError, match='不支持'):
|
|
update_storage_cert(
|
|
db,
|
|
sample_id=sample_id,
|
|
fields={'remark': 'x'},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
collection.sales_user_id = 'u2'
|
|
db.commit()
|
|
with pytest.raises(HTTPException) as exc:
|
|
update_storage_cert(
|
|
db,
|
|
sample_id='cell_storage:C-moved',
|
|
fields={'remark': 'x'},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
assert exc.value.status_code == 404
|
|
|
|
|
|
def test_lightweight_dtos_forbid_status_qc_production_lab_and_sample_fields():
|
|
from app.schemas.market_mobile import (
|
|
MarketSampleInsuranceBody,
|
|
MarketSampleStorageCertBody,
|
|
)
|
|
|
|
allowed = {
|
|
'handleTime': '2026-10-11 09:30:00',
|
|
'handlerUserId': 'u1',
|
|
'attachments': 'a.jpg',
|
|
'remark': 'ok',
|
|
}
|
|
MarketSampleStorageCertBody.model_validate(allowed)
|
|
MarketSampleInsuranceBody.model_validate(allowed)
|
|
|
|
for forbidden in (
|
|
'sampleCode',
|
|
'sampleStatus',
|
|
'storageStatus',
|
|
'qcConclusion',
|
|
'productionResult',
|
|
'labStorageCode',
|
|
'makeStatus',
|
|
'todoStatus',
|
|
'tenantNo',
|
|
):
|
|
with pytest.raises(ValidationError):
|
|
MarketSampleStorageCertBody.model_validate({**allowed, forbidden: 'bad'})
|
|
|
|
|
|
def test_write_rolls_back_when_flush_fails(monkeypatch):
|
|
from app.services.market_sample import update_storage_cert
|
|
|
|
db = _session()
|
|
_cell_sample(db, 'rollback')
|
|
original_flush = db.flush
|
|
|
|
def fail_flush(*args, **kwargs):
|
|
original_flush(*args, **kwargs)
|
|
raise RuntimeError('injected failure')
|
|
|
|
monkeypatch.setattr(db, 'flush', fail_flush)
|
|
with pytest.raises(RuntimeError, match='injected failure'):
|
|
update_storage_cert(
|
|
db,
|
|
sample_id='cell_storage:C-rollback',
|
|
fields={'remark': 'must rollback'},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
monkeypatch.setattr(db, 'flush', original_flush)
|
|
assert db.query(BizProtocolStorageCert).count() == 0
|
|
|
|
|
|
def test_list_batch_enrichment_has_bounded_query_count():
|
|
from app.services.market_sample import list_samples
|
|
|
|
db = _session()
|
|
for index in range(8):
|
|
source = 'cell_storage' if index % 2 == 0 else 'research_donation'
|
|
if source == 'cell_storage':
|
|
_cell_sample(db, f'q{index}')
|
|
code = f'C-q{index}'
|
|
else:
|
|
_research_sample(db, f'q{index}')
|
|
code = f'R-q{index}'
|
|
_downstream(db, f'q{index}', code, source)
|
|
|
|
statements = []
|
|
|
|
def count_statement(*args):
|
|
statements.append(args[2])
|
|
|
|
event.listen(db.bind, 'before_cursor_execute', count_statement)
|
|
try:
|
|
items, total = list_samples(db, ctx=_ctx(), user_info=_user(), page_size=20)
|
|
finally:
|
|
event.remove(db.bind, 'before_cursor_execute', count_statement)
|
|
|
|
assert total == len(items) == 8
|
|
assert len(statements) <= 16
|
|
|
|
|
|
def test_market_sample_routes_are_not_exposed_to_market_roles():
|
|
from app.api.market_mobile import router
|
|
|
|
paths = {route.path for route in router.routes}
|
|
for kind in ('specialist', 'manager'):
|
|
base = f'/mini/market/{kind}/samples'
|
|
assert not any(path.startswith(base) for path in paths)
|
|
|
|
|
|
def test_global_identity_query_deduplicates_legacy_duplicate_child_rows():
|
|
from app.services.market_sample import list_samples
|
|
|
|
db = _session()
|
|
protocol, _, _ = _cell_sample(db, 'duplicate-child')
|
|
db.add(
|
|
BizProtocolCollectionInfo(
|
|
id='cc-duplicate-child-legacy',
|
|
collection_protocol_id=protocol.id,
|
|
region_id='r1',
|
|
sales_user_id='u1',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
)
|
|
)
|
|
db.commit()
|
|
|
|
items, total = list_samples(db, ctx=_ctx(), user_info=_user())
|
|
|
|
assert total == len(items) == 1
|
|
|
|
|
|
def test_partial_handle_update_preserves_omitted_lightweight_fields():
|
|
from app.services.market_sample import update_storage_cert
|
|
|
|
db = _session()
|
|
_cell_sample(db, 'partial')
|
|
db.add(
|
|
SysUser(
|
|
id='handler',
|
|
user_no='handler',
|
|
nick_name='办理人',
|
|
email='',
|
|
phone_no='',
|
|
status='1',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
)
|
|
)
|
|
_assign_region_staff(db, 'handler', 'partial')
|
|
db.commit()
|
|
update_storage_cert(
|
|
db,
|
|
sample_id='cell_storage:C-partial',
|
|
fields={
|
|
'handleTime': '2026-10-11 09:30:00',
|
|
'handlerUserId': 'handler',
|
|
'attachments': 'old.jpg',
|
|
'remark': '旧备注',
|
|
},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
update_storage_cert(
|
|
db,
|
|
sample_id='cell_storage:C-partial',
|
|
fields={'remark': '新备注'},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
row = db.query(BizProtocolStorageCert).one()
|
|
assert _fmt(row.handle_time) == '2026-10-11 09:30:00'
|
|
assert row.handler_user_id == 'handler'
|
|
assert row.cert_image_urls == 'old.jpg'
|
|
assert row.remark == '新备注'
|
|
|
|
|
|
def _fmt(value):
|
|
return value.strftime('%Y-%m-%d %H:%M:%S') if value else ''
|
|
|
|
|
|
def test_materialized_collection_row_itself_must_be_inside_fixed_scope():
|
|
from app.services.market_sample import list_samples
|
|
|
|
db = _session()
|
|
protocol, visible, _ = _cell_sample(db, 'mixed-collection', region='visible-region')
|
|
db.add(
|
|
BizProtocolCollectionInfo(
|
|
id='cc-mixed-hidden',
|
|
collection_protocol_id=protocol.id,
|
|
region_id='hidden-region',
|
|
sales_user_id='u2',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
)
|
|
)
|
|
db.commit()
|
|
|
|
items, total = list_samples(db, ctx=_ctx(), user_info=_user())
|
|
|
|
assert total == 1
|
|
assert items[0]['regionId'] == visible.region_id
|
|
|
|
|
|
def test_progress_uses_normalized_codes_and_explicit_semantic_tones():
|
|
from app.services.market_sample import get_sample_detail
|
|
|
|
db = _session()
|
|
_cell_sample(db, 'tone')
|
|
detail = get_sample_detail(
|
|
db, sample_id='cell_storage:C-tone', ctx=_ctx(), user_info=_user()
|
|
)
|
|
|
|
assert detail['progress']['collection'] == {
|
|
'code': 'collected',
|
|
'label': '已采集',
|
|
'statusTone': 'success',
|
|
'tagType': 'success',
|
|
}
|
|
assert detail['progress']['reception']['statusTone'] == 'info'
|
|
assert detail['progress']['qc']['statusTone'] == 'warning'
|
|
|
|
|
|
def test_legacy_storage_cert_and_insurance_statuses_are_read_as_handled():
|
|
from app.services.market_sample import get_sample_detail
|
|
|
|
db = _session()
|
|
protocol, _, _ = _cell_sample(db, 'legacy-handled')
|
|
protocol_alt, _, _ = _cell_sample(db, 'legacy-handled-alt')
|
|
db.add_all(
|
|
[
|
|
BizProtocolStorageCert(
|
|
id='cert-legacy',
|
|
collection_protocol_id=protocol.id,
|
|
make_status='0',
|
|
make_date='20261001',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
),
|
|
BizProtocolFullmoonTodo(
|
|
id='todo-legacy',
|
|
collection_protocol_id=protocol.id,
|
|
todo_status='1',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
),
|
|
BizProtocolStorageCert(
|
|
id='cert-legacy-status',
|
|
collection_protocol_id=protocol_alt.id,
|
|
make_status='1',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
),
|
|
BizProtocolFullmoonTodo(
|
|
id='todo-legacy-date',
|
|
collection_protocol_id=protocol_alt.id,
|
|
todo_status='0',
|
|
handle_date='20261002',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
),
|
|
]
|
|
)
|
|
db.commit()
|
|
|
|
detail = get_sample_detail(
|
|
db,
|
|
sample_id='cell_storage:C-legacy-handled',
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
section = next(item for item in detail['sections'] if item['key'] == 'market-handles')
|
|
assert section['storageCert']['code'] == 'handled'
|
|
assert section['storageCert']['statusTone'] == 'success'
|
|
assert section['insurance']['code'] == 'handled'
|
|
assert section['insurance']['statusTone'] == 'success'
|
|
alt = get_sample_detail(
|
|
db,
|
|
sample_id='cell_storage:C-legacy-handled-alt',
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
alt_section = next(
|
|
item for item in alt['sections'] if item['key'] == 'market-handles'
|
|
)
|
|
assert alt_section['storageCert']['code'] == 'handled'
|
|
assert alt_section['insurance']['code'] == 'handled'
|
|
|
|
|
|
def test_setting_handle_time_synchronizes_legacy_status_and_date_for_both_actions():
|
|
from app.services.market_sample import update_insurance, update_storage_cert
|
|
|
|
db = _session()
|
|
_cell_sample(db, 'sync-set')
|
|
db.add(
|
|
SysUser(
|
|
id='handler',
|
|
user_no='handler',
|
|
nick_name='办理人',
|
|
email='',
|
|
phone_no='',
|
|
status='1',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
)
|
|
)
|
|
_assign_region_staff(db, 'handler', 'sync-set')
|
|
db.commit()
|
|
fields = {
|
|
'handleTime': '2026-10-11 09:30:00',
|
|
'handlerUserId': 'handler',
|
|
'attachments': 'keep.jpg',
|
|
'remark': '保留备注',
|
|
}
|
|
|
|
update_storage_cert(
|
|
db,
|
|
sample_id='cell_storage:C-sync-set',
|
|
fields=fields,
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
update_insurance(
|
|
db,
|
|
sample_id='cell_storage:C-sync-set',
|
|
fields={**fields, 'attachments': 'keep-insurance.jpg'},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
cert = db.query(BizProtocolStorageCert).one()
|
|
todo = db.query(BizProtocolFullmoonTodo).one()
|
|
assert (cert.make_status, cert.make_date) == ('1', '20261011')
|
|
assert (todo.todo_status, todo.handle_date) == ('1', '20261011')
|
|
|
|
|
|
def test_clearing_handle_time_clears_handler_and_legacy_status_but_preserves_content():
|
|
from app.services.market_sample import update_insurance, update_storage_cert
|
|
|
|
db = _session()
|
|
_cell_sample(db, 'sync-clear')
|
|
db.add(
|
|
SysUser(
|
|
id='handler',
|
|
user_no='handler',
|
|
nick_name='办理人',
|
|
email='',
|
|
phone_no='',
|
|
status='1',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
)
|
|
)
|
|
_assign_region_staff(db, 'handler', 'sync-clear')
|
|
db.commit()
|
|
initial = {
|
|
'handleTime': '2026-10-11 09:30:00',
|
|
'handlerUserId': 'handler',
|
|
'attachments': 'keep.jpg',
|
|
'remark': '保留备注',
|
|
}
|
|
for updater, attachment in (
|
|
(update_storage_cert, 'keep.jpg'),
|
|
(update_insurance, 'keep-insurance.jpg'),
|
|
):
|
|
updater(
|
|
db,
|
|
sample_id='cell_storage:C-sync-clear',
|
|
fields={**initial, 'attachments': attachment},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
updater(
|
|
db,
|
|
sample_id='cell_storage:C-sync-clear',
|
|
fields={'handleTime': None, 'handlerUserId': 'handler'},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
cert = db.query(BizProtocolStorageCert).one()
|
|
todo = db.query(BizProtocolFullmoonTodo).one()
|
|
assert (cert.handle_time, cert.handler_user_id, cert.make_status, cert.make_date) == (
|
|
None,
|
|
None,
|
|
'0',
|
|
None,
|
|
)
|
|
assert (todo.handle_time, todo.handler_user_id, todo.todo_status, todo.handle_date) == (
|
|
None,
|
|
None,
|
|
'0',
|
|
None,
|
|
)
|
|
assert (cert.cert_image_urls, cert.remark) == ('keep.jpg', '保留备注')
|
|
assert (todo.insurance_attachments, todo.handle_remark) == (
|
|
'keep-insurance.jpg',
|
|
'保留备注',
|
|
)
|
|
|
|
|
|
def test_partial_insurance_update_preserves_omitted_lightweight_fields():
|
|
from app.services.market_sample import update_insurance
|
|
|
|
db = _session()
|
|
_cell_sample(db, 'partial-insurance')
|
|
db.add(
|
|
SysUser(
|
|
id='handler',
|
|
user_no='handler',
|
|
nick_name='办理人',
|
|
email='',
|
|
phone_no='',
|
|
status='1',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
)
|
|
)
|
|
_assign_region_staff(db, 'handler', 'partial-insurance')
|
|
db.commit()
|
|
update_insurance(
|
|
db,
|
|
sample_id='cell_storage:C-partial-insurance',
|
|
fields={
|
|
'handleTime': '2026-10-11 09:30:00',
|
|
'handlerUserId': 'handler',
|
|
'attachments': 'old.jpg',
|
|
'remark': '旧备注',
|
|
},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
update_insurance(
|
|
db,
|
|
sample_id='cell_storage:C-partial-insurance',
|
|
fields={'remark': '新备注'},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
row = db.query(BizProtocolFullmoonTodo).one()
|
|
assert _fmt(row.handle_time) == '2026-10-11 09:30:00'
|
|
assert row.handler_user_id == 'handler'
|
|
assert row.insurance_attachments == 'old.jpg'
|
|
assert row.handle_remark == '新备注'
|
|
|
|
|
|
def test_partial_update_preserves_legacy_handled_fields_when_time_is_omitted():
|
|
from app.services.market_sample import update_insurance, update_storage_cert
|
|
|
|
db = _session()
|
|
protocol, _, _ = _cell_sample(db, 'partial-legacy')
|
|
db.add_all(
|
|
[
|
|
SysUser(
|
|
id='handler',
|
|
user_no='handler',
|
|
nick_name='办理人',
|
|
email='',
|
|
phone_no='',
|
|
status='1',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
),
|
|
BizProtocolStorageCert(
|
|
id='cert-partial-legacy',
|
|
collection_protocol_id=protocol.id,
|
|
make_status='1',
|
|
make_date='20261001',
|
|
handler_user_id='handler',
|
|
cert_image_urls='cert-old.jpg',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
),
|
|
BizProtocolFullmoonTodo(
|
|
id='todo-partial-legacy',
|
|
collection_protocol_id=protocol.id,
|
|
todo_status='1',
|
|
handle_date='20261002',
|
|
handler_user_id='handler',
|
|
insurance_attachments='insurance-old.jpg',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
),
|
|
]
|
|
)
|
|
db.commit()
|
|
|
|
update_storage_cert(
|
|
db,
|
|
sample_id='cell_storage:C-partial-legacy',
|
|
fields={'remark': '证书新备注'},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
update_insurance(
|
|
db,
|
|
sample_id='cell_storage:C-partial-legacy',
|
|
fields={'remark': '保险新备注'},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
cert = db.query(BizProtocolStorageCert).one()
|
|
todo = db.query(BizProtocolFullmoonTodo).one()
|
|
assert (cert.make_status, cert.make_date, cert.handler_user_id) == (
|
|
'1',
|
|
'20261001',
|
|
'handler',
|
|
)
|
|
assert cert.cert_image_urls == 'cert-old.jpg'
|
|
assert (todo.todo_status, todo.handle_date, todo.handler_user_id) == (
|
|
'1',
|
|
'20261002',
|
|
'handler',
|
|
)
|
|
assert todo.insurance_attachments == 'insurance-old.jpg'
|
|
|
|
|
|
def test_handler_only_update_changes_legacy_handled_records_without_clearing_status():
|
|
from app.services.market_sample import update_insurance, update_storage_cert
|
|
|
|
db = _session()
|
|
protocol, _, _ = _cell_sample(db, 'handler-only-legacy')
|
|
db.add_all(
|
|
[
|
|
SysUser(
|
|
id='new-handler',
|
|
user_no='new-handler',
|
|
nick_name='新办理人',
|
|
email='',
|
|
phone_no='',
|
|
status='1',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
),
|
|
BizProtocolStorageCert(
|
|
id='cert-handler-only',
|
|
collection_protocol_id=protocol.id,
|
|
make_status='1',
|
|
make_date='20261001',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
),
|
|
BizProtocolFullmoonTodo(
|
|
id='todo-handler-only',
|
|
collection_protocol_id=protocol.id,
|
|
todo_status='0',
|
|
handle_date='20261002',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
),
|
|
]
|
|
)
|
|
_assign_region_staff(db, 'new-handler', 'handler-only-legacy')
|
|
db.commit()
|
|
|
|
for updater in (update_storage_cert, update_insurance):
|
|
result = updater(
|
|
db,
|
|
sample_id='cell_storage:C-handler-only-legacy',
|
|
fields={'handlerUserId': 'new-handler'},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
assert result['handlerName'] == '新办理人'
|
|
|
|
cert = db.query(BizProtocolStorageCert).one()
|
|
todo = db.query(BizProtocolFullmoonTodo).one()
|
|
assert (cert.handler_user_id, cert.make_status, cert.make_date) == (
|
|
'new-handler',
|
|
'1',
|
|
'20261001',
|
|
)
|
|
assert (todo.handler_user_id, todo.todo_status, todo.handle_date) == (
|
|
'new-handler',
|
|
'0',
|
|
'20261002',
|
|
)
|
|
|
|
|
|
def test_handler_only_update_rejects_truly_unhandled_cert_and_insurance():
|
|
from app.services.market_sample import update_insurance, update_storage_cert
|
|
|
|
db = _session()
|
|
protocol, _, _ = _cell_sample(db, 'handler-only-unhandled')
|
|
db.add_all(
|
|
[
|
|
SysUser(
|
|
id='handler',
|
|
user_no='handler',
|
|
nick_name='办理人',
|
|
email='',
|
|
phone_no='',
|
|
status='1',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
),
|
|
BizProtocolStorageCert(
|
|
id='cert-unhandled',
|
|
collection_protocol_id=protocol.id,
|
|
make_status='0',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
),
|
|
BizProtocolFullmoonTodo(
|
|
id='todo-unhandled',
|
|
collection_protocol_id=protocol.id,
|
|
todo_status='0',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
),
|
|
]
|
|
)
|
|
db.commit()
|
|
|
|
for updater in (update_storage_cert, update_insurance):
|
|
with pytest.raises(ValueError, match='未办理'):
|
|
updater(
|
|
db,
|
|
sample_id='cell_storage:C-handler-only-unhandled',
|
|
fields={'handlerUserId': 'handler'},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
|
|
def test_historical_disabled_handler_does_not_block_content_only_updates():
|
|
from app.services.market_sample import update_insurance, update_storage_cert
|
|
|
|
db = _session()
|
|
protocol, _, _ = _cell_sample(db, 'disabled-history')
|
|
db.add_all(
|
|
[
|
|
SysUser(
|
|
id='disabled-history-handler',
|
|
user_no='disabled-history-handler',
|
|
nick_name='历史办理人',
|
|
email='',
|
|
phone_no='',
|
|
status='0',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=1,
|
|
),
|
|
BizProtocolStorageCert(
|
|
id='cert-disabled-history',
|
|
collection_protocol_id=protocol.id,
|
|
handle_time=datetime(2026, 10, 1, 8, 0),
|
|
handler_user_id='disabled-history-handler',
|
|
make_status='1',
|
|
make_date='20261001',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
),
|
|
BizProtocolFullmoonTodo(
|
|
id='todo-disabled-history',
|
|
collection_protocol_id=protocol.id,
|
|
handle_time=datetime(2026, 10, 2, 8, 0),
|
|
handler_user_id='disabled-history-handler',
|
|
todo_status='1',
|
|
handle_date='20261002',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
),
|
|
]
|
|
)
|
|
db.commit()
|
|
|
|
update_storage_cert(
|
|
db,
|
|
sample_id='cell_storage:C-disabled-history',
|
|
fields={'attachments': 'cert-new.jpg', 'remark': '证书新备注'},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
update_insurance(
|
|
db,
|
|
sample_id='cell_storage:C-disabled-history',
|
|
fields={'attachments': 'insurance-new.jpg', 'remark': '保险新备注'},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
cert = db.query(BizProtocolStorageCert).one()
|
|
todo = db.query(BizProtocolFullmoonTodo).one()
|
|
assert cert.handler_user_id == todo.handler_user_id == 'disabled-history-handler'
|
|
assert (cert.cert_image_urls, cert.remark) == ('cert-new.jpg', '证书新备注')
|
|
assert (todo.insurance_attachments, todo.handle_remark) == (
|
|
'insurance-new.jpg',
|
|
'保险新备注',
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize('handler_state', ['disabled', 'deleted', 'out-of-region'])
|
|
@pytest.mark.parametrize('kind', ['storage-cert', 'insurance'])
|
|
def test_full_unchanged_historical_handler_payload_allows_content_update(
|
|
handler_state, kind
|
|
):
|
|
from app.services.market_sample import update_insurance, update_storage_cert
|
|
|
|
db = _session()
|
|
protocol, _, _ = _cell_sample(db, f'historical-full-{handler_state}-{kind}')
|
|
handler_id = f'historical-{handler_state}-{kind}'
|
|
handle_time = datetime(2026, 10, 1, 8, 0)
|
|
user = SysUser(
|
|
id=handler_id,
|
|
user_no=handler_id,
|
|
nick_name='历史办理人',
|
|
email='',
|
|
phone_no='',
|
|
status='0' if handler_state == 'disabled' else '1',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=1 if handler_state == 'deleted' else 0,
|
|
)
|
|
staff = BizCollectionRegionStaff(
|
|
id=f'staff-{handler_id}',
|
|
region_id='r2' if handler_state == 'out-of-region' else 'r1',
|
|
user_id=handler_id,
|
|
is_leader=0,
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
)
|
|
db.add_all([user, staff])
|
|
if kind == 'storage-cert':
|
|
db.add(
|
|
BizProtocolStorageCert(
|
|
id=f'cert-{handler_id}',
|
|
collection_protocol_id=protocol.id,
|
|
handle_time=handle_time,
|
|
handler_user_id=handler_id,
|
|
cert_image_urls='cert-old.jpg',
|
|
make_status='1',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
)
|
|
)
|
|
updater = update_storage_cert
|
|
attachments = 'cert-new.jpg'
|
|
else:
|
|
db.add(
|
|
BizProtocolFullmoonTodo(
|
|
id=f'todo-{handler_id}',
|
|
collection_protocol_id=protocol.id,
|
|
handle_time=handle_time,
|
|
handler_user_id=handler_id,
|
|
insurance_attachments='insurance-old.jpg',
|
|
todo_status='1',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
)
|
|
)
|
|
updater = update_insurance
|
|
attachments = 'insurance-new.jpg'
|
|
db.commit()
|
|
|
|
result = updater(
|
|
db,
|
|
sample_id=f'cell_storage:C-historical-full-{handler_state}-{kind}',
|
|
fields={
|
|
'handleTime': '2026-10-01 08:00:00',
|
|
'handlerUserId': handler_id,
|
|
'attachments': attachments,
|
|
'remark': '只改内容',
|
|
},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
assert result['handlerUserId'] == handler_id
|
|
assert result['handleTime'] == '2026-10-01 08:00:00'
|
|
assert result['attachments'] == attachments
|
|
assert result['remark'] == '只改内容'
|
|
|
|
|
|
@pytest.mark.parametrize('kind', ['storage-cert', 'insurance'])
|
|
def test_changed_handle_time_revalidates_historical_ineligible_handler(kind):
|
|
from app.services.market_sample import update_insurance, update_storage_cert
|
|
|
|
db = _session()
|
|
protocol, _, _ = _cell_sample(db, f'historical-time-change-{kind}')
|
|
handler_id = f'disabled-time-change-{kind}'
|
|
db.add(
|
|
SysUser(
|
|
id=handler_id,
|
|
user_no=handler_id,
|
|
nick_name='停用历史办理人',
|
|
email='',
|
|
phone_no='',
|
|
status='0',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
)
|
|
)
|
|
if kind == 'storage-cert':
|
|
row = BizProtocolStorageCert(
|
|
id=f'cert-{handler_id}',
|
|
collection_protocol_id=protocol.id,
|
|
handle_time=datetime(2026, 10, 1, 8, 0),
|
|
handler_user_id=handler_id,
|
|
make_status='1',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
)
|
|
updater = update_storage_cert
|
|
else:
|
|
row = BizProtocolFullmoonTodo(
|
|
id=f'todo-{handler_id}',
|
|
collection_protocol_id=protocol.id,
|
|
handle_time=datetime(2026, 10, 1, 8, 0),
|
|
handler_user_id=handler_id,
|
|
todo_status='1',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
del_flag=0,
|
|
)
|
|
updater = update_insurance
|
|
db.add(row)
|
|
db.commit()
|
|
|
|
with pytest.raises(ValueError, match='办理人员'):
|
|
updater(
|
|
db,
|
|
sample_id=f'cell_storage:C-historical-time-change-{kind}',
|
|
fields={
|
|
'handleTime': '2026-10-02 08:00:00',
|
|
'handlerUserId': handler_id,
|
|
'remark': '不得写入',
|
|
},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
db.expire_all()
|
|
assert row.handle_time == datetime(2026, 10, 1, 8, 0)
|
|
|
|
|
|
def test_first_handle_parent_lock_compiles_to_mysql_for_update():
|
|
from sqlalchemy.dialects import mysql
|
|
|
|
from app.services.market_sample import _cell_parent_lock_query
|
|
|
|
db = _session()
|
|
query = _cell_parent_lock_query(db, 'cp-lock', _ctx())
|
|
sql = str(
|
|
query.statement.compile(
|
|
dialect=mysql.dialect(), compile_kwargs={'literal_binds': True}
|
|
)
|
|
)
|
|
|
|
assert 'FOR UPDATE' in sql
|
|
assert "biz_collection_protocol.id = 'cp-lock'" in sql
|
|
assert "biz_collection_protocol.tenant_no = 'tenant-a'" in sql
|
|
assert 'biz_collection_protocol.del_flag = 0' in sql
|
|
|
|
|
|
def test_repeated_first_handle_requests_reuse_single_child_row():
|
|
from app.services.market_sample import update_insurance, update_storage_cert
|
|
|
|
db = _session()
|
|
_cell_sample(db, 'single-child')
|
|
for updater in (update_storage_cert, update_insurance):
|
|
updater(
|
|
db,
|
|
sample_id='cell_storage:C-single-child',
|
|
fields={'remark': 'first'},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
updater(
|
|
db,
|
|
sample_id='cell_storage:C-single-child',
|
|
fields={'remark': 'second'},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
assert db.query(BizProtocolStorageCert).count() == 1
|
|
assert db.query(BizProtocolFullmoonTodo).count() == 1
|
|
assert db.query(BizProtocolStorageCert).one().remark == 'second'
|
|
assert db.query(BizProtocolFullmoonTodo).one().handle_remark == 'second'
|
|
|
|
|
|
def test_voided_progress_uses_danger_tone():
|
|
from app.services.market_sample import get_sample_detail
|
|
|
|
db = _session()
|
|
_, _, sample = _cell_sample(db, 'voided-tone')
|
|
sample.sample_status = 'yzf'
|
|
db.commit()
|
|
|
|
detail = get_sample_detail(
|
|
db,
|
|
sample_id='cell_storage:C-voided-tone',
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
assert detail['progress']['collection']['code'] == 'voided'
|
|
assert detail['progress']['collection']['statusTone'] == 'danger'
|
|
assert detail['progress']['collection']['tagType'] == 'danger'
|
|
|
|
|
|
def test_source_qualified_whitespace_token_returns_uniform_404():
|
|
from app.services.market_sample import get_sample_detail
|
|
|
|
db = _session()
|
|
for sample_id in ('cell_storage: ', 'research_donation:\t'):
|
|
with pytest.raises(HTTPException) as exc:
|
|
get_sample_detail(
|
|
db, sample_id=sample_id, ctx=_ctx(), user_info=_user()
|
|
)
|
|
assert exc.value.status_code == 404
|
|
assert exc.value.detail == '数据不存在或无权访问'
|
|
|
|
|
|
def test_duplicate_active_children_use_same_newest_fields_in_list_and_detail():
|
|
from app.services.market_sample import get_sample_detail, list_samples
|
|
|
|
db = _session()
|
|
protocol, collection, _ = _cell_sample(db, 'deterministic')
|
|
collection.create_time = datetime(2026, 10, 1, 8, 0)
|
|
original_party = db.query(BizProtocolPartyCustomer).filter_by(
|
|
collection_protocol_id=protocol.id
|
|
).one()
|
|
original_party.create_time = datetime(2026, 10, 1, 8, 0)
|
|
db.add_all(
|
|
[
|
|
BizProtocolCollectionInfo(
|
|
id='cc-deterministic-new',
|
|
collection_protocol_id=protocol.id,
|
|
region_id='newest-region',
|
|
sales_user_id='u1',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
create_time=datetime(2026, 10, 2, 8, 0),
|
|
del_flag=0,
|
|
),
|
|
BizProtocolPartyCustomer(
|
|
id='pc-deterministic-new',
|
|
collection_protocol_id=protocol.id,
|
|
mother_name='最新客户',
|
|
tenant_no='tenant-a',
|
|
create_dept_id='',
|
|
create_time=datetime(2026, 10, 2, 8, 0),
|
|
del_flag=0,
|
|
),
|
|
]
|
|
)
|
|
db.commit()
|
|
|
|
items, total = list_samples(db, ctx=_ctx(), user_info=_user())
|
|
detail = get_sample_detail(
|
|
db,
|
|
sample_id='cell_storage:C-deterministic',
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
assert total == 1
|
|
assert items[0]['regionId'] == detail['card']['regionId'] == 'newest-region'
|
|
assert items[0]['customerName'] == detail['card']['customerName'] == '最新客户'
|
|
|
|
|
|
def test_write_reasserts_current_scope_after_parent_lock(monkeypatch):
|
|
from app.services import market_sample
|
|
|
|
db = _session()
|
|
_, collection, _ = _cell_sample(db, 'scope-race')
|
|
original_find = market_sample._find_sample
|
|
calls = []
|
|
|
|
def move_out_of_scope_after_initial_read(*args, **kwargs):
|
|
calls.append(bool(kwargs.get('for_update')))
|
|
item = original_find(*args, **kwargs)
|
|
if len(calls) == 1:
|
|
collection.sales_user_id = 'u2'
|
|
db.commit()
|
|
return item
|
|
|
|
monkeypatch.setattr(
|
|
market_sample, '_find_sample', move_out_of_scope_after_initial_read
|
|
)
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
market_sample.update_storage_cert(
|
|
db,
|
|
sample_id='cell_storage:C-scope-race',
|
|
fields={'remark': 'must not write'},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
assert exc.value.status_code == 404
|
|
assert calls == [False, True]
|
|
assert not db.in_transaction()
|
|
assert db.query(BizProtocolStorageCert).count() == 0
|
|
|
|
|
|
def test_current_sample_and_child_reads_compile_as_mysql_locking_reads():
|
|
from sqlalchemy.dialects import mysql
|
|
|
|
from app.services.market_sample import (
|
|
_cell_sample_lookup_query,
|
|
_handle_child_current_query,
|
|
)
|
|
|
|
db = _session()
|
|
sample_query = _cell_sample_lookup_query(
|
|
db,
|
|
token='C-locking',
|
|
by_row_id=False,
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
for_update=True,
|
|
)
|
|
cert_query = _handle_child_current_query(
|
|
db, BizProtocolStorageCert, 'cp-locking', _ctx()
|
|
)
|
|
todo_query = _handle_child_current_query(
|
|
db, BizProtocolFullmoonTodo, 'cp-locking', _ctx()
|
|
)
|
|
|
|
def mysql_sql(query):
|
|
return str(
|
|
query.statement.compile(
|
|
dialect=mysql.dialect(), compile_kwargs={'literal_binds': True}
|
|
)
|
|
)
|
|
|
|
sample_sql = mysql_sql(sample_query)
|
|
cert_sql = mysql_sql(cert_query)
|
|
todo_sql = mysql_sql(todo_query)
|
|
assert 'FOR UPDATE' in sample_sql
|
|
assert 'FOR UPDATE' in cert_sql
|
|
assert 'FOR UPDATE' in todo_sql
|
|
assert 'ORDER BY biz_protocol_storage_cert.update_time DESC' in cert_sql
|
|
assert 'ORDER BY biz_protocol_fullmoon_todo.update_time DESC' in todo_sql
|
|
|
|
|
|
def test_write_lock_order_is_parent_then_current_sample_then_child(monkeypatch):
|
|
from app.services import market_sample
|
|
|
|
db = _session()
|
|
_cell_sample(db, 'lock-order')
|
|
events = []
|
|
original_parent_lock = market_sample._lock_cell_parent
|
|
original_find = market_sample._find_sample
|
|
original_child_query = market_sample._handle_child_current_query
|
|
|
|
def record_parent(*args, **kwargs):
|
|
events.append('parent')
|
|
return original_parent_lock(*args, **kwargs)
|
|
|
|
def record_find(*args, **kwargs):
|
|
events.append('sample-current' if kwargs.get('for_update') else 'sample-initial')
|
|
return original_find(*args, **kwargs)
|
|
|
|
def record_child(*args, **kwargs):
|
|
events.append('child')
|
|
return original_child_query(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(market_sample, '_lock_cell_parent', record_parent)
|
|
monkeypatch.setattr(market_sample, '_find_sample', record_find)
|
|
monkeypatch.setattr(
|
|
market_sample, '_handle_child_current_query', record_child
|
|
)
|
|
|
|
market_sample.update_storage_cert(
|
|
db,
|
|
sample_id='cell_storage:C-lock-order',
|
|
fields={'remark': 'locked'},
|
|
ctx=_ctx(),
|
|
user_info=_user(),
|
|
)
|
|
|
|
assert events == ['sample-initial', 'parent', 'sample-current', 'child']
|