From 5f60db615ac8e87f09e6ccfefe748ce2cd2a4897 Mon Sep 17 00:00:00 2001
From: Stefan Adrian Danaita <me@dsa.io>
Date: Wed, 31 Jan 2024 15:18:23 +0100
Subject: [PATCH 01/11] [PHP-70] Support for setting the user's address and
 date of birth

---
 config/bindings.php                           |   5 +-
 src/Entities/Address.php                      | 185 ++++++++++++++++++
 src/Entities/Entity.php                       |   2 +-
 src/Entities/User.php                         |  89 ++++++++-
 src/Interfaces/AddressInterface.php           |  80 ++++++++
 src/Interfaces/UserInterface.php              |  35 ++++
 tests/acceptance/Payment/CreatePayment.php    |  20 ++
 ...erchantAccountPaymentAuthorizationTest.php |  47 +++++
 tests/integration/Mocks/CreatePayment.php     |  21 ++
 tests/integration/PaymentCreateTest.php       |  56 ++++++
 10 files changed, 532 insertions(+), 8 deletions(-)
 create mode 100644 src/Entities/Address.php
 create mode 100644 src/Interfaces/AddressInterface.php

diff --git a/config/bindings.php b/config/bindings.php
index d439a85a..4fac259f 100644
--- a/config/bindings.php
+++ b/config/bindings.php
@@ -3,13 +3,14 @@
 declare(strict_types=1);
 
 use TrueLayer\Entities;
-use TrueLayer\Entities\User;
 use TrueLayer\Interfaces;
 
 return [
-    Interfaces\UserInterface::class => User::class,
     Interfaces\HppInterface::class => 'makeHpp',
 
+    Interfaces\AddressInterface::class => Entities\Address::class,
+    Interfaces\UserInterface::class => Entities\User::class,
+
     Interfaces\Beneficiary\BeneficiaryBuilderInterface::class => Entities\Beneficiary\BeneficiaryBuilder::class,
     Interfaces\Beneficiary\MerchantBeneficiaryInterface::class => Entities\Beneficiary\MerchantBeneficiary::class,
     Interfaces\Beneficiary\ExternalAccountBeneficiaryInterface::class => Entities\Beneficiary\ExternalAccountBeneficiary::class,
diff --git a/src/Entities/Address.php b/src/Entities/Address.php
new file mode 100644
index 00000000..e902561a
--- /dev/null
+++ b/src/Entities/Address.php
@@ -0,0 +1,185 @@
+<?php
+
+namespace TrueLayer\Entities;
+
+use TrueLayer\Interfaces\AddressInterface;
+
+class Address extends Entity 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 array
+     */
+    protected function rules(): array
+    {
+        return [
+            'address_line1' => 'string|required',
+            'address_line2' => 'string|nullable',
+            'city' => 'string|required',
+            'state' => 'string|required',
+            'zip' => 'string|required',
+            'country_code' => 'string|required',
+        ];
+    }
+
+    /**
+     * @return string
+     */
+    public function getAddressLine1(): string
+    {
+        return $this->addressLine1;
+    }
+
+    /**
+     * @param string $addressLine1
+     *
+     * @return AddressInterface
+     */
+    public function addressLine1(string $addressLine1): AddressInterface
+    {
+        $this->addressLine1 = $addressLine1;
+
+        return $this;
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getAddressLine2(): ?string
+    {
+        return $this->addressLine2 ?? null;
+    }
+
+    /**
+     * @param string $addressLine2
+     *
+     * @return AddressInterface
+     */
+    public function addressLine2(string $addressLine2): AddressInterface
+    {
+        $this->addressLine2 = $addressLine2;
+
+        return $this;
+    }
+
+    /**
+     * @return string
+     */
+    public function getCity(): string
+    {
+        return $this->city;
+    }
+
+    /**
+     * @param string $city
+     *
+     * @return AddressInterface
+     */
+    public function city(string $city): AddressInterface
+    {
+        $this->city = $city;
+
+        return $this;
+    }
+
+    /**
+     * @return string
+     */
+    public function getState(): string
+    {
+        return $this->state;
+    }
+
+    /**
+     * @param string $state
+     *
+     * @return AddressInterface
+     */
+    public function state(string $state): AddressInterface
+    {
+        $this->state = $state;
+
+        return $this;
+    }
+
+    /**
+     * @return string
+     */
+    public function getZip(): string
+    {
+        return $this->zip;
+    }
+
+    /**
+     * @param string $zip
+     *
+     * @return AddressInterface
+     */
+    public function zip(string $zip): AddressInterface
+    {
+        $this->zip = $zip;
+
+        return $this;
+    }
+
+    /**
+     * @return string
+     */
+    public function getCountryCode(): string
+    {
+        return $this->countryCode;
+    }
+
+    /**
+     * @param string $countryCode
+     *
+     * @return AddressInterface
+     */
+    public function countryCode(string $countryCode): AddressInterface
+    {
+        $this->countryCode = $countryCode;
+
+        return $this;
+    }
+}
diff --git a/src/Entities/Entity.php b/src/Entities/Entity.php
index 1977f8fe..6d5c0afe 100644
--- a/src/Entities/Entity.php
+++ b/src/Entities/Entity.php
@@ -23,7 +23,7 @@ abstract class Entity implements ArrayableInterface, HasAttributesInterface
     /**
      * @var EntityFactoryInterface
      */
-    private EntityFactoryInterface $entityFactory;
+    protected EntityFactoryInterface $entityFactory;
 
     /**
      * @param ValidatorFactory       $validatorFactory
diff --git a/src/Entities/User.php b/src/Entities/User.php
index 31bd31d5..a39b3e8a 100644
--- a/src/Entities/User.php
+++ b/src/Entities/User.php
@@ -4,7 +4,11 @@
 
 namespace TrueLayer\Entities;
 
+use TrueLayer\Exceptions\InvalidArgumentException;
+use TrueLayer\Exceptions\ValidationException;
+use TrueLayer\Interfaces\AddressInterface;
 use TrueLayer\Interfaces\UserInterface;
+use TrueLayer\Validation\ValidType;
 
 final class User extends Entity implements UserInterface
 {
@@ -28,6 +32,16 @@ final class User extends Entity implements UserInterface
      */
     protected string $phone;
 
+    /**
+     * @var AddressInterface
+     */
+    protected AddressInterface $address;
+
+    /**
+     * @var string
+     */
+    protected string $dateOfBirth;
+
     /**
      * @var string[]
      */
@@ -36,18 +50,32 @@ final class User extends Entity implements UserInterface
         'name',
         'email',
         'phone',
+        'address',
+        'date_of_birth',
     ];
 
     /**
      * @var string[]
      */
