From 705b7f1ab125fb3ef68c8f09bba6643fe9c91af9 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Mon, 20 Apr 2026 14:38:06 +0100 Subject: [PATCH] feat(SDK-2614): Python - Support configuration for IDV shortened flow [python] --- yoti_python_sdk/doc_scan/__init__.py | 10 ++ yoti_python_sdk/doc_scan/constants.py | 11 ++ .../doc_scan/session/create/check/__init__.py | 2 + .../create/check/watchlist_advanced_ca.py | 164 ++++++++++++++++++ .../doc_scan/session/create/sdk_config.py | 28 +++ .../doc_scan/session/create/task/__init__.py | 2 + .../session/create/task/face_capture.py | 47 +++++ .../session/retrieve/check_response.py | 8 + .../session/retrieve/get_session_result.py | 24 ++- .../session/retrieve/resource_response.py | 2 + .../session/retrieve/task_response.py | 8 + .../session/create/test_sdk_config.py | 38 ++++ 12 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 yoti_python_sdk/doc_scan/session/create/check/watchlist_advanced_ca.py create mode 100644 yoti_python_sdk/doc_scan/session/create/task/face_capture.py diff --git a/yoti_python_sdk/doc_scan/__init__.py b/yoti_python_sdk/doc_scan/__init__.py index b9068ee9..bf51ebf2 100644 --- a/yoti_python_sdk/doc_scan/__init__.py +++ b/yoti_python_sdk/doc_scan/__init__.py @@ -6,10 +6,16 @@ ) from .session.create.check.face_match import RequestedFaceMatchCheckBuilder from .session.create.check.liveness import RequestedLivenessCheckBuilder +from .session.create.check.watchlist_screen import WatchlistScreeningCheckBuilder +from .session.create.check.watchlist_advanced_ca import ( + WatchlistAdvancedCaProfilesCheckBuilder, + WatchlistAdvancedCaSourcesConfig, +) from .session.create.task.text_extraction import RequestedTextExtractionTaskBuilder from .session.create.task.supplementary_doc_text_extraction import ( RequestedSupplementaryDocTextExtractionTaskBuilder, ) +from .session.create.task.face_capture import RequestedFaceCaptureTaskBuilder from .session.create.notification_config import NotificationConfigBuilder from .session.create.sdk_config import SdkConfigBuilder from .session.create.session_spec import SessionSpecBuilder @@ -20,8 +26,12 @@ "RequestedLivenessCheckBuilder", "RequestedFaceMatchCheckBuilder", "RequestedIDDocumentComparisonCheckBuilder", + "WatchlistScreeningCheckBuilder", + "WatchlistAdvancedCaProfilesCheckBuilder", + "WatchlistAdvancedCaSourcesConfig", "RequestedTextExtractionTaskBuilder", "RequestedSupplementaryDocTextExtractionTaskBuilder", + "RequestedFaceCaptureTaskBuilder", "SessionSpecBuilder", "NotificationConfigBuilder", "SdkConfigBuilder", diff --git a/yoti_python_sdk/doc_scan/constants.py b/yoti_python_sdk/doc_scan/constants.py index ec2ea3a3..c5fc3cd6 100644 --- a/yoti_python_sdk/doc_scan/constants.py +++ b/yoti_python_sdk/doc_scan/constants.py @@ -39,3 +39,14 @@ PROOF_OF_ADDRESS = "PROOF_OF_ADDRESS" WATCHLIST_SCREENING_CHECK_TYPE = "WATCHLIST_SCREENING" +WATCHLIST_ADVANCED_CA_CHECK_TYPE = "WATCHLIST_ADVANCED_CA" + +FACE_CAPTURE = "FACE_CAPTURE" + +ID_DOCUMENT_EDUCATION = "ID_DOCUMENT_EDUCATION" +ID_DOCUMENT_REQUIREMENTS = "ID_DOCUMENT_REQUIREMENTS" +SUPPLEMENTARY_DOCUMENT_EDUCATION = "SUPPLEMENTARY_DOCUMENT_EDUCATION" +ZOOM_LIVENESS_EDUCATION = "ZOOM_LIVENESS_EDUCATION" +STATIC_LIVENESS_EDUCATION = "STATIC_LIVENESS_EDUCATION" +FACE_CAPTURE_EDUCATION = "FACE_CAPTURE_EDUCATION" +FLOW_COMPLETION = "FLOW_COMPLETION" diff --git a/yoti_python_sdk/doc_scan/session/create/check/__init__.py b/yoti_python_sdk/doc_scan/session/create/check/__init__.py index 89d5689f..4ef16b89 100644 --- a/yoti_python_sdk/doc_scan/session/create/check/__init__.py +++ b/yoti_python_sdk/doc_scan/session/create/check/__init__.py @@ -3,6 +3,7 @@ from .face_match import RequestedFaceMatchCheckBuilder from .liveness import RequestedLivenessCheckBuilder from .watchlist_screen import WatchlistScreeningCheckBuilder +from .watchlist_advanced_ca import WatchlistAdvancedCaProfilesCheckBuilder __all__ = [ @@ -11,4 +12,5 @@ "RequestedFaceMatchCheckBuilder", "RequestedLivenessCheckBuilder", "WatchlistScreeningCheckBuilder", + "WatchlistAdvancedCaProfilesCheckBuilder", ] diff --git a/yoti_python_sdk/doc_scan/session/create/check/watchlist_advanced_ca.py b/yoti_python_sdk/doc_scan/session/create/check/watchlist_advanced_ca.py new file mode 100644 index 00000000..30a4c27d --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/check/watchlist_advanced_ca.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from yoti_python_sdk.doc_scan import constants +from yoti_python_sdk.utils import YotiSerializable, remove_null_values +from .requested_check import RequestedCheck + + +class WatchlistAdvancedCaProfilesCheckConfig(YotiSerializable): + """ + The configuration applied when creating a Watchlist Advanced CA Profiles check. + """ + + def __init__(self, remove_deceased=None, share_url=None, sources=None): + """ + :param remove_deceased: whether to remove deceased individuals from results + :type remove_deceased: bool or None + :param share_url: whether to share the URL in results + :type share_url: bool or None + :param sources: the sources configuration + :type sources: WatchlistAdvancedCaSourcesConfig or None + """ + self.__remove_deceased = remove_deceased + self.__share_url = share_url + self.__sources = sources + + @property + def remove_deceased(self): + """ + Whether deceased individuals are removed from results + + :return: remove deceased flag + :rtype: bool or None + """ + return self.__remove_deceased + + @property + def share_url(self): + """ + Whether the URL is shared in results + + :return: share URL flag + :rtype: bool or None + """ + return self.__share_url + + @property + def sources(self): + """ + The sources configuration for the advanced CA profiles check + + :return: the sources configuration + :rtype: WatchlistAdvancedCaSourcesConfig or None + """ + return self.__sources + + def to_json(self): + return remove_null_values( + { + "remove_deceased": self.__remove_deceased, + "share_url": self.__share_url, + "sources": self.__sources, + } + ) + + +class WatchlistAdvancedCaSourcesConfig(YotiSerializable): + """ + Configures the sources for a Watchlist Advanced CA Profiles check. + """ + + def __init__(self, types=None): + """ + :param types: the list of source types to check against + :type types: list[str] or None + """ + self.__types = types or [] + + @property + def types(self): + """ + The list of source types + + :return: the source types + :rtype: list[str] + """ + return self.__types + + def to_json(self): + return remove_null_values({"types": self.__types}) + + +class WatchlistAdvancedCaProfilesCheck(RequestedCheck): + """ + Requests creation of a Watchlist Advanced CA Profiles check + """ + + def __init__(self, config): + """ + :param config: the Watchlist Advanced CA Profiles check configuration + :type config: WatchlistAdvancedCaProfilesCheckConfig + """ + self.__config = config + + @property + def type(self): + return constants.WATCHLIST_ADVANCED_CA_CHECK_TYPE + + @property + def config(self): + return self.__config + + +class WatchlistAdvancedCaProfilesCheckBuilder(object): + """ + Builder to assist creation of :class:`WatchlistAdvancedCaProfilesCheck` + """ + + def __init__(self): + self.__remove_deceased = None + self.__share_url = None + self.__sources = None + + def with_remove_deceased(self, remove_deceased): + """ + Sets whether deceased individuals should be removed from results + + :param remove_deceased: the remove deceased flag + :type remove_deceased: bool + :return: the builder + :rtype: WatchlistAdvancedCaProfilesCheckBuilder + """ + self.__remove_deceased = remove_deceased + return self + + def with_share_url(self, share_url): + """ + Sets whether the URL should be shared in results + + :param share_url: the share URL flag + :type share_url: bool + :return: the builder + :rtype: WatchlistAdvancedCaProfilesCheckBuilder + """ + self.__share_url = share_url + return self + + def with_sources(self, sources): + """ + Sets the sources configuration for the check + + :param sources: the sources configuration + :type sources: WatchlistAdvancedCaSourcesConfig + :return: the builder + :rtype: WatchlistAdvancedCaProfilesCheckBuilder + """ + self.__sources = sources + return self + + def build(self): + config = WatchlistAdvancedCaProfilesCheckConfig( + self.__remove_deceased, self.__share_url, self.__sources + ) + return WatchlistAdvancedCaProfilesCheck(config) diff --git a/yoti_python_sdk/doc_scan/session/create/sdk_config.py b/yoti_python_sdk/doc_scan/session/create/sdk_config.py index 37cde6b5..5ce35bfc 100644 --- a/yoti_python_sdk/doc_scan/session/create/sdk_config.py +++ b/yoti_python_sdk/doc_scan/session/create/sdk_config.py @@ -23,6 +23,7 @@ def __init__( error_url, allow_handoff=None, privacy_policy_url=None, + suppressed_screens=None, ): """ :param allowed_capture_methods: the allowed capture methods @@ -45,6 +46,8 @@ def __init__( :type privacy_policy_url: str :param allow_handoff: boolean flag for allow_handoff :type allow_handoff: bool + :param suppressed_screens: the screens to suppress in the IDV flow + :type suppressed_screens: list[str] """ self.__allowed_capture_methods = allowed_capture_methods self.__primary_colour = primary_colour @@ -56,6 +59,7 @@ def __init__( self.__error_url = error_url self.__privacy_policy_url = privacy_policy_url self.__allow_handoff = allow_handoff + self.__suppressed_screens = suppressed_screens @property def allowed_capture_methods(self): @@ -148,6 +152,15 @@ def allow_handoff(self): """ return self.__allow_handoff + @property + def suppressed_screens(self): + """ + The screens to suppress in the IDV flow. + + :return: the suppressed screens + """ + return self.__suppressed_screens + def to_json(self): return remove_null_values( { @@ -161,6 +174,7 @@ def to_json(self): "error_url": self.error_url, "privacy_policy_url": self.privacy_policy_url, "allow_handoff": self.allow_handoff, + "suppressed_screens": self.suppressed_screens, } ) @@ -181,6 +195,7 @@ def __init__(self): self.__error_url = None self.__privacy_policy_url = None self.__allow_handoff = None + self.__suppressed_screens = None def with_allowed_capture_methods(self, allowed_capture_methods): """ @@ -320,6 +335,18 @@ def with_allow_handoff(self, flag): self.__allow_handoff = flag return self + def with_suppressed_screens(self, screens): + """ + Sets the screens to suppress in the IDV flow + + :param screens: the suppressed screen identifiers + :type screens: list[str] + :return: the builder + :rtype: SdkConfigBuilder + """ + self.__suppressed_screens = screens + return self + def build(self): return SdkConfig( self.__allowed_capture_methods, @@ -332,4 +359,5 @@ def build(self): self.__error_url, self.__allow_handoff, self.__privacy_policy_url, + self.__suppressed_screens, ) diff --git a/yoti_python_sdk/doc_scan/session/create/task/__init__.py b/yoti_python_sdk/doc_scan/session/create/task/__init__.py index ab8cec54..3a0ef31a 100644 --- a/yoti_python_sdk/doc_scan/session/create/task/__init__.py +++ b/yoti_python_sdk/doc_scan/session/create/task/__init__.py @@ -2,8 +2,10 @@ from .supplementary_doc_text_extraction import ( RequestedSupplementaryDocTextExtractionTaskBuilder, ) +from .face_capture import RequestedFaceCaptureTaskBuilder __all__ = [ "RequestedTextExtractionTaskBuilder", "RequestedSupplementaryDocTextExtractionTaskBuilder", + "RequestedFaceCaptureTaskBuilder", ] diff --git a/yoti_python_sdk/doc_scan/session/create/task/face_capture.py b/yoti_python_sdk/doc_scan/session/create/task/face_capture.py new file mode 100644 index 00000000..a2b404a8 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/task/face_capture.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from yoti_python_sdk.doc_scan import constants +from yoti_python_sdk.utils import YotiSerializable +from .requested_task import RequestedTask + + +class RequestedFaceCaptureTaskConfig(YotiSerializable): + """ + The configuration applied when creating a Face Capture Task + """ + + def to_json(self): + return {} + + +class RequestedFaceCaptureTask(RequestedTask): + """ + Requests creation of a Face Capture Task + """ + + def __init__(self, config): + """ + :param config: the face capture task configuration + :type config: RequestedFaceCaptureTaskConfig + """ + self.__config = config + + @property + def type(self): + return constants.FACE_CAPTURE + + @property + def config(self): + return self.__config + + +class RequestedFaceCaptureTaskBuilder(object): + """ + Builder to assist creation of :class:`RequestedFaceCaptureTask` + """ + + @staticmethod + def build(): + config = RequestedFaceCaptureTaskConfig() + return RequestedFaceCaptureTask(config) diff --git a/yoti_python_sdk/doc_scan/session/retrieve/check_response.py b/yoti_python_sdk/doc_scan/session/retrieve/check_response.py index f05c4cf5..556686a6 100644 --- a/yoti_python_sdk/doc_scan/session/retrieve/check_response.py +++ b/yoti_python_sdk/doc_scan/session/retrieve/check_response.py @@ -192,3 +192,11 @@ class WatchlistScreeningCheckResponse(CheckResponse): """ pass + + +class WatchlistAdvancedCaProfilesCheckResponse(CheckResponse): + """ + Represents a Watchlist Advanced CA Profiles check for a given session + """ + + pass diff --git a/yoti_python_sdk/doc_scan/session/retrieve/get_session_result.py b/yoti_python_sdk/doc_scan/session/retrieve/get_session_result.py index 7f238281..0c4a715c 100644 --- a/yoti_python_sdk/doc_scan/session/retrieve/get_session_result.py +++ b/yoti_python_sdk/doc_scan/session/retrieve/get_session_result.py @@ -17,6 +17,7 @@ TextDataCheckResponse, SupplementaryDocumentTextDataCheckResponse, WatchlistScreeningCheckResponse, + WatchlistAdvancedCaProfilesCheckResponse, ) from .resource_container import ResourceContainer @@ -81,7 +82,8 @@ def __parse_check(check): constants.ID_DOCUMENT_FACE_MATCH: FaceMatchCheckResponse, constants.ID_DOCUMENT_TEXT_DATA_CHECK: TextDataCheckResponse, constants.LIVENESS: LivenessCheckResponse, - constants.WATCHLIST_SCREENING_CHECK_TYPE: WatchlistScreeningCheckResponse, + constants.WATCHLIST_SCREENING_CHECK_TYPE: WatchlistScreeningCheckResponse, + constants.WATCHLIST_ADVANCED_CA_CHECK_TYPE: WatchlistAdvancedCaProfilesCheckResponse, constants.ID_DOCUMENT_COMPARISON: IDDocumentComparisonCheckResponse, constants.SUPPLEMENTARY_DOCUMENT_TEXT_DATA_CHECK: SupplementaryDocumentTextDataCheckResponse, } @@ -230,6 +232,26 @@ def id_document_comparison_checks(self): """ return self.__checks_of_type((IDDocumentComparisonCheckResponse,)) + @property + def watchlist_screening_checks(self): + """ + A filtered list of checks, returning only Watchlist Screening checks + + :return: the Watchlist Screening checks + :rtype: list[WatchlistScreeningCheckResponse] + """ + return self.__checks_of_type((WatchlistScreeningCheckResponse,)) + + @property + def watchlist_advanced_ca_checks(self): + """ + A filtered list of checks, returning only Watchlist Advanced CA Profiles checks + + :return: the Watchlist Advanced CA Profiles checks + :rtype: list[WatchlistAdvancedCaProfilesCheckResponse] + """ + return self.__checks_of_type((WatchlistAdvancedCaProfilesCheckResponse,)) + @property def resources(self): """ diff --git a/yoti_python_sdk/doc_scan/session/retrieve/resource_response.py b/yoti_python_sdk/doc_scan/session/retrieve/resource_response.py index bd5c8087..0fc35661 100644 --- a/yoti_python_sdk/doc_scan/session/retrieve/resource_response.py +++ b/yoti_python_sdk/doc_scan/session/retrieve/resource_response.py @@ -3,6 +3,7 @@ TaskResponse, TextExtractionTaskResponse, SupplementaryDocumentTextExtractionTaskResponse, + FaceCaptureTaskResponse, ) @@ -35,6 +36,7 @@ def __parse_task(task): types = { constants.ID_DOCUMENT_TEXT_DATA_EXTRACTION: TextExtractionTaskResponse, constants.SUPPLEMENTARY_DOCUMENT_TEXT_DATA_EXTRACTION: SupplementaryDocumentTextExtractionTaskResponse, + constants.FACE_CAPTURE: FaceCaptureTaskResponse, } clazz = types.get( task.get("type", None), TaskResponse # Default fallback for task type diff --git a/yoti_python_sdk/doc_scan/session/retrieve/task_response.py b/yoti_python_sdk/doc_scan/session/retrieve/task_response.py index 899e140b..c0d173c7 100644 --- a/yoti_python_sdk/doc_scan/session/retrieve/task_response.py +++ b/yoti_python_sdk/doc_scan/session/retrieve/task_response.py @@ -179,3 +179,11 @@ def generated_text_data_checks(self): for check in self.generated_checks if isinstance(check, GeneratedSupplementaryDocumentTextDataCheckResponse) ] + + +class FaceCaptureTaskResponse(TaskResponse): + """ + Represents a Face Capture task response + """ + + pass diff --git a/yoti_python_sdk/tests/doc_scan/session/create/test_sdk_config.py b/yoti_python_sdk/tests/doc_scan/session/create/test_sdk_config.py index d621a441..6615fba5 100644 --- a/yoti_python_sdk/tests/doc_scan/session/create/test_sdk_config.py +++ b/yoti_python_sdk/tests/doc_scan/session/create/test_sdk_config.py @@ -1,6 +1,7 @@ import json import unittest +from yoti_python_sdk.doc_scan import constants from yoti_python_sdk.doc_scan.session.create import SdkConfigBuilder from yoti_python_sdk.doc_scan.session.create.sdk_config import SdkConfig from yoti_python_sdk.utils import YotiEncoder @@ -16,6 +17,15 @@ class SdkConfigTest(unittest.TestCase): SOME_ERROR_URL = "https://mysite.com/yoti/error" SOME_PRIVACY_POLICY_URL = "https://mysite.com/privacy" SOME_ALLOW_HANDOFF = True + SOME_SUPPRESSED_SCREENS = [ + constants.ID_DOCUMENT_EDUCATION, + constants.ID_DOCUMENT_REQUIREMENTS, + constants.SUPPLEMENTARY_DOCUMENT_EDUCATION, + constants.ZOOM_LIVENESS_EDUCATION, + constants.STATIC_LIVENESS_EDUCATION, + constants.FACE_CAPTURE_EDUCATION, + constants.FLOW_COMPLETION, + ] def test_should_build_correctly(self): result = ( @@ -30,6 +40,7 @@ def test_should_build_correctly(self): .with_error_url(self.SOME_ERROR_URL) .with_privacy_policy_url(self.SOME_PRIVACY_POLICY_URL) .with_allow_handoff(self.SOME_ALLOW_HANDOFF) + .with_suppressed_screens(self.SOME_SUPPRESSED_SCREENS) .build() ) @@ -44,6 +55,7 @@ def test_should_build_correctly(self): assert result.error_url is self.SOME_ERROR_URL assert result.privacy_policy_url is self.SOME_PRIVACY_POLICY_URL assert result.allow_handoff is True + assert result.suppressed_screens is self.SOME_SUPPRESSED_SCREENS def test_should_allows_camera(self): result = SdkConfigBuilder().with_allows_camera().build() @@ -60,6 +72,32 @@ def test_passing_allow_handoff_false_value(self): assert result.allow_handoff is False + def test_not_passing_suppressed_screens(self): + result = SdkConfigBuilder().with_allows_camera().build() + + assert result.suppressed_screens is None + + def test_passing_empty_suppressed_screens(self): + result = SdkConfigBuilder().with_suppressed_screens([]).build() + + assert result.suppressed_screens == [] + + def test_suppressed_screens_serializes_to_json(self): + result = ( + SdkConfigBuilder() + .with_suppressed_screens(self.SOME_SUPPRESSED_SCREENS) + .build() + ) + + payload = result.to_json() + assert payload["suppressed_screens"] == self.SOME_SUPPRESSED_SCREENS + + def test_unset_suppressed_screens_not_in_json(self): + result = SdkConfigBuilder().with_allows_camera().build() + + payload = result.to_json() + assert "suppressed_screens" not in payload + def test_should_serialize_to_json_without_error(self): result = ( SdkConfigBuilder()