From 7ba4ff3b3b952cde0d0320b02de28651d1a5bdce Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Fri, 23 Jun 2023 08:56:38 +0100 Subject: [PATCH 1/5] Copy addresses to user when registering during checkout --- CHANGELOG.md | 2 +- src/services/Customers.php | 45 +++++++++++++++++++++++++++++++- src/translations/en/commerce.php | 1 + 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04f13ad331..794b9c9ced 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 4.3.0 - Unreleased -- PLACEHOLDER +- Guest customers registering during checkout now have their addresses saved to their account. ## Unreleased diff --git a/src/services/Customers.php b/src/services/Customers.php index 563f121c51..8dd15e6a4b 100644 --- a/src/services/Customers.php +++ b/src/services/Customers.php @@ -319,8 +319,11 @@ private function _activateUserFromOrder(Order $order): void $user = Craft::$app->getUsers()->ensureUserByEmail($order->email); if (!$user->getIsCredentialed()) { + $billingAddress = $order->getBillingAddress(); + $shippingAddress = $order->getShippingAddress(); + if (!$user->fullName) { - $user->fullName = $order->getBillingAddress()?->fullName ?? $order->getShippingAddress()?->fullName ?? ''; + $user->fullName = $billingAddress?->fullName ?? $shippingAddress?->fullName ?? ''; } $user->username = $order->email; @@ -334,6 +337,46 @@ private function _activateUserFromOrder(Order $order): void if (!$emailSent) { Craft::warning('"registerUserOnOrderComplete" used to create the user, but couldn’t send an activation email. Check your email settings.', __METHOD__); } + + if ($billingAddress || $shippingAddress) { + $newAttributes = ['ownerId' => $user->id]; + + // If there is only one address make sure we don't add duplicates to the user + if ($order->hasMatchingAddresses()) { + $newAttributes['title'] = Craft::t('commerce', 'Address'); + $shippingAddress = null; + } + + // Copy addresses to user + if ($billingAddress) { + $newBillingAddress = Craft::$app->getElements()->duplicateElement($billingAddress, $newAttributes); + + /** + * Because we are cloning from an order address the `CustomerAddressBehavior` hasn't been instantiated + * therefore we are unable to simply set the `isPrimaryBilling` property when specifying the new attributes during duplication. + */ + if (!$newBillingAddress->hasErrors()) { + $this->savePrimaryBillingAddressId($user, $newBillingAddress->id); + + if ($order->hasMatchingAddresses()) { + $this->savePrimaryShippingAddressId($user, $newBillingAddress->id); + } + } + } + + if ($shippingAddress) { + $newShippingAddress = Craft::$app->getElements()->duplicateElement($shippingAddress, $newAttributes); + + /** + * Because we are cloning from an order address the `CustomerAddressBehavior` hasn't been instantiated + * therefore we are unable to simply set the `isPrimaryShipping` property when specifying the new attributes during duplication. + */ + if (!$newShippingAddress->hasErrors()) { + $this->savePrimaryShippingAddressId($user, $newShippingAddress->id); + } + } + } + } else { $errors = $user->getErrors(); Craft::warning('Could not create user on order completion.', __METHOD__); diff --git a/src/translations/en/commerce.php b/src/translations/en/commerce.php index 942ee29265..609da30687 100644 --- a/src/translations/en/commerce.php +++ b/src/translations/en/commerce.php @@ -39,6 +39,7 @@ 'Address Line 2' => 'Address Line 2', 'Address Updated.' => 'Address Updated.', 'Address not found.' => 'Address not found.', + 'Address' => 'Address', 'Adjust price when included rate is disqualified?' => 'Adjust price when included rate is disqualified?', 'Adjustments' => 'Adjustments', 'All Orders' => 'All Orders', From a6ac347000e54932d07e09da66974e64cac25dc2 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Fri, 23 Jun 2023 09:43:05 +0100 Subject: [PATCH 2/5] Fix cs --- src/services/Customers.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/Customers.php b/src/services/Customers.php index 8dd15e6a4b..9f6259a816 100644 --- a/src/services/Customers.php +++ b/src/services/Customers.php @@ -376,7 +376,6 @@ private function _activateUserFromOrder(Order $order): void } } } - } else { $errors = $user->getErrors(); Craft::warning('Could not create user on order completion.', __METHOD__); From 1ff098f76f8bff6c40a12177a43d3ae131526e33 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Fri, 23 Jun 2023 10:09:02 +0100 Subject: [PATCH 3/5] Add customers service test --- tests/unit/services/CustomersTest.php | 114 ++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 tests/unit/services/CustomersTest.php diff --git a/tests/unit/services/CustomersTest.php b/tests/unit/services/CustomersTest.php new file mode 100644 index 0000000000..50586f5cff --- /dev/null +++ b/tests/unit/services/CustomersTest.php @@ -0,0 +1,114 @@ + + * @since 4.3.0 + */ +class CustomersTest extends Unit +{ + /** + * @var UnitTester + */ + protected UnitTester $tester; + + /** + * @var OrdersFixture + */ + protected OrdersFixture $fixtureData; + + /** + * @var array + */ + private array $_deleteElementIds = []; + + /** + * @return array + */ + public function _fixtures(): array + { + return [ + 'customer' => [ + 'class' => CustomerFixture::class, + ], + 'orders' => [ + 'class' => OrdersFixture::class, + ], + ]; + } + + protected function _before(): void + { + parent::_before(); + + $this->fixtureData = $this->tester->grabFixture('orders'); + } + + public function testOrderCompleteHandlerNotCalled(): void + { + Plugin::getInstance()->set('customers', $this->make(Customers::class, [ + 'orderCompleteHandler' => function() { + self::never(); + }, + ])); + + /** @var Order $completedOrder */ + $completedOrder = $this->fixtureData->getElement('completed-new'); + + self::assertTrue($completedOrder->markAsComplete()); + } + + public function testOrderCompleteHandlerCalled(): void + { + Plugin::getInstance()->set('customers', $this->make(Customers::class, [ + 'orderCompleteHandler' => function() { + self::once(); + }, + ])); + + $order = new Order(); + $email = 'test@newemailaddress.xyz'; + $order->setEmail($email); + + /** @var Order $order */ + $completedOrder = $this->fixtureData->getElement('completed-new'); + $lineItem = $completedOrder->getLineItems()[0]; + $qty = 4; + $note = 'My note'; + $lineItem = Plugin::getInstance()->getLineItems()->createLineItem($order, $lineItem->purchasableId, [], $qty, $note); + $order->setLineItems([$lineItem]); + + self::assertTrue($order->markAsComplete()); + + $this->_deleteElementIds[] = $order->id; + } + + /** + * @inheritdoc + */ + protected function _after(): void + { + parent::_after(); + + // Cleanup data. + foreach ($this->_deleteElementIds as $elementId) { + \Craft::$app->getElements()->deleteElementById($elementId, null, null, true); + } + } +} From 48844d12fb4e51cc69148dd5cef607c245663978 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Fri, 23 Jun 2023 11:16:17 +0100 Subject: [PATCH 4/5] add test for registering on checkout --- tests/unit/services/CustomersTest.php | 68 ++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/tests/unit/services/CustomersTest.php b/tests/unit/services/CustomersTest.php index 50586f5cff..d29136b705 100644 --- a/tests/unit/services/CustomersTest.php +++ b/tests/unit/services/CustomersTest.php @@ -9,11 +9,16 @@ use Codeception\Test\Unit; use craft\commerce\elements\Order; +use craft\commerce\errors\OrderStatusException; use craft\commerce\Plugin; use craft\commerce\services\Customers; +use craft\elements\User; +use craft\errors\ElementNotFoundException; use craftcommercetests\fixtures\CustomerFixture; use craftcommercetests\fixtures\OrdersFixture; use UnitTester; +use yii\base\Exception; +use yii\base\InvalidConfigException; /** * CustomersTest @@ -83,8 +88,8 @@ public function testOrderCompleteHandlerCalled(): void ])); $order = new Order(); - $email = 'test@newemailaddress.xyz'; - $order->setEmail($email); + $user = \Craft::$app->getUsers()->ensureUserByEmail('test@newemailaddress.xyz'); + $order->setCustomer($user); /** @var Order $order */ $completedOrder = $this->fixtureData->getElement('completed-new'); @@ -97,6 +102,65 @@ public function testOrderCompleteHandlerCalled(): void self::assertTrue($order->markAsComplete()); $this->_deleteElementIds[] = $order->id; + $this->_deleteElementIds[] = $user->id; + } + + /** + * @param string $email + * @param bool $register + * @param bool $deleteUser an argument to help with cleanup + * @return void + * @throws \Throwable + * @throws OrderStatusException + * @throws ElementNotFoundException + * @throws Exception + * @throws InvalidConfigException + * @dataProvider registerOnCheckoutDataProvider + */ + public function testRegisterOnCheckout(string $email, bool $register, bool $deleteUser): void + { + $order = new Order(); + $user = \Craft::$app->getUsers()->ensureUserByEmail($email); + $originallyCredentialed = $user->getIsCredentialed(); + $order->setCustomer($user); + + $order->registerUserOnOrderComplete = $register; + + $completedOrder = $this->fixtureData->getElement('completed-new'); + $lineItem = $completedOrder->getLineItems()[0]; + $qty = 4; + $note = 'My note'; + $lineItem = Plugin::getInstance()->getLineItems()->createLineItem($order, $lineItem->purchasableId, [], $qty, $note); + $order->setLineItems([$lineItem]); + + self::assertTrue($order->markAsComplete()); + + $foundUser = User::find()->email($email)->status(null)->one(); + self::assertNotNull($foundUser); + + if ($register || $originallyCredentialed) { + self::assertTrue($foundUser->getIsCredentialed()); + } else { + self::assertFalse($foundUser->getIsCredentialed()); + } + + $this->_deleteElementIds[] = $order->id; + if ($deleteUser) { + $this->_deleteElementIds[] = $user->id; + } + } + + /** + * @return array[] + */ + public function registerOnCheckoutDataProvider(): array + { + return [ + 'dont-register-guest' => ['guest@customer.xyz', false, true], + 'register-guest' => ['guest@customer.xyz', true, true], + 'register-credentialed-user' => ['cred.user@crafttest.com', true, false], + 'dont-register-credentialed-user' => ['cred.user@crafttest.com', false, false], + ]; } /** From d2e817a0f75e13ec758c707aa397e12a86479fd0 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Fri, 23 Jun 2023 13:04:27 +0100 Subject: [PATCH 5/5] Add tests --- tests/unit/services/CustomersTest.php | 181 ++++++++++++++++++++++---- 1 file changed, 155 insertions(+), 26 deletions(-) diff --git a/tests/unit/services/CustomersTest.php b/tests/unit/services/CustomersTest.php index d29136b705..f34f44a90c 100644 --- a/tests/unit/services/CustomersTest.php +++ b/tests/unit/services/CustomersTest.php @@ -12,6 +12,7 @@ use craft\commerce\errors\OrderStatusException; use craft\commerce\Plugin; use craft\commerce\services\Customers; +use craft\elements\Address; use craft\elements\User; use craft\errors\ElementNotFoundException; use craftcommercetests\fixtures\CustomerFixture; @@ -87,22 +88,12 @@ public function testOrderCompleteHandlerCalled(): void }, ])); - $order = new Order(); - $user = \Craft::$app->getUsers()->ensureUserByEmail('test@newemailaddress.xyz'); - $order->setCustomer($user); - - /** @var Order $order */ - $completedOrder = $this->fixtureData->getElement('completed-new'); - $lineItem = $completedOrder->getLineItems()[0]; - $qty = 4; - $note = 'My note'; - $lineItem = Plugin::getInstance()->getLineItems()->createLineItem($order, $lineItem->purchasableId, [], $qty, $note); - $order->setLineItems([$lineItem]); + $order = $this->_createOrder('test@newemailaddress.xyz'); self::assertTrue($order->markAsComplete()); $this->_deleteElementIds[] = $order->id; - $this->_deleteElementIds[] = $user->id; + $this->_deleteElementIds[] = $order->getCustomer()->id; } /** @@ -119,20 +110,11 @@ public function testOrderCompleteHandlerCalled(): void */ public function testRegisterOnCheckout(string $email, bool $register, bool $deleteUser): void { - $order = new Order(); - $user = \Craft::$app->getUsers()->ensureUserByEmail($email); - $originallyCredentialed = $user->getIsCredentialed(); - $order->setCustomer($user); + $order = $this->_createOrder($email); + $originallyCredentialed = $order->getCustomer()->getIsCredentialed(); $order->registerUserOnOrderComplete = $register; - $completedOrder = $this->fixtureData->getElement('completed-new'); - $lineItem = $completedOrder->getLineItems()[0]; - $qty = 4; - $note = 'My note'; - $lineItem = Plugin::getInstance()->getLineItems()->createLineItem($order, $lineItem->purchasableId, [], $qty, $note); - $order->setLineItems([$lineItem]); - self::assertTrue($order->markAsComplete()); $foundUser = User::find()->email($email)->status(null)->one(); @@ -146,7 +128,7 @@ public function testRegisterOnCheckout(string $email, bool $register, bool $dele $this->_deleteElementIds[] = $order->id; if ($deleteUser) { - $this->_deleteElementIds[] = $user->id; + $this->_deleteElementIds[] = $order->getCustomer()->id; } } @@ -156,13 +138,144 @@ public function testRegisterOnCheckout(string $email, bool $register, bool $dele public function registerOnCheckoutDataProvider(): array { return [ - 'dont-register-guest' => ['guest@customer.xyz', false, true], - 'register-guest' => ['guest@customer.xyz', true, true], + 'dont-register-guest' => ['guest@crafttest.com', false, true], + 'register-guest' => ['guest@crafttest.com', true, true], 'register-credentialed-user' => ['cred.user@crafttest.com', true, false], 'dont-register-credentialed-user' => ['cred.user@crafttest.com', false, false], ]; } + /** + * @param string $email + * @param bool $deleteUser + * @param Address|null $billingAddres + * @param Address|null $shippingAddress + * @return void + * @throws ElementNotFoundException + * @throws Exception + * @throws OrderStatusException + * @throws \Throwable + * @dataProvider registerOnCheckoutCopyAddressesDataProvider + */ + public function testRegisterOnCheckoutCopyAddresses(string $email, ?array $billingAddress, ?array $shippingAddress, int $addressCount): void + { + $isOnlyOneAddress = empty($billingAddress) || empty($shippingAddress); + $order = $this->_createOrder($email); + $order->registerUserOnOrderComplete = true; + \Craft::$app->getElements()->saveElement($order, false); + + if (!empty($billingAddress)) { + $order->setBillingAddress($billingAddress); + } + + if (!empty($shippingAddress)) { + $order->setShippingAddress($shippingAddress); + } + + self::assertTrue($order->markAsComplete()); + + $userAddresses = Address::find()->ownerId($order->getCustomer()->id)->all(); + self::assertCount($addressCount, $userAddresses); + + $primaryCount = 0; + foreach ($userAddresses as $userAddress) { + if ($addressCount === 1) { + $addressTitle = \Craft::t('commerce', 'Address'); + if ($isOnlyOneAddress) { + $addressTitle = !empty($billingAddress) ? \Craft::t('commerce', 'Billing Address') : \Craft::t('commerce', 'Shipping Address'); + } + self::assertEquals($addressTitle, $userAddress->title); + + $address = $billingAddress ?? $shippingAddress; + self::assertEquals($address['fullName'], $userAddress->fullName); + self::assertEquals($address['addressLine1'], $userAddress->addressLine1); + self::assertEquals($address['locality'], $userAddress->locality); + self::assertEquals($address['administrativeArea'], $userAddress->administrativeArea); + self::assertEquals($address['postalCode'], $userAddress->postalCode); + self::assertEquals($address['countryCode'], $userAddress->countryCode); + } + + if ($userAddress->getIsPrimaryBilling()) { + if ($addressCount === 2) { + self::assertEquals(\Craft::t('commerce', 'Billing Address'), $userAddress->title); + self::assertEquals($billingAddress['fullName'], $userAddress->fullName); + self::assertEquals($billingAddress['addressLine1'], $userAddress->addressLine1); + self::assertEquals($billingAddress['locality'], $userAddress->locality); + self::assertEquals($billingAddress['administrativeArea'], $userAddress->administrativeArea); + self::assertEquals($billingAddress['postalCode'], $userAddress->postalCode); + self::assertEquals($billingAddress['countryCode'], $userAddress->countryCode); + } + + $primaryCount++; + } + if ($userAddress->getIsPrimaryShipping()) { + if ($addressCount === 2) { + self::assertEquals(\Craft::t('commerce', 'Shipping Address'), $userAddress->title); + self::assertEquals($shippingAddress['fullName'], $userAddress->fullName); + self::assertEquals($shippingAddress['addressLine1'], $userAddress->addressLine1); + self::assertEquals($shippingAddress['locality'], $userAddress->locality); + self::assertEquals($shippingAddress['administrativeArea'], $userAddress->administrativeArea); + self::assertEquals($shippingAddress['postalCode'], $userAddress->postalCode); + self::assertEquals($shippingAddress['countryCode'], $userAddress->countryCode); + } + + $primaryCount++; + } + } + + self::assertEquals($isOnlyOneAddress ? 1 : 2, $primaryCount); + + $this->_deleteElementIds[] = $order->id; + $this->_deleteElementIds[] = $order->getCustomer()->id; + } + + public function registerOnCheckoutCopyAddressesDataProvider(): array + { + $billingAddress = [ + 'fullName' => 'Guest Billing', + 'addressLine1' => '1 Main Billing Street', + 'locality' => 'Billingsville', + 'administrativeArea' => 'OR', + 'postalCode' => '12345', + 'countryCode' => 'US', + ]; + $shippingAddress = [ + 'fullName' => 'Guest Shipping', + 'addressLine1' => '1 Main Shipping Street', + 'locality' => 'Shippingsville', + 'administrativeArea' => 'AL', + 'postalCode' => '98765', + 'countryCode' => 'US', + ]; + + return [ + 'guest-two-addresses' => [ + 'guest.person@crafttest.com', + $billingAddress, + $shippingAddress, + 2, + ], + 'guest-matching-addresses' => [ + 'guest.person@crafttest.com', + $billingAddress, + $billingAddress, + 1, + ], + 'guest-one-billing-address' => [ + 'guest.person@crafttest.com', + $billingAddress, + null, + 1, + ], + 'guest-one-shipping-address' => [ + 'guest.person@crafttest.com', + null, + $shippingAddress, + 1, + ], + ]; + } + /** * @inheritdoc */ @@ -175,4 +288,20 @@ protected function _after(): void \Craft::$app->getElements()->deleteElementById($elementId, null, null, true); } } + + private function _createOrder(string $email): Order + { + $order = new Order(); + $user = \Craft::$app->getUsers()->ensureUserByEmail($email); + $order->setCustomer($user); + + $completedOrder = $this->fixtureData->getElement('completed-new'); + $lineItem = $completedOrder->getLineItems()[0]; + $qty = 4; + $note = 'My note'; + $lineItem = Plugin::getInstance()->getLineItems()->createLineItem($order, $lineItem->purchasableId, [], $qty, $note); + $order->setLineItems([$lineItem]); + + return $order; + } }