-    protected array $rules = [
-        'id' => 'string|nullable',
-        'name' => 'string|nullable|required_without:id',
-        'email' => 'string|nullable|email|required_without_all:phone,id',
-        'phone' => 'string|nullable|required_without_all:email,id',
+    protected array $casts = [
+        'address' => AddressInterface::class,
     ];
 
+    /**
+     * @return array
+     */
+    protected function rules(): array
+    {
+        return [
+            'id' => 'string|nullable',
+            'name' => 'string|nullable|required_without:id',
+            'email' => 'string|nullable|email|required_without_all:phone,id',
+            'phone' => 'string|nullable|required_without_all:email,id',
+            'address' => ['nullable', ValidType::of(AddressInterface::class)],
+            'date_of_birth' => 'string|nullable|date',
+        ];
+    }
+
     /**
      * @return string|null
      */
@@ -127,4 +155,55 @@ public function phone(string $phone): UserInterface
 
         return $this;
     }
+
+    /**
+     * @return AddressInterface|null
+     */
+    public function getAddress(): ?AddressInterface
+    {
+        return $this->address ?? null;
+    }
+
+    /**
+     * @param AddressInterface $address
+     *
+     * @return UserInterface
+     */
+    public function address(AddressInterface $address): UserInterface
+    {
+        $this->address = $address;
+
+        return $this;
+    }
+
+    /**
+     * @throws ValidationException
+     * @throws InvalidArgumentException
+     *
+     * @return AddressInterface
+     */
+    public function addressBuilder(): AddressInterface
+    {
+        return $this->entityFactory->make(AddressInterface::class);
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getDateOfBirth(): ?string
+    {
+        return $this->dateOfBirth ?? null;
+    }
+
+    /**
+     * @param string $dateOfBirth
+     *
+     * @return UserInterface
+     */
+    public function dateOfBirth(string $dateOfBirth): UserInterface
+    {
+        $this->dateOfBirth = $dateOfBirth;
+
+        return $this;
+    }
 }
diff --git a/src/Interfaces/AddressInterface.php b/src/Interfaces/AddressInterface.php
new file mode 100644
index 00000000..0efab398
--- /dev/null
+++ b/src/Interfaces/AddressInterface.php
@@ -0,0 +1,80 @@
+<?php
+
+declare(strict_types=1);
+
+namespace TrueLayer\Interfaces;
+
+interface AddressInterface extends ArrayableInterface, HasAttributesInterface
+{
+    /**
+     * @return string
+     */
+    public function getAddressLine1(): string;
+
+    /**
+     * @param string $addressLine1
+     *
+     * @return AddressInterface
+     */
+    public function addressLine1(string $addressLine1): AddressInterface;
+
+    /**
+     * @return string|null
+     */
+    public function getAddressLine2(): ?string;
+
+    /**
+     * @param string $addressLine2
+     *
+     * @return AddressInterface
+     */
+    public function addressLine2(string $addressLine2): AddressInterface;
+
+    /**
+     * @return string
+     */
+    public function getCity(): string;
+
+    /**
+     * @param string $city
+     *
+     * @return AddressInterface
+     */
+    public function city(string $city): AddressInterface;
+
+    /**
+     * @return string
+     */
+    public function getState(): string;
+
+    /**
+     * @param string $state
+     *
+     * @return AddressInterface
+     */
+    public function state(string $state): AddressInterface;
+
+    /**
+     * @return string
+     */
+    public function getZip(): string;
+
+    /**
+     * @param string $zip
+     *
+     * @return AddressInterface
+     */
+    public function zip(string $zip): AddressInterface;
+
+    /**
+     * @return string
+     */
+    public function getCountryCode(): string;
+
+    /**
+     * @param string $countryCode
+     *
+     * @return AddressInterface
+     */
+    public function countryCode(string $countryCode): AddressInterface;
+}
diff --git a/src/Interfaces/UserInterface.php b/src/Interfaces/UserInterface.php
index 6a22c924..276e569c 100644
--- a/src/Interfaces/UserInterface.php
+++ b/src/Interfaces/UserInterface.php
@@ -4,6 +4,9 @@
 
 namespace TrueLayer\Interfaces;
 
+use TrueLayer\Exceptions\InvalidArgumentException;
+use TrueLayer\Exceptions\ValidationException;
+
 interface UserInterface extends ArrayableInterface, HasAttributesInterface
 {
     /**
@@ -53,4 +56,36 @@ public function getPhone(): ?string;
      * @return UserInterface
      */
     public function phone(string $phone): UserInterface;
+
+    /**
+     * @return AddressInterface|null
+     */
+    public function getAddress(): ?AddressInterface;
+
+    /**
+     * @param AddressInterface $address
+     *
+     * @return UserInterface
+     */
+    public function address(AddressInterface $address): UserInterface;
+
+    /**
+     * @throws ValidationException
+     * @throws InvalidArgumentException
+     *
+     * @return AddressInterface
+     */
+    public function addressBuilder(): AddressInterface;
+
+    /**
+     * @return string|null
+     */
+    public function getDateOfBirth(): ?string;
+
+    /**
+     * @param string $dateOfBirth
+     *
+     * @return UserInterface
+     */
+    public function dateOfBirth(string $dateOfBirth): UserInterface;
 }
