@pisell/pisellos 2.2.298 → 2.2.300

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.
@@ -673,14 +673,9 @@ function normalizeResource(raw) {
673
673
  var id = (_ref5 = (_raw$resourceId = raw.resourceId) !== null && _raw$resourceId !== void 0 ? _raw$resourceId : raw.resource_id) !== null && _ref5 !== void 0 ? _ref5 : raw.id;
674
674
  if (id === undefined || id === null || id === '') return null;
675
675
  var rawWindows = raw.windows || raw.times || [];
676
- return {
677
- id: id,
678
- formId: (_ref6 = (_raw$formId = raw.formId) !== null && _raw$formId !== void 0 ? _raw$formId : raw.form_id) !== null && _ref6 !== void 0 ? _ref6 : raw.resource_form_id,
679
- name: raw.resourceName || raw.main_field || raw.name || raw.title,
680
- code: raw.code,
681
- resourceType: normalizeResourceType(raw.type || raw.resourceType),
682
- capacity: Math.max(1, Number(raw.capacity || raw.max_capacity || 1) || 1),
683
- windows: (Array.isArray(rawWindows) ? rawWindows : []).flatMap(function (window) {
676
+ var rawExcludedWindows = raw.excludedWindows || raw.excluded_windows || [];
677
+ var normalizeWindows = function normalizeWindows(windows) {
678
+ return (Array.isArray(windows) ? windows : []).flatMap(function (window) {
684
679
  var range = normalizeAvailabilityLocalDateTimeRange({
685
680
  startAt: window.startAt || window.start_at || window.start,
686
681
  endAt: window.endAt || window.end_at || window.end,
@@ -694,7 +689,17 @@ function normalizeResource(raw) {
694
689
  source: window.source,
695
690
  raw: window
696
691
  }];
697
- }),
692
+ });
693
+ };
694
+ return {
695
+ id: id,
696
+ formId: (_ref6 = (_raw$formId = raw.formId) !== null && _raw$formId !== void 0 ? _raw$formId : raw.form_id) !== null && _ref6 !== void 0 ? _ref6 : raw.resource_form_id,
697
+ name: raw.resourceName || raw.main_field || raw.name || raw.title,
698
+ code: raw.code,
699
+ resourceType: normalizeResourceType(raw.type || raw.resourceType),
700
+ capacity: Math.max(1, Number(raw.capacity || raw.max_capacity || 1) || 1),
701
+ windows: normalizeWindows(rawWindows),
702
+ excludedWindows: normalizeWindows(rawExcludedWindows),
698
703
  raw: raw
699
704
  };
700
705
  }
@@ -718,6 +723,16 @@ function dateRangeDays(dateRange) {
718
723
  }
719
724
  return dates;
720
725
  }
726
+ function getExcludedScheduleDateOverrideTimePoints(date, schedule) {
727
+ var _schedule$repeat_rule;
728
+ if (!((_schedule$repeat_rule = schedule.repeat_rule) !== null && _schedule$repeat_rule !== void 0 && (_schedule$repeat_rule = _schedule$repeat_rule.excluded_date) !== null && _schedule$repeat_rule !== void 0 && _schedule$repeat_rule.length)) return [];
729
+ if (getScheduleStartEndTimePoints(date, [schedule]).length > 0) return [];
730
+ return getScheduleStartEndTimePoints(date, [_objectSpread(_objectSpread({}, schedule), {}, {
731
+ repeat_rule: _objectSpread(_objectSpread({}, schedule.repeat_rule), {}, {
732
+ excluded_date: []
733
+ })
734
+ })]);
735
+ }
721
736
  function normalizeResourceOccupancy(rawEvent, resourceId) {
722
737
  var _ref9, _ref10, _rawEvent$start_at, _ref11, _ref12, _rawEvent$end_at, _ref13, _ref14, _rawEvent$pax, _rawEvent$schedule_ev, _ref15, _rawEvent$schedule_ev2, _ref16, _rawEvent$booking_uid, _rawEvent$metadata, _ref17, _rawEvent$product_uid, _rawEvent$metadata2;
723
738
  var source = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'remote';
@@ -824,7 +839,30 @@ export function buildAvailabilityResourcesFromV2(params) {
824
839
  });
825
840
  });
826
841
  });
842
+ var excludedWindows = dates.flatMap(function (date) {
843
+ return schedules.flatMap(function (schedule) {
844
+ return getExcludedScheduleDateOverrideTimePoints(date, schedule).flatMap(function (slot) {
845
+ var range = normalizeAvailabilityLocalDateTimeRange({
846
+ startAt: slot.start_at,
847
+ endAt: slot.end_at,
848
+ fixedOffsetMinutes: fixedOffsetMinutes,
849
+ rollOverEnd: true
850
+ });
851
+ if (!range) return [];
852
+ return [{
853
+ startAt: range.startAt,
854
+ endAt: range.endAt,
855
+ source: "resource_schedule_excluded:".concat(schedule.id),
856
+ raw: {
857
+ scheduleId: schedule.id,
858
+ excludedDate: date
859
+ }
860
+ }];
861
+ });
862
+ });
863
+ });
827
864
  resource.windows = materializedWindows.length > 0 ? materializedWindows : explicitWindows;
865
+ resource.excludedWindows = [].concat(_toConsumableArray(resource.excludedWindows || []), _toConsumableArray(excludedWindows));
828
866
  if (resource.windows.length === 0 && missingScheduleIds.length > 0) {
829
867
  resource.windows = buildResourceWindowFallbackTimes({
830
868
  raw: raw,
@@ -1092,7 +1130,7 @@ function buildBookingFromAvailabilitySelection(params) {
1092
1130
  } : {})
1093
1131
  });
1094
1132
  }
