@pisell/pisellos 2.2.314 → 2.2.315

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +3 -1
  3. package/dist/model/strategy/adapter/itemRule/adapter.d.ts +3 -1
  4. package/dist/model/strategy/adapter/itemRule/adapter.js +74 -7
  5. package/dist/model/strategy/adapter/itemRule/evaluator.d.ts +3 -1
  6. package/dist/model/strategy/adapter/itemRule/evaluator.js +13 -0
  7. package/dist/model/strategy/adapter/itemRule/type.d.ts +40 -1
  8. package/dist/model/strategy/adapter/itemRule/type.js +10 -0
  9. package/dist/modules/BookingContext/utils/productExtend.d.ts +7 -0
  10. package/dist/modules/BookingContext/utils/productExtend.js +62 -18
  11. package/dist/modules/BookingContext/utils/resources.js +1 -1
  12. package/dist/modules/BusinessAvailability/index.d.ts +30 -0
  13. package/dist/modules/BusinessAvailability/index.js +204 -0
  14. package/dist/modules/Cart/index.js +4 -3
  15. package/dist/modules/Cart/types.d.ts +2 -0
  16. package/dist/modules/Cart/utils/cartProduct.d.ts +3 -1
  17. package/dist/modules/Cart/utils/cartProduct.js +59 -32
  18. package/dist/modules/FulfillmentTiming/index.d.ts +11 -0
  19. package/dist/modules/FulfillmentTiming/index.js +407 -0
  20. package/dist/modules/FulfillmentTiming/types.d.ts +67 -0
  21. package/dist/modules/FulfillmentTiming/types.js +1 -0
  22. package/dist/modules/OpenData/types.d.ts +14 -0
  23. package/dist/modules/Order/index.d.ts +14 -2
  24. package/dist/modules/Order/index.js +856 -486
  25. package/dist/modules/Order/localEftposState.d.ts +13 -0
  26. package/dist/modules/Order/localEftposState.js +39 -0
  27. package/dist/modules/Order/pickupTiming.d.ts +46 -0
  28. package/dist/modules/Order/pickupTiming.js +53 -0
  29. package/dist/modules/Order/types.d.ts +30 -2
  30. package/dist/modules/Order/utils.js +25 -7
  31. package/dist/modules/Payment/index.js +9 -10
  32. package/dist/modules/Product/types.d.ts +11 -0
  33. package/dist/modules/ProductList/index.js +1 -1
  34. package/dist/modules/ResourcePlanner/planner.d.ts +2 -0
  35. package/dist/modules/ResourcePlanner/planner.js +332 -23
  36. package/dist/modules/ResourcePlanner/types.d.ts +29 -0
  37. package/dist/modules/Schedule/index.d.ts +8 -0
  38. package/dist/modules/Schedule/index.js +141 -39
  39. package/dist/modules/index.d.ts +1 -0
  40. package/dist/modules/index.js +1 -0
  41. package/dist/plugins/request.d.ts +7 -0
  42. package/dist/server/index.d.ts +1 -0
  43. package/dist/server/index.js +1536 -1309
  44. package/dist/server/modules/form/index.d.ts +19 -0
  45. package/dist/server/modules/form/index.js +205 -0
  46. package/dist/server/modules/form/types.d.ts +13 -0
  47. package/dist/server/modules/form/types.js +3 -0
  48. package/dist/server/modules/index.d.ts +3 -0
  49. package/dist/server/modules/index.js +4 -0
  50. package/dist/server/modules/order/index.d.ts +15 -0
  51. package/dist/server/modules/order/index.js +920 -688
  52. package/dist/server/modules/order/types.d.ts +1 -0
  53. package/dist/server/test.json +1 -1
  54. package/dist/server/utils/small-ticket.d.ts +1 -85
  55. package/dist/server/utils/small-ticket.js +2 -2542
  56. package/dist/shared/payment/loadPaymentMethods.d.ts +9 -0
  57. package/dist/shared/payment/loadPaymentMethods.js +66 -0
  58. package/dist/shared/receipt/customerInvoiceLanguages.d.ts +8 -0
  59. package/dist/shared/receipt/customerInvoiceLanguages.js +60 -0
  60. package/dist/shared/receipt/loadCustomerInvoiceLayout.d.ts +27 -0
  61. package/dist/shared/receipt/loadCustomerInvoiceLayout.js +184 -0
  62. package/dist/shared/receipt/small-ticket.d.ts +96 -0
  63. package/dist/shared/receipt/small-ticket.js +2751 -0
  64. package/dist/shared/receipt/templateVisibility.d.ts +23 -0
  65. package/dist/shared/receipt/templateVisibility.js +64 -0
  66. package/dist/shared/sales/loadSalesOrderSnapshot.d.ts +9 -0
  67. package/dist/shared/sales/loadSalesOrderSnapshot.js +21 -0
  68. package/dist/solution/BaseSales/fulfillmentItemRules.d.ts +27 -0
  69. package/dist/solution/BaseSales/fulfillmentItemRules.js +750 -0
  70. package/dist/solution/BaseSales/index.d.ts +39 -16
  71. package/dist/solution/BaseSales/index.js +1754 -1154
  72. package/dist/solution/BaseSales/pickupTiming.d.ts +47 -0
  73. package/dist/solution/BaseSales/pickupTiming.js +616 -0
  74. package/dist/solution/BaseSales/types.d.ts +11 -0
  75. package/dist/solution/BaseSales/utils/parseSalesResponse.js +3 -0
  76. package/dist/solution/BookingByStep/index.d.ts +3 -1
  77. package/dist/solution/BookingByStep/index.js +8 -3
  78. package/dist/solution/BookingTicket/index.d.ts +8 -2
  79. package/dist/solution/BookingTicket/index.js +513 -436
  80. package/dist/solution/BookingTicket/utils/addProductDecision.d.ts +0 -1
  81. package/dist/solution/BookingTicket/utils/addProductDecision.js +1 -1
  82. package/dist/solution/BookingTicket/utils/cartView.js +3 -1
  83. package/dist/solution/OrderInvoicePreview/index.d.ts +66 -0
  84. package/dist/solution/OrderInvoicePreview/index.js +340 -0
  85. package/dist/solution/RegisterAndLogin/config.js +54 -11
  86. package/dist/solution/RegisterAndLogin/index.d.ts +2 -0
  87. package/dist/solution/RegisterAndLogin/index.js +1 -1
  88. package/dist/solution/RegisterAndLogin/utils.d.ts +12 -4
  89. package/dist/solution/RegisterAndLogin/utils.js +36 -16
  90. package/dist/solution/UnifiedBookingSales/businessAvailability.d.ts +27 -0
  91. package/dist/solution/UnifiedBookingSales/businessAvailability.js +292 -0
  92. package/dist/solution/UnifiedBookingSales/index.d.ts +36 -4
  93. package/dist/solution/UnifiedBookingSales/index.js +1714 -905
  94. package/dist/solution/UnifiedBookingSales/types.d.ts +16 -2
  95. package/dist/solution/VenueBooking/index.d.ts +15 -2
  96. package/dist/solution/VenueBooking/index.js +1062 -785
  97. package/dist/solution/VenueBooking/types.d.ts +2 -1
  98. package/dist/solution/VenueBooking/types.js +1 -0
  99. package/dist/solution/VenueBooking/utils/dateSummary.d.ts +1 -0
  100. package/dist/solution/VenueBooking/utils/dateSummary.js +7 -4
  101. package/dist/solution/VenueBooking/utils/smartPricing.d.ts +71 -0
  102. package/dist/solution/VenueBooking/utils/smartPricing.js +273 -0
  103. package/dist/solution/VenueBooking/utils/timeSlot.d.ts +1 -0
  104. package/dist/solution/VenueBooking/utils/timeSlot.js +6 -3
  105. package/dist/solution/index.d.ts +1 -0
  106. package/dist/solution/index.js +2 -1
  107. package/lib/index.d.ts +2 -0
  108. package/lib/index.js +21 -1
  109. package/lib/model/strategy/adapter/itemRule/adapter.d.ts +3 -1
  110. package/lib/model/strategy/adapter/itemRule/adapter.js +54 -4
  111. package/lib/model/strategy/adapter/itemRule/evaluator.d.ts +3 -1
  112. package/lib/model/strategy/adapter/itemRule/evaluator.js +9 -0
  113. package/lib/model/strategy/adapter/itemRule/type.d.ts +40 -1
  114. package/lib/model/strategy/adapter/itemRule/type.js +10 -0
  115. package/lib/model/strategy/adapter/promotion/index.js +46 -1
  116. package/lib/modules/BookingContext/utils/productExtend.d.ts +7 -0
  117. package/lib/modules/BookingContext/utils/productExtend.js +45 -2
  118. package/lib/modules/BookingContext/utils/resources.js +1 -1
  119. package/lib/modules/BusinessAvailability/index.d.ts +30 -0
  120. package/lib/modules/BusinessAvailability/index.js +146 -0
  121. package/lib/modules/Cart/index.js +4 -2
  122. package/lib/modules/Cart/types.d.ts +2 -0
  123. package/lib/modules/Cart/utils/cartProduct.d.ts +3 -1
  124. package/lib/modules/Cart/utils/cartProduct.js +39 -20
  125. package/lib/modules/FulfillmentTiming/index.d.ts +11 -0
  126. package/lib/modules/FulfillmentTiming/index.js +359 -0
  127. package/lib/modules/FulfillmentTiming/types.d.ts +67 -0
  128. package/lib/modules/FulfillmentTiming/types.js +5 -0
  129. package/lib/modules/OpenData/types.d.ts +14 -0
  130. package/lib/modules/Order/index.d.ts +14 -2
  131. package/lib/modules/Order/index.js +240 -38
  132. package/lib/modules/Order/localEftposState.d.ts +13 -0
  133. package/lib/modules/Order/localEftposState.js +44 -0
  134. package/lib/modules/Order/pickupTiming.d.ts +46 -0
  135. package/lib/modules/Order/pickupTiming.js +61 -0
  136. package/lib/modules/Order/types.d.ts +30 -2
  137. package/lib/modules/Order/utils.js +21 -5
  138. package/lib/modules/Payment/index.js +3 -4
  139. package/lib/modules/Product/types.d.ts +11 -0
  140. package/lib/modules/ProductList/index.js +1 -1
  141. package/lib/modules/ResourcePlanner/planner.d.ts +2 -0
  142. package/lib/modules/ResourcePlanner/planner.js +252 -8
  143. package/lib/modules/ResourcePlanner/types.d.ts +29 -0
  144. package/lib/modules/Schedule/index.d.ts +8 -0
  145. package/lib/modules/Schedule/index.js +68 -12
  146. package/lib/modules/index.d.ts +1 -0
  147. package/lib/modules/index.js +11 -0
  148. package/lib/plugins/request.d.ts +7 -0
  149. package/lib/server/index.d.ts +1 -0
  150. package/lib/server/index.js +158 -37
  151. package/lib/server/modules/form/index.d.ts +19 -0
  152. package/lib/server/modules/form/index.js +73 -0
  153. package/lib/server/modules/form/types.d.ts +13 -0
  154. package/lib/server/modules/form/types.js +9 -0
  155. package/lib/server/modules/index.d.ts +3 -0
  156. package/lib/server/modules/index.js +15 -1
  157. package/lib/server/modules/order/index.d.ts +15 -0
  158. package/lib/server/modules/order/index.js +114 -3
  159. package/lib/server/modules/order/types.d.ts +1 -0
  160. package/lib/server/test.json +1 -1
  161. package/lib/server/utils/small-ticket.d.ts +1 -85
  162. package/lib/server/utils/small-ticket.js +9 -2305
  163. package/lib/shared/payment/loadPaymentMethods.d.ts +9 -0
  164. package/lib/shared/payment/loadPaymentMethods.js +40 -0
  165. package/lib/shared/receipt/customerInvoiceLanguages.d.ts +8 -0
  166. package/lib/shared/receipt/customerInvoiceLanguages.js +58 -0
  167. package/lib/shared/receipt/loadCustomerInvoiceLayout.d.ts +27 -0
  168. package/lib/shared/receipt/loadCustomerInvoiceLayout.js +124 -0
  169. package/lib/shared/receipt/small-ticket.d.ts +96 -0
  170. package/lib/shared/receipt/small-ticket.js +2546 -0
  171. package/lib/shared/receipt/templateVisibility.d.ts +23 -0
  172. package/lib/shared/receipt/templateVisibility.js +63 -0
  173. package/lib/shared/sales/loadSalesOrderSnapshot.d.ts +9 -0
  174. package/lib/shared/sales/loadSalesOrderSnapshot.js +22 -0
  175. package/lib/solution/BaseSales/fulfillmentItemRules.d.ts +27 -0
  176. package/lib/solution/BaseSales/fulfillmentItemRules.js +259 -0
  177. package/lib/solution/BaseSales/index.d.ts +39 -16
  178. package/lib/solution/BaseSales/index.js +333 -31
  179. package/lib/solution/BaseSales/pickupTiming.d.ts +47 -0
  180. package/lib/solution/BaseSales/pickupTiming.js +290 -0
  181. package/lib/solution/BaseSales/types.d.ts +11 -0
  182. package/lib/solution/BaseSales/utils/parseSalesResponse.js +3 -0
  183. package/lib/solution/BookingByStep/index.d.ts +3 -1
  184. package/lib/solution/BookingByStep/index.js +7 -3
  185. package/lib/solution/BookingTicket/index.d.ts +8 -2
  186. package/lib/solution/BookingTicket/index.js +65 -6
  187. package/lib/solution/BookingTicket/utils/addProductDecision.d.ts +0 -1
  188. package/lib/solution/BookingTicket/utils/addProductDecision.js +1 -1
  189. package/lib/solution/BookingTicket/utils/cartView.js +3 -1
  190. package/lib/solution/OrderInvoicePreview/index.d.ts +66 -0
  191. package/lib/solution/OrderInvoicePreview/index.js +246 -0
  192. package/lib/solution/RegisterAndLogin/config.js +57 -11
  193. package/lib/solution/RegisterAndLogin/index.d.ts +2 -0
  194. package/lib/solution/RegisterAndLogin/index.js +9 -1
  195. package/lib/solution/RegisterAndLogin/utils.d.ts +12 -4
  196. package/lib/solution/RegisterAndLogin/utils.js +24 -17
  197. package/lib/solution/UnifiedBookingSales/businessAvailability.d.ts +27 -0
  198. package/lib/solution/UnifiedBookingSales/businessAvailability.js +138 -0
  199. package/lib/solution/UnifiedBookingSales/index.d.ts +36 -4
  200. package/lib/solution/UnifiedBookingSales/index.js +561 -74
  201. package/lib/solution/UnifiedBookingSales/types.d.ts +16 -2
  202. package/lib/solution/VenueBooking/index.d.ts +15 -2
  203. package/lib/solution/VenueBooking/index.js +243 -96
  204. package/lib/solution/VenueBooking/types.d.ts +2 -1
  205. package/lib/solution/VenueBooking/types.js +1 -0
  206. package/lib/solution/VenueBooking/utils/dateSummary.d.ts +1 -0
  207. package/lib/solution/VenueBooking/utils/dateSummary.js +7 -4
  208. package/lib/solution/VenueBooking/utils/smartPricing.d.ts +71 -0
  209. package/lib/solution/VenueBooking/utils/smartPricing.js +224 -0
  210. package/lib/solution/VenueBooking/utils/timeSlot.d.ts +1 -0
  211. package/lib/solution/VenueBooking/utils/timeSlot.js +5 -2
  212. package/lib/solution/index.d.ts +1 -0
  213. package/lib/solution/index.js +11 -0
  214. package/package.json +1 -1
