Skip to content

Commit

Permalink
Merge pull request #3203 from craftcms/feature/copy-addresses-when-re…
Browse files Browse the repository at this point in the history
…gistering-during-checkout

Guest customers registering during checkout have addresses copied to their account
  • Loading branch information
lukeholder committed Jun 27, 2023
2 parents 6fd05ee + d2e817a commit 6ff1c11
Show file tree
Hide file tree
Showing 4 changed files with 352 additions and 2 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## 4.3.0 - Unreleased

- PLACEHOLDER
- Guest customers registering during checkout now have their addresses saved to their account.

## Unreleased

Expand Down
44 changes: 43 additions & 1 deletion src/services/Customers.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -334,6 +337,45 @@ 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__);
Expand Down
1 change: 1 addition & 0 deletions src/translations/en/commerce.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
307 changes: 307 additions & 0 deletions tests/unit/services/CustomersTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/

namespace unit\services;

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\Address;
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
*
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
* @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 = $this->_createOrder('test@newemailaddress.xyz');

self::assertTrue($order->markAsComplete());

$this->_deleteElementIds[] = $order->id;
$this->_deleteElementIds[] = $order->getCustomer()->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 = $this->_createOrder($email);
$originallyCredentialed = $order->getCustomer()->getIsCredentialed();

$order->registerUserOnOrderComplete = $register;

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[] = $order->getCustomer()->id;
}
}

/**
* @return array[]
*/
public function registerOnCheckoutDataProvider(): array
{
return [
'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
*/
protected function _after(): void
{
parent::_after();

// Cleanup data.
foreach ($this->_deleteElementIds as $elementId) {
\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;
}
}

0 comments on commit 6ff1c11

Please sign in to comment.