1095
- var POS_OVERRIDABLE_AVAILABILITY_CONFLICT_CODES = new Set(['past', 'booking_cutoff', 'resource_full', 'resource_unavailable', 'resource_capacity_exceeded',
1133
+ var POS_OVERRIDABLE_AVAILABILITY_CONFLICT_CODES = new Set(['past', 'booking_cutoff', 'schedule_excluded_date', 'resource_full', 'resource_unavailable', 'resource_capacity_exceeded',
1096
1134
  // A required group whose selected resource is outside its window is surfaced
1097
1135
  // as missing_resource at group level. Structural validation below still
1098
1136
  // rejects a genuinely missing selection.
@@ -3053,12 +3091,35 @@ export var UnifiedBookingSalesImpl = /*#__PURE__*/function (_BookingTicket) {
3053
3091
  }] : [];
3054
3092
  });
3055
3093
  });
3056
- return {
3094
+ var excludedWindows = dateRangeDays(params.dateRange).flatMap(function (date) {
3095
+ return schedules.flatMap(function (schedule) {
3096
+ return getExcludedScheduleDateOverrideTimePoints(date, schedule).flatMap(function (slot) {
3097
+ var range = normalizeAvailabilityLocalDateTimeRange({
3098
+ startAt: slot.start_at,
3099
+ endAt: slot.end_at,
3100
+ fixedOffsetMinutes: _this3.getAvailabilityRuntimeOffsetMinutes(),
3101
+ rollOverEnd: true
3102
+ });
3103
+ return range ? [{
3104
+ startAt: range.startAt,
3105
+ endAt: range.endAt,
3106
+ source: "operating_schedule_excluded:".concat(schedule.id),
3107
+ raw: {
3108
+ scheduleId: schedule.id,
3109
+ excludedDate: date
3110
+ }
3111
+ }] : [];
3112
+ });
3113
+ });
3114
+ });
3115
+ return _objectSpread({
3057
3116
  source: descriptor.source,
3058
3117
  scheduleIds: _toConsumableArray(descriptor.scheduleIds),
3059
3118
  missingScheduleIds: missingScheduleIds,
3060
3119
  windows: windows
3061
- };
3120
+ }, excludedWindows.length > 0 ? {
3121
+ excludedWindows: excludedWindows
3122
+ } : {});
3062
3123
  }
