Problem/Motivation

Commerce changes carts without telling the customer. An order processor or event subscriber can remove an item, drop a coupon or pick up a new price during the order refresh, and nothing in the chain lets it leave a message for the customer:

  • AvailabilityOrderProcessor deletes unavailable order items silently. AvailabilityResult already carries a reason and a code, which are discarded.
  • PromotionOrderProcessor removes coupons that are no longer valid, also silently.
  • When a product's price changes after it was added to the cart, the customer only sees a different total.
  • If every item is removed, the cart page shows "Your shopping cart is empty" with no explanation, the case where one matters most.

Drupal's messenger can't carry these messages: the refresh often runs in a request the customer never sees (cron, an admin viewing the order, the cart block, another tab, an API call), so the message would reach the wrong person or no one.

Steps to reproduce

  1. Add a product to the cart.
  2. As an admin, change the product variation's price, or make it unavailable (with an availability checker, e.g. from a stock module).
  3. Reload the cart once it's due for a refresh: the price or item changed with no explanation.

Proposed resolution

Add order notices: customer-facing messages that any code changing a draft order can set, stored with the order and shown to the customer.

  • Service: commerce_order.order_notice_manager (OrderNoticeManagerInterface), with set(), resolve(), getNotices() and dismiss(). Notices are buffered and written when the order is saved, in the same transaction, so a notice is only stored if the change it describes is. Each notice has a key; an order has at most one active notice per key, so a price that changes three times shows one notice compared with the price the customer saw.
  • Storage: a dedicated commerce_order_notice table with Views data, rather than the order's data field (not queryable; dismissing would need a full, locked order save) or a content entity (an entity save per notice on every refresh). Dismissed and resolved notices are kept for as long as the order exists, for reporting (e.g. which products are most often removed as unavailable, and whether orders with a price increase still converted).
  • Built-in notices: unavailable item removed (with the checker's reason), unit price changed since the item was added, and coupon removed.
  • Language: messages and their placeholders (translatable strings, prices, entity labels) are stored untranslated and formatted when displayed, in the customer's language, since a refresh can run in another language (e.g. cron).
  • Display: the cart page shows notices above each cart, including on an emptied cart, with no configuration changes for existing sites. The cart block shows a count and lists the notices in its dropdown. Customers can dismiss notices (AJAX, with a redirect fallback).
  • Checkout: sends the customer back to the cart once if the cart has notices they haven't seen on the cart page. Only for draft, unlocked orders, so never during or after payment.
  • Settings: per order type, price change notices can be turned off (an OrderType setting), and so can the checkout redirect (a commerce_checkout third-party setting).
  • Extensibility: the OrderEvents::ORDER_NOTICE event lets sites alter or suppress notices.

Change record: https://www.drupal.org/node/3638096

Remaining tasks

  • Review the merge request.
  • Follow-up in Commerce API: expose notices on the cart resource and add a dismiss endpoint.
  • Follow-ups for express checkout buttons (payment gateway modules) and Commerce API checkout, which skip the checkout form and so the redirect.

User interface changes

  • Notices on the cart page and in the cart block, with a Dismiss link, and a notice count in the cart block summary.
  • Checkout can redirect to the cart with "Your cart has changed. Review the changes before checking out."
  • Order type form: "Tell customers when an order item price has changed" (Order refresh settings) and "Send customers back to the cart when it has changes they haven't seen" (Checkout settings).
  • Views: new "Order notices" base table.

Introduced terminology

Order notice: a persistent, customer-facing message about a change made to a draft order without the customer's involvement. Named "notice" to avoid confusion with Drupal's session-based messenger messages.

API changes

  • New: OrderNotice, OrderNoticeManagerInterface, OrderNoticeFormatterInterface, OrderNoticeEvent and OrderEvents::ORDER_NOTICE, PriceChangeOrderProcessor, CartNoticeBuilder.
  • New methods on OrderTypeInterface: shouldShowPriceChangeNotices() and setShowPriceChangeNotices().
  • New optional constructor arguments, deprecated to omit in 3.4.0: AvailabilityOrderProcessor, PromotionOrderProcessor, CartLazyBuilders.
  • New template commerce-order-notices.html.twig, and new variables in commerce-cart-block.html.twig (notices, notice_count, notice_count_indicator).

Data model changes

  • New commerce_order_notice table (commerce_order_update_10304()).
  • New priceChangeNotices property on order types (commerce_order_update_10305() sets it on existing ones), and a redirect_to_cart_on_notices third-party setting from commerce_checkout.
  • Order items store the price compared against under the price_change_baseline data key.

Release notes snippet

Commerce now tells customers when their cart changes without their involvement, such as a product removed because it's no longer available, a price change, or a coupon that's no longer valid. Notices are shown on the cart page and in the cart block, and checkout sends customers back to the cart to review changes they haven't seen. See the change record.

Issue fork commerce-3638101

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

tomtech created an issue.