diff --git a/tests/acceptance/Payment/CreatePayment.php b/tests/acceptance/Payment/CreatePayment.php
index 57a23d6c..961b39a8 100644
--- a/tests/acceptance/Payment/CreatePayment.php
+++ b/tests/acceptance/Payment/CreatePayment.php
@@ -69,6 +69,26 @@ public function user(): UserInterface
             ->email('alice@truelayer.com');
     }
 
+    public function userWithAddress(): UserInterface
+    {
+        $user = $this->user();
+        $address = $user->addressBuilder()
+            ->addressLine1("The Gilbert")
+            ->city("London")
+            ->state("London")
+            ->zip("EC2A 1PX")
+            ->countryCode("GB");
+
+        return $this->user()
+            ->address($address);
+    }
+
+    public function userWithDateOfBirth(string $date): UserInterface
+    {
+        return $this->user()
+            ->dateOfBirth($date);
+    }
+
     /**
      * @param BeneficiaryInterface $beneficiary
      *
diff --git a/tests/acceptance/Payment/MerchantAccountPaymentAuthorizationTest.php b/tests/acceptance/Payment/MerchantAccountPaymentAuthorizationTest.php
index 42d48ad7..ec85d2d5 100644
--- a/tests/acceptance/Payment/MerchantAccountPaymentAuthorizationTest.php
+++ b/tests/acceptance/Payment/MerchantAccountPaymentAuthorizationTest.php
@@ -134,6 +134,53 @@
     \expect($fetched->getId())->toBeString();
 });
 
+\it('creates payment with user address', function () {
+    $helper = \paymentHelper();
+
+    $payment = $helper->client()->payment()
+        ->paymentMethod($helper->bankTransferMethod($helper->sortCodeBeneficiary()))
+        ->amountInMinor(10)
+        ->currency('GBP')
+        ->user($helper->userWithAddress())
+        ->create();
+
+    $fetched = $payment->getDetails();
+
+    \expect($payment)->toBeInstanceOf(PaymentCreatedInterface::class);
+    \expect($payment->getId())->toBeString();
+    \expect($fetched)->toBeInstanceOf(PaymentRetrievedInterface::class);
+    \expect($fetched->getId())->toBeString();
+});
+
+\it('creates payment with valid user date of birth', function () {
+    $helper = \paymentHelper();
+
+    $payment = $helper->client()->payment()
+        ->paymentMethod($helper->bankTransferMethod($helper->sortCodeBeneficiary()))
+        ->amountInMinor(10)
+        ->currency('GBP')
+        ->user($helper->userWithDateOfBirth('2024-01-01'))
+        ->create();
+
+    $fetched = $payment->getDetails();
+
+    \expect($payment)->toBeInstanceOf(PaymentCreatedInterface::class);
+    \expect($payment->getId())->toBeString();
+    \expect($fetched)->toBeInstanceOf(PaymentRetrievedInterface::class);
+    \expect($fetched->getId())->toBeString();
+});
+
+\it('throws exception when creating payment with invalid user date of birth', function () {
+    $helper = \paymentHelper();
+
+    $helper->client()->payment()
+        ->paymentMethod($helper->bankTransferMethod($helper->sortCodeBeneficiary()))
+        ->amountInMinor(10)
+        ->currency('GBP')
+        ->user($helper->userWithDateOfBirth('invalid date'))
+        ->create();
+})->throws(TrueLayer\Exceptions\ValidationException::class);
+
 \it('creates payment with idempotency key', function () {
     $helper = \paymentHelper();
 
diff --git a/tests/integration/Mocks/CreatePayment.php b/tests/integration/Mocks/CreatePayment.php
index 5426a7e8..377b1f0c 100644
--- a/tests/integration/Mocks/CreatePayment.php
+++ b/tests/integration/Mocks/CreatePayment.php
@@ -57,6 +57,27 @@ public function newUser(): UserInterface
             ->email('alice@truelayer.com');
     }
 
+    public function newUserWithAddress(): UserInterface
+    {
+        $address = $this->client
+            ->user()
+            ->addressBuilder()
+            ->addressLine1("The Gilbert")
+            ->city("London")
+            ->state("London")
+            ->zip("EC2A 1PX")
+            ->countryCode("GB");
+
+        return $this->newUser()
+            ->address($address);
+    }
+
+    public function newUserWithDateOfBirth(string $date)
+    {
+        return $this->newUser()
+            ->dateOfBirth($date);
+    }
+
     /**
      * @return UserInterface
      */
diff --git a/tests/integration/PaymentCreateTest.php b/tests/integration/PaymentCreateTest.php
index f27cc1c5..34714f95 100644
--- a/tests/integration/PaymentCreateTest.php
+++ b/tests/integration/PaymentCreateTest.php
@@ -67,6 +67,8 @@
             'name' => 'Alice',
             'phone' => '+447837485713',
             'email' => 'alice@truelayer.com',
+            'address' => null,
+            'date_of_birth' => null,
         ],
     ]);
 
@@ -76,10 +78,64 @@
             'name' => null,
             'phone' => null,
             'email' => null,
+            'address' => null,
+            'date_of_birth' => null,
         ],
     ]);
 });
 
