@pisell/pisellos 2.2.299 → 2.2.301
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.
- package/dist/modules/ResourcePlanner/planner.d.ts +2 -2
- package/dist/modules/ResourcePlanner/planner.js +163 -46
- package/dist/modules/ResourcePlanner/types.d.ts +17 -0
- package/dist/solution/UnifiedBookingSales/index.js +92 -19
- package/lib/model/strategy/adapter/promotion/index.js +46 -1
- package/lib/modules/ResourcePlanner/planner.d.ts +2 -2
- package/lib/modules/ResourcePlanner/planner.js +137 -35
- package/lib/modules/ResourcePlanner/types.d.ts +17 -0
- package/lib/solution/UnifiedBookingSales/index.js +84 -20
- package/package.json +1 -1
|
@@ -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)) {
|
|
@@ -213,10 +281,12 @@ function evaluateCandidateTimeGate(params) {
|
|
|
213
281
|
};
|
|
214
282
|
}
|
|
215
283
|
if (policy.type === 'before_start') {
|
|
216
|
-
// Flexible-duration candidates
|
|
217
|
-
// minute
|
|
218
|
-
// already started because the query
|
|
219
|
-
|
|
284
|
+
// Flexible-duration candidates and POS fixed-duration anchors start at
|
|
285
|
+
// minute precision. Keep that current minute selectable instead of treating
|
|
286
|
+
// the freshly generated candidate as already started because the query
|
|
287
|
+
// clock still contains seconds.
|
|
288
|
+
const usesMinutePrecision = product.isFlexibleDuration || evaluationContext.useExactDurationCandidateMinute && product.kind === 'duration';
|
|
289
|
+
const hasNotReachedStart = usesMinutePrecision ? floorToMinute(now) <= candidateRange.start : now < candidateRange.start;
|
|
220
290
|
if (hasNotReachedStart) return null;
|
|
221
291
|
return {
|
|
222
292
|
status: 'unavailable',
|
|
@@ -324,8 +394,8 @@ function normalizeCoverage(input) {
|
|
|
324
394
|
}
|
|
325
395
|
};
|
|
326
396
|
}
|
|
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)));
|
|
397
|
+
function normalizeResourceWindow(resource, coverage, timezoneName, includeExcludedWindows = false) {
|
|
398
|
+
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
399
|
}
|
|
330
400
|
function normalizeOccupancy(occupancy, coverage, timezoneName, fixedOffsetMinutes) {
|
|
331
401
|
const normalized = normalizeAbsoluteRange(occupancy, timezoneName);
|
|
@@ -389,6 +459,9 @@ function normalizeResources(resources) {
|
|
|
389
459
|
capacity: Math.max(1, Number(resource.capacity || 1) || 1),
|
|
390
460
|
windows: (resource.windows || []).map(window => ({
|
|
391
461
|
...window
|
|
462
|
+
})),
|
|
463
|
+
excludedWindows: (resource.excludedWindows || []).map(window => ({
|
|
464
|
+
...window
|
|
392
465
|
}))
|
|
393
466
|
}));
|
|
394
467
|
}
|
|
@@ -408,15 +481,20 @@ function normalizeOperatingHours(params) {
|
|
|
408
481
|
startAt: formatDateTime(range.start, params.timezoneName, params.fixedOffsetMinutes),
|
|
409
482
|
endAt: formatDateTime(range.end, params.timezoneName, params.fixedOffsetMinutes)
|
|
410
483
|
}));
|
|
411
|
-
const
|
|
484
|
+
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
485
|
startAt: formatDateTime(range.start, params.timezoneName, params.fixedOffsetMinutes),
|
|
413
486
|
endAt: formatDateTime(range.end, params.timezoneName, params.fixedOffsetMinutes)
|
|
414
487
|
}));
|
|
488
|
+
const windows = normalizeWindows(sourceWindows);
|
|
489
|
+
const excludedWindows = normalizeWindows(params.input?.excludedWindows || []);
|
|
415
490
|
return {
|
|
416
491
|
source: params.input?.source || params.fallbackSource,
|
|
417
492
|
scheduleIds: uniqById(params.input?.scheduleIds || []),
|
|
418
493
|
missingScheduleIds: uniqById(params.input?.missingScheduleIds || []),
|
|
419
|
-
windows
|
|
494
|
+
windows,
|
|
495
|
+
...(excludedWindows.length > 0 ? {
|
|
496
|
+
excludedWindows
|
|
497
|
+
} : {})
|
|
420
498
|
};
|
|
421
499
|
}
|
|
422
500
|
function matchesRequirementGroup(requirementGroup, resource, product, partySize = 1) {
|
|
@@ -698,19 +776,19 @@ function resolveProjectionDateRange(projection, requestDateRange) {
|
|
|
698
776
|
endDate
|
|
699
777
|
};
|
|
700
778
|
}
|
|
701
|
-
function buildOperatingRanges(projection, dateRange) {
|
|
779
|
+
function buildOperatingRanges(projection, dateRange, includeExcludedWindows = false) {
|
|
702
780
|
const coverage = {
|
|
703
781
|
start: toDateStart(dateRange.startDate, projection.meta.timezone),
|
|
704
782
|
end: toDateEnd(nextDate(dateRange.endDate), projection.meta.timezone)
|
|
705
783
|
};
|
|
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)));
|
|
784
|
+
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
785
|
}
|
|
708
|
-
function buildResourceWindowIndex(projection) {
|
|
786
|
+
function buildResourceWindowIndex(projection, includeExcludedWindows = false) {
|
|
709
787
|
const coverage = {
|
|
710
788
|
start: toDateTime(projection.meta.coverage.startAt, projection.meta.timezone),
|
|
711
789
|
end: toDateTime(projection.meta.coverage.endAt, projection.meta.timezone)
|
|
712
790
|
};
|
|
713
|
-
return new Map(projection.resources.map(resource => [String(resource.id), normalizeResourceWindow(resource, coverage, projection.meta.timezone)]));
|
|
791
|
+
return new Map(projection.resources.map(resource => [String(resource.id), normalizeResourceWindow(resource, coverage, projection.meta.timezone, includeExcludedWindows)]));
|
|
714
792
|
}
|
|
715
793
|
function buildGroupWorkingRanges(params) {
|
|
716
794
|
const {
|
|
@@ -727,7 +805,7 @@ function buildGroupWorkingRanges(params) {
|
|
|
727
805
|
};
|
|
728
806
|
const filteredResourceIdSet = resourceIds?.length ? new Set(resourceIds.map(String)) : null;
|
|
729
807
|
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);
|
|
808
|
+
const windows = params.resourceWindowIndex?.get(String(resource.id)) || normalizeResourceWindow(resource, coverage, projection.meta.timezone, params.includeExcludedWindows === true);
|
|
731
809
|
return windows.map(window => clipRange(window, coverage)).filter(window => Boolean(window));
|
|
732
810
|
});
|
|
733
811
|
const requiredConcurrentResources = Math.max(requirementGroup.required === false ? 0 : 1, Number(requirementGroup.min ?? (requirementGroup.required === false ? 0 : 1)) || 0);
|
|
@@ -758,12 +836,12 @@ function buildGroupWorkingRanges(params) {
|
|
|
758
836
|
}
|
|
759
837
|
return mergeRanges(satisfiedRanges);
|
|
760
838
|
}
|
|
761
|
-
function materializeScheduleRules(rules, dateRange, timezoneName, fixedOffsetMinutes) {
|
|
839
|
+
function materializeScheduleRules(rules, dateRange, timezoneName, fixedOffsetMinutes, allowExcludedScheduleDates = false) {
|
|
762
840
|
const dates = enumerateDates(dateRange.startDate, dateRange.endDate);
|
|
763
841
|
const seen = new Map();
|
|
764
842
|
rules.forEach(rule => {
|
|
765
843
|
dates.forEach(date => {
|
|
766
|
-
if (!matchesScheduleRuleDate(rule, date, timezoneName)) return;
|
|
844
|
+
if (!matchesScheduleRuleDate(rule, date, timezoneName, allowExcludedScheduleDates)) return;
|
|
767
845
|
const start = toDateTimeInDate(date, rule.startTime, timezoneName);
|
|
768
846
|
let end = toDateTimeInDate(date, rule.endTime, timezoneName);
|
|
769
847
|
if (end <= start) end = toDateTimeInDate(nextDate(date, timezoneName), rule.endTime, timezoneName);
|
|
@@ -795,9 +873,9 @@ function materializeScheduleRules(rules, dateRange, timezoneName, fixedOffsetMin
|
|
|
795
873
|
});
|
|
796
874
|
return Array.from(seen.values());
|
|
797
875
|
}
|
|
798
|
-
function matchesScheduleRuleDate(rule, date, _timezoneName) {
|
|
876
|
+
function matchesScheduleRuleDate(rule, date, _timezoneName, allowExcludedScheduleDate = false) {
|
|
799
877
|
if (rule.includeDates?.includes(date)) return true;
|
|
800
|
-
if (rule.excludeDates?.includes(date)) return false;
|
|
878
|
+
if (!allowExcludedScheduleDate && rule.excludeDates?.includes(date)) return false;
|
|
801
879
|
if (rule.startDate && date < rule.startDate) return false;
|
|
802
880
|
if (rule.endDate && date > rule.endDate) return false;
|
|
803
881
|
if (rule.mode === 'designation') {
|
|
@@ -841,7 +919,7 @@ function buildGroupResourceIndex(params) {
|
|
|
841
919
|
});
|
|
842
920
|
return index;
|
|
843
921
|
}
|
|
844
|
-
function getSessionRangesForProduct(projection, product, dateRange) {
|
|
922
|
+
function getSessionRangesForProduct(projection, product, dateRange, allowExcludedScheduleDates = false) {
|
|
845
923
|
const fixedOffsetMinutes = (0, _localClock.extractFixedOffsetMinutes)(projection.meta.now) ?? (0, _localClock.captureRuntimeOffsetMinutes)();
|
|
846
924
|
const ranges = (product.sessionRanges || []).map(range => {
|
|
847
925
|
const normalized = normalizeAbsoluteRange(range, projection.meta.timezone);
|
|
@@ -866,7 +944,7 @@ function getSessionRangesForProduct(projection, product, dateRange) {
|
|
|
866
944
|
primaryScheduleRef: range.scheduleRefs?.[0]
|
|
867
945
|
};
|
|
868
946
|
}).filter(Boolean);
|
|
869
|
-
const ruleRanges = materializeScheduleRules(product.sessionRules || [], dateRange, projection.meta.timezone, fixedOffsetMinutes);
|
|
947
|
+
const ruleRanges = materializeScheduleRules(product.sessionRules || [], dateRange, projection.meta.timezone, fixedOffsetMinutes, allowExcludedScheduleDates);
|
|
870
948
|
return dedupeProductRanges([...ranges, ...ruleRanges.map(range => ({
|
|
871
949
|
...range,
|
|
872
950
|
key: `${String(product.id)}:${range.startAt}:${range.endAt}`,
|
|
@@ -916,7 +994,8 @@ function getProductTimeRanges(projection, request = {}, runtimeOptions = {}) {
|
|
|
916
994
|
const dateRange = resolveProjectionDateRange(projection, request.dateRange);
|
|
917
995
|
const slotStepMinutes = projection.meta.defaults.slotStepMinutes;
|
|
918
996
|
const fixedOffsetMinutes = (0, _localClock.extractFixedOffsetMinutes)(projection.meta.now) ?? (0, _localClock.captureRuntimeOffsetMinutes)();
|
|
919
|
-
const
|
|
997
|
+
const allowExcludedScheduleDateOverride = runtimeOptions.allowExcludedScheduleDateOverride === true;
|
|
998
|
+
const resourceWindowIndex = buildResourceWindowIndex(projection, allowExcludedScheduleDateOverride);
|
|
920
999
|
const filteredProductIdSet = request.productIds?.length ? new Set(request.productIds.map(String)) : null;
|
|
921
1000
|
return projection.products.filter(product => !filteredProductIdSet || filteredProductIdSet.has(String(product.id))).map(product => {
|
|
922
1001
|
if (product.kind === 'normal') {
|
|
@@ -929,7 +1008,7 @@ function getProductTimeRanges(projection, request = {}, runtimeOptions = {}) {
|
|
|
929
1008
|
};
|
|
930
1009
|
}
|
|
931
1010
|
if (product.kind === 'session' || product.kind === 'venue_slot') {
|
|
932
|
-
const fixedRanges = getSessionRangesForProduct(projection, product, dateRange);
|
|
1011
|
+
const fixedRanges = getSessionRangesForProduct(projection, product, dateRange, allowExcludedScheduleDateOverride);
|
|
933
1012
|
const customRanges = [];
|
|
934
1013
|
const customDurationMinutes = resolveRequestedSessionDurationMinutes(product, request, runtimeOptions.allowSessionCustomRanges === true);
|
|
935
1014
|
const requestedStartTimes = resolveRequestedDurationStartTimes(product, request, runtimeOptions.allowSessionCustomRanges === true);
|
|
@@ -949,7 +1028,7 @@ function getProductTimeRanges(projection, request = {}, runtimeOptions = {}) {
|
|
|
949
1028
|
};
|
|
950
1029
|
}
|
|
951
1030
|
const requiredGroups = (product.requirementGroups || []).filter(group => group.required !== false);
|
|
952
|
-
const operatingRanges = buildOperatingRanges(projection, dateRange);
|
|
1031
|
+
const operatingRanges = buildOperatingRanges(projection, dateRange, allowExcludedScheduleDateOverride);
|
|
953
1032
|
const operatingEndAt = dateRange.startDate === dateRange.endDate && operatingRanges.length > 0 ? formatDateTime(Math.max(...operatingRanges.map(range => range.end)), projection.meta.timezone, fixedOffsetMinutes) : undefined;
|
|
954
1033
|
const resourceRanges = requiredGroups.length ? requiredGroups.map(group => buildGroupWorkingRanges({
|
|
955
1034
|
projection,
|
|
@@ -958,7 +1037,8 @@ function getProductTimeRanges(projection, request = {}, runtimeOptions = {}) {
|
|
|
958
1037
|
dateRange,
|
|
959
1038
|
resourceIds: request.resourceIds,
|
|
960
1039
|
partySize: normalizedPartySize,
|
|
961
|
-
resourceWindowIndex
|
|
1040
|
+
resourceWindowIndex,
|
|
1041
|
+
includeExcludedWindows: allowExcludedScheduleDateOverride
|
|
962
1042
|
})).reduce((current, ranges) => current === null ? ranges : intersectRangeLists(current, ranges), null) || [] : null;
|
|
963
1043
|
const baseRanges = resourceRanges === null ? operatingRanges : intersectRangeLists(resourceRanges, operatingRanges);
|
|
964
1044
|
const durationMinutes = resolveRequestedDurationMinutes(product, request);
|
|
@@ -972,7 +1052,8 @@ function getProductTimeRanges(projection, request = {}, runtimeOptions = {}) {
|
|
|
972
1052
|
}
|
|
973
1053
|
return floorToMinute(evaluatedAt);
|
|
974
1054
|
})();
|
|
975
|
-
const
|
|
1055
|
+
const durationCandidateAnchor = toDateTime(projection.meta.durationCandidateAnchorAt, projection.meta.timezone);
|
|
1056
|
+
const durationAnchor = runtimeOptions.useExactDurationCandidateMinute ? floorToMinute(durationCandidateAnchor) : ceilToTenMinutes(durationCandidateAnchor, projection.meta.timezone);
|
|
976
1057
|
const requestedStartTimes = resolveRequestedDurationStartTimes(product, request);
|
|
977
1058
|
const ranges = [];
|
|
978
1059
|
baseRanges.forEach(range => {
|
|
@@ -1177,7 +1258,9 @@ function buildEvaluationContext(params) {
|
|
|
1177
1258
|
evaluatedAt,
|
|
1178
1259
|
evaluatedAtMs: evaluatedAt,
|
|
1179
1260
|
resourceIdSet: params.resourceIds?.length ? new Set(params.resourceIds.map(String)) : null,
|
|
1180
|
-
selectionsByGroupKey
|
|
1261
|
+
selectionsByGroupKey,
|
|
1262
|
+
useExactDurationCandidateMinute: params.useExactDurationCandidateMinute === true,
|
|
1263
|
+
allowExcludedScheduleDateOverride: params.allowExcludedScheduleDateOverride === true
|
|
1181
1264
|
};
|
|
1182
1265
|
}
|
|
1183
1266
|
function summarizeStatuses(statuses) {
|
|
@@ -1327,7 +1410,8 @@ function evaluateCandidateSummary(projection, candidate, request) {
|
|
|
1327
1410
|
const evaluationContext = request.evaluationContext || buildEvaluationContext({
|
|
1328
1411
|
projection,
|
|
1329
1412
|
evaluatedAt: request.evaluatedAt,
|
|
1330
|
-
resourceIds: request.resourceIds
|
|
1413
|
+
resourceIds: request.resourceIds,
|
|
1414
|
+
allowExcludedScheduleDateOverride: request.allowExcludedScheduleDateOverride
|
|
1331
1415
|
});
|
|
1332
1416
|
const requestedPartySize = normalizePartySize(request.partySize);
|
|
1333
1417
|
const requiredGroups = (product.requirementGroups || []).filter(group => group.required !== false).map(requirementGroup => evaluateRequirementGroupSummary({
|
|
@@ -1341,7 +1425,9 @@ function evaluateCandidateSummary(projection, candidate, request) {
|
|
|
1341
1425
|
groupResourceIndex: request.groupResourceIndex
|
|
1342
1426
|
}));
|
|
1343
1427
|
const gate = evaluateCandidateTimeGate({
|
|
1428
|
+
projection,
|
|
1344
1429
|
product,
|
|
1430
|
+
candidate,
|
|
1345
1431
|
candidateRange,
|
|
1346
1432
|
evaluationContext,
|
|
1347
1433
|
timezoneName: projection.meta.timezone
|
|
@@ -1383,7 +1469,8 @@ function evaluateCandidate(projection, candidate, request) {
|
|
|
1383
1469
|
projection,
|
|
1384
1470
|
evaluatedAt: request.evaluatedAt,
|
|
1385
1471
|
resourceIds: request.resourceIds,
|
|
1386
|
-
requirementSelections: request.requirementSelections
|
|
1472
|
+
requirementSelections: request.requirementSelections,
|
|
1473
|
+
allowExcludedScheduleDateOverride: request.allowExcludedScheduleDateOverride
|
|
1387
1474
|
});
|
|
1388
1475
|
const requestedPartySize = normalizePartySize(request.partySize);
|
|
1389
1476
|
const partySize = requestedPartySize ?? 1;
|
|
@@ -1396,6 +1483,12 @@ function evaluateCandidate(projection, candidate, request) {
|
|
|
1396
1483
|
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)));
|
|
1397
1484
|
const options = matchedResources.map(resource => {
|
|
1398
1485
|
const matchedCell = aggregateMatchedCells(product.id, requirementGroup.id, resource.id, candidateRange, request.cellIndex?.get([String(product.id), requirementGroup.id, String(resource.id)].join(':')) || []);
|
|
1486
|
+
const isExcludedScheduleDate = evaluationContext.allowExcludedScheduleDateOverride && isRangeFromExcludedWindows({
|
|
1487
|
+
normalWindows: resource.windows || [],
|
|
1488
|
+
excludedWindows: resource.excludedWindows || [],
|
|
1489
|
+
candidateRange,
|
|
1490
|
+
timezoneName: projection.meta.timezone
|
|
1491
|
+
});
|
|
1399
1492
|
if (!matchedCell) {
|
|
1400
1493
|
const isPast = candidateRange.endMs <= evaluationContext.evaluatedAtMs;
|
|
1401
1494
|
return {
|
|
@@ -1406,7 +1499,7 @@ function evaluateCandidate(projection, candidate, request) {
|
|
|
1406
1499
|
resourceType: resource.resourceType,
|
|
1407
1500
|
status: isPast ? 'past' : 'unavailable',
|
|
1408
1501
|
maxPartySize: 0,
|
|
1409
|
-
reasonCodes: isPast ? ['past'] : ['outside_resource_window']
|
|
1502
|
+
reasonCodes: isPast ? ['past'] : [...(isExcludedScheduleDate ? ['schedule_excluded_date'] : []), 'outside_resource_window']
|
|
1410
1503
|
};
|
|
1411
1504
|
}
|
|
1412
1505
|
const remainingCapacity = matchedCell.remainingCapacity;
|
|
@@ -1414,6 +1507,7 @@ function evaluateCandidate(projection, candidate, request) {
|
|
|
1414
1507
|
const isPast = candidateRange.endMs <= evaluationContext.evaluatedAtMs;
|
|
1415
1508
|
const maxPartySize = isPast ? 0 : Math.max(0, remainingCapacity || 0);
|
|
1416
1509
|
const requiredCapacity = resolveRequiredCapacity(requirementGroup, product, requestedPartySize);
|
|
1510
|
+
const reasonCodes = Array.from(new Set([...matchedCell.reasonCodes, ...(isExcludedScheduleDate ? ['schedule_excluded_date'] : [])]));
|
|
1417
1511
|
const status = (() => {
|
|
1418
1512
|
if (isPast) return 'past';
|
|
1419
1513
|
if (matchedCell.status === 'unavailable') return 'unavailable';
|
|
@@ -1431,7 +1525,7 @@ function evaluateCandidate(projection, candidate, request) {
|
|
|
1431
1525
|
remainingCapacity,
|
|
1432
1526
|
maxCapacity,
|
|
1433
1527
|
maxPartySize,
|
|
1434
|
-
reasonCodes: isPast ? Array.from(new Set([...
|
|
1528
|
+
reasonCodes: isPast ? Array.from(new Set([...reasonCodes, 'past'])) : reasonCodes,
|
|
1435
1529
|
matchedCell
|
|
1436
1530
|
};
|
|
1437
1531
|
});
|
|
@@ -1471,7 +1565,9 @@ function evaluateCandidate(projection, candidate, request) {
|
|
|
1471
1565
|
const requiredGroups = requirementGroups.filter(group => group.required);
|
|
1472
1566
|
const requiredStatuses = requiredGroups.map(group => group.status);
|
|
1473
1567
|
const gate = evaluateCandidateTimeGate({
|
|
1568
|
+
projection,
|
|
1474
1569
|
product,
|
|
1570
|
+
candidate,
|
|
1475
1571
|
candidateRange,
|
|
1476
1572
|
evaluationContext,
|
|
1477
1573
|
timezoneName: projection.meta.timezone
|
|
@@ -1547,7 +1643,7 @@ function assertRangesWithinProjection(projection, ranges) {
|
|
|
1547
1643
|
}
|
|
1548
1644
|
});
|
|
1549
1645
|
}
|
|
1550
|
-
function queryAvailabilityProjection(projection, request) {
|
|
1646
|
+
function queryAvailabilityProjection(projection, request, runtimeOptions = {}) {
|
|
1551
1647
|
const normalizedPartySize = normalizePartySize(request.partySize);
|
|
1552
1648
|
const evaluatedAt = formatDateTime((0, _localClock.localDateToScalar)(new Date()), projection.meta.timezone, (0, _localClock.extractFixedOffsetMinutes)(projection.meta.now) ?? (0, _localClock.captureRuntimeOffsetMinutes)());
|
|
1553
1649
|
if (request.target === 'time') {
|
|
@@ -1570,7 +1666,9 @@ function queryAvailabilityProjection(projection, request) {
|
|
|
1570
1666
|
const evaluationContext = buildEvaluationContext({
|
|
1571
1667
|
projection,
|
|
1572
1668
|
evaluatedAt,
|
|
1573
|
-
resourceIds: request.resourceIds
|
|
1669
|
+
resourceIds: request.resourceIds,
|
|
1670
|
+
useExactDurationCandidateMinute: runtimeOptions.useExactDurationCandidateMinute,
|
|
1671
|
+
allowExcludedScheduleDateOverride: runtimeOptions.allowExcludedScheduleDateOverride
|
|
1574
1672
|
});
|
|
1575
1673
|
if (request.target === 'time') {
|
|
1576
1674
|
const candidates = dedupeProductRanges(request.candidates);
|
|
@@ -1644,7 +1742,9 @@ function queryAvailabilityProjection(projection, request) {
|
|
|
1644
1742
|
resourceIds: request.resourceIds,
|
|
1645
1743
|
partySize: normalizedPartySize
|
|
1646
1744
|
}, {
|
|
1647
|
-
evaluatedAt
|
|
1745
|
+
evaluatedAt,
|
|
1746
|
+
useExactDurationCandidateMinute: runtimeOptions.useExactDurationCandidateMinute,
|
|
1747
|
+
allowExcludedScheduleDateOverride: runtimeOptions.allowExcludedScheduleDateOverride
|
|
1648
1748
|
});
|
|
1649
1749
|
const candidateGroupMap = new Map(candidateGroups.map(group => [String(group.productId), group]));
|
|
1650
1750
|
if (request.target === 'date') {
|
|
@@ -1794,7 +1894,7 @@ function summarizeProductStatus(statuses) {
|
|
|
1794
1894
|
if (statuses.every(status => status === 'past')) return 'past';
|
|
1795
1895
|
return 'unavailable';
|
|
1796
1896
|
}
|
|
1797
|
-
function validateAvailabilitySelection(projection, params) {
|
|
1897
|
+
function validateAvailabilitySelection(projection, params, runtimeOptions = {}) {
|
|
1798
1898
|
const normalizedPartySize = normalizePartySize(params.partySize);
|
|
1799
1899
|
const normalizedBookingCount = normalizeBookingCount(params.bookingCount);
|
|
1800
1900
|
assertRangesWithinProjection(projection, [params.candidate]);
|
|
@@ -1815,14 +1915,16 @@ function validateAvailabilitySelection(projection, params) {
|
|
|
1815
1915
|
evaluationContext: buildEvaluationContext({
|
|
1816
1916
|
projection,
|
|
1817
1917
|
evaluatedAt,
|
|
1818
|
-
requirementSelections: params.requirementSelections
|
|
1918
|
+
requirementSelections: params.requirementSelections,
|
|
1919
|
+
useExactDurationCandidateMinute: runtimeOptions.useExactDurationCandidateMinute,
|
|
1920
|
+
allowExcludedScheduleDateOverride: runtimeOptions.allowExcludedScheduleDateOverride
|
|
1819
1921
|
})
|
|
1820
1922
|
});
|
|
1821
1923
|
const conflicts = [];
|
|
1822
1924
|
if (evaluated.gate) {
|
|
1823
1925
|
conflicts.push({
|
|
1824
1926
|
code: evaluated.gate.conflictCode,
|
|
1825
|
-
message: evaluated.gate.
|
|
1927
|
+
message: evaluated.gate.conflictCode === 'past' ? '候选时间已过期' : evaluated.gate.conflictCode === 'schedule_excluded_date' ? '候选日期已被日程排除' : '候选时间不满足预订时间门禁',
|
|
1826
1928
|
reasonCodes: evaluated.gate.reasonCodes,
|
|
1827
1929
|
productId: params.candidate.productId,
|
|
1828
1930
|
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 {
|
|
@@ -245,6 +250,13 @@ export interface GetProductTimeRangesRequest {
|
|
|
245
250
|
}
|
|
246
251
|
export interface GetProductTimeRangesRuntimeOptions {
|
|
247
252
|
evaluatedAt?: string;
|
|
253
|
+
/**
|
|
254
|
+
* Internal POS capability: fixed-duration candidates start from the stable
|
|
255
|
+
* context anchor's current minute instead of rounding up to ten minutes, and
|
|
256
|
+
* that minute remains selectable during before-start cutoff evaluation.
|
|
257
|
+
* Other callers keep the legacy alignment and strict second-level cutoff.
|
|
258
|
+
*/
|
|
259
|
+
useExactDurationCandidateMinute?: boolean;
|
|
248
260
|
/**
|
|
249
261
|
* Internal POS capability: allow explicit duration/start overrides to append
|
|
250
262
|
* a custom duration candidate for Session products. Fixed Session ranges are
|
|
@@ -252,6 +264,11 @@ export interface GetProductTimeRangesRuntimeOptions {
|
|
|
252
264
|
* disabled.
|
|
253
265
|
*/
|
|
254
266
|
allowSessionCustomRanges?: boolean;
|
|
267
|
+
/**
|
|
268
|
+
* Internal POS capability: materialize candidates whose schedule date was
|
|
269
|
+
* explicitly excluded, while availability evaluation keeps them unavailable.
|
|
270
|
+
*/
|
|
271
|
+
allowExcludedScheduleDateOverride?: boolean;
|
|
255
272
|
}
|
|
256
273
|
export interface QueryAvailabilityBaseRequest {
|
|
257
274
|
productIds?: AvailabilityId[];
|
|
@@ -638,6 +638,22 @@ function normalizeResource(raw, fixedOffsetMinutes = (0, _localClock.captureRunt
|
|
|
638
638
|
const id = raw.resourceId ?? raw.resource_id ?? raw.id;
|
|
639
639
|
if (id === undefined || id === null || id === '') return null;
|
|
640
640
|
const rawWindows = raw.windows || raw.times || [];
|
|
641
|
+
const rawExcludedWindows = raw.excludedWindows || raw.excluded_windows || [];
|
|
642
|
+
const normalizeWindows = windows => (Array.isArray(windows) ? windows : []).flatMap(window => {
|
|
643
|
+
const range = normalizeAvailabilityLocalDateTimeRange({
|
|
644
|
+
startAt: window.startAt || window.start_at || window.start,
|
|
645
|
+
endAt: window.endAt || window.end_at || window.end,
|
|
646
|
+
fixedOffsetMinutes,
|
|
647
|
+
rollOverEnd: true
|
|
648
|
+
});
|
|
649
|
+
if (!range) return [];
|
|
650
|
+
return [{
|
|
651
|
+
startAt: range.startAt,
|
|
652
|
+
endAt: range.endAt,
|
|
653
|
+
source: window.source,
|
|
654
|
+
raw: window
|
|
655
|
+
}];
|
|
656
|
+
});
|
|
641
657
|
return {
|
|
642
658
|
id,
|
|
643
659
|
formId: raw.formId ?? raw.form_id ?? raw.resource_form_id,
|
|
@@ -645,21 +661,8 @@ function normalizeResource(raw, fixedOffsetMinutes = (0, _localClock.captureRunt
|
|
|
645
661
|
code: raw.code,
|
|
646
662
|
resourceType: normalizeResourceType(raw.type || raw.resourceType),
|
|
647
663
|
capacity: Math.max(1, Number(raw.capacity || raw.max_capacity || 1) || 1),
|
|
648
|
-
windows: (
|
|
649
|
-
|
|
650
|
-
startAt: window.startAt || window.start_at || window.start,
|
|
651
|
-
endAt: window.endAt || window.end_at || window.end,
|
|
652
|
-
fixedOffsetMinutes,
|
|
653
|
-
rollOverEnd: true
|
|
654
|
-
});
|
|
655
|
-
if (!range) return [];
|
|
656
|
-
return [{
|
|
657
|
-
startAt: range.startAt,
|
|
658
|
-
endAt: range.endAt,
|
|
659
|
-
source: window.source,
|
|
660
|
-
raw: window
|
|
661
|
-
}];
|
|
662
|
-
}),
|
|
664
|
+
windows: normalizeWindows(rawWindows),
|
|
665
|
+
excludedWindows: normalizeWindows(rawExcludedWindows),
|
|
663
666
|
raw
|
|
664
667
|
};
|
|
665
668
|
}
|
|
@@ -678,6 +681,17 @@ function dateRangeDays(dateRange) {
|
|
|
678
681
|
}
|
|
679
682
|
return dates;
|
|
680
683
|
}
|
|
684
|
+
function getExcludedScheduleDateOverrideTimePoints(date, schedule) {
|
|
685
|
+
if (!schedule.repeat_rule?.excluded_date?.length) return [];
|
|
686
|
+
if ((0, _getDateIsInSchedule.getScheduleStartEndTimePoints)(date, [schedule]).length > 0) return [];
|
|
687
|
+
return (0, _getDateIsInSchedule.getScheduleStartEndTimePoints)(date, [{
|
|
688
|
+
...schedule,
|
|
689
|
+
repeat_rule: {
|
|
690
|
+
...schedule.repeat_rule,
|
|
691
|
+
excluded_date: []
|
|
692
|
+
}
|
|
693
|
+
}]);
|
|
694
|
+
}
|
|
681
695
|
function normalizeResourceOccupancy(rawEvent, resourceId, source = 'remote', fixedOffsetMinutes = (0, _localClock.captureRuntimeOffsetMinutes)()) {
|
|
682
696
|
const range = normalizeAvailabilityLocalDateTimeRange({
|
|
683
697
|
startAt: rawEvent.start_at ?? rawEvent.startAt ?? rawEvent.start_time ?? rawEvent.startTime,
|
|
@@ -770,7 +784,26 @@ function buildAvailabilityResourcesFromV2(params) {
|
|
|
770
784
|
}
|
|
771
785
|
}];
|
|
772
786
|
})));
|
|
787
|
+
const excludedWindows = dates.flatMap(date => schedules.flatMap(schedule => getExcludedScheduleDateOverrideTimePoints(date, schedule).flatMap(slot => {
|
|
788
|
+
const range = normalizeAvailabilityLocalDateTimeRange({
|
|
789
|
+
startAt: slot.start_at,
|
|
790
|
+
endAt: slot.end_at,
|
|
791
|
+
fixedOffsetMinutes,
|
|
792
|
+
rollOverEnd: true
|
|
793
|
+
});
|
|
794
|
+
if (!range) return [];
|
|
795
|
+
return [{
|
|
796
|
+
startAt: range.startAt,
|
|
797
|
+
endAt: range.endAt,
|
|
798
|
+
source: `resource_schedule_excluded:${schedule.id}`,
|
|
799
|
+
raw: {
|
|
800
|
+
scheduleId: schedule.id,
|
|
801
|
+
excludedDate: date
|
|
802
|
+
}
|
|
803
|
+
}];
|
|
804
|
+
})));
|
|
773
805
|
resource.windows = materializedWindows.length > 0 ? materializedWindows : explicitWindows;
|
|
806
|
+
resource.excludedWindows = [...(resource.excludedWindows || []), ...excludedWindows];
|
|
774
807
|
if (resource.windows.length === 0 && missingScheduleIds.length > 0) {
|
|
775
808
|
resource.windows = buildResourceWindowFallbackTimes({
|
|
776
809
|
raw,
|
|
@@ -1011,7 +1044,7 @@ function buildBookingFromAvailabilitySelection(params) {
|
|
|
1011
1044
|
}
|
|
1012
1045
|
};
|
|
1013
1046
|
}
|
|
1014
|
-
const POS_OVERRIDABLE_AVAILABILITY_CONFLICT_CODES = new Set(['past', 'booking_cutoff', 'resource_full', 'resource_unavailable', 'resource_capacity_exceeded',
|
|
1047
|
+
const POS_OVERRIDABLE_AVAILABILITY_CONFLICT_CODES = new Set(['past', 'booking_cutoff', 'schedule_excluded_date', 'resource_full', 'resource_unavailable', 'resource_capacity_exceeded',
|
|
1015
1048
|
// A required group whose selected resource is outside its window is surfaced
|
|
1016
1049
|
// as missing_resource at group level. Structural validation below still
|
|
1017
1050
|
// rejects a genuinely missing selection.
|
|
@@ -2118,11 +2151,31 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
2118
2151
|
source: `operating_schedule:${descriptor.scheduleIds.map(String).join(',')}`
|
|
2119
2152
|
}] : [];
|
|
2120
2153
|
}));
|
|
2154
|
+
const excludedWindows = dateRangeDays(params.dateRange).flatMap(date => schedules.flatMap(schedule => getExcludedScheduleDateOverrideTimePoints(date, schedule).flatMap(slot => {
|
|
2155
|
+
const range = normalizeAvailabilityLocalDateTimeRange({
|
|
2156
|
+
startAt: slot.start_at,
|
|
2157
|
+
endAt: slot.end_at,
|
|
2158
|
+
fixedOffsetMinutes: this.getAvailabilityRuntimeOffsetMinutes(),
|
|
2159
|
+
rollOverEnd: true
|
|
2160
|
+
});
|
|
2161
|
+
return range ? [{
|
|
2162
|
+
startAt: range.startAt,
|
|
2163
|
+
endAt: range.endAt,
|
|
2164
|
+
source: `operating_schedule_excluded:${schedule.id}`,
|
|
2165
|
+
raw: {
|
|
2166
|
+
scheduleId: schedule.id,
|
|
2167
|
+
excludedDate: date
|
|
2168
|
+
}
|
|
2169
|
+
}] : [];
|
|
2170
|
+
})));
|
|
2121
2171
|
return {
|
|
2122
2172
|
source: descriptor.source,
|
|
2123
2173
|
scheduleIds: [...descriptor.scheduleIds],
|
|
2124
2174
|
missingScheduleIds,
|
|
2125
|
-
windows
|
|
2175
|
+
windows,
|
|
2176
|
+
...(excludedWindows.length > 0 ? {
|
|
2177
|
+
excludedWindows
|
|
2178
|
+
} : {})
|
|
2126
2179
|
};
|
|
2127
2180
|
}
|
|
2128
2181
|
async loadAvailabilityProducts(params) {
|
|
@@ -2820,7 +2873,9 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
2820
2873
|
} : {})
|
|
2821
2874
|
}, {
|
|
2822
2875
|
evaluatedAt: this.getAvailabilityRuntimeDateTime(),
|
|
2823
|
-
|
|
2876
|
+
useExactDurationCandidateMinute: isPosAvailabilityRequest,
|
|
2877
|
+
allowSessionCustomRanges: isPosAvailabilityRequest,
|
|
2878
|
+
allowExcludedScheduleDateOverride: isPosAvailabilityRequest
|
|
2824
2879
|
}).map(group => ({
|
|
2825
2880
|
...group,
|
|
2826
2881
|
ranges: group.ranges.map(candidate => this.decorateAvailabilityCandidate(context.contextId, contextVersion, candidate))
|
|
@@ -2835,7 +2890,11 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
2835
2890
|
refreshed
|
|
2836
2891
|
} = await this.ensureAvailabilityContextProjection(contextId, 'query_booking_availability');
|
|
2837
2892
|
this.assertContextualQueryFreshness(context, request);
|
|
2838
|
-
const
|
|
2893
|
+
const isPosAvailabilityRequest = String(this.otherParams?.platform ?? '').trim().toLowerCase() === 'pos';
|
|
2894
|
+
const result = (0, _ResourcePlanner.queryAvailabilityProjection)(projectionEntry.projection, request, {
|
|
2895
|
+
useExactDurationCandidateMinute: isPosAvailabilityRequest,
|
|
2896
|
+
allowExcludedScheduleDateOverride: isPosAvailabilityRequest
|
|
2897
|
+
});
|
|
2839
2898
|
const contextVersion = context.activeVersion;
|
|
2840
2899
|
const contextualResult = result.target === 'time' ? {
|
|
2841
2900
|
...result,
|
|
@@ -3092,7 +3151,9 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
3092
3151
|
} : {})
|
|
3093
3152
|
} : {})
|
|
3094
3153
|
}, {
|
|
3095
|
-
|
|
3154
|
+
useExactDurationCandidateMinute: allowPosAvailabilityOverride,
|
|
3155
|
+
allowSessionCustomRanges: allowPosAvailabilityOverride,
|
|
3156
|
+
allowExcludedScheduleDateOverride: allowPosAvailabilityOverride
|
|
3096
3157
|
}).flatMap(group => group.ranges);
|
|
3097
3158
|
const refreshedCandidateBase = refreshedRanges.find(candidate => candidate.startAt === params.candidate.startAt && candidate.endAt === params.candidate.endAt && candidate.bookingStartAt === params.candidate.bookingStartAt && candidate.bookingEndAt === params.candidate.bookingEndAt);
|
|
3098
3159
|
if (!refreshedCandidateBase) {
|
|
@@ -3113,6 +3174,9 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
3113
3174
|
requirementSelections: params.requirementSelections,
|
|
3114
3175
|
partySize,
|
|
3115
3176
|
bookingCount
|
|
3177
|
+
}, {
|
|
3178
|
+
useExactDurationCandidateMinute: allowPosAvailabilityOverride,
|
|
3179
|
+
allowExcludedScheduleDateOverride: allowPosAvailabilityOverride
|
|
3116
3180
|
});
|
|
3117
3181
|
if (allowPosAvailabilityOverride && !validation.ok && validation.conflicts.every(conflict => POS_OVERRIDABLE_AVAILABILITY_CONFLICT_CODES.has(conflict.code))) {
|
|
3118
3182
|
const forced = buildPosForcedAvailabilityAssignments({
|