Skip to content
Merged
3 changes: 3 additions & 0 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

- Added a `cart/peek-cart` controller action, which returns the existing cart for the current request without creating a new cart or setting cookies — useful for cached pages such as a header cart badge, where `Set-Cookie` responses should be avoided. ([#4263](https://github.com/craftcms/commerce/pull/4263))
- `commerce/cart/load-cart` now returns JSON responses for `application/json` requests, including a `challengeUrl` on failure.
- Improved the performance of shipping method and rule matching.

### Extensibility

Expand All @@ -42,12 +43,14 @@
- Added `craft\commerce\elements\deletionblockers\SubscriptionCustomersDeletionBlocker`.
- 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\services\Carts::getLoadCartUrl()`.
- Added `craft\commerce\services\Carts::peekCart()`.
- Added `craft\commerce\services\Orders::reassignOrders()`.
- Added `craft\commerce\services\Orders::removeCustomerData()`.
- Added `craft\commerce\services\PaymentCurrencies::EVENT_DEFINE_PAYMENT_CURRENCY_RATE`.
- Added `craft\commerce\services\PaymentCurrencies::getRateFor()`.
- Added `craft\commerce\services\ShippingRuleCategories::getAllShippingRuleCategoriesData()`.
- Added `craft\commerce\services\ProductTypes::getCreatableProductTypeIds()`.
- Added `craft\commerce\services\ProductTypes::getViewableProductTypeIds()`.
- Added `craft\commerce\services\ProductTypes::getViewableProductTypes()`.
Expand Down
28 changes: 21 additions & 7 deletions src/base/ShippingMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ abstract class ShippingMethod extends BaseModel implements ShippingMethodInterfa
*/
public ?DateTime $dateUpdated = null;

/**
* @var array<string, ShippingRuleInterface|null>
*/
private array $_matchingRuleByOrderNumber = [];

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -274,11 +279,8 @@ public function matchOrder(Order $order): bool
return false;
}

/** @var ShippingRuleInterface $rule */
foreach ($this->getShippingRules()->all() as $rule) {
if ($rule->matchOrder($order)) {
return true;
}
if ($this->getMatchingShippingRule($order)) {
return true;
}

return false;
Expand All @@ -289,14 +291,26 @@ public function matchOrder(Order $order): bool
*/
public function getMatchingShippingRule(Order $order): ?ShippingRuleInterface
{
if (array_key_exists($order->number, $this->_matchingRuleByOrderNumber)) {
return $this->_matchingRuleByOrderNumber[$order->number];
}

foreach ($this->getShippingRules() as $rule) {
/** @var ShippingRuleInterface $rule */
if ($rule->matchOrder($order)) {
return $rule;
return $this->_matchingRuleByOrderNumber[$order->number] = $rule;
}
}

return null;
return $this->_matchingRuleByOrderNumber[$order->number] = null;
}

/**
* @return void
*/
public function clearMatchingShippingRuleCache(): void
{
$this->_matchingRuleByOrderNumber = [];
}

public function getPriceForOrder(Order $order): float
Expand Down
4 changes: 2 additions & 2 deletions src/controllers/InventoryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@
use craft\commerce\models\InventoryLocation;
use craft\commerce\Plugin;
use craft\commerce\web\assets\inventory\InventoryAsset;
use craft\db\Query;
use craft\db\Table as CraftTable;
use craft\enums\MenuItemType;
use craft\errors\DeprecationException;
use craft\helpers\AdminTable;
use craft\helpers\ArrayHelper;
use craft\helpers\Cp;
use craft\helpers\Html;
use craft\db\Query;
use craft\db\Table as CraftTable;
use craft\web\assets\htmx\HtmxAsset;
use craft\web\CpScreenResponseBehavior;
use yii\base\InvalidConfigException;
Expand Down
11 changes: 8 additions & 3 deletions src/elements/Order.php
Original file line number Diff line number Diff line change
Expand Up @@ -2176,7 +2176,11 @@ public function getAvailableShippingMethodOptions(): array

// Get all regular methods and add them to the list, for use only when the order is complete.
if ($this->isCompleted) {
$allShippingMethods = ArrayHelper::index(Plugin::getInstance()->getShippingMethods()->getAllShippingMethods()->all(), fn(ShippingMethodInterface $sm) => $sm->getHandle());
$allShippingMethods = Plugin::getInstance()->getShippingMethods()->getAllShippingMethods()
->keyBy(fn(ShippingMethodInterface $sm) => $sm->getHandle())
->filter(fn(ShippingMethodInterface $sm) => $sm->getIsEnabled())
->all();

$methods = ArrayHelper::merge($allShippingMethods, $methods);
}

Expand All @@ -2198,13 +2202,14 @@ public function getAvailableShippingMethodOptions(): array
}
}

$matchesOrder = ArrayHelper::isIn($method->getHandle(), $matchingMethodHandles);
$option->setOrder($this);
$option->enabled = $method->getIsEnabled();
$option->id = $method->getId();
$option->name = $method->getName();
$option->handle = $method->getHandle();
$option->matchesOrder = ArrayHelper::isIn($method->getHandle(), $matchingMethodHandles);
$option->price = $method->getPriceForOrder($this);
$option->matchesOrder = $matchesOrder;
$option->price = $matchesOrder ? $method->getPriceForOrder($this) : 0;
$option->shippingMethod = $method;
$option->storeId = $storeId;

Expand Down
67 changes: 39 additions & 28 deletions src/models/ShippingRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,20 +117,20 @@ class ShippingRule extends Model implements ShippingRuleInterface, HasStoreInter
private ?array $_shippingRuleCategories = null;

/**
* @var ShippingRuleOrderCondition|null
* @var string|array|ShippingRuleOrderCondition|null
* @see setOrderCondition()
* @see getOrderCondition()
* @since 5.0.0
*/
private ?ShippingRuleOrderCondition $_orderCondition = null;
private ShippingRuleOrderCondition|string|array|null $_orderCondition = null;

/**
* @var ShippingRuleCustomerCondition|null
* @var string|array|ShippingRuleCustomerCondition|null
* @see setCustomerCondition()
* @see getCustomerCondition()
* @since 5.4.0
*/
private ?ShippingRuleCustomerCondition $_customerCondition = null;
private ShippingRuleCustomerCondition|string|array|null $_customerCondition = null;

/**
* @throws InvalidConfigException
Expand Down Expand Up @@ -248,17 +248,6 @@ public function setOrderCondition(ShippingRuleOrderCondition|string|array|null $
return;
}

if (is_string($condition)) {
$condition = Json::decodeIfJson($condition);
}

if (!$condition instanceof ShippingRuleOrderCondition) {
$condition['class'] = ShippingRuleOrderCondition::class;
$condition = Craft::$app->getConditions()->createCondition($condition);
/** @var ShippingRuleOrderCondition $condition */
}
$condition->forProjectConfig = false;

$this->_orderCondition = $condition;
}

Expand All @@ -268,12 +257,26 @@ public function setOrderCondition(ShippingRuleOrderCondition|string|array|null $
*/
public function getOrderCondition(): ShippingRuleOrderCondition
{
$condition = $this->_orderCondition ?? new ShippingRuleOrderCondition();
if ($this->_orderCondition instanceof ShippingRuleOrderCondition) {
return $this->_orderCondition;
}

$condition = $this->_orderCondition ?? [];
if (is_string($condition)) {
$condition = Json::decodeIfJson($condition);
}

$condition['class'] = ShippingRuleOrderCondition::class;
$condition = Craft::$app->getConditions()->createCondition($condition);
/** @var ShippingRuleOrderCondition $condition */
$condition->forProjectConfig = false;
$condition->mainTag = 'div';
$condition->name = 'orderCondition';
$condition->storeId = $this->storeId;

return $condition;
$this->_orderCondition = $condition;

return $this->_orderCondition;
}

/**
Expand All @@ -284,31 +287,39 @@ public function getOrderCondition(): ShippingRuleOrderCondition
*/
public function setCustomerCondition(ShippingRuleCustomerCondition|string|array|null $condition): void
{
if (is_string($condition)) {
$condition = Json::decodeIfJson($condition);
}

if (!$condition instanceof ShippingRuleCustomerCondition) {
$condition['class'] = ShippingRuleCustomerCondition::class;
$condition = Craft::$app->getConditions()->createCondition($condition);
/** @var ShippingRuleCustomerCondition $condition */
if (empty($condition)) {
$this->_customerCondition = null;
return;
}
$condition->forProjectConfig = false;

$this->_customerCondition = $condition;
}

/**
* @return ShippingRuleCustomerCondition
* @throws InvalidConfigException
* @since 5.4.0
*/
public function getCustomerCondition(): ShippingRuleCustomerCondition
{
$condition = $this->_customerCondition ?? new ShippingRuleCustomerCondition();
if ($this->_customerCondition instanceof ShippingRuleCustomerCondition) {
return $this->_customerCondition;
}

$condition = $this->_customerCondition ?? [];
if (is_string($condition)) {
$condition = Json::decodeIfJson($condition);
}

$condition['class'] = ShippingRuleCustomerCondition::class;
$condition = Craft::$app->getConditions()->createCondition($condition);
/** @var ShippingRuleCustomerCondition $condition */
$condition->forProjectConfig = false;
$condition->mainTag = 'div';
$condition->name = 'customerCondition';
$this->_customerCondition = $condition;

return $condition;
return $this->_customerCondition;
}

/**
Expand Down
10 changes: 8 additions & 2 deletions src/services/ShippingMethods.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,10 @@ public function getMatchingShippingMethods(Order $order): array

/** @var ShippingMethod $method */
foreach ($event->getShippingMethods() as $method) {
$totalPrice = $method->getPriceForOrder($order);

if ($method->getIsEnabled() && $method->matchOrder($order)) {
// Now we know the method matches, let's get the price
$totalPrice = $method->getPriceForOrder($order);

$matchingMethods[$method->getHandle()] = [
'method' => $method,
'price' => $totalPrice, // Store the price so we can sort on it before returning
Expand All @@ -155,6 +156,11 @@ public function getMatchingShippingMethods(Order $order): array
foreach ($matchingMethods as $shippingMethod) {
$method = $shippingMethod['method'];
$shippingMethods[$method->getHandle()] = $method; // Keep the key being the handle of the method for front-end use.

// Clear the matching cache in case things change in the future
if ($method instanceof \craft\commerce\base\ShippingMethod) {
$method->clearMatchingShippingRuleCache();
}
}

// Clear the memoized data so next time we watch to match rules, we get fresh data.
Expand Down
52 changes: 48 additions & 4 deletions src/services/ShippingRuleCategories.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,38 @@
*/
class ShippingRuleCategories extends Component
{
/**
* @var array|null
*/
private ?array $_shippingRuleCategories = null;

/**
* Returns shipping rule category data without instantiating the classes for performances purposes
*
* @return array
*/
public function getAllShippingRuleCategoriesData(): array
{
if ($this->_shippingRuleCategories === null) {
$data = $this->_createShippingRuleCategoriesQuery()->all();

if (!empty($data)) {
$ruleCategories = [];
foreach ($data as $row) {
if (!isset($ruleCategories[$row['shippingRuleId']])) {
$ruleCategories[$row['shippingRuleId']] = [];
}

$ruleCategories[$row['shippingRuleId']][$row['shippingCategoryId']] = $row;
}

$this->_shippingRuleCategories = $ruleCategories;
}
}

return $this->_shippingRuleCategories ?? [];
}

/**
* Returns an array of shipping rules categories per the rule's ID.
*
Expand All @@ -34,15 +66,22 @@ public function getShippingRuleCategoriesByRuleId(int $ruleId): array
{
$rules = [];

$rows = $this->_createShippingRuleCategoriesQuery()
->where(['shippingRuleId' => $ruleId])
->all();
$shippingRuleCategories = $this->getAllShippingRuleCategoriesData();
if (!isset($shippingRuleCategories[$ruleId])) {
return [];
}

foreach ($shippingRuleCategories[$ruleId] as $row) {
if ($row instanceof ShippingRuleCategory) {
continue;
}

foreach ($rows as $row) {
$id = $row['shippingCategoryId'];
$rules[$id] = new ShippingRuleCategory($row);
}

$this->_shippingRuleCategories[$ruleId] = $rules;

return $rules;
}

Expand Down Expand Up @@ -110,6 +149,8 @@ public function createShippingRuleCategory(ShippingRuleCategory $model, bool $ru
// Now that we have a record ID, save it on the model
$model->id = $record->id;

$this->_shippingRuleCategories = null;

return true;
}

Expand All @@ -127,6 +168,9 @@ public function deleteShippingRuleCategoryById(int $id): bool
$record = ShippingRuleCategoryRecord::findOne($id);

if ($record) {
// Clear cache if required
$this->_shippingRuleCategories = null;

return (bool)$record->delete();
}

Expand Down
Loading