@@ -32,6 +32,8 @@ exports.normalizeProductForAvailability = normalizeProductForAvailability;
32
32
  var _dayjs = _interopRequireDefault(require("dayjs"));
33
33
  var _lodashEs = require("lodash-es");
34
34
  var _modules = require("../../modules");
35
+ var _pickupTiming = require("../../modules/Order/pickupTiming");
36
+ var _cacheItemToBookingInput = require("../../modules/BookingContext/utils/cacheItemToBookingInput");
35
37
  var _utils = require("../../modules/BookingContext/utils");
36
38
  var _OpenData = require("../../modules/OpenData");
37
39
  var _getDateIsInSchedule = require("../../modules/Schedule/getDateIsInSchedule");
@@ -49,13 +51,13 @@ Object.keys(_types).forEach(function (key) {
49
51
  });
50
52
  });
51
53
  var _remoteOccupancyCache = require("./remoteOccupancyCache");
54
+ var _businessAvailability = require("./businessAvailability");
52
55
  var _BookingTicket = require("../BookingTicket");
53
56
  var _localClock = require("../../modules/ResourcePlanner/localClock");
54
57
  var _platform = require("../../utils/platform");
55
58
  var _itemRule = require("../../model/strategy/adapter/itemRule");
56
59
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
57
60
  const OPEN_DATA_SECTION_CODES = ['sale', 'reservation', 'fulfillment', 'menu', 'workflow', 'documents', 'basic', 'checkout', 'availability'];