+\it('sends the right user address', function () {
+    $factory = CreatePayment::responses([
+        PaymentResponse::created(),
+    ]);
+    $factory->payment($factory->newUserWithAddress(), $factory->bankTransferMethod($factory->sortCodeBeneficiary()))->create();
+
+    \expect(\getRequestPayload(1))->toMatchArray([
+        'user' => [
+            'id' => null,
+            'name' => 'Alice',
+            'phone' => '+447837485713',
+            'email' => 'alice@truelayer.com',
+            'address' => [
+                'address_line1' => 'The Gilbert',
+                'address_line2' => null,
+                'city' => 'London',
+                'state' => 'London',
+                'zip' => 'EC2A 1PX',
+                'country_code' => 'GB',
+            ],
+            'date_of_birth' => null,
+        ],
+    ]);
+});
+
+\it('sends the right date of birth', function () {
+    $factory = CreatePayment::responses([
+        PaymentResponse::created(),
+    ]);
+
+    $factory->payment($factory->newUserWithDateOfBirth("2024-01-01"), $factory->bankTransferMethod($factory->sortCodeBeneficiary()))->create();
+
+    \expect(\getRequestPayload(1))->toMatchArray([
+        'user' => [
+            'id' => null,
+            'name' => 'Alice',
+            'phone' => '+447837485713',
+            'email' => 'alice@truelayer.com',
+            'address' => null,
+            'date_of_birth' => "2024-01-01",
+        ],
+    ]);
+});
+
+\it('should throw when sending an invalid user date of birth', function () {
+    $factory = CreatePayment::responses([
+        PaymentResponse::created(),
+    ]);
+
+    $factory->payment($factory->newUserWithDateOfBirth("invalid data"), $factory->bankTransferMethod($factory->sortCodeBeneficiary()))->create();
+})->throws(TrueLayer\Exceptions\ValidationException::class);
+
 \it('parses payment creation response correctly', function () {
     $factory = CreatePayment::responses([PaymentResponse::created()]);
     $payment = $factory->payment($factory->newUser(), $factory->bankTransferMethod($factory->sortCodeBeneficiary()))->create();

From 2f18b1b0e142777d4641e04c5b51249b4a717b94 Mon Sep 17 00:00:00 2001
From: Stefan Adrian Danaita <me@dsa.io>
Date: Wed, 31 Jan 2024 15:20:00 +0100
Subject: [PATCH 02/11] [PHP-70] Strict types

---
 src/Entities/Address.php | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/Entities/Address.php b/src/Entities/Address.php
index e902561a..d64ffc77 100644
--- a/src/Entities/Address.php
+++ b/src/Entities/Address.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace TrueLayer\Entities;
 
 use TrueLayer\Interfaces\AddressInterface;

From 0e78bb240666e63b7e9d79d1c88f5047b84a6b18 Mon Sep 17 00:00:00 2001
From: Stefan Adrian Danaita <me@dsa.io>
Date: Wed, 31 Jan 2024 19:27:32 +0100
Subject: [PATCH 03/11] [PHP-70] Simplify the address setter

---
 src/Entities/User.php                     | 19 +++++--------------
 src/Interfaces/UserInterface.php          | 12 ++----------
 tests/integration/Mocks/CreatePayment.php | 11 +++++------
 3 files changed, 12 insertions(+), 30 deletions(-)

diff --git a/src/Entities/User.php b/src/Entities/User.php
index a39b3e8a..08e64695 100644
--- a/src/Entities/User.php
+++ b/src/Entities/User.php
@@ -165,28 +165,19 @@ public function getAddress(): ?AddressInterface
     }
 
     /**
-     * @param AddressInterface $address
+     * @param AddressInterface|null $address
      *
-     * @return UserInterface
-     */
-    public function address(AddressInterface $address): UserInterface
-    {
-        $this->address = $address;
-
-        return $this;
-    }
-
-    /**
      * @throws ValidationException
      * @throws InvalidArgumentException
      *
      * @return AddressInterface
      */
-    public function addressBuilder(): AddressInterface
+    public function address(?AddressInterface $address = null): AddressInterface
     {
-        return $this->entityFactory->make(AddressInterface::class);
-    }
+        $this->address = $address ?: $this->entityFactory->make(AddressInterface::class);
 
+        return $this->getAddress();
+    }
     /**
      * @return string|null
      */
diff --git a/src/Interfaces/UserInterface.php b/src/Interfaces/UserInterface.php
index 276e569c..a3254e91 100644
--- a/src/Interfaces/UserInterface.php
+++ b/src/Interfaces/UserInterface.php
@@ -63,19 +63,11 @@ public function phone(string $phone): UserInterface;
     public function getAddress(): ?AddressInterface;
 
     /**
-     * @param AddressInterface $address
-     *
-     * @return UserInterface
-     */
-    public function address(AddressInterface $address): UserInterface;
-
-    /**
-     * @throws ValidationException
-     * @throws InvalidArgumentException
+     * @param AddressInterface|null $address
      *
      * @return AddressInterface
      */
-    public function addressBuilder(): AddressInterface;
+    public function address(?AddressInterface $address): AddressInterface;
 
     /**
      * @return string|null
diff --git a/tests/integration/Mocks/CreatePayment.php b/tests/integration/Mocks/CreatePayment.php
index 377b1f0c..3b2006f3 100644
--- a/tests/integration/Mocks/CreatePayment.php
+++ b/tests/integration/Mocks/CreatePayment.php
@@ -59,20 +59,19 @@ public function newUser(): UserInterface
 
     public function newUserWithAddress(): UserInterface
     {
-        $address = $this->client
-            ->user()
-            ->addressBuilder()
+        $user = $this->newUser();
+
+        $user->address()
             ->addressLine1("The Gilbert")
             ->city("London")
             ->state("London")
             ->zip("EC2A 1PX")
             ->countryCode("GB");
 
-        return $this->newUser()
-            ->address($address);
+        return $user;
     }
 
-    public function newUserWithDateOfBirth(string $date)
+    public function newUserWithDateOfBirth(string $date): UserInterface
     {
         return $this->newUser()
             ->dateOfBirth($date);

From 092afed0f8081dba9ae7a506dcc7a5d11ee265f4 Mon Sep 17 00:00:00 2001
From: Stefan Adrian Danaita <me@dsa.io>
Date: Wed, 31 Jan 2024 19:33:08 +0100
Subject: [PATCH 04/11] [PHP-70] PHPStan errors

---
 src/Entities/Address.php | 21 +++++++++------------
 src/Entities/User.php    |  4 ++--
 2 files changed, 11 insertions(+), 14 deletions(-)

diff --git a/src/Entities/Address.php b/src/Entities/Address.php
index d64ffc77..8a9d8efd 100644
--- a/src/Entities/Address.php
+++ b/src/Entities/Address.php
@@ -51,19 +51,16 @@ class Address extends Entity implements AddressInterface
     ];
 
     /**
-     * @return array
+     * @var string[]
      */
