@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
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.createAvailabilityProjection = createAvailabilityProjection;
7
+ exports.getDurationBusinessDate = getDurationBusinessDate;
7
8
  exports.getProductTimeRanges = getProductTimeRanges;
8
9
  exports.queryAvailabilityProjection = queryAvailabilityProjection;
9
10
  exports.validateAvailabilitySelection = validateAvailabilitySelection;
@@ -250,6 +251,18 @@ function evaluateCandidateTimeGate(params) {
250
251
  };
251
252
  }
252
253
  const policy = product.cutOffPolicy;
254
+ // Current duration items can be shown outside business hours. Their real
255
+ // interval must remain unavailable, including products with no resource groups.
256
+ if (evaluationContext.useDurationTimeGrid && product.kind === 'duration' && !projection.meta.defaults.operatingHours.windows.some(window => {
257
+ const range = normalizeAbsoluteRange(window);
258
+ return range && contains(range, candidateRange);
259
+ })) {
260
+ return {
261
+ status: 'unavailable',
262
+ reasonCodes: ['outside_operating_hours'],
263
+ conflictCode: 'booking_cutoff'
264
+ };
265
+ }
253
266
  if (!policy) return null;
254
267
  if (resolveCutOffPolicyIssue(policy)) {
255
268
  return {
@@ -286,8 +299,8 @@ function evaluateCandidateTimeGate(params) {
286
299
  // otherwise a minute refresh turns it into 0 capacity and incorrectly
287
300
  // advances a create flow to the next slot. The earlier `past` gate still
288
301
  // rejects the anchor once its booking range has ended.
289
- const isActivePosDurationAnchor = evaluationContext.useExactDurationCandidateMinute && product.kind === 'duration' && !product.isFlexibleDuration && evaluationContext.durationCandidateAnchorAtMs === candidateRange.start;
290
- const usesMinutePrecision = product.isFlexibleDuration || evaluationContext.useExactDurationCandidateMinute && product.kind === 'duration';
302
+ const isActivePosDurationAnchor = evaluationContext.useExactDurationCandidateMinute && !evaluationContext.enforceSaleCutoff && product.kind === 'duration' && !product.isFlexibleDuration && evaluationContext.durationCandidateAnchorAtMs === candidateRange.start;
303
+ const usesMinutePrecision = product.isFlexibleDuration || evaluationContext.useDurationTimeGrid || evaluationContext.useExactDurationCandidateMinute && product.kind === 'duration';
291
304
  const hasNotReachedStart = isActivePosDurationAnchor || (usesMinutePrecision ? floorToMinute(now) <= candidateRange.start : now < candidateRange.start);
292
305
  if (hasNotReachedStart) return null;
293
306
  return {
@@ -489,11 +502,34 @@ function normalizeOperatingHours(params) {
489
502
  }));
490
503
  const windows = normalizeWindows(sourceWindows);
491
504
  const excludedWindows = normalizeWindows(params.input?.excludedWindows || []);
505
+ // Keep original dates and opening anchors separately: the availability
506
+ // windows below may be clipped at midnight or merged without segment identity.
507
+ const periodMap = new Map();
508
+ const originalPeriods = params.input?.businessPeriods || [...sourceWindows, ...(params.input?.excludedWindows || [])].map(window => ({
509
+ businessDate: window.businessDate || window.startAt.slice(0, 10),
510
+ startAt: window.openingAnchorAt || window.startAt,
511
+ endAt: window.endAt
512
+ }));
513
+ originalPeriods.forEach(period => {
514
+ const range = normalizeAbsoluteRange(period);
515
+ if (!range) return;
516
+ const previous = periodMap.get(period.businessDate);
517
+ periodMap.set(period.businessDate, {
518
+ start: previous ? Math.min(previous.start, range.start) : range.start,
519
+ end: previous ? Math.max(previous.end, range.end) : range.end
520
+ });
521
+ });
522
+ const businessPeriods = Array.from(periodMap, ([businessDate, range]) => ({
523
+ businessDate,
524
+ startAt: formatDateTime(range.start, params.timezoneName, params.fixedOffsetMinutes),
525
+ endAt: formatDateTime(range.end, params.timezoneName, params.fixedOffsetMinutes)
526
+ })).sort((left, right) => left.startAt.localeCompare(right.startAt));
492
527
  return {
493
528
  source: params.input?.source || params.fallbackSource,
494
529
  scheduleIds: uniqById(params.input?.scheduleIds || []),
495
530
  missingScheduleIds: uniqById(params.input?.missingScheduleIds || []),
496
531
  windows,
532
+ businessPeriods,
497
533
  ...(excludedWindows.length > 0 ? {
498
534
  excludedWindows
499
535
  } : {})
@@ -992,6 +1028,162 @@ function createDurationTimeRangeWithEnd(product, start, bookingEnd, projection,
992
1028
  scheduleRefs: []
993
1029
  };
994
1030
  }
1031
+
1032
+ /** Resolve the label without changing the real calendar timestamp. */
1033
+ function getDurationBusinessDate(projection, startAt) {
1034
+ const start = toDateTime(startAt);
1035
+ const periods = projection.meta.defaults.operatingHours.businessPeriods || [];
1036
+ const period = [...periods].reverse().find(item => {
1037
+ const range = normalizeAbsoluteRange(item);
1038
+ return range && range.start <= start && start < range.end;
1039
+ });
1040
+ return period?.businessDate || startAt.slice(0, 10);
1041
+ }
1042
+ function getAlignedDurationRanges(params) {
1043
+ const {
1044
+ projection,
1045
+ product,
1046
+ request,
1047
+ runtimeOptions,
1048
+ dateRange,
1049
+ operatingRanges,
1050
+ baseRanges,
1051
+ resourceWindowIndex,
1052
+ fixedOffsetMinutes
1053
+ } = params;
1054
+ const step = projection.meta.defaults.slotStepMinutes;
1055
+ const now = floorToMinute(toDateTime(runtimeOptions.evaluatedAt || projection.meta.durationCandidateAnchorAt));
1056
+ if (!Number.isFinite(now)) throw availabilityError('INVALID_TARGET_FILTER', 'evaluatedAt 必须是有效时间');
1057
+ const nowAt = formatDateTime(now, projection.meta.timezone, fixedOffsetMinutes);
1058
+ const currentBusinessDate = getDurationBusinessDate(projection, nowAt);
1059
+ const inRequestedDays = date => date >= dateRange.startDate && date <= dateRange.endDate;
1060
+ const periods = (projection.meta.defaults.operatingHours.businessPeriods || []).filter(period => inRequestedDays(period.businessDate));
1061
+ // A prepared month includes tomorrow's windows too; the single-day UI must
1062
+ // receive the selected business cycle's end, not tomorrow's closing time.
1063
+ const operatingEndAt = dateRange.startDate === dateRange.endDate && periods.length ? periods.reduce((latest, period) => period.endAt > latest ? period.endAt : latest, periods[0].endAt) : undefined;
1064
+ const ranges = [];
1065
+ const duration = resolveRequestedDurationMinutes(product, request);
1066
+ const addRange = (start, end, businessDate) => {
1067
+ if (end <= start) return;
1068
+ const candidate = createDurationTimeRangeWithEnd(product, start, end, projection, fixedOffsetMinutes);
1069
+ candidate.businessDate = businessDate || getDurationBusinessDate(projection, candidate.startAt);
1070
+ if (inRequestedDays(candidate.businessDate)) ranges.push(candidate);
1071
+ };
1072
+ // A flexible end comes from resources that actually cover this exact start,
1073
+ // never from the next future window. Keep alternative real ends separate.
1074
+ const flexibleEnds = start => {
1075
+ const operating = operatingRanges.find(range => range.start <= start && start < range.end);
1076
+ if (!operating) return [];
1077
+ const businessDate = getDurationBusinessDate(projection, formatDateTime(start, projection.meta.timezone, fixedOffsetMinutes));
1078
+ const period = projection.meta.defaults.operatingHours.businessPeriods?.find(item => item.businessDate === businessDate);
1079
+ const operatingEnd = Math.min(operating.end, period ? toDateTime(period.endAt) : operating.end);
1080
+ const requiredGroups = (product.requirementGroups || []).filter(group => group.required !== false);
1081
+ const groups = requiredGroups.map(group => projection.resources.filter(resource => matchesRequirementGroup(group, resource, product, request.partySize)).filter(resource => !request.resourceIds?.length || request.resourceIds.some(id => sameId(id, resource.id))).flatMap(resource => (resourceWindowIndex.get(String(resource.id)) || []).filter(range => range.start <= start && start < range.end).map(range => Math.min(range.end, operatingEnd))));
1082
+ if (groups.some(ends => ends.length === 0)) return [];
1083
+ if (!groups.length) return [operatingEnd];
1084
+ return Array.from(new Set(groups.flat())).filter(end => groups.every(ends => ends.some(availableEnd => availableEnd >= end))).sort((left, right) => left - right);
1085
+ };
1086
+ if (product.isFlexibleDuration) {
1087
+ // Preserve the existing continuous-window UI, with the business-day label.
1088
+ periods.forEach(period => {
1089
+ const periodRange = normalizeAbsoluteRange(period);
1090
+ baseRanges.forEach(range => {
1091
+ const start = Math.max(range.start, periodRange.start, now);
1092
+ const end = Math.min(range.end, periodRange.end);
1093
+ if (start < end) addRange(start, end, period.businessDate);
1094
+ });
1095
+ });
1096
+ } else {
1097
+ periods.forEach(period => {
1098
+ const periodRange = normalizeAbsoluteRange(period);
1099
+ const anchor = 60 % step === 0 ? toDateStart(period.businessDate) : periodRange.start;
1100
+ baseRanges.forEach(range => {
1101
+ const lower = Math.max(range.start, periodRange.start, now);
1102
+ const upper = Math.min(range.end, periodRange.end);
1103
+ let cursor = anchor + Math.ceil((lower - anchor) / (step * 60000)) * step * 60000;
1104
+ for (; cursor + duration * 60000 <= upper; cursor += step * 60000) {
1105
+ addRange(cursor, cursor + duration * 60000, period.businessDate);
1106
+ }
1107
+ });
1108
+ });
1109
+ }
1110
+ let unavailableCurrentTime;
1111
+ if (inRequestedDays(currentBusinessDate)) {
1112
+ if (product.isFlexibleDuration) {
1113
+ const ends = flexibleEnds(now);
1114
+ ends.forEach(end => addRange(now, end, currentBusinessDate));
1115
+ if (!ends.length) {
1116
+ unavailableCurrentTime = {
1117
+ productId: product.id,
1118
+ startAt: nowAt,
1119
+ date: formatDate(now),
1120
+ businessDate: currentBusinessDate,
1121
+ startTime: formatTime(now),
1122
+ status: 'unavailable',
1123
+ reasonCodes: [operatingRanges.some(range => range.start <= now && now < range.end) ? 'outside_resource_window' : 'outside_operating_hours']
1124
+ };
1125
+ }
1126
+ } else {
1127
+ // Even outside a working window the fixed product has a real end. Query
1128
+ // reports unavailable; POS manual capabilities remain a separate policy.
1129
+ addRange(now, now + duration * 60000, currentBusinessDate);
1130
+ }
1131
+ }
1132
+ const rememberedAt = request.rememberedStartAtByProductId?.[String(product.id)];
1133
+ if (rememberedAt !== undefined) {
1134
+ const start = toDateTime(rememberedAt);
1135
+ if (!Number.isFinite(start)) throw availabilityError('INVALID_TARGET_FILTER', '记忆开始时间必须是完整日期时间');
1136
+ if (product.isFlexibleDuration) {
1137
+ flexibleEnds(start).forEach(end => addRange(start, end));
1138
+ } else {
1139
+ addRange(start, start + duration * 60000);
1140
+ }
1141
+ }
1142
+ const editingAt = request.editingStartAtByProductId?.[String(product.id)];
1143
+ if (editingAt !== undefined) {
1144
+ const start = toDateTime(editingAt);
1145
+ const editingDuration = Number(request.durationMinutesByProductId?.[String(product.id)]);
1146
+ if (!Number.isFinite(start) || !Number.isInteger(editingDuration) || editingDuration <= 0) {
1147
+ throw availabilityError('INVALID_TARGET_FILTER', '编辑时间必须包含完整开始日期时间和明确的正整数时长');
1148
+ }
1149
+ // Editing restores the original interval, including its actual overnight
1150
+ // date. It neither adopts the current grid nor recomputes a flexible end.
1151
+ if (request.allowOutsideOperatingHoursForRequestedStartTimes || operatingRanges.some(range => range.start <= start && start < range.end)) {
1152
+ addRange(start, addMinutesZoned(start, editingDuration, projection.meta.timezone));
1153
+ }
1154
+ }
1155
+
1156
+ // Explicit custom input keeps its existing start + requested duration
1157
+ // semantics. Remembered input above must never use this default-30 path.
1158
+ if (dateRange.startDate === dateRange.endDate) {
1159
+ resolveRequestedDurationStartTimes(product, request).forEach(startTime => {
1160
+ const sameDayStart = toDateTimeInDate(dateRange.startDate, startTime);
1161
+ const period = periods.find(item => item.businessDate === dateRange.startDate);
1162
+ const periodRange = period ? normalizeAbsoluteRange(period) : null;
1163
+ const nextDayStart = (0, _localClock.addLocalCalendarDays)(sameDayStart, 1);
1164
+ // HH:mm is entered within a selected business day, whose early hours
1165
+ // may belong to the next calendar day. Do not move other out-of-hours
1166
+ // manual input to tomorrow merely because the business closes overnight.
1167
+ const start = periodRange && !(periodRange.start <= sameDayStart && sameDayStart < periodRange.end) && periodRange.start <= nextDayStart && nextDayStart < periodRange.end ? nextDayStart : sameDayStart;
1168
+ if (request.allowOutsideOperatingHoursForRequestedStartTimes || operatingRanges.some(range => range.start <= start && start < range.end)) {
1169
+ addRange(start, start + duration * 60000);
1170
+ }
1171
+ });
1172
+ }
1173
+ return {
1174
+ productId: product.id,
1175
+ title: product.title,
1176
+ kind: product.kind,
1177
+ slotStepMinutes: step,
1178
+ ...(operatingEndAt ? {
1179
+ operatingEndAt
1180
+ } : {}),
1181
+ ranges: dedupeProductRanges(ranges),
1182
+ ...(unavailableCurrentTime ? {
1183
+ unavailableCurrentTime
1184
+ } : {})
1185
+ };
1186
+ }
995
1187
  function getProductTimeRanges(projection, request = {}, runtimeOptions = {}) {
996
1188
  const normalizedPartySize = normalizePartySize(request.partySize);
997
1189
  const dateRange = resolveProjectionDateRange(projection, request.dateRange);
@@ -1044,6 +1236,19 @@ function getProductTimeRanges(projection, request = {}, runtimeOptions = {}) {
1044
1236
  includeExcludedWindows: allowExcludedScheduleDateOverride
1045
1237
  })).reduce((current, ranges) => current === null ? ranges : intersectRangeLists(current, ranges), null) || [] : null;
1046
1238
  const baseRanges = resourceRanges === null ? operatingRanges : intersectRangeLists(resourceRanges, operatingRanges);
1239
+ if (runtimeOptions.useDurationTimeGrid) {
1240
+ return getAlignedDurationRanges({
1241
+ projection,
1242
+ product,
1243
+ request,
1244
+ runtimeOptions,
1245
+ dateRange,
1246
+ operatingRanges,
1247
+ baseRanges,
1248
+ resourceWindowIndex,
1249
+ fixedOffsetMinutes
1250
+ });
1251
+ }
1047
1252
  const durationMinutes = resolveRequestedDurationMinutes(product, request);
1048
1253
  const flexibleStartAt = (() => {
1049
1254
  if (!product.isFlexibleDuration) return undefined;
@@ -1265,7 +1470,9 @@ function buildEvaluationContext(params) {
1265
1470
  selectionsByGroupKey,
1266
1471
  useExactDurationCandidateMinute: params.useExactDurationCandidateMinute === true,
1267
1472
  durationCandidateAnchorAtMs: Number.isFinite(durationCandidateAnchorAt) ? floorToMinute(durationCandidateAnchorAt) : null,
1268
- allowExcludedScheduleDateOverride: params.allowExcludedScheduleDateOverride === true
1473
+ allowExcludedScheduleDateOverride: params.allowExcludedScheduleDateOverride === true,
1474
+ useDurationTimeGrid: params.useDurationTimeGrid === true,
1475
+ enforceSaleCutoff: params.enforceSaleCutoff === true
1269
1476
  };
1270
1477
  }
1271
1478
  function summarizeStatuses(statuses) {
@@ -1672,6 +1879,8 @@ function queryAvailabilityProjection(projection, request, runtimeOptions = {}) {
1672
1879
  projection,
1673
1880
  evaluatedAt,
1674
1881
  resourceIds: request.resourceIds,
1882
+ useDurationTimeGrid: runtimeOptions.useDurationTimeGrid,
1883
+ enforceSaleCutoff: runtimeOptions.enforceSaleCutoff,
1675
1884
  useExactDurationCandidateMinute: runtimeOptions.useExactDurationCandidateMinute,
1676
1885
  allowExcludedScheduleDateOverride: runtimeOptions.allowExcludedScheduleDateOverride
1677
1886
  });
@@ -1748,6 +1957,8 @@ function queryAvailabilityProjection(projection, request, runtimeOptions = {}) {
1748
1957
  partySize: normalizedPartySize
1749
1958
  }, {
1750
1959
  evaluatedAt,
1960
+ useDurationTimeGrid: runtimeOptions.useDurationTimeGrid,
1961
+ enforceSaleCutoff: runtimeOptions.enforceSaleCutoff,
1751
1962
  useExactDurationCandidateMinute: runtimeOptions.useExactDurationCandidateMinute,
1752
1963
  allowExcludedScheduleDateOverride: runtimeOptions.allowExcludedScheduleDateOverride
1753
1964
  });
@@ -1772,7 +1983,8 @@ function queryAvailabilityProjection(projection, request, runtimeOptions = {}) {
1772
1983
  candidateRangeCache,
1773
1984
  evaluationContext
1774
1985
  });
