Skip to content

Commit cd02066

Browse files
committed
add logic for demographics match and integrate with nhs number processing
1 parent 516ac9a commit cd02066

File tree

3 files changed

+181
-3
lines changed

3 files changed

+181
-3
lines changed

lambdas/id_sync/src/ieds_db_operations.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
from boto3.dynamodb.conditions import Key
23
from os_vars import get_ieds_table_name
34
from common.aws_dynamodb import get_dynamodb_table
@@ -141,3 +142,41 @@ def get_items_from_patient_id(id: str, limit=BATCH_SIZE) -> list:
141142
nhs_numbers=[patient_pk],
142143
exception=e
143144
)
145+
146+
def extract_patient_resource_from_item(item: dict) -> dict | None:
147+
"""Extract a Patient resource dict from an IEDS item.
148+
149+
Accepts common shapes: item['Resource'] (dict or JSON string), item['resource'], or a wrapper with 'resource'.
150+
Returns the patient resource dict or None if not found/parsible.
151+
"""
152+
if not isinstance(item, dict):
153+
return None
154+
155+
candidate = item.get("Resource") or item.get("resource") or item.get("Body") or item.get("body")
156+
if not candidate:
157+
return None
158+
159+
# candidate might be a JSON string
160+
if isinstance(candidate, str):
161+
try:
162+
candidate = json.loads(candidate)
163+
except Exception:
164+
return None
165+
166+
if isinstance(candidate, dict):
167+
# if wrapped like {"resource": {...}}
168+
if "resource" in candidate and isinstance(candidate["resource"], dict):
169+
candidate = candidate["resource"]
170+
171+
# If this dict is the Patient resource itself
172+
if candidate.get("resourceType") == "Patient":
173+
return candidate
174+
175+
# If it's a bundle/entry, search for Patient
176+
if isinstance(candidate.get("entry"), list):
177+
for entry in candidate.get("entry", []):
178+
r = entry.get("resource") or entry.get("Resource")
179+
if isinstance(r, dict) and r.get("resourceType") == "Patient":
180+
return r
181+
182+
return None

lambdas/id_sync/src/pds_details.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,22 @@ def pds_get_patient_id(nhs_number: str) -> str:
5454
msg = f"Error getting PDS patient ID for {nhs_number}"
5555
logger.exception(msg)
5656
raise IdSyncException(message=msg, exception=e)
57+
58+
def normalize_name_from_pds(pds_get_patient_details: dict) -> str | None:
59+
"""Return a normalized full name (given + family) from PDS patient details or None."""
60+
try:
61+
name = pds_get_patient_details.get("name")
62+
if not name:
63+
return None
64+
name_entry = name[0] if isinstance(name, list) else name
65+
given = name_entry.get("given")
66+
given_str = None
67+
if isinstance(given, list) and given:
68+
given_str = given[0]
69+
elif isinstance(given, str):
70+
given_str = given
71+
family = name_entry.get("family")
72+
parts = [p for p in [given_str, family] if p]
73+
return " ".join(parts).strip().lower() if parts else None
74+
except Exception:
75+
return None

lambdas/id_sync/src/record_processor.py

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
from common.clients import logger
22
from typing import Dict, Any
3-
from pds_details import pds_get_patient_id
4-
from ieds_db_operations import ieds_check_exist, ieds_update_patient_id
3+
from pds_details import pds_get_patient_id, pds_get_patient_details, normalize_name_from_pds
4+
from ieds_db_operations import (
5+
ieds_check_exist,
6+
ieds_update_patient_id,
7+
extract_patient_resource_from_item,
8+
get_items_from_patient_id,
9+
)
510
import json
611
import ast
712

@@ -53,10 +58,125 @@ def process_nhs_number(nhs_number: str) -> Dict[str, Any]:
5358
logger.info("Update patient ID from %s to %s", nhs_number, new_nhs_number)
5459