-    protected function rules(): array
-    {
-        return [
-            'address_line1' => 'string|required',
-            'address_line2' => 'string|nullable',
-            'city' => 'string|required',
-            'state' => 'string|required',
-            'zip' => 'string|required',
-            'country_code' => 'string|required',
-        ];
-    }
+    protected array $rules = [
+        'address_line1' => 'string|required',
+        'address_line2' => 'string|nullable',
+        'city' => 'string|required',
+        'state' => 'string|required',
+        'zip' => 'string|required',
+        'country_code' => 'string|required',
+    ];
 
     /**
      * @return string
diff --git a/src/Entities/User.php b/src/Entities/User.php
index 08e64695..33725541 100644
--- a/src/Entities/User.php
+++ b/src/Entities/User.php
@@ -62,7 +62,7 @@ final class User extends Entity implements UserInterface
     ];
 
     /**
-     * @return array
+     * @return mixed[]
      */
     protected function rules(): array
     {
@@ -176,7 +176,7 @@ public function address(?AddressInterface $address = null): AddressInterface
     {
         $this->address = $address ?: $this->entityFactory->make(AddressInterface::class);
 
-        return $this->getAddress();
+        return $this->address;
     }
     /**
      * @return string|null

From 55032c11842372d2d2b0c73e676b0ed38c201e45 Mon Sep 17 00:00:00 2001
From: Stefan Adrian Danaita <me@dsa.io>
Date: Wed, 31 Jan 2024 19:36:45 +0100
Subject: [PATCH 05/11] [PHP-70] Add PHP 8.2 and 8.3 to the code quality check
 matrix

---
 .github/workflows/php.yml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml
index d6cab5a1..8bf1d30c 100644
--- a/.github/workflows/php.yml
+++ b/.github/workflows/php.yml
@@ -23,6 +23,8 @@ jobs:
           - "7.4"
           - "8.0"
           - "8.1"
+          - "8.2"
+          - "8.3"
     steps:
       - name: Checkout
         uses: actions/checkout@v2

From fe2536be7607c74765cbfb78c13f8877ad6a4a4a Mon Sep 17 00:00:00 2001
From: Stefan Adrian Danaita <me@dsa.io>
Date: Wed, 31 Jan 2024 19:43:12 +0100
Subject: [PATCH 06/11] Revert "[PHP-70] Add PHP 8.2 and 8.3 to the code
 quality check matrix"

This reverts commit 55032c11842372d2d2b0c73e676b0ed38c201e45.
---
 .github/workflows/php.yml | 2 --
 1 file changed, 2 deletions(-)

diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml
index 8bf1d30c..d6cab5a1 100644
--- a/.github/workflows/php.yml
+++ b/.github/workflows/php.yml
@@ -23,8 +23,6 @@ jobs:
           - "7.4"
           - "8.0"
           - "8.1"
-          - "8.2"
-          - "8.3"
     steps:
       - name: Checkout
         uses: actions/checkout@v2

From 304c1b10a64228ff05c34382e5581180df4ca2eb Mon Sep 17 00:00:00 2001
From: Stefan Adrian Danaita <me@dsa.io>
Date: Tue, 6 Feb 2024 16:23:13 +0000
Subject: [PATCH 07/11] [PHP-70] Make address fields optional and fix tests
 method

---
 src/Entities/Address.php                   | 30 +++++++++++-----------
 src/Entities/User.php                      |  1 +
 src/Interfaces/AddressInterface.php        | 20 +++++++--------
 src/Interfaces/UserInterface.php           |  3 ---
 tests/acceptance/Payment/CreatePayment.php | 17 ++++++------
 tests/integration/Mocks/CreatePayment.php  | 10 ++++----
 tests/integration/PaymentCreateTest.php    |  6 ++---
 7 files changed, 42 insertions(+), 45 deletions(-)

diff --git a/src/Entities/Address.php b/src/Entities/Address.php
index 8a9d8efd..80dfaf61 100644
--- a/src/Entities/Address.php
+++ b/src/Entities/Address.php
@@ -63,11 +63,11 @@ class Address extends Entity implements AddressInterface
     ];
 
     /**
-     * @return string
+     * @return string|null
      */
-    public function getAddressLine1(): string
+    public function getAddressLine1(): ?string
     {
-        return $this->addressLine1;
+        return $this->addressLine1 ?? null;
     }
 
     /**
@@ -103,11 +103,11 @@ public function addressLine2(string $addressLine2): AddressInterface
     }
 
     /**
-     * @return string
+     * @return string|null
      */
-    public function getCity(): string
+    public function getCity(): ?string
     {
-        return $this->city;
+        return $this->city ?? null;
     }
 
     /**
@@ -123,11 +123,11 @@ public function city(string $city): AddressInterface
     }
 
     /**
-     * @return string
+     * @return string|null
      */
-    public function getState(): string
+    public function getState(): ?string
     {
-        return $this->state;
+        return $this->state ?? null;
     }
 
     /**
@@ -143,11 +143,11 @@ public function state(string $state): AddressInterface
     }
 
     /**
-     * @return string
+     * @return string|null
      */
-    public function getZip(): string
+    public function getZip(): ?string
     {
-        return $this->zip;
+        return $this->zip ?? null;
     }
 
     /**
@@ -163,11 +163,11 @@ public function zip(string $zip): AddressInterface
     }
 
     /**
-     * @return string
+     * @return string|null
      */
-    public function getCountryCode(): string
+    public function getCountryCode(): ?string
     {
-        return $this->countryCode;
+        return $this->countryCode ?? null;
     }
 
     /**
diff --git a/src/Entities/User.php b/src/Entities/User.php
index 33725541..5f19b71c 100644
--- a/src/Entities/User.php
+++ b/src/Entities/User.php
@@ -178,6 +178,7 @@ public function address(?AddressInterface $address = null): AddressInterface
 
         return $this->address;
     }
+
     /**
      * @return string|null
      */
diff --git a/src/Interfaces/AddressInterface.php b/src/Interfaces/AddressInterface.php
index 0efab398..dfc301da 100644
--- a/src/Interfaces/AddressInterface.php
+++ b/src/Interfaces/AddressInterface.php
@@ -7,9 +7,9 @@
 interface AddressInterface extends ArrayableInterface, HasAttributesInterface
 {
     /**
-     * @return string
+     * @return string|null
      */
-    public function getAddressLine1(): string;
+    public function getAddressLine1(): ?string;
 
     /**
      * @param string $addressLine1
@@ -31,9 +31,9 @@ public function getAddressLine2(): ?string;
     public function addressLine2(string $addressLine2): AddressInterface;
 
     /**
-     * @return string
+     * @return string|null
      */
-    public function getCity(): string;
+    public function getCity(): ?string;
 
     /**
      * @param string $city
@@ -43,9 +43,9 @@ public function getCity(): string;
     public function city(string $city): AddressInterface;
 
     /**
-     * @return string
+     * @return string|null
      */
-    public function getState(): string;
+    public function getState(): ?string;
 
     /**
      * @param string $state
@@ -55,9 +55,9 @@ public function getState(): string;
     public function state(string $state): AddressInterface;
 
     /**
-     * @return string
+     * @return string|null
      */
-    public function getZip(): string;
+    public function getZip(): ?string;
 
     /**
      * @param string $zip
@@ -67,9 +67,9 @@ public function getZip(): string;
     public function zip(string $zip): AddressInterface;
 
     /**
-     * @return string
+     * @return string|null
      */
-    public function getCountryCode(): string;
+    public function getCountryCode(): ?string;
 
     /**
      * @param string $countryCode
diff --git a/src/Interfaces/UserInterface.php b/src/Interfaces/UserInterface.php
index a3254e91..5fad0f74 100644
--- a/src/Interfaces/UserInterface.php
+++ b/src/Interfaces/UserInterface.php
@@ -4,9 +4,6 @@
 
 namespace TrueLayer\Interfaces;
 
-use TrueLayer\Exceptions\InvalidArgumentException;
-use TrueLayer\Exceptions\ValidationException;
-
 interface UserInterface extends ArrayableInterface, HasAttributesInterface
 {
     /**
diff --git a/tests/acceptance/Payment/CreatePayment.php b/tests/acceptance/Payment/CreatePayment.php
index 961b39a8..db72e38f 100644
--- a/tests/acceptance/Payment/CreatePayment.php
+++ b/tests/acceptance/Payment/CreatePayment.php
@@ -72,15 +72,14 @@ public function user(): UserInterface
     public function userWithAddress(): UserInterface
     {
         $user = $this->user();
-        $address = $user->addressBuilder()
-            ->addressLine1("The Gilbert")
-            ->city("London")
-            ->state("London")
-            ->zip("EC2A 1PX")
-            ->countryCode("GB");
-
-        return $this->user()
-            ->address($address);
+        $user->address()
+            ->addressLine1('The Gilbert')
+            ->city('London')
+            ->state('London')
+            ->zip('EC2A 1PX')
+            ->countryCode('GB');
+
+        return $user;
     }
 
     public function userWithDateOfBirth(string $date): UserInterface
diff --git a/tests/integration/Mocks/CreatePayment.php b/tests/integration/Mocks/CreatePayment.php
index 3b2006f3..1a8c3e54 100644
--- a/tests/integration/Mocks/CreatePayment.php
+++ b/tests/integration/Mocks/CreatePayment.php
@@ -62,11 +62,11 @@ public function newUserWithAddress(): UserInterface
         $user = $this->newUser();
 
         $user->address()
-            ->addressLine1("The Gilbert")
-            ->city("London")
-            ->state("London")
-            ->zip("EC2A 1PX")
-            ->countryCode("GB");
+            ->addressLine1('The Gilbert')
+            ->city('London')
+            ->state('London')
+            ->zip('EC2A 1PX')
+            ->countryCode('GB');
 
         return $user;
     }
diff --git a/tests/integration/PaymentCreateTest.php b/tests/integration/PaymentCreateTest.php
index 34714f95..66de72dc 100644
--- a/tests/integration/PaymentCreateTest.php
+++ b/tests/integration/PaymentCreateTest.php
@@ -114,7 +114,7 @@
         PaymentResponse::created(),
     ]);
 
-    $factory->payment($factory->newUserWithDateOfBirth("2024-01-01"), $factory->bankTransferMethod($factory->sortCodeBeneficiary()))->create();
+    $factory->payment($factory->newUserWithDateOfBirth('2024-01-01'), $factory->bankTransferMethod($factory->sortCodeBeneficiary()))->create();
 
     \expect(\getRequestPayload(1))->toMatchArray([
         'user' => [
@@ -123,7 +123,7 @@
             'phone' => '+447837485713',
             'email' => 'alice@truelayer.com',
             'address' => null,
-            'date_of_birth' => "2024-01-01",
+            'date_of_birth' => '2024-01-01',
         ],
     ]);
 });
@@ -133,7 +133,7 @@
         PaymentResponse::created(),
     ]);
 
-    $factory->payment($factory->newUserWithDateOfBirth("invalid data"), $factory->bankTransferMethod($factory->sortCodeBeneficiary()))->create();
+    $factory->payment($factory->newUserWithDateOfBirth('invalid data'), $factory->bankTransferMethod($factory->sortCodeBeneficiary()))->create();
 })->throws(TrueLayer\Exceptions\ValidationException::class);
 
 \it('parses payment creation response correctly', function () {

From 0df43fcd2ad411fef86c4e84a00973bcd4c91f9a Mon Sep 17 00:00:00 2001
From: Stefan Adrian Danaita <me@dsa.io>
Date: Fri, 9 Feb 2024 14:01:14 +0000
Subject: [PATCH 08/11] [PHP-70] Fix tests

---
 .../Payment/ExternalAccountPaymentAuthorizationTest.php   | 8 ++++----
 tests/integration/Mocks/CreatePayment.php                 | 1 -
 2 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/tests/acceptance/Payment/ExternalAccountPaymentAuthorizationTest.php b/tests/acceptance/Payment/ExternalAccountPaymentAuthorizationTest.php
index d36d0e5a..a6fbea6c 100644
--- a/tests/acceptance/Payment/ExternalAccountPaymentAuthorizationTest.php
+++ b/tests/acceptance/Payment/ExternalAccountPaymentAuthorizationTest.php
@@ -66,7 +66,7 @@
     \expect($config['redirect']['return_uri'])->toBe('https://console.truelayer.com/redirect-page');
 
     return $created;
-})->only();
+});
 
 \it('starts payment authorization', function () {
     $created = \paymentHelper()->create();
@@ -90,7 +90,7 @@
     \expect($config['form']['input_types'])->toBeEmpty();
 
     return $created;
-})->only();
+});
 
 \it('starts payment authorization with hpp capabilities', function () {
     $created = \paymentHelper()->create();
@@ -108,7 +108,7 @@
     \expect($config['redirect']['return_uri'])->toBe('https://console.truelayer.com/redirect-page');
     \expect($config['redirect'])->not->toHaveKey('direct_return_uri');
     \expect($config['form']['input_types'])->toContain(FormInputTypes::SELECT, FormInputTypes::TEXT, FormInputTypes::TEXT_WITH_IMAGE);
-})->only();
+});
 
 \it('starts payment authorization with all capabilities', function () {
     $created = \paymentHelper()->create();
@@ -130,7 +130,7 @@
     \expect($config['redirect']['return_uri'])->toBe('https://console.truelayer.com/redirect-page');
     \expect($config['redirect']['direct_return_uri'])->toBe('https://console.truelayer.com/direct-return-page');
     \expect($config['form']['input_types'])->toContain(FormInputTypes::TEXT);
-})->only();
+});
 
 \it('retrieves payment as authorizing - provider selection', function (PaymentCreatedInterface $created) {
     /** @var PaymentAuthorizingInterface $payment */
diff --git a/tests/integration/Mocks/CreatePayment.php b/tests/integration/Mocks/CreatePayment.php
index 1a8c3e54..508a3e98 100644
--- a/tests/integration/Mocks/CreatePayment.php
+++ b/tests/integration/Mocks/CreatePayment.php
@@ -60,7 +60,6 @@ public function newUser(): UserInterface
     public function newUserWithAddress(): UserInterface
     {
         $user = $this->newUser();
-
         $user->address()
             ->addressLine1('The Gilbert')
             ->city('London')

From 8a0e1cea2e933328f7412445cc4150c20fe53e46 Mon Sep 17 00:00:00 2001
From: Stefan Adrian Danaita <me@dsa.io>
Date: Fri, 9 Feb 2024 15:25:20 +0000
Subject: [PATCH 09/11] [PHP-70] User address's State should be optional

---
 src/Entities/Address.php                  |  2 +-
 tests/integration/Mocks/CreatePayment.php | 21 ++++---
 tests/integration/PaymentCreateTest.php   | 77 ++++++++++++++++++++++-
 3 files changed, 90 insertions(+), 10 deletions(-)

diff --git a/src/Entities/Address.php b/src/Entities/Address.php
index 80dfaf61..00afc20f 100644
--- a/src/Entities/Address.php
+++ b/src/Entities/Address.php
@@ -57,7 +57,7 @@ class Address extends Entity implements AddressInterface
         'address_line1' => 'string|required',
         'address_line2' => 'string|nullable',
         'city' => 'string|required',
-        'state' => 'string|required',
+        'state' => 'string|nullable',
         'zip' => 'string|required',
         'country_code' => 'string|required',
     ];
diff --git a/tests/integration/Mocks/CreatePayment.php b/tests/integration/Mocks/CreatePayment.php
index 508a3e98..6e53a2a2 100644
--- a/tests/integration/Mocks/CreatePayment.php
+++ b/tests/integration/Mocks/CreatePayment.php
@@ -57,15 +57,22 @@ public function newUser(): UserInterface
             ->email('alice@truelayer.com');
     }
 
-    public function newUserWithAddress(): UserInterface
+    public function newUserWithAddress($a): UserInterface
     {
         $user = $this->newUser();
-        $user->address()
-            ->addressLine1('The Gilbert')
-            ->city('London')
-            ->state('London')
-            ->zip('EC2A 1PX')
-            ->countryCode('GB');
+        $address = $user->address()
+            ->addressLine1($a['addressLine1'])
+            ->city($a['city'])
+            ->zip($a['zip'])
+            ->countryCode($a['countryCode']);
+
+        if (array_key_exists('addressLine2', $a)) {
+            $address->addressLine2($a['addressLine2']);
+        }
+
+        if (array_key_exists('state', $a)) {
+           $address->state($a['state']);
+        }
 
         return $user;
     }
diff --git a/tests/integration/PaymentCreateTest.php b/tests/integration/PaymentCreateTest.php
index 66de72dc..dbcd07e0 100644
--- a/tests/integration/PaymentCreateTest.php
+++ b/tests/integration/PaymentCreateTest.php
@@ -88,7 +88,80 @@
     $factory = CreatePayment::responses([
         PaymentResponse::created(),
     ]);
-    $factory->payment($factory->newUserWithAddress(), $factory->bankTransferMethod($factory->sortCodeBeneficiary()))->create();
+
+    $address = [
+        'addressLine1' => 'The Gilbert',
+        'addressLine2' => 'City of',
+        'city' => 'London',
+        'state' => 'Greater London',
+        'zip' => 'EC2A 1PX',
+        'countryCode' => 'GB',
+    ];
+    $factory->payment($factory->newUserWithAddress($address), $factory->bankTransferMethod($factory->sortCodeBeneficiary()))->create();
+
+    \expect(\getRequestPayload(1))->toMatchArray([
+        'user' => [
+            'id' => null,
+            'name' => 'Alice',
+            'phone' => '+447837485713',
+            'email' => 'alice@truelayer.com',
+            'address' => [
+                'address_line1' => 'The Gilbert',
+                'address_line2' => 'City of',
+                'city' => 'London',
+                'state' => 'Greater London',
+                'zip' => 'EC2A 1PX',
+                'country_code' => 'GB',
+            ],
+            'date_of_birth' => null,
+        ],
+    ]);
+});
+
+\it('ensures user address state is optional', function () {
+    $factory = CreatePayment::responses([
+        PaymentResponse::created(),
+    ]);
+    $address = [
+        'addressLine1' => 'The Gilbert',
+        'addressLine2' => 'City of',
+        'city' => 'London',
+        'zip' => 'EC2A 1PX',
+        'countryCode' => 'GB',
+    ];
+    $factory->payment($factory->newUserWithAddress($address), $factory->bankTransferMethod($factory->sortCodeBeneficiary()))->create();
+
+    \expect(\getRequestPayload(1))->toMatchArray([
+        'user' => [
+            'id' => null,
+            'name' => 'Alice',
+            'phone' => '+447837485713',
+            'email' => 'alice@truelayer.com',
+            'address' => [
+                'address_line1' => 'The Gilbert',
+                'address_line2' => 'City of',
+                'city' => 'London',
+                'state' => null,
+                'zip' => 'EC2A 1PX',
+                'country_code' => 'GB',
+            ],
+            'date_of_birth' => null,
+        ],
+    ]);
+});
+
+\it('ensures user address addressLine2 is optional', function () {
+    $factory = CreatePayment::responses([
+        PaymentResponse::created(),
+    ]);
+    $address = [
+        'addressLine1' => 'The Gilbert',
+        'city' => 'London',
+        'state' => 'Greater London',
+        'zip' => 'EC2A 1PX',
+        'countryCode' => 'GB',
+    ];
+    $factory->payment($factory->newUserWithAddress($address), $factory->bankTransferMethod($factory->sortCodeBeneficiary()))->create();
 
     \expect(\getRequestPayload(1))->toMatchArray([
         'user' => [
@@ -100,7 +173,7 @@
                 'address_line1' => 'The Gilbert',
                 'address_line2' => null,
                 'city' => 'London',
-                'state' => 'London',
+                'state' => 'Greater London',
                 'zip' => 'EC2A 1PX',
                 'country_code' => 'GB',
             ],

From ada24550b2cb97345291d380fdd723585270896b Mon Sep 17 00:00:00 2001
From: Stefan Adrian Danaita <me@dsa.io>
Date: Fri, 9 Feb 2024 16:05:10 +0000
Subject: [PATCH 10/11] [PHP-70] Update readme for user address and dob support

---
 README.md                                 | 16 +++++++++++++++-
 tests/integration/Mocks/CreatePayment.php |  6 +++---
 2 files changed, 18 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index 4edd4993..984fbc49 100644
--- a/README.md
+++ b/README.md
@@ -207,7 +207,21 @@ $beneficiary = $client->beneficiary()->externalAccount()
 $user = $client->user()
     ->name('Jane Doe')
     ->phone('+44123456789')
-    ->email('jane.doe@truelayer.com');
+    ->email('jane.doe@truelayer.com')
+    ->dateOfBirth('2024-01-01');
+```
+
+You are also able to set the user's address:
+
+```php
+$address = $client->user()
+    ->address()
+    ->addressLine1('The Gilbert')
+    ->addressLine2('City of')
+    ->city('London')
+    ->state('London')
+    ->zip('EC2A 1PX')
+    ->countryCode('GB');
 ```
 
 <a name="creating-a-payment-method"></a>
diff --git a/tests/integration/Mocks/CreatePayment.php b/tests/integration/Mocks/CreatePayment.php
index 6e53a2a2..20e6b7a1 100644
--- a/tests/integration/Mocks/CreatePayment.php
+++ b/tests/integration/Mocks/CreatePayment.php
@@ -66,12 +66,12 @@ public function newUserWithAddress($a): UserInterface
             ->zip($a['zip'])
             ->countryCode($a['countryCode']);
 
-        if (array_key_exists('addressLine2', $a)) {
+        if (\array_key_exists('addressLine2', $a)) {
             $address->addressLine2($a['addressLine2']);
         }
 
-        if (array_key_exists('state', $a)) {
-           $address->state($a['state']);
+        if (\array_key_exists('state', $a)) {
+            $address->state($a['state']);
         }
 
         return $user;

From 8c1d9f2e5ed0ed15ca8412a67f00bfa65526d150 Mon Sep 17 00:00:00 2001
From: Stefan Adrian Danaita <me@dsa.io>
Date: Fri, 9 Feb 2024 16:07:46 +0000
Subject: [PATCH 11/11] [PHP-70] Update changelog with the new version changes

---
 CHANGELOG.md | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index eba4e317..dedeb0ac 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).
 
+## [1.6.0] - 2024-02-09
+
+### Added
+
+- Support for setting the user's address using `$client->user()->address()`
+- Support for setting the user's date of birth using `$client->user()->dateOfBirth()`
+
 ## [1.5.0] - 2024-01-12
 
 ### Added