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:
AvailabilityOrderProcessordeletes unavailable order items silently.AvailabilityResultalready carries a reason and a code, which are discarded.PromotionOrderProcessorremoves 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
- Add a product to the cart.
- As an admin, change the product variation's price, or make it unavailable (with an availability checker, e.g. from a stock module).
- 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), withset(),resolve(),getNotices()anddismiss(). 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_noticetable with Views data, rather than the order'sdatafield (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
OrderTypesetting), and so can the checkout redirect (acommerce_checkoutthird-party setting). - Extensibility: the
OrderEvents::ORDER_NOTICEevent 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,OrderNoticeEventandOrderEvents::ORDER_NOTICE,PriceChangeOrderProcessor,CartNoticeBuilder. - New methods on
OrderTypeInterface:shouldShowPriceChangeNotices()andsetShowPriceChangeNotices(). - New optional constructor arguments, deprecated to omit in 3.4.0:
AvailabilityOrderProcessor,PromotionOrderProcessor,CartLazyBuilders. - New template
commerce-order-notices.html.twig, and new variables incommerce-cart-block.html.twig(notices,notice_count,notice_count_indicator).
Data model changes
- New
commerce_order_noticetable (commerce_order_update_10304()). - New
priceChangeNoticesproperty on order types (commerce_order_update_10305()sets it on existing ones), and aredirect_to_cart_on_noticesthird-party setting fromcommerce_checkout. - Order items store the price compared against under the
price_change_baselinedata 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
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