From cc622ab85fcef4fc11afdea4db46f88988fc797c Mon Sep 17 00:00:00 2001 From: Bob Olde Hampsink Date: Wed, 3 Jun 2026 11:05:31 +0200 Subject: [PATCH 1/6] Guard catalog pricing rules query during migrations --- src/services/CatalogPricingRules.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/CatalogPricingRules.php b/src/services/CatalogPricingRules.php index 7d69559e3b..80fc00b3ff 100644 --- a/src/services/CatalogPricingRules.php +++ b/src/services/CatalogPricingRules.php @@ -52,7 +52,8 @@ public function hasCatalogPricingRules(): bool } if ($this->_hasCatalogPricingRules === null) { - $this->_hasCatalogPricingRules = $this->_createCatalogPricingRuleQuery()->exists(); + $this->_hasCatalogPricingRules = Craft::$app->getDb()->tableExists(Table::CATALOG_PRICING_RULES) && + $this->_createCatalogPricingRuleQuery()->exists(); } return (bool)$this->_hasCatalogPricingRules; From 9ed2fabed5de663b7b0e58770bec133e559e2e1a Mon Sep 17 00:00:00 2001 From: Bob Olde Hampsink Date: Wed, 3 Jun 2026 11:43:59 +0200 Subject: [PATCH 2/6] Guard Commerce element queries during migrations --- src/elements/db/ProductQuery.php | 50 +++++++++++-- src/elements/db/PurchasableQuery.php | 106 +++++++++++++++++++++------ 2 files changed, 127 insertions(+), 29 deletions(-) diff --git a/src/elements/db/ProductQuery.php b/src/elements/db/ProductQuery.php index bce927bc1b..a7b9381771 100644 --- a/src/elements/db/ProductQuery.php +++ b/src/elements/db/ProductQuery.php @@ -745,9 +745,15 @@ protected function afterPrepare(): bool // Store dependent related joins to the sub query need to be done after the `elements_sites` is joined in the base `ElementQuery` class. $customerId = Craft::$app->getUser()->getIdentity()?->id; + $hasStoreTables = $this->_storeTablesExist(); + + if (!$hasStoreTables) { + return parent::afterPrepare(); + } + $this->subQuery->leftJoin(['sitestores' => Table::SITESTORES], '[[elements_sites.siteId]] = [[sitestores.siteId]]'); - if (Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) { + if ($hasStoreTables && Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) { $catalogPricesQuery = Plugin::getInstance() ->getCatalogPricing() ->createCatalogPricesQuery(userId: $customerId) @@ -771,6 +777,7 @@ protected function afterPrepare(): bool protected function beforePrepare(): bool { $this->_normalizeTypeId(); + $hasStoreTables = $this->_storeTablesExist(); // See if 'type' were set to invalid handles if ($this->typeId === []) { @@ -784,36 +791,55 @@ protected function beforePrepare(): bool 'commerce_products.typeId', 'commerce_products.postDate', 'commerce_products.expiryDate', - 'purchasablesstores.basePrice as defaultBasePrice', - 'purchasablesstores.basePromotionalPrice as defaultBasePromotionalPrice', 'commerce_products.defaultVariantId', 'purchasables.sku as defaultSku', 'purchasables.weight as defaultWeight', 'purchasables.length as defaultLength', 'purchasables.width as defaultWidth', 'purchasables.height as defaultHeight', - 'sitestores.storeId', ]); + if ($hasStoreTables) { + $this->query->addSelect([ + 'purchasablesstores.basePrice as defaultBasePrice', + 'purchasablesstores.basePromotionalPrice as defaultBasePromotionalPrice', + 'sitestores.storeId', + ]); + } else { + $this->query->addSelect([ + new Expression('NULL as [[defaultBasePrice]]'), + new Expression('NULL as [[defaultBasePromotionalPrice]]'), + new Expression('NULL as [[storeId]]'), + ]); + } + // Join in sites stores to get product's store for current request - $this->query->leftJoin(['sitestores' => Table::SITESTORES], '[[elements_sites.siteId]] = [[sitestores.siteId]]'); $this->query->leftJoin(['purchasables' => Table::PURCHASABLES], '[[purchasables.id]] = [[commerce_products.defaultVariantId]]'); - $this->query->leftJoin(['purchasablesstores' => Table::PURCHASABLES_STORES], '[[purchasablesstores.purchasableId]] = [[commerce_products.defaultVariantId]] and [[sitestores.storeId]] = [[purchasablesstores.storeId]]'); + if ($hasStoreTables) { + $this->query->leftJoin(['sitestores' => Table::SITESTORES], '[[elements_sites.siteId]] = [[sitestores.siteId]]'); + $this->query->leftJoin(['purchasablesstores' => Table::PURCHASABLES_STORES], '[[purchasablesstores.purchasableId]] = [[commerce_products.defaultVariantId]] and [[sitestores.storeId]] = [[purchasablesstores.storeId]]'); + } // Tailor the query based on whether or not there is catalog pricing rules - if (Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) { + if ($hasStoreTables && Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) { $this->query->addSelect(['subquery.price as defaultPrice']); $this->subQuery->addSelect(['catalogprices.price']); if (isset($this->defaultPrice)) { $this->subQuery->andWhere(Db::parseParam('catalogprices.price', $this->defaultPrice)); } - } else { + } elseif ($hasStoreTables) { $this->query->addSelect(['purchasablesstores.basePrice as defaultPrice']); if (isset($this->defaultPrice)) { $this->subQuery->andWhere(Db::parseParam('purchasablesstores.basePrice', $this->defaultPrice)); } + } else { + $this->query->addSelect([new Expression('NULL as [[defaultPrice]]')]); + + if (isset($this->defaultPrice)) { + return false; + } } if (isset($this->postDate)) { @@ -1053,4 +1079,12 @@ protected function cacheTags(): array return $tags; } + + private function _storeTablesExist(): bool + { + $db = Craft::$app->getDb(); + + return $db->tableExists(Table::SITESTORES) && + $db->tableExists(Table::PURCHASABLES_STORES); + } } diff --git a/src/elements/db/PurchasableQuery.php b/src/elements/db/PurchasableQuery.php index 0b15d89ffa..66aceca96a 100755 --- a/src/elements/db/PurchasableQuery.php +++ b/src/elements/db/PurchasableQuery.php @@ -655,11 +655,15 @@ public function onPromotion(bool|null $value = true): static protected function afterPrepare(): bool { // Store dependent related joins to the sub query need to be done after the `elements_sites` is joined in the base `ElementQuery` class. - $this->subQuery->leftJoin(['sitestores' => Table::SITESTORES], '[[elements_sites.siteId]] = [[sitestores.siteId]]'); - $this->subQuery->leftJoin(['purchasables_stores' => Table::PURCHASABLES_STORES], '[[purchasables_stores.storeId]] = [[sitestores.storeId]] AND [[purchasables_stores.purchasableId]] = [[commerce_purchasables.id]]'); + $hasStoreTables = $this->_storeTablesExist(); + + if ($hasStoreTables) { + $this->subQuery->leftJoin(['sitestores' => Table::SITESTORES], '[[elements_sites.siteId]] = [[sitestores.siteId]]'); + $this->subQuery->leftJoin(['purchasables_stores' => Table::PURCHASABLES_STORES], '[[purchasables_stores.storeId]] = [[sitestores.storeId]] AND [[purchasables_stores.purchasableId]] = [[commerce_purchasables.id]]'); + } // Only do the extra catalog pricing query join if we have catalog pricing rules. - if (Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) { + if ($hasStoreTables && Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) { $customerId = $this->forCustomer; if ($customerId === null) { $customerId = Craft::$app->getUser()->getIdentity()?->id; @@ -675,7 +679,9 @@ protected function afterPrepare(): bool $this->subQuery->leftJoin(['catalogprices' => $catalogPricesQuery], '[[catalogprices.purchasableId]] = [[commerce_purchasables.id]] AND [[catalogprices.storeId]] = [[sitestores.storeId]]'); } - $this->subQuery->leftJoin(['inventoryitems' => Table::INVENTORYITEMS], '[[inventoryitems.purchasableId]] = [[commerce_purchasables.id]]'); + if (Craft::$app->getDb()->tableExists(Table::INVENTORYITEMS)) { + $this->subQuery->leftJoin(['inventoryitems' => Table::INVENTORYITEMS], '[[inventoryitems.purchasableId]] = [[commerce_purchasables.id]]'); + } return parent::afterPrepare(); } @@ -685,6 +691,9 @@ protected function afterPrepare(): bool */ protected function beforePrepare(): bool { + $hasStoreTables = $this->_storeTablesExist(); + $hasInventoryTable = Craft::$app->getDb()->tableExists(Table::INVENTORYITEMS); + $this->joinElementTable('commerce_purchasables'); $this->query->addSelect([ 'commerce_purchasables.sku', @@ -693,25 +702,48 @@ protected function beforePrepare(): bool 'commerce_purchasables.length', 'commerce_purchasables.weight', 'commerce_purchasables.taxCategoryId', - 'purchasables_stores.availableForPurchase', - 'purchasables_stores.basePrice', - 'purchasables_stores.basePromotionalPrice', - 'purchasables_stores.freeShipping', - 'purchasables_stores.maxQty', - 'purchasables_stores.minQty', - 'purchasables_stores.inventoryTracked', - 'purchasables_stores.allowOutOfStockPurchases', - 'purchasables_stores.promotable', - 'purchasables_stores.shippingCategoryId', - 'inventoryitems.id as inventoryItemId', ]); - $this->query->leftJoin(Table::SITESTORES . ' sitestores', '[[elements_sites.siteId]] = [[sitestores.siteId]]'); - $this->query->leftJoin(Table::PURCHASABLES_STORES . ' purchasables_stores', '[[purchasables_stores.storeId]] = [[sitestores.storeId]] AND [[purchasables_stores.purchasableId]] = [[commerce_purchasables.id]]'); - $this->query->leftJoin(['inventoryitems' => Table::INVENTORYITEMS], '[[inventoryitems.purchasableId]] = [[commerce_purchasables.id]]'); + if ($hasStoreTables) { + $this->query->addSelect([ + 'purchasables_stores.availableForPurchase', + 'purchasables_stores.basePrice', + 'purchasables_stores.basePromotionalPrice', + 'purchasables_stores.freeShipping', + 'purchasables_stores.maxQty', + 'purchasables_stores.minQty', + 'purchasables_stores.inventoryTracked', + 'purchasables_stores.allowOutOfStockPurchases', + 'purchasables_stores.promotable', + 'purchasables_stores.shippingCategoryId', + ]); + + $this->query->leftJoin(Table::SITESTORES . ' sitestores', '[[elements_sites.siteId]] = [[sitestores.siteId]]'); + $this->query->leftJoin(Table::PURCHASABLES_STORES . ' purchasables_stores', '[[purchasables_stores.storeId]] = [[sitestores.storeId]] AND [[purchasables_stores.purchasableId]] = [[commerce_purchasables.id]]'); + } else { + $this->query->addSelect([ + new Expression('NULL as [[availableForPurchase]]'), + new Expression('NULL as [[basePrice]]'), + new Expression('NULL as [[basePromotionalPrice]]'), + new Expression('NULL as [[freeShipping]]'), + new Expression('NULL as [[maxQty]]'), + new Expression('NULL as [[minQty]]'), + new Expression('NULL as [[inventoryTracked]]'), + new Expression('NULL as [[allowOutOfStockPurchases]]'), + new Expression('NULL as [[promotable]]'), + new Expression('NULL as [[shippingCategoryId]]'), + ]); + } + + if ($hasInventoryTable) { + $this->query->addSelect(['inventoryitems.id as inventoryItemId']); + $this->query->leftJoin(['inventoryitems' => Table::INVENTORYITEMS], '[[inventoryitems.purchasableId]] = [[commerce_purchasables.id]]'); + } else { + $this->query->addSelect([new Expression('NULL as [[inventoryItemId]]')]); + } // Only do the extra catalog pricing query join if we have catalog pricing rules. - if (Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) { + if ($hasStoreTables && Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) { $this->query->addSelect([ 'subquery.price', 'subquery.promotionalPrice as promotionalPrice', @@ -743,7 +775,7 @@ protected function beforePrepare(): bool if (isset($this->salePrice)) { $this->subQuery->andWhere(Db::parseNumericParam('catalogprices.salePrice' , $this->salePrice)); } - } else { + } elseif ($hasStoreTables) { // If Catalog pricing rules are not being used $this->query->addSelect([ 'purchasables_stores.basePrice as price', @@ -777,6 +809,17 @@ protected function beforePrepare(): bool if (isset($this->salePrice)) { $this->subQuery->andWhere(Db::parseNumericParam(new Expression('CASE WHEN [[purchasables_stores.basePromotionalPrice]] < [[purchasables_stores.basePrice]] THEN [[purchasables_stores.basePromotionalPrice]] ELSE [[purchasables_stores.basePrice]] END') , $this->salePrice)); } + } else { + $this->query->addSelect([ + new Expression('NULL as [[price]]'), + new Expression('NULL as [[promotionalPrice]]'), + new Expression('NULL as [[salePrice]]'), + new Expression('NULL as [[catalogPricingRuleId]]'), + ]); + + if ($this->_hasStoreTableParams()) { + return false; + } } if (isset($this->sku)) { @@ -880,7 +923,7 @@ protected function beforePrepare(): bool */ public function populate($rows): array { - if (!empty($rows) && Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) { + if (!empty($rows) && $this->_storeTablesExist() && Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) { $row = ArrayHelper::firstValue($rows); $store = Plugin::getInstance()->getStores()->getStoreBySiteId($row['siteId']); $purchasableIds = ArrayHelper::getColumn($rows, 'id'); @@ -920,4 +963,25 @@ public function populate($rows): array return parent::populate($rows); } + + private function _storeTablesExist(): bool + { + $db = Craft::$app->getDb(); + + return $db->tableExists(Table::SITESTORES) && + $db->tableExists(Table::PURCHASABLES_STORES); + } + + private function _hasStoreTableParams(): bool + { + return isset($this->price) || + isset($this->promotionalPrice) || + isset($this->onPromotion) || + isset($this->salePrice) || + isset($this->stock) || + isset($this->inventoryTracked) || + isset($this->availableForPurchase) || + isset($this->shippingCategoryId) || + isset($this->hasStock); + } } From 4328f195c9ec43c16b989174ab9c64222c40d9d1 Mon Sep 17 00:00:00 2001 From: Bob Olde Hampsink Date: Wed, 3 Jun 2026 11:48:43 +0200 Subject: [PATCH 3/6] Guard purchasable detail columns during migrations --- src/elements/db/ProductQuery.php | 39 +++++++++++++++++--- src/elements/db/PurchasableQuery.php | 55 +++++++++++++++++++++++++--- 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/src/elements/db/ProductQuery.php b/src/elements/db/ProductQuery.php index a7b9381771..782f57695c 100644 --- a/src/elements/db/ProductQuery.php +++ b/src/elements/db/ProductQuery.php @@ -778,6 +778,7 @@ protected function beforePrepare(): bool { $this->_normalizeTypeId(); $hasStoreTables = $this->_storeTablesExist(); + $hasPurchasableDimensionColumns = $this->_purchasableDimensionColumnsExist(); // See if 'type' were set to invalid handles if ($this->typeId === []) { @@ -793,12 +794,24 @@ protected function beforePrepare(): bool 'commerce_products.expiryDate', 'commerce_products.defaultVariantId', 'purchasables.sku as defaultSku', - 'purchasables.weight as defaultWeight', - 'purchasables.length as defaultLength', - 'purchasables.width as defaultWidth', - 'purchasables.height as defaultHeight', ]); + if ($hasPurchasableDimensionColumns) { + $this->query->addSelect([ + 'purchasables.weight as defaultWeight', + 'purchasables.length as defaultLength', + 'purchasables.width as defaultWidth', + 'purchasables.height as defaultHeight', + ]); + } else { + $this->query->addSelect([ + new Expression('NULL as [[defaultWeight]]'), + new Expression('NULL as [[defaultLength]]'), + new Expression('NULL as [[defaultWidth]]'), + new Expression('NULL as [[defaultHeight]]'), + ]); + } + if ($hasStoreTables) { $this->query->addSelect([ 'purchasablesstores.basePrice as defaultBasePrice', @@ -852,7 +865,13 @@ protected function beforePrepare(): bool $this->_applyProductTypeIdParam(); - if (isset($this->defaultHeight) || isset($this->defaultLength) || isset($this->defaultWidth) || isset($this->defaultWeight) || isset($this->defaultSku)) { + if (isset($this->defaultHeight) || isset($this->defaultLength) || isset($this->defaultWidth) || isset($this->defaultWeight)) { + if (!$hasPurchasableDimensionColumns) { + return false; + } + + $this->subQuery->leftJoin(['purchasables' => Table::PURCHASABLES], '[[purchasables.id]] = [[commerce_products.defaultVariantId]]'); + } elseif (isset($this->defaultSku)) { $this->subQuery->leftJoin(['purchasables' => Table::PURCHASABLES], '[[purchasables.id]] = [[commerce_products.defaultVariantId]]'); } @@ -1087,4 +1106,14 @@ private function _storeTablesExist(): bool return $db->tableExists(Table::SITESTORES) && $db->tableExists(Table::PURCHASABLES_STORES); } + + private function _purchasableDimensionColumnsExist(): bool + { + $db = Craft::$app->getDb(); + + return $db->columnExists(Table::PURCHASABLES, 'weight') && + $db->columnExists(Table::PURCHASABLES, 'length') && + $db->columnExists(Table::PURCHASABLES, 'width') && + $db->columnExists(Table::PURCHASABLES, 'height'); + } } diff --git a/src/elements/db/PurchasableQuery.php b/src/elements/db/PurchasableQuery.php index 66aceca96a..70649af4da 100755 --- a/src/elements/db/PurchasableQuery.php +++ b/src/elements/db/PurchasableQuery.php @@ -693,17 +693,31 @@ protected function beforePrepare(): bool { $hasStoreTables = $this->_storeTablesExist(); $hasInventoryTable = Craft::$app->getDb()->tableExists(Table::INVENTORYITEMS); + $hasPurchasableDetailColumns = $this->_purchasableDetailColumnsExist(); $this->joinElementTable('commerce_purchasables'); $this->query->addSelect([ 'commerce_purchasables.sku', - 'commerce_purchasables.width', - 'commerce_purchasables.height', - 'commerce_purchasables.length', - 'commerce_purchasables.weight', - 'commerce_purchasables.taxCategoryId', ]); + if ($hasPurchasableDetailColumns) { + $this->query->addSelect([ + 'commerce_purchasables.width', + 'commerce_purchasables.height', + 'commerce_purchasables.length', + 'commerce_purchasables.weight', + 'commerce_purchasables.taxCategoryId', + ]); + } else { + $this->query->addSelect([ + new Expression('NULL as [[width]]'), + new Expression('NULL as [[height]]'), + new Expression('NULL as [[length]]'), + new Expression('NULL as [[weight]]'), + new Expression('NULL as [[taxCategoryId]]'), + ]); + } + if ($hasStoreTables) { $this->query->addSelect([ 'purchasables_stores.availableForPurchase', @@ -854,6 +868,10 @@ protected function beforePrepare(): bool } if (isset($this->taxCategoryId)) { + if (!$hasPurchasableDetailColumns) { + return false; + } + if ($this->taxCategoryId instanceof Query) { $taxCategoryWhere = ['exists', $this->taxCategoryId]; } else { @@ -864,6 +882,10 @@ protected function beforePrepare(): bool } if ($this->width !== false) { + if (!$hasPurchasableDetailColumns) { + return false; + } + if ($this->width === null) { $this->subQuery->andWhere(['commerce_purchasables.width' => $this->width]); } else { @@ -872,6 +894,10 @@ protected function beforePrepare(): bool } if ($this->height !== false) { + if (!$hasPurchasableDetailColumns) { + return false; + } + if ($this->height === null) { $this->subQuery->andWhere(['commerce_purchasables.height' => $this->height]); } else { @@ -880,6 +906,10 @@ protected function beforePrepare(): bool } if ($this->length !== false) { + if (!$hasPurchasableDetailColumns) { + return false; + } + if ($this->length === null) { $this->subQuery->andWhere(['commerce_purchasables.length' => $this->length]); } else { @@ -888,6 +918,10 @@ protected function beforePrepare(): bool } if ($this->weight !== false) { + if (!$hasPurchasableDetailColumns) { + return false; + } + if ($this->weight === null) { $this->subQuery->andWhere(['commerce_purchasables.weight' => $this->weight]); } else { @@ -984,4 +1018,15 @@ private function _hasStoreTableParams(): bool isset($this->shippingCategoryId) || isset($this->hasStock); } + + private function _purchasableDetailColumnsExist(): bool + { + $db = Craft::$app->getDb(); + + return $db->columnExists(Table::PURCHASABLES, 'width') && + $db->columnExists(Table::PURCHASABLES, 'height') && + $db->columnExists(Table::PURCHASABLES, 'length') && + $db->columnExists(Table::PURCHASABLES, 'weight') && + $db->columnExists(Table::PURCHASABLES, 'taxCategoryId'); + } } From cf7b800e94f2fe06bd2fe956271b9615bbbfa0e4 Mon Sep 17 00:00:00 2001 From: Bob Olde Hampsink Date: Wed, 3 Jun 2026 11:52:08 +0200 Subject: [PATCH 4/6] Backfill donation purchasable records during migration --- .../m240219_194855_donation_multi_store.php | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/migrations/m240219_194855_donation_multi_store.php b/src/migrations/m240219_194855_donation_multi_store.php index 70578f9b4b..b110e50c98 100644 --- a/src/migrations/m240219_194855_donation_multi_store.php +++ b/src/migrations/m240219_194855_donation_multi_store.php @@ -29,6 +29,8 @@ public function safeUp(): bool ->all(); foreach ($donations as $donation) { + $this->_ensureDonationPurchasable($donation); + foreach ($storeIds as $storeId) { if (PurchasableStore::findOne(['purchasableId' => $donation['id'], 'storeId' => $storeId])) { continue; @@ -57,6 +59,43 @@ public function safeUp(): bool return true; } + private function _ensureDonationPurchasable(array $donation): void + { + $purchasableExists = (new Query()) + ->from(Table::PURCHASABLES) + ->where(['id' => $donation['id']]) + ->exists(); + + if ($purchasableExists) { + return; + } + + $purchasable = [ + 'id' => $donation['id'], + 'sku' => $donation['sku'] ?: "DONATION-{$donation['id']}", + ]; + + if ($this->db->columnExists(Table::PURCHASABLES, 'description')) { + $purchasable['description'] = null; + } + + foreach (['width', 'height', 'length', 'weight'] as $column) { + if ($this->db->columnExists(Table::PURCHASABLES, $column)) { + $purchasable[$column] = 0; + } + } + + if ($this->db->columnExists(Table::PURCHASABLES, 'taxCategoryId')) { + $purchasable['taxCategoryId'] = (new Query()) + ->select('id') + ->from(Table::TAXCATEGORIES) + ->orderBy(['id' => SORT_ASC]) + ->scalar(); + } + + $this->insert(Table::PURCHASABLES, $purchasable); + } + /** * @inheritdoc */ From 7051b0a1c68389ae86ab1eafcc48d747a0252693 Mon Sep 17 00:00:00 2001 From: Bob Olde Hampsink Date: Wed, 3 Jun 2026 11:55:21 +0200 Subject: [PATCH 5/6] Remove redundant store table guard --- src/elements/db/ProductQuery.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/elements/db/ProductQuery.php b/src/elements/db/ProductQuery.php index 782f57695c..4fbabae8ca 100644 --- a/src/elements/db/ProductQuery.php +++ b/src/elements/db/ProductQuery.php @@ -753,7 +753,7 @@ protected function afterPrepare(): bool $this->subQuery->leftJoin(['sitestores' => Table::SITESTORES], '[[elements_sites.siteId]] = [[sitestores.siteId]]'); - if ($hasStoreTables && Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) { + if (Plugin::getInstance()->getCatalogPricingRules()->hasCatalogPricingRules()) { $catalogPricesQuery = Plugin::getInstance() ->getCatalogPricing() ->createCatalogPricesQuery(userId: $customerId) From f382275c7181a0da6e539ec8a8c89f241af5e740 Mon Sep 17 00:00:00 2001 From: Bob Olde Hampsink Date: Wed, 17 Jun 2026 10:00:16 +0200 Subject: [PATCH 6/6] Address migration guard review feedback --- src/elements/db/PurchasableQuery.php | 10 +++++----- .../m240219_194855_donation_multi_store.php | 13 ++++++++++++- src/services/CatalogPricingRules.php | 7 +++++-- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/elements/db/PurchasableQuery.php b/src/elements/db/PurchasableQuery.php index 70649af4da..7500bddfb5 100755 --- a/src/elements/db/PurchasableQuery.php +++ b/src/elements/db/PurchasableQuery.php @@ -736,15 +736,15 @@ protected function beforePrepare(): bool $this->query->leftJoin(Table::PURCHASABLES_STORES . ' purchasables_stores', '[[purchasables_stores.storeId]] = [[sitestores.storeId]] AND [[purchasables_stores.purchasableId]] = [[commerce_purchasables.id]]'); } else { $this->query->addSelect([ - new Expression('NULL as [[availableForPurchase]]'), + new Expression('1 as [[availableForPurchase]]'), new Expression('NULL as [[basePrice]]'), new Expression('NULL as [[basePromotionalPrice]]'), - new Expression('NULL as [[freeShipping]]'), + new Expression('0 as [[freeShipping]]'), new Expression('NULL as [[maxQty]]'), new Expression('NULL as [[minQty]]'), - new Expression('NULL as [[inventoryTracked]]'), - new Expression('NULL as [[allowOutOfStockPurchases]]'), - new Expression('NULL as [[promotable]]'), + new Expression('0 as [[inventoryTracked]]'), + new Expression('0 as [[allowOutOfStockPurchases]]'), + new Expression('0 as [[promotable]]'), new Expression('NULL as [[shippingCategoryId]]'), ]); } diff --git a/src/migrations/m240219_194855_donation_multi_store.php b/src/migrations/m240219_194855_donation_multi_store.php index b110e50c98..d5e5cff987 100644 --- a/src/migrations/m240219_194855_donation_multi_store.php +++ b/src/migrations/m240219_194855_donation_multi_store.php @@ -6,6 +6,8 @@ use craft\commerce\records\PurchasableStore; use craft\db\Migration; use craft\db\Query; +use craft\helpers\StringHelper; +use yii\base\Exception; /** * m240219_194855_donation_multi_store migration. @@ -73,6 +75,9 @@ private function _ensureDonationPurchasable(array $donation): void $purchasable = [ 'id' => $donation['id'], 'sku' => $donation['sku'] ?: "DONATION-{$donation['id']}", + 'dateCreated' => $donation['dateCreated'], + 'dateUpdated' => $donation['dateUpdated'], + 'uid' => StringHelper::UUID(), ]; if ($this->db->columnExists(Table::PURCHASABLES, 'description')) { @@ -86,11 +91,17 @@ private function _ensureDonationPurchasable(array $donation): void } if ($this->db->columnExists(Table::PURCHASABLES, 'taxCategoryId')) { - $purchasable['taxCategoryId'] = (new Query()) + $taxCategoryId = (new Query()) ->select('id') ->from(Table::TAXCATEGORIES) ->orderBy(['id' => SORT_ASC]) ->scalar(); + + if (!$taxCategoryId) { + throw new Exception('Unable to backfill donation purchasable record because no tax category exists.'); + } + + $purchasable['taxCategoryId'] = $taxCategoryId; } $this->insert(Table::PURCHASABLES, $purchasable); diff --git a/src/services/CatalogPricingRules.php b/src/services/CatalogPricingRules.php index 80fc00b3ff..8280e0d0b8 100644 --- a/src/services/CatalogPricingRules.php +++ b/src/services/CatalogPricingRules.php @@ -51,9 +51,12 @@ public function hasCatalogPricingRules(): bool return false; } + if (!Craft::$app->getDb()->tableExists(Table::CATALOG_PRICING_RULES)) { + return false; + } + if ($this->_hasCatalogPricingRules === null) { - $this->_hasCatalogPricingRules = Craft::$app->getDb()->tableExists(Table::CATALOG_PRICING_RULES) && - $this->_createCatalogPricingRuleQuery()->exists(); + $this->_hasCatalogPricingRules = $this->_createCatalogPricingRuleQuery()->exists(); } return (bool)$this->_hasCatalogPricingRules;