@pisell/pisellos 2.3.144 → 2.3.145

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 (30) hide show
  1. package/dist/modules/BookingContext/utils/productExtend.d.ts +7 -0
  2. package/dist/modules/BookingContext/utils/productExtend.js +62 -18
  3. package/dist/modules/Order/index.d.ts +1 -0
  4. package/dist/modules/Order/index.js +380 -198
  5. package/dist/modules/Order/types.d.ts +9 -0
  6. package/dist/modules/ResourcePlanner/planner.d.ts +2 -0
  7. package/dist/modules/ResourcePlanner/planner.js +327 -22
  8. package/dist/modules/ResourcePlanner/types.d.ts +29 -0
  9. package/dist/solution/BookingByStep/index.d.ts +1 -1
  10. package/dist/solution/BookingTicket/utils/addProductDecision.d.ts +0 -1
  11. package/dist/solution/BookingTicket/utils/addProductDecision.js +1 -1
  12. package/dist/solution/UnifiedBookingSales/index.d.ts +3 -0
  13. package/dist/solution/UnifiedBookingSales/index.js +432 -163
  14. package/dist/solution/UnifiedBookingSales/types.d.ts +14 -2
  15. package/lib/model/strategy/adapter/promotion/index.js +46 -1
  16. package/lib/modules/BookingContext/utils/productExtend.d.ts +7 -0
  17. package/lib/modules/BookingContext/utils/productExtend.js +45 -2
  18. package/lib/modules/Order/index.d.ts +1 -0
  19. package/lib/modules/Order/index.js +92 -10
  20. package/lib/modules/Order/types.d.ts +9 -0
  21. package/lib/modules/ResourcePlanner/planner.d.ts +2 -0
  22. package/lib/modules/ResourcePlanner/planner.js +247 -7
  23. package/lib/modules/ResourcePlanner/types.d.ts +29 -0
  24. package/lib/solution/BookingByStep/index.d.ts +1 -1
  25. package/lib/solution/BookingTicket/utils/addProductDecision.d.ts +0 -1
  26. package/lib/solution/BookingTicket/utils/addProductDecision.js +1 -1
  27. package/lib/solution/UnifiedBookingSales/index.d.ts +3 -0
  28. package/lib/solution/UnifiedBookingSales/index.js +220 -27
  29. package/lib/solution/UnifiedBookingSales/types.d.ts +14 -2
  30. package/package.json +1 -1
@@ -445,6 +445,15 @@ export interface AddProductToOrderOptions {
445
445
  disableMerge?: boolean;
446
446
  /** 商品行 UID 落定后,由 OrderModule 原子绑定到该行的 Sales Notes 草稿。 */
447
447
  salesNotes?: SalesNoteDraftInput[];
448
+ /**
449
+ * 自动预约加购的同步提交门禁;抛错时不发布本次新增。
450
+ * 带门禁时先在私有草稿中准备全部商品/booking,异步等待后及原子发布前重新调用。
451
+ * 门禁须校验当前订单身份/版本和真实时间;发布前不会因本次拆行改变订单版本。
452
+ * 不传时保留原有人工加购行为。
453
+ */
454
+ availabilityCommitGuard?: () => void;
455
+ /** 发布后的订单/业务归属检查;只用于阻止旧异步收尾写入新单,不再校验截止或 revision。 */
456
+ availabilityCommitTargetGuard?: () => boolean;
448
457
  }
449
458
  /**
450
459
  * 删除商品后的可选位置保持策略。
@@ -1,5 +1,7 @@
1
1
  import type { AvailabilityProjection, AvailabilityProjectionInput, AvailabilityQuery, AvailabilityQueryResult, AvailabilitySelectionValidationParams, AvailabilitySelectionValidationResult, GetProductTimeRangesRequest, GetProductTimeRangesRuntimeOptions, ProductTimeRangeGroup } from './types';
2
2
  export declare function createAvailabilityProjection(input: AvailabilityProjectionInput): AvailabilityProjection;
3
+ /** Resolve the label without changing the real calendar timestamp. */
4
+ export declare function getDurationBusinessDate(projection: AvailabilityProjection, startAt: string): string;
3
5
  export declare function getProductTimeRanges(projection: AvailabilityProjection, request?: GetProductTimeRangesRequest, runtimeOptions?: GetProductTimeRangesRuntimeOptions): ProductTimeRangeGroup[];
4
6
  export declare function queryAvailabilityProjection(projection: AvailabilityProjection, request: AvailabilityQuery, runtimeOptions?: GetProductTimeRangesRuntimeOptions): AvailabilityQueryResult;
5
7
  export declare function validateAvailabilitySelection(projection: AvailabilityProjection, params: AvailabilitySelectionValidationParams, runtimeOptions?: GetProductTimeRangesRuntimeOptions): AvailabilitySelectionValidationResult;
@@ -1,4 +1,8 @@
1
1
  function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
2
+ function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
3
+ function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
4
+ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
5
+ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
2
6
  function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
3
7
  function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
4
8
  function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
@@ -263,6 +267,18 @@ function evaluateCandidateTimeGate(params) {
263
267
  };
264
268
  }
265
269
  var policy = product.cutOffPolicy;
