Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
73c108e
WIP consolidate catalog pricing jobs
nfourtythree Apr 14, 2026
d0d83c7
Merge branch '5.x' into nathaniel/pt-2823-5x-catalog-pricing-doesnt-u…
nfourtythree Apr 15, 2026
d0dc031
Simplify consolidation code
nfourtythree Apr 15, 2026
1ce41fa
Merge branch '5.x' into nathaniel/pt-2823-5x-catalog-pricing-doesnt-u…
nfourtythree Apr 15, 2026
87b6c73
Merge branch '5.7' into nathaniel/pt-2823-5x-catalog-pricing-doesnt-u…
nfourtythree Apr 15, 2026
1954beb
More tidying and changelog
nfourtythree Apr 15, 2026
153b001
Fix install migration
nfourtythree Apr 15, 2026
826ad9f
Make sure upgrade migration and install match
nfourtythree Apr 15, 2026
99b8e57
rector tidy
nfourtythree Apr 15, 2026
5494520
Merge branch '5.7' into nathaniel/pt-2823-5x-catalog-pricing-doesnt-u…
nfourtythree Apr 16, 2026
8513bac
Fix change of rule IDs not being queued
nfourtythree Apr 16, 2026
40de3f2
Tweak method names
nfourtythree Apr 16, 2026
5b731f8
Initialise variables early to make sure they exist
nfourtythree Apr 16, 2026
dcd4bc2
Fix queuing of IDs
nfourtythree Apr 16, 2026
50832fd
Merge branch 'nathaniel/pt-2823-5x-catalog-pricing-doesnt-update-for-…
nfourtythree Apr 16, 2026
feee469
Throw on mutex failure
nfourtythree Apr 16, 2026
fae1fc9
Merge branch 'nathaniel/pt-2823-5x-catalog-pricing-doesnt-update-for-…
nfourtythree Apr 16, 2026
0a82a27
Add test and fixes
nfourtythree Apr 16, 2026
2443f55
fix cs
nfourtythree Apr 16, 2026
9c38dd0
Merge branch '5.7' into nathaniel/pt-2823-5x-catalog-pricing-doesnt-u…
nfourtythree May 13, 2026
6aae0fb
Merge branch '5.7' into nathaniel/pt-2823-5x-catalog-pricing-doesnt-u…
nfourtythree May 20, 2026
1f4a63c
Rename catalog pricing queue row methods to match changelog
lukeholder Jun 3, 2026
81ee442
Load catalog pricing rules across stores when queue row has no storeId
lukeholder Jun 3, 2026
078f34e
Tighten exception types and test assertions for catalog pricing queue
lukeholder Jun 3, 2026
f840f71
Merge branch '5.7' into nathaniel/pt-2823-5x-catalog-pricing-doesnt-u…
nfourtythree Jun 3, 2026
f32f4ea
Revert "Load catalog pricing rules across stores when queue row has n…
nfourtythree Jun 3, 2026
54f5126
fix cs
nfourtythree Jun 3, 2026
d992aa9
Merge branch '5.7' into nathaniel/pt-2823-5x-catalog-pricing-doesnt-u…
nfourtythree Jun 3, 2026
91520bb
Merge branch '5.7' into nathaniel/pt-2823-5x-catalog-pricing-doesnt-u…
nfourtythree Jun 3, 2026
cb3b0bc
Fix cs
nfourtythree Jun 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

### Extensibility