5560
if ieds_check_exist(nhs_number):
56-
response = ieds_update_patient_id(nhs_number, new_nhs_number)
61+
# Fetch PDS details for demographic comparison
62+
try:
63+
pds_details = pds_get_patient_details(nhs_number)
64+
except Exception:
65+
logger.exception("process_nhs_number: failed to fetch PDS details, aborting update")
66+
return {
67+
"status": "error",
68+
"message": "Failed to fetch PDS details for demographic comparison",
69+
"nhs_number": nhs_number,
70+
}
71+
72+
# Get IEDS items for this patient id and compare demographics
73+
try:
74+
items = get_items_from_patient_id(nhs_number)
75+
except Exception:
76+
logger.exception("process_nhs_number: failed to fetch IEDS items, aborting update")
77+
return {
78+
"status": "error",
79+
"message": "Failed to fetch IEDS items for demographic comparison",
80+
"nhs_number": nhs_number,
81+
}
82+
83+
# If at least one IEDS item matches demographics, proceed with update
84+
match_found = False
85+
for item in items:
86+
try:
87+
if demographics_match(pds_details, item):
88+
match_found = True
89+
break
90+
except Exception:
91+
logger.exception("process_nhs_number: error while comparing demographics for item: %s", item)
92+
93+
if not match_found:
94+
logger.info("process_nhs_number: No IEDS items matched PDS demographics. Skipping update for %s", nhs_number)
95+
response = {
96+
"status": "success",
97+
"message": "No IEDS items matched PDS demographics; update skipped",
98+
}
99+
else:
100+
response = ieds_update_patient_id(nhs_number, new_nhs_number)
57101
else:
58102
logger.info("No IEDS record found for: %s", nhs_number)
59103
response = {"status": "success", "message": f"No records returned for ID: {nhs_number}"}
60104

61105
response["nhs_number"] = nhs_number
62106
return response
107+
108+
def extract_normalized_name_from_patient(patient: dict) -> str | None:
109+
"""Return a normalized 'given family' name string from a Patient resource or None."""
110+
if not patient:
111+
return None
112+
name = patient.get("name")
113+
if not name:
114+
return None
115+
try:
116+
name_entry = name[0] if isinstance(name, list) else name
117+
given = name_entry.get("given")
118+
given_str = None
119+
if isinstance(given, list) and given:
120+
given_str = given[0]
121+
elif isinstance(given, str):
122+
given_str = given
123+
family = name_entry.get("family")
124+
parts = [p for p in [given_str, family] if p]
125+
return " ".join(parts).strip().lower() if parts else None
126+
except Exception:
127+
return None
128+
129+
130+
def demographics_match(pds_details: dict, ieds_item: dict) -> bool:
131+
"""Compare PDS patient details to an IEDS item (FHIR Patient resource).
132+
133+
Parameters:
134+
- pds_details: dict returned by PDS (patient details)
135+
- ieds_item: dict representing a single IEDS item containing a FHIR Patient resource
136+
137+
Returns True if name, birthDate and gender match (when present in both sources).
138+
If required fields are missing or unparsable on the IEDS side the function returns False.
139+
"""
140+
try:
141+
# extract pds values
142+
pds_name = normalize_name_from_pds(pds_details) if isinstance(pds_details, dict) else None
143+
pds_gender = pds_details.get("gender") if isinstance(pds_details, dict) else None
144+
pds_birth = pds_details.get("birthDate") if isinstance(pds_details, dict) else None
145+
146+
patient = extract_patient_resource_from_item(ieds_item)
147+
if not patient:
148+
logger.debug("demographics_match: no patient resource in item")
149+
return False
150+
151+
# normalize incoming patient name
152+
incoming_name = extract_normalized_name_from_patient(patient)
153+
154+
incoming_gender = patient.get("gender")
155+
incoming_birth = patient.get("birthDate")
156+
157+
def _norm_str(x):
158+
return str(x).strip().lower() if x is not None else None
159+
160+
# Compare birthDate (strict if both present)
161+
if pds_birth and incoming_birth:
162+
if str(pds_birth).strip() != str(incoming_birth).strip():
163+
logger.debug("demographics_match: birthDate mismatch %s != %s", pds_birth, incoming_birth)
164+
return False
165+
166+
# Compare gender (case-insensitive)
167+
if pds_gender and incoming_gender:
168+
if _norm_str(pds_gender) != _norm_str(incoming_gender):
169+
logger.debug("demographics_match: gender mismatch %s != %s", pds_gender, incoming_gender)
170+
return False
171+
172+
# Compare names if both present (normalized)
173+
if pds_name and incoming_name:
174+
if _norm_str(pds_name) != _norm_str(incoming_name):
175+
logger.debug("demographics_match: name mismatch %s != %s", pds_name, incoming_name)
176+
return False
177+
178+
# If we reached here, all present fields matched (or were not present to compare)
179+
return True
180+
except Exception:
181+
logger.exception("demographics_match: comparison failed with exception")
182+
return False

0 commit comments

Comments
 (0)