58
- const OPEN_DATA_CACHE_TTL = 5 * 60 * 1000;
59
61
  function normalizePositiveInteger(value) {
60
62
  const normalized = Number(value);
61
63
  if (!Number.isInteger(normalized) || normalized <= 0) return undefined;
@@ -865,6 +867,20 @@ function collectAvailabilityResourceIds(products, explicitResourceIds = []) {
865
867
  function getProductLineUid(product) {
866
868
  return String(product?.metadata?.unique_identification_number ?? product?.unique_identification_number ?? '');
867
869
  }
870
+ function getVisibleCalendarDate(value) {
871
+ if (typeof value !== 'string') return null;
872
+ const raw = value.trim();
873
+ try {
874
+ if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
875
+ (0, _localClock.parseLocalDate)(raw);
876
+ } else {
877
+ (0, _localClock.parseLocalDateTime)(raw);
878
+ }
879
+ return raw.slice(0, 10);
880
+ } catch (_error) {
881
+ return null;
882
+ }
883
+ }
868
884
  function buildAvailabilityLineIdentity(product) {
869
885
  return {
870
886
  product_id: product.product_id,
@@ -1198,6 +1214,14 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1198
1214
  defaultName = 'unifiedBookingSales';
1199
1215
  defaultVersion = '1.0.0';
1200
1216
  store = {};
1217
+ pickupTimingEnabled = true;
1218
+ businessAvailabilityController;
1219
+ runtimeOpenDataScope = null;
1220
+ runtimeOpenDataSnapshot = null;
1221
+ runtimeOpenDataModule = null;
1222
+ runtimeOpenDataCore;
1223
+ runtimeOpenDataGeneration = 0;
1224
+ runtimeOpenDataDestroyed = false;
1201
1225
  unifiedProductCatalog = [];
1202
1226
  availabilityRuntimeOffsetMinutes = null;
1203
1227
  availabilityScheduleCatalog = null;
@@ -1217,23 +1241,75 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1217
1241
  allergyItemRuleConfigs = [];
1218
1242
  allergyItemRuleConfigsPromise = null;
1219
1243
  async initialize(core, options = {}) {
1244
+ this.resetRuntimeOpenData();
1245
+ this.runtimeOpenDataDestroyed = false;
1246
+ const openDataGeneration = this.runtimeOpenDataGeneration;
1220
1247
  this.availabilityScheduleCatalog = null;
1221
- this.openDataTarget = null;
1222
1248
  this.availabilityRuntimeOffsetMinutes = (0, _localClock.captureRuntimeOffsetMinutes)();
1223
1249
  this.clearAvailabilityRemoteResourceState();
1224
1250
  this.resetAllergyItemRuleState();
1225
1251
  await super.initialize(core, options);
1226
- const openDataBusinessCode = String(this.otherParams?.businessCode ?? this.otherParams?.business_code ?? '').trim();
1227
- const openDataChannel = String(this.otherParams?.channel ?? '').trim();
1228
- if (this.otherParams?.openData && openDataBusinessCode && openDataChannel) {
1229
- this.openDataTarget = `${openDataBusinessCode}+${openDataChannel}`;
1252
+ if (this.runtimeOpenDataDestroyed || openDataGeneration !== this.runtimeOpenDataGeneration) {
1253
+ throw new Error('Store settings changed. Please try again.');
1230
1254
  }
1231
1255
  this.captureAvailabilityScheduleCatalog();
1256
+ await this.ensureAllergyItemRuleConfigsLoaded();
1232
1257
  }
1233
1258
  getSubmitOrderSalesChannel() {
1234
1259
  const channel = String(this.otherParams?.channel ?? '').trim();
1235
1260
  return channel || super.getSubmitOrderSalesChannel();
1236
1261
  }
1262
+
1263
+ /** Only the explicit Kiosk Sales consumer entry enables this policy; POS stays unchanged. */
1264
+ isBusinessAvailabilityEnabled() {
1265
+ return this.otherParams?.enableBusinessAvailability === true && ['pc', 'h5', 'kiosk'].includes((0, _platform.normalizeRuntimePlatform)(this.otherParams?.platform));
1266
+ }
1267
+ getBusinessAvailabilityScope() {
1268
+ return `${this.getRuntimeOpenDataScope()}:${this.runtimeOpenDataGeneration}`;
1269
+ }
1270
+ getBusinessAvailabilityController() {
1271
+ if (!this.businessAvailabilityController) {
1272
+ this.businessAvailabilityController = new _businessAvailability.BusinessAvailabilityController({
1273
+ loadOpenData: () => this.loadOpenDataConfig(),
1274
+ getSchedule: () => this.store.schedule,
1275
+ getTimezone: () => String(this.otherParams?.businessTimezone || this.getPickupTimingContextData('timezone') || ''),
1276
+ getScope: () => this.getBusinessAvailabilityScope()
1277
+ });
1278
+ }
1279
+ return this.businessAvailabilityController;
1280
+ }
1281
+ getBusinessAvailability() {
1282
+ return this.getBusinessAvailabilityController().getAvailability();
1283
+ }
1284
+
1285
+ /** Internal final-write assertion; does not fetch OpenData or Schedule. */
1286
+ assertBusinessAvailability() {
1287
+ if (this.isBusinessAvailabilityEnabled()) this.getBusinessAvailabilityController().assertAvailable();
1288
+ }
1289
+ getSubModuleHooks(moduleName) {
1290
+ const hooks = super.getSubModuleHooks(moduleName);
1291
+ if (moduleName !== 'order' || !this.isBusinessAvailabilityEnabled()) return hooks;
1292
+ let lastScope;
1293
+ let lastExistingScope;
1294
+ let scopeToken = {};
1295
+ return {
1296
+ ...hooks,
1297
+ getCartIntentScope: () => {
1298
+ const scope = this.getBusinessAvailabilityScope();
1299
+ const existingScope = hooks?.getCartIntentScope?.();
1300
+ if (scope !== lastScope || !Object.is(existingScope, lastExistingScope)) {
1301
+ lastScope = scope;
1302
+ lastExistingScope = existingScope;
1303
+ scopeToken = {};
1304
+ }
1305
+ return scopeToken;
1306
+ },
1307
+ beforeCartIntentCommit: context => {
1308
+ this.assertBusinessAvailability();
1309
+ hooks?.beforeCartIntentCommit?.(context);
1310
+ }
1311
+ };
1312
+ }
1237
1313
  createAvailabilityFacadeError(code, message, details) {
1238
1314
  return new _ResourcePlanner.AvailabilityError(code, message, details);
1239
1315
  }
@@ -1246,6 +1322,26 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1246
1322
  businessTimezone: String(otherParams.businessTimezone ?? '')
1247
1323
  };
1248
1324
  }
1325
+ usesDurationTimeGrid(projection) {
1326
+ const platform = String(this.otherParams?.platform ?? '').trim().toLowerCase();
1327
+ return (platform === 'pos' || platform === 'kiosk') && (!projection || projection.products.some(product => product.kind === 'duration'));
1328
+ }
1329
+ getCurrentAvailabilityBusinessDate(projection) {
1330
+ return (0, _ResourcePlanner.getDurationBusinessDate)(projection, this.getAvailabilityRuntimeDateTime());
1331
+ }
1332
+ getAvailabilityBusinessDateRange(projection) {
1333
+ const prepare = projection.meta.prepareParams;
1334
+ const range = prepare?.dateRange;
1335
+ if (!this.usesDurationTimeGrid(projection) || !range?.startDate || !range.endDate) {
1336
+ return projection.meta.coverage;
1337
+ }
1338
+ const businessDate = this.getCurrentAvailabilityBusinessDate(projection);
1339
+ const today = this.getAvailabilityRuntimeDateTime().slice(0, 10);
1340
+ return {
1341
+ startDate: range.startDate === today && businessDate < today ? businessDate : range.startDate,
1342
+ endDate: range.endDate
1343
+ };
1344
+ }
1249
1345
  normalizeAvailabilityIdList(values) {
1250
1346
  if (!Array.isArray(values) || values.length === 0) return undefined;
1251
1347
  const uniqueMap = new Map();
@@ -1289,6 +1385,90 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1289
1385
  mode
1290
1386
  });
1291
1387
  }
