From c0cef0a8e3623a57f8a1c1f5110ff460a626e7e0 Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:09:03 +0300 Subject: [PATCH 01/12] Sdk 2795 php upgrade to protobuf 4 33 6 and phpseclib 3 0 50 (#408) * SDK-2795-php-upgrade-to-protobuf-4-33-6-and-phpseclib-3-0-50 --- .github/workflows/tests.yaml | 44 ++++++++++++++++-------------------- README.md | 6 ++--- composer.json | 8 +++---- src/Constants.php | 2 +- 4 files changed, 28 insertions(+), 32 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 1c8a1e9c..ac6b83a5 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -8,8 +8,8 @@ jobs: # always run on push events # only run on pull_request_target event when pull request pulls from fork repository if: > - github.event_name == 'push' || - github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository + github.event_name == 'push' || + github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository strategy: fail-fast: false matrix: @@ -31,18 +31,16 @@ jobs: - run: composer test - php8-4: - name: Unit Tests php8.4 (php ${{ matrix.php-version }}) + php8-2: + name: Unit Tests php8.2 (php ${{ matrix.php-version }}) runs-on: ubuntu-latest - # always run on push events - # only run on pull_request_target event when pull request pulls from fork repository if: > github.event_name == 'push' || github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository strategy: fail-fast: false matrix: - php-version: [ 8.4 ] + php-version: [ 8.2 ] steps: - uses: actions/checkout@v2 @@ -51,8 +49,6 @@ jobs: with: php-version: ${{ matrix.php-version }} - # Remove php-cs-fixer until compatible with PHP 8 - # This step might be removable if php-cs-fixer is compatible with 8.4 by the time this runs - run: composer remove --dev --no-update --no-interaction friendsofphp/php-cs-fixer - run: composer self-update @@ -61,18 +57,16 @@ jobs: - run: composer test - php7-4: - name: Unit Tests php7.4 (php ${{ matrix.php-version }}) + php8-3: + name: Unit Tests php8.3 (php ${{ matrix.php-version }}) runs-on: ubuntu-latest - # always run on push events - # only run on pull_request_target event when pull request pulls from fork repository if: > - github.event_name == 'push' || + github.event_name == 'push' || github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository strategy: fail-fast: false matrix: - php-version: [ 7.4 ] + php-version: [ 8.3 ] steps: - uses: actions/checkout@v2 @@ -81,24 +75,24 @@ jobs: with: php-version: ${{ matrix.php-version }} + - run: composer remove --dev --no-update --no-interaction friendsofphp/php-cs-fixer + - run: composer self-update - run: composer install --no-interaction --prefer-source --dev - run: composer test - php8-0: - name: Unit Tests php8.0 (php ${{ matrix.php-version }}) + php8-4: + name: Unit Tests php8.4 (php ${{ matrix.php-version }}) runs-on: ubuntu-latest - # always run on push events - # only run on pull_request_target event when pull request pulls from fork repository if: > - github.event_name == 'push' || + github.event_name == 'push' || github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository strategy: fail-fast: false matrix: - php-version: [ 8.0 ] + php-version: [ 8.4 ] steps: - uses: actions/checkout@v2 @@ -107,6 +101,8 @@ jobs: with: php-version: ${{ matrix.php-version }} + - run: composer remove --dev --no-update --no-interaction friendsofphp/php-cs-fixer + - run: composer self-update - run: composer install --no-interaction --prefer-source --dev @@ -114,13 +110,13 @@ jobs: - run: composer test protobuf: - name: Unit Tests With Protobuf C Extension 3.13 (php ${{ matrix.php-version }}) + name: Unit Tests With Protobuf C Extension (php ${{ matrix.php-version }}) runs-on: ubuntu-latest # always run on push events # only run on pull_request_target event when pull request pulls from fork repository if: > - github.event_name == 'push' || - github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository + github.event_name == 'push' || + github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository strategy: fail-fast: false matrix: diff --git a/README.md b/README.md index 371c4fba..a68b056b 100755 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Please feel free to reach out ## Requirements -* PHP ^7.4 || ^8.0 || ^8.1 +* PHP ^8.1 * CURL PHP extension (must support TLSv1.2) ### Recommended (optional) @@ -42,13 +42,13 @@ Add the Yoti SDK dependency: ```json "require": { - "yoti/yoti-php-sdk" : "^4.4.1" + "yoti/yoti-php-sdk" : "^4.5.0" } ``` Or run this Composer command ```console -$ composer require yoti/yoti-php-sdk "^4.4.1" +$ composer require yoti/yoti-php-sdk "^4.5.0" ``` ## Setup diff --git a/composer.json b/composer.json index a78e472c..91b4d313 100755 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "yoti/yoti-php-sdk", "description": "Yoti SDK for quickly integrating your PHP backend with Yoti", - "version": "4.4.1", + "version": "4.5.0", "keywords": [ "yoti", "sdk" @@ -9,10 +9,10 @@ "homepage": "https://yoti.com", "license": "MIT", "require": { - "php": "^7.4 || ^8.0 || ^8.1 || ^8.4", + "php": "^8.1", "ext-json": "*", - "google/protobuf": "^3.10", - "phpseclib/phpseclib": "^3.0", + "google/protobuf": "^4.33.6", + "phpseclib/phpseclib": "^3.0.50", "guzzlehttp/guzzle": "^7.0", "psr/http-client": "^1.0", "psr/http-message": "^2.0", diff --git a/src/Constants.php b/src/Constants.php index 9ac17a1d..921b1fbe 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -37,7 +37,7 @@ class Constants public const SDK_IDENTIFIER = 'PHP'; /** Default SDK version */ - public const SDK_VERSION = '4.4.1'; + public const SDK_VERSION = '4.5.0'; /** Base url for connect page (user will be redirected to this page eg. baseurl/app-id) */ public const CONNECT_BASE_URL = 'https://www.yoti.com/connect'; From 6acbf1c35674b31ff2d07f9816c29e4e8131725a Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:30:13 +0300 Subject: [PATCH 02/12] Sdk 2791 php add support for retrieving the extraction image ids field from the idv pages (#409) * SDK-2791-php-add-support-for-retrieving-the-extraction-image-ids-field-from-the-idv-pages * Update README.md --- README.md | 3 +++ src/DocScan/Session/Retrieve/PageResponse.php | 15 ++++++++++++++ .../Session/Retrieve/PageResponseTest.php | 20 +++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/README.md b/README.md index a68b056b..4cfbbe17 100755 --- a/README.md +++ b/README.md @@ -30,6 +30,9 @@ Please feel free to reach out * PHP ^8.1 * CURL PHP extension (must support TLSv1.2) +> **Breaking change:** this SDK release supports PHP 8.1 and above only. +> Support for PHP 7.4 and PHP 8.0 has been removed. +> If you are still running on PHP 7.4/8.0, please remain on an earlier SDK version until you can upgrade your runtime. ### Recommended (optional) - [Protobuf C extension](https://github.com/protocolbuffers/protobuf/tree/master/php) (PHP package will be used by default) diff --git a/src/DocScan/Session/Retrieve/PageResponse.php b/src/DocScan/Session/Retrieve/PageResponse.php index 163b9ecf..eb3e6c45 100644 --- a/src/DocScan/Session/Retrieve/PageResponse.php +++ b/src/DocScan/Session/Retrieve/PageResponse.php @@ -21,6 +21,11 @@ class PageResponse */ private $frames = []; + /** + * @var string[] + */ + private $extractionImageIds = []; + /** * PageInfo constructor. * @param array $page @@ -38,6 +43,8 @@ public function __construct(array $page) $this->frames[] = new FrameResponse($frame); } } + + $this->extractionImageIds = $page['extraction_image_ids'] ?? []; } /** @@ -63,4 +70,12 @@ public function getFrames(): array { return $this->frames; } + + /** + * @return string[] + */ + public function getExtractionImageIds(): array + { + return $this->extractionImageIds; + } } diff --git a/tests/DocScan/Session/Retrieve/PageResponseTest.php b/tests/DocScan/Session/Retrieve/PageResponseTest.php index 91306f12..89527f10 100644 --- a/tests/DocScan/Session/Retrieve/PageResponseTest.php +++ b/tests/DocScan/Session/Retrieve/PageResponseTest.php @@ -56,6 +56,25 @@ public function testGetFrames() $this->containsOnlyInstancesOf(FrameResponse::class, $pageResponse->getFrames()); } + /** + * @covers ::__construct + * @covers ::getExtractionImageIds + */ + public function testGetExtractionImageIds() + { + $extractionImageIds = [ + '066a9372-0a52-4fe4-a026-866f8aee6fcb', + '9b0c9c0a-ff30-41ed-815b-d95d63271d45', + ]; + + $pageResponse = new PageResponse([ + 'extraction_image_ids' => $extractionImageIds, + ]); + + $this->assertCount(2, $pageResponse->getExtractionImageIds()); + $this->assertEquals($extractionImageIds, $pageResponse->getExtractionImageIds()); + } + /** * @test * @covers ::__construct @@ -67,5 +86,6 @@ public function shouldNotThrowExceptionWhenMissingValues() $this->assertNull($result->getCaptureMethod()); $this->assertNull($result->getMedia()); $this->assertEquals([], $result->getFrames()); + $this->assertEquals([], $result->getExtractionImageIds()); } } From 9ba67c805b7d1ffad5c568803995d78850c6e2b4 Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:31:10 +0300 Subject: [PATCH 03/12] Sdk 2742 php expose idv breakdown process property (#410) * SDK-2742-php-expose-idv-breakdown-process-property --- src/DocScan/Session/Retrieve/BreakdownResponse.php | 14 ++++++++++++++ .../Session/Retrieve/BreakdownResponseTest.php | 7 +++++++ 2 files changed, 21 insertions(+) diff --git a/src/DocScan/Session/Retrieve/BreakdownResponse.php b/src/DocScan/Session/Retrieve/BreakdownResponse.php index 8e9ba9c3..410e635c 100644 --- a/src/DocScan/Session/Retrieve/BreakdownResponse.php +++ b/src/DocScan/Session/Retrieve/BreakdownResponse.php @@ -16,6 +16,11 @@ class BreakdownResponse */ private $result; + /** + * @var string|null + */ + private $process; + /** * @var DetailsResponse[] */ @@ -29,6 +34,7 @@ public function __construct(array $breakdown) { $this->subCheck = $breakdown['sub_check'] ?? null; $this->result = $breakdown['result'] ?? null; + $this->process = $breakdown['process'] ?? null; if (isset($breakdown['details'])) { foreach ($breakdown['details'] as $detail) { @@ -53,6 +59,14 @@ public function getResult(): ?string return $this->result; } + /** + * @return string|null + */ + public function getProcess(): ?string + { + return $this->process; + } + /** * @return DetailsResponse[] */ diff --git a/tests/DocScan/Session/Retrieve/BreakdownResponseTest.php b/tests/DocScan/Session/Retrieve/BreakdownResponseTest.php index dc92d2fd..9e4e9a30 100644 --- a/tests/DocScan/Session/Retrieve/BreakdownResponseTest.php +++ b/tests/DocScan/Session/Retrieve/BreakdownResponseTest.php @@ -25,11 +25,14 @@ class BreakdownResponseTest extends TestCase ], ]; + private const SOME_PROCESS = 'AUTOMATED'; + /** * @test * @covers ::__construct * @covers ::getSubCheck * @covers ::getResult + * @covers ::getProcess * @covers ::getDetails * @covers \Yoti\DocScan\Session\Retrieve\DetailsResponse::__construct * @covers \Yoti\DocScan\Session\Retrieve\DetailsResponse::getName @@ -40,6 +43,7 @@ public function shouldBuildCorrectly() $input = [ 'sub_check' => self::SOME_SUB_CHECK, 'result' => self::SOME_RESULT, + 'process' => self::SOME_PROCESS, 'details' => self::SOME_DETAILS, ]; @@ -47,6 +51,7 @@ public function shouldBuildCorrectly() $this->assertEquals(self::SOME_SUB_CHECK, $result->getSubCheck()); $this->assertEquals(self::SOME_RESULT, $result->getResult()); + $this->assertEquals(self::SOME_PROCESS, $result->getProcess()); $details = $result->getDetails(); for ($i = 0; $i < count(self::SOME_DETAILS); $i++) { @@ -61,6 +66,7 @@ public function shouldBuildCorrectly() * @covers ::__construct * @covers ::getSubCheck * @covers ::getResult + * @covers ::getProcess * @covers ::getDetails */ public function shouldNotThrowExceptionWhenValuesAreMissing() @@ -71,6 +77,7 @@ public function shouldNotThrowExceptionWhenValuesAreMissing() $this->assertNull($result->getSubCheck()); $this->assertNull($result->getResult()); + $this->assertNull($result->getProcess()); $this->assertCount(0, $result->getDetails()); } } From 648c99964b6672ae2615e64fb7f323b62d55e17c Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:35:15 +0300 Subject: [PATCH 04/12] SDK-2793-php-task-response-recommandation added (#404) --- .../TaskRecommendationReasonResponse.php | 44 +++++++++++++++ .../Retrieve/TaskRecommendationResponse.php | 47 ++++++++++++++++ src/DocScan/Session/Retrieve/TaskResponse.php | 17 ++++++ .../TaskRecommendationReasonResponseTest.php | 48 ++++++++++++++++ .../TaskRecommendationResponseTest.php | 55 +++++++++++++++++++ .../Session/Retrieve/TaskResponseTest.php | 29 ++++++++++ 6 files changed, 240 insertions(+) create mode 100644 src/DocScan/Session/Retrieve/TaskRecommendationReasonResponse.php create mode 100644 src/DocScan/Session/Retrieve/TaskRecommendationResponse.php create mode 100644 tests/DocScan/Session/Retrieve/TaskRecommendationReasonResponseTest.php create mode 100644 tests/DocScan/Session/Retrieve/TaskRecommendationResponseTest.php diff --git a/src/DocScan/Session/Retrieve/TaskRecommendationReasonResponse.php b/src/DocScan/Session/Retrieve/TaskRecommendationReasonResponse.php new file mode 100644 index 00000000..c365b7cc --- /dev/null +++ b/src/DocScan/Session/Retrieve/TaskRecommendationReasonResponse.php @@ -0,0 +1,44 @@ + $reason + */ + public function __construct(array $reason) + { + $this->value = $reason['value'] ?? null; + $this->detail = $reason['detail'] ?? null; + } + + /** + * @return string|null + */ + public function getValue(): ?string + { + return $this->value; + } + + /** + * @return string|null + */ + public function getDetail(): ?string + { + return $this->detail; + } +} diff --git a/src/DocScan/Session/Retrieve/TaskRecommendationResponse.php b/src/DocScan/Session/Retrieve/TaskRecommendationResponse.php new file mode 100644 index 00000000..9e4ffa72 --- /dev/null +++ b/src/DocScan/Session/Retrieve/TaskRecommendationResponse.php @@ -0,0 +1,47 @@ + $recommendation + */ + public function __construct(array $recommendation) + { + $this->value = $recommendation['value'] ?? null; + + if (isset($recommendation['reason'])) { + $this->reason = new TaskRecommendationReasonResponse($recommendation['reason']); + } + } + + /** + * @return string|null + */ + public function getValue(): ?string + { + return $this->value; + } + + /** + * @return TaskRecommendationReasonResponse|null + */ + public function getReason(): ?TaskRecommendationReasonResponse + { + return $this->reason; + } +} diff --git a/src/DocScan/Session/Retrieve/TaskResponse.php b/src/DocScan/Session/Retrieve/TaskResponse.php index e824cc51..caee2233 100644 --- a/src/DocScan/Session/Retrieve/TaskResponse.php +++ b/src/DocScan/Session/Retrieve/TaskResponse.php @@ -44,6 +44,11 @@ class TaskResponse */ private $generatedMedia = []; + /** + * @var TaskRecommendationResponse|null + */ + private $recommendation; + /** * TaskResponse constructor. * @param array $task @@ -67,6 +72,10 @@ public function __construct(array $task) if (isset($task['generated_media'])) { $this->generatedMedia = $this->parseGeneratedMedia($task['generated_media']); } + + if (isset($task['recommendation'])) { + $this->recommendation = new TaskRecommendationResponse($task['recommendation']); + } } /** @@ -161,6 +170,14 @@ public function getGeneratedMedia(): array return $this->generatedMedia; } + /** + * @return TaskRecommendationResponse|null + */ + public function getRecommendation(): ?TaskRecommendationResponse + { + return $this->recommendation; + } + /** * @return GeneratedCheckResponse[] */ diff --git a/tests/DocScan/Session/Retrieve/TaskRecommendationReasonResponseTest.php b/tests/DocScan/Session/Retrieve/TaskRecommendationReasonResponseTest.php new file mode 100644 index 00000000..80c0a74c --- /dev/null +++ b/tests/DocScan/Session/Retrieve/TaskRecommendationReasonResponseTest.php @@ -0,0 +1,48 @@ + self::SOME_VALUE, + 'detail' => self::SOME_DETAIL, + ]; + + $result = new TaskRecommendationReasonResponse($input); + + $this->assertEquals(self::SOME_VALUE, $result->getValue()); + $this->assertEquals(self::SOME_DETAIL, $result->getDetail()); + } + + /** + * @test + * @covers ::__construct + */ + public function shouldNotThrowExceptionWhenMissingValues() + { + $result = new TaskRecommendationReasonResponse([]); + + $this->assertNull($result->getValue()); + $this->assertNull($result->getDetail()); + } +} diff --git a/tests/DocScan/Session/Retrieve/TaskRecommendationResponseTest.php b/tests/DocScan/Session/Retrieve/TaskRecommendationResponseTest.php new file mode 100644 index 00000000..46c265b9 --- /dev/null +++ b/tests/DocScan/Session/Retrieve/TaskRecommendationResponseTest.php @@ -0,0 +1,55 @@ + self::SOME_VALUE, + 'reason' => [ + 'value' => self::SOME_REASON_VALUE, + 'detail' => self::SOME_REASON_DETAIL, + ], + ]; + + $result = new TaskRecommendationResponse($input); + + $this->assertEquals(self::SOME_VALUE, $result->getValue()); + $this->assertInstanceOf(TaskRecommendationReasonResponse::class, $result->getReason()); + $this->assertEquals(self::SOME_REASON_VALUE, $result->getReason()->getValue()); + $this->assertEquals(self::SOME_REASON_DETAIL, $result->getReason()->getDetail()); + } + + /** + * @test + * @covers ::__construct + */ + public function shouldNotThrowExceptionWhenMissingValues() + { + $result = new TaskRecommendationResponse([]); + + $this->assertNull($result->getValue()); + $this->assertNull($result->getReason()); + } +} diff --git a/tests/DocScan/Session/Retrieve/TaskResponseTest.php b/tests/DocScan/Session/Retrieve/TaskResponseTest.php index 2b63366b..b3483119 100644 --- a/tests/DocScan/Session/Retrieve/TaskResponseTest.php +++ b/tests/DocScan/Session/Retrieve/TaskResponseTest.php @@ -7,6 +7,8 @@ use Yoti\DocScan\Session\Retrieve\GeneratedCheckResponse; use Yoti\DocScan\Session\Retrieve\GeneratedMedia; use Yoti\DocScan\Session\Retrieve\GeneratedTextDataCheckResponse; +use Yoti\DocScan\Session\Retrieve\TaskRecommendationReasonResponse; +use Yoti\DocScan\Session\Retrieve\TaskRecommendationResponse; use Yoti\DocScan\Session\Retrieve\TaskResponse; use Yoti\Test\TestCase; use Yoti\Util\DateTime; @@ -25,6 +27,9 @@ class TaskResponseTest extends TestCase private const SOME_UNKNOWN_TYPE = 'someUnknownType'; private const ID_DOCUMENT_TEXT_DATA_CHECK = 'ID_DOCUMENT_TEXT_DATA_CHECK'; private const SUPPLEMENTARY_DOCUMENT_TEXT_DATA_CHECK = 'SUPPLEMENTARY_DOCUMENT_TEXT_DATA_CHECK'; + private const SOME_RECOMMENDATION_VALUE = 'MUST_TRY_AGAIN'; + private const SOME_RECOMMENDATION_REASON_VALUE = 'USER_ERROR'; + private const SOME_RECOMMENDATION_REASON_DETAIL = 'NO_DOCUMENT'; /** * @var TaskResponse @@ -57,6 +62,13 @@ public function setup(): void [], [], ], + 'recommendation' => [ + 'value' => self::SOME_RECOMMENDATION_VALUE, + 'reason' => [ + 'value' => self::SOME_RECOMMENDATION_REASON_VALUE, + 'detail' => self::SOME_RECOMMENDATION_REASON_DETAIL, + ], + ], ]); } @@ -185,5 +197,22 @@ public function shouldNotThrowExceptionWhenAllMissingValuesExceptType() $this->assertNull($result->getLastUpdated()); $this->assertCount(0, $result->getGeneratedChecks()); $this->assertCount(0, $result->getGeneratedMedia()); + $this->assertNull($result->getRecommendation()); + } + + /** + * @test + * @covers ::__construct + * @covers ::getRecommendation + */ + public function shouldReturnRecommendation() + { + $recommendation = $this->taskResponse->getRecommendation(); + + $this->assertInstanceOf(TaskRecommendationResponse::class, $recommendation); + $this->assertEquals(self::SOME_RECOMMENDATION_VALUE, $recommendation->getValue()); + $this->assertInstanceOf(TaskRecommendationReasonResponse::class, $recommendation->getReason()); + $this->assertEquals(self::SOME_RECOMMENDATION_REASON_VALUE, $recommendation->getReason()->getValue()); + $this->assertEquals(self::SOME_RECOMMENDATION_REASON_DETAIL, $recommendation->getReason()->getDetail()); } } From 83c62fe1663a97535efe3fa727651c37e89f7851 Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:37:37 +0300 Subject: [PATCH 05/12] SDK-2215 allow rb to supply an applicant profile for identity profile sessions (#405) --- .../Session/Create/ApplicantProfile.php | 95 ++++++++++ .../Create/ApplicantProfileBuilder.php | 81 +++++++++ .../Create/ResourceCreationContainer.php | 43 +++++ .../ResourceCreationContainerBuilder.php | 33 ++++ .../Session/Create/SessionSpecification.php | 19 +- .../Create/SessionSpecificationBuilder.php | 19 ++ .../Create/StructuredPostalAddress.php | 163 ++++++++++++++++++ .../Create/StructuredPostalAddressBuilder.php | 145 ++++++++++++++++ .../Create/ApplicantProfileBuilderTest.php | 142 +++++++++++++++ .../ResourceCreationContainerBuilderTest.php | 93 ++++++++++ .../SessionSpecificationBuilderTest.php | 65 +++++++ .../StructuredPostalAddressBuilderTest.php | 121 +++++++++++++ 12 files changed, 1018 insertions(+), 1 deletion(-) create mode 100644 src/DocScan/Session/Create/ApplicantProfile.php create mode 100644 src/DocScan/Session/Create/ApplicantProfileBuilder.php create mode 100644 src/DocScan/Session/Create/ResourceCreationContainer.php create mode 100644 src/DocScan/Session/Create/ResourceCreationContainerBuilder.php create mode 100644 src/DocScan/Session/Create/StructuredPostalAddress.php create mode 100644 src/DocScan/Session/Create/StructuredPostalAddressBuilder.php create mode 100644 tests/DocScan/Session/Create/ApplicantProfileBuilderTest.php create mode 100644 tests/DocScan/Session/Create/ResourceCreationContainerBuilderTest.php create mode 100644 tests/DocScan/Session/Create/StructuredPostalAddressBuilderTest.php diff --git a/src/DocScan/Session/Create/ApplicantProfile.php b/src/DocScan/Session/Create/ApplicantProfile.php new file mode 100644 index 00000000..0eb26dc0 --- /dev/null +++ b/src/DocScan/Session/Create/ApplicantProfile.php @@ -0,0 +1,95 @@ +fullName = $fullName; + $this->dateOfBirth = $dateOfBirth; + $this->namePrefix = $namePrefix; + $this->structuredPostalAddress = $structuredPostalAddress; + } + + /** + * @return stdClass + */ + public function jsonSerialize(): stdClass + { + return (object) Json::withoutNullValues([ + 'full_name' => $this->fullName, + 'date_of_birth' => $this->dateOfBirth, + 'name_prefix' => $this->namePrefix, + 'structured_postal_address' => $this->structuredPostalAddress, + ]); + } + + /** + * @return string|null + */ + public function getFullName(): ?string + { + return $this->fullName; + } + + /** + * @return string|null + */ + public function getDateOfBirth(): ?string + { + return $this->dateOfBirth; + } + + /** + * @return string|null + */ + public function getNamePrefix(): ?string + { + return $this->namePrefix; + } + + /** + * @return StructuredPostalAddress|null + */ + public function getStructuredPostalAddress(): ?StructuredPostalAddress + { + return $this->structuredPostalAddress; + } +} diff --git a/src/DocScan/Session/Create/ApplicantProfileBuilder.php b/src/DocScan/Session/Create/ApplicantProfileBuilder.php new file mode 100644 index 00000000..e0c85d3d --- /dev/null +++ b/src/DocScan/Session/Create/ApplicantProfileBuilder.php @@ -0,0 +1,81 @@ +fullName = $fullName; + return $this; + } + + /** + * @param string $dateOfBirth + * @return $this + */ + public function withDateOfBirth(string $dateOfBirth): self + { + $this->dateOfBirth = $dateOfBirth; + return $this; + } + + /** + * @param string $namePrefix + * @return $this + */ + public function withNamePrefix(string $namePrefix): self + { + $this->namePrefix = $namePrefix; + return $this; + } + + /** + * @param StructuredPostalAddress $structuredPostalAddress + * @return $this + */ + public function withStructuredPostalAddress(StructuredPostalAddress $structuredPostalAddress): self + { + $this->structuredPostalAddress = $structuredPostalAddress; + return $this; + } + + /** + * @return ApplicantProfile + */ + public function build(): ApplicantProfile + { + return new ApplicantProfile( + $this->fullName, + $this->dateOfBirth, + $this->namePrefix, + $this->structuredPostalAddress + ); + } +} diff --git a/src/DocScan/Session/Create/ResourceCreationContainer.php b/src/DocScan/Session/Create/ResourceCreationContainer.php new file mode 100644 index 00000000..ebbbd776 --- /dev/null +++ b/src/DocScan/Session/Create/ResourceCreationContainer.php @@ -0,0 +1,43 @@ +applicantProfile = $applicantProfile; + } + + /** + * @return stdClass + */ + public function jsonSerialize(): stdClass + { + return (object) Json::withoutNullValues([ + 'applicant_profile' => $this->applicantProfile, + ]); + } + + /** + * @return ApplicantProfile|null + */ + public function getApplicantProfile(): ?ApplicantProfile + { + return $this->applicantProfile; + } +} diff --git a/src/DocScan/Session/Create/ResourceCreationContainerBuilder.php b/src/DocScan/Session/Create/ResourceCreationContainerBuilder.php new file mode 100644 index 00000000..9ea9e394 --- /dev/null +++ b/src/DocScan/Session/Create/ResourceCreationContainerBuilder.php @@ -0,0 +1,33 @@ +applicantProfile = $applicantProfile; + return $this; + } + + /** + * @return ResourceCreationContainer + */ + public function build(): ResourceCreationContainer + { + return new ResourceCreationContainer( + $this->applicantProfile + ); + } +} diff --git a/src/DocScan/Session/Create/SessionSpecification.php b/src/DocScan/Session/Create/SessionSpecification.php index e080e5d0..060245d7 100644 --- a/src/DocScan/Session/Create/SessionSpecification.php +++ b/src/DocScan/Session/Create/SessionSpecification.php @@ -90,6 +90,11 @@ class SessionSpecification implements JsonSerializable */ private $importToken; + /** + * @var ResourceCreationContainer|null + */ + private $resources; + /** * @param int|null $clientSessionTokenTtl * @param string|null $sessionDeadline @@ -107,6 +112,7 @@ class SessionSpecification implements JsonSerializable * @param object|null $advancedIdentityProfileRequirements * @param bool|null $createIdentityProfilePreview * @param ImportToken|null $importToken + * @param ResourceCreationContainer|null $resources */ public function __construct( ?int $clientSessionTokenTtl, @@ -124,7 +130,8 @@ public function __construct( ?object $identityProfileRequirements = null, ?object $advancedIdentityProfileRequirements = null, ?bool $createIdentityProfilePreview = null, - ?ImportToken $importToken = null + ?ImportToken $importToken = null, + ?ResourceCreationContainer $resources = null ) { $this->clientSessionTokenTtl = $clientSessionTokenTtl; $this->sessionDeadline = $sessionDeadline; @@ -142,6 +149,7 @@ public function __construct( $this->advancedIdentityProfileRequirements = $advancedIdentityProfileRequirements; $this->createIdentityProfilePreview = $createIdentityProfilePreview; $this->importToken = $importToken; + $this->resources = $resources; } /** @@ -166,6 +174,7 @@ public function jsonSerialize(): stdClass 'advanced_identity_profile_requirements' => $this->getAdvancedIdentityProfileRequirements(), 'create_identity_profile_preview' => $this->getCreateIdentityProfilePreview(), 'import_token' => $this->getImportToken(), + 'resources' => $this->getResources(), ]); } @@ -295,4 +304,12 @@ public function getImportToken(): ?ImportToken { return $this->importToken; } + + /** + * @return ResourceCreationContainer|null + */ + public function getResources(): ?ResourceCreationContainer + { + return $this->resources; + } } diff --git a/src/DocScan/Session/Create/SessionSpecificationBuilder.php b/src/DocScan/Session/Create/SessionSpecificationBuilder.php index 5afc9c0c..2348a16e 100644 --- a/src/DocScan/Session/Create/SessionSpecificationBuilder.php +++ b/src/DocScan/Session/Create/SessionSpecificationBuilder.php @@ -88,6 +88,11 @@ class SessionSpecificationBuilder */ private $importToken; + /** + * @var ResourceCreationContainer|null + */ + private $resources; + /** * @var bool */ @@ -292,6 +297,19 @@ public function withImportToken($importToken): self return $this; } + /** + * Sets the resources for the session + * + * @param ResourceCreationContainer $resources + * + * @return $this + */ + public function withResources(ResourceCreationContainer $resources): self + { + $this->resources = $resources; + return $this; + } + /** * @return SessionSpecification */ @@ -314,6 +332,7 @@ public function build(): SessionSpecification $this->advancedIdentityProfileRequirements, $this->createIdentityProfilePreview, $this->importToken, + $this->resources, ); } } diff --git a/src/DocScan/Session/Create/StructuredPostalAddress.php b/src/DocScan/Session/Create/StructuredPostalAddress.php new file mode 100644 index 00000000..bb915e4f --- /dev/null +++ b/src/DocScan/Session/Create/StructuredPostalAddress.php @@ -0,0 +1,163 @@ +addressFormat = $addressFormat; + $this->buildingNumber = $buildingNumber; + $this->addressLine1 = $addressLine1; + $this->townCity = $townCity; + $this->postalCode = $postalCode; + $this->countryIso = $countryIso; + $this->country = $country; + $this->formattedAddress = $formattedAddress; + } + + /** + * @return stdClass + */ + public function jsonSerialize(): stdClass + { + return (object) Json::withoutNullValues([ + 'address_format' => $this->addressFormat, + 'building_number' => $this->buildingNumber, + 'address_line1' => $this->addressLine1, + 'town_city' => $this->townCity, + 'postal_code' => $this->postalCode, + 'country_iso' => $this->countryIso, + 'country' => $this->country, + 'formatted_address' => $this->formattedAddress, + ]); + } + + /** + * @return int|null + */ + public function getAddressFormat(): ?int + { + return $this->addressFormat; + } + + /** + * @return string|null + */ + public function getBuildingNumber(): ?string + { + return $this->buildingNumber; + } + + /** + * @return string|null + */ + public function getAddressLine1(): ?string + { + return $this->addressLine1; + } + + /** + * @return string|null + */ + public function getTownCity(): ?string + { + return $this->townCity; + } + + /** + * @return string|null + */ + public function getPostalCode(): ?string + { + return $this->postalCode; + } + + /** + * @return string|null + */ + public function getCountryIso(): ?string + { + return $this->countryIso; + } + + /** + * @return string|null + */ + public function getCountry(): ?string + { + return $this->country; + } + + /** + * @return string|null + */ + public function getFormattedAddress(): ?string + { + return $this->formattedAddress; + } +} diff --git a/src/DocScan/Session/Create/StructuredPostalAddressBuilder.php b/src/DocScan/Session/Create/StructuredPostalAddressBuilder.php new file mode 100644 index 00000000..3750c25f --- /dev/null +++ b/src/DocScan/Session/Create/StructuredPostalAddressBuilder.php @@ -0,0 +1,145 @@ +addressFormat = $addressFormat; + return $this; + } + + /** + * @param string $buildingNumber + * @return $this + */ + public function withBuildingNumber(string $buildingNumber): self + { + $this->buildingNumber = $buildingNumber; + return $this; + } + + /** + * @param string $addressLine1 + * @return $this + */ + public function withAddressLine1(string $addressLine1): self + { + $this->addressLine1 = $addressLine1; + return $this; + } + + /** + * @param string $townCity + * @return $this + */ + public function withTownCity(string $townCity): self + { + $this->townCity = $townCity; + return $this; + } + + /** + * @param string $postalCode + * @return $this + */ + public function withPostalCode(string $postalCode): self + { + $this->postalCode = $postalCode; + return $this; + } + + /** + * @param string $countryIso + * @return $this + */ + public function withCountryIso(string $countryIso): self + { + $this->countryIso = $countryIso; + return $this; + } + + /** + * @param string $country + * @return $this + */ + public function withCountry(string $country): self + { + $this->country = $country; + return $this; + } + + /** + * @param string $formattedAddress + * @return $this + */ + public function withFormattedAddress(string $formattedAddress): self + { + $this->formattedAddress = $formattedAddress; + return $this; + } + + /** + * @return StructuredPostalAddress + */ + public function build(): StructuredPostalAddress + { + return new StructuredPostalAddress( + $this->addressFormat, + $this->buildingNumber, + $this->addressLine1, + $this->townCity, + $this->postalCode, + $this->countryIso, + $this->country, + $this->formattedAddress + ); + } +} diff --git a/tests/DocScan/Session/Create/ApplicantProfileBuilderTest.php b/tests/DocScan/Session/Create/ApplicantProfileBuilderTest.php new file mode 100644 index 00000000..bb000cf3 --- /dev/null +++ b/tests/DocScan/Session/Create/ApplicantProfileBuilderTest.php @@ -0,0 +1,142 @@ +withFullName(self::SOME_FULL_NAME) + ->build(); + + $this->assertEquals(self::SOME_FULL_NAME, $profile->getFullName()); + } + + /** + * @test + * @covers ::build + * @covers ::withDateOfBirth + * @covers \Yoti\DocScan\Session\Create\ApplicantProfile::__construct + * @covers \Yoti\DocScan\Session\Create\ApplicantProfile::getDateOfBirth + */ + public function shouldBuildWithDateOfBirth() + { + $profile = (new ApplicantProfileBuilder()) + ->withDateOfBirth(self::SOME_DATE_OF_BIRTH) + ->build(); + + $this->assertEquals(self::SOME_DATE_OF_BIRTH, $profile->getDateOfBirth()); + } + + /** + * @test + * @covers ::build + * @covers ::withNamePrefix + * @covers \Yoti\DocScan\Session\Create\ApplicantProfile::__construct + * @covers \Yoti\DocScan\Session\Create\ApplicantProfile::getNamePrefix + */ + public function shouldBuildWithNamePrefix() + { + $profile = (new ApplicantProfileBuilder()) + ->withNamePrefix(self::SOME_NAME_PREFIX) + ->build(); + + $this->assertEquals(self::SOME_NAME_PREFIX, $profile->getNamePrefix()); + } + + /** + * @test + * @covers ::build + * @covers ::withStructuredPostalAddress + * @covers \Yoti\DocScan\Session\Create\ApplicantProfile::__construct + * @covers \Yoti\DocScan\Session\Create\ApplicantProfile::getStructuredPostalAddress + */ + public function shouldBuildWithStructuredPostalAddress() + { + $address = (new StructuredPostalAddressBuilder()) + ->withBuildingNumber('74') + ->withPostalCode('E143RN') + ->build(); + + $profile = (new ApplicantProfileBuilder()) + ->withStructuredPostalAddress($address) + ->build(); + + $this->assertEquals('74', $profile->getStructuredPostalAddress()->getBuildingNumber()); + $this->assertEquals('E143RN', $profile->getStructuredPostalAddress()->getPostalCode()); + } + + /** + * @test + * @covers ::build + * @covers \Yoti\DocScan\Session\Create\ApplicantProfile::jsonSerialize + */ + public function shouldCorrectlySerializeWithAllProperties() + { + $address = (new StructuredPostalAddressBuilder()) + ->withAddressFormat(1) + ->withBuildingNumber('74') + ->withAddressLine1('AddressLine1') + ->withTownCity('CityName') + ->withPostalCode('E143RN') + ->withCountryIso('GBR') + ->withCountry('United Kingdom') + ->build(); + + $profile = (new ApplicantProfileBuilder()) + ->withFullName(self::SOME_FULL_NAME) + ->withDateOfBirth(self::SOME_DATE_OF_BIRTH) + ->withNamePrefix(self::SOME_NAME_PREFIX) + ->withStructuredPostalAddress($address) + ->build(); + + $json = json_encode($profile); + + $this->assertStringContainsString('"full_name":"John Doe"', $json); + $this->assertStringContainsString('"date_of_birth":"1988-11-02"', $json); + $this->assertStringContainsString('"name_prefix":"Mr"', $json); + $this->assertStringContainsString('"structured_postal_address"', $json); + $this->assertStringContainsString('"building_number":"74"', $json); + $this->assertStringContainsString('"country_iso":"GBR"', $json); + } + + /** + * @test + * @covers ::build + * @covers \Yoti\DocScan\Session\Create\ApplicantProfile::jsonSerialize + */ + public function shouldSerializeWithoutNullValues() + { + $profile = (new ApplicantProfileBuilder()) + ->withFullName(self::SOME_FULL_NAME) + ->build(); + + $this->assertJsonStringEqualsJsonString( + json_encode([ + 'full_name' => self::SOME_FULL_NAME, + ]), + json_encode($profile) + ); + } +} diff --git a/tests/DocScan/Session/Create/ResourceCreationContainerBuilderTest.php b/tests/DocScan/Session/Create/ResourceCreationContainerBuilderTest.php new file mode 100644 index 00000000..4ad23b4d --- /dev/null +++ b/tests/DocScan/Session/Create/ResourceCreationContainerBuilderTest.php @@ -0,0 +1,93 @@ +withFullName('John Doe') + ->withDateOfBirth('1988-11-02') + ->build(); + + $container = (new ResourceCreationContainerBuilder()) + ->withApplicantProfile($applicantProfile) + ->build(); + + $this->assertEquals($applicantProfile, $container->getApplicantProfile()); + $this->assertEquals('John Doe', $container->getApplicantProfile()->getFullName()); + $this->assertEquals('1988-11-02', $container->getApplicantProfile()->getDateOfBirth()); + } + + /** + * @test + * @covers ::build + * @covers \Yoti\DocScan\Session\Create\ResourceCreationContainer::jsonSerialize + */ + public function shouldCorrectlySerializeApplicantProfile() + { + $address = (new StructuredPostalAddressBuilder()) + ->withAddressFormat(1) + ->withBuildingNumber('74') + ->withAddressLine1('AddressLine1') + ->withTownCity('CityName') + ->withPostalCode('E143RN') + ->withCountryIso('GBR') + ->withCountry('United Kingdom') + ->build(); + + $applicantProfile = (new ApplicantProfileBuilder()) + ->withFullName('John Doe') + ->withDateOfBirth('1988-11-02') + ->withNamePrefix('Mr') + ->withStructuredPostalAddress($address) + ->build(); + + $container = (new ResourceCreationContainerBuilder()) + ->withApplicantProfile($applicantProfile) + ->build(); + + $json = json_encode($container); + + $this->assertStringContainsString('"applicant_profile"', $json); + $this->assertStringContainsString('"full_name":"John Doe"', $json); + $this->assertStringContainsString('"date_of_birth":"1988-11-02"', $json); + $this->assertStringContainsString('"name_prefix":"Mr"', $json); + $this->assertStringContainsString('"building_number":"74"', $json); + $this->assertStringContainsString('"country_iso":"GBR"', $json); + } + + /** + * @test + * @covers ::build + * @covers \Yoti\DocScan\Session\Create\ResourceCreationContainer::jsonSerialize + */ + public function shouldSerializeWithoutNullValues() + { + $container = (new ResourceCreationContainerBuilder()) + ->build(); + + $this->assertJsonStringEqualsJsonString( + json_encode(new \stdClass()), + json_encode($container) + ); + } +} diff --git a/tests/DocScan/Session/Create/SessionSpecificationBuilderTest.php b/tests/DocScan/Session/Create/SessionSpecificationBuilderTest.php index 266e47e4..f1d274b2 100644 --- a/tests/DocScan/Session/Create/SessionSpecificationBuilderTest.php +++ b/tests/DocScan/Session/Create/SessionSpecificationBuilderTest.php @@ -9,6 +9,7 @@ use Yoti\DocScan\Session\Create\IbvOptions; use Yoti\DocScan\Session\Create\ImportToken; use Yoti\DocScan\Session\Create\NotificationConfig; +use Yoti\DocScan\Session\Create\ResourceCreationContainer; use Yoti\DocScan\Session\Create\SdkConfig; use Yoti\DocScan\Session\Create\SessionSpecificationBuilder; use Yoti\DocScan\Session\Create\Task\RequestedTask; @@ -73,6 +74,11 @@ class SessionSpecificationBuilderTest extends TestCase */ private $importTokenMock; + /** + * @var ResourceCreationContainer + */ + private $resourcesMock; + public function setup(): void { $this->sdkConfigMock = $this->createMock(SdkConfig::class); @@ -94,6 +100,11 @@ public function setup(): void $this->importTokenMock = $this->createMock(ImportToken::class); + $this->resourcesMock = $this->createMock(ResourceCreationContainer::class); + $this->resourcesMock + ->method('jsonSerialize') + ->willReturn((object)['applicant_profile' => (object)['full_name' => 'John Doe']]); + $this->subject = (object)[1 => 'some']; $this->identityProfileRequirements = (object)[ @@ -605,4 +616,58 @@ public function shouldReturnCorrectJsonStringWithAdvancedIdentityProfileRequirem json_encode($sessionSpecification) ); } + + /** + * @test + * @covers \Yoti\DocScan\Session\Create\SessionSpecification::getResources + * @covers \Yoti\DocScan\Session\Create\SessionSpecification::__construct + * @covers \Yoti\DocScan\Session\Create\SessionSpecificationBuilder::withResources + * @covers \Yoti\DocScan\Session\Create\SessionSpecificationBuilder::build + */ + public function shouldBuildWithResources() + { + $sessionSpecificationResult = (new SessionSpecificationBuilder()) + ->withResources($this->resourcesMock) + ->build(); + + $this->assertEquals($this->resourcesMock, $sessionSpecificationResult->getResources()); + } + + /** + * @test + * @covers \Yoti\DocScan\Session\Create\SessionSpecification::getResources + * @covers \Yoti\DocScan\Session\Create\SessionSpecification::__construct + * @covers \Yoti\DocScan\Session\Create\SessionSpecificationBuilder::build + */ + public function shouldNotImplicitlySetAValueForResources() + { + $sessionSpecificationResult = (new SessionSpecificationBuilder()) + ->build(); + + $this->assertNull($sessionSpecificationResult->getResources()); + } + + /** + * @test + * @covers \Yoti\DocScan\Session\Create\SessionSpecification::jsonSerialize + * @covers \Yoti\DocScan\Session\Create\SessionSpecificationBuilder::withResources + * @covers \Yoti\DocScan\Session\Create\SessionSpecificationBuilder::build + */ + public function shouldReturnCorrectJsonStringWithResources() + { + $sessionSpecification = (new SessionSpecificationBuilder()) + ->withResources($this->resourcesMock) + ->build(); + + $this->assertJsonStringEqualsJsonString( + json_encode([ + 'requested_checks' => [], + 'requested_tasks' => [], + 'required_documents' => [], + 'create_identity_profile_preview' => false, + 'resources' => $this->resourcesMock, + ]), + json_encode($sessionSpecification) + ); + } } diff --git a/tests/DocScan/Session/Create/StructuredPostalAddressBuilderTest.php b/tests/DocScan/Session/Create/StructuredPostalAddressBuilderTest.php new file mode 100644 index 00000000..ad4f6a80 --- /dev/null +++ b/tests/DocScan/Session/Create/StructuredPostalAddressBuilderTest.php @@ -0,0 +1,121 @@ +withAddressFormat(self::SOME_ADDRESS_FORMAT) + ->withBuildingNumber(self::SOME_BUILDING_NUMBER) + ->withAddressLine1(self::SOME_ADDRESS_LINE_1) + ->withTownCity(self::SOME_TOWN_CITY) + ->withPostalCode(self::SOME_POSTAL_CODE) + ->withCountryIso(self::SOME_COUNTRY_ISO) + ->withCountry(self::SOME_COUNTRY) + ->withFormattedAddress(self::SOME_FORMATTED_ADDRESS) + ->build(); + + $this->assertEquals(self::SOME_ADDRESS_FORMAT, $address->getAddressFormat()); + $this->assertEquals(self::SOME_BUILDING_NUMBER, $address->getBuildingNumber()); + $this->assertEquals(self::SOME_ADDRESS_LINE_1, $address->getAddressLine1()); + $this->assertEquals(self::SOME_TOWN_CITY, $address->getTownCity()); + $this->assertEquals(self::SOME_POSTAL_CODE, $address->getPostalCode()); + $this->assertEquals(self::SOME_COUNTRY_ISO, $address->getCountryIso()); + $this->assertEquals(self::SOME_COUNTRY, $address->getCountry()); + $this->assertEquals(self::SOME_FORMATTED_ADDRESS, $address->getFormattedAddress()); + } + + /** + * @test + * @covers ::build + * @covers \Yoti\DocScan\Session\Create\StructuredPostalAddress::jsonSerialize + */ + public function shouldCorrectlySerialize() + { + $address = (new StructuredPostalAddressBuilder()) + ->withAddressFormat(self::SOME_ADDRESS_FORMAT) + ->withBuildingNumber(self::SOME_BUILDING_NUMBER) + ->withAddressLine1(self::SOME_ADDRESS_LINE_1) + ->withTownCity(self::SOME_TOWN_CITY) + ->withPostalCode(self::SOME_POSTAL_CODE) + ->withCountryIso(self::SOME_COUNTRY_ISO) + ->withCountry(self::SOME_COUNTRY) + ->withFormattedAddress(self::SOME_FORMATTED_ADDRESS) + ->build(); + + $this->assertJsonStringEqualsJsonString( + json_encode([ + 'address_format' => self::SOME_ADDRESS_FORMAT, + 'building_number' => self::SOME_BUILDING_NUMBER, + 'address_line1' => self::SOME_ADDRESS_LINE_1, + 'town_city' => self::SOME_TOWN_CITY, + 'postal_code' => self::SOME_POSTAL_CODE, + 'country_iso' => self::SOME_COUNTRY_ISO, + 'country' => self::SOME_COUNTRY, + 'formatted_address' => self::SOME_FORMATTED_ADDRESS, + ]), + json_encode($address) + ); + } + + /** + * @test + * @covers ::build + * @covers \Yoti\DocScan\Session\Create\StructuredPostalAddress::jsonSerialize + */ + public function shouldSerializeWithoutNullValues() + { + $address = (new StructuredPostalAddressBuilder()) + ->withBuildingNumber(self::SOME_BUILDING_NUMBER) + ->withPostalCode(self::SOME_POSTAL_CODE) + ->build(); + + $this->assertJsonStringEqualsJsonString( + json_encode([ + 'building_number' => self::SOME_BUILDING_NUMBER, + 'postal_code' => self::SOME_POSTAL_CODE, + ]), + json_encode($address) + ); + } +} From 56d53ca34814358dc7e1dbd1c553c9e4f6ac3f33 Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:03:02 +0300 Subject: [PATCH 06/12] Sdk 2296 php allow rb to fetch applicant profile from get sessions (#406) * SDK-2215 allow rb to supply an applicant profile for identity profile sessions * SDK-2296 added allow rb to fetch application profile from get sessions * Add missing @throws DateTimeException annotations --- .../ApplicantProfileResourceResponse.php | 70 ++++++++++++++++ .../Session/Retrieve/ResourceContainer.php | 30 +++++++ src/DocScan/Session/Retrieve/TaskResponse.php | 2 + .../ApplicantProfileResourceResponseTest.php | 81 +++++++++++++++++++ .../Retrieve/ResourceContainerTest.php | 46 ++++++++++- 5 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 src/DocScan/Session/Retrieve/ApplicantProfileResourceResponse.php create mode 100644 tests/DocScan/Session/Retrieve/ApplicantProfileResourceResponseTest.php diff --git a/src/DocScan/Session/Retrieve/ApplicantProfileResourceResponse.php b/src/DocScan/Session/Retrieve/ApplicantProfileResourceResponse.php new file mode 100644 index 00000000..35dc108b --- /dev/null +++ b/src/DocScan/Session/Retrieve/ApplicantProfileResourceResponse.php @@ -0,0 +1,70 @@ + $applicantProfile + * @throws DateTimeException + */ + public function __construct(array $applicantProfile) + { + parent::__construct($applicantProfile); + + if (isset($applicantProfile['media'])) { + $this->media = new MediaResponse($applicantProfile['media']); + } + + $this->createdAt = isset($applicantProfile['created_at']) + ? DateTime::stringToDateTime($applicantProfile['created_at']) : null; + + $this->lastUpdated = isset($applicantProfile['last_updated']) + ? DateTime::stringToDateTime($applicantProfile['last_updated']) : null; + } + + /** + * @return MediaResponse|null + */ + public function getMedia(): ?MediaResponse + { + return $this->media; + } + + /** + * @return \DateTime|null + */ + public function getCreatedAt(): ?\DateTime + { + return $this->createdAt; + } + + /** + * @return \DateTime|null + */ + public function getLastUpdated(): ?\DateTime + { + return $this->lastUpdated; + } +} diff --git a/src/DocScan/Session/Retrieve/ResourceContainer.php b/src/DocScan/Session/Retrieve/ResourceContainer.php index 5cc26097..dd0d07c3 100644 --- a/src/DocScan/Session/Retrieve/ResourceContainer.php +++ b/src/DocScan/Session/Retrieve/ResourceContainer.php @@ -31,6 +31,11 @@ class ResourceContainer */ private $shareCodes = []; + /** + * @var ApplicantProfileResourceResponse[] + */ + private $applicantProfiles = []; + /** * ResourceContainer constructor. * @param array $resources @@ -56,6 +61,10 @@ public function __construct(array $resources) if (isset($resources['share_codes'])) { $this->shareCodes = $this->parseShareCodes($resources['share_codes']); } + + if (isset($resources['applicant_profiles'])) { + $this->applicantProfiles = $this->parseApplicantProfiles($resources['applicant_profiles']); + } } /** @@ -178,6 +187,14 @@ public function getShareCodes(): array return $this->shareCodes; } + /** + * @return ApplicantProfileResourceResponse[] + */ + public function getApplicantProfiles(): array + { + return $this->applicantProfiles; + } + /** * @param array> $shareCodes * @return ShareCodeResourceResponse[] @@ -191,6 +208,19 @@ private function parseShareCodes(array $shareCodes): array return $parsedShareCodes; } + /** + * @param array> $applicantProfiles + * @return ApplicantProfileResourceResponse[] + */ + private function parseApplicantProfiles(array $applicantProfiles): array + { + $parsedApplicantProfiles = []; + foreach ($applicantProfiles as $applicantProfile) { + $parsedApplicantProfiles[] = new ApplicantProfileResourceResponse($applicantProfile); + } + return $parsedApplicantProfiles; + } + /** * @param string $class * @return mixed[] diff --git a/src/DocScan/Session/Retrieve/TaskResponse.php b/src/DocScan/Session/Retrieve/TaskResponse.php index caee2233..324d8266 100644 --- a/src/DocScan/Session/Retrieve/TaskResponse.php +++ b/src/DocScan/Session/Retrieve/TaskResponse.php @@ -5,6 +5,7 @@ namespace Yoti\DocScan\Session\Retrieve; use Yoti\DocScan\Constants; +use Yoti\Exception\DateTimeException; use Yoti\Util\DateTime; class TaskResponse @@ -52,6 +53,7 @@ class TaskResponse /** * TaskResponse constructor. * @param array $task + * @throws DateTimeException */ public function __construct(array $task) { diff --git a/tests/DocScan/Session/Retrieve/ApplicantProfileResourceResponseTest.php b/tests/DocScan/Session/Retrieve/ApplicantProfileResourceResponseTest.php new file mode 100644 index 00000000..645deaf2 --- /dev/null +++ b/tests/DocScan/Session/Retrieve/ApplicantProfileResourceResponseTest.php @@ -0,0 +1,81 @@ + self::SOME_ID, + 'source' => [ + 'type' => self::SOME_SOURCE_TYPE, + ], + 'media' => [ + 'id' => self::SOME_MEDIA_ID, + 'type' => self::SOME_MEDIA_TYPE, + 'created' => self::SOME_CREATED_AT, + 'last_updated' => self::SOME_LAST_UPDATED, + ], + 'created_at' => self::SOME_CREATED_AT, + 'last_updated' => self::SOME_LAST_UPDATED, + 'tasks' => [], + ]; + + $result = new ApplicantProfileResourceResponse($input); + + $this->assertEquals(self::SOME_ID, $result->getId()); + $this->assertNotNull($result->getSource()); + $this->assertInstanceOf(MediaResponse::class, $result->getMedia()); + $this->assertEquals(self::SOME_MEDIA_ID, $result->getMedia()->getId()); + $this->assertEquals(self::SOME_MEDIA_TYPE, $result->getMedia()->getType()); + $this->assertEquals( + DateTime::stringToDateTime(self::SOME_CREATED_AT), + $result->getCreatedAt() + ); + $this->assertEquals( + DateTime::stringToDateTime(self::SOME_LAST_UPDATED), + $result->getLastUpdated() + ); + $this->assertCount(0, $result->getTasks()); + } + + /** + * @test + * @covers ::__construct + */ + public function shouldNotThrowExceptionWhenMissingValues() + { + $result = new ApplicantProfileResourceResponse([]); + + $this->assertNull($result->getId()); + $this->assertNull($result->getMedia()); + $this->assertNull($result->getCreatedAt()); + $this->assertNull($result->getLastUpdated()); + $this->assertCount(0, $result->getTasks()); + } +} diff --git a/tests/DocScan/Session/Retrieve/ResourceContainerTest.php b/tests/DocScan/Session/Retrieve/ResourceContainerTest.php index 48dba28d..f73beb81 100644 --- a/tests/DocScan/Session/Retrieve/ResourceContainerTest.php +++ b/tests/DocScan/Session/Retrieve/ResourceContainerTest.php @@ -4,6 +4,7 @@ namespace Yoti\Test\DocScan\Session\Retrieve; +use Yoti\DocScan\Session\Retrieve\ApplicantProfileResourceResponse; use Yoti\DocScan\Session\Retrieve\ResourceContainer; use Yoti\DocScan\Session\Retrieve\ShareCodeResourceResponse; use Yoti\DocScan\Session\Retrieve\StaticLivenessResourceResponse; @@ -53,7 +54,10 @@ public function shouldBuildCorrectly() 'share_codes' => [ ['id' => 'share-code-1'], ['id' => 'share-code-2'], - ] + ], + 'applicant_profiles' => [ + ['id' => 'applicant-profile-1'], + ], ]; $result = new ResourceContainer($input); @@ -65,6 +69,7 @@ public function shouldBuildCorrectly() $this->assertCount(2, $result->getSupplementaryDocuments()); $this->assertCount(1, $result->getFaceCapture()); $this->assertCount(2, $result->getShareCodes()); + $this->assertCount(1, $result->getApplicantProfiles()); } /** @@ -78,6 +83,7 @@ public function shouldNotThrowExceptionWhenMissingValues() $this->assertCount(0, $result->getIdDocuments()); $this->assertCount(0, $result->getLivenessCapture()); $this->assertCount(0, $result->getShareCodes()); + $this->assertCount(0, $result->getApplicantProfiles()); } /** @@ -175,4 +181,42 @@ public function shouldParseShareCodes(): void $this->assertEquals('share-code-1', $result->getShareCodes()[0]->getId()); $this->assertEquals('share-code-2', $result->getShareCodes()[1]->getId()); } + + /** + * @test + * @covers ::parseApplicantProfiles + * @covers ::getApplicantProfiles + */ + public function shouldParseApplicantProfiles(): void + { + $input = [ + 'applicant_profiles' => [ + [ + 'id' => '3fa85f64-5717-4562-b3fc-2c963f66afa6', + 'source' => ['type' => 'END_USER'], + 'media' => [ + 'id' => 'media-id-123', + 'type' => 'IMAGE', + 'created' => '2021-06-11T11:39:24Z', + 'last_updated' => '2021-06-11T11:39:24Z', + ], + 'created_at' => '2021-06-11T11:39:24Z', + 'last_updated' => '2021-06-11T11:39:24Z', + 'tasks' => [], + ], + ], + ]; + + $result = new ResourceContainer($input); + + $this->assertCount(1, $result->getApplicantProfiles()); + $this->assertContainsOnlyInstancesOf( + ApplicantProfileResourceResponse::class, + $result->getApplicantProfiles() + ); + $this->assertEquals( + '3fa85f64-5717-4562-b3fc-2c963f66afa6', + $result->getApplicantProfiles()[0]->getId() + ); + } } From 4cb5f03fac18057558ed3c38fb166c5943d15a1e Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Mon, 20 Apr 2026 08:55:22 +0100 Subject: [PATCH 07/12] update examples (cherry picked from commit eebbb1936ffc38bf7fc9cdadd56759b191d375ad) --- .gitignore | 1 + .../app/Http/Controllers/HomeController.php | 16 ++++++++++++---- .../Session/Create/SdkConfigBuilderTest.php | 4 ++-- .../SessionConfigurationResponseTest.php | 4 ++-- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index b83fe4bd..2250573d 100755 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ coverage .DS_Store .php-cs-fixer.cache +.claude/ diff --git a/examples/doc-scan/app/Http/Controllers/HomeController.php b/examples/doc-scan/app/Http/Controllers/HomeController.php index d52fdd8e..1ec2ac49 100644 --- a/examples/doc-scan/app/Http/Controllers/HomeController.php +++ b/examples/doc-scan/app/Http/Controllers/HomeController.php @@ -152,11 +152,19 @@ public function show(Request $request, DocScanClient $client) ->withPrivacyPolicyUrl(config('app.url') . '/privacy-policy') ->withBiometricConsentFlow('EARLY') ->withBrandId('brand_id') - // Suppress specific screens to shorten the flow - ->withSuppressedScreens(['intro_screen', 'document_capture_instruction']) + // Suppress specific screens to shorten the flow. + // Available screen identifiers: + // 'IDENTITY_DOCUMENT_EDUCATION' + // 'IDENTITY_DOCUMENT_REQUIREMENTS' + // 'SUPPLEMENTARY_DOCUMENT_REQUIREMENTS' + // 'ZOOM_LIVENESS_EDUCATION' + // 'STATIC_LIVENESS_EDUCATION' + // 'FACE_CAPTURE_EDUCATION' + // 'FLOW_COMPLETION' + // ->withSuppressedScreens(['IDENTITY_DOCUMENT_EDUCATION', 'IDENTITY_DOCUMENT_REQUIREMENTS']) // Or add screens individually: - // ->withSuppressedScreen('intro_screen') - // ->withSuppressedScreen('document_capture_instruction') + // ->withSuppressedScreen('IDENTITY_DOCUMENT_EDUCATION') + // ->withSuppressedScreen('IDENTITY_DOCUMENT_REQUIREMENTS') ->build() ) ->withRequiredDocument( diff --git a/tests/DocScan/Session/Create/SdkConfigBuilderTest.php b/tests/DocScan/Session/Create/SdkConfigBuilderTest.php index 3c879bc3..dd9ef56d 100644 --- a/tests/DocScan/Session/Create/SdkConfigBuilderTest.php +++ b/tests/DocScan/Session/Create/SdkConfigBuilderTest.php @@ -26,8 +26,8 @@ class SdkConfigBuilderTest extends TestCase private const SOME_DARK_MODE = 'someDarkMode'; private const SOME_PRIMARY_COLOUR_DARK_MODE = 'somePrimaryColourDarkMode'; private const SOME_BRAND_ID = 'someBrandId'; - private const SOME_SCREEN_IDENTIFIER = 'someScreenIdentifier'; - private const ANOTHER_SCREEN_IDENTIFIER = 'anotherScreenIdentifier'; + private const SOME_SCREEN_IDENTIFIER = 'IDENTITY_DOCUMENT_EDUCATION'; + private const ANOTHER_SCREEN_IDENTIFIER = 'IDENTITY_DOCUMENT_REQUIREMENTS'; /** * @test diff --git a/tests/DocScan/Session/Retrieve/Configuration/SessionConfigurationResponseTest.php b/tests/DocScan/Session/Retrieve/Configuration/SessionConfigurationResponseTest.php index 7a73a6e4..84c9e3a6 100644 --- a/tests/DocScan/Session/Retrieve/Configuration/SessionConfigurationResponseTest.php +++ b/tests/DocScan/Session/Retrieve/Configuration/SessionConfigurationResponseTest.php @@ -14,8 +14,8 @@ class SessionConfigurationResponseTest extends TestCase private const SOME_CLIENT_SESSION_TTL = 12345678; private const SOME_SESSION_ID = 'SOME_SESSION_ID'; private const SOME_REQUESTED_CHECKS = ['SOME_CHECK', 'SOME_ANOTHER_CHECK']; - private const SOME_SCREEN_IDENTIFIER = 'someScreenIdentifier'; - private const ANOTHER_SCREEN_IDENTIFIER = 'anotherScreenIdentifier'; + private const SOME_SCREEN_IDENTIFIER = 'IDENTITY_DOCUMENT_EDUCATION'; + private const ANOTHER_SCREEN_IDENTIFIER = 'IDENTITY_DOCUMENT_REQUIREMENTS'; private const SOME_CAPTURE = [ 'biometric_consent' => 'SOME_STRING', 'required_resources' => [ From 952d7b842f600c652d25a819df6907ee1d500d87 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Mon, 20 Apr 2026 12:06:59 +0100 Subject: [PATCH 08/12] updated version number --- README.md | 4 ++-- composer.json | 2 +- src/Constants.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4cfbbe17..2dcfeaa4 100755 --- a/README.md +++ b/README.md @@ -45,13 +45,13 @@ Add the Yoti SDK dependency: ```json "require": { - "yoti/yoti-php-sdk" : "^4.5.0" + "yoti/yoti-php-sdk" : "^4.5.1" } ``` Or run this Composer command ```console -$ composer require yoti/yoti-php-sdk "^4.5.0" +$ composer require yoti/yoti-php-sdk "^4.5.1" ``` ## Setup diff --git a/composer.json b/composer.json index 91b4d313..d913b799 100755 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "yoti/yoti-php-sdk", "description": "Yoti SDK for quickly integrating your PHP backend with Yoti", - "version": "4.5.0", + "version": "4.5.1", "keywords": [ "yoti", "sdk" diff --git a/src/Constants.php b/src/Constants.php index 921b1fbe..51053dfe 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -37,7 +37,7 @@ class Constants public const SDK_IDENTIFIER = 'PHP'; /** Default SDK version */ - public const SDK_VERSION = '4.5.0'; + public const SDK_VERSION = '4.5.1'; /** Base url for connect page (user will be redirected to this page eg. baseurl/app-id) */ public const CONNECT_BASE_URL = 'https://www.yoti.com/connect'; From bd248d864a96228465c09a7730fbe558ad904eba Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Mon, 20 Apr 2026 13:35:41 +0100 Subject: [PATCH 09/12] updated examples on controller --- examples/doc-scan/app/Http/Controllers/HomeController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/doc-scan/app/Http/Controllers/HomeController.php b/examples/doc-scan/app/Http/Controllers/HomeController.php index 1ec2ac49..53aec60f 100644 --- a/examples/doc-scan/app/Http/Controllers/HomeController.php +++ b/examples/doc-scan/app/Http/Controllers/HomeController.php @@ -151,7 +151,7 @@ public function show(Request $request, DocScanClient $client) ->withErrorUrl(config('app.url') . '/error') ->withPrivacyPolicyUrl(config('app.url') . '/privacy-policy') ->withBiometricConsentFlow('EARLY') - ->withBrandId('brand_id') + //->withBrandId('brand_id') // Suppress specific screens to shorten the flow. // Available screen identifiers: // 'IDENTITY_DOCUMENT_EDUCATION' From b61f235c80a657715b8baa57d35de702f4d1e969 Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Mon, 27 Apr 2026 13:17:22 +0100 Subject: [PATCH 10/12] Add process field rendering to breakdown view in doc-scan example --- .../resources/views/partial/check.blade.php | 6 ++++++ .../Session/Retrieve/BreakdownResponseTest.php | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/examples/doc-scan/resources/views/partial/check.blade.php b/examples/doc-scan/resources/views/partial/check.blade.php index 651a6b22..062ddaeb 100644 --- a/examples/doc-scan/resources/views/partial/check.blade.php +++ b/examples/doc-scan/resources/views/partial/check.blade.php @@ -67,6 +67,12 @@ Result {{ $breakdown->getResult() }} + @if ($breakdown->getProcess()) + + Process + {{ $breakdown->getProcess() }} + + @endif @if (count($breakdown->getDetails()) > 0) Details diff --git a/tests/DocScan/Session/Retrieve/BreakdownResponseTest.php b/tests/DocScan/Session/Retrieve/BreakdownResponseTest.php index 9e4e9a30..7d2c9a30 100644 --- a/tests/DocScan/Session/Retrieve/BreakdownResponseTest.php +++ b/tests/DocScan/Session/Retrieve/BreakdownResponseTest.php @@ -26,6 +26,7 @@ class BreakdownResponseTest extends TestCase ]; private const SOME_PROCESS = 'AUTOMATED'; + private const SOME_EXPERT_REVIEW_PROCESS = 'EXPERT_REVIEW'; /** * @test @@ -80,4 +81,16 @@ public function shouldNotThrowExceptionWhenValuesAreMissing() $this->assertNull($result->getProcess()); $this->assertCount(0, $result->getDetails()); } + + /** + * @test + * @covers ::__construct + * @covers ::getProcess + */ + public function shouldExposeExpertReviewProcessValue() + { + $result = new BreakdownResponse(['process' => self::SOME_EXPERT_REVIEW_PROCESS]); + + $this->assertEquals(self::SOME_EXPERT_REVIEW_PROCESS, $result->getProcess()); + } } From 28f10ce574209c10ed3cbf2e68b2c356245f46a2 Mon Sep 17 00:00:00 2001 From: mehmet-yoti <111424390+mehmet-yoti@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:27:58 +0300 Subject: [PATCH 11/12] SDK-2791: Render extraction_image_ids on doc-scan success page (#415) Add UI block to display extraction image IDs for each page of ID documents and supplementary documents on the success view. Each ID is rendered as a link to the existing /media/{id} route so the image can be opened directly for QA verification. --- .../resources/views/success.blade.php | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/examples/doc-scan/resources/views/success.blade.php b/examples/doc-scan/resources/views/success.blade.php index 63b0b486..612311ca 100644 --- a/examples/doc-scan/resources/views/success.blade.php +++ b/examples/doc-scan/resources/views/success.blade.php @@ -562,6 +562,23 @@ class="badge badge-primary">{{ $document->getIssuingCountry() }} @endif + @if (count($page->getExtractionImageIds()) > 0) +
+
+
+
Extraction Image IDs
+ +
+
+
+ @endif + @endforeach @@ -743,6 +760,23 @@ class="badge badge-primary">{{ $document->getIssuingCountry() }} @endif + @if (count($page->getExtractionImageIds()) > 0) +
+
+
+
Extraction Image IDs
+ +
+
+
+ @endif + @endforeach From 024eafb3d5b8ca9dae73a233cd5574225cd8d5ed Mon Sep 17 00:00:00 2001 From: mehmet-yoti Date: Tue, 5 May 2026 08:54:21 +0100 Subject: [PATCH 12/12] Address Copilot review feedback on PageResponse --- src/DocScan/Session/Retrieve/PageResponse.php | 4 +++- tests/DocScan/Session/Retrieve/PageResponseTest.php | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/DocScan/Session/Retrieve/PageResponse.php b/src/DocScan/Session/Retrieve/PageResponse.php index eb3e6c45..62805cd0 100644 --- a/src/DocScan/Session/Retrieve/PageResponse.php +++ b/src/DocScan/Session/Retrieve/PageResponse.php @@ -44,7 +44,9 @@ public function __construct(array $page) } } - $this->extractionImageIds = $page['extraction_image_ids'] ?? []; + $this->extractionImageIds = isset($page['extraction_image_ids']) && is_array($page['extraction_image_ids']) + ? $page['extraction_image_ids'] + : []; } /** diff --git a/tests/DocScan/Session/Retrieve/PageResponseTest.php b/tests/DocScan/Session/Retrieve/PageResponseTest.php index 89527f10..063e0628 100644 --- a/tests/DocScan/Session/Retrieve/PageResponseTest.php +++ b/tests/DocScan/Session/Retrieve/PageResponseTest.php @@ -53,7 +53,7 @@ public function testGetFrames() ]); $this->assertCount(2, $pageResponse->getFrames()); - $this->containsOnlyInstancesOf(FrameResponse::class, $pageResponse->getFrames()); + $this->assertContainsOnlyInstancesOf(FrameResponse::class, $pageResponse->getFrames()); } /**