feat: derive subject status from visits

This commit is contained in:
Cheng Zhou
2026-05-21 14:47:42 +08:00
parent 9305ced664
commit da6f2f2d41
6 changed files with 125 additions and 26 deletions
+49 -1
View File
@@ -1,7 +1,7 @@
from datetime import date
from types import SimpleNamespace
from app.crud.subject import should_generate_visits_after_subject_update
from app.crud.subject import derive_subject_status, should_generate_visits_after_subject_update
from app.crud.visit import (
build_early_termination_visit_changes,
build_visit_schedule_dates,
@@ -227,3 +227,51 @@ def test_validate_early_termination_date_requires_date_before_last_visit_window_
assert "提前终止日期必须早于方案最后一个计划访视窗口开始日" in str(exc)
else:
raise AssertionError("same-day final visit window start should not be accepted as early termination")
def test_derive_subject_status_marks_dropped_when_early_termination_has_actual_date():
subject = SimpleNamespace(screening_date=date(2026, 5, 1), enrollment_date=date(2026, 5, 3))
visits = [
SimpleNamespace(visit_code="V1", planned_date=date(2026, 5, 10), actual_date=date(2026, 5, 10), status="DONE", created_at=None),
SimpleNamespace(visit_code="提前终止", planned_date=None, actual_date=date(2026, 5, 12), status="DONE", created_at=None),
]
assert derive_subject_status(subject, visits, [{"visit_code": "V1"}]) == "DROPPED"
def test_derive_subject_status_marks_completed_when_last_standard_visit_has_actual_date():
subject = SimpleNamespace(screening_date=date(2026, 5, 1), enrollment_date=date(2026, 5, 3))
visits = [
SimpleNamespace(visit_code="V1", planned_date=date(2026, 5, 10), actual_date=date(2026, 5, 10), status="DONE", created_at=None),
SimpleNamespace(visit_code="V2", planned_date=date(2026, 5, 17), actual_date=date(2026, 5, 17), status="DONE", created_at=None),
]
assert derive_subject_status(subject, visits, [{"visit_code": "V1"}, {"visit_code": "V2"}]) == "COMPLETED"
def test_derive_subject_status_rolls_back_to_enrolled_when_followup_after_standard_end_is_unfinished():
subject = SimpleNamespace(screening_date=date(2026, 5, 1), enrollment_date=date(2026, 5, 3))
visits = [
SimpleNamespace(visit_code="V1", planned_date=date(2026, 5, 10), actual_date=date(2026, 5, 10), status="DONE", created_at=None),
SimpleNamespace(visit_code="V2", planned_date=date(2026, 5, 17), actual_date=date(2026, 5, 17), status="DONE", created_at=None),
SimpleNamespace(visit_code="安全性随访", planned_date=date(2026, 5, 24), actual_date=None, status="PLANNED", created_at=None),
]
assert derive_subject_status(subject, visits, [{"visit_code": "V1"}, {"visit_code": "V2"}]) == "ENROLLED"
def test_derive_subject_status_uses_completed_standard_visit_when_extra_visit_is_before_standard_end():
subject = SimpleNamespace(screening_date=date(2026, 5, 1), enrollment_date=date(2026, 5, 3))
visits = [
SimpleNamespace(visit_code="V1", planned_date=date(2026, 5, 10), actual_date=date(2026, 5, 10), status="DONE", created_at=None),
SimpleNamespace(visit_code="安全性随访", planned_date=date(2026, 5, 12), actual_date=None, status="PLANNED", created_at=None),
SimpleNamespace(visit_code="V2", planned_date=date(2026, 5, 17), actual_date=date(2026, 5, 17), status="DONE", created_at=None),
]
assert derive_subject_status(subject, visits, [{"visit_code": "V1"}, {"visit_code": "V2"}]) == "COMPLETED"
def test_derive_subject_status_marks_screening_until_enrollment_date_is_filled():
subject = SimpleNamespace(screening_date=date(2026, 5, 1), enrollment_date=None)
assert derive_subject_status(subject, [], []) == "SCREENING"