From 4abd9cebdacd8f238d7e2cd1a4114a86dd0a7c83 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Thu, 2 Jul 2026 12:25:20 +0100 Subject: [PATCH] SDK-2765: Add handled_check_limit property to IDV sandbox check-report types Add an optional `handled_check_limit` integer property to the base `SandboxCheck` class and `SandboxCheckBuilder`, so all concrete sandbox check types (DocumentAuthenticity, DocumentFaceMatch, DocumentTextData, IdDocumentComparison, SupplementaryDocumentTextData, ZoomLiveness) inherit support for setting how many times a configured check response is used before the sandbox advances to the next response. The field is omitted from serialized JSON when not set so the backend treats absence as no limit. --- .../doc_scan/check/sandbox_check.py | 39 ++++- .../sandbox_document_authenticity_check.py | 4 +- .../doc_scan/check/sandbox_document_check.py | 4 +- .../sandbox_document_face_match_check.py | 4 +- .../check/sandbox_document_text_data_check.py | 4 +- .../sandbox_id_document_comparison_check.py | 6 +- .../doc_scan/check/sandbox_liveness_check.py | 4 +- ..._supplementary_document_text_data_check.py | 4 +- .../check/sandbox_zoom_liveness_check.py | 6 +- .../check/test_handled_check_limit.py | 144 ++++++++++++++++++ 10 files changed, 203 insertions(+), 16 deletions(-) create mode 100644 yoti_python_sandbox/tests/doc_scan/check/test_handled_check_limit.py diff --git a/yoti_python_sandbox/doc_scan/check/sandbox_check.py b/yoti_python_sandbox/doc_scan/check/sandbox_check.py index 4ef7524..58b09f8 100644 --- a/yoti_python_sandbox/doc_scan/check/sandbox_check.py +++ b/yoti_python_sandbox/doc_scan/check/sandbox_check.py @@ -6,12 +6,15 @@ class SandboxCheck(YotiSerializable): - def __init__(self, result): + def __init__(self, result, handled_check_limit=None): """ :param result: the result :type result: SandboxCheckResult + :param handled_check_limit: optional limit on how many times this check response is used + :type handled_check_limit: int or None """ self.__result = result + self.__handled_check_limit = handled_check_limit @property def result(self): @@ -20,8 +23,22 @@ def result(self): """ return self.__result + @property + def handled_check_limit(self): + """ + The number of times this configured check response is used before the sandbox + advances to the next configured response. When None, the backend treats it as + no limit. + + :rtype: int or None + """ + return self.__handled_check_limit + def to_json(self): - return {"result": self.result} + json = {"result": self.result} + if self.__handled_check_limit is not None: + json["handled_check_limit"] = self.__handled_check_limit + return json class SandboxCheckBuilder(object): @@ -30,6 +47,7 @@ class SandboxCheckBuilder(object): def __init__(self): self.__recommendation = None self.__breakdown = [] + self.__handled_check_limit = None def with_recommendation(self, recommendation): self.__recommendation = recommendation @@ -43,6 +61,19 @@ def with_breakdowns(self, breakdowns): self.__breakdown = breakdowns return self + def with_handled_check_limit(self, handled_check_limit): + """ + Sets the number of times this configured check response is used before the sandbox + advances to the next configured response. + + :param handled_check_limit: the limit + :type handled_check_limit: int + :return: the builder + :rtype: SandboxCheckBuilder + """ + self.__handled_check_limit = handled_check_limit + return self + @property def recommendation(self): return self.__recommendation @@ -51,6 +82,10 @@ def recommendation(self): def breakdown(self): return self.__breakdown + @property + def handled_check_limit(self): + return self.__handled_check_limit + @abstractmethod def build(self): raise NotImplementedError diff --git a/yoti_python_sandbox/doc_scan/check/sandbox_document_authenticity_check.py b/yoti_python_sandbox/doc_scan/check/sandbox_document_authenticity_check.py index fcbf35d..7c23abd 100644 --- a/yoti_python_sandbox/doc_scan/check/sandbox_document_authenticity_check.py +++ b/yoti_python_sandbox/doc_scan/check/sandbox_document_authenticity_check.py @@ -18,4 +18,6 @@ class SandboxDocumentAuthenticityCheckBuilder(SandboxDocumentCheckBuilder): def build(self): report = SandboxCheckReport(self.recommendation, self.breakdown) result = SandboxCheckResult(report) - return SandboxDocumentAuthenticityCheck(result, self.document_filter) + return SandboxDocumentAuthenticityCheck( + result, self.document_filter, self.handled_check_limit + ) diff --git a/yoti_python_sandbox/doc_scan/check/sandbox_document_check.py b/yoti_python_sandbox/doc_scan/check/sandbox_document_check.py index bd0f8bb..afc10d2 100644 --- a/yoti_python_sandbox/doc_scan/check/sandbox_document_check.py +++ b/yoti_python_sandbox/doc_scan/check/sandbox_document_check.py @@ -5,8 +5,8 @@ class SandboxDocumentCheck(SandboxCheck): - def __init__(self, result, document_filter): - SandboxCheck.__init__(self, result) + def __init__(self, result, document_filter, handled_check_limit=None): + SandboxCheck.__init__(self, result, handled_check_limit) self.__document_filter = document_filter @property diff --git a/yoti_python_sandbox/doc_scan/check/sandbox_document_face_match_check.py b/yoti_python_sandbox/doc_scan/check/sandbox_document_face_match_check.py index dbee703..49c28a1 100644 --- a/yoti_python_sandbox/doc_scan/check/sandbox_document_face_match_check.py +++ b/yoti_python_sandbox/doc_scan/check/sandbox_document_face_match_check.py @@ -18,4 +18,6 @@ class SandboxDocumentFaceMatchCheckBuilder(SandboxDocumentCheckBuilder): def build(self): report = SandboxCheckReport(self.recommendation, self.breakdown) result = SandboxCheckResult(report) - return SandboxDocumentFaceMatchCheck(result, self.document_filter) + return SandboxDocumentFaceMatchCheck( + result, self.document_filter, self.handled_check_limit + ) diff --git a/yoti_python_sandbox/doc_scan/check/sandbox_document_text_data_check.py b/yoti_python_sandbox/doc_scan/check/sandbox_document_text_data_check.py index 3c01347..c0490fc 100644 --- a/yoti_python_sandbox/doc_scan/check/sandbox_document_text_data_check.py +++ b/yoti_python_sandbox/doc_scan/check/sandbox_document_text_data_check.py @@ -58,4 +58,6 @@ def with_document_fields(self, document_fields): def build(self): report = SandboxCheckReport(self.recommendation, self.breakdown) result = SandboxDocumentTextDataCheckResult(report, self.__document_fields) - return SandboxDocumentTextDataCheck(result, self.document_filter) + return SandboxDocumentTextDataCheck( + result, self.document_filter, self.handled_check_limit + ) diff --git a/yoti_python_sandbox/doc_scan/check/sandbox_id_document_comparison_check.py b/yoti_python_sandbox/doc_scan/check/sandbox_id_document_comparison_check.py index 1278af3..528dee0 100644 --- a/yoti_python_sandbox/doc_scan/check/sandbox_id_document_comparison_check.py +++ b/yoti_python_sandbox/doc_scan/check/sandbox_id_document_comparison_check.py @@ -5,8 +5,8 @@ class SandboxIdDocumentComparisonCheck(SandboxCheck): - def __init__(self, result, secondary_document_filter): - SandboxCheck.__init__(self, result) + def __init__(self, result, secondary_document_filter, handled_check_limit=None): + SandboxCheck.__init__(self, result, handled_check_limit) self.__secondary_document_filter = secondary_document_filter @staticmethod @@ -37,5 +37,5 @@ def build(self): report = SandboxCheckReport(self.recommendation, self.breakdown) result = SandboxCheckResult(report) return SandboxIdDocumentComparisonCheck( - result, self.__secondary_document_filter + result, self.__secondary_document_filter, self.handled_check_limit ) diff --git a/yoti_python_sandbox/doc_scan/check/sandbox_liveness_check.py b/yoti_python_sandbox/doc_scan/check/sandbox_liveness_check.py index 57d5aa3..fa07f18 100644 --- a/yoti_python_sandbox/doc_scan/check/sandbox_liveness_check.py +++ b/yoti_python_sandbox/doc_scan/check/sandbox_liveness_check.py @@ -2,8 +2,8 @@ class SandboxLivenessCheck(SandboxCheck): - def __init__(self, result, liveness_type): - SandboxCheck.__init__(self, result) + def __init__(self, result, liveness_type, handled_check_limit=None): + SandboxCheck.__init__(self, result, handled_check_limit) self.__liveness_type = liveness_type @property diff --git a/yoti_python_sandbox/doc_scan/check/sandbox_supplementary_document_text_data_check.py b/yoti_python_sandbox/doc_scan/check/sandbox_supplementary_document_text_data_check.py index 971a003..f220f38 100644 --- a/yoti_python_sandbox/doc_scan/check/sandbox_supplementary_document_text_data_check.py +++ b/yoti_python_sandbox/doc_scan/check/sandbox_supplementary_document_text_data_check.py @@ -60,4 +60,6 @@ def build(self): result = SandboxSupplementaryDocumentTextDataCheckResult( report, self.__document_fields ) - return SandboxSupplementaryDocumentTextDataCheck(result, self.document_filter) + return SandboxSupplementaryDocumentTextDataCheck( + result, self.document_filter, self.handled_check_limit + ) diff --git a/yoti_python_sandbox/doc_scan/check/sandbox_zoom_liveness_check.py b/yoti_python_sandbox/doc_scan/check/sandbox_zoom_liveness_check.py index 783b777..c3ef5ba 100644 --- a/yoti_python_sandbox/doc_scan/check/sandbox_zoom_liveness_check.py +++ b/yoti_python_sandbox/doc_scan/check/sandbox_zoom_liveness_check.py @@ -9,12 +9,12 @@ class SandboxZoomLivenessCheck(SandboxLivenessCheck): - def __init__(self, result): - SandboxLivenessCheck.__init__(self, result, constants.ZOOM) + def __init__(self, result, handled_check_limit=None): + SandboxLivenessCheck.__init__(self, result, constants.ZOOM, handled_check_limit) class SandboxZoomLivenessCheckBuilder(SandboxCheckBuilder): def build(self): report = SandboxCheckReport(self.recommendation, self.breakdown) result = SandboxCheckResult(report) - return SandboxZoomLivenessCheck(result) + return SandboxZoomLivenessCheck(result, self.handled_check_limit) diff --git a/yoti_python_sandbox/tests/doc_scan/check/test_handled_check_limit.py b/yoti_python_sandbox/tests/doc_scan/check/test_handled_check_limit.py new file mode 100644 index 0000000..c8a3b78 --- /dev/null +++ b/yoti_python_sandbox/tests/doc_scan/check/test_handled_check_limit.py @@ -0,0 +1,144 @@ +""" +Tests for handled_check_limit on all IDV sandbox check-report types. +""" +import pytest + +from yoti_python_sandbox.doc_scan.check import ( + SandboxDocumentAuthenticityCheckBuilder, + SandboxDocumentFaceMatchCheckBuilder, + SandboxDocumentTextDataCheckBuilder, + SandboxIdDocumentComparisonCheckBuilder, + SandboxSupplementaryDocumentTextDataCheckBuilder, + SandboxZoomLivenessCheckBuilder, +) + + +HANDLED_CHECK_LIMIT = 3 + +ALL_BUILDER_CLASSES = [ + SandboxDocumentAuthenticityCheckBuilder, + SandboxDocumentFaceMatchCheckBuilder, + SandboxDocumentTextDataCheckBuilder, + SandboxIdDocumentComparisonCheckBuilder, + SandboxSupplementaryDocumentTextDataCheckBuilder, + SandboxZoomLivenessCheckBuilder, +] + + +@pytest.mark.parametrize("builder_class", ALL_BUILDER_CLASSES) +def test_handled_check_limit_is_included_in_json_when_set(builder_class): + check = builder_class().with_handled_check_limit(HANDLED_CHECK_LIMIT).build() + + json = check.to_json() + + assert json.get("handled_check_limit") == HANDLED_CHECK_LIMIT + + +@pytest.mark.parametrize("builder_class", ALL_BUILDER_CLASSES) +def test_handled_check_limit_is_omitted_from_json_when_not_set(builder_class): + check = builder_class().build() + + json = check.to_json() + + assert "handled_check_limit" not in json + + +@pytest.mark.parametrize("builder_class", ALL_BUILDER_CLASSES) +def test_handled_check_limit_property_returns_set_value(builder_class): + check = builder_class().with_handled_check_limit(HANDLED_CHECK_LIMIT).build() + + assert check.handled_check_limit == HANDLED_CHECK_LIMIT + + +@pytest.mark.parametrize("builder_class", ALL_BUILDER_CLASSES) +def test_handled_check_limit_property_returns_none_when_not_set(builder_class): + check = builder_class().build() + + assert check.handled_check_limit is None + + +def test_document_authenticity_check_handled_check_limit_end_to_end(): + check = ( + SandboxDocumentAuthenticityCheckBuilder() + .with_handled_check_limit(5) + .build() + ) + + json = check.to_json() + + assert json.get("handled_check_limit") == 5 + assert check.handled_check_limit == 5 + + +def test_zoom_liveness_check_handled_check_limit_end_to_end(): + check = ( + SandboxZoomLivenessCheckBuilder() + .with_handled_check_limit(2) + .build() + ) + + json = check.to_json() + + assert json.get("handled_check_limit") == 2 + assert check.handled_check_limit == 2 + assert check.liveness_type == "ZOOM" + + +def test_id_document_comparison_check_handled_check_limit_end_to_end(): + check = ( + SandboxIdDocumentComparisonCheckBuilder() + .with_handled_check_limit(1) + .build() + ) + + json = check.to_json() + + assert json.get("handled_check_limit") == 1 + + +def test_document_face_match_check_handled_check_limit_end_to_end(): + check = ( + SandboxDocumentFaceMatchCheckBuilder() + .with_handled_check_limit(10) + .build() + ) + + json = check.to_json() + + assert json.get("handled_check_limit") == 10 + + +def test_document_text_data_check_handled_check_limit_end_to_end(): + check = ( + SandboxDocumentTextDataCheckBuilder() + .with_handled_check_limit(7) + .build() + ) + + json = check.to_json() + + assert json.get("handled_check_limit") == 7 + + +def test_supplementary_document_text_data_check_handled_check_limit_end_to_end(): + check = ( + SandboxSupplementaryDocumentTextDataCheckBuilder() + .with_handled_check_limit(4) + .build() + ) + + json = check.to_json() + + assert json.get("handled_check_limit") == 4 + + +def test_existing_builder_calls_remain_backward_compatible(): + """Ensure existing callers that do not set handled_check_limit still work.""" + check = ( + SandboxDocumentAuthenticityCheckBuilder() + .build() + ) + + assert check.handled_check_limit is None + json = check.to_json() + assert "handled_check_limit" not in json