Skip to content
87 changes: 75 additions & 12 deletions src/elements/db/ProductQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,12 @@ 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()) {
Expand All @@ -771,6 +777,8 @@ protected function afterPrepare(): bool
protected function beforePrepare(): bool
{
$this->_normalizeTypeId();
$hasStoreTables = $this->_storeTablesExist();
$hasPurchasableDimensionColumns = $this->_purchasableDimensionColumnsExist();

// See if 'type' were set to invalid handles
if ($this->typeId === []) {
Expand All @@ -784,36 +792,67 @@ 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 ($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',
'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)) {
Expand All @@ -826,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]]');
}

Expand Down Expand Up @@ -1053,4 +1098,22 @@ protected function cacheTags(): array

return $tags;
}

private function _storeTablesExist(): bool
{
$db = Craft::$app->getDb();

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');
}
}
161 changes: 135 additions & 26 deletions src/elements/db/PurchasableQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
}
Expand All @@ -685,33 +691,73 @@ protected function afterPrepare(): bool
*/
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',
'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 ($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',
'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('1 as [[availableForPurchase]]'),
new Expression('NULL as [[basePrice]]'),
new Expression('NULL as [[basePromotionalPrice]]'),
new Expression('0 as [[freeShipping]]'),
new Expression('NULL as [[maxQty]]'),
new Expression('NULL as [[minQty]]'),
new Expression('0 as [[inventoryTracked]]'),
new Expression('0 as [[allowOutOfStockPurchases]]'),
new Expression('0 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',
Expand Down Expand Up @@ -743,7 +789,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',
Expand Down Expand Up @@ -777,6 +823,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)) {
Expand Down Expand Up @@ -811,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 {
Expand All @@ -821,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 {
Expand All @@ -829,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 {
Expand All @@ -837,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 {
Expand All @@ -845,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 {
Expand Down Expand Up @@ -880,7 +957,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');
Expand Down Expand Up @@ -920,4 +997,36 @@ 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);
}

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');
}
}
Loading
Loading