1388
+
1389
+ /**
1390
+ * 列表直加没有经过 SKU availability flow,legacy cacheItem 的灵活时长可能落到
1391
+ * 23:59。提交前用同一套营业排期补齐结束时间,保留已选开始时间、资源和 Holder。
1392
+ * 仅供直加调用;手动时间选择和 availability candidate 不经过此方法。
1393
+ */
1394
+ async prepareDirectAddFlexibleBooking(cacheItem) {
1395
+ if (!(0, _utils.isProductFlexibleDuration)(cacheItem?.duration) || (0, _utils.isSessionProduct)(cacheItem)) {
1396
+ return cacheItem;
1397
+ }
1398
+ const extend = cacheItem?._extend;
1399
+ const {
1400
+ startDate
1401
+ } = (0, _utils.getServiceTimes)(cacheItem);
1402
+ if (!extend?.start_date || !extend?.start_time || !startDate.isValid()) {
1403
+ throw new _ResourcePlanner.AvailabilityError('INVALID_TARGET_FILTER', '灵活时长商品缺少有效开始时间');
1404
+ }
1405
+ const date = startDate.format('YYYY-MM-DD');
1406
+ // 带上前一天,覆盖凌晨仍处在前一营业时段内的情况。
1407
+ const dateRange = {
1408
+ startDate: startDate.subtract(1, 'day').format('YYYY-MM-DD'),
1409
+ endDate: date
1410
+ };
1411
+ const prepare = await this.resolveAvailabilityPrepareDefaults({
1412
+ dateRange
1413
+ });
1414
+ if (prepare._availabilityOperatingHours?.source !== 'open_data_schedule') {
1415
+ return cacheItem;
1416
+ }
1417
+ const operatingHours = this.buildAvailabilityOperatingHoursSnapshot({
1418
+ prepare,
1419
+ dateRange,
1420
+ scheduleList: this.getAvailabilityScheduleCatalog(),
1421
+ businessHours: DEFAULT_AVAILABILITY_BUSINESS_HOURS
1422
+ });
1423
+ if (operatingHours.missingScheduleIds.length > 0) {
1424
+ throw new _ResourcePlanner.AvailabilityError('OPERATING_HOURS_RESOLVE_FAILED', '店铺营业时间排期缺失,请重试', {
1425
+ missingScheduleIds: operatingHours.missingScheduleIds
1426
+ });
1427
+ }
1428
+ const start = (0, _localClock.localDateTimeToScalar)((0, _localClock.parseLocalDateTime)(startDate.format('YYYY-MM-DDTHH:mm:00')));
1429
+ const nextDay = (0, _localClock.localDateTimeToScalar)((0, _localClock.parseLocalDateTime)(`${startDate.add(1, 'day').format('YYYY-MM-DD')}T00:00:00`));
1430
+ const windows = operatingHours.windows.map(window => ({
1431
+ start: (0, _localClock.localDateTimeToScalar)((0, _localClock.parseLocalDateTime)(window.startAt)),
1432
+ end: (0, _localClock.localDateTimeToScalar)((0, _localClock.parseLocalDateTime)(window.endAt))
1433
+ })).filter(window => window.end > start && window.start < nextDay).sort((left, right) => left.start - right.start);
1434
+ const merged = [];
1435
+ for (const window of windows) {
1436
+ const previous = merged[merged.length - 1];
1437
+ if (previous && window.start <= previous.end) {
1438
+ previous.end = Math.max(previous.end, window.end);
1439
+ } else {
1440
+ merged.push({
1441
+ ...window
1442
+ });
1443
+ }
1444
+ }
1445
+ // POS 可以提前售票;保留开始分钟,结束取当前或当天下一段营业时间。
1446
+ // 不跨越午休,也不把闭店后的直加顺延到次日。
1447
+ let end = merged[0]?.end;
1448
+ const resourceEndAt = (0, _utils.getUsableTime)(cacheItem)?._extend?.usableTime?.end_at;
1449
+ if (resourceEndAt) {
1450
+ const resourceEnd = (0, _localClock.localDateTimeToScalar)((0, _localClock.parseLocalDateTime)((0, _dayjs.default)(resourceEndAt).format('YYYY-MM-DDTHH:mm:00')));
1451
+ if (end !== undefined) end = Math.min(end, resourceEnd);
1452
+ }
1453
+ if (end === undefined || end <= start) {
1454
+ throw new _ResourcePlanner.AvailabilityError('REVALIDATION_FAILED', '当前时间没有可用营业时段,请选择预约时间');
1455
+ }
1456
+ const duration = Math.floor((end - start) / 60000);
1457
+ if (duration <= 0) {
1458
+ throw new _ResourcePlanner.AvailabilityError('REVALIDATION_FAILED', '灵活时长商品的结束时间必须晚于开始时间');
1459
+ }
1460
+ const endDate = (0, _dayjs.default)((0, _localClock.formatLocalDateTime)(start + duration * 60000));
1461
+ return {
1462
+ ...cacheItem,
1463
+ _extend: {
1464
+ ...extend,
1465
+ duration: _utils.flexibleObj.create(duration),
1466
+ endDate,
1467
+ end_date: endDate.format('YYYY-MM-DD'),
1468
+ end_time: endDate.format('HH:mm')
1469
+ }
1470
+ };
1471
+ }
1292
1472
  async resolveAvailabilityPrepareDefaults(params) {
1293
1473
  const needsOperatingHours = params.businessHours === undefined;
1294
1474
  const needsTimeSlice = params.slotStepMinutes === undefined;
@@ -1304,7 +1484,7 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1304
1484
  const businessCode = String(this.otherParams?.businessCode ?? this.otherParams?.business_code ?? '').trim();
1305
1485
  const channel = String(this.otherParams?.channel ?? '').trim();
1306
1486
  const currentTarget = businessCode && channel ? `${businessCode}+${channel}` : null;
1307
- let openData = !currentTarget || this.openDataTarget === currentTarget ? this.otherParams?.openData : null;
1487
+ let openData = (!currentTarget || this.openDataTarget === currentTarget) && (this.runtimeOpenDataScope === null || this.runtimeOpenDataScope === this.getRuntimeOpenDataScope()) ? this.otherParams?.openData : null;
1308
1488
  if (!openData && this.store.openData) {
1309
1489
  try {
1310
1490
  openData = await this.getOpenData();
@@ -1368,6 +1548,13 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1368
1548
  const previousScope = this.getAvailabilityScopeDescriptor(this.otherParams || {});
1369
1549
  const previousItemRuleConfig = this.otherParams?.itemRule;
1370
1550
  const explicitlySetsOpenData = Object.prototype.hasOwnProperty.call(params, 'openData');
1551
+ const nextParams = cover ? params : {
1552
+ ...this.otherParams,
1553
+ ...params
1554
+ };
1555
+ if (explicitlySetsOpenData || this.getRuntimeOpenDataScope() !== this.getRuntimeOpenDataScope(nextParams)) {
1556
+ this.clearOpenDataCache();
1557
+ }
1371
1558
  await super.setOtherParams(params, {
1372
1559
  cover
1373
1560
  });
@@ -1384,6 +1571,8 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1384
1571
  }
1385
1572
  }
1386
1573
  async destroy() {
1574
+ this.runtimeOpenDataDestroyed = true;
1575
+ this.clearOpenDataCache();
1387
1576
  this.availabilityScheduleCatalog = null;
1388
1577
  this.availabilityRuntimeOffsetMinutes = null;
1389
1578
  await this.resetAvailabilityProjectionState('destroy');
@@ -1440,8 +1629,15 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1440
1629
  return typeof value === 'string' && value.trim() ? value : undefined;
1441
1630
  }
1442
1631
  createAvailabilityContextRef(context) {
1632
+ const projection = this.getActiveAvailabilityProjectionEntry(context)?.projection;
1443
1633
  return {
1444
- contextId: context.contextId
1634
+ contextId: context.contextId,
1635
+ ...(projection && this.usesDurationTimeGrid(projection) ? {
1636
+ durationTimeSelection: {
1637
+ version: 1,
1638
+ currentBusinessDate: this.getCurrentAvailabilityBusinessDate(projection)
1639
+ }
1640
+ } : {})
1445
1641
  };
1446
1642
  }
1447
1643
  async emitAvailabilityDiagnostic(payload) {
@@ -1579,6 +1775,42 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1579
1775
  this.store.order?.clearSessionStoragePersistedTempOrder?.();
1580
1776
  }
1581
1777
 
1778
+ /** 在 Kiosk 拉起支付设备前补齐订单号,同一订单重试复用已分配的号码。 */
1779
+ async ensureLocalOrderNumbersForCheckout() {
1780
+ if ((0, _platform.normalizeRuntimePlatform)(this.otherParams?.platform) !== 'kiosk') return;
1781
+ const order = this.store.order;
1782
+ const tempOrder = order?.getTempOrder();
1783
+ if (!tempOrder) throw new Error('当前订单不存在,无法生成订单编号');
1784
+ if (tempOrder.shop_order_number && tempOrder.shop_full_order_number) return;
1785
+ const biz = this.getBookingTicketBusinessCode();
1786
+ if (!biz) throw new Error('当前订单缺少业务类型,无法生成订单编号');
1787
+ const idGenerator = this.appPlugin?.getApp?.()?.getPlugin?.('idGenerator');
1788
+ if (!tempOrder.shop_order_number && typeof idGenerator?.generateShopOrderNumber !== 'function' || !tempOrder.shop_full_order_number && typeof idGenerator?.generateReceiptId !== 'function') {
1789
+ throw new Error('订单编号生成器不可用,请重启应用后重试');
1790
+ }
1791
+ try {
1792
+ if (!tempOrder.shop_order_number) {
1793
+ tempOrder.shop_order_number = await idGenerator.generateShopOrderNumber({
1794
+ biz
1795
+ });
1796
+ }
1797
+ if (!tempOrder.shop_full_order_number) {
1798
+ tempOrder.shop_full_order_number = await idGenerator.generateReceiptId({
1799
+ biz
1800
+ });
1801
+ }
1802
+ if (order.getTempOrder() !== tempOrder) {
1803
+ throw new Error('当前订单已切换,请重新发起支付');
1804
+ }
1805
+ if (!tempOrder.shop_order_number || !tempOrder.shop_full_order_number) {
1806
+ throw new Error('订单编号生成失败,请重试');
1807
+ }
1808
+ } finally {
1809
+ // 部分生成成功时也保留号码,后续只补仍然缺失的字段。
1810
+ if (order.getTempOrder() === tempOrder) order.persistTempOrder();
1811
+ }
1812
+ }
1813
+
1582
1814
  /**
1583
1815
  * PC/H5/Kiosk 结账默认直接等待云端 checkout;POS/WebPOS 仍保持本地待同步。
1584
1816
  * 显式传 checkoutSyncTaskEnabled: true 时仍可回退到本地同步模式。
@@ -1684,52 +1916,118 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1684
1916
  action: this.resolveDiscountAction(this.resolveDiscountOrderIdentity(tempOrder))
1685
1917
  });
1686
1918
  }
1919
+ getRuntimeOpenDataScope(otherParams = this.otherParams) {
1920
+ return JSON.stringify({
1921
+ shop: this.getPickupTimingContextData('shop_id'),
1922
+ appShop: this.appPlugin ? this.getAppData('shop_id') : undefined,
1923
+ tenant: this.appPlugin ? this.getAppData('tenant_id') : undefined,
1924
+ businessCode: String(otherParams?.businessCode ?? otherParams?.business_code ?? '').trim(),
1925
+ channel: String(otherParams?.channel || super.getSubmitOrderSalesChannel()).trim(),
1926
+ platform: (0, _platform.normalizeRuntimePlatform)(otherParams?.platform),
1927
+ timezone: this.getPickupTimingContextData('timezone'),
1928
+ businessTimezone: otherParams?.businessTimezone,
1929
+ cacheId: otherParams === this.otherParams ? this.cacheId : otherParams?.cacheId
1930
+ });
1931
+ }
1932
+ resetRuntimeOpenData() {
1933
+ this.businessAvailabilityController?.invalidate();
1934
+ this.runtimeOpenDataGeneration += 1;
1935
+ this.runtimeOpenDataScope = null;
1936
+ this.runtimeOpenDataSnapshot = null;
1937
+ this.runtimeOpenDataModule = null;
1938
+ this.runtimeOpenDataCore = undefined;
1939
+ this.openDataTarget = null;
1940
+ this.loadOpenDataInFlight = null;
1941
+ this.loadOpenDataInFlightTarget = null;
1942
+ this.otherParams = {
1943
+ ...this.otherParams,
1944
+ openData: null
1945
+ };
1946
+ this.store.openData?.clearCache?.();
1947
+ }
1948
+ async prepareInvoiceTemplateAtSolutionReady(injectedOpenData) {
1949
+ const businessCode = this.getBookingTicketBusinessCode();
1950
+ if (!businessCode || !this.store.openData) return;
1951
+ if (injectedOpenData) {
1952
+ this.otherParams.openData = injectedOpenData;
1953
+ this.openDataTarget = `${businessCode}+${this.getSubmitOrderSalesChannel()}`;
1954
+ }
1955
+ try {
1956
+ await this.loadOpenDataConfig();
1957
+ } catch (error) {
1958
+ console.warn('[UnifiedBookingSales] Invoice 模板预加载失败,使用默认配置', error);
1959
+ }
1960
+ }
1961
+ async loadOpenDataConfig() {
1962
+ const businessCode = this.getBookingTicketBusinessCode();
1963
+ if (!businessCode) return null;
1964
+ return this.loadOpenData({
1965
+ businessCode,
1966
+ channel: this.getSubmitOrderSalesChannel()
1967
+ });
1968
+ }
1687
1969
 
1688
- /**
1689
- * Loads OpenData without borrowing BookingTicket. Cache ownership is tied to the exact
1690
- * business/channel target so a reused solution instance cannot leak another entry's menus.
1691
- */
1970
+ /** A successful configuration belongs to this business runtime until an explicit refresh. */
1692
1971
  async loadOpenData(params) {
1693
- if (!this.store.openData) {
1972
+ const module = this.store.openData;
1973
+ if (!module) {
1694
1974
  throw new Error('[UnifiedBookingSales] openData 模块未初始化');
1695
1975
  }
1976
+ if (this.runtimeOpenDataDestroyed) {
1977
+ throw new Error('Store settings changed. Please try again.');
1978
+ }
1696
1979
  const businessCode = String(params.businessCode || '').trim();
1697
1980
  const channel = String(params.channel || '').trim();
1698
1981
  if (!businessCode || !channel) {
1699
1982
  throw new Error('[UnifiedBookingSales] OpenData 需要 businessCode 与 channel');
1700
1983
  }
1701
1984
  const target = `${businessCode}+${channel}`;
1702
- const cachedData = this.store.openData.getOpenData();
1703
- const lastFetchedAt = this.store.openData.getLastFetchedAt();
1704
- if (!params.force && this.openDataTarget === target && cachedData && lastFetchedAt && Date.now() - lastFetchedAt < OPEN_DATA_CACHE_TTL) {
1705
- if (typeof this.store.openData.ensureInvoiceTemplateSettings === 'function') {
1706
- await this.store.openData.ensureInvoiceTemplateSettings(target);
1985
+ this.otherParams.businessCode = businessCode;
1986
+ this.otherParams.channel = channel;
1987
+ const scope = this.getRuntimeOpenDataScope();
1988
+ if (params.force) {
1989
+ this.clearOpenDataCache();
1990
+ }
1991
+ if (this.runtimeOpenDataScope !== scope || this.runtimeOpenDataModule !== module || this.runtimeOpenDataCore !== this.core) {
1992
+ // Only the first, explicitly injected target may seed a new runtime snapshot.
1993
+ const injectedOpenData = this.runtimeOpenDataScope === null && (!this.openDataTarget || this.openDataTarget === target) && isRecord(this.otherParams.openData) ? this.otherParams.openData : null;
1994
+ if (this.runtimeOpenDataScope !== null) {
1995
+ // The caller's Pickup Timing scope already changes with this runtime scope.
1996
+ this.resetRuntimeOpenData();
1707
1997
  }
1708
- return cachedData;
1998
+ this.runtimeOpenDataScope = scope;
1999
+ this.runtimeOpenDataModule = module;
2000
+ this.runtimeOpenDataCore = this.core;
2001
+ this.runtimeOpenDataSnapshot = injectedOpenData;
1709
2002
  }
1710
2003
  if (this.loadOpenDataInFlight) {
1711
- if (this.loadOpenDataInFlightTarget === target) {
1712
- return await this.loadOpenDataInFlight;
2004
+ return this.loadOpenDataInFlight;
2005
+ }
2006
+ const generation = this.runtimeOpenDataGeneration;
2007
+ const core = this.core;
2008
+ const request = (async () => {
2009
+ let openData = this.runtimeOpenDataSnapshot;
2010
+ if (openData) {
2011
+ await module.ensureInvoiceTemplateSettings?.(target, openData);
2012
+ } else {
2013
+ openData = await module.fetchOpenData({
2014
+ scope: 'board',
2015
+ target,
2016
+ section_code: [...OPEN_DATA_SECTION_CODES]
2017
+ });
1713
2018
  }
1714
- await this.loadOpenDataInFlight;
1715
- return this.loadOpenData(params);
1716
- }
2019
+ if (this.runtimeOpenDataDestroyed || generation !== this.runtimeOpenDataGeneration || scope !== this.getRuntimeOpenDataScope() || module !== this.store.openData || core !== this.core) {
2020
+ throw new Error('Store settings changed. Please try again.');
2021
+ }
2022
+ this.runtimeOpenDataSnapshot = openData;
2023
+ this.openDataTarget = target;
2024
+ this.otherParams.openData = openData;
2025
+ return openData;
2026
+ })();
1717
2027
  this.loadOpenDataInFlightTarget = target;
1718
- const request = this.store.openData.fetchOpenData({
1719
- scope: 'board',
1720
- target,
1721
- section_code: [...OPEN_DATA_SECTION_CODES]
1722
- });
1723
2028
  this.loadOpenDataInFlight = request;
1724
2029
  try {
1725
- const openData = await request;
1726
- if (this.loadOpenDataInFlight === request) {
1727
- this.openDataTarget = target;
1728
- this.otherParams.openData = openData;
1729
- this.otherParams.businessCode = businessCode;
1730
- this.otherParams.channel = channel;
1731
- }
1732
- return openData;
2030
+ return await request;
1733
2031
  } finally {
1734
2032
  if (this.loadOpenDataInFlight === request) {
1735
2033
  this.loadOpenDataInFlight = null;
@@ -1738,13 +2036,19 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1738
2036
  }
1739
2037
  }
1740
2038
  async getOpenData() {
1741
- const businessCode = String(this.otherParams?.businessCode || '').trim();
1742
- const channel = String(this.otherParams?.channel || '').trim();
1743
- if (!businessCode || !channel) return null;
1744
- return this.loadOpenData({
2039
+ return this.loadOpenDataConfig();
2040
+ }
2041
+ async loadPickupTimingOpenData(_force) {
2042
+ const scope = this.getPickupTimingScope();
2043
+ const businessCode = String(this.otherParams?.businessCode ?? this.otherParams?.business_code ?? '').trim();
2044
+ const config = await this.loadOpenData({
1745
2045
  businessCode,
1746
- channel
2046
+ channel: this.getSubmitOrderSalesChannel()
1747
2047
  });
2048
+ if (scope !== this.getPickupTimingScope()) {
2049
+ throw new Error('Store settings changed. Please try again.');
2050
+ }
2051
+ return config;
1748
2052
  }
1749
2053
  resetAllergyItemRuleState() {
1750
2054
  this.allergyItemRuleConfigs = [];
@@ -1797,7 +2101,7 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1797
2101
  this.allergyItemRuleConfigs = loaded;
1798
2102
  this.allergyItemRuleEvaluator.setStrategyConfigs(loaded);
1799
2103
  return loaded;
1800
- })();
2104
+ })().catch(() => []);
1801
2105
  return this.allergyItemRuleConfigsPromise;
1802
2106
  }
1803
2107
 
@@ -1839,10 +2143,8 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1839
2143
  });
1840
2144
  }
