Skip to content

Commit fb42415

Browse files
committed
schema rules for vaccination procedure code
1 parent e5f43ba commit fb42415

File tree

6 files changed

+74
-45
lines changed

6 files changed

+74
-45
lines changed

backend/src/models/utils/pre_validator_utils.py

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from datetime import date, datetime
22
from decimal import Decimal
3-
from typing import Optional, Union
3+
from typing import Union
44

55
from .generic_utils import is_valid_simple_snomed, nhs_number_mod11_check
66

@@ -23,9 +23,6 @@ def for_string(
2323
if not isinstance(field_value, str):
2424
raise TypeError(f"{field_location} must be a string")
2525

26-
if field_value.isspace():
27-
raise ValueError(f"{field_location} must be a non-empty string")
28-
2926
if defined_length:
3027
if len(field_value) != defined_length:
3128
raise ValueError(f"{field_location} must be {defined_length} characters")
@@ -49,15 +46,14 @@ def for_string(
4946
def for_list(
5047
field_value: list,
5148
field_location: str,
52-
defined_length: Optional[int] = None,
53-
max_length: Optional[int] = None,
49+
defined_length: int = None,
5450
elements_are_strings: bool = False,
55-
string_element_max_length: Optional[int] = None,
5651
elements_are_dicts: bool = False,
5752
):
5853
"""
59-
Apply pre-validation to a list field to ensure it is a non-empty list which meets the length requirements and
60-
requirements, if applicable, for each list element to be a non-empty string or non-empty dictionary
54+
Apply pre-validation to a list field to ensure it is a non-empty list which meets the length
55+
requirements and requirements, if applicable, for each list element to be a non-empty string
56+
or non-empty dictionary
6157
"""
6258
if not isinstance(field_value, list):
6359
raise TypeError(f"{field_location} must be an array")
@@ -69,12 +65,12 @@ def for_list(
6965
if len(field_value) == 0:
7066
raise ValueError(f"{field_location} must be a non-empty array")
7167

72-
if max_length is not None and len(field_value) > max_length:
73-
raise ValueError(f"{field_location} must be an array of maximum length {max_length}")
74-
7568
if elements_are_strings:
76-
for idx, element in enumerate(field_value):
77-
PreValidation.for_string(element, f"{field_location}[{idx}]", max_length=string_element_max_length)
69+
for element in field_value:
70+
if not isinstance(element, str):
71+
raise TypeError(f"{field_location} must be an array of strings")
72+
if len(element) == 0:
73+
raise ValueError(f"{field_location} must be an array of non-empty strings")
7874

7975
if elements_are_dicts:
8076
for element in field_value:
@@ -185,7 +181,6 @@ def for_positive_integer(field_value: int, field_location: str, max_value: int =
185181
Apply pre-validation to an integer field to ensure that it is a positive integer,
186182
which does not exceed the maximum allowed value (if applicable)
187183
"""
188-
# This check uses type() instead of isinstance() because bool is a subclass of int.
189184
if type(field_value) is not int: # pylint: disable=unidiomatic-typecheck
190185
raise TypeError(f"{field_location} must be a positive integer")
191186

@@ -203,7 +198,6 @@ def for_integer_or_decimal(field_value: Union[int, Decimal], field_location: str
203198
which does not exceed the maximum allowed number of decimal places (if applicable)
204199
"""
205200
if not (
206-
# This check uses type() instead of isinstance() because bool is a subclass of int.
207201
type(field_value) is int # pylint: disable=unidiomatic-typecheck
208202
or type(field_value) is Decimal # pylint: disable=unidiomatic-typecheck
209203
):

lambdas/shared/src/common/validator/expression_checker.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from common.validator.expression_rule import expression_rule_per_field
99
from common.validator.lookup_expressions.key_data import KeyData
1010
from common.validator.lookup_expressions.lookup_data import LookUpData
11-
from common.validator.validation_utils import check_if_future_date, nhs_number_mod11_check
11+
from common.validator.validation_utils import check_if_future_date, is_valid_simple_snomed, nhs_number_mod11_check
1212

1313

1414
class ExpressionChecker:
@@ -31,8 +31,6 @@ def validate_expression(
3131
return self.validation_for_string_values(expression_rule, field_name, field_value)
3232
case "LIST":
3333
return self.validation_for_list(expression_rule, field_name, field_value)
34-
case "NHS_NUMBER":
35-
return self.validation_for_nhs_number(expression_rule, field_name, field_value)
3634
case "DATE":
3735
return self.validation_for_date(expression_rule, field_name, field_value)
3836
case "DATETIME":
@@ -45,6 +43,10 @@ def validate_expression(
4543
return self.validation_for_boolean(expression_rule, field_name, field_value)
4644
case "INTDECIMAL":
4745
return self.validation_for_integer_or_decimal(expression_rule, field_name, field_value)
46+
case "NHS_NUMBER":
47+
return self.validation_for_nhs_number(expression_rule, field_name, field_value)
48+
case "SNOMED_CODE":
49+
return self.validation_for_snomed_code(expression_rule, field_name, field_value)
4850
case _:
4951
return "Schema expression not found! Check your expression type : " + expression_type
5052

@@ -317,3 +319,20 @@ def validation_for_nhs_number(self, expression_rule: str, field_name: str, field
317319
if self.report_unexpected_exception:
318320
message = MESSAGES[ExceptionLevels.UNEXPECTED_EXCEPTION] % (e.__class__.__name__, e)
319321
return ErrorReport(ExceptionLevels.UNEXPECTED_EXCEPTION, message, None, field_name)
322+
323+
def validation_for_snomed_code(self, expression_rule: str, field_location: str, field_value: str):
324+
"""
325+
Apply prevalidation to snomed code to ensure that its a valid one.
326+
"""
327+
328+
error_message = f"{field_location} is not a valid snomed code"
329+
330+
try:
331+
is_valid = is_valid_simple_snomed(field_value)
332+
if not is_valid:
333+
raise ValueError(error_message)
334+
except ValueError as e:
335+
code = ExceptionLevels.RECORD_CHECK_FAILED
336+
message = MESSAGES[ExceptionLevels.RECORD_CHECK_FAILED]
337+
details = str(e)
338+
return ErrorReport(code, message, None, field_location, details)

lambdas/shared/src/common/validator/validation_utils.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,6 @@ def check_if_future_date(parsed_value: date | datetime):
1616
return False
1717

1818

19-
def is_valid_simple_snomed(simple_snomed: str) -> bool:
20-
"check the snomed code valid or not."
21-
min_snomed_length = 6
22-
max_snomed_length = 18
23-
return (
24-
simple_snomed is not None
25-
and simple_snomed.isdigit()
26-
and simple_snomed[0] != "0"
27-
and min_snomed_length <= len(simple_snomed) <= max_snomed_length
28-
and validate(simple_snomed)
29-
and (simple_snomed[-3:-1] in ("00", "10"))
30-
)
31-
32-
3319
def nhs_number_mod11_check(nhs_number: str) -> bool:
3420
"""
3521
Parameters:-
@@ -56,3 +42,17 @@ def nhs_number_mod11_check(nhs_number: str) -> bool:
5642
is_mod11 = check_digit == int(nhs_number[-1])
5743

5844
return is_mod11
45+
46+
47+
def is_valid_simple_snomed(simple_snomed: str) -> bool:
48+
"check the snomed code valid or not."
49+
min_snomed_length = 6
50+
max_snomed_length = 18
51+
return (
52+
simple_snomed is not None
53+
and simple_snomed.isdigit()
54+
and simple_snomed[0] != "0"
55+
and min_snomed_length <= len(simple_snomed) <= max_snomed_length
56+
and validate(simple_snomed)
57+
and (simple_snomed[-3:-1] in ("00", "10"))
58+
)

lambdas/shared/tests/test_common/validator/test_expression_checker.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,19 @@ def test_vaccination_procedure_code_string_valid_and_invalid(self):
195195
# Invalid: non-string value
196196
self.assertIsInstance(checker.validate_expression("STRING", "", field_path, 123456), ErrorReport)
197197

198+
# After parent check succeeds - SNOMED_CODE for VACCINATION_PROCEDURE_CODE
199+
def test_vaccination_procedure_snomed_code_valid_and_invalid(self):
200+
checker = self.make_checker()
201+
field_path = "extension|0|valueCodeableConcept|coding|0|code"
202+
# Valid SNOMED example (passes Verhoeff, doesn't start with 0, length ok, suffix rule)
203+
self.assertIsNone(checker.validate_expression("SNOMED_CODE", "", field_path, "1119349007"))
204+
# Invalid: empty
205+
self.assertIsInstance(checker.validate_expression("SNOMED_CODE", "", field_path, ""), ErrorReport)
206+
# Invalid: non-digit
207+
self.assertIsInstance(checker.validate_expression("SNOMED_CODE", "", field_path, "ABC123"), ErrorReport)
208+
# Invalid: starts with 0
209+
self.assertIsInstance(checker.validate_expression("SNOMED_CODE", "", field_path, "012345"), ErrorReport)
210+
198211
# STRING with VACCINATION_PROCEDURE_TERM
199212
def test_vaccination_procedure_term_string_valid_and_invalid(self):
200213
checker = self.make_checker()
@@ -388,16 +401,5 @@ def test_postcode_string_rule_valid_and_invalid(self):
388401
self.assertIsInstance(checker.validate_expression("STRING", "", field_path, ""), ErrorReport)
389402

390403

391-
# class DummyParserEx:
392-
# def __init__(self, data=None, raise_on_get=False):
393-
# self._data = data or {}
394-
# self._raise_on_get = raise_on_get
395-
396-
# def get_key_value(self, field_name):
397-
# if self._raise_on_get:
398-
# raise RuntimeError("boom")
399-
# return [self._data.get(field_name, "")]
400-
401-
402404
if __name__ == "__main__":
403405
unittest.main()

lambdas/shared/tests/test_common/validator/test_schemas/test_schema.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,20 @@
226226
},
227227
"errorGroup": "completeness"
228228
},
229+
{
230+
"expressionId": "01K9ZR2QZ6D9MTX3T7SQMK37C7",
231+
"parentExpressionId": "01K8S0YYGDFWJXN2W3THYG24EZ",
232+
"fieldNameFHIR": "extension|0|valueCodeableConcept|coding|0|code",
233+
"fieldNameFlat": "VACCINATION_PROCEDURE_CODE",
234+
"fieldNumber": 17,
235+
"errorLevel": 0,
236+
"expression": {
237+
"expressionName": "Procedure Code Not Empty Check",
238+
"expressionType": "SNOMED_CODE",
239+
"expressionRule": ""
240+
},
241+
"errorGroup": "completeness"
242+
},
229243
{
230244
"expressionId": "01K5EGR0C85HY6MDNN6TTR1K48",
231245
"fieldNameFHIR": "extension|0|valueCodeableConcept|coding|0|display",

lambdas/shared/tests/test_common/validator/testing_utils/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"PERFORMING_PROFESSIONAL_SURNAME": "SMITH",
2929
"RECORDED_DATE": "2025-03-06",
3030
"PRIMARY_SOURCE": True,
31-
"VACCINATION_PROCEDURE_CODE": "PROC123",
31+
"VACCINATION_PROCEDURE_CODE": "1324681000000101",
3232
"VACCINATION_PROCEDURE_TERM": "Procedure Term",
3333
"DOSE_SEQUENCE": 1,
3434
"VACCINE_PRODUCT_CODE": "VACC123",

0 commit comments

Comments
 (0)