Change record status: 
Project: 
Introduced in branch: 
3.x
Introduced in version: 
3.4.0
Description: 

Summary

When Commerce changes a draft order without the customer's involvement, it now tells the customer. Previously these changes were silent. Out of the box, three changes create a notice:

  • Unavailable item removed: "%title has been removed from your cart: @reason", using the reason returned by the availability checker, or "...because it is no longer available." when there is none.
  • Price changed: "The price of %title has changed from @previous_price to @price.", comparing the current unit price with the price when the item was added, or the price the customer last acknowledged. Several changes in a row show as one notice, and a price that changes back removes the notice.
  • Coupon removed: "The coupon %code is no longer valid and has been removed from your cart."

Notices are shown on the cart page (including on a cart that was emptied) and in the cart block. Customers can dismiss them, without a page reload when JavaScript is available. Checkout sends customers back to the cart once if it has notices they haven't seen yet.

Notices are stored in a new commerce_order_notice table, kept for as long as the order exists (including after the order is placed, for reporting), and exposed to Views.

Site builders

  • Price change notices can be turned off per order type: "Tell customers when an order item price has changed", in the order type's "Order refresh" settings. On by default.
  • Checkout redirect can be turned off per order type: "Send customers back to the cart when it has changes they haven't seen", in the order type's "Checkout settings". On by default. It only applies before payment: orders are locked during payment, and locked orders are never refreshed.
  • Cart block: shows a notice count next to the item count, and the notices in the dropdown. A block configured not to show for empty carts is still shown when the empty cart has notices.
  • Reporting: Views has a new "Order notices" base table, with a relationship to the order and, when Commerce Product is enabled, to the product variation. Dismissed and resolved notices are kept, so you can report on, for example, which products were most often removed from carts as unavailable, or whether orders with a price increase still converted.
  • Updates: run database updates. commerce_order_update_10304() creates the table, and commerce_order_update_10305() turns price change notices on for existing order types. Until the updates run, notices are skipped rather than causing errors.

Module developers

Setting a notice

Code that changes a draft order (order processors, event subscribers) can tell the customer through the commerce_order.order_notice_manager service (OrderNoticeManagerInterface). Notices are written when the order is saved, in the same transaction, so an order processor doesn't need to do anything else. Each notice has a key; an order has at most one active notice per key, so setting it again updates it.

use Drupal\commerce_order\OrderNotice;
use Drupal\Core\Messenger\MessengerInterface;

$this->orderNoticeManager->set($order, new OrderNotice([
  'key' => 'mymodule_gift_removed:' . $variation->id(),
  'type' => 'mymodule_gift_removed',
  'severity' => MessengerInterface::TYPE_WARNING,
  'message' => $this->t('The free gift %title was removed because your order no longer qualifies.', [
    '%title' => $variation,
  ]),
  'purchased_entity_type' => $variation->getEntityTypeId(),
  'purchased_entity_id' => $variation->id(),
]));

// When the condition no longer applies:
$this->orderNoticeManager->resolve($order, 'mymodule_gift_removed:' . $variation->id());

Messages are translated when displayed, in the customer's language, not when the notice is set (a refresh can run during cron or an admin request). Pass placeholder values as objects so they can be translated too:

  • TranslatableMarkup: stored untranslated, translated on display.
  • Price: formatted for the current locale on display.
  • An entity: shown with its label in the current language, falling back to its stored labels if it was deleted.
  • Anything else is stored as a string and shown as is.

Use the commerce_order.order_notice_formatter service (OrderNoticeFormatterInterface::formatMessage()) to get a notice's message, for example in a decoupled frontend's API.

Altering or suppressing notices

The new OrderEvents::ORDER_NOTICE event (OrderNoticeEvent) is dispatched for every notice set. Subscribers can change the notice, or set it to NULL to prevent it:

public static function getSubscribedEvents(): array {
  return [OrderEvents::ORDER_NOTICE => 'onNotice'];
}

public function onNotice(OrderNoticeEvent $event): void {
  $notice = $event->getNotice();
  // Only tell customers about price increases.
  if ($notice->getType() === 'price_changed' && $notice->getSeverity() === MessengerInterface::TYPE_STATUS) {
    $event->setNotice(NULL);
  }
}

Availability checkers

The reason returned by an availability checker (AvailabilityResult::unavailable($reason)) is now shown to customers. Return it as TranslatableMarkup (e.g. $this->t('it is temporarily out of stock')) so it is translated with the message; plain strings are shown as is.

API changes

  • New methods on OrderTypeInterface: shouldShowPriceChangeNotices(): bool and setShowPriceChangeNotices(bool $show_notices): static. Classes implementing the interface directly, rather than extending OrderType, need to add them.
  • Constructor deprecations: each class below takes a new argument. Omitting it is deprecated in commerce:3.4.0 and it is required in commerce:4.0.0. Subclasses and code instantiating these classes directly should pass it.
    • AvailabilityOrderProcessor::__construct(): $order_notice_manager (OrderNoticeManagerInterface).
    • PromotionOrderProcessor::__construct(): $orderNoticeManager (OrderNoticeManagerInterface).
    • CartLazyBuilders::__construct(): $cartNoticeBuilder (CartNoticeBuilder).
  • New services: commerce_order.order_notice_manager, commerce_order.order_notice_formatter, commerce_order.price_change_order_processor (an order processor, priority 290) and commerce_cart.cart_notice_builder.
  • Order item data: order items store the price that price changes are compared with under the price_change_baseline data key.

Themers

  • New template commerce-order-notices.html.twig (theme hook commerce_order_notices), with a default stylesheet in the commerce_order/order_notices library. The markup uses its own classes (commerce-order-notice, commerce-order-notice--{severity}, commerce-order-notice--type-{type}), not the messages classes.
  • commerce-cart-block.html.twig has new variables: notices, notice_count and notice_count_indicator. Themes that override this template need to print them to show notices in the cart block.
  • Keep the data attributes if you override the notices template: dismissing without a page reload finds elements by data-notice-id and data-order-notices, and updates the element with data-cart-notice-count.
Impacts: 
Site builders, administrators, editors
Module developers
Themers