1775
- const aggregate = byDate.get(candidate.date) || {
1986
+ const candidateDate = runtimeOptions.useDurationTimeGrid ? candidate.businessDate || candidate.date : candidate.date;
1987
+ const aggregate = byDate.get(candidateDate) || {
1776
1988
  availableCount: 0,
1777
1989
  totalCount: 0,
1778
1990
  maxPartySize: 0,
@@ -1790,8 +2002,24 @@ function queryAvailabilityProjection(projection, request, runtimeOptions = {}) {
1790
2002
  aggregate.maxCapacity += evaluated.maxCapacity;
1791
2003
  pushStatusAggregate(aggregate.statusAggregate, evaluated.status);
1792
2004
  evaluated.reasonCodes.forEach(reasonCode => aggregate.reasonCodeSet.add(reasonCode));
1793
- byDate.set(candidate.date, aggregate);
2005
+ byDate.set(candidateDate, aggregate);
1794
2006
  });
2007
+ if (group.unavailableCurrentTime) {
2008
+ const current = group.unavailableCurrentTime;
2009
+ const aggregate = byDate.get(current.businessDate) || {
2010
+ availableCount: 0,
2011
+ totalCount: 0,
2012
+ maxPartySize: 0,
2013
+ remainingCapacity: 0,
2014
+ maxCapacity: 0,
2015
+ reasonCodeSet: new Set(),
2016
+ statusAggregate: createStatusAggregate()
2017
+ };
2018
+ aggregate.totalCount += 1;
2019
+ pushStatusAggregate(aggregate.statusAggregate, 'unavailable');
2020
+ current.reasonCodes.forEach(reason => aggregate.reasonCodeSet.add(reason));
2021
+ byDate.set(current.businessDate, aggregate);
2022
+ }
1795
2023
  const dates = enumerateDates(dateRange.startDate, dateRange.endDate).map(date => {
1796
2024
  const aggregate = byDate.get(date);
1797
2025
  return {
@@ -1849,10 +2077,20 @@ function queryAvailabilityProjection(projection, request, runtimeOptions = {}) {
1849
2077
  maxPartySize = Math.max(maxPartySize, evaluated.maxPartySize);
1850
2078
  pushStatusAggregate(productStatusAggregate, evaluated.status);
1851
2079
  evaluated.reasonCodes.forEach(reasonCode => productReasonCodeSet.add(reasonCode));
1852
- const dateAggregate = dateStatusMap.get(candidate.date) || createStatusAggregate();
2080
+ const candidateDate = runtimeOptions.useDurationTimeGrid ? candidate.businessDate || candidate.date : candidate.date;
2081
+ const dateAggregate = dateStatusMap.get(candidateDate) || createStatusAggregate();
1853
2082
  pushStatusAggregate(dateAggregate, evaluated.status);
1854
- dateStatusMap.set(candidate.date, dateAggregate);
2083
+ dateStatusMap.set(candidateDate, dateAggregate);
1855
2084
  });
2085
+ if (group.unavailableCurrentTime) {
2086
+ const current = group.unavailableCurrentTime;
2087
+ totalTimeCount += 1;
2088
+ pushStatusAggregate(productStatusAggregate, 'unavailable');
2089
+ current.reasonCodes.forEach(reason => productReasonCodeSet.add(reason));
2090
+ const dateAggregate = dateStatusMap.get(current.businessDate) || createStatusAggregate();
2091
+ pushStatusAggregate(dateAggregate, 'unavailable');
2092
+ dateStatusMap.set(current.businessDate, dateAggregate);
2093
+ }
1856
2094
  const availableDateCount = Array.from(dateStatusMap.values()).filter(aggregate => {
1857
2095
  const status = summarizeStatusAggregate(aggregate);
1858
2096
  return status === 'available' || status === 'limited';
@@ -1921,6 +2159,8 @@ function validateAvailabilitySelection(projection, params, runtimeOptions = {})
1921
2159
  projection,
1922
2160
  evaluatedAt,
1923
2161
  requirementSelections: params.requirementSelections,
2162
+ useDurationTimeGrid: runtimeOptions.useDurationTimeGrid,
2163
+ enforceSaleCutoff: runtimeOptions.enforceSaleCutoff,
1924
2164
  useExactDurationCandidateMinute: runtimeOptions.useExactDurationCandidateMinute,
1925
2165
  allowExcludedScheduleDateOverride: runtimeOptions.allowExcludedScheduleDateOverride
1926
2166
  })
@@ -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
  * 统一补齐预约商品的运行态字段。
@@ -146,7 +146,7 @@ function isMultiDayLike(cacheItem) {
146
146
  * `_extend.resource / capacity / startDate / endDate` 等后续 booking mapper 需要的字段。
147
147
  */
148
148
  function prepareBookingCacheItem(cacheItem, ctx) {
149
- if (isNormalBookingProduct(cacheItem) || hasPreparedBookingExtend(cacheItem)) {
149
+ if (ctx?.rememberedTime === undefined && (isNormalBookingProduct(cacheItem) || hasPreparedBookingExtend(cacheItem))) {
150
150
  return cacheItem;
151
151
  }
152
152
  return (0, _BookingContext.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;