diff --git a/example-templates/dist/shop/_private/layouts/includes/nav-checkout.twig b/example-templates/dist/shop/_private/layouts/includes/nav-checkout.twig index 032fe3a4ef..4c0b17c8d7 100644 --- a/example-templates/dist/shop/_private/layouts/includes/nav-checkout.twig +++ b/example-templates/dist/shop/_private/layouts/includes/nav-checkout.twig @@ -23,6 +23,10 @@ Outputs the checkout progress navigation using the request path and included `ch label: 'Payment Method', url: 'shop/checkout/payment-method' }, + { + label: 'Options', + url: 'shop/checkout/options' + }, { label: 'Payment', url: 'shop/checkout/payment' diff --git a/example-templates/dist/shop/_private/layouts/index.twig b/example-templates/dist/shop/_private/layouts/index.twig index 4200dad079..7cf2b6f129 100644 --- a/example-templates/dist/shop/_private/layouts/index.twig +++ b/example-templates/dist/shop/_private/layouts/index.twig @@ -28,6 +28,17 @@ Common, top-level layout template. {% set flashNotice = craft.app.session.getFlash('notice') %} {% set flashError = craft.app.session.getFlash('error') %} +{% macro docs(text, link) %} + + {{ tag('a', { + text: 'ℹ︎ ' ~ text, + href: link, + class: 'text-gray-400 hover:text-gray-600 hover:underline', + target: '_blank', + }) }} + +{% endmacro %} +
diff --git a/example-templates/dist/shop/checkout/options.twig b/example-templates/dist/shop/checkout/options.twig new file mode 100644 index 0000000000..df0381ee31 --- /dev/null +++ b/example-templates/dist/shop/checkout/options.twig @@ -0,0 +1,108 @@ +{% extends 'shop/_private/layouts' %} + +{# @var cart \craft\commerce\elements\Order #} + +{% if cart is not defined %} + {% set cart = craft.commerce.carts.cart %} +{% endif %} + +{# + We can skip the "options" step if the customer is a logged in user and + the addresses have come from the address book +#} +{% if currentUser and cart.sourceBillingAddressId and cart.sourceShippingAddressId %} + {% redirect 'shop/checkout/payment' %} +{% endif %} + +{% if not cart.getCustomer() %} + {% redirect 'shop/checkout/email' %} +{% endif %} + +{% if not cart.gateway %} + {% redirect 'shop/checkout/payment-method' %} +{% endif %} + +{% block main %} +
+
+ +

+ {{- 'Options'|t -}} +

+ +
+ {{ csrfInput() }} + {{ actionInput('commerce/cart/update-cart') }} + {{ redirectInput(siteUrl('shop/checkout/payment')) }} + {{ successMessageInput('Options saved.') }} + + {% set user = cart.email ? craft.users.email(cart.email).one() : null %} + {% if not user or not user.getIsCredentialed() %} +
+ +
+ {{ _self.docs('Registering a user on order complete.', 'https://craftcms.com/docs/commerce/4.x/customers.html#registration-at-checkout') }} +
+
+ {% endif %} + + {% set saveAddressCheckboxesShown = false %} + {% if currentUser and cart.billingAddressId and not cart.sourceBillingAddressId %} + {% set saveAddressCheckboxesShown = true %} +
+ +
+ {% endif %} + + {% if currentUser and cart.shippingAddressId and not cart.sourceShippingAddressId %} +
+ {% set saveAddressCheckboxesShown = true %} + +
+ {% endif %} + + {% if saveAddressCheckboxesShown %} +
+ {{ _self.docs('Saving addresses on order complete.', '#') }} +
+ {% endif %} + +
+ {{ tag('button', { + type: 'submit', + name: 'submit', + class: 'cursor-pointer rounded px-4 py-2 inline-block bg-blue-500 hover:bg-blue-600 text-white hover:text-white', + text: 'Next'|t + }) }} +
+
+
+ +
+ {{ include('shop/checkout/_includes/order-summary', { + showShippingAddress: true, + showShippingMethod: true + }) }} +
+
+{% endblock %} diff --git a/example-templates/dist/shop/checkout/payment-method.twig b/example-templates/dist/shop/checkout/payment-method.twig index aea54601a8..5a55567cc6 100644 --- a/example-templates/dist/shop/checkout/payment-method.twig +++ b/example-templates/dist/shop/checkout/payment-method.twig @@ -20,7 +20,7 @@
{{ csrfInput() }} {{ actionInput('commerce/cart/update-cart') }} - {{ redirectInput(siteUrl('shop/checkout/payment')) }} + {{ redirectInput(siteUrl('shop/checkout/options')) }} {{ successMessageInput('Payment options selected.') }}
diff --git a/example-templates/dist/shop/checkout/payment.twig b/example-templates/dist/shop/checkout/payment.twig index b029f1637e..629bdb19cd 100644 --- a/example-templates/dist/shop/checkout/payment.twig +++ b/example-templates/dist/shop/checkout/payment.twig @@ -115,19 +115,6 @@ {{ cart.gateway.getPaymentConfirmationFormHtml({})|raw }} {% endif %} - {% set user = cart.email ? craft.users.email(cart.email).one() : null %} - {% if not user or not user.getIsCredentialed() %} -
- -
- {% endif %} - {{ include('shop/checkout/_includes/partial-payment') }} {% if cart.paymentSourceId or cart.gateway.showPaymentFormSubmitButton() %} diff --git a/example-templates/src/shop/_private/layouts/includes/nav-checkout.twig b/example-templates/src/shop/_private/layouts/includes/nav-checkout.twig index 04e3827fe1..85b9f9affa 100755 --- a/example-templates/src/shop/_private/layouts/includes/nav-checkout.twig +++ b/example-templates/src/shop/_private/layouts/includes/nav-checkout.twig @@ -23,6 +23,10 @@ Outputs the checkout progress navigation using the request path and included `ch label: 'Payment Method', url: '[[folderName]]/checkout/payment-method' }, + { + label: 'Options', + url: '[[folderName]]/checkout/options' + }, { label: 'Payment', url: '[[folderName]]/checkout/payment' diff --git a/example-templates/src/shop/_private/layouts/index.twig b/example-templates/src/shop/_private/layouts/index.twig index 8a002b0df7..3f2c9cfd4e 100755 --- a/example-templates/src/shop/_private/layouts/index.twig +++ b/example-templates/src/shop/_private/layouts/index.twig @@ -28,6 +28,17 @@ Common, top-level layout template. {% set flashNotice = craft.app.session.getFlash('notice') %} {% set flashError = craft.app.session.getFlash('error') %} +{% macro docs(text, link) %} + + {{ tag('a', { + text: 'ℹ︎ ' ~ text, + href: link, + class: '[[classes.docs]]', + target: '_blank', + }) }} + +{% endmacro %} +
diff --git a/example-templates/src/shop/checkout/options.twig b/example-templates/src/shop/checkout/options.twig new file mode 100755 index 0000000000..8e4492960e --- /dev/null +++ b/example-templates/src/shop/checkout/options.twig @@ -0,0 +1,108 @@ +{% extends '[[folderName]]/_private/layouts' %} + +{# @var cart \craft\commerce\elements\Order #} + +{% if cart is not defined %} + {% set cart = craft.commerce.carts.cart %} +{% endif %} + +{# + We can skip the "options" step if the customer is a logged in user and + the addresses have come from the address book +#} +{% if currentUser and cart.sourceBillingAddressId and cart.sourceShippingAddressId %} + {% redirect '[[folderName]]/checkout/payment' %} +{% endif %} + +{% if not cart.getCustomer() %} + {% redirect '[[folderName]]/checkout/email' %} +{% endif %} + +{% if not cart.gateway %} + {% redirect '[[folderName]]/checkout/payment-method' %} +{% endif %} + +{% block main %} +
+
+ +

+ {{- 'Options'|t -}} +

+ + + {{ csrfInput() }} + {{ actionInput('commerce/cart/update-cart') }} + {{ redirectInput(siteUrl('shop/checkout/payment')) }} + {{ successMessageInput('Options saved.') }} + + {% set user = cart.email ? craft.users.email(cart.email).one() : null %} + {% if not user or not user.getIsCredentialed() %} +
+ +
+ {{ _self.docs('Registering a user on order complete.', 'https://craftcms.com/docs/commerce/4.x/customers.html#registration-at-checkout') }} +
+
+ {% endif %} + + {% set saveAddressCheckboxesShown = false %} + {% if currentUser and cart.billingAddressId and not cart.sourceBillingAddressId %} + {% set saveAddressCheckboxesShown = true %} +
+ +
+ {% endif %} + + {% if currentUser and cart.shippingAddressId and not cart.sourceShippingAddressId %} +
+ {% set saveAddressCheckboxesShown = true %} + +
+ {% endif %} + + {% if saveAddressCheckboxesShown %} +
+ {{ _self.docs('Saving addresses on order complete.', '#') }} +
+ {% endif %} + +
+ {{ tag('button', { + type: 'submit', + name: 'submit', + class: '[[classes.btn.base]] [[classes.btn.mainColor]]', + text: 'Next'|t + }) }} +
+ +
+ +
+ {{ include('[[folderName]]/checkout/_includes/order-summary', { + showShippingAddress: true, + showShippingMethod: true + }) }} +
+
+{% endblock %} diff --git a/example-templates/src/shop/checkout/payment-method.twig b/example-templates/src/shop/checkout/payment-method.twig index d6b0e064a2..1831ca9e22 100644 --- a/example-templates/src/shop/checkout/payment-method.twig +++ b/example-templates/src/shop/checkout/payment-method.twig @@ -20,7 +20,7 @@
{{ csrfInput() }} {{ actionInput('commerce/cart/update-cart') }} - {{ redirectInput(siteUrl('[[folderName]]/checkout/payment')) }} + {{ redirectInput(siteUrl('[[folderName]]/checkout/options')) }} {{ successMessageInput('Payment options selected.') }}
diff --git a/example-templates/src/shop/checkout/payment.twig b/example-templates/src/shop/checkout/payment.twig index 973991a1ee..f0247626cb 100755 --- a/example-templates/src/shop/checkout/payment.twig +++ b/example-templates/src/shop/checkout/payment.twig @@ -115,19 +115,6 @@ {{ cart.gateway.getPaymentConfirmationFormHtml({})|raw }} {% endif %} - {% set user = cart.email ? craft.users.email(cart.email).one() : null %} - {% if not user or not user.getIsCredentialed() %} -
- -
- {% endif %} - {{ include('[[folderName]]/checkout/_includes/partial-payment') }} {% if cart.paymentSourceId or cart.gateway.showPaymentFormSubmitButton() %} diff --git a/src/Plugin.php b/src/Plugin.php index b2e0ee0e92..59c2ce8e9c 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -209,7 +209,7 @@ public static function editions(): array /** * @inheritDoc */ - public string $schemaVersion = '4.2.4'; + public string $schemaVersion = '4.2.5'; /** * @inheritdoc diff --git a/src/console/controllers/ExampleTemplatesController.php b/src/console/controllers/ExampleTemplatesController.php index 0423e40691..b72242b7b3 100644 --- a/src/console/controllers/ExampleTemplatesController.php +++ b/src/console/controllers/ExampleTemplatesController.php @@ -239,6 +239,7 @@ private function _addCssClassesToReplacementData(): void '[[classes.text.color]]' => "text-$mainColor-500", '[[classes.text.dangerColor]]' => "text-$dangerColor-500", '[[classes.a]]' => "text-$mainColor-500 hover:text-$mainColor-600", + '[[classes.docs]]' => "text-gray-400 hover:text-gray-600 hover:underline", '[[classes.input]]' => "border border-gray-300 hover:border-gray-500 px-4 py-2 leading-tight rounded", '[[classes.box.base]]' => "bg-gray-100 border-$mainColor-300 border-b-2 p-6", '[[classes.box.selection]]' => "border-$mainColor-300 border-b-2 px-6 py-4 rounded-md shadow-md hover:shadow-lg", diff --git a/src/controllers/CartController.php b/src/controllers/CartController.php index aad1586e95..61af99d9bd 100644 --- a/src/controllers/CartController.php +++ b/src/controllers/CartController.php @@ -229,12 +229,25 @@ public function actionUpdateCart(): ?Response } // Set if the customer should be registered on order completion - if ($this->request->getBodyParam('registerUserOnOrderComplete')) { - $this->_cart->registerUserOnOrderComplete = true; + $registerUserOnOrderComplete = $this->request->getBodyParam('registerUserOnOrderComplete'); + if ($registerUserOnOrderComplete !== null) { + $this->_cart->registerUserOnOrderComplete = (bool)$registerUserOnOrderComplete; } - if ($this->request->getBodyParam('registerUserOnOrderComplete') === 'false') { - $this->_cart->registerUserOnOrderComplete = false; + $saveBillingAddressOnOrderComplete = $this->request->getBodyParam('saveBillingAddressOnOrderComplete'); + if ($saveBillingAddressOnOrderComplete !== null) { + $this->_cart->saveBillingAddressOnOrderComplete = (bool)$saveBillingAddressOnOrderComplete; + } + + $saveShippingAddressOnOrderComplete = $this->request->getBodyParam('saveShippingAddressOnOrderComplete'); + if ($saveShippingAddressOnOrderComplete !== null) { + $this->_cart->saveShippingAddressOnOrderComplete = (bool)$saveShippingAddressOnOrderComplete; + } + + $saveAddressesOnOrderComplete = $this->request->getBodyParam('saveAddressesOnOrderComplete'); + if ($saveAddressesOnOrderComplete !== null) { + $this->_cart->saveBillingAddressOnOrderComplete = (bool)$saveAddressesOnOrderComplete; + $this->_cart->saveShippingAddressOnOrderComplete = (bool)$saveAddressesOnOrderComplete; } // Set payment currency on cart diff --git a/src/controllers/PaymentsController.php b/src/controllers/PaymentsController.php index 9190a163bd..13286648a6 100644 --- a/src/controllers/PaymentsController.php +++ b/src/controllers/PaymentsController.php @@ -141,12 +141,25 @@ public function actionPay(): ?Response } // Set if the customer should be registered on order completion - if ($this->request->getBodyParam('registerUserOnOrderComplete')) { - $order->registerUserOnOrderComplete = true; + $registerUserOnOrderComplete = $this->request->getBodyParam('registerUserOnOrderComplete'); + if ($registerUserOnOrderComplete !== null) { + $order->registerUserOnOrderComplete = (bool)$registerUserOnOrderComplete; } - if ($this->request->getBodyParam('registerUserOnOrderComplete') === 'false') { - $order->registerUserOnOrderComplete = false; + $saveBillingAddressOnOrderComplete = $this->request->getBodyParam('saveBillingAddressOnOrderComplete'); + if ($saveBillingAddressOnOrderComplete !== null) { + $order->saveBillingAddressOnOrderComplete = (bool)$saveBillingAddressOnOrderComplete; + } + + $saveShippingAddressOnOrderComplete = $this->request->getBodyParam('saveShippingAddressOnOrderComplete'); + if ($saveShippingAddressOnOrderComplete !== null) { + $order->saveShippingAddressOnOrderComplete = (bool)$saveShippingAddressOnOrderComplete; + } + + $saveAddressesOnOrderComplete = $this->request->getBodyParam('saveAddressesOnOrderComplete'); + if ($saveAddressesOnOrderComplete !== null) { + $order->saveBillingAddressOnOrderComplete = (bool)$saveAddressesOnOrderComplete; + $order->saveShippingAddressOnOrderComplete = (bool)$saveAddressesOnOrderComplete; } // These are used to compare if the order changed during its final diff --git a/src/elements/Order.php b/src/elements/Order.php index 56061cdf25..a38c40a3b8 100644 --- a/src/elements/Order.php +++ b/src/elements/Order.php @@ -858,6 +858,36 @@ class Order extends Element */ public bool $registerUserOnOrderComplete = false; + /** + * Whether the billing address on the order should be saved to the customer's + * address book when the order is complete. + * + * @var bool Save the order's billing address to the customer's address book + * --- + * ```php + * echo $order->saveBillingAddressOnOrderComplete; + * ``` + * ```twig + * {{ order.saveBillingAddressOnOrderComplete }} + * ``` + */ + public bool $saveBillingAddressOnOrderComplete = false; + + /** + * Whether the shipping address on the order should be saved to the customer's + * address book when the order is complete. + * + * @var bool Save the order's shipping address to the customer's address book + * --- + * ```php + * echo $order->saveShippingAddressOnOrderComplete; + * ``` + * ```twig + * {{ order.saveShippingAddressOnOrderComplete }} + * ``` + */ + public bool $saveShippingAddressOnOrderComplete = false; + /** * The current payment source that should be used to make payments on the * order. If this is set, the `gatewayId` will also be set to the related @@ -1483,7 +1513,7 @@ protected function defineRules(): array [['paymentSourceId'], 'validatePaymentSourceId'], [['email'], 'email'], - [['number', 'user', 'orderCompletedEmail'], 'safe'], + [['number', 'user', 'orderCompletedEmail', 'saveBillingAddressOnOrderComplete', 'saveShippingAddressOnOrderComplete'], 'safe'], ]); } @@ -2080,6 +2110,8 @@ public function afterSave(bool $isNew): void $orderRecord->paymentCurrency = $this->paymentCurrency; $orderRecord->customerId = $this->getCustomerId(); $orderRecord->registerUserOnOrderComplete = $this->registerUserOnOrderComplete; + $orderRecord->saveBillingAddressOnOrderComplete = $this->saveBillingAddressOnOrderComplete; + $orderRecord->saveShippingAddressOnOrderComplete = $this->saveShippingAddressOnOrderComplete; $orderRecord->returnUrl = $this->returnUrl; $orderRecord->cancelUrl = $this->cancelUrl; $orderRecord->message = $this->message; diff --git a/src/elements/db/OrderQuery.php b/src/elements/db/OrderQuery.php index 17ee7689ec..49eacced74 100644 --- a/src/elements/db/OrderQuery.php +++ b/src/elements/db/OrderQuery.php @@ -1485,6 +1485,8 @@ protected function beforePrepare(): bool 'commerce_orders.customerId', 'commerce_orders.dateUpdated', 'commerce_orders.registerUserOnOrderComplete', + 'commerce_orders.saveBillingAddressOnOrderComplete', + 'commerce_orders.saveShippingAddressOnOrderComplete', 'commerce_orders.recalculationMode', 'commerce_orders.origin', 'commerce_orders.dateAuthorized', diff --git a/src/migrations/Install.php b/src/migrations/Install.php index db548dbf69..3edc055cc6 100644 --- a/src/migrations/Install.php +++ b/src/migrations/Install.php @@ -373,6 +373,8 @@ public function createTables(): void 'origin' => $this->enum('origin', ['web', 'cp', 'remote'])->notNull()->defaultValue('web'), 'message' => $this->text(), 'registerUserOnOrderComplete' => $this->boolean()->notNull()->defaultValue(false), + 'saveBillingAddressOnOrderComplete' => $this->boolean()->notNull()->defaultValue(false), + 'saveShippingAddressOnOrderComplete' => $this->boolean()->notNull()->defaultValue(false), 'recalculationMode' => $this->enum('recalculationMode', ['all', 'none', 'adjustmentsOnly'])->notNull()->defaultValue('all'), 'returnUrl' => $this->text(), 'cancelUrl' => $this->text(), diff --git a/src/migrations/m230705_124845_add_save_address_columns.php b/src/migrations/m230705_124845_add_save_address_columns.php new file mode 100644 index 0000000000..bcf32d2e04 --- /dev/null +++ b/src/migrations/m230705_124845_add_save_address_columns.php @@ -0,0 +1,32 @@ +addColumn(Table::ORDERS, 'saveBillingAddressOnOrderComplete', $this->boolean()->notNull()->defaultValue(false)); + $this->addColumn(Table::ORDERS, 'saveShippingAddressOnOrderComplete', $this->boolean()->notNull()->defaultValue(false)); + + return true; + } + + /** + * @inheritdoc + */ + public function safeDown(): bool + { + echo "m230705_124845_add_save_address_columns cannot be reverted.\n"; + return false; + } +} diff --git a/src/records/Order.php b/src/records/Order.php index 3fc2570ab0..954143636a 100644 --- a/src/records/Order.php +++ b/src/records/Order.php @@ -49,6 +49,8 @@ * @property string $paymentCurrency * @property int $paymentSourceId * @property bool $registerUserOnOrderComplete + * @property bool $saveBillingAddressOnOrderComplete + * @property bool $saveShippingAddressOnOrderComplete * @property string $returnUrl * @property string $reference * @property string $recalculationMode diff --git a/src/services/Customers.php b/src/services/Customers.php index 9f6259a816..4459d805fa 100644 --- a/src/services/Customers.php +++ b/src/services/Customers.php @@ -20,6 +20,8 @@ use craft\db\Query; use craft\elements\User; use craft\errors\ElementNotFoundException; +use craft\errors\InvalidElementException; +use craft\errors\UnsupportedSiteException; use craft\helpers\ArrayHelper; use craft\helpers\Db; use yii\db\Expression; @@ -167,6 +169,10 @@ public function orderCompleteHandler(Order $order): void if ($order->registerUserOnOrderComplete) { $this->_activateUserFromOrder($order); } + + if ($order->saveBillingAddressOnOrderComplete || $order->saveShippingAddressOnOrderComplete) { + $this->_saveAddressesFromOrder($order); + } } /** @@ -307,6 +313,68 @@ public function transferCustomerData(User $fromCustomer, User $toCustomer): bool return true; } + /** + * @param Order $order + * @return void + * @throws \Throwable + * @throws InvalidElementException + * @throws UnsupportedSiteException + */ + private function _saveAddressesFromOrder(Order $order): void + { + // Only for completed orders + if ($order->isCompleted === false) { + return; + } + + // Check for a credentialed user + if ($order->getCustomer() === null || !$order->getCustomer()->getIsCredentialed()) { + return; + } + + $saveBillingAddress = $order->saveBillingAddressOnOrderComplete && $order->sourceBillingAddressId === null && $order->billingAddressId; + $saveShippingAddress = $order->saveShippingAddressOnOrderComplete && $order->sourceShippingAddressId === null && $order->shippingAddressId; + $newSourceBillingAddressId = null; + $newSourceShippingAddressId = null; + + if ($saveBillingAddress && $saveShippingAddress && $order->hasMatchingAddresses()) { + // Only save one address if they are matching + $newAddress = Craft::$app->getElements()->duplicateElement($order->getBillingAddress(), ['ownerId' => $order->getCustomer()->id]); + $newSourceBillingAddressId = $newAddress->id; + $newSourceShippingAddressId = $newAddress->id; + } else { + if ($saveBillingAddress) { + $newBillingAddress = Craft::$app->getElements()->duplicateElement($order->getBillingAddress(), ['ownerId' => $order->getCustomer()->id]); + $newSourceBillingAddressId = $newBillingAddress->id; + } + + if ($saveShippingAddress) { + $newShippingAddress = Craft::$app->getElements()->duplicateElement($order->getShippingAddress(), ['ownerId' => $order->getCustomer()->id]); + $newSourceShippingAddressId = $newShippingAddress->id; + } + } + + if ($newSourceBillingAddressId) { + $order->sourceBillingAddressId = $newSourceBillingAddressId; + } + + if ($newSourceShippingAddressId) { + $order->sourceShippingAddressId = $newSourceShippingAddressId; + } + + // Manually update the order DB record to avoid looped element saves + if ($newSourceBillingAddressId || $newSourceShippingAddressId) { + \craft\commerce\records\Order::updateAll([ + 'sourceBillingAddressId' => $order->sourceBillingAddressId, + 'sourceShippingAddressId' => $order->sourceShippingAddressId, + ], + [ + 'id' => $order->id, + ] + ); + } + } + /** * Makes sure the user has an email address and sets them to pending and sends the activation email */ diff --git a/tests/unit/controllers/CartTest.php b/tests/unit/controllers/CartTest.php index a64214df51..e49a61c543 100644 --- a/tests/unit/controllers/CartTest.php +++ b/tests/unit/controllers/CartTest.php @@ -406,4 +406,79 @@ public function autoSetNewCartAddressesDataProvider(): array ], ]; } + + /** + * @param bool|null $saveBillingAddress + * @param bool|null $saveShippingAddress + * @param bool|null $saveBoth + * @return void + * @throws ElementNotFoundException + * @throws Exception + * @throws InvalidConfigException + * @throws InvalidPluginException + * @throws InvalidRouteException + * @throws Throwable + * @since 4.3.0 + * @dataProvider setSaveAddressesDataProvider + */ + public function testSetSaveAddresses(?bool $saveBillingAddress, ?bool $saveShippingAddress, ?bool $saveBoth): void + { + Craft::$app->getPlugins()->switchEdition('commerce', Plugin::EDITION_PRO); + $this->request->headers->set('X-Http-Method-Override', 'POST'); + + $bodyParams = []; + if ($saveBoth) { + $bodyParams['saveAddressesOnOrderComplete'] = true; + } else { + $bodyParams['saveBillingAddressOnOrderComplete'] = $saveBillingAddress; + $bodyParams['saveShippingAddressOnOrderComplete'] = $saveShippingAddress; + } + + $this->request->setBodyParams($bodyParams); + $this->cartController->runAction('update-cart'); + + $cart = Plugin::getInstance()->getCarts()->getCart(); + + if ($saveBoth) { + self::assertTrue($cart->saveBillingAddressOnOrderComplete); + self::assertTrue($cart->saveShippingAddressOnOrderComplete); + } else { + self::assertEquals($saveBillingAddress, $cart->saveBillingAddressOnOrderComplete); + self::assertEquals($saveShippingAddress, $cart->saveShippingAddressOnOrderComplete); + } + + Plugin::getInstance()->getCarts()->forgetCart(); + + Craft::$app->getElements()->deleteElement($cart, true); + } + + /** + * @return array[] + * @since 4.3.0 + */ + public function setSaveAddressesDataProvider(): array + { + return [ + 'save-billing' => [ + true, // save billing + false, // save shipping + false, // save both + ], + 'save-shipping' => [ + false, // save billing + true, // save shipping + false, // save both + ], + 'save-both' => [ + false, // save billing + false, // save shipping + true, // save both + ], + 'save-both-individually' => [ + true, // save billing + true, // save shipping + false, // save both + ], + ]; + } } diff --git a/tests/unit/services/CustomersTest.php b/tests/unit/services/CustomersTest.php index f34f44a90c..d938d69072 100644 --- a/tests/unit/services/CustomersTest.php +++ b/tests/unit/services/CustomersTest.php @@ -229,6 +229,9 @@ public function testRegisterOnCheckoutCopyAddresses(string $email, ?array $billi $this->_deleteElementIds[] = $order->getCustomer()->id; } + /** + * @return array[] + */ public function registerOnCheckoutCopyAddressesDataProvider(): array { $billingAddress = [ @@ -276,6 +279,201 @@ public function registerOnCheckoutCopyAddressesDataProvider(): array ]; } + /** + * @param bool|null $saveBilling + * @param array|null $billingAddress + * @param bool|null $saveShipping + * @param array|null $shippingAddress + * @param bool $setSourceBilling + * @param bool $setSourceShipping + * @return void + * @throws ElementNotFoundException + * @throws Exception + * @throws OrderStatusException + * @throws \Throwable + * @dataProvider saveAddressesOnOrderCompleteDataProvider + * @since 4.3.0 + */ + public function testSaveAddressesOnOrderComplete(?bool $saveBilling, ?array $billingAddress, ?bool $saveShipping, ?array $shippingAddress, int $newAddressCount, bool $setSourceBilling, bool $setSourceShipping): void + { + $order = $this->_createOrder('cred.user@crafttest.com'); + $customer = $order->getCustomer(); + $sourceAddress = [ + 'fullName' => 'Source Address', + 'addressLine1' => '1 Source Road', + 'locality' => 'Sourcington', + 'administrativeArea' => 'OR', + 'postalCode' => '991199', + 'countryCode' => 'US', + 'ownerId' => $customer->id, + ]; + + if ($setSourceBilling || $setSourceShipping) { + $sourceAddressModel = \Craft::createObject([ + 'class' => Address::class, + 'attributes' => $sourceAddress, + ]); + \Craft::$app->getElements()->saveElement($sourceAddressModel, false, false ,false); + $this->_deleteElementIds[] = $sourceAddressModel->id; + + if ($setSourceBilling) { + $order->sourceBillingAddressId = $sourceAddressModel->id; + } + + if ($setSourceShipping) { + $order->sourceShippingAddressId = $sourceAddressModel->id; + } + } + $originalAddressIds = collect($customer->getAddresses())->pluck('id')->all(); + + $order->saveBillingAddressOnOrderComplete = $saveBilling; + $order->saveShippingAddressOnOrderComplete = $saveShipping; + + $order->setBillingAddress($billingAddress); + $order->setShippingAddress($shippingAddress); + + \Craft::$app->getElements()->saveElement($order, false, false, false); + + self::assertTrue($order->markAsComplete()); + + // @TODO change this to `$customer->getAddresses()` when `getAddresses()` memoization is fixed + $idWhere = array_merge(['not'], $originalAddressIds); + $addresses = Address::find()->ownerId($customer->id)->id($idWhere)->all(); + self::assertCount($newAddressCount, $addresses); + $addressNames = collect($addresses)->pluck('fullName')->all(); + $addressLine1s = collect($addresses)->pluck('addressLine1')->all(); + + if ($billingAddress && $saveBilling && !$setSourceBilling) { + self::assertContains($billingAddress['fullName'], $addressNames); + self::assertContains($billingAddress['addressLine1'], $addressLine1s); + } + + if ($shippingAddress && $saveShipping && !$setSourceShipping) { + self::assertContains($shippingAddress['fullName'], $addressNames); + self::assertContains($shippingAddress['addressLine1'], $addressLine1s); + } + + $this->_deleteElementIds[] = $order->id; +// \Craft::$app->getElements()->deleteElementById($order->id, null, null, true); + // No need to delete the customer as it comes from the fixtures + } + + /** + * @return array + */ + public function saveAddressesOnOrderCompleteDataProvider(): array + { + $billingAddress = [ + 'fullName' => 'Billing Name', + 'addressLine1' => '1 Main Billing Street', + 'locality' => 'Billingsville', + 'administrativeArea' => 'OR', + 'postalCode' => '12345', + 'countryCode' => 'US', + ]; + $shippingAddress = [ + 'fullName' => 'Shipping Name', + 'addressLine1' => '1 Main Shipping Street', + 'locality' => 'Shippingsville', + 'administrativeArea' => 'AL', + 'postalCode' => '98765', + 'countryCode' => 'US', + ]; + + return [ + 'save-both' => [ + true, // save billing + $billingAddress, // billing address + true, // save shipping + $shippingAddress, // shipping address + 2, // new address count + false, // set source billing + false, // set source shipping + ], + 'save-billing-only' => [ + true, + $billingAddress, + false, + null, + 1, + false, + false, + ], + 'save-shipping-only' => [ + false, + null, + true, + $shippingAddress, + 1, + false, + false, + ], + 'save-both-but-same-address' => [ + true, + $billingAddress, + true, + $billingAddress, + 1, + false, + false, + ], + 'try-to-save-both-but-no-addresses' => [ + true, + null, + true, + null, + 0, + false, + false, + ], + 'try-to-save-but-source-billing-present' => [ + true, + $billingAddress, + false, + null, + 0, + true, + false, + ], + 'try-to-save-but-source-shipping-present' => [ + false, + null, + true, + $shippingAddress, + 0, + false, + true, + ], + 'try-to-save-both-but-sources-present' => [ + true, + $billingAddress, + true, + $shippingAddress, + 0, + true, + true, + ], + 'try-save-both-but-billing-source-present' => [ + true, + $billingAddress, + true, + $shippingAddress, + 1, + true, + false, + ], + 'try-save-both-but-shipping-source-present' => [ + true, + $billingAddress, + true, + $shippingAddress, + 1, + false, + true, + ], + ]; + } + /** * @inheritdoc */