3063
3124
  }, {
3064
3125
  key: "loadAvailabilityProducts",
@@ -4232,7 +4293,8 @@ export var UnifiedBookingSalesImpl = /*#__PURE__*/function (_BookingTicket) {
4232
4293
  allowOutsideOperatingHoursForRequestedStartTimes: true
4233
4294
  } : {}), {
4234
4295
  evaluatedAt: this.getAvailabilityRuntimeDateTime(),
4235
- allowSessionCustomRanges: isPosAvailabilityRequest
4296
+ allowSessionCustomRanges: isPosAvailabilityRequest,
4297
+ allowExcludedScheduleDateOverride: isPosAvailabilityRequest
4236
4298
  }).map(function (group) {
4237
4299
  return _objectSpread(_objectSpread({}, group), {}, {
4238
4300
  ranges: group.ranges.map(function (candidate) {
@@ -4255,7 +4317,8 @@ export var UnifiedBookingSalesImpl = /*#__PURE__*/function (_BookingTicket) {
4255
4317
  key: "queryBookingAvailability",
4256
4318
  value: (function () {
4257
4319
  var _queryBookingAvailability = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee36(contextId, request) {
4258
- var _yield$this$ensureAva2, context, projectionEntry, refreshed, result, contextVersion, contextualResult;
4320
+ var _this$otherParams$pla2, _this$otherParams19;
4321
+ var _yield$this$ensureAva2, context, projectionEntry, refreshed, isPosAvailabilityRequest, result, contextVersion, contextualResult;
4259
4322
  return _regeneratorRuntime().wrap(function _callee36$(_context36) {
4260
4323
  while (1) switch (_context36.prev = _context36.next) {
4261
4324
  case 0:
@@ -4267,7 +4330,10 @@ export var UnifiedBookingSalesImpl = /*#__PURE__*/function (_BookingTicket) {
4267
4330
  projectionEntry = _yield$this$ensureAva2.projectionEntry;
4268
4331
  refreshed = _yield$this$ensureAva2.refreshed;
4269
4332
  this.assertContextualQueryFreshness(context, request);
4270
- result = queryAvailabilityProjection(projectionEntry.projection, request);
4333
+ isPosAvailabilityRequest = String((_this$otherParams$pla2 = (_this$otherParams19 = this.otherParams) === null || _this$otherParams19 === void 0 ? void 0 : _this$otherParams19.platform) !== null && _this$otherParams$pla2 !== void 0 ? _this$otherParams$pla2 : '').trim().toLowerCase() === 'pos';
4334
+ result = queryAvailabilityProjection(projectionEntry.projection, request, {
4335
+ allowExcludedScheduleDateOverride: isPosAvailabilityRequest
4336
+ });
4271
4337
  contextVersion = context.activeVersion;
4272
4338
  contextualResult = result.target === 'time' ? _objectSpread(_objectSpread({}, result), {}, {
4273
4339
  data: result.data.map(function (group) {
@@ -4299,7 +4365,7 @@ export var UnifiedBookingSalesImpl = /*#__PURE__*/function (_BookingTicket) {
4299
4365
  contextVersion: contextVersion,
4300
4366
  refreshed: refreshed
4301
4367
  }));
4302
- case 11:
4368
+ case 12:
4303
4369
  case "end":
4304
4370
  return _context36.stop();
4305
4371
  }
@@ -4376,7 +4442,7 @@ export var UnifiedBookingSalesImpl = /*#__PURE__*/function (_BookingTicket) {
4376
4442
  key: "commitAvailabilitySelection",
4377
4443
  value: (function () {
4378
4444
  var _commitAvailabilitySelection = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee37(contextId, params) {
4379
- var normalizedContextId, _params$bookingCount, _ref46, _params$orderProduct$, _params$orderProduct, _params$orderProduct2, _params$orderProduct3, _params$orderProduct$2, _params$orderProduct4, _params$orderProduct5, _editingLine$product_, _baseProjection$meta$, _refreshedProjection$, _baseProjection$meta$2, _this$otherParams$pla2, _this$otherParams19, _ref47, _params$orderProduct$3, _params$orderProduct6, _existingBooking$meta, _ref48, _ref49, _params$orderProduct$4, _params$orderProduct7, _params$orderProduct8, _existingBooking$meta2, _existingBooking$hold, bookingCount, orderProductQuantity, partySize, _yield$this$ensureAva3, context, projectionEntry, refreshed, boundEditingUids, orderProductId, editingLine, _editingLine$product_2, baseProjection, commitWriteGuard, prepareParams, fallbackResources, commitSnapshot, refreshedProjection, _baseProjection$meta$3, latestContext, candidateDurationMinutes, candidateStartTime, allowPosAvailabilityOverride, refreshedRanges, refreshedCandidateBase, product, validation, forced, capacityConflict, revalidationEntry, revalidatedContext, refreshedCandidate, existingBooking, existingBookingUid, booking, transformed, productLineUid, bookingUid, productLineUids, bookingUids, _transformed$product_, _transformed$product_2, _transformed$product_3, _ref50, _transformed$product_4, _this$getTempOrder4, _this$getTempOrder5, _this$getTempOrder6, beforeProductLineUids, beforeBookingUids, products, addedLines, addedBookings, nextContext, result;
4445
+ var normalizedContextId, _params$bookingCount, _ref46, _params$orderProduct$, _params$orderProduct, _params$orderProduct2, _params$orderProduct3, _params$orderProduct$2, _params$orderProduct4, _params$orderProduct5, _editingLine$product_, _baseProjection$meta$, _refreshedProjection$, _baseProjection$meta$2, _this$otherParams$pla3, _this$otherParams20, _ref47, _params$orderProduct$3, _params$orderProduct6, _existingBooking$meta, _ref48, _ref49, _params$orderProduct$4, _params$orderProduct7, _params$orderProduct8, _existingBooking$meta2, _existingBooking$hold, bookingCount, orderProductQuantity, partySize, _yield$this$ensureAva3, context, projectionEntry, refreshed, boundEditingUids, orderProductId, editingLine, _editingLine$product_2, baseProjection, commitWriteGuard, prepareParams, fallbackResources, commitSnapshot, refreshedProjection, _baseProjection$meta$3, latestContext, candidateDurationMinutes, candidateStartTime, allowPosAvailabilityOverride, refreshedRanges, refreshedCandidateBase, product, validation, forced, capacityConflict, revalidationEntry, revalidatedContext, refreshedCandidate, existingBooking, existingBookingUid, booking, transformed, productLineUid, bookingUid, productLineUids, bookingUids, _transformed$product_, _transformed$product_2, _transformed$product_3, _ref50, _transformed$product_4, _this$getTempOrder4, _this$getTempOrder5, _this$getTempOrder6, beforeProductLineUids, beforeBookingUids, products, addedLines, addedBookings, nextContext, result;
4380
4446
  return _regeneratorRuntime().wrap(function _callee37$(_context37) {
4381
4447
  while (1) switch (_context37.prev = _context37.next) {
4382
4448
  case 0:
@@ -4606,7 +4672,7 @@ export var UnifiedBookingSalesImpl = /*#__PURE__*/function (_BookingTicket) {
4606
4672
  this.assertAvailabilityCommitWriteCAS(commitWriteGuard);
4607
4673
  candidateDurationMinutes = getCandidateDurationMinutes(params.candidate);
4608
4674
  candidateStartTime = getCandidateStartTime(params.candidate);
4609
- allowPosAvailabilityOverride = String((_this$otherParams$pla2 = (_this$otherParams19 = this.otherParams) === null || _this$otherParams19 === void 0 ? void 0 : _this$otherParams19.platform) !== null && _this$otherParams$pla2 !== void 0 ? _this$otherParams$pla2 : '').trim().toLowerCase() === 'pos';
4675
+ allowPosAvailabilityOverride = String((_this$otherParams$pla3 = (_this$otherParams20 = this.otherParams) === null || _this$otherParams20 === void 0 ? void 0 : _this$otherParams20.platform) !== null && _this$otherParams$pla3 !== void 0 ? _this$otherParams$pla3 : '').trim().toLowerCase() === 'pos';
4610
4676
  refreshedRanges = getProjectionProductTimeRanges(refreshedProjection, _objectSpread({
4611
4677
  productIds: [params.candidate.productId],
4612
4678
  dateRange: {
@@ -4620,7 +4686,8 @@ export var UnifiedBookingSalesImpl = /*#__PURE__*/function (_BookingTicket) {
4620
4686
  }, allowPosAvailabilityOverride ? {
4621
4687
  allowOutsideOperatingHoursForRequestedStartTimes: true
4622
4688
  } : {}) : {}) : {}), {
4623
- allowSessionCustomRanges: allowPosAvailabilityOverride
4689
+ allowSessionCustomRanges: allowPosAvailabilityOverride,
4690
+ allowExcludedScheduleDateOverride: allowPosAvailabilityOverride
4624
4691
  }).flatMap(function (group) {
4625
4692
  return group.ranges;
4626
4693
  });
@@ -4653,6 +4720,8 @@ export var UnifiedBookingSalesImpl = /*#__PURE__*/function (_BookingTicket) {
4653
4720
  requirementSelections: params.requirementSelections,
4654
4721
  partySize: partySize,
4655
4722
  bookingCount: bookingCount
4723
+ }, {
4724
+ allowExcludedScheduleDateOverride: allowPosAvailabilityOverride
4656
4725
  });
4657
4726
  if (allowPosAvailabilityOverride && !validation.ok && validation.conflicts.every(function (conflict) {
4658
4727
  return POS_OVERRIDABLE_AVAILABILITY_CONFLICT_CODES.has(conflict.code);
@@ -1,5 +1,5 @@
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
3
  export declare function getProductTimeRanges(projection: AvailabilityProjection, request?: GetProductTimeRangesRequest, runtimeOptions?: GetProductTimeRangesRuntimeOptions): ProductTimeRangeGroup[];
4
- export declare function queryAvailabilityProjection(projection: AvailabilityProjection, request: AvailabilityQuery): AvailabilityQueryResult;
5
- export declare function validateAvailabilitySelection(projection: AvailabilityProjection, params: AvailabilitySelectionValidationParams): AvailabilitySelectionValidationResult;
4
+ export declare function queryAvailabilityProjection(projection: AvailabilityProjection, request: AvailabilityQuery, runtimeOptions?: GetProductTimeRangesRuntimeOptions): AvailabilityQueryResult;
5
+ export declare function validateAvailabilitySelection(projection: AvailabilityProjection, params: AvailabilitySelectionValidationParams, runtimeOptions?: GetProductTimeRangesRuntimeOptions): AvailabilitySelectionValidationResult;
@@ -166,9 +166,65 @@ function addLeadWindow(base, unit, unitType, _timezoneName) {
166
166
  if (unitType === 'hours') return (0, _localClock.addLocalHours)(base, unit);
167
167
  return (0, _localClock.addLocalMinutes)(base, unit);
168
168
  }
169
+ function normalizedWindowContainsRange(windows, candidateRange, timezoneName) {
170
+ return windows.some(window => {
171
+ const normalized = normalizeAbsoluteRange(window, timezoneName);
172
+ return Boolean(normalized && contains(normalized, candidateRange));
173
+ });
174
+ }
175
+ function normalizedWindowsContainDate(windows, date, timezoneName) {
176
+ return windows.some(window => {
177
+ const normalized = normalizeAbsoluteRange(window, timezoneName);
178
+ return Boolean(normalized && formatDate(normalized.start, timezoneName) === date);
179
+ });
180
+ }
181
+ function isRangeFromExcludedWindows(params) {
182
+ const candidateDate = formatDate(params.candidateRange.start, params.timezoneName);
183
+ const hasNormalWindowOnDate = normalizedWindowsContainDate(params.normalWindows, candidateDate, params.timezoneName);
184
+ const hasExcludedWindowOnDate = normalizedWindowsContainDate(params.excludedWindows, candidateDate, params.timezoneName);
185
+ if (!hasNormalWindowOnDate && hasExcludedWindowOnDate) return true;
186
+ return !normalizedWindowContainsRange(params.normalWindows, params.candidateRange, params.timezoneName) && normalizedWindowContainsRange(params.excludedWindows, params.candidateRange, params.timezoneName);
187
+ }
188
+ function isCandidateFromExcludedScheduleDate(params) {
189
+ const {
190
+ projection,
191
+ product,
192
+ candidate,
193
+ candidateRange
194
+ } = params;
195
+ const operatingHours = projection.meta.defaults.operatingHours;
196
+ if (isRangeFromExcludedWindows({
197
+ normalWindows: operatingHours.windows,
198
+ excludedWindows: operatingHours.excludedWindows || [],
199
+ candidateRange,
200
+ timezoneName: projection.meta.timezone
201
+ })) {
202
+ return true;
203
+ }
204
+ if (product.kind !== 'session' && product.kind !== 'venue_slot') {
205
+ return false;
206
+ }
207
+ if (candidate.source !== 'session_schedule') {
208
+ const hasNormalRule = (product.sessionRules || []).some(rule => matchesScheduleRuleDate(rule, candidate.date, projection.meta.timezone));
209
+ const hasExcludedOverrideRule = (product.sessionRules || []).some(rule => !matchesScheduleRuleDate(rule, candidate.date, projection.meta.timezone) && matchesScheduleRuleDate(rule, candidate.date, projection.meta.timezone, true));
210
+ return !hasNormalRule && hasExcludedOverrideRule;
211
+ }
212
+ const candidateDateRange = {
213
+ startDate: candidate.date,
214
+ endDate: candidate.date
215
+ };
216
+ const candidateIdentity = getProductTimeRangeIdentity(candidate);
217
+ const normalRanges = getSessionRangesForProduct(projection, product, candidateDateRange);
218
+ if (normalRanges.some(range => getProductTimeRangeIdentity(range) === candidateIdentity)) {
219
+ return false;
220
+ }
221
+ return getSessionRangesForProduct(projection, product, candidateDateRange, true).some(range => getProductTimeRangeIdentity(range) === candidateIdentity);
222
+ }
169
223
  function evaluateCandidateTimeGate(params) {
170
224
  const {
225
+ projection,
171
226
  product,
227
+ candidate,
172
228
  candidateRange,
173
229
  evaluationContext,
174
230
  timezoneName
@@ -181,6 +237,18 @@ function evaluateCandidateTimeGate(params) {
181
237
  conflictCode: 'past'
182
238
  };
183
239
  }
240
+ if (evaluationContext.allowExcludedScheduleDateOverride && isCandidateFromExcludedScheduleDate({
241
+ projection,
242
+ product,
243
+ candidate,
244
+ candidateRange
245
+ })) {
246
+ return {
247
+ status: 'unavailable',
248
+ reasonCodes: ['schedule_excluded_date'],
249
+ conflictCode: 'schedule_excluded_date'
250
+ };
251
+ }
184
252
  const policy = product.cutOffPolicy;
185
253
  if (!policy) return null;
186
254
  if (resolveCutOffPolicyIssue(policy)) {
@@ -324,8 +392,8 @@ function normalizeCoverage(input) {
324
392
  }
325
393
  };
326
394
  }
327
- function normalizeResourceWindow(resource, coverage, timezoneName) {
328
- return mergeRanges((resource.windows || []).map(window => normalizeAbsoluteRange(window, timezoneName)).filter(window => Boolean(window)).map(window => clipRange(window, coverage)).filter(window => Boolean(window)));
395
+ function normalizeResourceWindow(resource, coverage, timezoneName, includeExcludedWindows = false) {
396
+ return mergeRanges([...(resource.windows || []), ...(includeExcludedWindows ? resource.excludedWindows || [] : [])].map(window => normalizeAbsoluteRange(window, timezoneName)).filter(window => Boolean(window)).map(window => clipRange(window, coverage)).filter(window => Boolean(window)));
329
397
  }
330
398
  function normalizeOccupancy(occupancy, coverage, timezoneName, fixedOffsetMinutes) {
331
399
  const normalized = normalizeAbsoluteRange(occupancy, timezoneName);
@@ -389,6 +457,9 @@ function normalizeResources(resources) {
389
457
  capacity: Math.max(1, Number(resource.capacity || 1) || 1),
390
458
  windows: (resource.windows || []).map(window => ({
391
459
  ...window
460
+ })),
461
+ excludedWindows: (resource.excludedWindows || []).map(window => ({
462
+ ...window
392
463
  }))
393
464
  }));
394
465
  }
@@ -408,15 +479,20 @@ function normalizeOperatingHours(params) {
408
479
  startAt: formatDateTime(range.start, params.timezoneName, params.fixedOffsetMinutes),
409
480
  endAt: formatDateTime(range.end, params.timezoneName, params.fixedOffsetMinutes)
410
481
  }));
411
- const windows = mergeRanges(sourceWindows.map(window => normalizeAbsoluteRange(window, params.timezoneName)).filter(window => Boolean(window)).map(window => clipRange(window, params.coverage)).filter(window => Boolean(window))).map(range => ({
482
+ const normalizeWindows = source => mergeRanges(source.map(window => normalizeAbsoluteRange(window, params.timezoneName)).filter(window => Boolean(window)).map(window => clipRange(window, params.coverage)).filter(window => Boolean(window))).map(range => ({
412
483
  startAt: formatDateTime(range.start, params.timezoneName, params.fixedOffsetMinutes),
413
484
  endAt: formatDateTime(range.end, params.timezoneName, params.fixedOffsetMinutes)
414
485
  }));
486
+ const windows = normalizeWindows(sourceWindows);
487
+ const excludedWindows = normalizeWindows(params.input?.excludedWindows || []);
415
488
  return {
416
489
  source: params.input?.source || params.fallbackSource,
417
490
  scheduleIds: uniqById(params.input?.scheduleIds || []),
418
491
  missingScheduleIds: uniqById(params.input?.missingScheduleIds || []),
419
- windows
492
+ windows,
493
+ ...(excludedWindows.length > 0 ? {
494
+ excludedWindows
495
+ } : {})
420
496
  };
421
497
  }
422
498
  function matchesRequirementGroup(requirementGroup, resource, product, partySize = 1) {
@@ -698,19 +774,19 @@ function resolveProjectionDateRange(projection, requestDateRange) {
698
774
  endDate
699
775
  };
700
776
  }
701
- function buildOperatingRanges(projection, dateRange) {
777
+ function buildOperatingRanges(projection, dateRange, includeExcludedWindows = false) {
702
778
  const coverage = {
703
779
  start: toDateStart(dateRange.startDate, projection.meta.timezone),
704
780
  end: toDateEnd(nextDate(dateRange.endDate), projection.meta.timezone)
705
781
  };
706
- return mergeRanges(projection.meta.defaults.operatingHours.windows.map(window => normalizeAbsoluteRange(window, projection.meta.timezone)).filter(window => Boolean(window)).map(window => clipRange(window, coverage)).filter(window => Boolean(window)));
782
+ return mergeRanges([...projection.meta.defaults.operatingHours.windows, ...(includeExcludedWindows ? projection.meta.defaults.operatingHours.excludedWindows || [] : [])].map(window => normalizeAbsoluteRange(window, projection.meta.timezone)).filter(window => Boolean(window)).map(window => clipRange(window, coverage)).filter(window => Boolean(window)));
707
783
  }
708
- function buildResourceWindowIndex(projection) {
784
+ function buildResourceWindowIndex(projection, includeExcludedWindows = false) {
709
785
  const coverage = {
710
786
  start: toDateTime(projection.meta.coverage.startAt, projection.meta.timezone),
711
787
  end: toDateTime(projection.meta.coverage.endAt, projection.meta.timezone)
712
788
  };
713
- return new Map(projection.resources.map(resource => [String(resource.id), normalizeResourceWindow(resource, coverage, projection.meta.timezone)]));
789
+ return new Map(projection.resources.map(resource => [String(resource.id), normalizeResourceWindow(resource, coverage, projection.meta.timezone, includeExcludedWindows)]));
714
790
  }
715
791
  function buildGroupWorkingRanges(params) {
716
792
  const {
@@ -727,7 +803,7 @@ function buildGroupWorkingRanges(params) {
727
803
  };
728
804
  const filteredResourceIdSet = resourceIds?.length ? new Set(resourceIds.map(String)) : null;
729
805
  const resourceWindows = projection.resources.filter(resource => matchesRequirementGroup(requirementGroup, resource, params.product, partySize)).filter(resource => !filteredResourceIdSet || filteredResourceIdSet.has(String(resource.id))).map(resource => {
730
- const windows = params.resourceWindowIndex?.get(String(resource.id)) || normalizeResourceWindow(resource, coverage, projection.meta.timezone);
806
+ const windows = params.resourceWindowIndex?.get(String(resource.id)) || normalizeResourceWindow(resource, coverage, projection.meta.timezone, params.includeExcludedWindows === true);
731
807
  return windows.map(window => clipRange(window, coverage)).filter(window => Boolean(window));
732
808
  });
733
809
  const requiredConcurrentResources = Math.max(requirementGroup.required === false ? 0 : 1, Number(requirementGroup.min ?? (requirementGroup.required === false ? 0 : 1)) || 0);
@@ -758,12 +834,12 @@ function buildGroupWorkingRanges(params) {
758
834
  }
759
835
  return mergeRanges(satisfiedRanges);
760
836
  }
761
- function materializeScheduleRules(rules, dateRange, timezoneName, fixedOffsetMinutes) {
837
+ function materializeScheduleRules(rules, dateRange, timezoneName, fixedOffsetMinutes, allowExcludedScheduleDates = false) {
762
838
  const dates = enumerateDates(dateRange.startDate, dateRange.endDate);
763
839
  const seen = new Map();
764
840
  rules.forEach(rule => {
765
841
  dates.forEach(date => {
766
- if (!matchesScheduleRuleDate(rule, date, timezoneName)) return;
842
+ if (!matchesScheduleRuleDate(rule, date, timezoneName, allowExcludedScheduleDates)) return;
767
843
  const start = toDateTimeInDate(date, rule.startTime, timezoneName);
768
844
  let end = toDateTimeInDate(date, rule.endTime, timezoneName);
769
845
  if (end <= start) end = toDateTimeInDate(nextDate(date, timezoneName), rule.endTime, timezoneName);
@@ -795,9 +871,9 @@ function materializeScheduleRules(rules, dateRange, timezoneName, fixedOffsetMin
795
871
  });
796
872
  return Array.from(seen.values());
797
873
  }
798
- function matchesScheduleRuleDate(rule, date, _timezoneName) {
874
+ function matchesScheduleRuleDate(rule, date, _timezoneName, allowExcludedScheduleDate = false) {
799
875
  if (rule.includeDates?.includes(date)) return true;
800
- if (rule.excludeDates?.includes(date)) return false;
876
+ if (!allowExcludedScheduleDate && rule.excludeDates?.includes(date)) return false;
801
877
  if (rule.startDate && date < rule.startDate) return false;
802
878
  if (rule.endDate && date > rule.endDate) return false;
803
879
  if (rule.mode === 'designation') {
@@ -841,7 +917,7 @@ function buildGroupResourceIndex(params) {
841
917
  });
842
918
  return index;
843
919
  }
844
- function getSessionRangesForProduct(projection, product, dateRange) {
920
+ function getSessionRangesForProduct(projection, product, dateRange, allowExcludedScheduleDates = false) {
845
921
  const fixedOffsetMinutes = (0, _localClock.extractFixedOffsetMinutes)(projection.meta.now) ?? (0, _localClock.captureRuntimeOffsetMinutes)();
846
922
  const ranges = (product.sessionRanges || []).map(range => {
847
923
  const normalized = normalizeAbsoluteRange(range, projection.meta.timezone);
@@ -866,7 +942,7 @@ function getSessionRangesForProduct(projection, product, dateRange) {
866
942
  primaryScheduleRef: range.scheduleRefs?.[0]
867
943
  };
868
944
  }).filter(Boolean);
869
- const ruleRanges = materializeScheduleRules(product.sessionRules || [], dateRange, projection.meta.timezone, fixedOffsetMinutes);
945
+ const ruleRanges = materializeScheduleRules(product.sessionRules || [], dateRange, projection.meta.timezone, fixedOffsetMinutes, allowExcludedScheduleDates);
870
946
  return dedupeProductRanges([...ranges, ...ruleRanges.map(range => ({
871
947
  ...range,
872
948
  key: `${String(product.id)}:${range.startAt}:${range.endAt}`,
@@ -916,7 +992,8 @@ function getProductTimeRanges(projection, request = {}, runtimeOptions = {}) {
916
992
  const dateRange = resolveProjectionDateRange(projection, request.dateRange);
917
993
  const slotStepMinutes = projection.meta.defaults.slotStepMinutes;
918
994
  const fixedOffsetMinutes = (0, _localClock.extractFixedOffsetMinutes)(projection.meta.now) ?? (0, _localClock.captureRuntimeOffsetMinutes)();
919
- const resourceWindowIndex = buildResourceWindowIndex(projection);
995
+ const allowExcludedScheduleDateOverride = runtimeOptions.allowExcludedScheduleDateOverride === true;
996
+ const resourceWindowIndex = buildResourceWindowIndex(projection, allowExcludedScheduleDateOverride);
920
997
  const filteredProductIdSet = request.productIds?.length ? new Set(request.productIds.map(String)) : null;
921
998
  return projection.products.filter(product => !filteredProductIdSet || filteredProductIdSet.has(String(product.id))).map(product => {
922
999
  if (product.kind === 'normal') {
@@ -929,7 +1006,7 @@ function getProductTimeRanges(projection, request = {}, runtimeOptions = {}) {
929
1006
  };
930
1007
  }
931
1008
  if (product.kind === 'session' || product.kind === 'venue_slot') {
932
- const fixedRanges = getSessionRangesForProduct(projection, product, dateRange);
1009
+ const fixedRanges = getSessionRangesForProduct(projection, product, dateRange, allowExcludedScheduleDateOverride);
933
1010
  const customRanges = [];
934
1011
  const customDurationMinutes = resolveRequestedSessionDurationMinutes(product, request, runtimeOptions.allowSessionCustomRanges === true);
935
1012
  const requestedStartTimes = resolveRequestedDurationStartTimes(product, request, runtimeOptions.allowSessionCustomRanges === true);
@@ -949,7 +1026,8 @@ function getProductTimeRanges(projection, request = {}, runtimeOptions = {}) {
949
1026
  };
950
1027
  }
951
1028
  const requiredGroups = (product.requirementGroups || []).filter(group => group.required !== false);
952
- const operatingRanges = buildOperatingRanges(projection, dateRange);
1029
+ const operatingRanges = buildOperatingRanges(projection, dateRange, allowExcludedScheduleDateOverride);
1030
+ const operatingEndAt = dateRange.startDate === dateRange.endDate && operatingRanges.length > 0 ? formatDateTime(Math.max(...operatingRanges.map(range => range.end)), projection.meta.timezone, fixedOffsetMinutes) : undefined;
953
1031
  const resourceRanges = requiredGroups.length ? requiredGroups.map(group => buildGroupWorkingRanges({
954
1032
  projection,
955
1033
  product,
@@ -957,7 +1035,8 @@ function getProductTimeRanges(projection, request = {}, runtimeOptions = {}) {
957
1035
  dateRange,
958
1036
  resourceIds: request.resourceIds,
959
1037
  partySize: normalizedPartySize,
960
- resourceWindowIndex
1038
+ resourceWindowIndex,
1039
+ includeExcludedWindows: allowExcludedScheduleDateOverride
961
1040
  })).reduce((current, ranges) => current === null ? ranges : intersectRangeLists(current, ranges), null) || [] : null;
962
1041
  const baseRanges = resourceRanges === null ? operatingRanges : intersectRangeLists(resourceRanges, operatingRanges);
963
1042
  const durationMinutes = resolveRequestedDurationMinutes(product, request);
@@ -1016,6 +1095,9 @@ function getProductTimeRanges(projection, request = {}, runtimeOptions = {}) {
1016
1095
  title: product.title,
1017
1096
  kind: product.kind,
1018
1097
  slotStepMinutes,
1098
+ ...(operatingEndAt ? {
1099
+ operatingEndAt
1100
+ } : {}),
1019
1101
  ranges: dedupeProductRanges(ranges)
1020
1102
  };
1021
1103
  });
@@ -1173,7 +1255,8 @@ function buildEvaluationContext(params) {
1173
1255
  evaluatedAt,
1174
1256
  evaluatedAtMs: evaluatedAt,
1175
1257
  resourceIdSet: params.resourceIds?.length ? new Set(params.resourceIds.map(String)) : null,
1176
- selectionsByGroupKey
1258
+ selectionsByGroupKey,
1259
+ allowExcludedScheduleDateOverride: params.allowExcludedScheduleDateOverride === true
1177
1260
  };
1178
1261
  }
1179
1262
  function summarizeStatuses(statuses) {
@@ -1323,7 +1406,8 @@ function evaluateCandidateSummary(projection, candidate, request) {
1323
1406
  const evaluationContext = request.evaluationContext || buildEvaluationContext({
1324
1407
  projection,
1325
1408
  evaluatedAt: request.evaluatedAt,
1326
- resourceIds: request.resourceIds
1409
+ resourceIds: request.resourceIds,
1410
+ allowExcludedScheduleDateOverride: request.allowExcludedScheduleDateOverride
1327
1411
  });
1328
1412
  const requestedPartySize = normalizePartySize(request.partySize);
1329
1413
  const requiredGroups = (product.requirementGroups || []).filter(group => group.required !== false).map(requirementGroup => evaluateRequirementGroupSummary({
@@ -1337,7 +1421,9 @@ function evaluateCandidateSummary(projection, candidate, request) {
1337
1421
  groupResourceIndex: request.groupResourceIndex
1338
1422
  }));
1339
1423
  const gate = evaluateCandidateTimeGate({
1424
+ projection,
1340
1425
  product,
1426
+ candidate,
1341
1427
  candidateRange,
1342
1428
  evaluationContext,
1343
1429
  timezoneName: projection.meta.timezone
@@ -1379,7 +1465,8 @@ function evaluateCandidate(projection, candidate, request) {
1379
1465
  projection,
1380
1466
  evaluatedAt: request.evaluatedAt,
1381
1467
  resourceIds: request.resourceIds,
1382
- requirementSelections: request.requirementSelections
1468
+ requirementSelections: request.requirementSelections,
1469
+ allowExcludedScheduleDateOverride: request.allowExcludedScheduleDateOverride
1383
1470
  });
1384
1471
  const requestedPartySize = normalizePartySize(request.partySize);
1385
1472
  const partySize = requestedPartySize ?? 1;
@@ -1392,6 +1479,12 @@ function evaluateCandidate(projection, candidate, request) {
1392
1479
  const matchedResources = (request.groupResourceIndex?.get([String(product.id), requirementGroup.id].join(':')) || projection.resources.filter(resource => matchesRequirementGroup(requirementGroup, resource, product, partySize)).filter(resource => !evaluationContext.resourceIdSet || evaluationContext.resourceIdSet.has(String(resource.id)))).filter(resource => !selection || selection.resourceIds.some(resourceId => sameId(resourceId, resource.id)));
1393
1480
  const options = matchedResources.map(resource => {
1394
1481
  const matchedCell = aggregateMatchedCells(product.id, requirementGroup.id, resource.id, candidateRange, request.cellIndex?.get([String(product.id), requirementGroup.id, String(resource.id)].join(':')) || []);
1482
+ const isExcludedScheduleDate = evaluationContext.allowExcludedScheduleDateOverride && isRangeFromExcludedWindows({
1483
+ normalWindows: resource.windows || [],
1484
+ excludedWindows: resource.excludedWindows || [],
1485
+ candidateRange,
1486
+ timezoneName: projection.meta.timezone
1487
+ });
1395
1488
  if (!matchedCell) {
1396
1489
  const isPast = candidateRange.endMs <= evaluationContext.evaluatedAtMs;
1397
1490
  return {
@@ -1402,7 +1495,7 @@ function evaluateCandidate(projection, candidate, request) {
1402
1495
  resourceType: resource.resourceType,
1403
1496
  status: isPast ? 'past' : 'unavailable',
1404
1497
  maxPartySize: 0,
1405
- reasonCodes: isPast ? ['past'] : ['outside_resource_window']
1498
+ reasonCodes: isPast ? ['past'] : [...(isExcludedScheduleDate ? ['schedule_excluded_date'] : []), 'outside_resource_window']
1406
1499
  };
1407
1500
  }
1408
1501
  const remainingCapacity = matchedCell.remainingCapacity;
@@ -1410,6 +1503,7 @@ function evaluateCandidate(projection, candidate, request) {
1410
1503
  const isPast = candidateRange.endMs <= evaluationContext.evaluatedAtMs;
1411
1504
  const maxPartySize = isPast ? 0 : Math.max(0, remainingCapacity || 0);
1412
1505
  const requiredCapacity = resolveRequiredCapacity(requirementGroup, product, requestedPartySize);
1506
+ const reasonCodes = Array.from(new Set([...matchedCell.reasonCodes, ...(isExcludedScheduleDate ? ['schedule_excluded_date'] : [])]));
1413
1507
  const status = (() => {
1414
1508
  if (isPast) return 'past';
1415
1509
  if (matchedCell.status === 'unavailable') return 'unavailable';
@@ -1427,7 +1521,7 @@ function evaluateCandidate(projection, candidate, request) {
1427
1521
  remainingCapacity,
1428
1522
  maxCapacity,
1429
1523
  maxPartySize,
1430
- reasonCodes: isPast ? Array.from(new Set([...matchedCell.reasonCodes, 'past'])) : matchedCell.reasonCodes,
1524
+ reasonCodes: isPast ? Array.from(new Set([...reasonCodes, 'past'])) : reasonCodes,
1431
1525
  matchedCell
1432
1526
  };
1433
1527
  });
@@ -1467,7 +1561,9 @@ function evaluateCandidate(projection, candidate, request) {
1467
1561
  const requiredGroups = requirementGroups.filter(group => group.required);
1468
1562
  const requiredStatuses = requiredGroups.map(group => group.status);
1469
1563
  const gate = evaluateCandidateTimeGate({
1564
+ projection,
1470
1565
  product,
1566
+ candidate,
1471
1567
  candidateRange,
1472
1568
  evaluationContext,
1473
1569
  timezoneName: projection.meta.timezone
@@ -1543,7 +1639,7 @@ function assertRangesWithinProjection(projection, ranges) {
1543
1639
  }
1544
1640
  });
1545
1641
  }
1546
- function queryAvailabilityProjection(projection, request) {
1642
+ function queryAvailabilityProjection(projection, request, runtimeOptions = {}) {
1547
1643
  const normalizedPartySize = normalizePartySize(request.partySize);
1548
1644
  const evaluatedAt = formatDateTime((0, _localClock.localDateToScalar)(new Date()), projection.meta.timezone, (0, _localClock.extractFixedOffsetMinutes)(projection.meta.now) ?? (0, _localClock.captureRuntimeOffsetMinutes)());
1549
1645
  if (request.target === 'time') {
@@ -1566,7 +1662,8 @@ function queryAvailabilityProjection(projection, request) {
1566
1662
  const evaluationContext = buildEvaluationContext({
1567
1663
  projection,
1568
1664
  evaluatedAt,
1569
- resourceIds: request.resourceIds
1665
+ resourceIds: request.resourceIds,
1666
+ allowExcludedScheduleDateOverride: runtimeOptions.allowExcludedScheduleDateOverride
1570
1667
  });
1571
1668
  if (request.target === 'time') {
1572
1669
  const candidates = dedupeProductRanges(request.candidates);
@@ -1640,7 +1737,8 @@ function queryAvailabilityProjection(projection, request) {
1640
1737
  resourceIds: request.resourceIds,
1641
1738
  partySize: normalizedPartySize
1642
1739
  }, {
1643
- evaluatedAt
1740
+ evaluatedAt,
1741
+ allowExcludedScheduleDateOverride: runtimeOptions.allowExcludedScheduleDateOverride
1644
1742
  });
1645
1743
  const candidateGroupMap = new Map(candidateGroups.map(group => [String(group.productId), group]));
1646
1744
  if (request.target === 'date') {
@@ -1790,7 +1888,7 @@ function summarizeProductStatus(statuses) {
1790
1888
  if (statuses.every(status => status === 'past')) return 'past';
1791
1889
  return 'unavailable';
1792
1890
  }
1793
- function validateAvailabilitySelection(projection, params) {
1891
+ function validateAvailabilitySelection(projection, params, runtimeOptions = {}) {
1794
1892
  const normalizedPartySize = normalizePartySize(params.partySize);
1795
1893
  const normalizedBookingCount = normalizeBookingCount(params.bookingCount);
1796
1894
  assertRangesWithinProjection(projection, [params.candidate]);
@@ -1811,14 +1909,15 @@ function validateAvailabilitySelection(projection, params) {
1811
1909
  evaluationContext: buildEvaluationContext({
1812
1910
  projection,
1813
1911
  evaluatedAt,
1814
- requirementSelections: params.requirementSelections
1912
+ requirementSelections: params.requirementSelections,
1913
+ allowExcludedScheduleDateOverride: runtimeOptions.allowExcludedScheduleDateOverride
1815
1914
  })
1816
1915
  });
1817
1916
  const conflicts = [];
1818
1917
  if (evaluated.gate) {
1819
1918
  conflicts.push({
1820
1919
  code: evaluated.gate.conflictCode,
1821
- message: evaluated.gate.status === 'past' ? '候选时间已过期' : '候选时间不满足预订时间门禁',
1920
+ message: evaluated.gate.conflictCode === 'past' ? '候选时间已过期' : evaluated.gate.conflictCode === 'schedule_excluded_date' ? '候选日期已被日程排除' : '候选时间不满足预订时间门禁',
1822
1921
  reasonCodes: evaluated.gate.reasonCodes,
1823
1922
  productId: params.candidate.productId,
1824
1923
  candidateKey: params.candidate.key
@@ -101,6 +101,9 @@ export interface AvailabilityOperatingHoursSnapshot {
101
101
  scheduleIds: AvailabilityId[];
102
102
  missingScheduleIds: AvailabilityId[];
103
103
  windows: AvailabilityResourceWindow[];
104
+ /** Schedule windows omitted only because their date is excluded. POS may use
105
+ * these to surface an unavailable, manually selectable candidate. */
106
+ excludedWindows?: AvailabilityResourceWindow[];
104
107
  }
105
108
  export interface AvailabilityResource {
106
109
  id: AvailabilityId;
@@ -110,6 +113,8 @@ export interface AvailabilityResource {
110
113
  resourceType: AvailabilityResourceType;
111
114
  capacity: number;
112
115
  windows: AvailabilityResourceWindow[];
116
+ /** Resource work windows omitted only by an excluded schedule date. */
117
+ excludedWindows?: AvailabilityResourceWindow[];
113
118
  raw?: Record<string, any>;
114
119
  }
115
120
  export interface AvailabilityOccupancy extends AvailabilityAbsoluteRange {
@@ -213,6 +218,8 @@ export interface ProductTimeRangeGroup {
213
218
  title?: string;
214
219
  kind: AvailabilityProductKind;
215
220
  slotStepMinutes?: number;
221
+ /** 单日查询对应营业窗口的最终结束时刻,不受资源排班是否为空影响。 */
222
+ operatingEndAt?: string;
216
223
  ranges: ProductTimeRange[];
217
224
  }
218
225
  export interface AvailabilityAssignment extends AvailabilityAbsoluteRange {
@@ -250,6 +257,11 @@ export interface GetProductTimeRangesRuntimeOptions {
250
257
  * disabled.
251
258
  */
252
259
  allowSessionCustomRanges?: boolean;
260
+ /**
261
+ * Internal POS capability: materialize candidates whose schedule date was
262
+ * explicitly excluded, while availability evaluation keeps them unavailable.
263
+ */
264
+ allowExcludedScheduleDateOverride?: boolean;
253
265
  }
254
266
  export interface QueryAvailabilityBaseRequest {
255
267
  productIds?: AvailabilityId[];