1841
2145
  clearOpenDataCache() {
1842
- this.openDataTarget = null;
1843
- this.loadOpenDataInFlight = null;
1844
- this.loadOpenDataInFlightTarget = null;
1845
- super.clearOpenDataCache();
2146
+ this.resetRuntimeOpenData();
2147
+ this.resetPickupTimingCache();
1846
2148
  }
1847
2149
  async getInvoiceLayoutConfig() {
1848
2150
  const openData = await this.getOpenData();
@@ -1862,7 +2164,7 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1862
2164
  const businessCode = String(this.otherParams?.businessCode || '').trim();
1863
2165
  const channel = String(this.otherParams?.channel || '').trim();
1864
2166
  if (!businessCode || !channel) return false;
1865
- return this.openDataTarget === `${businessCode}+${channel}`;
2167
+ return this.openDataTarget === `${businessCode}+${channel}` && this.runtimeOpenDataScope === this.getRuntimeOpenDataScope();
1866
2168
  }
1867
2169
 
1868
2170
  /**
@@ -2274,7 +2576,9 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
2274
2576
  return range ? [{
2275
2577
  startAt: range.startAt,
2276
2578
  endAt: range.endAt,
2277
- source: descriptor.source
2579
+ source: descriptor.source,
2580
+ businessDate: date,
2581
+ openingAnchorAt: range.startAt
2278
2582
  }] : [];
2279
2583
  });
2280
2584
  return {
@@ -2298,7 +2602,8 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
2298
2602
  return range ? [{
2299
2603
  startAt: range.startAt,
2300
2604
  endAt: range.endAt,
2301
- source: `operating_schedule:${descriptor.scheduleIds.map(String).join(',')}`
2605
+ source: `operating_schedule:${descriptor.scheduleIds.map(String).join(',')}`,
2606
+ businessDate: date
2302
2607
  }] : [];
2303
2608
  }));
2304
2609
  const excludedWindows = dateRangeDays(params.dateRange).flatMap(date => schedules.flatMap(schedule => getExcludedScheduleDateOverrideTimePoints(date, schedule).flatMap(slot => {
@@ -2312,12 +2617,23 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
2312
2617
  startAt: range.startAt,
2313
2618
  endAt: range.endAt,
2314
2619
  source: `operating_schedule_excluded:${schedule.id}`,
2620
+ businessDate: date,
2315
2621
  raw: {
2316
2622
  scheduleId: schedule.id,
2317
2623
  excludedDate: date
2318
2624
  }
2319
2625
  }] : [];
2320
2626
  })));
2627
+ // All lunch-break windows share the first opening of their business day.
2628
+ const openings = new Map();
2629
+ [...windows, ...excludedWindows].forEach(window => {
2630
+ const date = window.businessDate || window.startAt.slice(0, 10);
2631
+ const previous = openings.get(date);
2632
+ if (!previous || window.startAt < previous) openings.set(date, window.startAt);
2633
+ });
2634
+ [...windows, ...excludedWindows].forEach(window => {
2635
+ window.openingAnchorAt = openings.get(window.businessDate || window.startAt.slice(0, 10));
2636
+ });
2321
2637
  return {
2322
2638
  source: descriptor.source,
2323
2639
  scheduleIds: [...descriptor.scheduleIds],
@@ -2707,24 +3023,34 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
2707
3023
  const normalizedPrepareParams = this.normalizeAvailabilityPrepareDescriptor(params);
2708
3024
  const timezoneLabel = String(normalizedPrepareParams.timezone);
2709
3025
  const dateRange = normalizedPrepareParams.dateRange;
3026
+ const scheduleList = this.getAvailabilityScheduleCatalog();
3027
+ const products = await this.loadAvailabilityProducts({
3028
+ prepare: normalizedPrepareParams,
3029
+ dateRange,
3030
+ scheduleList
3031
+ });
3032
+ // Coverage is physical local time; the caller's date range remains business
3033
+ // dates. Keep yesterday's opening and the last business day's overnight tail.
3034
+ const useDurationTimeGrid = this.usesDurationTimeGrid() && products.some(product => product.kind === 'duration');
3035
+ const coverageDateRange = useDurationTimeGrid ? {
3036
+ startDate: (0, _localClock.formatLocalDate)((0, _localClock.addLocalCalendarDays)((0, _localClock.localDateToScalarStart)((0, _localClock.parseLocalDate)(dateRange.startDate)), -1)),
3037
+ endDate: (0, _localClock.formatLocalDate)((0, _localClock.addLocalCalendarDays)((0, _localClock.localDateToScalarStart)((0, _localClock.parseLocalDate)(dateRange.endDate)), 1))
3038
+ } : dateRange;
2710
3039
  const slotStepMinutes = normalizedPrepareParams.slotStepMinutes || DEFAULT_AVAILABILITY_SLOT_STEP_MINUTES;
2711
3040
  const businessHours = normalizedPrepareParams.businessHours || DEFAULT_AVAILABILITY_BUSINESS_HOURS;
2712
3041
  const mode = normalizedPrepareParams.mode || 'appointment';
2713
- const scheduleList = this.getAvailabilityScheduleCatalog();
2714
3042
  const operatingHours = this.buildAvailabilityOperatingHoursSnapshot({
2715
3043
  prepare: normalizedPrepareParams,
2716
- dateRange,
3044
+ dateRange: useDurationTimeGrid ? {
3045
+ startDate: coverageDateRange.startDate,
3046
+ endDate: dateRange.endDate
3047
+ } : dateRange,
2717
3048
  scheduleList,
2718
3049
  businessHours
2719
3050
  });
2720
- const products = await this.loadAvailabilityProducts({
2721
- prepare: normalizedPrepareParams,
2722
- dateRange,
2723
- scheduleList
2724
- });
2725
3051
  const loadedResources = await this.loadAvailabilityResources({
2726
3052
  timezone: timezoneLabel,
2727
- dateRange,
3053
+ dateRange: coverageDateRange,
2728
3054
  resourceIds: normalizedPrepareParams.resourceIds,
2729
3055
  products,
2730
3056
  rawResources: normalizedPrepareParams.rawResources,
@@ -2760,7 +3086,7 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
2760
3086
  resourceOccupancies: [...remoteOccupancies, ...cartOccupancies],
2761
3087
  mode,
2762
3088
  timezone: timezoneLabel,
2763
- coverage: dateRange,
3089
+ coverage: coverageDateRange,
2764
3090
  slotStepMinutes,
2765
3091
  businessHours,
2766
3092
  operatingHours,
@@ -3015,14 +3341,47 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
3015
3341
  } = await this.ensureAvailabilityContextProjection(contextId, 'get_product_time_ranges');
3016
3342
  const contextVersion = context.activeVersion;
3017
3343
  const isPosAvailabilityRequest = String(this.otherParams?.platform ?? '').trim().toLowerCase() === 'pos';
3018
- const allowOutsideOperatingHoursForRequestedStartTimes = isPosAvailabilityRequest && Object.values(request.durationStartTimesByProductId || {}).some(startTimes => Array.isArray(startTimes) && startTimes.length > 0);
3344
+ const isEditingContext = Boolean(context.prepareParams.editingProductLineUids?.length);
3345
+ const editingOffset = (0, _localClock.extractFixedOffsetMinutes)(projectionEntry.projection.meta.now) ?? this.getAvailabilityRuntimeOffsetMinutes();
3346
+ // This new editing input represents an absolute instant. Normalize it once
3347
+ // to the projection's fixed business clock before the wall-clock planner.
3348
+ const editingStartAtByProductId = isEditingContext && request.editingStartAtByProductId ? Object.fromEntries(Object.entries(request.editingStartAtByProductId).map(([productId, startAt]) => {
3349
+ try {
3350
+ const scalar = (0, _localClock.localDateTimeToScalar)((0, _localClock.parseLocalDateTime)(startAt));
3351
+ const inputOffset = (0, _localClock.extractFixedOffsetMinutes)(startAt) ?? editingOffset;
3352
+ return [productId, (0, _localClock.formatLocalDateTime)(scalar + (editingOffset - inputOffset) * 60000, editingOffset)];
3353
+ } catch {
3354
+ throw new _ResourcePlanner.AvailabilityError('INVALID_TARGET_FILTER', '编辑开始时间必须是完整有效日期时间');
3355
+ }
3356
+ })) : undefined;
3357
+ let dateRange = request.dateRange || this.getAvailabilityBusinessDateRange(projectionEntry.projection);
3358
+ // Older edit drafts carry the real calendar date, not a business-date label.
3359
+ // The explicit absolute start disambiguates the previous day's overnight tail.
3360
+ const editingStarts = Object.values(editingStartAtByProductId || {});
3361
+ if (this.usesDurationTimeGrid(projectionEntry.projection) && editingStarts.length === 1 && dateRange.startDate === dateRange.endDate && dateRange.startDate === (0, _localClock.formatLocalDate)((0, _localClock.parseLocalDateTime)(editingStarts[0]))) {
3362
+ const businessDate = (0, _ResourcePlanner.getDurationBusinessDate)(projectionEntry.projection, editingStarts[0]);
3363
+ dateRange = {
3364
+ startDate: businessDate,
3365
+ endDate: businessDate
3366
+ };
3367
+ }
3368
+ const rememberedStartAtByProductId = isPosAvailabilityRequest && !isEditingContext ? request.rememberedStartAtByProductId : undefined;
3369
+ const allowOutsideOperatingHoursForRequestedStartTimes = isPosAvailabilityRequest && !rememberedStartAtByProductId && (editingStarts.length > 0 || Object.values(request.durationStartTimesByProductId || {}).some(startTimes => Array.isArray(startTimes) && startTimes.length > 0));
3019
3370
  return (0, _ResourcePlanner.getProductTimeRanges)(projectionEntry.projection, {
3020
3371
  ...request,
3372
+ dateRange,
3373
+ editingStartAtByProductId,
3374
+ rememberedStartAtByProductId,
3375
+ ...(rememberedStartAtByProductId ? {
3376
+ allowOutsideOperatingHoursForRequestedStartTimes: false
3377
+ } : {}),
3021
3378
  ...(allowOutsideOperatingHoursForRequestedStartTimes ? {
3022
3379
  allowOutsideOperatingHoursForRequestedStartTimes: true
3023
3380
  } : {})
3024
3381
  }, {
3025
3382
  evaluatedAt: this.getAvailabilityRuntimeDateTime(),
3383
+ useDurationTimeGrid: this.usesDurationTimeGrid(projectionEntry.projection),
3384
+ enforceSaleCutoff: Boolean(rememberedStartAtByProductId),
3026
3385
  useExactDurationCandidateMinute: isPosAvailabilityRequest,
3027
3386
  allowSessionCustomRanges: isPosAvailabilityRequest,
3028
3387
  allowExcludedScheduleDateOverride: isPosAvailabilityRequest
@@ -3041,7 +3400,13 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
3041
3400
  } = await this.ensureAvailabilityContextProjection(contextId, 'query_booking_availability');
3042
3401
  this.assertContextualQueryFreshness(context, request);
3043
3402
  const isPosAvailabilityRequest = String(this.otherParams?.platform ?? '').trim().toLowerCase() === 'pos';
3044
- const result = (0, _ResourcePlanner.queryAvailabilityProjection)(projectionEntry.projection, request, {
3403
+ const queryRequest = request.target === 'date' || request.target === 'product' ? {
3404
+ ...request,
3405
+ dateRange: request.dateRange || this.getAvailabilityBusinessDateRange(projectionEntry.projection)
3406
+ } : request;
3407
+ const result = (0, _ResourcePlanner.queryAvailabilityProjection)(projectionEntry.projection, queryRequest, {
3408
+ useDurationTimeGrid: this.usesDurationTimeGrid(projectionEntry.projection),
3409
+ enforceSaleCutoff: isPosAvailabilityRequest && !context.prepareParams.editingProductLineUids?.length && request.selectionIntent === 'remembered',
3045
3410
  useExactDurationCandidateMinute: isPosAvailabilityRequest,
3046
3411
  allowExcludedScheduleDateOverride: isPosAvailabilityRequest
3047
3412
  });
@@ -3121,6 +3486,24 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
3121
3486
  const bookingUid = String(product.booking_uid ?? product.metadata?.booking_uid ?? '');
3122
3487
  return (this.getTempOrder()?.bookings || []).find(booking => bookingUid && getBookingUid(booking) === bookingUid || productUid && String(booking.product_uid ?? '') === productUid);
3123
3488
  }
3489
+ async removeBookingProductsOutsideStartDate(startDate) {
3490
+ const products = this.getTempOrder()?.products || [];
3491
+ if (this.otherParams?.enforceSingleBookingStartDate !== true) {
3492
+ return products;
3493
+ }
3494
+ const targetDate = getVisibleCalendarDate(startDate);
3495
+ if (!targetDate) return products;
3496
+ const identities = products.flatMap(product => {
3497
+ if (!getProductLineUid(product) || product?.metadata?.product_add_schedule_time != null) {
3498
+ return [];
3499
+ }
3500
+ const booking = this.findLinkedBooking(product);
3501
+ if (!booking || (0, _pickupTiming.isFulfillmentTimingBooking)(booking)) return [];
3502
+ const bookingDate = getVisibleCalendarDate(booking.start_date);
3503
+ return bookingDate && bookingDate !== targetDate ? [buildAvailabilityLineIdentity(product)] : [];
3504
+ });
3505
+ return identities.length ? this.removeProductsFromOrder(identities) : products;
3506
+ }
3124
3507
 
3125
3508
  /** Revalidates against the latest landed occupancy cache, then mutates exactly one product line. */