- Added `craft\commerce\base\ShippingMethod::clearMatchingShippingRuleCache()`.
- Added `craft\commerce\controllers\CartController::actionCartChallenge()`.
- Added `craft\commerce\controllers\CartController::actionCartSent()`.
- Added `craft\commerce\controllers\CartController::actionEmailChallenge()`.
Expand All @@ -33,6 +34,7 @@
- Added `craft\commerce\controllers\OrdersController::actionRemoveCustomerDataModal()`.
- Added `craft\commerce\controllers\SubscriptionsController::actionDeleteSubscriptions()`.
- Added `craft\commerce\controllers\SubscriptionsController::actionDeleteSubscriptionsModal()`.
- Added `craft\commerce\db\Table::CATALOG_PRICING_QUEUE`.
- Added `craft\commerce\elements\Order::getCustomerDeleted()`.
- Added `craft\commerce\elements\Order::hasPurchasables()`.
- Added `craft\commerce\elements\Order::setCustomerDeleted()`.
Expand All @@ -42,10 +44,17 @@
- Added `craft\commerce\elements\deletionblockers\OrderCustomersDeletionBlocker`.
- Added `craft\commerce\elements\deletionblockers\SubscriptionCustomersDeletionBlocker`.
- Added `craft\commerce\enums\ContainsPurchasablesMatch`.
- Added `craft\commerce\elements\db\OrderQuery::$containsPurchasables`.
- Added `craft\commerce\elements\db\OrderQuery::containsPurchasables()`.
- Added `craft\commerce\elements\Order::hasPurchasables()`.
- Added `craft\commerce\enums\ContainsPurchasablesMatch`.
- Added `craft\commerce\events\PaymentCurrencyRateEvent`, allowing plugins to override a payment currency's exchange rate at the point of use.
- Added `craft\commerce\base\ShippingMethod::clearMatchingShippingRuleCache()`.
- Added `craft\commerce\records\CatalogPricingQueue`.
- Added `craft\commerce\services\Carts::getLoadCartUrl()`.
- Added `craft\commerce\services\Carts::peekCart()`.
- Added `craft\commerce\services\CatalogPricing::reserveCatalogPricingQueueRow()`.
- Added `craft\commerce\services\CatalogPricing::releaseCatalogPricingQueueRowById()`.
- Added `craft\commerce\services\CatalogPricing::deleteCatalogPricingQueueRowById()`.
- Added `craft\commerce\services\Orders::reassignOrders()`.
- Added `craft\commerce\services\Orders::removeCustomerData()`.
- Added `craft\commerce\services\PaymentCurrencies::EVENT_DEFINE_PAYMENT_CURRENCY_RATE`.
Expand Down
3 changes: 1 addition & 2 deletions src/controllers/DownloadsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ private function renderEmailChallenge(
bool $inline,
array $errors = [],
?string $email = null,
): Response
{
): Response {
$params = [
'order' => $order,
'orderNumber' => $orderNumber,
Expand Down
3 changes: 3 additions & 0 deletions src/db/Table.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,7 @@ abstract class Table
public const INVENTORYLOCATIONS = '{{%commerce_inventorylocations}}';
public const INVENTORYLOCATIONS_STORES = '{{%commerce_inventorylocations_stores}}';
public const INVENTORYTRANSACTIONS = '{{%commerce_inventorytransactions}}';

/** @since 5.7.0 */
public const CATALOG_PRICING_QUEUE = '{{%commerce_catalogpricing_queue}}';
}
16 changes: 16 additions & 0 deletions src/migrations/Install.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use craft\commerce\models\SiteStore;
use craft\commerce\models\Store;
use craft\commerce\Plugin;
use craft\commerce\records\CatalogPricingQueue;
use craft\commerce\records\CatalogPricingRule;
use craft\commerce\records\InventoryLocation;
use craft\commerce\records\TaxCategory;
Expand Down Expand Up @@ -132,6 +133,18 @@ public function createTables(): void
'uid' => $this->uid(),
]);

$this->archiveTableIfExists(Table::CATALOG_PRICING_QUEUE);
$this->createTable(Table::CATALOG_PRICING_QUEUE, [
'id' => $this->primaryKey(),
'storeId' => $this->integer(),
'type' => $this->enum('type', [CatalogPricingQueue::TYPE_PURCHASABLE, CatalogPricingQueue::TYPE_RULE])->notNull(),
'ids' => $this->mediumText(),
'reserved' => $this->boolean()->notNull()->defaultValue(false),
'dateCreated' => $this->dateTime()->notNull(),
'dateUpdated' => $this->dateTime()->notNull(),
'uid' => $this->uid(),
]);