270
+ // Current duration items can be shown outside business hours. Their real
271
+ // interval must remain unavailable, including products with no resource groups.
272
+ if (evaluationContext.useDurationTimeGrid && product.kind === 'duration' && !projection.meta.defaults.operatingHours.windows.some(function (window) {
273
+ var range = normalizeAbsoluteRange(window);
274
+ return range && contains(range, candidateRange);
275
+ })) {
276
+ return {
277
+ status: 'unavailable',
278
+ reasonCodes: ['outside_operating_hours'],
279
+ conflictCode: 'booking_cutoff'
280
+ };
281
+ }
266
282
  if (!policy) return null;
267
283
  if (resolveCutOffPolicyIssue(policy)) {
268
284
  return {
@@ -299,8 +315,8 @@ function evaluateCandidateTimeGate(params) {
299
315
  // otherwise a minute refresh turns it into 0 capacity and incorrectly
300
316
  // advances a create flow to the next slot. The earlier `past` gate still
301
317
  // rejects the anchor once its booking range has ended.
302
- var isActivePosDurationAnchor = evaluationContext.useExactDurationCandidateMinute && product.kind === 'duration' && !product.isFlexibleDuration && evaluationContext.durationCandidateAnchorAtMs === candidateRange.start;
303
- var usesMinutePrecision = product.isFlexibleDuration || evaluationContext.useExactDurationCandidateMinute && product.kind === 'duration';
318
+ var isActivePosDurationAnchor = evaluationContext.useExactDurationCandidateMinute && !evaluationContext.enforceSaleCutoff && product.kind === 'duration' && !product.isFlexibleDuration && evaluationContext.durationCandidateAnchorAtMs === candidateRange.start;
319
+ var usesMinutePrecision = product.isFlexibleDuration || evaluationContext.useDurationTimeGrid || evaluationContext.useExactDurationCandidateMinute && product.kind === 'duration';
304
320
  var hasNotReachedStart = isActivePosDurationAnchor || (usesMinutePrecision ? floorToMinute(now) <= candidateRange.start : now < candidateRange.start);
305
321
  if (hasNotReachedStart) return null;
306
322
  return {
@@ -494,7 +510,7 @@ function normalizeResources(resources) {
494
510
  });
495
511
  }
496
512
  function normalizeOperatingHours(params) {
497
- var _params$input, _params$input2, _params$input3, _params$input4;
513
+ var _params$input, _params$input2, _params$input3, _params$input4, _params$input5, _params$input6;
498
514
  var fallbackWindows = enumerateDates(params.dateRange.startDate, params.dateRange.endDate).map(function (date) {
499
515
  var start = toDateTimeInDate(date, params.businessHours.startTime, params.timezoneName);
500
516
  var end = toDateTimeInDate(date, params.businessHours.endTime, params.timezoneName);
@@ -530,11 +546,43 @@ function normalizeOperatingHours(params) {
530
546
  };
531
547
  var windows = normalizeWindows(sourceWindows);
532
548
  var excludedWindows = normalizeWindows(((_params$input = params.input) === null || _params$input === void 0 ? void 0 : _params$input.excludedWindows) || []);
549
+ // Keep original dates and opening anchors separately: the availability
550
+ // windows below may be clipped at midnight or merged without segment identity.
551
+ var periodMap = new Map();
552
+ var originalPeriods = ((_params$input2 = params.input) === null || _params$input2 === void 0 ? void 0 : _params$input2.businessPeriods) || [].concat(_toConsumableArray(sourceWindows), _toConsumableArray(((_params$input3 = params.input) === null || _params$input3 === void 0 ? void 0 : _params$input3.excludedWindows) || [])).map(function (window) {
553
+ return {
554
+ businessDate: window.businessDate || window.startAt.slice(0, 10),
555
+ startAt: window.openingAnchorAt || window.startAt,
556
+ endAt: window.endAt
557
+ };
558
+ });
559
+ originalPeriods.forEach(function (period) {
560
+ var range = normalizeAbsoluteRange(period);
561
+ if (!range) return;
562
+ var previous = periodMap.get(period.businessDate);
563
+ periodMap.set(period.businessDate, {
564
+ start: previous ? Math.min(previous.start, range.start) : range.start,
565
+ end: previous ? Math.max(previous.end, range.end) : range.end
566
+ });
567
+ });
568
+ var businessPeriods = Array.from(periodMap, function (_ref) {
569
+ var _ref2 = _slicedToArray(_ref, 2),
570
+ businessDate = _ref2[0],
571
+ range = _ref2[1];
572
+ return {
573
+ businessDate: businessDate,
574
+ startAt: formatDateTime(range.start, params.timezoneName, params.fixedOffsetMinutes),
575
+ endAt: formatDateTime(range.end, params.timezoneName, params.fixedOffsetMinutes)
576
+ };
577
+ }).sort(function (left, right) {
578
+ return left.startAt.localeCompare(right.startAt);
579
+ });
533
580
  return _objectSpread({
534
- source: ((_params$input2 = params.input) === null || _params$input2 === void 0 ? void 0 : _params$input2.source) || params.fallbackSource,
535
- scheduleIds: uniqById(((_params$input3 = params.input) === null || _params$input3 === void 0 ? void 0 : _params$input3.scheduleIds) || []),
536
- missingScheduleIds: uniqById(((_params$input4 = params.input) === null || _params$input4 === void 0 ? void 0 : _params$input4.missingScheduleIds) || []),
537
- windows: windows
581
+ source: ((_params$input4 = params.input) === null || _params$input4 === void 0 ? void 0 : _params$input4.source) || params.fallbackSource,
582
+ scheduleIds: uniqById(((_params$input5 = params.input) === null || _params$input5 === void 0 ? void 0 : _params$input5.scheduleIds) || []),
583
+ missingScheduleIds: uniqById(((_params$input6 = params.input) === null || _params$input6 === void 0 ? void 0 : _params$input6.missingScheduleIds) || []),
584
+ windows: windows,
585
+ businessPeriods: businessPeriods
538
586
  }, excludedWindows.length > 0 ? {
539
587
  excludedWindows: excludedWindows
540
588
  } : {});
@@ -553,8 +601,8 @@ function matchesRequirementGroup(requirementGroup, resource, product) {
553
601
  return resource.capacity >= resolveRequiredCapacity(requirementGroup, product, partySize);
554
602
  }
555
603
  function resolveRequiredCapacity(requirementGroup, product, partySize) {
556
- var _ref, _ref2;
557
- return Math.max(1, Number((_ref = (_ref2 = partySize !== null && partySize !== void 0 ? partySize : requirementGroup.capacityRequired) !== null && _ref2 !== void 0 ? _ref2 : product.capacityRequired) !== null && _ref !== void 0 ? _ref : 1) || 1);
604
+ var _ref3, _ref4;
605
+ return Math.max(1, Number((_ref3 = (_ref4 = partySize !== null && partySize !== void 0 ? partySize : requirementGroup.capacityRequired) !== null && _ref4 !== void 0 ? _ref4 : product.capacityRequired) !== null && _ref3 !== void 0 ? _ref3 : 1) || 1);
558
606
  }
559
607
  function buildCellKey(params) {
560
608
  return [String(params.productId), params.groupId, String(params.resourceId), params.start, params.end].join(':');
@@ -608,8 +656,8 @@ function buildCells(projection) {
608
656
  boundaries.add(window.start);
609
657
  boundaries.add(window.end);
610
658
  });
611
- occupancies.forEach(function (_ref3) {
612
- var range = _ref3.range;
659
+ occupancies.forEach(function (_ref5) {
660
+ var range = _ref5.range;
613
661
  boundaries.add(range.start);
614
662
  boundaries.add(range.end);
615
663
  });
@@ -627,11 +675,11 @@ function buildCells(projection) {
627
675
  var isInsideWorkingWindow = windows.some(function (window) {
628
676
  return contains(window, segment);
629
677
  });
630
- var segmentOccupancies = isInsideWorkingWindow ? occupancies.filter(function (_ref4) {
631
- var range = _ref4.range;
678
+ var segmentOccupancies = isInsideWorkingWindow ? occupancies.filter(function (_ref6) {
679
+ var range = _ref6.range;
632
680
  return overlaps(range, segment);
633
- }).map(function (_ref5) {
634
- var occupancy = _ref5.occupancy;
681
+ }).map(function (_ref7) {
682
+ var occupancy = _ref7.occupancy;
635
683
  return occupancy;
636
684
  }) : [];
637
685
  var usedCapacity = segmentOccupancies.reduce(function (sum, occupancy) {
@@ -1108,6 +1156,211 @@ function createDurationTimeRangeWithEnd(product, start, bookingEnd, projection,
1108
1156
  scheduleRefs: []
1109
1157
  };
1110
1158
  }
1159
+
1160
+ /** Resolve the label without changing the real calendar timestamp. */
1161
+ export function getDurationBusinessDate(projection, startAt) {
1162
+ var start = toDateTime(startAt);
1163
+ var periods = projection.meta.defaults.operatingHours.businessPeriods || [];
1164
+ var period = _toConsumableArray(periods).reverse().find(function (item) {
1165
+ var range = normalizeAbsoluteRange(item);
1166
+ return range && range.start <= start && start < range.end;
1167
+ });
1168
+ return (period === null || period === void 0 ? void 0 : period.businessDate) || startAt.slice(0, 10);
1169
+ }
1170
+ function getAlignedDurationRanges(params) {
1171
+ var _request$rememberedSt, _request$editingStart;
1172
+ var projection = params.projection,
1173
+ product = params.product,
1174
+ request = params.request,
1175
+ runtimeOptions = params.runtimeOptions,
1176
+ dateRange = params.dateRange,
1177
+ operatingRanges = params.operatingRanges,
1178
+ baseRanges = params.baseRanges,
1179
+ resourceWindowIndex = params.resourceWindowIndex,
1180
+ fixedOffsetMinutes = params.fixedOffsetMinutes;
1181
+ var step = projection.meta.defaults.slotStepMinutes;
1182
+ var now = floorToMinute(toDateTime(runtimeOptions.evaluatedAt || projection.meta.durationCandidateAnchorAt));
1183
+ if (!Number.isFinite(now)) throw availabilityError('INVALID_TARGET_FILTER', 'evaluatedAt 必须是有效时间');
1184
+ var nowAt = formatDateTime(now, projection.meta.timezone, fixedOffsetMinutes);
1185
+ var currentBusinessDate = getDurationBusinessDate(projection, nowAt);
1186
+ var inRequestedDays = function inRequestedDays(date) {
1187
+ return date >= dateRange.startDate && date <= dateRange.endDate;
1188
+ };
1189
+ var periods = (projection.meta.defaults.operatingHours.businessPeriods || []).filter(function (period) {
1190
+ return inRequestedDays(period.businessDate);
1191
+ });
1192
+ // A prepared month includes tomorrow's windows too; the single-day UI must
1193
+ // receive the selected business cycle's end, not tomorrow's closing time.
1194
+ var operatingEndAt = dateRange.startDate === dateRange.endDate && periods.length ? periods.reduce(function (latest, period) {
1195
+ return period.endAt > latest ? period.endAt : latest;
1196
+ }, periods[0].endAt) : undefined;
1197
+ var ranges = [];
1198
+ var duration = resolveRequestedDurationMinutes(product, request);
1199
+ var addRange = function addRange(start, end, businessDate) {
1200
+ if (end <= start) return;
1201
+ var candidate = createDurationTimeRangeWithEnd(product, start, end, projection, fixedOffsetMinutes);
1202
+ candidate.businessDate = businessDate || getDurationBusinessDate(projection, candidate.startAt);
1203
+ if (inRequestedDays(candidate.businessDate)) ranges.push(candidate);
1204
+ };
1205
+ // A flexible end comes from resources that actually cover this exact start,
1206
+ // never from the next future window. Keep alternative real ends separate.
1207
+ var flexibleEnds = function flexibleEnds(start) {
1208
+ var _projection$meta$defa;
1209
+ var operating = operatingRanges.find(function (range) {
1210
+ return range.start <= start && start < range.end;
1211
+ });
1212
+ if (!operating) return [];
1213
+ var businessDate = getDurationBusinessDate(projection, formatDateTime(start, projection.meta.timezone, fixedOffsetMinutes));
1214
+ var period = (_projection$meta$defa = projection.meta.defaults.operatingHours.businessPeriods) === null || _projection$meta$defa === void 0 ? void 0 : _projection$meta$defa.find(function (item) {
1215
+ return item.businessDate === businessDate;
1216
+ });
1217
+ var operatingEnd = Math.min(operating.end, period ? toDateTime(period.endAt) : operating.end);
1218
+ var requiredGroups = (product.requirementGroups || []).filter(function (group) {
1219
+ return group.required !== false;
1220
+ });
1221
+ var groups = requiredGroups.map(function (group) {
1222
+ return projection.resources.filter(function (resource) {
1223
+ return matchesRequirementGroup(group, resource, product, request.partySize);
1224
+ }).filter(function (resource) {
1225
+ var _request$resourceIds;
1226
+ return !((_request$resourceIds = request.resourceIds) !== null && _request$resourceIds !== void 0 && _request$resourceIds.length) || request.resourceIds.some(function (id) {
1227
+ return sameId(id, resource.id);
1228
+ });
1229
+ }).flatMap(function (resource) {
1230
+ return (resourceWindowIndex.get(String(resource.id)) || []).filter(function (range) {
1231
+ return range.start <= start && start < range.end;
1232
+ }).map(function (range) {
1233
+ return Math.min(range.end, operatingEnd);
1234
+ });
1235
+ });
1236
+ });
1237
+ if (groups.some(function (ends) {
1238
+ return ends.length === 0;
1239
+ })) return [];
1240
+ if (!groups.length) return [operatingEnd];
1241
+ return Array.from(new Set(groups.flat())).filter(function (end) {
1242
+ return groups.every(function (ends) {
1243
+ return ends.some(function (availableEnd) {
1244
+ return availableEnd >= end;
1245
+ });
1246
+ });
1247
+ }).sort(function (left, right) {
1248
+ return left - right;
1249
+ });
1250
+ };
1251
+ if (product.isFlexibleDuration) {
1252
+ // Preserve the existing continuous-window UI, with the business-day label.
1253
+ periods.forEach(function (period) {
1254
+ var periodRange = normalizeAbsoluteRange(period);
1255
+ baseRanges.forEach(function (range) {
1256
+ var start = Math.max(range.start, periodRange.start, now);
1257
+ var end = Math.min(range.end, periodRange.end);
1258
+ if (start < end) addRange(start, end, period.businessDate);
1259
+ });
1260
+ });
1261
+ } else {
1262
+ periods.forEach(function (period) {
1263
+ var periodRange = normalizeAbsoluteRange(period);
1264
+ var anchor = 60 % step === 0 ? toDateStart(period.businessDate) : periodRange.start;
1265
+ baseRanges.forEach(function (range) {
1266
+ var lower = Math.max(range.start, periodRange.start, now);
1267
+ var upper = Math.min(range.end, periodRange.end);
1268
+ var cursor = anchor + Math.ceil((lower - anchor) / (step * 60000)) * step * 60000;
1269
+ for (; cursor + duration * 60000 <= upper; cursor += step * 60000) {
1270
+ addRange(cursor, cursor + duration * 60000, period.businessDate);
1271
+ }
1272
+ });
1273
+ });
1274
+ }
1275
+ var unavailableCurrentTime;
1276
+ if (inRequestedDays(currentBusinessDate)) {
1277
+ if (product.isFlexibleDuration) {
1278
+ var ends = flexibleEnds(now);
1279
+ ends.forEach(function (end) {
1280
+ return addRange(now, end, currentBusinessDate);
1281
+ });
1282
+ if (!ends.length) {
1283
+ unavailableCurrentTime = {
1284
+ productId: product.id,
1285
+ startAt: nowAt,
1286
+ date: formatDate(now),
1287
+ businessDate: currentBusinessDate,
1288
+ startTime: formatTime(now),
1289
+ status: 'unavailable',
1290
+ reasonCodes: [operatingRanges.some(function (range) {
1291
+ return range.start <= now && now < range.end;
1292
+ }) ? 'outside_resource_window' : 'outside_operating_hours']
1293
+ };
1294
+ }
1295
+ } else {
1296
+ // Even outside a working window the fixed product has a real end. Query
1297
+ // reports unavailable; POS manual capabilities remain a separate policy.
1298
+ addRange(now, now + duration * 60000, currentBusinessDate);
1299
+ }
1300
+ }
1301
+ var rememberedAt = (_request$rememberedSt = request.rememberedStartAtByProductId) === null || _request$rememberedSt === void 0 ? void 0 : _request$rememberedSt[String(product.id)];
1302
+ if (rememberedAt !== undefined) {
1303
+ var start = toDateTime(rememberedAt);
1304
+ if (!Number.isFinite(start)) throw availabilityError('INVALID_TARGET_FILTER', '记忆开始时间必须是完整日期时间');
1305
+ if (product.isFlexibleDuration) {
1306
+ flexibleEnds(start).forEach(function (end) {
1307
+ return addRange(start, end);
1308
+ });
1309
+ } else {
1310
+ addRange(start, start + duration * 60000);
1311
+ }
1312
+ }
1313
+ var editingAt = (_request$editingStart = request.editingStartAtByProductId) === null || _request$editingStart === void 0 ? void 0 : _request$editingStart[String(product.id)];
1314
+ if (editingAt !== undefined) {
1315
+ var _request$durationMinu3;
1316
+ var _start = toDateTime(editingAt);
1317
+ var editingDuration = Number((_request$durationMinu3 = request.durationMinutesByProductId) === null || _request$durationMinu3 === void 0 ? void 0 : _request$durationMinu3[String(product.id)]);
1318
+ if (!Number.isFinite(_start) || !Number.isInteger(editingDuration) || editingDuration <= 0) {
1319
+ throw availabilityError('INVALID_TARGET_FILTER', '编辑时间必须包含完整开始日期时间和明确的正整数时长');
1320
+ }
1321
+ // Editing restores the original interval, including its actual overnight
1322
+ // date. It neither adopts the current grid nor recomputes a flexible end.
1323
+ if (request.allowOutsideOperatingHoursForRequestedStartTimes || operatingRanges.some(function (range) {
1324
+ return range.start <= _start && _start < range.end;
1325
+ })) {
1326
+ addRange(_start, addMinutesZoned(_start, editingDuration, projection.meta.timezone));
1327
+ }
1328
+ }
1329
+
1330
+ // Explicit custom input keeps its existing start + requested duration
1331
+ // semantics. Remembered input above must never use this default-30 path.
1332
+ if (dateRange.startDate === dateRange.endDate) {
1333
+ resolveRequestedDurationStartTimes(product, request).forEach(function (startTime) {
1334
+ var sameDayStart = toDateTimeInDate(dateRange.startDate, startTime);
1335
+ var period = periods.find(function (item) {
1336
+ return item.businessDate === dateRange.startDate;
1337
+ });
1338
+ var periodRange = period ? normalizeAbsoluteRange(period) : null;
1339
+ var nextDayStart = addLocalCalendarDays(sameDayStart, 1);
1340
+ // HH:mm is entered within a selected business day, whose early hours
1341
+ // may belong to the next calendar day. Do not move other out-of-hours
1342
+ // manual input to tomorrow merely because the business closes overnight.
1343
+ var start = periodRange && !(periodRange.start <= sameDayStart && sameDayStart < periodRange.end) && periodRange.start <= nextDayStart && nextDayStart < periodRange.end ? nextDayStart : sameDayStart;
1344
+ if (request.allowOutsideOperatingHoursForRequestedStartTimes || operatingRanges.some(function (range) {
1345
+ return range.start <= start && start < range.end;
1346
+ })) {
1347
+ addRange(start, start + duration * 60000);
1348
+ }
1349
+ });
1350
+ }
1351
+ return _objectSpread(_objectSpread({
1352
+ productId: product.id,
1353
+ title: product.title,
1354
+ kind: product.kind,
1355
+ slotStepMinutes: step
1356
+ }, operatingEndAt ? {
1357
+ operatingEndAt: operatingEndAt
1358
+ } : {}), {}, {
1359
+ ranges: dedupeProductRanges(ranges)
1360
+ }, unavailableCurrentTime ? {
1361
+ unavailableCurrentTime: unavailableCurrentTime
1362
+ } : {});
1363
+ }
1111
1364
  export function getProductTimeRanges(projection) {
1112
1365
  var _extractFixedOffsetMi3, _request$productIds;
1113
1366
  var request = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
@@ -1173,6 +1426,19 @@ export function getProductTimeRanges(projection) {
1173
1426
  return current === null ? ranges : intersectRangeLists(current, ranges);
1174
1427
  }, null) || [] : null;
1175
1428
  var baseRanges = resourceRanges === null ? operatingRanges : intersectRangeLists(resourceRanges, operatingRanges);
1429
+ if (runtimeOptions.useDurationTimeGrid) {
1430
+ return getAlignedDurationRanges({
1431
+ projection: projection,
1432
+ product: product,
1433
+ request: request,
1434
+ runtimeOptions: runtimeOptions,
1435
+ dateRange: dateRange,
1436
+ operatingRanges: operatingRanges,
1437
+ baseRanges: baseRanges,
1438
+ resourceWindowIndex: resourceWindowIndex,
1439
+ fixedOffsetMinutes: fixedOffsetMinutes
1440
+ });
1441
+ }
1176
1442
  var durationMinutes = resolveRequestedDurationMinutes(product, request);
1177
1443
  var flexibleStartAt = function () {
1178
1444
  if (!product.isFlexibleDuration) return undefined;
@@ -1454,7 +1720,9 @@ function buildEvaluationContext(params) {
1454
1720
  selectionsByGroupKey: selectionsByGroupKey,
1455
1721
  useExactDurationCandidateMinute: params.useExactDurationCandidateMinute === true,
1456
1722
  durationCandidateAnchorAtMs: Number.isFinite(durationCandidateAnchorAt) ? floorToMinute(durationCandidateAnchorAt) : null,
1457
- allowExcludedScheduleDateOverride: params.allowExcludedScheduleDateOverride === true
1723
+ allowExcludedScheduleDateOverride: params.allowExcludedScheduleDateOverride === true,
1724
+ useDurationTimeGrid: params.useDurationTimeGrid === true,
1725
+ enforceSaleCutoff: params.enforceSaleCutoff === true
1458
1726
  };
1459
1727
  }
1460
1728
  function summarizeStatuses(statuses) {
@@ -1504,7 +1772,7 @@ function summarizeStatusAggregate(aggregate) {
1504
1772
  return 'unavailable';
1505
1773
  }
1506
1774
  function evaluateMatchedResourceSummary(params) {
1507
- var _ref6, _params$matchedCell$m;
1775
+ var _ref8, _params$matchedCell$m;
1508
1776
  var isPast = params.candidateRange.endMs <= params.evaluationContext.evaluatedAtMs;
1509
1777
  if (!params.matchedCell) {
1510
1778
  return {
@@ -1516,7 +1784,7 @@ function evaluateMatchedResourceSummary(params) {
1516
1784
  };
1517
1785
  }
1518
1786
  var remainingCapacity = Math.max(0, params.matchedCell.remainingCapacity || 0);
1519
- var maxCapacity = Math.max(0, (_ref6 = (_params$matchedCell$m = params.matchedCell.maxCapacity) !== null && _params$matchedCell$m !== void 0 ? _params$matchedCell$m : params.resourceCapacity) !== null && _ref6 !== void 0 ? _ref6 : 0);
1787
+ var maxCapacity = Math.max(0, (_ref8 = (_params$matchedCell$m = params.matchedCell.maxCapacity) !== null && _params$matchedCell$m !== void 0 ? _params$matchedCell$m : params.resourceCapacity) !== null && _ref8 !== void 0 ? _ref8 : 0);
1520
1788
  var maxPartySize = isPast ? 0 : remainingCapacity;
1521
1789
  var status = function () {
1522
1790
  if (isPast) return 'past';
@@ -1942,6 +2210,8 @@ export function queryAvailabilityProjection(projection, request) {
1942
2210
  projection: projection,
1943
2211
  evaluatedAt: evaluatedAt,
1944
2212
  resourceIds: request.resourceIds,
2213
+ useDurationTimeGrid: runtimeOptions.useDurationTimeGrid,
2214
+ enforceSaleCutoff: runtimeOptions.enforceSaleCutoff,
1945
2215
  useExactDurationCandidateMinute: runtimeOptions.useExactDurationCandidateMinute,
1946
2216
  allowExcludedScheduleDateOverride: runtimeOptions.allowExcludedScheduleDateOverride
1947
2217
  });
@@ -2032,6 +2302,8 @@ export function queryAvailabilityProjection(projection, request) {
2032
2302
  partySize: normalizedPartySize
2033
2303
  }, {
2034
2304
  evaluatedAt: evaluatedAt,
2305
+ useDurationTimeGrid: runtimeOptions.useDurationTimeGrid,
2306
+ enforceSaleCutoff: runtimeOptions.enforceSaleCutoff,
2035
2307
  useExactDurationCandidateMinute: runtimeOptions.useExactDurationCandidateMinute,
2036
2308
  allowExcludedScheduleDateOverride: runtimeOptions.allowExcludedScheduleDateOverride
2037
2309
  });
@@ -2060,7 +2332,8 @@ export function queryAvailabilityProjection(projection, request) {
2060
2332
  candidateRangeCache: candidateRangeCache,
2061
2333
  evaluationContext: evaluationContext
2062
2334
  });
2063
- var aggregate = byDate.get(candidate.date) || {
2335
+ var candidateDate = runtimeOptions.useDurationTimeGrid ? candidate.businessDate || candidate.date : candidate.date;
2336
+ var aggregate = byDate.get(candidateDate) || {
2064
2337
  availableCount: 0,
2065
2338
  totalCount: 0,
2066
2339
  maxPartySize: 0,
@@ -2080,8 +2353,26 @@ export function queryAvailabilityProjection(projection, request) {
2080
2353
  evaluated.reasonCodes.forEach(function (reasonCode) {
2081
2354
  return aggregate.reasonCodeSet.add(reasonCode);
2082
2355
  });
2083
- byDate.set(candidate.date, aggregate);
2356
+ byDate.set(candidateDate, aggregate);
2084
2357
  });
2358
+ if (group.unavailableCurrentTime) {
2359
+ var current = group.unavailableCurrentTime;
2360
+ var aggregate = byDate.get(current.businessDate) || {
2361
+ availableCount: 0,
2362
+ totalCount: 0,
2363
+ maxPartySize: 0,
2364
+ remainingCapacity: 0,
2365
+ maxCapacity: 0,
2366
+ reasonCodeSet: new Set(),
2367
+ statusAggregate: createStatusAggregate()
2368
+ };
2369
+ aggregate.totalCount += 1;
2370
+ pushStatusAggregate(aggregate.statusAggregate, 'unavailable');
2371
+ current.reasonCodes.forEach(function (reason) {
2372
+ return aggregate.reasonCodeSet.add(reason);
2373
+ });
2374
+ byDate.set(current.businessDate, aggregate);
2375
+ }
2085
2376
  var dates = enumerateDates(dateRange.startDate, dateRange.endDate).map(function (date) {
2086
2377
  var aggregate = byDate.get(date);
2087
2378
  return {
@@ -2143,10 +2434,22 @@ export function queryAvailabilityProjection(projection, request) {
2143
2434
  evaluated.reasonCodes.forEach(function (reasonCode) {
2144
2435
  return productReasonCodeSet.add(reasonCode);
2145
2436
  });
2146
- var dateAggregate = dateStatusMap.get(candidate.date) || createStatusAggregate();
2437
+ var candidateDate = runtimeOptions.useDurationTimeGrid ? candidate.businessDate || candidate.date : candidate.date;
2438
+ var dateAggregate = dateStatusMap.get(candidateDate) || createStatusAggregate();
2147
2439
  pushStatusAggregate(dateAggregate, evaluated.status);
2148
- dateStatusMap.set(candidate.date, dateAggregate);
2440
+ dateStatusMap.set(candidateDate, dateAggregate);
2149
2441
  });
2442
+ if (group.unavailableCurrentTime) {
2443
+ var current = group.unavailableCurrentTime;
2444
+ totalTimeCount += 1;
2445
+ pushStatusAggregate(productStatusAggregate, 'unavailable');
2446
+ current.reasonCodes.forEach(function (reason) {
2447
+ return productReasonCodeSet.add(reason);
2448
+ });
2449
+ var dateAggregate = dateStatusMap.get(current.businessDate) || createStatusAggregate();
2450
+ pushStatusAggregate(dateAggregate, 'unavailable');
2451
+ dateStatusMap.set(current.businessDate, dateAggregate);
2452
+ }
2150
2453
  var availableDateCount = Array.from(dateStatusMap.values()).filter(function (aggregate) {
2151
2454
  var status = summarizeStatusAggregate(aggregate);
2152
2455
  return status === 'available' || status === 'limited';
@@ -2233,6 +2536,8 @@ export function validateAvailabilitySelection(projection, params) {
2233
2536
  projection: projection,
2234
2537
  evaluatedAt: evaluatedAt,
2235
2538
  requirementSelections: params.requirementSelections,
2539
+ useDurationTimeGrid: runtimeOptions.useDurationTimeGrid,
2540
+ enforceSaleCutoff: runtimeOptions.enforceSaleCutoff,
2236
2541
  useExactDurationCandidateMinute: runtimeOptions.useExactDurationCandidateMinute,
2237
2542
  allowExcludedScheduleDateOverride: runtimeOptions.allowExcludedScheduleDateOverride
2238
2543
  })
@@ -92,6 +92,9 @@ export interface AvailabilityProduct {
92
92
  raw?: Record<string, any>;
93
93
  }
94
94
  export interface AvailabilityResourceWindow extends AvailabilityAbsoluteRange {
95
+ /** Original business day and opening anchor, before coverage clipping. */
96
+ businessDate?: string;
97
+ openingAnchorAt?: string;
95
98
  source?: string;
96
99
  raw?: Record<string, any>;
97
100
  }
@@ -101,10 +104,15 @@ export interface AvailabilityOperatingHoursSnapshot {
101
104
  scheduleIds: AvailabilityId[];
102
105
  missingScheduleIds: AvailabilityId[];
103
106
  windows: AvailabilityResourceWindow[];
107
+ /** Complete business cycles; lunch gaps remain in windows, not in this envelope. */
108
+ businessPeriods?: AvailabilityBusinessPeriod[];
104
109
  /** Schedule windows omitted only because their date is excluded. POS may use
105
110
  * these to surface an unavailable, manually selectable candidate. */
106
111
  excludedWindows?: AvailabilityResourceWindow[];
107
112
  }
113
+ export interface AvailabilityBusinessPeriod extends AvailabilityAbsoluteRange {
114
+ businessDate: string;
115
+ }
108
116
  export interface AvailabilityResource {
109
117
  id: AvailabilityId;
110
118
  formId?: AvailabilityId;
@@ -212,6 +220,17 @@ export interface ProductTimeRange extends AvailabilityDisplayRange {
212
220
  bookingEndAt: string;
213
221
  scheduleRefs: AvailabilityScheduleRef[];
214
222
  primaryScheduleRef?: AvailabilityScheduleRef;
223
+ /** Business-day label. date/startAt keep the actual calendar date. */
224
+ businessDate?: string;
225
+ }
226
+ export interface UnavailableCurrentTime {
227
+ productId: AvailabilityId;
228
+ startAt: string;
229
+ date: string;
230
+ businessDate: string;
231
+ startTime: string;
232
+ status: 'unavailable';
233
+ reasonCodes: string[];
215
234
  }
216
235
  export interface ProductTimeRangeGroup {
217
236
  productId: AvailabilityId;
@@ -221,6 +240,8 @@ export interface ProductTimeRangeGroup {
221
240
  /** 单日查询对应营业窗口的最终结束时刻,不受资源排班是否为空影响。 */
222
241
  operatingEndAt?: string;
223
242
  ranges: ProductTimeRange[];
243
+ /** A display-only current item when flexible duration has no valid end. Never submit it. */
244
+ unavailableCurrentTime?: UnavailableCurrentTime;
224
245
  }
225
246
  export interface AvailabilityAssignment extends AvailabilityAbsoluteRange {
226
247
  productId: AvailabilityId;
@@ -242,6 +263,10 @@ export interface GetProductTimeRangesRequest {
242
263
  partySize?: number;
243
264
  durationMinutesByProductId?: Record<string, number>;
244
265
  durationStartTimesByProductId?: Record<string, string[]>;
266
+ /** Restore only the exact remembered start; recompute the end from current product/windows. */
267
+ rememberedStartAtByProductId?: Record<string, string>;
268
+ /** Edit only: preserve the full actual start plus an explicit durationMinutesByProductId. */
269
+ editingStartAtByProductId?: Record<string, string>;
245
270
  /**
246
271
  * POS 手工输入时间的兼容开关:只为明确请求的 start time 补候选,
247
272
  * 不改变常规候选列表仍按营业时间生成的规则。
@@ -250,6 +275,10 @@ export interface GetProductTimeRangesRequest {
250
275
  }
251
276
  export interface GetProductTimeRangesRuntimeOptions {
252
277
  evaluatedAt?: string;
278
+ /** Enabled by the POS/Kiosk facade only; other consumers retain their existing grid. */
279
+ useDurationTimeGrid?: boolean;
280
+ /** Automatic remembered selection must not use POS opening-minute cutoff exceptions. */
281
+ enforceSaleCutoff?: boolean;
253
282
  /**
254
283
  * Internal POS capability: fixed-duration candidates start from the stable
255
284
  * context anchor's current minute instead of rounding up to ten minutes, and
@@ -328,7 +328,7 @@ export declare class BookingByStepImpl extends BaseModule implements Module {
328
328
  date: string;
329
329
  status: string;
330
330
  week: string;
331
- weekNum: 0 | 2 | 1 | 3 | 4 | 5 | 6;
331
+ weekNum: 0 | 2 | 1 | 6 | 4 | 5 | 3;
332
332
  }[]>;
333
333
  submitTimeSlot(timeSlots: TimeSliceItem): void;
334
334
  private getScheduleDataByIds;
@@ -65,7 +65,6 @@ export interface AddProductDecideContext extends BookingCalcContext {
65
65
  type?: 'select' | 'detail';
66
66
  quantity?: number;
67
67
  customerId?: number | string;
68
- channel?: string;
69
68
  }
70
69
  /**
71
70
  * 统一补齐预约商品的运行态字段。
@@ -141,7 +141,7 @@ function isMultiDayLike(cacheItem) {
141
141
  * `_extend.resource / capacity / startDate / endDate` 等后续 booking mapper 需要的字段。
142
142
  */
143
143
  export function prepareBookingCacheItem(cacheItem, ctx) {
144
- if (isNormalBookingProduct(cacheItem) || hasPreparedBookingExtend(cacheItem)) {
144
+ if ((ctx === null || ctx === void 0 ? void 0 : ctx.rememberedTime) === undefined && (isNormalBookingProduct(cacheItem) || hasPreparedBookingExtend(cacheItem))) {
145
145
  return cacheItem;
146
146
  }
147
147
  return getProductExtend({
@@ -99,6 +99,9 @@ export declare class UnifiedBookingSalesImpl extends BookingTicket {
99
99
  protected getSubModuleHooks(moduleName: string): any;
100
100
  private createAvailabilityFacadeError;
101
101
  private getAvailabilityScopeDescriptor;
102
+ private usesDurationTimeGrid;
103
+ private getCurrentAvailabilityBusinessDate;
104
+ private getAvailabilityBusinessDateRange;
102
105
  private normalizeAvailabilityIdList;
103
106
  private normalizeAvailabilityStringList;
104
107
  private normalizeAvailabilityPrepareDescriptor;