3126
3509
  async commitAvailabilitySelection(contextId, params) {
@@ -3130,6 +3513,25 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
3130
3513
  candidate: params.candidate
3131
3514
  });
3132
3515
  }
3516
+ const isRememberedSelection = params.selectionIntent === 'remembered';
3517
+ if (isRememberedSelection && (String(this.otherParams?.platform ?? '').trim().toLowerCase() !== 'pos' || params.mode !== 'add')) {
3518
+ throw new _ResourcePlanner.AvailabilityError('INVALID_TARGET_FILTER', '记忆时间自动提交仅支持 POS 新增 duration 商品');
3519
+ }
3520
+ const hasCommitGuard = isRememberedSelection || Boolean(params.availabilityCommitGuard);
3521
+ params.availabilityCommitGuard?.();
3522
+ // Stabilize the locally generated sale identity before taking the guard snapshot.
3523
+ if (hasCommitGuard) this.store.order?.ensureTempOrder?.();
3524
+ const rememberedOrder = hasCommitGuard ? this.getTempOrder() : null;
3525
+ const getRememberedCommitIdentity = () => JSON.stringify(stableValue({
3526
+ scope: this.getAvailabilityScopeDescriptor(),
3527
+ shopId: this.otherParams?.shop_id ?? this.otherParams?.shopId,
3528
+ contextShopId: this.core?.context?.shop_id,
3529
+ orderId: this.getTempOrder()?.order_id,
3530
+ externalSaleNumber: this.getTempOrder()?.external_sale_number
3531
+ }));
3532
+ const rememberedCommitIdentity = hasCommitGuard ? getRememberedCommitIdentity() : '';
3533
+ const availabilityCommitTargetGuard = () => this.getTempOrder() === rememberedOrder && getRememberedCommitIdentity() === rememberedCommitIdentity;
3534
+ let guardedCommitPublished = false;
3133
3535
  await this.tryAcquireAvailabilityCommitLock(normalizedContextId);
