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');
+});