$this->archiveTableIfExists(Table::CUSTOMERS);
$this->createTable(Table::CUSTOMERS, [
'id' => $this->primaryKey(), // Not used in v4 but is the old customerId
Expand Down Expand Up @@ -1091,6 +1104,8 @@ public function createIndexes(): void
$this->createIndex(null, Table::CATALOG_PRICING, ['purchasableId', 'storeId', 'isPromotionalPrice', 'price', 'catalogPricingRuleId', 'dateFrom', 'dateTo'], false);
$this->createIndex(null, Table::CATALOG_PRICING, ['purchasableId', 'storeId', 'isPromotionalPrice', 'price'], false);
$this->createIndex(null, Table::CATALOG_PRICING, ['purchasableId', 'storeId'], false);
$this->createIndex(null, Table::CATALOG_PRICING_QUEUE, 'reserved', false);
$this->createIndex(null, Table::CATALOG_PRICING_QUEUE, ['storeId', 'type', 'reserved'], false);
$this->createIndex(null, Table::CATALOG_PRICING_RULES, 'storeId', false);
$this->createIndex(null, Table::CATALOG_PRICING_RULES_USERS, 'catalogPricingRuleId', false);
$this->createIndex(null, Table::CATALOG_PRICING_RULES_USERS, 'userId', false);
Expand Down Expand Up @@ -1215,6 +1230,7 @@ public function addForeignKeys(): void
$this->addForeignKey(null, Table::CATALOG_PRICING, ['purchasableId'], Table::PURCHASABLES, ['id'], 'CASCADE', 'CASCADE');
$this->addForeignKey(null, Table::CATALOG_PRICING, ['storeId'], Table::STORES, ['id'], 'CASCADE');
$this->addForeignKey(null, Table::CATALOG_PRICING, ['userId'], CraftTable::USERS, ['id'], 'CASCADE');
$this->addForeignKey(null, Table::CATALOG_PRICING_QUEUE, ['storeId'], Table::STORES, ['id'], 'CASCADE', 'CASCADE');
$this->addForeignKey(null, Table::CATALOG_PRICING_RULES, ['storeId'], Table::STORES, ['id'], 'CASCADE', 'CASCADE');
$this->addForeignKey(null, Table::CATALOG_PRICING_RULES_USERS, ['catalogPricingRuleId'], Table::CATALOG_PRICING_RULES, ['id'], 'CASCADE', 'CASCADE');
$this->addForeignKey(null, Table::CATALOG_PRICING_RULES_USERS, ['userId'], CraftTable::USERS, ['id'], 'CASCADE', 'CASCADE');
Expand Down
47 changes: 47 additions & 0 deletions src/migrations/m260407_000000_add_catalog_pricing_queue_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace craft\commerce\migrations;

use craft\commerce\db\Table;
use craft\commerce\records\CatalogPricingQueue;
use craft\db\Migration;

/**
* m260407_000000_add_catalog_pricing_queue_table migration.
*/
class m260407_000000_add_catalog_pricing_queue_table extends Migration
{
/**
* @inheritdoc
*/
public function safeUp(): bool
{
if (!$this->db->tableExists(Table::CATALOG_PRICING_QUEUE)) {
$this->createTable(Table::CATALOG_PRICING_QUEUE, [
'id' => $this->primaryKey(),
'storeId' => $this->integer(),
'type' => $this->enum('type', [CatalogPricingQueue::TYPE_PURCHASABLE, CatalogPricingQueue::TYPE_RULE])->notNull(),
'ids' => $this->mediumText(),
'reserved' => $this->boolean()->notNull()->defaultValue(false),
'dateCreated' => $this->dateTime()->notNull(),
'dateUpdated' => $this->dateTime()->notNull(),
'uid' => $this->uid(),
]);
}

$this->createIndexIfMissing(Table::CATALOG_PRICING_QUEUE, 'reserved', false);
$this->createIndexIfMissing(Table::CATALOG_PRICING_QUEUE, ['storeId', 'type', 'reserved'], false);
$this->addForeignKey(null, Table::CATALOG_PRICING_QUEUE, ['storeId'], Table::STORES, ['id'], 'CASCADE', 'CASCADE');

return true;
}

/**
* @inheritdoc
*/
public function safeDown(): bool
{
echo "m260407_000000_add_catalog_pricing_queue_table cannot be reverted.\n";
return false;
}
}
52 changes: 48 additions & 4 deletions src/queue/jobs/CatalogPricing.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace craft\commerce\queue\jobs;

use craft\commerce\Plugin;
use craft\commerce\records\CatalogPricingQueue as CatalogPricingQueueRecord;
use craft\queue\BaseJob;

class CatalogPricing extends BaseJob
Expand All @@ -29,14 +30,57 @@ class CatalogPricing extends BaseJob

public function execute($queue): void
{
$catalogPricingService = Plugin::getInstance()->getCatalogPricing();
$isConsolidatedJob = $this->storeId === null && $this->purchasableIds === null && $this->catalogPricingRuleIds === null;
$catalogPricingRules = null;
if (!empty($this->catalogPricingRuleIds) && $this->storeId) {
$catalogPricingRules = Plugin::getInstance()->getCatalogPricingRules()->getAllCatalogPricingRules($this->storeId)->whereIn('id', $this->catalogPricingRuleIds)->all();
$reservedRowId = null;

// @TODO: remove these properties and behaviour at next breaking change
$storeId = $this->storeId;
$purchasableIds = $this->purchasableIds;
$catalogPricingRuleIds = $this->catalogPricingRuleIds;

if ($isConsolidatedJob) {
// New method of processing catalog pricing via queue table: reserve a row and process based on its type and IDs
$reservedRecord = $catalogPricingService->reserveCatalogPricingQueueRow();

if (!$reservedRecord) {
return;
}

$reservedRowId = $reservedRecord->id;
$storeId = $reservedRecord->storeId;

if ($reservedRecord->type === CatalogPricingQueueRecord::TYPE_PURCHASABLE) {
// Specific purchasable IDs: regenerate against all applicable rules
$purchasableIds = $reservedRecord->getIds();
} elseif ($reservedRecord->type === CatalogPricingQueueRecord::TYPE_RULE) {
$catalogPricingRuleIds = $reservedRecord->getIds();
} else {
throw new \UnexpectedValueException("Unrecognized catalog pricing queue row type: {$reservedRecord->type}");
}
}

Plugin::getInstance()->getCatalogPricing()->generateCatalogPrices($this->purchasableIds, $catalogPricingRules, queue: $queue);
if (!empty($catalogPricingRuleIds)) {
$catalogPricingRules = Plugin::getInstance()->getCatalogPricingRules()
->getAllCatalogPricingRules($storeId)
->whereIn('id', $catalogPricingRuleIds)
->all();
}
Comment thread
nfourtythree marked this conversation as resolved.

try {
$catalogPricingService->generateCatalogPrices($purchasableIds, $catalogPricingRules, queue: $queue);

Plugin::getInstance()->getCatalogPricing()->clearCatalogPricingJob($this);
if ($reservedRowId) {
$catalogPricingService->deleteCatalogPricingQueueRowById($reservedRowId);
}
} catch (\Throwable $e) {
if ($reservedRowId) {
$catalogPricingService->releaseCatalogPricingQueueRowById($reservedRowId);
}

throw $e;
}
}

protected function defaultDescription(): ?string
Expand Down
84 changes: 84 additions & 0 deletions src/records/CatalogPricingQueue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/

namespace craft\commerce\records;

use craft\commerce\db\Table;
use craft\db\ActiveRecord;
use craft\helpers\Json;
use yii\db\ActiveQueryInterface;

/**
* Catalog Pricing Queue record.
*
* @property int $id
* @property int|null $storeId
* @property string $type
* @property array|null $ids
* @property bool $reserved
* @property \DateTime $dateCreated
* @property \DateTime $dateUpdated
* @property string $uid
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
* @since 5.7.0
*/
class CatalogPricingQueue extends ActiveRecord
{
/**
* Row type for purchasable-ID-based catalog pricing work.
*/
public const TYPE_PURCHASABLE = 'purchasable';

/**
* Row type for rule-ID-based (or full-regeneration) catalog pricing work.
*/
public const TYPE_RULE = 'rule';

/**
* @inheritdoc
*/
public static function tableName(): string
{
return Table::CATALOG_PRICING_QUEUE;
}

/**
* Returns the decoded IDs array from the JSON column value.
*
* @return array|null
*/
public function getIds(): ?array
{
$raw = $this->getAttribute('ids');

if ($raw === null || $raw === '') {
return null;
}

$decoded = Json::decodeIfJson($raw);

return is_array($decoded) ? $decoded : null;
}

/**
* Encodes the IDs array to JSON and stores it in the column.
*
* @param array|null $ids
*/
public function setIds(?array $ids): void
{
$this->setAttribute('ids', $ids !== null ? Json::encode($ids) : null);
}

/**
* @return ActiveQueryInterface
*/
public function getStore(): ActiveQueryInterface
{
return $this->hasOne(Store::class, ['id' => 'storeId']);
}
}
Loading
Loading