3134
3536
  try {
3135
3537
  const bookingCount = Number(params.bookingCount ?? 1);
@@ -3235,6 +3637,10 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
3235
3637
  });
3236
3638
  }
3237
3639
  const baseProjection = projectionEntry.projection;
3640
+ const selectedProduct = baseProjection.products.find(item => sameAvailabilityId(item.id, params.candidate.productId));
3641
+ if (isRememberedSelection && (String(this.otherParams?.platform ?? '').trim().toLowerCase() !== 'pos' || params.mode !== 'add' || selectedProduct?.kind !== 'duration')) {
3642
+ throw new _ResourcePlanner.AvailabilityError('INVALID_TARGET_FILTER', '记忆时间自动提交仅支持 POS 新增 duration 商品');
3643
+ }
3238
3644
  let commitWriteGuard = {
3239
3645
  contextId: normalizedContextId,
3240
3646
  expectedVersion: context.activeVersion,
@@ -3280,17 +3686,32 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
3280
3686
  this.assertAvailabilityCommitWriteCAS(commitWriteGuard);
3281
3687
  const candidateDurationMinutes = getCandidateDurationMinutes(params.candidate);
3282
3688
  const candidateStartTime = getCandidateStartTime(params.candidate);
3283
- const allowPosAvailabilityOverride = String(this.otherParams?.platform ?? '').trim().toLowerCase() === 'pos';
3689
+ const allowPosAvailabilityOverride = String(this.otherParams?.platform ?? '').trim().toLowerCase() === 'pos' && !isRememberedSelection;
3690
+ const candidateBusinessDate = params.candidate.businessDate || (selectedProduct?.kind === 'duration' && this.usesDurationTimeGrid(refreshedProjection) ? (0, _ResourcePlanner.getDurationBusinessDate)(refreshedProjection, params.candidate.startAt) : params.candidate.startAt.slice(0, 10));
3284
3691
  const refreshedRanges = (0, _ResourcePlanner.getProductTimeRanges)(refreshedProjection, {
3285
3692
  productIds: [params.candidate.productId],
3286
3693
  dateRange: {
3287
- startDate: params.candidate.startAt.slice(0, 10),
3288
- endDate: params.candidate.startAt.slice(0, 10)
3694
+ startDate: candidateBusinessDate,
3695
+ endDate: candidateBusinessDate
3289
3696
  },
3290
- ...(candidateDurationMinutes ? {
3697
+ ...(isRememberedSelection ? {
3698
+ rememberedStartAtByProductId: {
3699
+ [String(params.candidate.productId)]: params.candidate.bookingStartAt
3700
+ },
3701
+ ...(!selectedProduct?.isFlexibleDuration && candidateDurationMinutes ? {
3702
+ durationMinutesByProductId: {
3703
+ [String(params.candidate.productId)]: candidateDurationMinutes
3704
+ }
3705
+ } : {})
3706
+ } : candidateDurationMinutes ? {
3291
3707
  durationMinutesByProductId: {
3292
3708
  [String(params.candidate.productId)]: candidateDurationMinutes
3293
3709
  },
3710
+ ...(params.mode === 'edit' && selectedProduct?.kind === 'duration' ? {
3711
+ editingStartAtByProductId: {
3712
+ [String(params.candidate.productId)]: params.candidate.bookingStartAt
3713
+ }
3714
+ } : {}),
3294
3715
  ...(candidateStartTime ? {
3295
3716
  durationStartTimesByProductId: {
3296
3717
  [String(params.candidate.productId)]: [candidateStartTime]
@@ -3301,6 +3722,9 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
3301
3722
  } : {})
3302
3723
  } : {})
3303
3724
  }, {
3725
+ evaluatedAt: this.getAvailabilityRuntimeDateTime(),
3726
+ useDurationTimeGrid: this.usesDurationTimeGrid(refreshedProjection),
3727
+ enforceSaleCutoff: isRememberedSelection,
3304
3728
  useExactDurationCandidateMinute: allowPosAvailabilityOverride,
3305
3729
  allowSessionCustomRanges: allowPosAvailabilityOverride,
3306
3730
  allowExcludedScheduleDateOverride: allowPosAvailabilityOverride
@@ -3325,6 +3749,8 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
3325
3749
  partySize,
3326
3750
  bookingCount
3327
3751
  }, {
3752
+ useDurationTimeGrid: this.usesDurationTimeGrid(refreshedProjection),
3753
+ enforceSaleCutoff: isRememberedSelection,
3328
3754
  useExactDurationCandidateMinute: allowPosAvailabilityOverride,
3329
3755
  allowExcludedScheduleDateOverride: allowPosAvailabilityOverride
3330
3756
  });
@@ -3385,6 +3811,30 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
3385
3811
  expectedOrderRevision: revalidationEntry.orderRevision
3386
3812
  };
3387
3813
  const refreshedCandidate = this.decorateAvailabilityCandidate(normalizedContextId, revalidatedContext.activeVersion, refreshedCandidateBase);
3814
+ // Order may await line/runtime setup. Its guarded transaction invokes this
3815
+ // again immediately before publishing, while the live cart is unchanged.
3816
+ const availabilityCommitGuard = hasCommitGuard ? () => {
3817
+ params.availabilityCommitGuard?.();
3818
+ this.assertAvailabilityCommitWriteCAS(commitWriteGuard);
3819
+ if (!availabilityCommitTargetGuard()) {
3820
+ throw this.createAvailabilityFacadeError('AVAILABILITY_SELECTION_STALE', '记忆时间加购期间订单或业务已变化');
3821
+ }
3822
+ if (!isRememberedSelection) return;
3823
+ const currentValidation = (0, _ResourcePlanner.validateAvailabilitySelection)(refreshedProjection, {
3824
+ candidate: refreshedCandidateBase,
3825
+ requirementSelections: params.requirementSelections,
3826
+ partySize,
3827
+ bookingCount
3828
+ }, {
3829
+ useDurationTimeGrid: true,
3830
+ enforceSaleCutoff: true
3831
+ });
3832
+ if (!currentValidation.ok) {
3833
+ throw new _ResourcePlanner.AvailabilityError('REVALIDATION_FAILED', '记忆时间在加购前已不可用,请重新选择', {
3834
+ validation: currentValidation
3835
+ });
3836
+ }
3837
+ } : undefined;
3388
3838
  const existingBooking = editingLine ? this.findLinkedBooking(editingLine) : undefined;
3389
3839
  const existingBookingUid = existingBooking ? getBookingUid(existingBooking) : '';
3390
3840
  if (params.mode === 'edit' && !existingBookingUid) {
@@ -3402,6 +3852,23 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
3402
3852
  holder: params.orderProduct?._extend?.holder ?? existingBooking?.holder ?? existingBooking?.metadata?.holder,
3403
3853
  holderIds: params.orderProduct?._extend?.holder_id ?? params.orderProduct?._extend?.holder?.form_record ?? existingBooking?.metadata?.holder_id ?? existingBooking?.holder?.form_record
3404
3854
  });
3855
+ if (isRememberedSelection && params.orderProduct?._extend?.resource) {
3856
+ // AutoClose 与编辑共用旧资源协议。仅复用其结构,实际资源及每份容量
3857
+ // 必须与本次严格校验结果一致;时间、Holder 和校验元数据仍使用上面的结果。
3858
+ const legacyBooking = (0, _cacheItemToBookingInput.cacheItemToBookingInput)(params.orderProduct);
3859
+ const legacyResources = legacyBooking.resources || [];
3860
+ const selectedResources = legacyResources.filter(resource => Number(resource.relation_id) !== 0);
3861
+ const resourceKeys = resources => resources.map(resource => JSON.stringify([String(resource.form_id), String(resource.relation_id), resource.relation_type, Number(resource.capacity)])).sort();
3862
+ if (JSON.stringify(resourceKeys(selectedResources)) !== JSON.stringify(resourceKeys(booking.resources)) || legacyResources.some(resource => Number(resource.relation_id) === 0 && !selectedResources.some(selected => sameAvailabilityId(selected.form_id, resource.form_id)))) {
3863
+ throw new _ResourcePlanner.AvailabilityError('REVALIDATION_FAILED', '记忆时间的资源与本次校验结果不一致');
3864
+ }
3865
+ Object.assign(booking, {
3866
+ relation_id: legacyBooking.relation_id,
3867
+ form_id: legacyBooking.form_id,
3868
+ relation_type: legacyBooking.relation_type,
3869
+ resources: (0, _lodashEs.cloneDeep)(legacyResources)
3870
+ });
3871
+ }
3405
3872
  const transformed = this.buildUnifiedOrderProduct({
3406
3873
  ...params.orderProduct,
3407
3874
  num: bookingCount,
@@ -3447,6 +3914,7 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
3447
3914
  allow_booking_discount_reapply: params.allowBookingDiscountReapply
3448
3915
  } : {})
3449
3916
  });
