diff --git a/.gitignore b/.gitignore index a6d27108..3dc61b62 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea +.phpunit.cache .phpunit.result.cache .php-cs-fixer.cache vendor/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 04eb81b8..78dcadb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.0.2] - 2025-02-27 + +### Added + +- Support for SignUp Plus authentication link creation +- Support for SignUp Plus user data retrieval + ## [3.0.1] - 2025-01-09 ### Changed diff --git a/README.md b/README.md index 01ead03e..d49499f0 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,11 @@ 10. [Payouts](#payouts) 11. [Merchant accounts](#merchant-accounts) 12. [Account identifiers](#account-identifiers) -13. [Receiving webhook notifications](#webhooks) -14. [Custom idempotency keys](#idempotency) -15. [Custom API calls](#custom-api-calls) -16. [Error Handling](#error-handling) +13. [SignUp Plus](#signup-plus) +14. [Receiving webhook notifications](#webhooks) +15. [Custom idempotency keys](#idempotency) +16. [Custom API calls](#custom-api-calls) +17. [Error Handling](#error-handling) @@ -1011,6 +1012,29 @@ foreach ($merchantAccount->getAccountIdentifiers() as $accountIdentifier) { } ``` + + +# SignUp Plus + +Generating a SignUp Plus authentication link: + +```php +$client->signupPlus() + ->authUri() + ->paymentId('some_payment_id') + ->state('some_state') + ->create(); +``` + +Retrieving user data: + +```php +$response = $client->signupPlus() + ->userData() + ->paymentId('fake_payment_id') + ->retrieve(); // SignupPlusUserDataRetrievedInterface +``` + # Receiving webhook notifications diff --git a/config/bindings.php b/config/bindings.php index c8240cda..9af12f58 100644 --- a/config/bindings.php +++ b/config/bindings.php @@ -9,6 +9,7 @@ Interfaces\HppInterface::class => 'makeHpp', Interfaces\AddressInterface::class => Entities\Address::class, + Interfaces\AddressRetrievedInterface::class => Entities\AddressRetrieved::class, Interfaces\UserInterface::class => Entities\User::class, Interfaces\Payment\Beneficiary\BeneficiaryBuilderInterface::class => Entities\Payment\Beneficiary\BeneficiaryBuilder::class, @@ -109,5 +110,12 @@ Interfaces\Webhook\Beneficiary\ExternalAccountBeneficiaryInterface::class => Entities\Webhook\Beneficiary\ExternalAccountBeneficiary::class, Interfaces\Webhook\Beneficiary\BeneficiaryInterface::class => Entities\Webhook\Beneficiary\Beneficiary::class, + Interfaces\SignupPlus\SignupPlusBuilderInterface::class => Entities\SignupPlus\SignupPlusBuilder::class, + Interfaces\SignupPlus\SignupPlusAuthUriRequestInterface::class => Entities\SignupPlus\SignupPlusAuthUriRequest::class, + Interfaces\SignupPlus\SignupPlusAuthUriCreatedInterface::class => Entities\SignupPlus\SignupPlusAuthUriCreated::class, + Interfaces\SignupPlus\SignupPlusAccountDetailsInterface::class => Entities\SignupPlus\SignupPlusAccountDetails::class, + Interfaces\SignupPlus\SignupPlusUserDataRequestInterface::class => Entities\SignupPlus\SignupPlusUserDataRequest::class, + Interfaces\SignupPlus\SignupPlusUserDataRetrievedInterface::class => Entities\SignupPlus\SignupPlusUserDataRetrieved::class, + Interfaces\RequestOptionsInterface::class => Entities\RequestOptions::class, ]; diff --git a/src/Constants/Endpoints.php b/src/Constants/Endpoints.php index 49048e83..adc713a1 100644 --- a/src/Constants/Endpoints.php +++ b/src/Constants/Endpoints.php @@ -35,4 +35,7 @@ class Endpoints public const PAYOUTS_CREATE = '/v3/payouts'; public const PAYOUTS_RETRIEVE = '/v3/payouts/{id}'; + public const SIGNUP_PLUS_AUTH = '/signup-plus/authuri'; + public const SIGNUP_PLUS_PAYMENTS = '/signup-plus/payments?payment_id={id}'; + public const SIGNUP_PLUS_MANDATES = '/signup-plus/mandates?mandate_id={id}'; } diff --git a/src/Entities/Address.php b/src/Entities/Address.php index 6c20f51c..b4f1d9ad 100644 --- a/src/Entities/Address.php +++ b/src/Entities/Address.php @@ -6,58 +6,8 @@ use TrueLayer\Interfaces\AddressInterface; -class Address extends Entity implements AddressInterface +class Address extends AddressRetrieved implements AddressInterface { - /** - * @var string - */ - protected string $addressLine1; - - /** - * @var string - */ - protected string $addressLine2; - - /** - * @var string - */ - protected string $city; - - /** - * @var string - */ - protected string $state; - - /** - * @var string - */ - protected string $zip; - - /** - * @var string - */ - protected string $countryCode; - - /** - * @var string[] - */ - protected array $arrayFields = [ - 'address_line1', - 'address_line2', - 'city', - 'state', - 'zip', - 'country_code', - ]; - - /** - * @return string|null - */ - public function getAddressLine1(): ?string - { - return $this->addressLine1 ?? null; - } - /** * @param string $addressLine1 * @@ -70,14 +20,6 @@ public function addressLine1(string $addressLine1): AddressInterface return $this; } - /** - * @return string|null - */ - public function getAddressLine2(): ?string - { - return $this->addressLine2 ?? null; - } - /** * @param string $addressLine2 * @@ -90,14 +32,6 @@ public function addressLine2(string $addressLine2): AddressInterface return $this; } - /** - * @return string|null - */ - public function getCity(): ?string - { - return $this->city ?? null; - } - /** * @param string $city * @@ -110,14 +44,6 @@ public function city(string $city): AddressInterface return $this; } - /** - * @return string|null - */ - public function getState(): ?string - { - return $this->state ?? null; - } - /** * @param string $state * @@ -130,14 +56,6 @@ public function state(string $state): AddressInterface return $this; } - /** - * @return string|null - */ - public function getZip(): ?string - { - return $this->zip ?? null; - } - /** * @param string $zip * @@ -150,14 +68,6 @@ public function zip(string $zip): AddressInterface return $this; } - /** - * @return string|null - */ - public function getCountryCode(): ?string - { - return $this->countryCode ?? null; - } - /** * @param string $countryCode * diff --git a/src/Entities/AddressRetrieved.php b/src/Entities/AddressRetrieved.php new file mode 100644 index 00000000..e875e411 --- /dev/null +++ b/src/Entities/AddressRetrieved.php @@ -0,0 +1,100 @@ +addressLine1 ?? null; + } + + /** + * @return string|null + */ + public function getAddressLine2(): ?string + { + return $this->addressLine2 ?? null; + } + + /** + * @return string|null + */ + public function getCity(): ?string + { + return $this->city ?? null; + } + + /** + * @return string|null + */ + public function getState(): ?string + { + return $this->state ?? null; + } + + /** + * @return string|null + */ + public function getZip(): ?string + { + return $this->zip ?? null; + } + + /** + * @return string|null + */ + public function getCountryCode(): ?string + { + return $this->countryCode ?? null; + } +} diff --git a/src/Entities/SignupPlus/SignupPlusAccountDetails.php b/src/Entities/SignupPlus/SignupPlusAccountDetails.php new file mode 100644 index 00000000..20d42989 --- /dev/null +++ b/src/Entities/SignupPlus/SignupPlusAccountDetails.php @@ -0,0 +1,73 @@ +accountNumber ?? null; + } + + /** + * @return string|null + */ + public function getSortCode(): ?string + { + return $this->sortCode ?? null; + } + + /** + * @return string|null + */ + public function getIban(): ?string + { + return $this->iban ?? null; + } + + /** + * @return string + */ + public function getProviderId(): string + { + return $this->providerId; + } +} diff --git a/src/Entities/SignupPlus/SignupPlusAuthUriCreated.php b/src/Entities/SignupPlus/SignupPlusAuthUriCreated.php new file mode 100644 index 00000000..64ce0ffe --- /dev/null +++ b/src/Entities/SignupPlus/SignupPlusAuthUriCreated.php @@ -0,0 +1,31 @@ +authUri; + } +} diff --git a/src/Entities/SignupPlus/SignupPlusAuthUriRequest.php b/src/Entities/SignupPlus/SignupPlusAuthUriRequest.php new file mode 100644 index 00000000..805aaaba --- /dev/null +++ b/src/Entities/SignupPlus/SignupPlusAuthUriRequest.php @@ -0,0 +1,80 @@ +paymentId = $paymentId; + + return $this; + } + + /** + * @param string $state + * + * @return SignupPlusAuthUriRequestInterface + */ + public function state(string $state): SignupPlusAuthUriRequestInterface + { + $this->state = $state; + + return $this; + } + + /** + * @throws ApiResponseUnsuccessfulException + * @throws ApiRequestJsonSerializationException + * @throws SignerException + * @throws InvalidArgumentException + * + * @return SignupPlusAuthUriCreatedInterface + */ + public function create(): SignupPlusAuthUriCreatedInterface + { + $data = $this->getApiFactory()->signupPlusApi()->createAuthUri( + $this->paymentId, + $this->state, + ); + + return $this->make(SignupPlusAuthUriCreatedInterface::class, $data); + } +} diff --git a/src/Entities/SignupPlus/SignupPlusBuilder.php b/src/Entities/SignupPlus/SignupPlusBuilder.php new file mode 100644 index 00000000..d714497d --- /dev/null +++ b/src/Entities/SignupPlus/SignupPlusBuilder.php @@ -0,0 +1,34 @@ +entityFactory->make(SignupPlusAuthUriRequestInterface::class); + } + + /** + * @throws InvalidArgumentException + * + * @return SignupPlusUserDataRequestInterface + */ + public function userData(): SignupPlusUserDataRequestInterface + { + return $this->entityFactory->make(SignupPlusUserDataRequestInterface::class); + } +} diff --git a/src/Entities/SignupPlus/SignupPlusUserDataRequest.php b/src/Entities/SignupPlus/SignupPlusUserDataRequest.php new file mode 100644 index 00000000..24b770b4 --- /dev/null +++ b/src/Entities/SignupPlus/SignupPlusUserDataRequest.php @@ -0,0 +1,75 @@ +paymentId = $paymentId; + + return $this; + } + + /** + * @param string $mandateId + * + * @return SignupPlusUserDataRequestInterface + */ + public function mandateId(string $mandateId): SignupPlusUserDataRequestInterface + { + $this->mandateId = $mandateId; + + return $this; + } + + /** + * @throws ApiRequestJsonSerializationException + * @throws ApiResponseUnsuccessfulException + * @throws InvalidArgumentException + * @throws SignerException + * + * @return SignupPlusUserDataRetrievedInterface + */ + public function retrieve(): SignupPlusUserDataRetrievedInterface + { + if (empty($this->paymentId) && empty($this->mandateId)) { + throw new ApiRequestJsonSerializationException('You need to pass a payment or mandate id'); + } + + $data = !empty($this->paymentId) + ? $this->getApiFactory()->signupPlusApi()->retrieveUserDataByPaymentId($this->paymentId) + : $this->getApiFactory()->signupPlusApi()->retrieveUserDataByMandateId($this->mandateId); + + return $this->make(SignupPlusUserDataRetrievedInterface::class, $data); + } +} diff --git a/src/Entities/SignupPlus/SignupPlusUserDataRetrieved.php b/src/Entities/SignupPlus/SignupPlusUserDataRetrieved.php new file mode 100644 index 00000000..22c05a8e --- /dev/null +++ b/src/Entities/SignupPlus/SignupPlusUserDataRetrieved.php @@ -0,0 +1,124 @@ + AddressRetrievedInterface::class, + 'account_details' => SignupPlusAccountDetailsInterface::class, + ]; + + /** + * @var string[] + */ + protected array $arrayFields = [ + 'title', + 'first_name', + 'last_name', + 'date_of_birth', + 'address', + 'national_identification_number', + 'sex', + 'account_details', + ]; + + /** + * @return string|null + */ + public function getTitle(): ?string + { + return $this->title ?? null; + } + + public function getFirstName(): string + { + return $this->firstName; + } + + public function getLastName(): string + { + return $this->lastName; + } + + public function getDateOfBirth(): string + { + return $this->dateOfBirth; + } + + public function getAddress(): AddressRetrievedInterface + { + return $this->address; + } + + /** + * @return string|null + */ + public function getNationalIdentificationNumber(): ?string + { + return $this->nationalIdentificationNumber ?? null; + } + + /** + * @return string|null + */ + public function getSex(): ?string + { + return $this->sex ?? null; + } + + public function getAccountDetails(): SignupPlusAccountDetailsInterface + { + return $this->accountDetails; + } +} diff --git a/src/Factories/ApiFactory.php b/src/Factories/ApiFactory.php index 9ca51662..b9cde3c8 100644 --- a/src/Factories/ApiFactory.php +++ b/src/Factories/ApiFactory.php @@ -7,11 +7,13 @@ use TrueLayer\Interfaces\Api\MerchantAccountsApiInterface; use TrueLayer\Interfaces\Api\PaymentsApiInterface; use TrueLayer\Interfaces\Api\PayoutsApiInterface; +use TrueLayer\Interfaces\Api\SignupPlusApiInterface; use TrueLayer\Interfaces\ApiClient\ApiClientInterface; use TrueLayer\Interfaces\Factories\ApiFactoryInterface; use TrueLayer\Services\Api\MerchantAccountsApi; use TrueLayer\Services\Api\PaymentsApi; use TrueLayer\Services\Api\PayoutsApi; +use TrueLayer\Services\Api\SignupPlusApi; final class ApiFactory implements ApiFactoryInterface { @@ -51,4 +53,12 @@ public function payoutsApi(): PayoutsApiInterface { return new PayoutsApi($this->apiClient); } + + /** + * @return SignupPlusApiInterface + */ + public function signupPlusApi(): SignupPlusApiInterface + { + return new SignupPlusApi($this->apiClient); + } } diff --git a/src/Interfaces/AddressInterface.php b/src/Interfaces/AddressInterface.php index dfc301da..9219e83c 100644 --- a/src/Interfaces/AddressInterface.php +++ b/src/Interfaces/AddressInterface.php @@ -4,13 +4,8 @@ namespace TrueLayer\Interfaces; -interface AddressInterface extends ArrayableInterface, HasAttributesInterface +interface AddressInterface extends AddressRetrievedInterface { - /** - * @return string|null - */ - public function getAddressLine1(): ?string; - /** * @param string $addressLine1 * @@ -18,11 +13,6 @@ public function getAddressLine1(): ?string; */ public function addressLine1(string $addressLine1): AddressInterface; - /** - * @return string|null - */ - public function getAddressLine2(): ?string; - /** * @param string $addressLine2 * @@ -30,11 +20,6 @@ public function getAddressLine2(): ?string; */ public function addressLine2(string $addressLine2): AddressInterface; - /** - * @return string|null - */ - public function getCity(): ?string; - /** * @param string $city * @@ -42,11 +27,6 @@ public function getCity(): ?string; */ public function city(string $city): AddressInterface; - /** - * @return string|null - */ - public function getState(): ?string; - /** * @param string $state * @@ -54,11 +34,6 @@ public function getState(): ?string; */ public function state(string $state): AddressInterface; - /** - * @return string|null - */ - public function getZip(): ?string; - /** * @param string $zip * @@ -66,11 +41,6 @@ public function getZip(): ?string; */ public function zip(string $zip): AddressInterface; - /** - * @return string|null - */ - public function getCountryCode(): ?string; - /** * @param string $countryCode * diff --git a/src/Interfaces/AddressRetrievedInterface.php b/src/Interfaces/AddressRetrievedInterface.php new file mode 100644 index 00000000..d80d0939 --- /dev/null +++ b/src/Interfaces/AddressRetrievedInterface.php @@ -0,0 +1,38 @@ +request() + ->uri(Endpoints::SIGNUP_PLUS_AUTH) + ->payload([ + 'payment_id' => $paymentId, + 'state' => $state, + ]) + ->post(); + } + + /** + * @param string $paymentId + * + * @throws SignerException + * @throws ApiResponseUnsuccessfulException + * @throws ApiRequestJsonSerializationException + * + * @return mixed[] + */ + public function retrieveUserDataByPaymentId(string $paymentId): array + { + $uri = \str_replace('{id}', $paymentId, Endpoints::SIGNUP_PLUS_PAYMENTS); + + return (array) $this->request() + ->uri($uri) + ->get(); + } + + /** + * @param string $mandateId + * + * @throws ApiRequestJsonSerializationException + * @throws ApiResponseUnsuccessfulException + * @throws SignerException + * + * @return array|mixed[] + */ + public function retrieveUserDataByMandateId(string $mandateId): array + { + $uri = \str_replace('{id}', $mandateId, Endpoints::SIGNUP_PLUS_MANDATES); + + return (array) $this->request() + ->uri($uri) + ->get(); + } +} diff --git a/src/Services/Client/Client.php b/src/Services/Client/Client.php index fafca06e..5d80644e 100644 --- a/src/Services/Client/Client.php +++ b/src/Services/Client/Client.php @@ -37,6 +37,7 @@ use TrueLayer\Interfaces\Remitter\RemitterVerification\RemitterVerificationBuilderInterface; use TrueLayer\Interfaces\RequestOptionsInterface; use TrueLayer\Interfaces\Payment\Scheme\SchemeSelectionBuilderInterface; +use TrueLayer\Interfaces\SignupPlus\SignupPlusBuilderInterface; use TrueLayer\Interfaces\UserInterface; use TrueLayer\Interfaces\Webhook\WebhookInterface; use TrueLayer\Services\Util\PaymentId; @@ -424,4 +425,14 @@ public function requestOptions(): RequestOptionsInterface { return $this->entityFactory->make(RequestOptionsInterface::class); } + + /** + * @throws InvalidArgumentException + * + * @return SignupPlusBuilderInterface + */ + public function signupPlus(): SignupPlusBuilderInterface + { + return $this->entityFactory->make(SignupPlusBuilderInterface::class); + } } diff --git a/tests/integration/Mocks/SignupPlusResponse.php b/tests/integration/Mocks/SignupPlusResponse.php new file mode 100644 index 00000000..c8b2300e --- /dev/null +++ b/tests/integration/Mocks/SignupPlusResponse.php @@ -0,0 +1,25 @@ +signupPlus() + ->authUri() + ->paymentId($paymentId); + + if (!empty($state)) { + $request->state($state); + } + + $request->create(); + + \expect(\getRequestPayload(1))->toMatchArray([ + 'payment_id' => $paymentId, + 'state' => $state, + ]); +})->with([ + 'no state' => ['fake_payment_id', null], + 'with state' => ['fake_payment_id', 'fake_state'], +]); + +\it('parses the signup plus auth uri creation response correctly', function () { + $client = \client(SignupPlusResponse::authUriCreated()); + + $response = $client->signupPlus() + ->authUri() + ->paymentId('fake_payment_id') + ->state('fake_state') + ->create(); + + \expect($response)->toBeInstanceOf(SignupPlusAuthUriCreatedInterface::class); + \expect($response->getAuthUri())->toBeString(); +}); + +\it('retrieves the user data by payment id', function () { + $client = \client(SignupPlusResponse::userDataRetrievedFinland()); + + $response = $client->signupPlus() + ->userData() + ->paymentId('fake_payment_id') + ->retrieve(); + + \expect($response)->toBeInstanceOf(SignupPlusUserDataRetrievedInterface::class); + + \expect($response->getTitle())->toBeNull(); + \expect($response->getFirstName())->toBe('Tero Testi'); + \expect($response->getLastName())->toBe('Äyrämö'); + \expect($response->getNationalIdentificationNumber())->toBe('010170-1234'); + \expect($response->getSex())->toBe('M'); + + $address = $response->getAddress(); + \expect($address)->toBeInstanceOf(AddressRetrievedInterface::class); + \expect($address->getAddressLine1())->toBe('Kauppa Puistikko 6 B 15'); + \expect($address->getCity())->toBe('VAASA'); + \expect($address->getZip())->toBe('65100'); + + $accountDetails = $response->getAccountDetails(); + \expect($accountDetails)->toBeInstanceOf(SignupPlusAccountDetailsInterface::class); + \expect($accountDetails->getIban())->toBe('FI53CLRB04066200002723'); + \expect($accountDetails->getProviderId())->toBe('fi-op'); +}); + +\it('retrieves the user data by mandate id', function () { + $client = \client(SignupPlusResponse::userDataRetrievedUk()); + + $response = $client->signupPlus() + ->userData() + ->mandateId('fake_mandate_id') + ->retrieve(); + + \expect($response)->toBeInstanceOf(SignupPlusUserDataRetrievedInterface::class); + + \expect($response->getTitle())->toBe('Mr'); + \expect($response->getFirstName())->toBe('Sherlock'); + \expect($response->getLastName())->toBe('Holmes'); + \expect($response->getDateOfBirth())->toBe('1854-01-06'); + + $address = $response->getAddress(); + \expect($address)->toBeInstanceOf(AddressRetrievedInterface::class); + \expect($address->getAddressLine1())->toBe('221B Baker St'); + \expect($address->getAddressLine2())->toBe('Flat 2'); + \expect($address->getCity())->toBe('London'); + \expect($address->getState())->toBe('Greater London'); + \expect($address->getZip())->toBe('NW1 6XE'); + + $accountDetails = $response->getAccountDetails(); + \expect($accountDetails)->toBeInstanceOf(SignupPlusAccountDetailsInterface::class); + \expect($accountDetails->getAccountNumber())->toBe('41921234'); + \expect($accountDetails->getSortCode())->toBe('04-01-02'); + \expect($accountDetails->getIban())->toBe('GB71MONZ04435141923452'); + \expect($accountDetails->getProviderId())->toBe('ob-monzo'); +});