From bdca36a7840bde676cfae05a08b5dabf87fe54e9 Mon Sep 17 00:00:00 2001 From: agchan12 Date: Mon, 20 Jul 2026 19:42:18 +1000 Subject: [PATCH] fix(inventory): BACK-786 Fallback to the default backorder message ID when backorder message ID is null --- CHANGELOG.md | 1 + .../theme/common/picklist-backorder.spec.js | 66 ++++++++++ .../theme/common/product-details-base.spec.js | 118 ++++++++++++++++++ assets/js/theme/common/picklist-backorder.js | 5 +- .../js/theme/common/product-details-base.js | 5 +- .../js/theme/common/utils/backorder-utils.js | 6 + 6 files changed, 197 insertions(+), 4 deletions(-) create mode 100644 assets/js/theme/common/utils/backorder-utils.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 11c5e2f197..f42b76cbd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Draft +- Fall back to the store's default backorder message on PDP when a product has no explicit message assignment or references a deleted message [#2708](https://github.com/bigcommerce/cornerstone/pull/2708) - Decouple "Show backorder message" from "Qty on Backorder" settings so each works independently on PDP [#2701](https://github.com/bigcommerce/cornerstone/pull/2701) ## 6.20.0 (07-02-2026) diff --git a/assets/js/test-unit/theme/common/picklist-backorder.spec.js b/assets/js/test-unit/theme/common/picklist-backorder.spec.js index 9e6a518f09..6e58d0cdf2 100644 --- a/assets/js/test-unit/theme/common/picklist-backorder.spec.js +++ b/assets/js/test-unit/theme/common/picklist-backorder.spec.js @@ -374,6 +374,72 @@ describe('PicklistBackorder', () => { expect($items.eq(0).text()).not.toContain('|'); }); + it('falls back to the default message when backorder_message_id is null', () => { + $scope = buildScope(oneAttr('Bundle 1', 98, 'opt')); + const renderer = new PicklistBackorder($scope, { + ...context, + backorderMessages: [ + { id: 1, message: 'Ships in 2 weeks', is_default: true }, + { id: 2, message: 'Backorder message 1' }, + ], + }); + + renderer.render({ + selected_picklist_options: [selection()], + picklist_products_details: [detail({ + available_on_hand: 0, + available_for_backorder: 10, + backorder_message_id: null, + })], + }, 5); + + const $items = $('[data-picklist-backorder-list] li', $scope); + expect($items.length).toBe(1); + expect($items.eq(0).text()).toContain('Ships in 2 weeks'); + }); + + it('falls back to the default message when backorder_message_id is a dangling reference', () => { + $scope = buildScope(oneAttr('Bundle 1', 98, 'opt')); + const renderer = new PicklistBackorder($scope, { + ...context, + backorderMessages: [ + { id: 1, message: 'Ships in 2 weeks', is_default: true }, + { id: 2, message: 'Backorder message 1' }, + ], + }); + + renderer.render({ + selected_picklist_options: [selection()], + picklist_products_details: [detail({ + available_on_hand: 0, + available_for_backorder: 10, + backorder_message_id: 9999, + })], + }, 5); + + const $items = $('[data-picklist-backorder-list] li', $scope); + expect($items.length).toBe(1); + expect($items.eq(0).text()).toContain('Ships in 2 weeks'); + }); + + it('shows nothing when backorder_message_id is null and the default message is blank', () => { + $scope = buildScope(oneAttr('Bundle 1', 98, 'opt')); + const renderer = new PicklistBackorder($scope, context); + + renderer.render({ + selected_picklist_options: [selection()], + picklist_products_details: [detail({ + available_on_hand: 0, + available_for_backorder: 10, + backorder_message_id: null, + })], + }, 5); + + const $items = $('[data-picklist-backorder-list] li', $scope); + expect($items.length).toBe(1); + expect($items.eq(0).text()).not.toContain('|'); + }); + it('omits the message suffix when context.backorderMessages is missing', () => { $scope = buildScope(oneAttr('Bundle 1', 98, 'opt')); const renderer = new PicklistBackorder( diff --git a/assets/js/test-unit/theme/common/product-details-base.spec.js b/assets/js/test-unit/theme/common/product-details-base.spec.js index 638ea3e174..83c4aa4ea0 100644 --- a/assets/js/test-unit/theme/common/product-details-base.spec.js +++ b/assets/js/test-unit/theme/common/product-details-base.spec.js @@ -134,6 +134,124 @@ describe('ProductDetailsBase.updateAddToCartForQty()', () => { }); }); +describe('ProductDetailsBase.updateBackorderMessage()', () => { + let $scope; + + const backorderMessages = [ + { id: 1, message: 'Ships in 2 weeks', is_default: true }, + { id: 2, message: 'Custom delay notice' }, + ]; + + const buildBackorderScope = () => $(` +
+
+ +
+
+ `).appendTo(document.body); + + const makeBackorderInstance = (context, backorderedQty) => { + const obj = Object.create(ProductDetailsBase.prototype); + obj.$scope = $scope; + obj.context = context; + obj.backorderedQty = backorderedQty; + return obj; + }; + + const backorderViewModel = () => ({ + $backordered: $('[data-product-backordered]', $scope), + $backorderMessage: $('[data-backorder-message]', $scope), + }); + + afterEach(() => { + if ($scope) { + $scope.remove(); + $scope = null; + } + }); + + it('shows the assigned message when backorder_message_id matches', () => { + $scope = buildBackorderScope(); + const instance = makeBackorderInstance({ + showBackorderMessage: true, + backorderMessages, + backorderMessageId: 2, + }, 5); + + instance.updateBackorderMessage(backorderViewModel()); + + expect($('[data-backorder-message]', $scope).text()).toBe('Custom delay notice'); + }); + + it('falls back to the default message when backorder_message_id is null', () => { + $scope = buildBackorderScope(); + const instance = makeBackorderInstance({ + showBackorderMessage: true, + backorderMessages, + backorderMessageId: null, + }, 5); + + instance.updateBackorderMessage(backorderViewModel()); + + expect($('[data-backorder-message]', $scope).text()).toBe('Ships in 2 weeks'); + }); + + it('falls back to the default message when backorder_message_id is a dangling reference', () => { + $scope = buildBackorderScope(); + const instance = makeBackorderInstance({ + showBackorderMessage: true, + backorderMessages, + backorderMessageId: 9999, + }, 5); + + instance.updateBackorderMessage(backorderViewModel()); + + expect($('[data-backorder-message]', $scope).text()).toBe('Ships in 2 weeks'); + }); + + it('shows nothing when backorder_message_id is null and the default message is blank', () => { + $scope = buildBackorderScope(); + const instance = makeBackorderInstance({ + showBackorderMessage: true, + backorderMessages: [ + { id: 1, message: '', is_default: true }, + { id: 2, message: 'Custom delay notice' }, + ], + backorderMessageId: null, + }, 5); + + instance.updateBackorderMessage(backorderViewModel()); + + expect($('[data-backorder-message]', $scope).text()).toBe(''); + }); + + it('shows nothing when showBackorderMessage is false', () => { + $scope = buildBackorderScope(); + const instance = makeBackorderInstance({ + showBackorderMessage: false, + backorderMessages, + backorderMessageId: 2, + }, 5); + + instance.updateBackorderMessage(backorderViewModel()); + + expect($('[data-backorder-message]', $scope).text()).toBe(''); + }); + + it('clears the message when backorderedQty is zero', () => { + $scope = buildBackorderScope(); + const instance = makeBackorderInstance({ + showBackorderMessage: true, + backorderMessages, + backorderMessageId: 2, + }, 0); + + instance.updateBackorderMessage(backorderViewModel()); + + expect($('[data-backorder-message]', $scope).text()).toBe(''); + }); +}); + describe('ProductDetailsBase.reselectHiddenSelectedValues', () => { let $scope; diff --git a/assets/js/theme/common/picklist-backorder.js b/assets/js/theme/common/picklist-backorder.js index 6b05387301..1bef3e4349 100644 --- a/assets/js/theme/common/picklist-backorder.js +++ b/assets/js/theme/common/picklist-backorder.js @@ -1,3 +1,5 @@ +import { findByBackorderMessageIdOrDefault } from './utils/backorder-utils'; + export default class PicklistBackorder { constructor($scope, context) { this.$scope = $scope; @@ -172,13 +174,12 @@ export default class PicklistBackorder { if (!detail) return ''; const messageId = detail.backorder_message_id; - if (messageId == null) return ''; const { backorderMessages, showBackorderMessage } = this.context; if (!showBackorderMessage) return ''; if (!Array.isArray(backorderMessages)) return ''; - const messageObj = backorderMessages.find(m => m.id === messageId); + const messageObj = findByBackorderMessageIdOrDefault(backorderMessages, messageId); return messageObj && messageObj.message ? messageObj.message : ''; } } diff --git a/assets/js/theme/common/product-details-base.js b/assets/js/theme/common/product-details-base.js index eacef0a195..000afe0cf7 100644 --- a/assets/js/theme/common/product-details-base.js +++ b/assets/js/theme/common/product-details-base.js @@ -1,6 +1,7 @@ import Wishlist from '../wishlist'; import { initRadioOptions } from './aria'; import PicklistBackorder from './picklist-backorder'; +import { findByBackorderMessageIdOrDefault } from './utils/backorder-utils'; const optionsTypesMap = { INPUT_FILE: 'input-file', @@ -471,8 +472,8 @@ export default class ProductDetailsBase { const { showBackorderMessage, backorderMessages, backorderMessageId } = this.context; - if (showBackorderMessage && backorderMessageId != null && Array.isArray(backorderMessages)) { - const messageObj = backorderMessages.find(m => m.id === backorderMessageId); + if (showBackorderMessage && Array.isArray(backorderMessages)) { + const messageObj = findByBackorderMessageIdOrDefault(backorderMessages, backorderMessageId); if (messageObj) { viewModel.$backorderMessage.text(messageObj.message); return; diff --git a/assets/js/theme/common/utils/backorder-utils.js b/assets/js/theme/common/utils/backorder-utils.js new file mode 100644 index 0000000000..38fbe350fa --- /dev/null +++ b/assets/js/theme/common/utils/backorder-utils.js @@ -0,0 +1,6 @@ +export function findByBackorderMessageIdOrDefault(messages, id) { + const assignedMessage = messages.find(m => m.id === id); + const defaultMessage = messages.find(m => m.is_default); + + return assignedMessage ?? defaultMessage; +}