3917
+ await this.removeBookingProductsOutsideStartDate(booking.start_date);
3450
3918
  productLineUid = params.productLineUid;
3451
3919
  bookingUid = existingBookingUid;
3452
3920
  productLineUids = [productLineUid];
@@ -3454,12 +3922,21 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
3454
3922
  } else {
3455
3923
  const beforeProductLineUids = new Set((this.getTempOrder()?.products || []).map(line => getProductLineUid(line)).filter(Boolean));
3456
3924
  const beforeBookingUids = new Set((this.getTempOrder()?.bookings || []).map(item => getBookingUid(item)).filter(Boolean));
3457
- const products = await this.addProductToOrder(transformed, booking, params.salesNotes?.length ? {
3458
- salesNotes: params.salesNotes
3925
+ availabilityCommitGuard?.();
3926
+ let products = await this.addProductToOrder(transformed, booking, params.salesNotes?.length || availabilityCommitGuard ? {
3927
+ ...(params.salesNotes?.length ? {
3928
+ salesNotes: params.salesNotes
3929
+ } : {}),
3930
+ ...(availabilityCommitGuard ? {
3931
+ availabilityCommitGuard,
3932
+ availabilityCommitTargetGuard
3933
+ } : {})
3459
3934
  } : undefined);
3935
+ products = await this.removeBookingProductsOutsideStartDate(booking.start_date);
3936
+ guardedCommitPublished = hasCommitGuard;
3460
3937
  const addedLines = products.filter(line => sameAvailabilityId(line.product_id ?? line.id, refreshedCandidate.productId) && line.metadata?.availability_candidate_key === refreshedCandidate.key && !beforeProductLineUids.has(getProductLineUid(line)));
3461
3938
  productLineUids = addedLines.map(line => getProductLineUid(line)).filter(Boolean);
3462
- const addedBookings = (this.getTempOrder()?.bookings || []).filter(item => item.metadata?.availability_candidate_key === refreshedCandidate.key && !beforeBookingUids.has(getBookingUid(item)));
3939
+ const addedBookings = ((hasCommitGuard ? rememberedOrder : this.getTempOrder())?.bookings || []).filter(item => item.metadata?.availability_candidate_key === refreshedCandidate.key && !beforeBookingUids.has(getBookingUid(item)));
3463
3940
  bookingUids = addedBookings.map(item => getBookingUid(item)).filter(Boolean);
3464
3941
  if (bookingUids.length === 0) {
3465
3942
  bookingUids = addedLines.map(line => String(line.booking_uid ?? line.metadata?.booking_uid ?? '')).filter(Boolean);
@@ -3467,12 +3944,13 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
3467
3944
  productLineUid = productLineUids[productLineUids.length - 1] || '';
3468
3945
  bookingUid = bookingUids[bookingUids.length - 1] || '';
3469
3946
  }
3470
- this.invalidateAvailabilityContextsAfterCommit();
3471
- const nextContext = this.getAvailabilityContextEntry(normalizedContextId);
3947
+ const targetStillCurrent = !hasCommitGuard || availabilityCommitTargetGuard();
3948
+ if (targetStillCurrent) this.invalidateAvailabilityContextsAfterCommit();else this.removeAvailabilityContext(normalizedContextId);
3949
+ const nextContext = hasCommitGuard ? this.availabilityContextRegistry.get(normalizedContextId) : this.getAvailabilityContextEntry(normalizedContextId);
3472
3950
  const result = {
3473
3951
  contextId: normalizedContextId,
3474
- contextVersion: nextContext.activeVersion,
3475
- tempOrder: this.getTempOrder(),
3952
+ contextVersion: nextContext?.activeVersion ?? refreshedCandidate.availabilityContextVersion,
3953
+ tempOrder: hasCommitGuard ? rememberedOrder : this.getTempOrder(),
3476
3954
  validation,
3477
3955
  candidate: refreshedCandidate,
3478
3956
  productLineUid,
@@ -3480,10 +3958,19 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
3480
3958
  productLineUids,
3481
3959
  bookingUids
3482
3960
  };
3483
- await this.core.effects.emit(`${this.name}:${_types.UnifiedBookingSalesHooks.onAvailabilitySelectionCommitted}`, result);
3961
+ if (targetStillCurrent) {
3962
+ // A notification failure after publication must not invite a duplicate add.
3963
+ const emit = () => this.core.effects.emit(`${this.name}:${_types.UnifiedBookingSalesHooks.onAvailabilitySelectionCommitted}`, result);
3964
+ if (guardedCommitPublished) {
3965
+ try {
3966
+ await emit();
3967
+ } catch {/* The cart mutation has already succeeded. */}
3968
+ } else await emit();
3969
+ }
3484
3970
  return result;
3485
3971
  } finally {
3486
- await this.releaseAvailabilityCommitLock(normalizedContextId);
3972
+ const release = this.releaseAvailabilityCommitLock(normalizedContextId);
3973
+ if (guardedCommitPublished) await release.catch(() => undefined);else await release;
3487
3974
  }
3488
3975
  }
3489
3976
  }