From 2d2ffd976d4c4c6ce2dd522143b90fd6d85ceb76 Mon Sep 17 00:00:00 2001 From: Luke Holder Date: Mon, 17 Jul 2023 21:04:05 +0800 Subject: [PATCH 01/11] WIP performance improvements --- CHANGELOG.md | 1 + src/services/Discounts.php | 72 ++++++++++++++++++++++++++++---------- 2 files changed, 55 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c08a4300e..d6adfb72cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - The `commerce/cart/update` action now accepts `firstName` and `lastName` in address params. ([#3015](https://github.com/craftcms/commerce/issues/3015)) - Removed the htmx option from the`commerce/example-templates` command. - Removed the color option from the`commerce/example-templates` command. +- Improved performance of discount recalculation. ## 4.2.11 - 2023-06-05 diff --git a/src/services/Discounts.php b/src/services/Discounts.php index 00287a83f3..866029a416 100644 --- a/src/services/Discounts.php +++ b/src/services/Discounts.php @@ -249,6 +249,12 @@ public function getAllDiscounts(): array */ public function getAllActiveDiscounts(Order $order = null): array { + if ($order && $order->getIsEmpty()) { + return []; + } + + $purchasableIds = collect($order->getLineItems())->pluck('purchasableId')->unique()->all(); + // Date condition for use with key if ($order && $order->dateOrdered) { $date = $order->dateOrdered; @@ -261,7 +267,7 @@ public function getAllActiveDiscounts(Order $order = null): array // Coupon condition key $couponKey = ($order && $order->couponCode) ? $order->couponCode : '*'; $dateKey = DateTimeHelper::toIso8601($date); - $cacheKey = implode(':', [$dateKey, $couponKey]); + $cacheKey = implode(':', [$dateKey, $couponKey, md5(serialize($purchasableIds))]); if (isset($this->_activeDiscountsByKey[$cacheKey])) { return $this->_activeDiscountsByKey[$cacheKey]; @@ -284,11 +290,12 @@ public function getAllActiveDiscounts(Order $order = null): array ['>=', 'dateTo', Db::prepareDateForDb($date)], ]); + $couponSubQuery = (new Query()) + ->from(Table::COUPONS) + ->where(new Expression('[[discountId]] = [[discounts.id]]')); + // If the order has a coupon code let's only get discounts for that code, or discounts that do not require a code if ($order && $order->couponCode) { - $couponSubQuery = (new Query()) - ->from(Table::COUPONS) - ->where(new Expression('[[discountId]] = [[discounts.id]]')); if (Craft::$app->getDb()->getIsPgsql()) { $codeWhere = ['ilike', 'code', $order->couponCode]; @@ -296,22 +303,51 @@ public function getAllActiveDiscounts(Order $order = null): array $codeWhere = ['code' => $order->couponCode]; } - $discountQuery->andWhere([ - 'or', - // Find discount where the coupon code matches + $discountQuery->andWhere( [ - 'exists', (clone $couponSubQuery) - ->andWhere($codeWhere) - ->andWhere([ - 'or', - ['maxUses' => null], - new Expression('[[uses]] < [[maxUses]]'), + 'or', + // Find discount where the coupon code matches + [ + 'exists', (clone $couponSubQuery) + ->andWhere($codeWhere) + ->andWhere([ + 'or', + ['maxUses' => null], + new Expression('[[uses]] < [[maxUses]]'), + ] + ), + ], + // OR find discounts that do not have a coupon code requirement + ['not exists', $couponSubQuery], + ] + ); + } elseif ($order && !$order->couponCode) { + $discountQuery->andWhere( + // only discounts that do not have a coupon code requirement + ['not exists', $couponSubQuery] + ); + } + + if ($order) { + $purchasableIds = collect($order->getLineItems())->pluck('purchasableId')->unique()->all(); + if ($purchasableIds) { + $matchPurchasableSubQuery = (new Query()) + ->from(['dp' => Table::DISCOUNT_PURCHASABLES]) + ->where(new Expression('[[dp.discountId]] = [[discounts.id]]')) + ->andWhere(['dp.purchasableId' => $purchasableIds]); + + $discountQuery->andWhere( + [ + 'or', + ['allPurchasables' => true], + [ + 'exists', $matchPurchasableSubQuery ] - ), - ], - // OR find discounts that do not have a coupon code requirement - ['not exists', $couponSubQuery], - ]); + ] + ); + } else { + $discountQuery->andWhere(['allPurchasables' => true]); + } } $this->_activeDiscountsByKey[$cacheKey] = $this->_populateDiscounts($discountQuery->all()); From dce0fa84aca2c3e53ae55df1d44f7919156b1efa Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 19 Jul 2023 16:14:32 +0100 Subject: [PATCH 02/11] WIP discount processing optimisation --- CHANGELOG.md | 4 + src/Plugin.php | 2 +- ...19_082348_discount_nullable_conditions.php | 49 ++++++ src/models/Discount.php | 119 ++++++++++++- src/services/Discounts.php | 164 ++++++++++-------- 5 files changed, 261 insertions(+), 77 deletions(-) create mode 100644 src/migrations/m230719_082348_discount_nullable_conditions.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d6adfb72cb..62a37ddb78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ - Removed the htmx option from the`commerce/example-templates` command. - Removed the color option from the`commerce/example-templates` command. - Improved performance of discount recalculation. +- Added `craft\commerce\models\Discount::hasOrderCondition()`. +- Added `craft\commerce\models\Discount::hasCustomerCondition()`. +- Added `craft\commerce\models\Discount::hasBillingAddressCondition()`. +- Added `craft\commerce\models\Discount::hasShippingAddressCondition()`. ## 4.2.11 - 2023-06-05 diff --git a/src/Plugin.php b/src/Plugin.php index b2e0ee0e92..ebaf484586 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.3'; /** * @inheritdoc diff --git a/src/migrations/m230719_082348_discount_nullable_conditions.php b/src/migrations/m230719_082348_discount_nullable_conditions.php new file mode 100644 index 0000000000..b4b8f6b16b --- /dev/null +++ b/src/migrations/m230719_082348_discount_nullable_conditions.php @@ -0,0 +1,49 @@ +getConfig()); + + $this->update(Table::DISCOUNTS, ['orderCondition' => null], ['orderCondition' => $orderConditionConfig], [], false); + + $customerCondition = new DiscountCustomerCondition(); + $customerConditionConfig = Json::encode($customerCondition->getConfig()); + + $this->update(Table::DISCOUNTS, ['customerCondition' => null], ['customerCondition' => $customerConditionConfig], [], false); + + $addressCondition = new DiscountAddressCondition(); + $addressConditionConfig = Json::encode($addressCondition->getConfig()); + + $this->update(Table::DISCOUNTS, ['billingAddressCondition' => null], ['billingAddressCondition' => $addressConditionConfig], [], false); + $this->update(Table::DISCOUNTS, ['shippingAddressCondition' => null], ['shippingAddressCondition' => $addressConditionConfig], [], false); + + return true; + } + + /** + * @inheritdoc + */ + public function safeDown(): bool + { + echo "m230719_082348_discount_nullable_conditions cannot be reverted.\n"; + return false; + } +} diff --git a/src/models/Discount.php b/src/models/Discount.php index 819da94293..26583c6764 100644 --- a/src/models/Discount.php +++ b/src/models/Discount.php @@ -279,12 +279,31 @@ public function getOrderCondition(): ElementConditionInterface return $condition; } + /** + * @return bool + * @since 4.3.0 + */ + public function hasOrderCondition(): bool + { + if ($this->_orderCondition === null) { + return false; + } + + return empty($this->getOrderCondition()->getConditionRules()); + } + /** * @param ElementConditionInterface|string|array $condition * @return void + * @throws InvalidConfigException */ public function setOrderCondition(ElementConditionInterface|string|array $condition): void { + if (empty($condition)) { + $this->_orderCondition = null; + return; + } + if (is_string($condition)) { $condition = Json::decodeIfJson($condition); } @@ -312,11 +331,30 @@ public function getCustomerCondition(): ElementConditionInterface } /** - * @param ElementConditionInterface|string|array $condition + * @return bool + * @since 4.3.0 + */ + public function hasCustomerCondition(): bool + { + if ($this->_orderCondition === null) { + return false; + } + + return empty($this->getCustomerCondition()->getConditionRules()); + } + + /** + * @param ElementConditionInterface|string $condition * @return void + * @throws InvalidConfigException */ - public function setCustomerCondition(ElementConditionInterface|string|array $condition): void + public function setCustomerCondition(ElementConditionInterface|string $condition): void { + if (empty($condition)) { + $this->_customerCondition = null; + return; + } + if (is_string($condition)) { $condition = Json::decodeIfJson($condition); } @@ -344,12 +382,31 @@ public function getShippingAddressCondition(): ElementConditionInterface return $condition; } + /** + * @return bool + * @since 4.3.0 + */ + public function hasShippingAddressCondition(): bool + { + if ($this->_orderCondition === null) { + return false; + } + + return empty($this->getShippingAddressCondition()->getConditionRules()); + } + /** * @param ElementConditionInterface|string|array $condition * @return void + * @throws InvalidConfigException */ public function setShippingAddressCondition(ElementConditionInterface|string|array $condition): void { + if (empty($condition)) { + $this->_shippingAddressCondition = null; + return; + } + if (is_string($condition)) { $condition = Json::decodeIfJson($condition); } @@ -377,12 +434,31 @@ public function getBillingAddressCondition(): ElementConditionInterface return $condition; } + /** + * @return bool + * @since 4.3.0 + */ + public function hasBillingAddressCondition(): bool + { + if ($this->_orderCondition === null) { + return false; + } + + return empty($this->getBillingAddressCondition()->getConditionRules()); + } + /** * @param ElementConditionInterface|string|array $condition * @return void + * @throws InvalidConfigException */ public function setBillingAddressCondition(ElementConditionInterface|string|array $condition): void { + if (empty($condition)) { + $this->_billingAddressCondition = null; + return; + } + if (is_string($condition)) { $condition = Json::decodeIfJson($condition); } @@ -552,6 +628,45 @@ function($attribute) { } }, ], + [[ + 'id', + 'allPurchasables', + 'allCategories', + 'purchasableIds', + 'categoryIds', + 'name', + 'description', + 'couponFormat', + 'orderCondition', + 'customerCondition', + 'billingAddressCondition', + 'shippingAddressCondition', + 'perUserLimit', + 'perEmailLimit', + 'totalDiscountUseLimit', + 'totalDiscountUses', + 'dateFrom', + 'dateTo', + 'purchaseTotal', + 'orderConditionFormula', + 'purchaseQty', + 'maxPurchaseQty', + 'baseDiscount', + 'baseDiscountType', + 'perItemDiscount', + 'percentDiscount', + 'percentageOffSubject', + 'excludeOnSale', + 'hasFreeShippingForMatchingItems', + 'hasFreeShippingForOrder', + 'categoryRelationshipType', + 'enabled', + 'stopProcessing', + 'dateCreated', + 'dateUpdated', + 'ignoreSales', + 'appliedTo', + ], 'safe'], ]; } diff --git a/src/services/Discounts.php b/src/services/Discounts.php index 866029a416..e4fe804227 100644 --- a/src/services/Discounts.php +++ b/src/services/Discounts.php @@ -33,6 +33,7 @@ use craft\helpers\ArrayHelper; use craft\helpers\DateTimeHelper; use craft\helpers\Db; +use craft\helpers\StringHelper; use DateTime; use Throwable; use Twig\Error\LoaderError; @@ -288,7 +289,50 @@ public function getAllActiveDiscounts(Order $order = null): array 'or', ['dateTo' => null], ['>=', 'dateTo', Db::prepareDateForDb($date)], + ]) + ->andWhere([ + 'or', + ['totalDiscountUseLimit' => 0], + ['<', 'totalDiscountUses', new Expression('[[totalDiscountUseLimit]]')], + ]); + + // Pre-qualify discounts based on purchase total + if ($order) { + if ($order->getEmail()) { + $emailUsesSubQuery = (new Query()) + ->select(['edu.discountId']) + ->from(['edu' => Table::EMAIL_DISCOUNTUSES]) + ->where(new Expression('[[edu.discountId]] = [[discounts.id]]')) + ->andWhere(new Expression('[[edu.uses]] < [[discounts.perEmailLimit]]')) + ->andWhere(['email' => $order->getEmail()]); + + $discountQuery->andWhere([ + 'or', + ['perEmailLimit' => 0], + ['and', ['>', 'perEmailLimit', 0], ['exists', $emailUsesSubQuery]], + ]); + } else { + $discountQuery->andWhere(['perEmailLimit' => 0]); + } + + $discountQuery->andWhere([ + 'or', + ['purchaseTotal' => 0], + ['and', ['allPurchasables' => true], ['allCategories' => true], ['<=', 'purchaseTotal', $order->getItemSubtotal()]], + ['allPurchasables' => false], + ['allCategories' => false], + ]); + + $discountQuery->andWhere([ + 'or', + ['purchaseQty' => 0, 'maxPurchaseQty' => 0], + ['and', ['allPurchasables' => true], ['allCategories' => true], ['>', 'purchaseQty', 0], ['maxPurchaseQty' => 0], ['<=', 'purchaseQty', $order->getTotalQty()]], + ['and', ['allPurchasables' => true], ['allCategories' => true], ['>', 'maxPurchaseQty', 0], ['purchaseQty' => 0], ['>=', 'maxPurchaseQty', $order->getTotalQty()]], + ['and', ['allPurchasables' => true], ['allCategories' => true], ['>', 'maxPurchaseQty', 0], ['>', 'purchaseQty', 0], ['<=', 'purchaseQty', $order->getTotalQty()], ['>=', 'maxPurchaseQty', $order->getTotalQty()]], + ['allPurchasables' => false], + ['allCategories' => false], ]); + } $couponSubQuery = (new Query()) ->from(Table::COUPONS) @@ -296,7 +340,6 @@ public function getAllActiveDiscounts(Order $order = null): array // If the order has a coupon code let's only get discounts for that code, or discounts that do not require a code if ($order && $order->couponCode) { - if (Craft::$app->getDb()->getIsPgsql()) { $codeWhere = ['ilike', 'code', $order->couponCode]; } else { @@ -332,17 +375,17 @@ public function getAllActiveDiscounts(Order $order = null): array $purchasableIds = collect($order->getLineItems())->pluck('purchasableId')->unique()->all(); if ($purchasableIds) { $matchPurchasableSubQuery = (new Query()) - ->from(['dp' => Table::DISCOUNT_PURCHASABLES]) - ->where(new Expression('[[dp.discountId]] = [[discounts.id]]')) - ->andWhere(['dp.purchasableId' => $purchasableIds]); + ->from(['subdp' => Table::DISCOUNT_PURCHASABLES]) + ->where(new Expression('[[subdp.discountId]] = [[discounts.id]]')) + ->andWhere(['subdp.purchasableId' => $purchasableIds]); $discountQuery->andWhere( [ 'or', ['allPurchasables' => true], [ - 'exists', $matchPurchasableSubQuery - ] + 'exists', $matchPurchasableSubQuery, + ], ] ); } else { @@ -531,41 +574,20 @@ public function matchOrder(Order $order, Discount $discount): bool $allItemsMatch = ($discount->allPurchasables && $discount->allCategories); - $orderCondition = $discount->getOrderCondition(); - $hasOrderConditionRules = count($orderCondition->getConditionRules()); - - if ($hasOrderConditionRules && !$discount->getOrderCondition()->matchElement($order)) { + if ($discount->hasOrderCondition() && !$discount->getOrderCondition()->matchElement($order)) { return false; } - $customerCondition = $discount->getCustomerCondition(); - $hasCustomerConditionRules = count($customerCondition->getConditionRules()); - $customer = $order->getCustomer(); - - if ($hasCustomerConditionRules) { - if (!$customer || !$customerCondition->matchElement($customer)) { - return false; - } + if ($discount->hasCustomerCondition() && (!$order->getCustomer() || !$discount->getCustomerCondition()->matchElement($order->getCustomer()))) { + return false; } - $shippingAddressCondition = $discount->getShippingAddressCondition(); - $hasShippingAddressConditionRules = count($shippingAddressCondition->getConditionRules()); - $shippingAddress = $order->getShippingAddress(); - - if ($hasShippingAddressConditionRules) { - if (!$shippingAddress || !$shippingAddressCondition->matchElement($shippingAddress)) { - return false; - } + if ($discount->hasShippingAddressCondition() && (!$order->getShippingAddress() || !$discount->getShippingAddressCondition()->matchElement($order->getShippingAddress()))) { + return false; } - $billingAddressCondition = $discount->getShippingAddressCondition(); - $hasBillingAddressConditionRules = count($billingAddressCondition->getConditionRules()); - $billingAddress = $order->getShippingAddress(); - - if ($hasBillingAddressConditionRules) { - if (!$billingAddress || !$billingAddressCondition->matchElement($billingAddress)) { - return false; - } + if ($discount->hasBillingAddressCondition() && (!$order->getBillingAddress() || !$discount->getBillingAddressCondition()->matchElement($order->getBillingAddress()))) { + return false; } if (!$this->_isDiscountCouponCodeValid($order, $discount)) { @@ -689,10 +711,10 @@ public function saveDiscount(Discount $model, bool $runValidation = true): bool $record->dateTo = $model->dateTo; $record->enabled = $model->enabled; $record->stopProcessing = $model->stopProcessing; - $record->orderCondition = $model->getOrderCondition()->getConfig(); - $record->customerCondition = $model->getCustomerCondition()->getConfig(); - $record->shippingAddressCondition = $model->getShippingAddressCondition()->getConfig(); - $record->billingAddressCondition = $model->getBillingAddressCondition()->getConfig(); + $record->orderCondition = $model->hasOrderCondition() ? $model->getOrderCondition()->getConfig() : null; + $record->customerCondition = $model->hasCustomerCondition() ? $model->getCustomerCondition()->getConfig() : null; + $record->shippingAddressCondition = $model->hasShippingAddressCondition() ? $model->getShippingAddressCondition()->getConfig() : null; + $record->billingAddressCondition = $model->hasBillingAddressCondition() ? $model->getBillingAddressCondition()->getConfig() : null; $record->orderConditionFormula = $model->orderConditionFormula; $record->purchaseQty = $model->purchaseQty; $record->maxPurchaseQty = $model->maxPurchaseQty; @@ -1128,42 +1150,27 @@ private function _isDiscountPerEmailLimitValid(Discount $discount, Order $order) /** * @param array $discounts * @return array + * @throws InvalidConfigException * @since 2.2.14 */ private function _populateDiscounts(array $discounts): array { - $allDiscountsById = []; - - if (empty($discounts)) { - return $allDiscountsById; - } - - $purchasables = []; - $categories = []; - - foreach ($discounts as $discount) { - $id = $discount['id']; - if ($discount['purchasableId']) { - $purchasables[$id][] = $discount['purchasableId']; - } - - if ($discount['categoryId']) { - $categories[$id][] = $discount['categoryId']; - } - - unset($discount['purchasableId'], $discount['categoryId']); - - if (!isset($allDiscountsById[$id])) { - $allDiscountsById[$id] = new Discount($discount); - } - } - - foreach ($allDiscountsById as $id => $discount) { - $discount->setPurchasableIds($purchasables[$id] ?? []); - $discount->setCategoryIds($categories[$id] ?? []); + foreach ($discounts as &$discount) { + // @TODO remove this when we can widen the accepted params on the setters + $discount['purchasableIds'] = !empty($discount['purchasableIds']) ? StringHelper::split($discount['purchasableIds']) : []; + $discount['categoryIds'] = !empty($discount['categoryIds']) ? StringHelper::split($discount['categoryIds']) : []; + $discount['orderCondition'] = $discount['orderCondition'] ?? ''; + $discount['customerCondition'] = $discount['customerCondition'] ?? ''; + $discount['billingAddressCondition'] = $discount['billingAddressCondition'] ?? ''; + $discount['shippingAddressCondition'] = $discount['shippingAddressCondition'] ?? ''; + + $discount = Craft::createObject([ + 'class' => Discount::class, + 'attributes' => $discount, + ]); } - return $allDiscountsById; + return $discounts; } /** @@ -1211,13 +1218,22 @@ private function _createDiscountQuery(): Query '[[discounts.billingAddressCondition]]', ]) ->from(['discounts' => Table::DISCOUNTS]) - ->orderBy(['sortOrder' => SORT_ASC]); + ->orderBy(['sortOrder' => SORT_ASC]) + ->leftJoin(Table::DISCOUNT_PURCHASABLES . ' dp', '[[dp.discountId]]=[[discounts.id]]') + ->leftJoin(Table::DISCOUNT_CATEGORIES . ' dpt', '[[dpt.discountId]]=[[discounts.id]]') + ->groupBy(['discounts.id']); - $query->addSelect([ - 'dp.purchasableId', - 'dpt.categoryId', - ])->leftJoin(Table::DISCOUNT_PURCHASABLES . ' dp', '[[dp.discountId]]=[[discounts.id]]') - ->leftJoin(Table::DISCOUNT_CATEGORIES . ' dpt', '[[dpt.discountId]]=[[discounts.id]]'); + if (Craft::$app->getDb()->getIsPgsql()) { + $query->addSelect([ + 'purchasableIds' => new Expression("STRING_AGG([[dp.purchasableId]]::text, ',')"), + 'categoryIds' => new Expression("STRING_AGG([[dpt.categoryId]]::text, ',')"), + ]); + } else { + $query->addSelect([ + 'purchasableIds' => new Expression('GROUP_CONCAT([[dp.purchasableId]])'), + 'categoryIds' => new Expression('GROUP_CONCAT([[dpt.categoryId]])'), + ]); + } return $query; } From b979371537e52551df93a0a33a2bb4e9c4e0b850 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 19 Jul 2023 16:32:57 +0100 Subject: [PATCH 03/11] Add `hasOrderCondtion` test --- tests/unit/models/DiscountTest.php | 41 ++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/unit/models/DiscountTest.php b/tests/unit/models/DiscountTest.php index 3b078e1c8c..c96f5f619c 100644 --- a/tests/unit/models/DiscountTest.php +++ b/tests/unit/models/DiscountTest.php @@ -8,7 +8,10 @@ namespace craftcommercetests\unit\models; use Codeception\Test\Unit; +use craft\commerce\elements\conditions\orders\OrderCondition; use craft\commerce\models\Discount; +use craft\elements\conditions\ElementConditionInterface; +use craft\elements\conditions\IdConditionRule; /** * DiscountTest @@ -44,4 +47,42 @@ public function getPercentDiscountAsPercentDataProvider(): array ['-0.1050400', '10.504%'], ]; } + + /** + * @return void + * @since 4.3.0 + * @dataProvider conditionBuilderDataProvider + */ + public function testHasOrderCondition(ElementConditionInterface|array|string $orderCondition, bool $expected): void + { + if ($orderCondition === 'class' || $orderCondition === 'rules') { + /** @var OrderCondition $orderCondition */ + $orderCondition = \Craft::$app->getConditions()->createCondition(OrderCondition::class); + + if ($orderCondition === 'rules') { + $rule = \Craft::$app->getConditions()->createConditionRule([ + 'type' => IdConditionRule::class, + 'value' => 1, + ]); + $orderCondition->addConditionRule($rule); + } + } + + $discount = \Craft::createObject([ + 'class' => Discount::class, + 'orderCondition' => $orderCondition, + ]); + + self::assertSame($expected, $discount->hasOrderCondition()); + } + + public function conditionBuilderDataProvider(): array + { + return [ + 'blank-string' => ['', false], + 'empty-array' => [[], false], + 'no-rules' => ['class', false], + 'rules' => ['rules', true], + ]; + } } From 6b883cb83dd7da6681f0c9293b7bbc8ccf3e70c6 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 19 Jul 2023 16:33:23 +0100 Subject: [PATCH 04/11] Fix logical bugs with `hasXCondtion` in discount model --- src/models/Discount.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/models/Discount.php b/src/models/Discount.php index 26583c6764..6f9a504d81 100644 --- a/src/models/Discount.php +++ b/src/models/Discount.php @@ -289,7 +289,7 @@ public function hasOrderCondition(): bool return false; } - return empty($this->getOrderCondition()->getConditionRules()); + return !empty($this->getOrderCondition()->getConditionRules()); } /** @@ -340,7 +340,7 @@ public function hasCustomerCondition(): bool return false; } - return empty($this->getCustomerCondition()->getConditionRules()); + return !empty($this->getCustomerCondition()->getConditionRules()); } /** @@ -392,7 +392,7 @@ public function hasShippingAddressCondition(): bool return false; } - return empty($this->getShippingAddressCondition()->getConditionRules()); + return !empty($this->getShippingAddressCondition()->getConditionRules()); } /** @@ -444,7 +444,7 @@ public function hasBillingAddressCondition(): bool return false; } - return empty($this->getBillingAddressCondition()->getConditionRules()); + return !empty($this->getBillingAddressCondition()->getConditionRules()); } /** From 55d9a7b695a80d31c76aa7eda7eb92e3cb532901 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 19 Jul 2023 17:04:44 +0100 Subject: [PATCH 05/11] Fix accidental removal of param types --- src/models/Discount.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/models/Discount.php b/src/models/Discount.php index 6f9a504d81..c8d957f7ac 100644 --- a/src/models/Discount.php +++ b/src/models/Discount.php @@ -336,7 +336,7 @@ public function getCustomerCondition(): ElementConditionInterface */ public function hasCustomerCondition(): bool { - if ($this->_orderCondition === null) { + if ($this->_customerCondition === null) { return false; } @@ -348,7 +348,7 @@ public function hasCustomerCondition(): bool * @return void * @throws InvalidConfigException */ - public function setCustomerCondition(ElementConditionInterface|string $condition): void + public function setCustomerCondition(ElementConditionInterface|string|array $condition): void { if (empty($condition)) { $this->_customerCondition = null; @@ -388,7 +388,7 @@ public function getShippingAddressCondition(): ElementConditionInterface */ public function hasShippingAddressCondition(): bool { - if ($this->_orderCondition === null) { + if ($this->_shippingAddressCondition === null) { return false; } @@ -440,7 +440,7 @@ public function getBillingAddressCondition(): ElementConditionInterface */ public function hasBillingAddressCondition(): bool { - if ($this->_orderCondition === null) { + if ($this->_billingAddressCondition === null) { return false; } From 4a748170a10c357ad8fa8e9295e7efef90520ff0 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 19 Jul 2023 17:05:16 +0100 Subject: [PATCH 06/11] Add more has condition tests --- tests/unit/models/DiscountTest.php | 128 +++++++++++++++++++++++++++-- 1 file changed, 120 insertions(+), 8 deletions(-) diff --git a/tests/unit/models/DiscountTest.php b/tests/unit/models/DiscountTest.php index c96f5f619c..e9858194ae 100644 --- a/tests/unit/models/DiscountTest.php +++ b/tests/unit/models/DiscountTest.php @@ -8,10 +8,13 @@ namespace craftcommercetests\unit\models; use Codeception\Test\Unit; -use craft\commerce\elements\conditions\orders\OrderCondition; +use craft\commerce\elements\conditions\addresses\DiscountAddressCondition; +use craft\commerce\elements\conditions\customers\DiscountCustomerCondition; +use craft\commerce\elements\conditions\orders\DiscountOrderCondition; use craft\commerce\models\Discount; use craft\elements\conditions\ElementConditionInterface; use craft\elements\conditions\IdConditionRule; +use yii\base\InvalidConfigException; /** * DiscountTest @@ -49,33 +52,142 @@ public function getPercentDiscountAsPercentDataProvider(): array } /** + * @param ElementConditionInterface|array|string $condition + * @param bool $expected * @return void + * @throws InvalidConfigException * @since 4.3.0 * @dataProvider conditionBuilderDataProvider */ - public function testHasOrderCondition(ElementConditionInterface|array|string $orderCondition, bool $expected): void + public function testHasOrderCondition(ElementConditionInterface|array|string $condition, bool $expected): void { - if ($orderCondition === 'class' || $orderCondition === 'rules') { - /** @var OrderCondition $orderCondition */ - $orderCondition = \Craft::$app->getConditions()->createCondition(OrderCondition::class); + if ($condition === 'class' || $condition === 'rules') { + /** @var DiscountOrderCondition $condition */ + $conditionBuilder = \Craft::$app->getConditions()->createCondition(DiscountOrderCondition::class); - if ($orderCondition === 'rules') { + if ($condition === 'rules') { $rule = \Craft::$app->getConditions()->createConditionRule([ 'type' => IdConditionRule::class, 'value' => 1, ]); - $orderCondition->addConditionRule($rule); + $conditionBuilder->addConditionRule($rule); } + + $condition = $conditionBuilder; } + /** @var Discount $discount */ $discount = \Craft::createObject([ 'class' => Discount::class, - 'orderCondition' => $orderCondition, + 'orderCondition' => $condition, ]); self::assertSame($expected, $discount->hasOrderCondition()); } + /** + * @param ElementConditionInterface|array|string $condition + * @param bool $expected + * @return void + * @throws InvalidConfigException + * @since 4.3.0 + * @dataProvider conditionBuilderDataProvider + */ + public function testHasCustomerCondition(ElementConditionInterface|array|string $condition, bool $expected): void + { + if ($condition === 'class' || $condition === 'rules') { + /** @var DiscountCustomerCondition $condition */ + $conditionBuilder = \Craft::$app->getConditions()->createCondition(DiscountCustomerCondition::class); + + if ($condition === 'rules') { + $rule = \Craft::$app->getConditions()->createConditionRule([ + 'type' => IdConditionRule::class, + 'value' => 1, + ]); + $conditionBuilder->addConditionRule($rule); + } + + $condition = $conditionBuilder; + } + + /** @var Discount $discount */ + $discount = \Craft::createObject([ + 'class' => Discount::class, + 'customerCondition' => $condition, + ]); + + self::assertSame($expected, $discount->hasCustomerCondition()); + } + + + /** + * @param ElementConditionInterface|array|string $condition + * @param bool $expected + * @return void + * @throws InvalidConfigException + * @since 4.3.0 + * @dataProvider conditionBuilderDataProvider + */ + public function testHasBillingAddressCondition(ElementConditionInterface|array|string $condition, bool $expected): void + { + if ($condition === 'class' || $condition === 'rules') { + /** @var DiscountAddressCondition $condition */ + $conditionBuilder = \Craft::$app->getConditions()->createCondition(DiscountAddressCondition::class); + + if ($condition === 'rules') { + $rule = \Craft::$app->getConditions()->createConditionRule([ + 'type' => IdConditionRule::class, + 'value' => 1, + ]); + $conditionBuilder->addConditionRule($rule); + } + + $condition = $conditionBuilder; + } + + /** @var Discount $discount */ + $discount = \Craft::createObject([ + 'class' => Discount::class, + 'billingAddressCondition' => $condition, + ]); + + self::assertSame($expected, $discount->hasBillingAddressCondition()); + } + + /** + * @param ElementConditionInterface|array|string $condition + * @param bool $expected + * @return void + * @throws InvalidConfigException + * @since 4.3.0 + * @dataProvider conditionBuilderDataProvider + */ + public function testHasShippingAddressCondition(ElementConditionInterface|array|string $condition, bool $expected): void + { + if ($condition === 'class' || $condition === 'rules') { + /** @var DiscountAddressCondition $condition */ + $conditionBuilder = \Craft::$app->getConditions()->createCondition(DiscountAddressCondition::class); + + if ($condition === 'rules') { + $rule = \Craft::$app->getConditions()->createConditionRule([ + 'type' => IdConditionRule::class, + 'value' => 1, + ]); + $conditionBuilder->addConditionRule($rule); + } + + $condition = $conditionBuilder; + } + + /** @var Discount $discount */ + $discount = \Craft::createObject([ + 'class' => Discount::class, + 'shippingAddressCondition' => $condition, + ]); + + self::assertSame($expected, $discount->hasShippingAddressCondition()); + } + public function conditionBuilderDataProvider(): array { return [ From e38aacb90a0411c3fb0b06e86291695325d3b1bc Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Thu, 20 Jul 2023 10:13:57 +0100 Subject: [PATCH 07/11] Fix bugs for tests --- src/services/Discounts.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/services/Discounts.php b/src/services/Discounts.php index e4fe804227..52942de3d3 100644 --- a/src/services/Discounts.php +++ b/src/services/Discounts.php @@ -250,12 +250,11 @@ public function getAllDiscounts(): array */ public function getAllActiveDiscounts(Order $order = null): array { - if ($order && $order->getIsEmpty()) { - return []; + $purchasableIds = []; + if ($order) { + $purchasableIds = collect($order->getLineItems())->pluck('purchasableId')->unique()->all(); } - $purchasableIds = collect($order->getLineItems())->pluck('purchasableId')->unique()->all(); - // Date condition for use with key if ($order && $order->dateOrdered) { $date = $order->dateOrdered; @@ -268,7 +267,8 @@ public function getAllActiveDiscounts(Order $order = null): array // Coupon condition key $couponKey = ($order && $order->couponCode) ? $order->couponCode : '*'; $dateKey = DateTimeHelper::toIso8601($date); - $cacheKey = implode(':', [$dateKey, $couponKey, md5(serialize($purchasableIds))]); + $purchasablesKey = !empty($purchasableIds) ? md5(serialize($purchasableIds)) : ''; + $cacheKey = implode(':', array_filter([$dateKey, $couponKey, $purchasablesKey])); if (isset($this->_activeDiscountsByKey[$cacheKey])) { return $this->_activeDiscountsByKey[$cacheKey]; @@ -372,7 +372,6 @@ public function getAllActiveDiscounts(Order $order = null): array } if ($order) { - $purchasableIds = collect($order->getLineItems())->pluck('purchasableId')->unique()->all(); if ($purchasableIds) { $matchPurchasableSubQuery = (new Query()) ->from(['subdp' => Table::DISCOUNT_PURCHASABLES]) From 274f1669d50f5fa5eb2aa8eff409789a2c6ec7cd Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Thu, 20 Jul 2023 16:39:43 +0100 Subject: [PATCH 08/11] WIP new active discounts tests --- src/services/Discounts.php | 5 +- tests/unit/services/DiscountsTest.php | 188 ++++++++++++++++++++++++-- 2 files changed, 180 insertions(+), 13 deletions(-) diff --git a/src/services/Discounts.php b/src/services/Discounts.php index 52942de3d3..bb24f5ae31 100644 --- a/src/services/Discounts.php +++ b/src/services/Discounts.php @@ -300,16 +300,15 @@ public function getAllActiveDiscounts(Order $order = null): array if ($order) { if ($order->getEmail()) { $emailUsesSubQuery = (new Query()) - ->select(['edu.discountId']) + ->select([new Expression('COALESCE(SUM([[edu.uses]]), 0)')]) ->from(['edu' => Table::EMAIL_DISCOUNTUSES]) ->where(new Expression('[[edu.discountId]] = [[discounts.id]]')) - ->andWhere(new Expression('[[edu.uses]] < [[discounts.perEmailLimit]]')) ->andWhere(['email' => $order->getEmail()]); $discountQuery->andWhere([ 'or', ['perEmailLimit' => 0], - ['and', ['>', 'perEmailLimit', 0], ['exists', $emailUsesSubQuery]], + ['and', ['>', 'perEmailLimit', 0], ['>', 'perEmailLimit', $emailUsesSubQuery]], ]); } else { $discountQuery->andWhere(['perEmailLimit' => 0]); diff --git a/tests/unit/services/DiscountsTest.php b/tests/unit/services/DiscountsTest.php index 308bfde8d4..90fa39c71a 100644 --- a/tests/unit/services/DiscountsTest.php +++ b/tests/unit/services/DiscountsTest.php @@ -24,9 +24,11 @@ use craftcommercetests\fixtures\DiscountsFixture; use DateInterval; use DateTime; +use DateTimeZone; use UnitTester; use yii\base\InvalidConfigException; use yii\db\Exception; +use yii\db\Expression; /** * DiscountsTest @@ -367,20 +369,186 @@ public function testVoidIfInvalidCouponCode(): void } /** + * @param array|false $attributes + * @param int $count * @return void * @throws \Exception + * @dataProvider gatAllActiveDiscountsDataProvider */ - public function testGetAllActiveDiscounts(): void + public function testGetAllActiveDiscounts(array|false $attributes, int $count, array $discounts): void { - $activeDiscounts = $this->discounts->getAllActiveDiscounts(); - $activeDiscountsCodeExists = $this->discounts->getAllActiveDiscounts(new Order(['couponCode' => 'discount_1'])); - $activeDiscountsCodeDoesntExists = $this->discounts->getAllActiveDiscounts(new Order(['couponCode' => 'coupon_code_doesnt_exist'])); - - self::assertNotEmpty($activeDiscounts); - self::assertCount(1, $activeDiscounts); - self::assertNotEmpty($activeDiscountsCodeExists); - self::assertCount(1, $activeDiscountsCodeExists); - self::assertEmpty($activeDiscountsCodeDoesntExists); + if (!empty($discounts)) { + foreach ($discounts as &$discount) { + $emailUses = $discount['_emailUses'] ?? []; + + $discountModel = Craft::createObject([ + 'class' => Discount::class, + 'attributes' => $discount, + ]); + Plugin::getInstance()->getDiscounts()->saveDiscount($discountModel); + $discount = $discountModel->id; + + if ($discountModel->totalDiscountUses > 0) { + Craft::$app->getDb()->createCommand() + ->update(Table::DISCOUNTS, [ + 'totalDiscountUses' => $discountModel->totalDiscountUses, + ], [ + 'id' => $discountModel->id, + ]) + ->execute(); + } + + if (!empty($emailUses)) { + $emailUses = collect($emailUses)->map(fn($uses, $email) => [$email, $discountModel->id, $uses])->all(); + Craft::$app->getDb()->createCommand() + ->batchInsert(Table::EMAIL_DISCOUNTUSES, ['email', 'discountId', 'uses'], $emailUses) + ->execute(); + } + } + } + + if ($attributes === false) { + $activeDiscounts = $this->discounts->getAllActiveDiscounts(); + } else { + $activeDiscounts = $this->discounts->getAllActiveDiscounts(new Order($attributes)); + } + + if ($count > 0) { + self::assertCount($count, $activeDiscounts); + self::assertNotEmpty($activeDiscounts); + } else { + self::assertEmpty($activeDiscounts); + } + + // Tidy up the discounts + if (!empty($discounts)) { + foreach ($discounts as $discountId) { + Plugin::getInstance()->getDiscounts()->deleteDiscountById($discountId); + } + } + } + + /** + * @return array[] + */ + public function gatAllActiveDiscountsDataProvider(): array + { + $yesterday = (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(12, 0)->modify('-1 day'); + $tomorrow = (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(12, 0)->modify('+1 day'); + + function _createDiscounts($discounts) { + return collect($discounts)->mapWithKeys(function(array $d, string $key) { + return [$key => array_merge($d, [ + 'name' => 'Discount - ' . $key, + 'perItemDiscount' => '1', + 'enabled' => true, + 'allCategories' => true, + 'allPurchasables' => true, + 'percentageOffSubject' => 'original', + ])]; + })->all(); + } + + return [ + 'no-order' => [false, 1, []], + 'order-with-valid-coupon' => [['couponCode' => 'discount_1'], 1, []], + 'order-with-invalid-coupon' => [['couponCode' => 'coupon_code_doesnt_exist'], 0, []], + 'order-discounts-dates' => [ + [], + 3, + _createDiscounts([ + 'date-from-valid' => [ + 'dateFrom' => $yesterday, + ], + 'date-from-invalid' => [ + 'dateFrom' => $tomorrow, + ], + 'date-to-valid' => [ + 'dateTo' => $tomorrow, + ], + 'date-to-invalid' => [ + 'dateTo' => $yesterday, + ], + 'date-to-from-valid' => [ + 'dateFrom' => $yesterday, + 'dateTo' => $tomorrow, + ], + 'date-to-from-invalid' => [ + 'dateFrom' => $tomorrow, + 'dateTo' => $tomorrow->modify('+1 day'), + ], + ]), + ], + 'order-discounts-limits' => [ + [], + 4, + _createDiscounts([ + 'total-limit-zero' => [ + 'totalDiscountUseLimit' => 0, + ], + 'total-limit-zero-with-uses' => [ + 'totalDiscountUses' => 10, + 'totalDiscountUseLimit' => 0, + ], + 'total-limit-valid-with-no-uses' => [ + 'totalDiscountUses' => 0, + 'totalDiscountUseLimit' => 10, + ], + 'total-limit-valid-with-uses' => [ + 'totalDiscountUses' => 7, + 'totalDiscountUseLimit' => 10, + ], + 'total-limit-invalid-equals' => [ + 'totalDiscountUses' => 10, + 'totalDiscountUseLimit' => 10, + ], + 'total-limit-invalid-extra' => [ + 'totalDiscountUses' => 11, + 'totalDiscountUseLimit' => 10, + ], + ]), + ], + 'order-discounts-email-limits-no-email' => [ + [], + 1, + _createDiscounts([ + 'total-limit-zero' => [ + 'perEmailLimit' => 0, + ], + 'total-limit' => [ + 'perEmailLimit' => 1, + ], + ]), + ], + 'order-discounts-email-limits' => [ + ['email' => 'per.email.limit@crafttest.com'], + 4, + _createDiscounts([ + 'total-limit-zero' => [ + 'perEmailLimit' => 0, + ], + 'total-limit-zero-with-uses' => [ + '_emailUses' => ['per.email.limit@crafttest.com' => 10], + 'perEmailLimit' => 0, + ], + 'total-limit-valid-with-no-uses' => [ + 'perEmailLimit' => 10, + ], + 'total-limit-valid-with-uses' => [ + '_emailUses' => ['per.email.limit@crafttest.com' => 7], + 'perEmailLimit' => 10, + ], + 'total-limit-invalid-equals' => [ + '_emailUses' => ['per.email.limit@crafttest.com' => 10], + 'perEmailLimit' => 10, + ], + 'total-limit-invalid-extra' => [ + '_emailUses' => ['per.email.limit@crafttest.com' => 11], + 'perEmailLimit' => 10, + ], + ]), + ], + ]; } /** From 8695ed8f54a7dcec41dfe7bff1e9b0c6db0d3789 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Fri, 21 Jul 2023 11:37:13 +0100 Subject: [PATCH 09/11] `purchaseTotal` tests --- src/services/Discounts.php | 30 ++++---- tests/unit/services/DiscountsTest.php | 101 +++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 21 deletions(-) diff --git a/src/services/Discounts.php b/src/services/Discounts.php index bb24f5ae31..cc4a5718c5 100644 --- a/src/services/Discounts.php +++ b/src/services/Discounts.php @@ -370,25 +370,21 @@ public function getAllActiveDiscounts(Order $order = null): array ); } - if ($order) { - if ($purchasableIds) { - $matchPurchasableSubQuery = (new Query()) - ->from(['subdp' => Table::DISCOUNT_PURCHASABLES]) - ->where(new Expression('[[subdp.discountId]] = [[discounts.id]]')) - ->andWhere(['subdp.purchasableId' => $purchasableIds]); + if ($order && !empty($purchasableIds)) { + $matchPurchasableSubQuery = (new Query()) + ->from(['subdp' => Table::DISCOUNT_PURCHASABLES]) + ->where(new Expression('[[subdp.discountId]] = [[discounts.id]]')) + ->andWhere(['subdp.purchasableId' => $purchasableIds]); - $discountQuery->andWhere( + $discountQuery->andWhere( + [ + 'or', + ['allPurchasables' => true], [ - 'or', - ['allPurchasables' => true], - [ - 'exists', $matchPurchasableSubQuery, - ], - ] - ); - } else { - $discountQuery->andWhere(['allPurchasables' => true]); - } + 'exists', $matchPurchasableSubQuery, + ], + ] + ); } $this->_activeDiscountsByKey[$cacheKey] = $this->_populateDiscounts($discountQuery->all()); diff --git a/tests/unit/services/DiscountsTest.php b/tests/unit/services/DiscountsTest.php index 90fa39c71a..d224c7a924 100644 --- a/tests/unit/services/DiscountsTest.php +++ b/tests/unit/services/DiscountsTest.php @@ -12,6 +12,7 @@ use Craft; use craft\commerce\db\Table; use craft\commerce\elements\Order; +use craft\commerce\elements\Variant; use craft\commerce\models\Discount; use craft\commerce\models\LineItem; use craft\commerce\models\OrderAdjustment; @@ -19,16 +20,18 @@ use craft\commerce\services\Discounts; use craft\commerce\test\mockclasses\Purchasable; use craft\db\Query; +use craft\elements\Category; use craft\elements\User; +use craftcommercetests\fixtures\CategoriesFixture; use craftcommercetests\fixtures\CustomerFixture; use craftcommercetests\fixtures\DiscountsFixture; +use craftcommercetests\fixtures\ProductFixture; use DateInterval; use DateTime; use DateTimeZone; use UnitTester; use yii\base\InvalidConfigException; use yii\db\Exception; -use yii\db\Expression; /** * DiscountsTest @@ -66,6 +69,12 @@ public function _fixtures(): array 'customers' => [ 'class' => CustomerFixture::class, ], + 'products' => [ + 'class' => ProductFixture::class, + ], + 'categories' => [ + 'class' => CategoriesFixture::class, + ], ]; } @@ -377,10 +386,21 @@ public function testVoidIfInvalidCouponCode(): void */ public function testGetAllActiveDiscounts(array|false $attributes, int $count, array $discounts): void { + $originalEdition = Plugin::getInstance()->edition; + Plugin::getInstance()->edition = Plugin::EDITION_PRO; + if (!empty($discounts)) { foreach ($discounts as &$discount) { $emailUses = $discount['_emailUses'] ?? []; + if (isset($discount['purchasableIds'])) { + $discount['purchasableIds'] = Variant::find()->sku($discount['purchasableIds'])->ids(); + } + + if (isset($discount['categoryIds'])) { + $discount['categoryIds'] = Category::find()->slug($discount['categoryIds'])->ids(); + } + $discountModel = Craft::createObject([ 'class' => Discount::class, 'attributes' => $discount, @@ -410,7 +430,18 @@ public function testGetAllActiveDiscounts(array|false $attributes, int $count, a if ($attributes === false) { $activeDiscounts = $this->discounts->getAllActiveDiscounts(); } else { - $activeDiscounts = $this->discounts->getAllActiveDiscounts(new Order($attributes)); + $order = new Order(array_diff_key($attributes, array_flip(['_lineItems']))); + + if (isset($attributes['_lineItems'])) { + $lineItems = []; + foreach ($attributes['_lineItems'] as $sku => $qty) { + $variant = Variant::find()->sku($sku)->one(); + $lineItems[] = Plugin::getInstance()->getLineItems()->createLineItem($order, $variant->id, [], $qty); + } + $order->setLineItems($lineItems); + } + + $activeDiscounts = $this->discounts->getAllActiveDiscounts($order); } if ($count > 0) { @@ -426,6 +457,8 @@ public function testGetAllActiveDiscounts(array|false $attributes, int $count, a Plugin::getInstance()->getDiscounts()->deleteDiscountById($discountId); } } + + Plugin::getInstance()->edition = $originalEdition; } /** @@ -438,14 +471,14 @@ public function gatAllActiveDiscountsDataProvider(): array function _createDiscounts($discounts) { return collect($discounts)->mapWithKeys(function(array $d, string $key) { - return [$key => array_merge($d, [ + return [$key => array_merge([ 'name' => 'Discount - ' . $key, 'perItemDiscount' => '1', 'enabled' => true, 'allCategories' => true, 'allPurchasables' => true, 'percentageOffSubject' => 'original', - ])]; + ], $d)]; })->all(); } @@ -548,6 +581,66 @@ function _createDiscounts($discounts) { ], ]), ], + 'purchase-total-limit-no-items' => [ + [], + 4, + _createDiscounts([ + 'purchase-total-zero' => [ + 'purchaseTotal' => 0, + ], + 'purchase-total-all-purchasables-false' => [ + 'purchaseTotal' => 10, + 'allPurchasables' => false, + 'purchasableIds' => ['rad-hood'], + ], + 'purchase-total-all-categories-false' => [ + 'purchaseTotal' => 10, + 'allCategories' => false, + 'categoryIds' => ['commerce-category'], + ], + 'purchase-total-both-all-false' => [ + 'purchaseTotal' => 10, + 'allPurchasables' => false, + 'purchasableIds' => ['rad-hood'], + 'allCategories' => false, + 'categoryIds' => ['commerce-category'], + ], + ]), + ], + 'purchase-total-limit-with-items' => [ + [ + '_lineItems' => ['rad-hood' => 1], + ], + 5, + _createDiscounts([ + 'purchase-total-zero' => [ + 'purchaseTotal' => 0, + ], + 'purchase-total-all-purchasables-false' => [ + 'purchaseTotal' => 10, + 'allPurchasables' => false, + 'purchasableIds' => ['rad-hood'], + ], + 'purchase-total-all-categories-false' => [ + 'purchaseTotal' => 10, + 'allCategories' => false, + 'categoryIds' => ['commerce-category'], + ], + 'purchase-total-both-all-false' => [ + 'purchaseTotal' => 10, + 'allPurchasables' => false, + 'purchasableIds' => ['rad-hood'], + 'allCategories' => false, + 'categoryIds' => ['commerce-category'], + ], + 'purchase-total-valid' => [ + 'purchaseTotal' => 150, + ], + 'purchase-total-invalid' => [ + 'purchaseTotal' => 10.99, + ], + ]), + ], ]; } From 1a904ca16fd41da8c3b43068229ab178bf2d6051 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Mon, 24 Jul 2023 08:41:52 +0100 Subject: [PATCH 10/11] Active discounts tests qty limits and purchasable limitations --- tests/unit/services/DiscountsTest.php | 108 ++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/tests/unit/services/DiscountsTest.php b/tests/unit/services/DiscountsTest.php index d224c7a924..c1d76a6b99 100644 --- a/tests/unit/services/DiscountsTest.php +++ b/tests/unit/services/DiscountsTest.php @@ -641,6 +641,114 @@ function _createDiscounts($discounts) { ], ]), ], + 'qty-limits-no-items' => [ + [], + 6, + _createDiscounts([ + 'purchase-qty-zero' => [ + 'purchaseQty' => 0, + ], + 'max-qty-zero' => [ + 'maxPurchaseQty' => 0, + ], + 'both-zero' => [ + 'purchaseQty' => 0, + 'maxPurchaseQty' => 0, + ], + 'purchase-qty-all-purchasables-false' => [ + 'purchaseQty' => 4, + 'allPurchasables' => false, + 'purchasableIds' => ['rad-hood'], + ], + 'purchase-total-all-categories-false' => [ + 'purchaseTotal' => 10, + 'allCategories' => false, + 'categoryIds' => ['commerce-category'], + ], + 'purchase-total-both-all-false' => [ + 'purchaseTotal' => 10, + 'allPurchasables' => false, + 'purchasableIds' => ['rad-hood'], + 'allCategories' => false, + 'categoryIds' => ['commerce-category'], + ], + ]), + ], + 'qty-limits-with-items' => [ + ['_lineItems' => ['rad-hood' => 4]], + 6, + _createDiscounts([ + 'purchase-qty-zero' => [ + 'purchaseQty' => 0, + ], + 'max-qty-zero' => [ + 'maxPurchaseQty' => 0, + ], + 'both-zero' => [ + 'purchaseQty' => 0, + 'maxPurchaseQty' => 0, + ], + 'purchase-qty-valid' => [ + 'purchaseQty' => 3, + ], + 'purchase-qty-invalid' => [ + 'purchaseQty' => 5, + ], + 'max-qty-valid' => [ + 'maxPurchaseQty' => 10, + ], + 'max-qty-invalid' => [ + 'maxPurchaseQty' => 3, + ], + 'both-valid' => [ + 'purchaseQty' => 2, + 'maxPurchaseQty' => 10, + ], + 'both-invalid' => [ + 'purchaseQty' => 10, + 'maxPurchaseQty' => 14, + ], + ]), + ], + 'purchasables-one-lineitem' => [ + ['_lineItems' => ['rad-hood' => 1]], + 3, + _createDiscounts([ + 'all-purchasables' => [ + 'allPurchasables' => true, + ], + 'one-to-one' => [ + 'allPurchasables' => false, + 'purchasableIds' => ['rad-hood'], + ], + 'one-to-many' => [ + 'allPurchasables' => false, + 'purchasableIds' => ['rad-hood', 'hct-white'], + ], + 'no-match' => [ + 'allPurchasables' => false, + 'purchasableIds' => ['hct-blue'], + ] + ]), + ], + 'purchasables-multi-lineitems' => [ + ['_lineItems' => ['rad-hood' => 1, 'hct-white' => 1]], + 2, + _createDiscounts([ + 'one' => [ + 'allPurchasables' => false, + 'purchasableIds' => ['rad-hood'], + ], + 'many' => [ + 'allPurchasables' => false, + 'purchasableIds' => ['rad-hood', 'hct-white'], + ], + 'no-match' => [ + 'allPurchasables' => false, + 'purchasableIds' => ['hct-blue'], + ] + ]), + ], ]; } From d6c0f3320bf9a387303128754c5cc4ace5d965e1 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Mon, 24 Jul 2023 08:44:30 +0100 Subject: [PATCH 11/11] fix cs --- tests/unit/services/DiscountsTest.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unit/services/DiscountsTest.php b/tests/unit/services/DiscountsTest.php index c1d76a6b99..d55ea74861 100644 --- a/tests/unit/services/DiscountsTest.php +++ b/tests/unit/services/DiscountsTest.php @@ -469,7 +469,8 @@ public function gatAllActiveDiscountsDataProvider(): array $yesterday = (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(12, 0)->modify('-1 day'); $tomorrow = (new DateTime('now', new DateTimeZone('America/Los_Angeles')))->setTime(12, 0)->modify('+1 day'); - function _createDiscounts($discounts) { + function _createDiscounts($discounts) + { return collect($discounts)->mapWithKeys(function(array $d, string $key) { return [$key => array_merge([ 'name' => 'Discount - ' . $key, @@ -728,7 +729,7 @@ function _createDiscounts($discounts) { 'no-match' => [ 'allPurchasables' => false, 'purchasableIds' => ['hct-blue'], - ] + ], ]), ], 'purchasables-multi-lineitems' => [ @@ -746,7 +747,7 @@ function _createDiscounts($discounts) { 'no-match' => [ 'allPurchasables' => false, 'purchasableIds' => ['hct-blue'], - ] + ], ]), ], ];