feat: implement 4-point demographic synchronization & patient validation

This commit is contained in:
2026-07-15 11:33:48 +05:30
parent ae5a928671
commit 554771abfe
6 changed files with 407 additions and 34 deletions
+167
View File
@@ -269,3 +269,170 @@ async def push_vitals(vitals: NormalizedVitals, api_key: str = None, db: Session
logger.error(f"Push-vitals error: {e}")
db.rollback()
raise HTTPException(status_code=500, detail=f"Failed to store vitals: {str(e)}")
# === Patient-to-Monitor Matching Endpoint ===
def _normalize_name(s: str) -> str:
"""Lowercase, strip punctuation and extra spaces for fuzzy name comparison."""
import re
if not s:
return ""
return re.sub(r"[^a-z0-9\s]", "", s.lower()).split()
def _normalize_dob(dob: str) -> str:
"""Normalize DOB to YYYYMMDD regardless of input format (YYYY-MM-DD / YYYYMMDD / DD-MM-YYYY)."""
if not dob:
return ""
dob = dob.strip().replace("/", "-")
if len(dob) == 8 and dob.isdigit():
return dob # already YYYYMMDD
parts = dob.split("-")
if len(parts) == 3:
if len(parts[0]) == 4: # YYYY-MM-DD
return "".join(parts)
elif len(parts[2]) == 4: # DD-MM-YYYY
return parts[2] + parts[1] + parts[0]
return dob.replace("-", "")
def _gender_code(g: str) -> str:
"""Return single-letter gender code: M / F / U."""
if not g:
return ""
g = g.strip().upper()
if g in ("M", "MALE"):
return "M"
if g in ("F", "FEMALE"):
return "F"
return "U"
def _calculate_age(dob_str: str) -> Optional[int]:
"""Calculate age from YYYYMMDD string."""
if not dob_str:
return None
normalized = _normalize_dob(dob_str)
if len(normalized) != 8 or not normalized.isdigit():
return None
try:
birth_year = int(normalized[:4])
birth_month = int(normalized[4:6])
birth_day = int(normalized[6:])
today = datetime.now()
return today.year - birth_year - ((today.month, today.day) < (birth_month, birth_day))
except Exception:
return None
@router.get("/match-patient")
def match_patient(
mrn: str = None,
name: str = None,
gender: str = None,
dob: str = None,
age: int = None,
db: Session = Depends(get_db)
):
"""
Match incoming patient demographics to an active Contec monitor.
Call this from the dashboard iframe URL:
/api/dashboard?minimal=true&mrn=MRN001&name=John+Doe&gender=M&dob=19900115&age=36
Returns:
{ matched: true/false, device_id, patient_id, bed_id, device_status, ... }
"""
if not mrn and not name:
return {"matched": False, "reason": "No patient identifiers supplied"}
# Fetch all patients
all_patients = db.query(Patient).all()
now = datetime.now(timezone.utc)
# Normalize incoming params
in_mrn = (mrn or "").strip().upper()
in_name = _normalize_name(name or "")
in_gender = _gender_code(gender or "")
in_dob = _normalize_dob(dob or "")
in_age = age
matched_patient = None
match_score = 0
for p in all_patients:
score = 0
# --- MRN check (primary) ---
p_mrn = (p.mrn or p.patient_id or "").strip().upper()
if in_mrn and p_mrn == in_mrn:
score += 10
elif in_mrn:
continue # MRN supplied but doesn't match → skip
# --- Name check ---
if in_name:
p_name = _normalize_name(p.name or "")
if p_name and any(w in p_name for w in in_name):
score += 3
# --- Gender check ---
if in_gender and p.gender:
if _gender_code(p.gender) == in_gender:
score += 2
# --- DOB check ---
if in_dob and p.dob:
if _normalize_dob(p.dob) == in_dob:
score += 3
# --- Age check ---
if in_age is not None and p.dob:
calc_p_age = _calculate_age(p.dob)
if calc_p_age is not None and calc_p_age == in_age:
score += 2
if score > match_score:
match_score = score
matched_patient = p
# To be matched, MRN must be found, and name/gender/dob must also be validated
# (Require at least a score of 12: MRN + Name, or MRN + Gender, etc.)
if not matched_patient or match_score < 12:
return {"matched": False, "reason": "No matching patient found in monitor database with 4-point verification"}
# Find the most recent vital reading for this patient
latest = (
db.query(VitalReading)
.filter(VitalReading.patient_id == matched_patient.patient_id)
.order_by(VitalReading.timestamp.desc())
.first()
)
if not latest:
return {
"matched": True,
"patient_id": matched_patient.patient_id,
"device_id": None,
"bed_id": matched_patient.bed_id,
"reason": "Patient found but no readings yet"
}
# Get device status
device = db.query(Device).filter(Device.device_id == latest.device_id).first()
device_status = "unknown"
if device:
last_seen = device.last_seen
if last_seen.tzinfo is None:
last_seen = last_seen.replace(tzinfo=timezone.utc)
device_status = "active" if (now - last_seen).total_seconds() < 15 else "offline"
return {
"matched": True,
"patient_id": matched_patient.patient_id,
"patient_name": matched_patient.name,
"patient_gender": matched_patient.gender,
"patient_dob": matched_patient.dob,
"device_id": latest.device_id,
"bed_id": matched_patient.bed_id,
"device_status": device_status,
"last_reading_at": latest.timestamp.isoformat() if latest.timestamp else None,
"match_score": match_score,
}