@pisell/pisellos 2.2.291 → 2.2.293

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 (174) hide show
  1. package/dist/core/index.d.ts +7 -0
  2. package/dist/core/index.js +108 -65
  3. package/dist/model/strategy/adapter/itemRule/adapter.d.ts +4 -1
  4. package/dist/model/strategy/adapter/itemRule/adapter.js +135 -35
  5. package/dist/model/strategy/adapter/itemRule/evaluator.d.ts +8 -1
  6. package/dist/model/strategy/adapter/itemRule/evaluator.js +25 -1
  7. package/dist/model/strategy/adapter/itemRule/type.d.ts +44 -0
  8. package/dist/model/strategy/adapter/itemRule/type.js +19 -1
  9. package/dist/model/strategy/adapter/promotion/index.js +0 -9
  10. package/dist/modules/BookingContext/index.d.ts +3 -0
  11. package/dist/modules/BookingContext/index.js +42 -13
  12. package/dist/modules/BookingContext/types.d.ts +4 -4
  13. package/dist/modules/BookingContext/types.js +3 -3
  14. package/dist/modules/OpenData/types.d.ts +1 -0
  15. package/dist/modules/Order/index.d.ts +18 -3
  16. package/dist/modules/Order/index.js +885 -485
  17. package/dist/modules/Order/salesNotes.d.ts +31 -0
  18. package/dist/modules/Order/salesNotes.js +492 -0
  19. package/dist/modules/Order/types.d.ts +93 -5
  20. package/dist/modules/Order/types.js +5 -0
  21. package/dist/modules/Order/utils.d.ts +12 -0
  22. package/dist/modules/Order/utils.js +96 -31
  23. package/dist/modules/Product/types.d.ts +2 -0
  24. package/dist/modules/ProductList/clientDataVariants.d.ts +50 -0
  25. package/dist/modules/ProductList/clientDataVariants.js +216 -0
  26. package/dist/modules/ProductList/index.d.ts +18 -3
  27. package/dist/modules/ProductList/index.js +220 -38
  28. package/dist/modules/ProductList/types.d.ts +10 -0
  29. package/dist/modules/Quotation/index.d.ts +22 -0
  30. package/dist/modules/Quotation/index.js +293 -28
  31. package/dist/modules/ResourcePlanner/index.d.ts +0 -22
  32. package/dist/modules/ResourcePlanner/index.js +1 -180
  33. package/dist/modules/ResourcePlanner/localClock.d.ts +33 -0
  34. package/dist/modules/ResourcePlanner/localClock.js +183 -0
  35. package/dist/modules/ResourcePlanner/planner.d.ts +5 -14
  36. package/dist/modules/ResourcePlanner/planner.js +2111 -958
  37. package/dist/modules/ResourcePlanner/types.d.ts +359 -187
  38. package/dist/modules/ResourcePlanner/types.js +33 -1
  39. package/dist/plugins/request.d.ts +1 -0
  40. package/dist/server/index.d.ts +17 -2
  41. package/dist/server/index.js +1134 -932
  42. package/dist/server/modules/floor-plan/index.d.ts +4 -0
  43. package/dist/server/modules/floor-plan/index.js +240 -98
  44. package/dist/server/modules/menu/index.js +48 -23
  45. package/dist/server/modules/order/index.d.ts +17 -7
  46. package/dist/server/modules/order/index.js +419 -207
  47. package/dist/server/modules/products/index.d.ts +10 -4
  48. package/dist/server/modules/products/index.js +177 -84
  49. package/dist/server/modules/resource/index.js +21 -13
  50. package/dist/server/utils/product.d.ts +1 -1
  51. package/dist/server/utils/product.js +186 -22
  52. package/dist/server/utils/small-ticket.d.ts +9 -1
  53. package/dist/server/utils/small-ticket.js +131 -84
  54. package/dist/solution/BaseSales/index.d.ts +26 -3
  55. package/dist/solution/BaseSales/index.js +1384 -905
  56. package/dist/solution/BaseSales/types.d.ts +2 -0
  57. package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.d.ts +3 -0
  58. package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +10 -3
  59. package/dist/solution/BaseSales/utils.js +6 -3
  60. package/dist/solution/BookingByStep/index.d.ts +4 -4
  61. package/dist/solution/BookingTicket/index.d.ts +1 -1
  62. package/dist/solution/BookingTicket/index.js +4 -2
  63. package/dist/solution/BookingTicket/utils/addProductDecision.d.ts +2 -1
  64. package/dist/solution/BookingTicket/utils/addProductDecision.js +6 -3
  65. package/dist/solution/BookingTicket/utils/orderCustomer.js +3 -2
  66. package/dist/solution/RegisterAndLogin/config.d.ts +9 -3
  67. package/dist/solution/RegisterAndLogin/config.js +95 -40
  68. package/dist/solution/RegisterAndLogin/index.js +4 -1
  69. package/dist/solution/ScanOrder/index.d.ts +1 -0
  70. package/dist/solution/ScanOrder/index.js +31 -27
  71. package/dist/solution/ScanOrder/types.d.ts +2 -0
  72. package/dist/solution/ScanOrder/utils.d.ts +1 -0
  73. package/dist/solution/ScanOrder/utils.js +8 -3
  74. package/dist/solution/UnifiedBookingSales/index.d.ts +128 -74
  75. package/dist/solution/UnifiedBookingSales/index.js +3877 -963
  76. package/dist/solution/UnifiedBookingSales/remoteOccupancyCache.d.ts +49 -0
  77. package/dist/solution/UnifiedBookingSales/remoteOccupancyCache.js +430 -0
  78. package/dist/solution/UnifiedBookingSales/types.d.ts +161 -77
  79. package/dist/solution/UnifiedBookingSales/types.js +4 -9
  80. package/dist/solution/VenueBooking/index.d.ts +3 -0
  81. package/dist/solution/VenueBooking/index.js +549 -497
  82. package/dist/solution/VenueBooking/utils/dateSummary.d.ts +1 -0
  83. package/dist/solution/VenueBooking/utils/dateSummary.js +3 -1
  84. package/dist/solution/VenueBooking/utils/slotMerge.d.ts +2 -0
  85. package/dist/solution/VenueBooking/utils/slotMerge.js +3 -1
  86. package/dist/utils/platform.d.ts +8 -0
  87. package/dist/utils/platform.js +15 -0
  88. package/lib/core/index.d.ts +7 -0
  89. package/lib/core/index.js +20 -2
  90. package/lib/model/strategy/adapter/itemRule/adapter.d.ts +4 -1
  91. package/lib/model/strategy/adapter/itemRule/adapter.js +73 -4
  92. package/lib/model/strategy/adapter/itemRule/evaluator.d.ts +8 -1
  93. package/lib/model/strategy/adapter/itemRule/evaluator.js +23 -1
  94. package/lib/model/strategy/adapter/itemRule/type.d.ts +44 -0
  95. package/lib/model/strategy/adapter/itemRule/type.js +19 -1
  96. package/lib/modules/BookingContext/index.d.ts +3 -0
  97. package/lib/modules/BookingContext/index.js +35 -8
  98. package/lib/modules/BookingContext/types.d.ts +4 -4
  99. package/lib/modules/BookingContext/types.js +3 -3
  100. package/lib/modules/OpenData/types.d.ts +1 -0
  101. package/lib/modules/Order/index.d.ts +18 -3
  102. package/lib/modules/Order/index.js +325 -28
  103. package/lib/modules/Order/salesNotes.d.ts +31 -0
  104. package/lib/modules/Order/salesNotes.js +382 -0
  105. package/lib/modules/Order/types.d.ts +93 -5
  106. package/lib/modules/Order/types.js +4 -0
  107. package/lib/modules/Order/utils.d.ts +12 -0
  108. package/lib/modules/Order/utils.js +85 -16
  109. package/lib/modules/Product/types.d.ts +2 -0
  110. package/lib/modules/ProductList/clientDataVariants.d.ts +50 -0
  111. package/lib/modules/ProductList/clientDataVariants.js +204 -0
  112. package/lib/modules/ProductList/index.d.ts +18 -3
  113. package/lib/modules/ProductList/index.js +137 -8
  114. package/lib/modules/ProductList/types.d.ts +10 -0
  115. package/lib/modules/Quotation/index.d.ts +22 -0
  116. package/lib/modules/Quotation/index.js +158 -11
  117. package/lib/modules/ResourcePlanner/index.d.ts +0 -22
  118. package/lib/modules/ResourcePlanner/index.js +9 -129
  119. package/lib/modules/ResourcePlanner/localClock.d.ts +33 -0
  120. package/lib/modules/ResourcePlanner/localClock.js +205 -0
  121. package/lib/modules/ResourcePlanner/planner.d.ts +5 -14
  122. package/lib/modules/ResourcePlanner/planner.js +1807 -783
  123. package/lib/modules/ResourcePlanner/types.d.ts +359 -187
  124. package/lib/modules/ResourcePlanner/types.js +13 -1
  125. package/lib/plugins/request.d.ts +1 -0
  126. package/lib/server/index.d.ts +17 -2
  127. package/lib/server/index.js +149 -21
  128. package/lib/server/modules/floor-plan/index.d.ts +4 -0
  129. package/lib/server/modules/floor-plan/index.js +60 -7
  130. package/lib/server/modules/menu/index.js +26 -4
  131. package/lib/server/modules/order/index.d.ts +17 -7
  132. package/lib/server/modules/order/index.js +194 -57
  133. package/lib/server/modules/products/index.d.ts +10 -4
  134. package/lib/server/modules/products/index.js +93 -23
  135. package/lib/server/modules/resource/index.js +12 -2
  136. package/lib/server/utils/product.d.ts +1 -1
  137. package/lib/server/utils/product.js +163 -3
  138. package/lib/server/utils/small-ticket.d.ts +9 -1
  139. package/lib/server/utils/small-ticket.js +123 -79
  140. package/lib/solution/BaseSales/index.d.ts +26 -3
  141. package/lib/solution/BaseSales/index.js +396 -70
  142. package/lib/solution/BaseSales/types.d.ts +2 -0
  143. package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.d.ts +3 -0
  144. package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +9 -2
  145. package/lib/solution/BaseSales/utils.js +3 -0
  146. package/lib/solution/BookingByStep/index.d.ts +4 -4
  147. package/lib/solution/BookingTicket/index.d.ts +1 -1
  148. package/lib/solution/BookingTicket/index.js +2 -1
  149. package/lib/solution/BookingTicket/utils/addProductDecision.d.ts +2 -1
  150. package/lib/solution/BookingTicket/utils/addProductDecision.js +5 -2
  151. package/lib/solution/BookingTicket/utils/orderCustomer.js +2 -1
  152. package/lib/solution/RegisterAndLogin/config.d.ts +9 -3
  153. package/lib/solution/RegisterAndLogin/config.js +49 -8
  154. package/lib/solution/RegisterAndLogin/index.js +4 -1
  155. package/lib/solution/ScanOrder/index.d.ts +1 -0
  156. package/lib/solution/ScanOrder/index.js +5 -1
  157. package/lib/solution/ScanOrder/types.d.ts +2 -0
  158. package/lib/solution/ScanOrder/utils.d.ts +1 -0
  159. package/lib/solution/ScanOrder/utils.js +4 -0
  160. package/lib/solution/UnifiedBookingSales/index.d.ts +128 -74
  161. package/lib/solution/UnifiedBookingSales/index.js +2592 -424
  162. package/lib/solution/UnifiedBookingSales/remoteOccupancyCache.d.ts +49 -0
  163. package/lib/solution/UnifiedBookingSales/remoteOccupancyCache.js +316 -0
  164. package/lib/solution/UnifiedBookingSales/types.d.ts +161 -77
  165. package/lib/solution/UnifiedBookingSales/types.js +4 -6
  166. package/lib/solution/VenueBooking/index.d.ts +3 -0
  167. package/lib/solution/VenueBooking/index.js +33 -9
  168. package/lib/solution/VenueBooking/utils/dateSummary.d.ts +1 -0
  169. package/lib/solution/VenueBooking/utils/dateSummary.js +3 -1
  170. package/lib/solution/VenueBooking/utils/slotMerge.d.ts +2 -0
  171. package/lib/solution/VenueBooking/utils/slotMerge.js +3 -1
  172. package/lib/utils/platform.d.ts +8 -0
  173. package/lib/utils/platform.js +22 -0
  174. package/package.json +1 -1
@@ -1,1069 +1,2222 @@
1
- function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
2
- function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
3
- function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
4
- function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); }
5
- function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
1
+ function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
6
2
  function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
7
3
  function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
4
+ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
8
5
  function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); }
9
6
  function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
10
- function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
11
- function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
12
7
  function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
13
- function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
14
- import dayjs from 'dayjs';
15
- export var DEFAULT_RESOURCE_PLANNER_CONTEXT = {
16
- products: [],
17
- resources: [],
18
- externalEvents: [],
19
- slotDurationMinutes: 30,
20
- businessStartTime: '09:00',
21
- businessEndTime: '18:00'
22
- };
23
- export var DEFAULT_RESOURCE_PLANNER_SELECTION = {
24
- productIds: [],
25
- resourceIds: [],
26
- assignments: []
8
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
9
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
10
+ function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
11
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); }
12
+ function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
13
+ import { AvailabilityError } from "./types";
14
+ import { addLocalCalendarDays, addLocalHours, addLocalMinutes, captureRuntimeOffsetMinutes, ceilLocalScalarToTenMinutes, dateOrdinalFromLocalDate, extractFixedOffsetMinutes, formatLocalDate, formatLocalDateTime, formatLocalTime, localDateTimeToScalar, localDateToScalar, localDateToScalarStart, nextDate as getNextLocalDate, parseLocalDate, parseLocalDateTime, weekdayFromLocalDate } from "./localClock";
15
+ var DEFAULT_SLOT_STEP_MINUTES = 30;
16
+ var DEFAULT_BUSINESS_HOURS = {
17
+ startTime: '00:00',
18
+ endTime: '00:00'
27
19
  };
28
- export function normalizePlannerContext() {
29
- var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
30
- return {
31
- products: Array.isArray(input.products) ? input.products : [],
32
- resources: Array.isArray(input.resources) ? input.resources : [],
33
- externalEvents: Array.isArray(input.externalEvents) ? input.externalEvents : [],
34
- slotDurationMinutes: input.slotDurationMinutes || DEFAULT_RESOURCE_PLANNER_CONTEXT.slotDurationMinutes,
35
- businessStartTime: input.businessStartTime || DEFAULT_RESOURCE_PLANNER_CONTEXT.businessStartTime,
36
- businessEndTime: input.businessEndTime || DEFAULT_RESOURCE_PLANNER_CONTEXT.businessEndTime
37
- };
20
+ function availabilityError(code, message, details) {
21
+ return new AvailabilityError(code, message, details);
38
22
  }
39
23
  function sameId(left, right) {
24
+ if (left === undefined || left === null || right === undefined || right === null) return false;
40
25
  return String(left) === String(right);
41
26
  }
42
- function dateOf(value) {
43
- return dayjs(value).format('YYYY-MM-DD');
44
- }
45
- function timeOf(value) {
46
- return dayjs(value).format('HH:mm');
27
+ function uniqById() {
28
+ var values = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
29
+ var map = new Map();
30
+ values.forEach(function (value) {
31
+ map.set(String(value), value);
32
+ });
33
+ return Array.from(map.values());
47
34
  }
48
- function toDateTime(date, time) {
49
- if (/^\d{4}-\d{2}-\d{2}/.test(time)) return dayjs(time);
50
- return dayjs("".concat(date, " ").concat(time.length === 5 ? "".concat(time, ":00") : time));
35
+ function normalizePartySize(partySize) {
36
+ if (partySize === undefined || partySize === null) return undefined;
37
+ var normalized = Number(partySize);
38
+ if (!Number.isInteger(normalized) || normalized <= 0) {
39
+ throw availabilityError('INVALID_TARGET_FILTER', 'partySize 必须是正整数', {
40
+ partySize: partySize
41
+ });
42
+ }
43
+ return normalized;
51
44
  }
52
- function overlaps(startA, endA, startB, endB) {
53
- return startA.isBefore(endB) && endA.isAfter(startB);
45
+ function normalizeBookingCount(bookingCount) {
46
+ if (bookingCount === undefined || bookingCount === null) return 1;
47
+ var normalized = Number(bookingCount);
48
+ if (!Number.isInteger(normalized) || normalized <= 0) {
49
+ throw availabilityError('INVALID_TARGET_FILTER', 'bookingCount 必须是正整数', {
50
+ bookingCount: bookingCount
51
+ });
52
+ }
53
+ return normalized;
54
54
  }
55
- function containsRange(outerStart, outerEnd, innerStart, innerEnd) {
56
- return !innerStart.isBefore(outerStart) && !innerEnd.isAfter(outerEnd);
55
+ function resolveRequestedDurationMinutes(product, request) {
56
+ var _request$durationMinu;
57
+ var configured = Math.max(1, Number(product.durationMinutes || 30) || 30);
58
+ if (product.kind !== 'duration') return configured;
59
+ var override = (_request$durationMinu = request.durationMinutesByProductId) === null || _request$durationMinu === void 0 ? void 0 : _request$durationMinu[String(product.id)];
60
+ var normalized = Number(override);
61
+ return Number.isInteger(normalized) && normalized > 0 ? normalized : configured;
57
62
  }
58
- function maxDayjs(left, right) {
59
- return left.isAfter(right) ? left : right;
63
+ function resolveRequestedDurationStartTimes(product, request) {
64
+ var _request$durationStar;
65
+ if (product.kind !== 'duration') return [];
66
+ var values = (_request$durationStar = request.durationStartTimesByProductId) === null || _request$durationStar === void 0 ? void 0 : _request$durationStar[String(product.id)];
67
+ if (!Array.isArray(values)) return [];
68
+ return Array.from(new Set(values.filter(function (value) {
69
+ return typeof value === 'string' && /^([01]\d|2[0-3]):[0-5]\d$/.test(value);
70
+ })));
60
71
  }
61
- function minDayjs(left, right) {
62
- return left.isBefore(right) ? left : right;
72
+ function toDateTime(value, _timezoneName) {
73
+ try {
74
+ return localDateTimeToScalar(parseLocalDateTime(value));
75
+ } catch (_error) {
76
+ return Number.NaN;
77
+ }
63
78
  }
64
- function addDays(start, end) {
65
- var result = [];
66
- var cursor = dayjs(start);
67
- var finalDay = dayjs(end);
68
- while (!cursor.isAfter(finalDay, 'day')) {
69
- result.push(cursor.format('YYYY-MM-DD'));
70
- cursor = cursor.add(1, 'day');
79
+ function toDateStart(value, _timezoneName) {
80
+ try {
81
+ return localDateToScalarStart(value);
82
+ } catch (_error) {
83
+ return Number.NaN;
71
84
  }
72
- return result;
73
- }
74
- function getQueryProducts(products, query) {
75
- var _query$productIds;
76
- if (!((_query$productIds = query.productIds) !== null && _query$productIds !== void 0 && _query$productIds.length)) return products;
77
- return products.filter(function (product) {
78
- return query.productIds.some(function (id) {
79
- return sameId(id, product.id);
80
- });
81
- });
82
85
  }
83
- function getQueryResources(resources, query) {
84
- var _query$resourceIds;
85
- if (!((_query$resourceIds = query.resourceIds) !== null && _query$resourceIds !== void 0 && _query$resourceIds.length)) return resources;
86
- return resources.filter(function (resource) {
87
- return query.resourceIds.some(function (id) {
88
- return sameId(id, resource.id);
89
- });
90
- });
86
+ function toDateEnd(value, _timezoneName) {
87
+ var start = toDateStart(value);
88
+ return Number.isFinite(start) ? addLocalCalendarDays(start, 1) - 1 : Number.NaN;
89
+ }
90
+ function toDateTimeInDate(date, time, _timezoneName) {
91
+ return toDateTime("".concat(date, "T").concat(time.length === 5 ? "".concat(time, ":00") : time));
91
92
  }
92
- function matchesResourceRequirement(resource, requirement) {
93
- var _requirement$resource;
94
- var isFormlessCandidate = resource.formId == null;
95
- var formMatches = requirement.formId == null || isFormlessCandidate || sameId(requirement.formId, resource.formId);
96
- var resourceMatches = !((_requirement$resource = requirement.resourceIds) !== null && _requirement$resource !== void 0 && _requirement$resource.length) || isFormlessCandidate || requirement.resourceIds.some(function (id) {
97
- return sameId(id, resource.id);
93
+ function addMinutesZoned(value, minutes, _timezoneName) {
94
+ return addLocalMinutes(value, minutes);
95
+ }
96
+ function ceilToTenMinutes(value, _timezoneName) {
97
+ return ceilLocalScalarToTenMinutes(value);
98
+ }
99
+ function floorToMinute(value) {
100
+ return Math.floor(value / 60000) * 60000;
101
+ }
102
+ function nextDate(value, _timezoneName) {
103
+ return getNextLocalDate(value);
104
+ }
105
+ function formatDateTime(value, _timezoneName, fixedOffsetMinutes) {
106
+ return formatLocalDateTime(value, fixedOffsetMinutes !== null && fixedOffsetMinutes !== void 0 ? fixedOffsetMinutes : captureRuntimeOffsetMinutes());
107
+ }
108
+ function formatDate(value, _timezoneName) {
109
+ return formatLocalDate(value);
110
+ }
111
+ function formatTime(value, _timezoneName) {
112
+ return formatLocalTime(value, false);
113
+ }
114
+ function normalizeAbsoluteRange(range, _timezoneName) {
115
+ var start = toDateTime(range.startAt);
116
+ var end = toDateTime(range.endAt);
117
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return null;
118
+ return {
119
+ start: start,
120
+ end: end
121
+ };
122
+ }
123
+ function getCandidateBookingRange(candidate) {
124
+ return {
125
+ // Runtime fallback keeps older caller-created candidates fail-safe during migration.
126
+ startAt: candidate.bookingStartAt || candidate.startAt,
127
+ endAt: candidate.bookingEndAt || candidate.endAt
128
+ };
129
+ }
130
+ function getCandidateRangeCacheKey(candidate) {
131
+ var bookingRange = getCandidateBookingRange(candidate);
132
+ return "".concat(candidate.key, ":").concat(bookingRange.startAt, ":").concat(bookingRange.endAt);
133
+ }
134
+ function getProductTimeRangeIdentity(range) {
135
+ var bookingRange = getCandidateBookingRange(range);
136
+ return [String(range.productId), range.startAt, range.endAt, bookingRange.startAt, bookingRange.endAt].join(':');
137
+ }
138
+ function toIndexedRange(range) {
139
+ return _objectSpread(_objectSpread({}, range), {}, {
140
+ startMs: range.start,
141
+ endMs: range.end
98
142
  });
99
- return formMatches && resourceMatches;
100
- }
101
- function resolveProductTitle(product) {
102
- var _product$raw, _product$raw2, _product$raw3;
103
- var rawTitle = ((_product$raw = product.raw) === null || _product$raw === void 0 ? void 0 : _product$raw.title) || ((_product$raw2 = product.raw) === null || _product$raw2 === void 0 ? void 0 : _product$raw2.name) || ((_product$raw3 = product.raw) === null || _product$raw3 === void 0 ? void 0 : _product$raw3.product_title);
104
- if (typeof product.title === 'string') return product.title;
105
- if (typeof rawTitle === 'string') return rawTitle;
106
- if (rawTitle && _typeof(rawTitle) === 'object') {
107
- return rawTitle.en || rawTitle.zh || rawTitle.default || Object.values(rawTitle)[0];
143
+ }
144
+ function normalizeNonNegativeNumber(value) {
145
+ var normalized = Number(value);
146
+ if (!Number.isFinite(normalized) || normalized < 0) return null;
147
+ return normalized;
148
+ }
149
+ function resolveCutOffPolicyIssue(policy) {
150
+ if (policy.type === 'invalid') {
151
+ return policy.reason || 'invalid_cutoff_policy';
108
152
  }
109
- return undefined;
110
- }
111
- export function buildDemandIntervals(products, query, context) {
112
- var dates = query.date ? [query.date] : query.dateRange ? addDays(query.dateRange.start, query.dateRange.end) : [];
113
- var selectedProducts = getQueryProducts(products, query);
114
- var intervals = [];
115
- var _iterator = _createForOfIteratorHelper(selectedProducts),
116
- _step;
117
- try {
118
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
119
- var product = _step.value;
120
- var quantity = Math.max(1, Number(product.quantity || 1));
121
- var capacityRequired = Math.max(1, Number(query.pax || product.capacityRequired || 1));
122
- var requirements = product.resourceRequirements || [];
123
- if (product.kind === 'session' || product.kind === 'venue_slot') {
124
- var schedules = product.schedule || [];
125
- var _iterator2 = _createForOfIteratorHelper(schedules),
126
- _step2;
127
- try {
128
- for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
129
- var schedule = _step2.value;
130
- var scheduleDate = dateOf(schedule.start_at);
131
- if (dates.length > 0 && !dates.includes(scheduleDate)) continue;
132
- var start = dayjs(schedule.start_at);
133
- var end = dayjs(schedule.end_at);
134
- intervals.push({
135
- id: "".concat(product.id, ":").concat(start.toISOString(), ":").concat(end.toISOString()),
136
- productId: product.id,
137
- source: product.kind,
138
- date: dateOf(schedule.start_at),
139
- start_at: start.toISOString(),
140
- end_at: end.toISOString(),
141
- durationMinutes: Math.max(1, end.diff(start, 'minute')),
142
- quantity: quantity,
143
- capacityRequired: capacityRequired,
144
- resourceRequirements: requirements
145
- });
146
- }
147
- } catch (err) {
148
- _iterator2.e(err);
149
- } finally {
150
- _iterator2.f();
151
- }
152
- continue;
153
- }
154
- if (product.kind === 'duration') {
155
- var duration = Math.max(1, Number(product.durationMinutes || 30));
156
- var _iterator3 = _createForOfIteratorHelper(dates),
157
- _step3;
158
- try {
159
- for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
160
- var currentDate = _step3.value;
161
- var _start = query.startTime ? toDateTime(currentDate, query.startTime) : toDateTime(currentDate, (context === null || context === void 0 ? void 0 : context.businessStartTime) || DEFAULT_RESOURCE_PLANNER_CONTEXT.businessStartTime);
162
- var _end = query.startTime && query.endTime ? toDateTime(currentDate, query.endTime) : query.startTime ? _start.add(duration, 'minute') : toDateTime(currentDate, (context === null || context === void 0 ? void 0 : context.businessEndTime) || DEFAULT_RESOURCE_PLANNER_CONTEXT.businessEndTime);
163
- if (!_end.isAfter(_start)) _end = _end.add(1, 'day');
164
- intervals.push({
165
- id: "".concat(product.id, ":").concat(currentDate, ":").concat(query.startTime || 'range'),
166
- productId: product.id,
167
- source: 'duration',
168
- date: currentDate,
169
- start_at: _start.toISOString(),
170
- end_at: _end.toISOString(),
171
- durationMinutes: duration,
172
- quantity: quantity,
173
- capacityRequired: capacityRequired,
174
- resourceRequirements: requirements
175
- });
176
- }
177
- } catch (err) {
178
- _iterator3.e(err);
179
- } finally {
180
- _iterator3.f();
181
- }
182
- }
183
- }
184
- } catch (err) {
185
- _iterator.e(err);
186
- } finally {
187
- _iterator.f();
153
+ if ('futureDay' in policy && policy.futureDay !== undefined && normalizeNonNegativeNumber(policy.futureDay) === null) {
154
+ return 'invalid_future_day';
155
+ }
156
+ if (policy.type === 'ongoing' && policy.mode === 'after_start' && normalizeNonNegativeNumber(policy.graceMinutes) === null) {
157
+ return 'invalid_grace_minutes';
188
158
  }
189
- return intervals;
159
+ if (policy.type === 'advance') {
160
+ if (normalizeNonNegativeNumber(policy.unit) === null) return 'invalid_advance_unit';
161
+ if (!['days', 'hours', 'minutes'].includes(policy.unitType)) return 'invalid_advance_unit_type';
162
+ }
163
+ return null;
190
164
  }
191
- function getEventsForResource(resource, contextEvents) {
192
- var embeddedEvents = resource.times.flatMap(function (time) {
193
- return time.event_list || [];
194
- });
195
- return [].concat(_toConsumableArray(embeddedEvents), _toConsumableArray(contextEvents.filter(function (event) {
196
- var _event$resourceId;
197
- return sameId((_event$resourceId = event.resourceId) !== null && _event$resourceId !== void 0 ? _event$resourceId : event.resource_id, resource.id);
198
- }))).filter(Boolean);
199
- }
200
- function collectEventsByResource(resources, contextEvents) {
201
- var eventsByResource = new Map();
202
- resources.forEach(function (resource) {
203
- eventsByResource.set(String(resource.id), getEventsForResource(resource, contextEvents));
204
- });
205
- return eventsByResource;
206
- }
207
- function getWorkingSegmentsForInterval(resource, interval) {
208
- var intervalStart = dayjs(interval.start_at);
209
- var intervalEnd = dayjs(interval.end_at);
210
- return resource.times.map(function (time) {
211
- var start = maxDayjs(dayjs(time.start_at), intervalStart);
212
- var end = minDayjs(dayjs(time.end_at), intervalEnd);
165
+ function addLeadWindow(base, unit, unitType, _timezoneName) {
166
+ if (unitType === 'days') return addLocalCalendarDays(base, unit);
167
+ if (unitType === 'hours') return addLocalHours(base, unit);
168
+ return addLocalMinutes(base, unit);
169
+ }
170
+ function evaluateCandidateTimeGate(params) {
171
+ var product = params.product,
172
+ candidateRange = params.candidateRange,
173
+ evaluationContext = params.evaluationContext,
174
+ timezoneName = params.timezoneName;
175
+ var now = evaluationContext.evaluatedAt;
176
+ if (candidateRange.endMs <= evaluationContext.evaluatedAtMs) {
213
177
  return {
214
- start: start,
215
- end: end
178
+ status: 'past',
179
+ reasonCodes: ['past'],
180
+ conflictCode: 'past'
216
181
  };
217
- }).filter(function (segment) {
218
- return segment.end.isAfter(segment.start);
219
- });
220
- }
221
- function mergeBusySegments(segments) {
222
- var sorted = _toConsumableArray(segments).filter(function (segment) {
223
- return segment.end.isAfter(segment.start);
224
- }).sort(function (left, right) {
225
- return left.start.valueOf() - right.start.valueOf();
226
- });
227
- var merged = [];
228
- sorted.forEach(function (segment) {
229
- var previous = merged[merged.length - 1];
230
- if (previous && !segment.start.isAfter(previous.end)) {
231
- previous.end = maxDayjs(previous.end, segment.end);
232
- } else {
233
- merged.push(_objectSpread({}, segment));
234
- }
235
- });
236
- return merged;
237
- }
238
- function subtractBusyIntervals(workingSegments, events) {
239
- var available = [];
240
- workingSegments.forEach(function (working) {
241
- var busySegments = mergeBusySegments(events.filter(function (event) {
242
- return overlaps(working.start, working.end, dayjs(event.start_at), dayjs(event.end_at));
243
- }).map(function (event) {
182
+ }
183
+ var policy = product.cutOffPolicy;
184
+ if (!policy) return null;
185
+ if (resolveCutOffPolicyIssue(policy)) {
186
+ return {
187
+ status: 'unavailable',
188
+ reasonCodes: ['booking_cutoff_invalid'],
189
+ conflictCode: 'booking_cutoff'
190
+ };
191
+ }
192
+ if ('futureDay' in policy && policy.futureDay !== undefined) {
193
+ var futureDay = normalizeNonNegativeNumber(policy.futureDay);
194
+ var lastAllowedDateOrdinal = dateOrdinalFromLocalDate(formatDate(now)) + futureDay;
195
+ if (dateOrdinalFromLocalDate(formatDate(candidateRange.start)) > lastAllowedDateOrdinal) {
244
196
  return {
245
- start: maxDayjs(dayjs(event.start_at), working.start),
246
- end: minDayjs(dayjs(event.end_at), working.end)
197
+ status: 'unavailable',
198
+ reasonCodes: ['booking_cutoff_future_day'],
199
+ conflictCode: 'booking_cutoff'
247
200
  };
248
- }));
249
- var cursor = working.start;
250
- busySegments.forEach(function (busy) {
251
- if (busy.start.isAfter(cursor)) {
252
- available.push({
253
- start: cursor,
254
- end: busy.start
255
- });
256
- }
257
- if (busy.end.isAfter(cursor)) cursor = busy.end;
258
- });
259
- if (working.end.isAfter(cursor)) {
260
- available.push({
261
- start: cursor,
262
- end: working.end
263
- });
264
- }
265
- });
266
- return available;
267
- }
268
- function buildCapacityRanges(params) {
269
- var workingSegments = params.workingSegments,
270
- events = params.events,
271
- maxCapacity = params.maxCapacity,
272
- requiredCapacity = params.requiredCapacity;
273
- var ranges = [];
274
- workingSegments.forEach(function (working) {
275
- var boundaries = new Map([[working.start.valueOf(), working.start], [working.end.valueOf(), working.end]]);
276
- events.forEach(function (event) {
277
- var eventStart = dayjs(event.start_at);
278
- var eventEnd = dayjs(event.end_at);
279
- if (!overlaps(working.start, working.end, eventStart, eventEnd)) return;
280
- var clippedStart = maxDayjs(eventStart, working.start);
281
- var clippedEnd = minDayjs(eventEnd, working.end);
282
- boundaries.set(clippedStart.valueOf(), clippedStart);
283
- boundaries.set(clippedEnd.valueOf(), clippedEnd);
284
- });
285
- var points = Array.from(boundaries.values()).sort(function (left, right) {
286
- return left.valueOf() - right.valueOf();
287
- });
288
- var _loop = function _loop() {
289
- var start = points[index];
290
- var end = points[index + 1];
291
- if (!end.isAfter(start)) return 0; // continue
292
- var usedCapacity = events.filter(function (event) {
293
- return overlaps(start, end, dayjs(event.start_at), dayjs(event.end_at));
294
- }).reduce(function (sum, event) {
295
- return sum + Math.max(1, Number(event.pax || 1));
296
- }, 0);
297
- var remainingCapacity = Math.max(0, maxCapacity - usedCapacity);
298
- if (remainingCapacity < requiredCapacity) return 0; // continue
299
- var status = remainingCapacity < maxCapacity ? 'limited' : 'available';
300
- var previous = ranges[ranges.length - 1];
301
- if (previous && previous.end.isSame(start) && previous.remainingCapacity === remainingCapacity && previous.maxCapacity === maxCapacity && previous.status === status) {
302
- previous.end = end;
303
- } else {
304
- ranges.push({
305
- start: start,
306
- end: end,
307
- remainingCapacity: remainingCapacity,
308
- maxCapacity: maxCapacity,
309
- status: status
310
- });
311
- }
312
- },
313
- _ret;
314
- for (var index = 0; index < points.length - 1; index += 1) {
315
- _ret = _loop();
316
- if (_ret === 0) continue;
317
201
  }
318
- });
319
- return ranges;
320
- }
321
- function rangeFitsDuration(segment, durationMinutes) {
322
- return segment.end.diff(segment.start, 'minute') >= durationMinutes;
323
- }
324
- function makeAvailabilityRangeCell(params) {
325
- var _segment$remainingCap;
326
- var interval = params.interval,
327
- resource = params.resource,
328
- segment = params.segment;
329
- var maxCapacity = Math.max(1, Number(segment.maxCapacity || resource.capacity || 1));
330
- var remainingCapacity = resource.resourceType === 'single' ? 1 : Math.max(0, Number((_segment$remainingCap = segment.remainingCapacity) !== null && _segment$remainingCap !== void 0 ? _segment$remainingCap : maxCapacity));
331
- var status = resource.resourceType === 'single' ? 'available' : segment.status || (remainingCapacity < maxCapacity ? 'limited' : 'available');
202
+ }
203
+ if (policy.type === 'ongoing') {
204
+ if (policy.mode === 'before_end') return null;
205
+ var graceMinutes = normalizeNonNegativeNumber(policy.graceMinutes);
206
+ var graceDeadline = addMinutesZoned(candidateRange.start, graceMinutes, timezoneName);
207
+ if (now < graceDeadline) return null;
208
+ return {
209
+ status: 'unavailable',
210
+ reasonCodes: ['booking_cutoff_after_start'],
211
+ conflictCode: 'booking_cutoff'
212
+ };
213
+ }
214
+ if (policy.type === 'before_start') {
215
+ if (now < candidateRange.start) return null;
216
+ return {
217
+ status: 'unavailable',
218
+ reasonCodes: ['booking_cutoff_before_start'],
219
+ conflictCode: 'booking_cutoff'
220
+ };
221
+ }
222
+ if (policy.type === 'advance') {
223
+ var leadWindow = addLeadWindow(now, normalizeNonNegativeNumber(policy.unit), policy.unitType, timezoneName);
224
+ if (leadWindow <= candidateRange.start) return null;
225
+ return {
226
+ status: 'unavailable',
227
+ reasonCodes: ['booking_cutoff_advance'],
228
+ conflictCode: 'booking_cutoff'
229
+ };
230
+ }
332
231
  return {
333
- key: "".concat(interval.productId, ":").concat(resource.id, ":").concat(segment.start.toISOString(), ":").concat(segment.end.toISOString()),
334
- productId: interval.productId,
335
- resourceId: resource.id,
336
- date: interval.date,
337
- start_at: segment.start.toISOString(),
338
- end_at: segment.end.toISOString(),
339
- startTime: segment.start.format('HH:mm'),
340
- endTime: segment.end.format('HH:mm'),
341
- durationMinutes: interval.durationMinutes,
342
- cellType: 'availability_range',
343
- status: status,
344
- remainingCapacity: remainingCapacity,
345
- maxCapacity: maxCapacity,
346
- reasonCodes: [],
347
- conflicts: []
232
+ status: 'unavailable',
233
+ reasonCodes: ['booking_cutoff_invalid'],
234
+ conflictCode: 'booking_cutoff'
348
235
  };
349
236
  }
350
- function buildAvailabilityCellsForResource(params) {
351
- var resource = params.resource,
352
- interval = params.interval,
353
- events = params.events;
354
- var requiredCapacity = Math.max(1, Number(interval.capacityRequired || 1));
355
- var durationMinutes = Math.max(1, Number(interval.durationMinutes || 1));
356
- var maxCapacity = Math.max(1, Number(resource.capacity || 1));
357
- var workingSegments = getWorkingSegmentsForInterval(resource, interval);
358
- var now = dayjs();
359
- workingSegments = workingSegments.map(function (segment) {
360
- return {
361
- start: segment.end.isAfter(now) ? maxDayjs(segment.start, now) : segment.start,
362
- end: segment.end
363
- };
364
- }).filter(function (segment) {
365
- return segment.end.isAfter(segment.start);
237
+ function overlaps(left, right) {
238
+ return left.start < right.end && left.end > right.start;
239
+ }
240
+ function contains(outer, inner) {
241
+ return inner.start >= outer.start && inner.end <= outer.end;
242
+ }
243
+ function clipRange(base, clip) {
244
+ var start = Math.max(base.start, clip.start);
245
+ var end = Math.min(base.end, clip.end);
246
+ if (end <= start) return null;
247
+ return {
248
+ start: start,
249
+ end: end
250
+ };
251
+ }
252
+ function sortRanges(ranges) {
253
+ return _toConsumableArray(ranges).sort(function (left, right) {
254
+ return left.start - right.start;
366
255
  });
367
- var availableSegments = resource.resourceType === 'single' ? subtractBusyIntervals(workingSegments, events).map(function (segment) {
368
- return _objectSpread(_objectSpread({}, segment), {}, {
369
- remainingCapacity: 1,
370
- maxCapacity: 1,
371
- status: 'available'
372
- });
373
- }) : buildCapacityRanges({
374
- workingSegments: workingSegments,
375
- events: events,
376
- maxCapacity: maxCapacity,
377
- requiredCapacity: requiredCapacity
256
+ }
257
+ function mergeRanges(ranges) {
258
+ var sorted = sortRanges(ranges);
259
+ var merged = [];
260
+ sorted.forEach(function (range) {
261
+ var last = merged[merged.length - 1];
262
+ if (!last) {
263
+ merged.push(range);
264
+ return;
265
+ }
266
+ if (range.start <= last.end) {
267
+ last.end = Math.max(range.end, last.end);
268
+ return;
269
+ }
270
+ merged.push(range);
378
271
  });
379
- return availableSegments.filter(function (segment) {
380
- return rangeFitsDuration(segment, durationMinutes);
381
- }).map(function (segment) {
382
- return makeAvailabilityRangeCell({
383
- interval: interval,
384
- resource: resource,
385
- segment: segment
272
+ return merged;
273
+ }
274
+ function intersectRangeLists(left, right) {
275
+ var results = [];
276
+ left.forEach(function (leftRange) {
277
+ right.forEach(function (rightRange) {
278
+ var clipped = clipRange(leftRange, rightRange);
279
+ if (clipped) results.push(clipped);
386
280
  });
387
281
  });
282
+ return mergeRanges(results);
388
283
  }
389
- export function evaluateResourceForInterval(params) {
390
- var resource = params.resource,
391
- interval = params.interval,
392
- context = params.context;
393
- var start = dayjs(interval.start_at);
394
- var end = dayjs(interval.end_at);
395
- var conflicts = [];
396
- var reasonCodes = [];
397
- var maxCapacity = Math.max(1, Number(resource.capacity || 1));
398
- var requiredCapacity = Math.max(1, Number(interval.capacityRequired || 1));
399
- if (start.isBefore(dayjs())) {
400
- reasonCodes.push('past');
401
- conflicts.push({
402
- type: 'past',
403
- message: '时间已过期',
404
- resourceId: resource.id,
405
- productId: interval.productId
406
- });
284
+ function enumerateDates(startDate, endDate) {
285
+ var dates = [];
286
+ var cursor = startDate;
287
+ while (cursor <= endDate) {
288
+ dates.push(cursor);
289
+ cursor = nextDate(cursor);
407
290
  }
408
- var insideResourceTime = resource.times.some(function (time) {
409
- return containsRange(dayjs(time.start_at), dayjs(time.end_at), start, end);
410
- });
411
- if (!insideResourceTime) {
412
- reasonCodes.push('outside_resource_time');
413
- conflicts.push({
414
- type: 'outside_resource_time',
415
- message: '所选时间不在资源可用时间内',
416
- resourceId: resource.id,
417
- productId: interval.productId
291
+ return dates;
292
+ }
293
+ function normalizeCoverage(input) {
294
+ var startDate = input.coverage.startDate;
295
+ var endDate = input.coverage.endDate;
296
+ var isCanonicalDate = function isCanonicalDate(value) {
297
+ try {
298
+ return formatLocalDate(parseLocalDate(value)) === value;
299
+ } catch (_error) {
300
+ return false;
301
+ }
302
+ };
303
+ if (!startDate || !endDate || !isCanonicalDate(startDate) || !isCanonicalDate(endDate)) {
304
+ throw availabilityError('INVALID_DATE_RANGE', 'coverage.startDate / coverage.endDate 非法', {
305
+ coverage: input.coverage
418
306
  });
419
307
  }
420
- var events = getEventsForResource(resource, context.externalEvents);
421
- var overlapping = events.filter(function (event) {
422
- return overlaps(start, end, dayjs(event.start_at), dayjs(event.end_at));
423
- });
424
- var usedCapacity = resource.resourceType === 'single' ? overlapping.length : overlapping.reduce(function (sum, event) {
425
- return sum + Math.max(1, Number(event.pax || 1));
426
- }, 0);
427
- var remainingCapacity = resource.resourceType === 'single' ? overlapping.length > 0 ? 0 : 1 : Math.max(0, maxCapacity - usedCapacity);
428
- if (overlapping.length > 0 && resource.resourceType === 'single') {
429
- reasonCodes.push('remote_event_overlap');
430
- conflicts.push.apply(conflicts, _toConsumableArray(overlapping.map(function (event) {
431
- return {
432
- type: 'remote_event_overlap',
433
- message: '资源时间被已有预约占用',
434
- event: event,
435
- resourceId: resource.id,
436
- productId: interval.productId
437
- };
438
- })));
439
- }
440
- if (resource.resourceType !== 'single' && remainingCapacity < requiredCapacity) {
441
- reasonCodes.push('capacity_full');
442
- conflicts.push({
443
- type: 'capacity_full',
444
- message: '资源剩余容量不足',
445
- resourceId: resource.id,
446
- productId: interval.productId
308
+ if (endDate < startDate) {
309
+ throw availabilityError('INVALID_DATE_RANGE', 'coverage.endDate 不能早于 startDate', {
310
+ coverage: input.coverage
447
311
  });
448
312
  }
449
- var status = 'available';
450
- if (reasonCodes.includes('past')) status = 'past';else if (reasonCodes.includes('outside_resource_time')) status = 'unavailable';else if (reasonCodes.includes('remote_event_overlap') || reasonCodes.includes('capacity_full')) {
451
- status = 'full';
452
- } else if (remainingCapacity < maxCapacity) {
453
- status = 'limited';
454
- }
455
313
  return {
456
- key: "".concat(interval.productId, ":").concat(resource.id, ":").concat(interval.start_at, ":").concat(interval.end_at),
457
- productId: interval.productId,
458
- resourceId: resource.id,
459
- date: interval.date,
460
- start_at: interval.start_at,
461
- end_at: interval.end_at,
462
- startTime: timeOf(interval.start_at),
463
- endTime: timeOf(interval.end_at),
464
- durationMinutes: interval.durationMinutes,
465
- cellType: status === 'available' || status === 'limited' ? 'fixed_interval' : 'unavailable',
466
- status: status,
467
- remainingCapacity: remainingCapacity,
468
- maxCapacity: maxCapacity,
469
- reasonCodes: reasonCodes,
470
- conflicts: conflicts
314
+ dateRange: {
315
+ startDate: startDate,
316
+ endDate: endDate
317
+ },
318
+ absolute: {
319
+ start: toDateStart(startDate, input.timezone),
320
+ end: toDateEnd(nextDate(endDate, input.timezone), input.timezone)
321
+ }
471
322
  };
472
323
  }
473
- function aggregateStatus(cells) {
474
- if (!cells.length) return 'unavailable';
475
- if (cells.some(function (cell) {
476
- return cell.status === 'available';
477
- })) return 'available';
478
- if (cells.some(function (cell) {
479
- return cell.status === 'limited';
480
- })) return 'limited';
481
- if (cells.some(function (cell) {
482
- return cell.status === 'past';
483
- })) return 'past';
484
- if (cells.some(function (cell) {
485
- return cell.status === 'full';
486
- })) return 'full';
487
- if (cells.some(function (cell) {
488
- return cell.status === 'conflict';
489
- })) return 'conflict';
490
- return 'unavailable';
324
+ function normalizeResourceWindow(resource, coverage, timezoneName) {
325
+ return mergeRanges((resource.windows || []).map(function (window) {
326
+ return normalizeAbsoluteRange(window, timezoneName);
327
+ }).filter(function (window) {
328
+ return Boolean(window);
329
+ }).map(function (window) {
330
+ return clipRange(window, coverage);
331
+ }).filter(function (window) {
332
+ return Boolean(window);
333
+ }));
491
334
  }
492
- function uniqueReasonCodes(cells) {
493
- return Array.from(new Set(cells.flatMap(function (cell) {
494
- return cell.reasonCodes || [];
495
- })));
335
+ function normalizeOccupancy(occupancy, coverage, timezoneName, fixedOffsetMinutes) {
336
+ var normalized = normalizeAbsoluteRange(occupancy, timezoneName);
337
+ if (!normalized) return null;
338
+ var clipped = clipRange(normalized, coverage);
339
+ if (!clipped) return null;
340
+ return _objectSpread(_objectSpread({}, occupancy), {}, {
341
+ startAt: formatDateTime(clipped.start, timezoneName, fixedOffsetMinutes),
342
+ endAt: formatDateTime(clipped.end, timezoneName, fixedOffsetMinutes),
343
+ pax: Math.max(1, Number(occupancy.pax || 1) || 1)
344
+ });
496
345
  }
497
- function buildProductOptions(products, cells, query) {
498
- var queryProducts = getQueryProducts(products, query);
499
- return queryProducts.map(function (product) {
500
- var productCells = cells.filter(function (cell) {
501
- return sameId(cell.productId, product.id);
502
- });
503
- var status = product.kind === 'normal' ? 'available' : aggregateStatus(productCells);
504
- return {
505
- productId: product.id,
506
- title: resolveProductTitle(product),
507
- status: status,
508
- reasonCodes: uniqueReasonCodes(productCells)
509
- };
346
+ function normalizeRequirementGroup(product, requirementGroup) {
347
+ var _requirementGroup$for;
348
+ var resourceType = 'single';
349
+ var required = requirementGroup.required !== false;
350
+ var min = required ? 1 : 0;
351
+ return _objectSpread(_objectSpread({}, requirementGroup), {}, {
352
+ id: requirementGroup.id || "group:".concat(String(product.id), ":").concat(String((_requirementGroup$for = requirementGroup.formId) !== null && _requirementGroup$for !== void 0 ? _requirementGroup$for : resourceType)),
353
+ resourceType: resourceType,
354
+ required: required,
355
+ min: min,
356
+ max: 1,
357
+ resourceIds: uniqById(requirementGroup.resourceIds || []),
358
+ capacityRequired: Math.max(1, Number(requirementGroup.capacityRequired || product.capacityRequired || 1) || 1)
510
359
  });
511
360
  }
512
- function buildResourceOptions(resources, cells, query) {
513
- var queryResources = getQueryResources(resources, query);
514
- return queryResources.map(function (resource) {
515
- var resourceCells = cells.filter(function (cell) {
516
- return cell.resourceId !== undefined && sameId(cell.resourceId, resource.id);
361
+ function normalizeProducts(products) {
362
+ return products.map(function (product) {
363
+ return _objectSpread(_objectSpread({}, product), {}, {
364
+ quantity: Math.max(1, Number(product.quantity || 1) || 1),
365
+ capacityRequired: Math.max(1, Number(product.capacityRequired || 1) || 1),
366
+ durationMinutes: Math.max(1, Number(product.durationMinutes || 30) || 30),
367
+ cutOffPolicy: product.cutOffPolicy ? _objectSpread({}, product.cutOffPolicy) : undefined,
368
+ requirementGroups: (product.requirementGroups || []).map(function (group) {
369
+ return normalizeRequirementGroup(product, group);
370
+ }),
371
+ sessionRules: (product.sessionRules || []).map(function (rule) {
372
+ return _objectSpread(_objectSpread({}, rule), {}, {
373
+ weekdays: rule.weekdays ? _toConsumableArray(rule.weekdays) : undefined,
374
+ dates: rule.dates ? _toConsumableArray(rule.dates) : undefined,
375
+ includeDates: rule.includeDates ? _toConsumableArray(rule.includeDates) : undefined,
376
+ excludeDates: rule.excludeDates ? _toConsumableArray(rule.excludeDates) : undefined,
377
+ scheduleRef: rule.scheduleRef ? _objectSpread({}, rule.scheduleRef) : undefined
378
+ });
379
+ }),
380
+ sessionRanges: (product.sessionRanges || []).map(function (range) {
381
+ var _range$scheduleRefs;
382
+ return _objectSpread(_objectSpread({}, range), {}, {
383
+ scheduleRefs: (_range$scheduleRefs = range.scheduleRefs) === null || _range$scheduleRefs === void 0 ? void 0 : _range$scheduleRefs.map(function (scheduleRef) {
384
+ return _objectSpread({}, scheduleRef);
385
+ })
386
+ });
387
+ })
517
388
  });
518
- var bestCell = resourceCells.find(function (cell) {
519
- return cell.status === 'available';
520
- }) || resourceCells.find(function (cell) {
521
- return cell.status === 'limited';
522
- }) || resourceCells[0];
523
- return {
524
- resourceId: resource.id,
525
- resourceName: resource.name,
526
- resourceFormId: resource.formId,
527
- resourceType: resource.resourceType,
528
- status: resourceCells.length ? aggregateStatus(resourceCells) : 'unavailable',
529
- remainingCapacity: bestCell === null || bestCell === void 0 ? void 0 : bestCell.remainingCapacity,
530
- maxCapacity: (bestCell === null || bestCell === void 0 ? void 0 : bestCell.maxCapacity) || resource.capacity,
531
- reasonCodes: uniqueReasonCodes(resourceCells)
532
- };
533
389
  });
534
390
  }
535
- function buildDateSummary(cells, query) {
536
- var dates = query.date ? [query.date] : query.dateRange ? addDays(query.dateRange.start, query.dateRange.end) : Array.from(new Set(cells.map(function (cell) {
537
- return cell.date;
538
- }).filter(Boolean)));
539
- return dates.map(function (date) {
540
- var dateCells = cells.filter(function (cell) {
541
- return cell.date === date;
391
+ function normalizeResources(resources) {
392
+ return resources.map(function (resource) {
393
+ return _objectSpread(_objectSpread({}, resource), {}, {
394
+ resourceType: 'single',
395
+ capacity: Math.max(1, Number(resource.capacity || 1) || 1),
396
+ windows: (resource.windows || []).map(function (window) {
397
+ return _objectSpread({}, window);
398
+ })
542
399
  });
543
- var availableCount = dateCells.filter(function (cell) {
544
- return cell.status === 'available' || cell.status === 'limited';
545
- }).length;
546
- return {
547
- date: date,
548
- status: dateCells.length ? aggregateStatus(dateCells) : 'unavailable',
549
- availableCount: availableCount,
550
- totalCount: dateCells.length
551
- };
552
400
  });
553
401
  }
554
- function buildTimeLabels(date, context, query) {
555
- if (!date) return [];
556
- var slotMinutes = query.slotDurationMinutes || context.slotDurationMinutes;
557
- var labels = [];
558
- var cursor = toDateTime(date, context.businessStartTime);
559
- var end = toDateTime(date, context.businessEndTime);
560
- if (!end.isAfter(cursor)) end = end.add(1, 'day');
561
- while (cursor.isBefore(end)) {
562
- labels.push(cursor.format('HH:mm'));
563
- cursor = cursor.add(slotMinutes, 'minute');
564
- }
565
- return labels;
566
- }
567
- function buildGridSlotInterval(product, date, label, context, query) {
568
- var slotMinutes = product.kind === 'duration' ? Math.max(1, Number(product.durationMinutes || query.slotDurationMinutes || context.slotDurationMinutes)) : query.slotDurationMinutes || context.slotDurationMinutes;
569
- var start = toDateTime(date, label);
570
- var end = start.add(slotMinutes, 'minute');
571
- return {
572
- id: "".concat(product.id, ":").concat(date, ":").concat(label),
573
- productId: product.id,
574
- source: product.kind === 'session' ? 'session' : product.kind === 'venue_slot' ? 'venue_slot' : 'duration',
575
- date: date,
576
- start_at: start.toISOString(),
577
- end_at: end.toISOString(),
578
- durationMinutes: slotMinutes,
579
- quantity: Math.max(1, Number(product.quantity || 1)),
580
- capacityRequired: Math.max(1, Number(query.pax || product.capacityRequired || 1)),
581
- resourceRequirements: product.resourceRequirements || []
582
- };
583
- }
584
- function buildTimeGrid(context, query, cells) {
585
- var _query$dateRange;
586
- var date = query.date || ((_query$dateRange = query.dateRange) === null || _query$dateRange === void 0 ? void 0 : _query$dateRange.start);
587
- var products = getQueryProducts(context.products, query).filter(function (product) {
588
- return product.kind !== 'normal';
402
+ function normalizeOperatingHours(params) {
403
+ var _params$input, _params$input2, _params$input3;
404
+ var fallbackWindows = enumerateDates(params.dateRange.startDate, params.dateRange.endDate).map(function (date) {
405
+ var start = toDateTimeInDate(date, params.businessHours.startTime, params.timezoneName);
406
+ var end = toDateTimeInDate(date, params.businessHours.endTime, params.timezoneName);
407
+ if (end <= start) {
408
+ end = toDateTimeInDate(nextDate(date), params.businessHours.endTime, params.timezoneName);
409
+ }
410
+ return {
411
+ start: start,
412
+ end: end
413
+ };
589
414
  });
590
- var resources = getQueryResources(context.resources, query);
591
- var timeLabels = buildTimeLabels(date, context, query);
592
- if (!date || !products.length || !resources.length) {
415
+ var sourceWindows = params.input ? params.input.windows : fallbackWindows.map(function (range) {
593
416
  return {
594
- date: date,
595
- timeLabels: timeLabels,
596
- resources: []
417
+ startAt: formatDateTime(range.start, params.timezoneName, params.fixedOffsetMinutes),
418
+ endAt: formatDateTime(range.end, params.timezoneName, params.fixedOffsetMinutes)
597
419
  };
598
- }
599
- var rows = resources.map(function (resource) {
600
- var slots = timeLabels.map(function (label) {
601
- var product = products[0];
602
- var interval = buildGridSlotInterval(product, date, label, context, query);
603
- var start = dayjs(interval.start_at);
604
- var end = dayjs(interval.end_at);
605
- var cell = cells.find(function (item) {
606
- return sameId(item.productId, product.id) && sameId(item.resourceId, resource.id) && item.date === date && (item.status === 'available' || item.status === 'limited') && containsRange(dayjs(item.start_at), dayjs(item.end_at), start, end);
607
- });
608
- return {
609
- startTime: label,
610
- endTime: dayjs(interval.end_at).format('HH:mm'),
611
- status: (cell === null || cell === void 0 ? void 0 : cell.status) || 'unavailable',
612
- resourceId: resource.id,
613
- productId: product.id,
614
- remainingCapacity: cell === null || cell === void 0 ? void 0 : cell.remainingCapacity,
615
- maxCapacity: (cell === null || cell === void 0 ? void 0 : cell.maxCapacity) || resource.capacity,
616
- reasonCodes: (cell === null || cell === void 0 ? void 0 : cell.reasonCodes) || ['outside_available_range']
617
- };
618
- });
420
+ });
421
+ var windows = mergeRanges(sourceWindows.map(function (window) {
422
+ return normalizeAbsoluteRange(window, params.timezoneName);
423
+ }).filter(function (window) {
424
+ return Boolean(window);
425
+ }).map(function (window) {
426
+ return clipRange(window, params.coverage);
427
+ }).filter(function (window) {
428
+ return Boolean(window);
429
+ })).map(function (range) {
619
430
  return {
620
- resourceId: resource.id,
621
- resourceName: resource.name,
622
- resourceFormId: resource.formId,
623
- slots: slots
431
+ startAt: formatDateTime(range.start, params.timezoneName, params.fixedOffsetMinutes),
432
+ endAt: formatDateTime(range.end, params.timezoneName, params.fixedOffsetMinutes)
624
433
  };
625
434
  });
626
435
  return {
627
- date: date,
628
- timeLabels: timeLabels,
629
- resources: rows
436
+ source: ((_params$input = params.input) === null || _params$input === void 0 ? void 0 : _params$input.source) || params.fallbackSource,
437
+ scheduleIds: uniqById(((_params$input2 = params.input) === null || _params$input2 === void 0 ? void 0 : _params$input2.scheduleIds) || []),
438
+ missingScheduleIds: uniqById(((_params$input3 = params.input) === null || _params$input3 === void 0 ? void 0 : _params$input3.missingScheduleIds) || []),
439
+ windows: windows
630
440
  };
631
441
  }
632
- export function queryPlannerAvailability(contextInput) {
633
- var query = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
634
- var context = normalizePlannerContext(contextInput);
635
- var intervals = buildDemandIntervals(context.products, query, context);
636
- var resources = getQueryResources(context.resources, query);
637
- var eventsByResource = collectEventsByResource(resources, context.externalEvents);
442
+ function matchesRequirementGroup(requirementGroup, resource, product) {
443
+ var _requirementGroup$res;
444
+ var partySize = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 1;
445
+ if (requirementGroup.formId !== undefined && resource.formId !== undefined && !sameId(requirementGroup.formId, resource.formId)) {
446
+ return false;
447
+ }
448
+ if ((_requirementGroup$res = requirementGroup.resourceIds) !== null && _requirementGroup$res !== void 0 && _requirementGroup$res.length && !requirementGroup.resourceIds.some(function (resourceId) {
449
+ return sameId(resourceId, resource.id);
450
+ })) {
451
+ return false;
452
+ }
453
+ return resource.capacity >= resolveRequiredCapacity(requirementGroup, product, partySize);
454
+ }
455
+ function resolveRequiredCapacity(requirementGroup, product, partySize) {
456
+ var _ref, _ref2;
457
+ return Math.max(1, Number((_ref = (_ref2 = partySize !== null && partySize !== void 0 ? partySize : requirementGroup.capacityRequired) !== null && _ref2 !== void 0 ? _ref2 : product.capacityRequired) !== null && _ref !== void 0 ? _ref : 1) || 1);
458
+ }
459
+ function buildCellKey(params) {
460
+ return [String(params.productId), params.groupId, String(params.resourceId), params.start, params.end].join(':');
461
+ }
462
+ function dedupeOccupancyRefKey(occupancy) {
463
+ if (occupancy.scheduleEventId !== undefined && occupancy.scheduleEventId !== null) {
464
+ return "scheduleEvent:".concat(String(occupancy.resourceId), ":").concat(String(occupancy.scheduleEventId));
465
+ }
466
+ if (occupancy.bookingUid !== undefined && occupancy.bookingUid !== null) {
467
+ return "bookingUid:".concat(String(occupancy.resourceId), ":").concat(String(occupancy.bookingUid));
468
+ }
469
+ if (occupancy.productUid !== undefined && occupancy.productUid !== null) {
470
+ return "productUid:".concat(String(occupancy.resourceId), ":").concat(String(occupancy.productUid));
471
+ }
472
+ return "range:".concat(String(occupancy.resourceId), ":").concat(occupancy.startAt, ":").concat(occupancy.endAt, ":").concat(String(occupancy.pax || 1));
473
+ }
474
+ function buildCells(projection) {
475
+ var _extractFixedOffsetMi;
476
+ var coverage = {
477
+ start: toDateTime(projection.meta.coverage.startAt, projection.meta.timezone),
478
+ end: toDateTime(projection.meta.coverage.endAt, projection.meta.timezone)
479
+ };
638
480
  var cells = [];
639
- var _iterator4 = _createForOfIteratorHelper(intervals),
640
- _step4;
641
- try {
642
- var _loop2 = function _loop2() {
643
- var interval = _step4.value;
644
- var candidateResources = resources.filter(function (resource) {
645
- var requirements = interval.resourceRequirements || [];
646
- if (!requirements.length) return true;
647
- return requirements.some(function (requirement) {
648
- return matchesResourceRequirement(resource, requirement);
481
+ var timezoneName = projection.meta.timezone;
482
+ var fixedOffsetMinutes = (_extractFixedOffsetMi = extractFixedOffsetMinutes(projection.meta.now)) !== null && _extractFixedOffsetMi !== void 0 ? _extractFixedOffsetMi : captureRuntimeOffsetMinutes();
483
+ var projectionNow = toDateTime(projection.meta.now);
484
+ projection.products.forEach(function (product) {
485
+ (product.requirementGroups || []).forEach(function (requirementGroup) {
486
+ projection.resources.filter(function (resource) {
487
+ return matchesRequirementGroup(requirementGroup, resource, product);
488
+ }).forEach(function (resource) {
489
+ var windows = normalizeResourceWindow(resource, coverage, timezoneName);
490
+ var occupancies = projection.resourceOccupancies.filter(function (occupancy) {
491
+ return sameId(occupancy.resourceId, resource.id);
492
+ }).map(function (occupancy) {
493
+ return {
494
+ occupancy: occupancy,
495
+ range: normalizeAbsoluteRange(occupancy, timezoneName)
496
+ };
497
+ }).filter(function (item) {
498
+ return Boolean(item.range);
499
+ }).map(function (item) {
500
+ return _objectSpread(_objectSpread({}, item), {}, {
501
+ range: clipRange(item.range, coverage)
502
+ });
503
+ }).filter(function (item) {
504
+ return Boolean(item.range);
649
505
  });
650
- });
651
- if (!candidateResources.length) {
652
- cells.push({
653
- key: "".concat(interval.productId, ":missing-resource:").concat(interval.start_at),
654
- productId: interval.productId,
655
- date: interval.date,
656
- start_at: interval.start_at,
657
- end_at: interval.end_at,
658
- startTime: timeOf(interval.start_at),
659
- endTime: timeOf(interval.end_at),
660
- durationMinutes: interval.durationMinutes,
661
- cellType: 'unavailable',
662
- status: 'unavailable',
663
- reasonCodes: ['missing_resource'],
664
- conflicts: [{
665
- type: 'missing_resource',
666
- message: '没有匹配商品需求的资源',
667
- productId: interval.productId
668
- }]
506
+ var boundaries = new Set([coverage.start, coverage.end]);
507
+ windows.forEach(function (window) {
508
+ boundaries.add(window.start);
509
+ boundaries.add(window.end);
669
510
  });
670
- return 1; // continue
671
- }
672
- candidateResources.forEach(function (resource) {
673
- if (interval.source === 'duration' && !query.startTime) {
674
- cells.push.apply(cells, _toConsumableArray(buildAvailabilityCellsForResource({
675
- resource: resource,
676
- interval: interval,
677
- events: eventsByResource.get(String(resource.id)) || []
511
+ occupancies.forEach(function (_ref3) {
512
+ var range = _ref3.range;
513
+ boundaries.add(range.start);
514
+ boundaries.add(range.end);
515
+ });
516
+ var sortedBoundaries = Array.from(boundaries.values()).sort(function (left, right) {
517
+ return left - right;
518
+ });
519
+ var _loop = function _loop() {
520
+ var start = sortedBoundaries[index];
521
+ var end = sortedBoundaries[index + 1];
522
+ if (end <= start) return 1; // continue
523
+ var segment = {
524
+ start: start,
525
+ end: end
526
+ };
527
+ var isInsideWorkingWindow = windows.some(function (window) {
528
+ return contains(window, segment);
529
+ });
530
+ var segmentOccupancies = isInsideWorkingWindow ? occupancies.filter(function (_ref4) {
531
+ var range = _ref4.range;
532
+ return overlaps(range, segment);
533
+ }).map(function (_ref5) {
534
+ var occupancy = _ref5.occupancy;
535
+ return occupancy;
536
+ }) : [];
537
+ var usedCapacity = segmentOccupancies.reduce(function (sum, occupancy) {
538
+ return sum + Math.max(1, Number(occupancy.pax || 1) || 1);
539
+ }, 0);
540
+ var remainingCapacity = Math.max(0, resource.capacity - usedCapacity);
541
+ var requiredCapacity = resolveRequiredCapacity(requirementGroup, product);
542
+ var reasonCodes = Array.from(new Set(segmentOccupancies.flatMap(function (occupancy) {
543
+ var source = occupancy.source || 'remote';
544
+ if (source === 'selection') return ['selection_occupied'];
545
+ if (source === 'cart') return ['cart_occupied'];
546
+ return ['resource_occupied'];
678
547
  })));
679
- return;
548
+ var status = 'available';
549
+ if (end <= projectionNow) {
550
+ status = 'past';
551
+ reasonCodes.push('past');
552
+ } else if (!isInsideWorkingWindow) {
553
+ status = 'unavailable';
554
+ reasonCodes.push('outside_resource_window');
555
+ } else if (remainingCapacity < requiredCapacity) {
556
+ status = 'full';
557
+ reasonCodes.push('capacity_full');
558
+ } else if (remainingCapacity < resource.capacity) {
559
+ status = 'limited';
560
+ }
561
+ cells.push({
562
+ key: buildCellKey({
563
+ productId: product.id,
564
+ groupId: requirementGroup.id,
565
+ resourceId: resource.id,
566
+ start: start,
567
+ end: end
568
+ }),
569
+ productId: product.id,
570
+ requirementGroupId: requirementGroup.id,
571
+ requirementFormId: requirementGroup.formId,
572
+ resourceId: resource.id,
573
+ resourceName: resource.name,
574
+ resourceType: resource.resourceType,
575
+ startAt: formatDateTime(start, timezoneName, fixedOffsetMinutes),
576
+ endAt: formatDateTime(end, timezoneName, fixedOffsetMinutes),
577
+ date: formatDate(start, timezoneName),
578
+ startTime: formatTime(start, timezoneName),
579
+ endTime: formatTime(end, timezoneName),
580
+ timezone: projection.meta.timezone,
581
+ status: status,
582
+ // 资源组当前统一单选;具体资源仍用 capacity 表达可承载的预约数量。
583
+ remainingCapacity: remainingCapacity,
584
+ maxCapacity: resource.capacity,
585
+ reasonCodes: Array.from(new Set(reasonCodes)),
586
+ occupancyRefs: segmentOccupancies.map(function (occupancy) {
587
+ return {
588
+ id: occupancy.id,
589
+ resourceId: occupancy.resourceId,
590
+ source: occupancy.source,
591
+ serverEventId: occupancy.serverEventId,
592
+ bookingUid: occupancy.bookingUid,
593
+ productUid: occupancy.productUid,
594
+ scheduleEventId: occupancy.scheduleEventId
595
+ };
596
+ })
597
+ });
598
+ };
599
+ for (var index = 0; index < sortedBoundaries.length - 1; index += 1) {
600
+ if (_loop()) continue;
680
601
  }
681
- cells.push(evaluateResourceForInterval({
682
- resource: resource,
683
- interval: interval,
684
- context: context
685
- }));
686
602
  });
687
- };
688
- for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
689
- if (_loop2()) continue;
603
+ });
604
+ });
605
+ return compressCells(cells);
606
+ }
607
+ function compressCells(cells) {
608
+ var sorted = _toConsumableArray(cells).sort(function (left, right) {
609
+ var keyLeft = [String(left.productId), String(left.requirementGroupId), String(left.resourceId), left.startAt].join(':');
610
+ var keyRight = [String(right.productId), String(right.requirementGroupId), String(right.resourceId), right.startAt].join(':');
611
+ return keyLeft.localeCompare(keyRight);
612
+ });
613
+ return sorted.reduce(function (result, cell) {
614
+ var last = result[result.length - 1];
615
+ if (last && sameId(last.productId, cell.productId) && sameId(last.requirementGroupId, cell.requirementGroupId) && sameId(last.resourceId, cell.resourceId) && last.status === cell.status && last.remainingCapacity === cell.remainingCapacity && last.maxCapacity === cell.maxCapacity && JSON.stringify(last.reasonCodes) === JSON.stringify(cell.reasonCodes) && JSON.stringify(last.occupancyRefs) === JSON.stringify(cell.occupancyRefs) && last.endAt === cell.startAt) {
616
+ last.endAt = cell.endAt;
617
+ last.endTime = cell.endTime;
618
+ return result;
690
619
  }
691
- } catch (err) {
692
- _iterator4.e(err);
693
- } finally {
694
- _iterator4.f();
695
- }
620
+ result.push(_objectSpread({}, cell));
621
+ return result;
622
+ }, []);
623
+ }
624
+ function makeCoverageAbsolute(dateRange, timezoneName, fixedOffsetMinutes) {
625
+ var guardEndDate = nextDate(dateRange.endDate, timezoneName);
696
626
  return {
697
- query: query,
698
- demandIntervals: intervals,
699
- productOptions: buildProductOptions(context.products, cells, query),
700
- resourceOptions: buildResourceOptions(context.resources, cells, query),
701
- dateSummary: buildDateSummary(cells, query),
702
- timeGrid: buildTimeGrid(context, query, cells),
703
- cells: cells
627
+ startAt: formatDateTime(toDateStart(dateRange.startDate, timezoneName), timezoneName, fixedOffsetMinutes),
628
+ endAt: formatDateTime(toDateEnd(guardEndDate, timezoneName), timezoneName, fixedOffsetMinutes)
704
629
  };
705
630
  }
706
- function getIntervalRequirements(interval) {
707
- return interval.resourceRequirements.length ? interval.resourceRequirements : [{
708
- capacityRequired: interval.capacityRequired,
709
- required: true
710
- }];
631
+ function normalizeProjectionInstant(value, fallbackScalar, timezoneName, fixedOffsetMinutes) {
632
+ var scalar = value ? toDateTime(value) : fallbackScalar;
633
+ return formatDateTime(Number.isFinite(scalar) ? scalar : fallbackScalar, timezoneName, fixedOffsetMinutes);
711
634
  }
712
- function buildAssignableCandidateGroups(params) {
713
- var interval = params.interval,
714
- cells = params.cells,
715
- resources = params.resources;
716
- var conflicts = [];
717
- var groups = getIntervalRequirements(interval).map(function (requirement) {
718
- var candidateCells = cells.filter(function (cell) {
719
- if (!sameId(cell.productId, interval.productId)) return false;
720
- if (cell.date !== interval.date) return false;
721
- if (cell.resourceId === undefined) return false;
722
- if (cell.status !== 'available' && cell.status !== 'limited') return false;
723
- var cellStart = dayjs(cell.start_at);
724
- var cellEnd = dayjs(cell.end_at);
725
- var intervalStart = dayjs(interval.start_at);
726
- var intervalEnd = dayjs(interval.end_at);
727
- return containsRange(cellStart, cellEnd, intervalStart, intervalEnd) || interval.source === 'duration';
728
- });
729
- return candidateCells.flatMap(function (cell) {
730
- var resource = resources.find(function (item) {
731
- return sameId(item.id, cell.resourceId);
732
- });
733
- if (!resource || !matchesResourceRequirement(resource, requirement)) return [];
734
- return [{
735
- requirement: requirement,
736
- resource: resource,
737
- cell: cell
738
- }];
635
+ function cloneSnapshotMetadataValue(value) {
636
+ if (Array.isArray(value)) {
637
+ return value.map(function (item) {
638
+ return cloneSnapshotMetadataValue(item);
739
639
  });
640
+ }
641
+ if (!value || _typeof(value) !== 'object') return value;
642
+ return Object.keys(value).reduce(function (result, key) {
643
+ result[key] = cloneSnapshotMetadataValue(value[key]);
644
+ return result;
645
+ }, {});
646
+ }
647
+ function deepFreeze(value) {
648
+ if (!value || _typeof(value) !== 'object' || Object.isFrozen(value)) return value;
649
+ Object.freeze(value);
650
+ Object.getOwnPropertyNames(value).forEach(function (key) {
651
+ // `raw` is an explicit escape hatch to the backend/catalog record. It is not owned by the
652
+ // projection and must not freeze mutable ProductList / Order data shared elsewhere.
653
+ if (key === 'raw') return;
654
+ var child = value[key];
655
+ if (child && (_typeof(child) === 'object' || typeof child === 'function')) {
656
+ deepFreeze(child);
657
+ }
740
658
  });
741
- groups.forEach(function (group, index) {
742
- var _interval$resourceReq;
743
- if (group.length) return;
744
- conflicts.push({
745
- type: 'missing_resource',
746
- message: '没有匹配商品需求的可用资源',
747
- productId: interval.productId,
748
- resourceId: (_interval$resourceReq = interval.resourceRequirements[index]) === null || _interval$resourceReq === void 0 || (_interval$resourceReq = _interval$resourceReq.resourceIds) === null || _interval$resourceReq === void 0 ? void 0 : _interval$resourceReq[0]
749
- });
659
+ return value;
660
+ }
661
+ export function createAvailabilityProjection(input) {
662
+ var _input$businessHours, _input$businessHours2;
663
+ var coverage = normalizeCoverage(input);
664
+ var fixedOffsetMinutes = captureRuntimeOffsetMinutes();
665
+ var runtimeNow = localDateToScalar(new Date());
666
+ var products = normalizeProducts(input.products || []);
667
+ var resources = normalizeResources(input.resources || []);
668
+ var resourceOccupancies = uniqByOccupancy((input.resourceOccupancies || []).map(function (occupancy) {
669
+ return normalizeOccupancy(occupancy, coverage.absolute, input.timezone, fixedOffsetMinutes);
670
+ }).filter(function (occupancy) {
671
+ return Boolean(occupancy);
672
+ }));
673
+ var preparedAt = normalizeProjectionInstant(input.preparedAt, runtimeNow, input.timezone, fixedOffsetMinutes);
674
+ var now = normalizeProjectionInstant(input.now, runtimeNow, input.timezone, fixedOffsetMinutes);
675
+ var durationCandidateAnchorAt = normalizeProjectionInstant(input.durationCandidateAnchorAt || now, runtimeNow, input.timezone, fixedOffsetMinutes);
676
+ var businessHours = {
677
+ startTime: ((_input$businessHours = input.businessHours) === null || _input$businessHours === void 0 ? void 0 : _input$businessHours.startTime) || DEFAULT_BUSINESS_HOURS.startTime,
678
+ endTime: ((_input$businessHours2 = input.businessHours) === null || _input$businessHours2 === void 0 ? void 0 : _input$businessHours2.endTime) || DEFAULT_BUSINESS_HOURS.endTime
679
+ };
680
+ var operatingHours = normalizeOperatingHours({
681
+ input: input.operatingHours,
682
+ businessHours: businessHours,
683
+ coverage: coverage.absolute,
684
+ dateRange: coverage.dateRange,
685
+ timezoneName: input.timezone,
686
+ fixedOffsetMinutes: fixedOffsetMinutes,
687
+ fallbackSource: input.businessHours ? 'prepare_business_hours' : 'default_business_hours'
750
688
  });
751
- return {
752
- groups: groups,
753
- conflicts: conflicts
689
+ var meta = {
690
+ snapshotId: input.snapshotId || "availability-".concat(Date.now(), "-").concat(Math.random().toString(36).slice(2, 8)),
691
+ preparedAt: preparedAt,
692
+ timezone: input.timezone,
693
+ orderRevision: input.orderRevision,
694
+ mode: input.mode || 'appointment',
695
+ coverage: _objectSpread(_objectSpread({}, coverage.dateRange), makeCoverageAbsolute(coverage.dateRange, input.timezone, fixedOffsetMinutes)),
696
+ defaults: {
697
+ slotStepMinutes: Math.max(1, Number(input.slotStepMinutes || DEFAULT_SLOT_STEP_MINUTES) || DEFAULT_SLOT_STEP_MINUTES),
698
+ businessHours: _objectSpread({}, businessHours),
699
+ operatingHours: operatingHours
700
+ },
701
+ now: now,
702
+ durationCandidateAnchorAt: durationCandidateAnchorAt,
703
+ anchors: input.anchors ? cloneSnapshotMetadataValue(input.anchors) : undefined,
704
+ prepareParams: input.prepareParams ? cloneSnapshotMetadataValue(input.prepareParams) : undefined
705
+ };
706
+ var projectionBase = {
707
+ meta: meta,
708
+ products: products,
709
+ resources: resources,
710
+ resourceOccupancies: resourceOccupancies
754
711
  };
712
+ return deepFreeze(_objectSpread(_objectSpread({}, projectionBase), {}, {
713
+ cells: buildCells(projectionBase)
714
+ }));
755
715
  }
756
- function combineCandidateGroups(groups) {
757
- if (!groups.length || groups.some(function (group) {
758
- return group.length === 0;
759
- })) return [];
760
- return groups.reduce(function (combinations, group) {
761
- return combinations.flatMap(function (combination) {
762
- return group.map(function (candidate) {
763
- return [].concat(_toConsumableArray(combination), [candidate]);
764
- });
765
- });
766
- }, [[]]);
767
- }
768
- function buildSlotFromCandidates(params) {
769
- var interval = params.interval,
770
- candidates = params.candidates,
771
- start = params.start,
772
- end = params.end;
773
- if (!end.isAfter(start)) return null;
774
- var status = candidates.some(function (candidate) {
775
- return candidate.cell.status === 'limited';
776
- }) ? 'limited' : 'available';
777
- var assignments = candidates.map(function (candidate) {
778
- return {
779
- productId: interval.productId,
780
- resourceId: candidate.resource.id,
781
- start_at: start.toISOString(),
782
- end_at: end.toISOString(),
783
- capacityRequired: Math.max(1, Number(candidate.requirement.capacityRequired || interval.capacityRequired || 1))
784
- };
716
+ function uniqByOccupancy(occupancies) {
717
+ var map = new Map();
718
+ occupancies.forEach(function (occupancy) {
719
+ map.set(dedupeOccupancyRefKey(occupancy), occupancy);
785
720
  });
786
- return {
787
- key: [interval.productId, start.toISOString(), end.toISOString()].concat(_toConsumableArray(candidates.map(function (candidate) {
788
- return candidate.resource.id;
789
- }))).join(':'),
790
- productId: interval.productId,
791
- date: interval.date,
792
- start_at: start.toISOString(),
793
- end_at: end.toISOString(),
794
- startTime: start.format('HH:mm'),
795
- endTime: end.format('HH:mm'),
796
- durationMinutes: Math.max(1, end.diff(start, 'minute')),
797
- status: status,
798
- resources: candidates.map(function (candidate) {
799
- return {
800
- resourceId: candidate.resource.id,
801
- resourceName: candidate.resource.name,
802
- resourceFormId: candidate.resource.formId,
803
- resourceType: candidate.resource.resourceType,
804
- capacityRequired: Math.max(1, Number(candidate.requirement.capacityRequired || interval.capacityRequired || 1)),
805
- remainingCapacity: candidate.cell.remainingCapacity,
806
- maxCapacity: candidate.cell.maxCapacity
807
- };
808
- }),
809
- assignments: assignments,
810
- conflicts: []
811
- };
721
+ return Array.from(map.values());
812
722
  }
813
- function buildSlotsForInterval(params) {
814
- var interval = params.interval,
815
- candidates = params.candidates,
816
- query = params.query,
817
- context = params.context;
818
- var start = candidates.reduce(function (current, candidate) {
819
- return maxDayjs(current, dayjs(candidate.cell.start_at));
820
- }, dayjs(interval.start_at));
821
- var end = candidates.reduce(function (current, candidate) {
822
- return minDayjs(current, dayjs(candidate.cell.end_at));
823
- }, dayjs(interval.end_at));
824
- var durationMinutes = Math.max(1, Number(interval.durationMinutes || 1));
825
- if (interval.source !== 'duration' || query.startTime) {
826
- var fixedStart = dayjs(interval.start_at);
827
- var fixedEnd = dayjs(interval.end_at);
828
- if (containsRange(start, end, fixedStart, fixedEnd) || candidates.every(function (candidate) {
829
- return candidate.cell.cellType === 'fixed_interval';
830
- })) {
831
- var slot = buildSlotFromCandidates({
832
- interval: interval,
833
- candidates: candidates,
834
- start: fixedStart,
835
- end: fixedEnd
836
- });
837
- return slot ? [slot] : [];
723
+ function resolveProjectionDateRange(projection, requestDateRange) {
724
+ var startDate = (requestDateRange === null || requestDateRange === void 0 ? void 0 : requestDateRange.startDate) || projection.meta.coverage.startDate;
725
+ var endDate = (requestDateRange === null || requestDateRange === void 0 ? void 0 : requestDateRange.endDate) || projection.meta.coverage.endDate;
726
+ var isCanonicalDate = function isCanonicalDate(value) {
727
+ try {
728
+ return formatLocalDate(parseLocalDate(value)) === value;
729
+ } catch (_error) {
730
+ return false;
838
731
  }
839
- return [];
840
- }
841
- var stepMinutes = Math.max(1, Number(query.slotDurationMinutes || context.slotDurationMinutes || 30));
842
- var slots = [];
843
- var cursor = start;
844
- while (!cursor.add(durationMinutes, 'minute').isAfter(end)) {
845
- var slotEnd = cursor.add(durationMinutes, 'minute');
846
- var _slot = buildSlotFromCandidates({
847
- interval: interval,
848
- candidates: candidates,
849
- start: cursor,
850
- end: slotEnd
732
+ };
733
+ if (!isCanonicalDate(startDate) || !isCanonicalDate(endDate) || endDate < startDate) {
734
+ throw availabilityError('INVALID_DATE_RANGE', '查询日期范围必须是有效的 YYYY-MM-DD,且结束日期不能早于开始日期', {
735
+ requested: requestDateRange
851
736
  });
852
- if (_slot) slots.push(_slot);
853
- cursor = cursor.add(stepMinutes, 'minute');
854
737
  }
855
- return slots;
856
- }
857
- export function resolveAssignableSlots(contextInput) {
858
- var query = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
859
- var context = normalizePlannerContext(contextInput);
860
- var projection = queryPlannerAvailability(context, query);
861
- var resources = getQueryResources(context.resources, query);
862
- var slotsByKey = new Map();
863
- var conflicts = [];
864
- projection.demandIntervals.forEach(function (interval) {
865
- var _buildAssignableCandi = buildAssignableCandidateGroups({
866
- interval: interval,
867
- cells: projection.cells,
868
- resources: resources
869
- }),
870
- groups = _buildAssignableCandi.groups,
871
- groupConflicts = _buildAssignableCandi.conflicts;
872
- conflicts.push.apply(conflicts, _toConsumableArray(groupConflicts));
873
- combineCandidateGroups(groups).forEach(function (candidates) {
874
- buildSlotsForInterval({
875
- interval: interval,
876
- candidates: candidates,
877
- query: query,
878
- context: context
879
- }).forEach(function (slot) {
880
- if (!slotsByKey.has(slot.key)) slotsByKey.set(slot.key, slot);
881
- });
738
+ if (startDate < projection.meta.coverage.startDate || endDate > projection.meta.coverage.endDate) {
739
+ throw availabilityError('OUT_OF_PROJECTION_COVERAGE', '查询范围超出 projection.coverage', {
740
+ requested: requestDateRange,
741
+ coverage: projection.meta.coverage
882
742
  });
883
- });
743
+ }
884
744
  return {
885
- projection: projection,
886
- slots: Array.from(slotsByKey.values()),
887
- conflicts: conflicts
745
+ startDate: startDate,
746
+ endDate: endDate
888
747
  };
889
748
  }
890
- export function autoAssignResources(contextInput) {
891
- var query = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
892
- var baseContext = normalizePlannerContext(contextInput);
893
- var projection = queryPlannerAvailability(baseContext, query);
894
- var assignments = [];
895
- var conflicts = [];
896
- var missingTimeProductIds = new Set();
897
- var _iterator5 = _createForOfIteratorHelper(projection.demandIntervals),
898
- _step5;
899
- try {
900
- var _loop3 = function _loop3() {
901
- var interval = _step5.value;
902
- if (interval.source === 'duration' && !query.startTime) {
903
- var productKey = String(interval.productId);
904
- if (!missingTimeProductIds.has(productKey)) {
905
- missingTimeProductIds.add(productKey);
906
- conflicts.push({
907
- type: 'missing_time',
908
- message: 'duration 商品需要先确定开始时间后才能自动分配资源',
909
- productId: interval.productId
910
- });
911
- }
912
- return 0; // continue
913
- }
914
- var contextWithAssignments = withAssignmentEvents(baseContext, assignments);
915
- var candidateResources = getQueryResources(contextWithAssignments.resources, query).filter(function (resource) {
916
- var requirements = interval.resourceRequirements || [];
917
- if (!requirements.length) return true;
918
- return requirements.some(function (requirement) {
919
- return matchesResourceRequirement(resource, requirement);
920
- });
921
- });
922
- var availableCell = candidateResources.map(function (resource) {
923
- return evaluateResourceForInterval({
924
- resource: resource,
925
- interval: interval,
926
- context: contextWithAssignments
927
- });
928
- }).find(function (cell) {
929
- return cell.status === 'available' || cell.status === 'limited';
930
- });
931
- if (!(availableCell !== null && availableCell !== void 0 && availableCell.resourceId)) {
932
- conflicts.push({
933
- type: 'missing_resource',
934
- message: '无法为商品自动分配可用资源',
935
- productId: interval.productId
936
- });
937
- return 0; // continue
749
+ function buildOperatingRanges(projection, dateRange) {
750
+ var coverage = {
751
+ start: toDateStart(dateRange.startDate, projection.meta.timezone),
752
+ end: toDateEnd(nextDate(dateRange.endDate), projection.meta.timezone)
753
+ };
754
+ return mergeRanges(projection.meta.defaults.operatingHours.windows.map(function (window) {
755
+ return normalizeAbsoluteRange(window, projection.meta.timezone);
756
+ }).filter(function (window) {
757
+ return Boolean(window);
758
+ }).map(function (window) {
759
+ return clipRange(window, coverage);
760
+ }).filter(function (window) {
761
+ return Boolean(window);
762
+ }));
763
+ }
764
+ function buildResourceWindowIndex(projection) {
765
+ var coverage = {
766
+ start: toDateTime(projection.meta.coverage.startAt, projection.meta.timezone),
767
+ end: toDateTime(projection.meta.coverage.endAt, projection.meta.timezone)
768
+ };
769
+ return new Map(projection.resources.map(function (resource) {
770
+ return [String(resource.id), normalizeResourceWindow(resource, coverage, projection.meta.timezone)];
771
+ }));
772
+ }
773
+ function buildGroupWorkingRanges(params) {
774
+ var _requirementGroup$min;
775
+ var projection = params.projection,
776
+ requirementGroup = params.requirementGroup,
777
+ dateRange = params.dateRange,
778
+ resourceIds = params.resourceIds,
779
+ partySize = params.partySize;
780
+ var guardEndDate = nextDate(dateRange.endDate, projection.meta.timezone);
781
+ var coverage = {
782
+ start: toDateStart(dateRange.startDate, projection.meta.timezone),
783
+ end: toDateEnd(guardEndDate, projection.meta.timezone)
784
+ };
785
+ var filteredResourceIdSet = resourceIds !== null && resourceIds !== void 0 && resourceIds.length ? new Set(resourceIds.map(String)) : null;
786
+ var resourceWindows = projection.resources.filter(function (resource) {
787
+ return matchesRequirementGroup(requirementGroup, resource, params.product, partySize);
788
+ }).filter(function (resource) {
789
+ return !filteredResourceIdSet || filteredResourceIdSet.has(String(resource.id));
790
+ }).map(function (resource) {
791
+ var _params$resourceWindo;
792
+ var windows = ((_params$resourceWindo = params.resourceWindowIndex) === null || _params$resourceWindo === void 0 ? void 0 : _params$resourceWindo.get(String(resource.id))) || normalizeResourceWindow(resource, coverage, projection.meta.timezone);
793
+ return windows.map(function (window) {
794
+ return clipRange(window, coverage);
795
+ }).filter(function (window) {
796
+ return Boolean(window);
797
+ });
798
+ });
799
+ var requiredConcurrentResources = Math.max(requirementGroup.required === false ? 0 : 1, Number((_requirementGroup$min = requirementGroup.min) !== null && _requirementGroup$min !== void 0 ? _requirementGroup$min : requirementGroup.required === false ? 0 : 1) || 0);
800
+ if (requiredConcurrentResources <= 1) {
801
+ return mergeRanges(resourceWindows.flatMap(function (windows) {
802
+ return windows;
803
+ }));
804
+ }
805
+ var boundaries = new Set();
806
+ resourceWindows.forEach(function (windows) {
807
+ windows.forEach(function (window) {
808
+ boundaries.add(window.start);
809
+ boundaries.add(window.end);
810
+ });
811
+ });
812
+ var sortedBoundaries = Array.from(boundaries.values()).sort(function (left, right) {
813
+ return left - right;
814
+ });
815
+ var satisfiedRanges = [];
816
+ var _loop2 = function _loop2() {
817
+ var start = sortedBoundaries[index];
818
+ var end = sortedBoundaries[index + 1];
819
+ if (end <= start) return 1; // continue
820
+ var segment = {
821
+ start: start,
822
+ end: end
823
+ };
824
+ var activeResources = resourceWindows.reduce(function (count, windows) {
825
+ return windows.some(function (window) {
826
+ return contains(window, segment);
827
+ }) ? count + 1 : count;
828
+ }, 0);
829
+ if (activeResources >= requiredConcurrentResources) {
830
+ satisfiedRanges.push(segment);
831
+ }
832
+ };
833
+ for (var index = 0; index < sortedBoundaries.length - 1; index += 1) {
834
+ if (_loop2()) continue;
835
+ }
836
+ return mergeRanges(satisfiedRanges);
837
+ }
838
+ function materializeScheduleRules(rules, dateRange, timezoneName, fixedOffsetMinutes) {
839
+ var dates = enumerateDates(dateRange.startDate, dateRange.endDate);
840
+ var seen = new Map();
841
+ rules.forEach(function (rule) {
842
+ dates.forEach(function (date) {
843
+ if (!matchesScheduleRuleDate(rule, date, timezoneName)) return;
844
+ var start = toDateTimeInDate(date, rule.startTime, timezoneName);
845
+ var end = toDateTimeInDate(date, rule.endTime, timezoneName);
846
+ if (end <= start) end = toDateTimeInDate(nextDate(date, timezoneName), rule.endTime, timezoneName);
847
+ var key = "".concat(date, ":").concat(start, ":").concat(end);
848
+ if (!seen.has(key)) {
849
+ seen.set(key, {
850
+ key: key,
851
+ productId: '',
852
+ source: 'session_schedule',
853
+ startAt: formatDateTime(start, timezoneName, fixedOffsetMinutes),
854
+ endAt: formatDateTime(end, timezoneName, fixedOffsetMinutes),
855
+ bookingStartAt: formatDateTime(start, timezoneName, fixedOffsetMinutes),
856
+ bookingEndAt: formatDateTime(end, timezoneName, fixedOffsetMinutes),
857
+ date: formatDate(start, timezoneName),
858
+ startTime: formatTime(start, timezoneName),
859
+ endTime: formatTime(end, timezoneName),
860
+ timezone: timezoneName,
861
+ scheduleRefs: []
862
+ });
863
+ }
864
+ var current = seen.get(key);
865
+ var scheduleRef = _objectSpread(_objectSpread({}, rule.scheduleRef || {}), {}, {
866
+ ruleId: rule.id
867
+ });
868
+ current.scheduleRefs = mergeScheduleRefs(current.scheduleRefs, [scheduleRef]);
869
+ if (!current.primaryScheduleRef) current.primaryScheduleRef = scheduleRef;
870
+ });
871
+ });
872
+ return Array.from(seen.values());
873
+ }
874
+ function matchesScheduleRuleDate(rule, date, _timezoneName) {
875
+ var _rule$includeDates, _rule$excludeDates;
876
+ if ((_rule$includeDates = rule.includeDates) !== null && _rule$includeDates !== void 0 && _rule$includeDates.includes(date)) return true;
877
+ if ((_rule$excludeDates = rule.excludeDates) !== null && _rule$excludeDates !== void 0 && _rule$excludeDates.includes(date)) return false;
878
+ if (rule.startDate && date < rule.startDate) return false;
879
+ if (rule.endDate && date > rule.endDate) return false;
880
+ if (rule.mode === 'designation') {
881
+ var _rule$dates;
882
+ if ((_rule$dates = rule.dates) !== null && _rule$dates !== void 0 && _rule$dates.includes(date)) return true;
883
+ return false;
884
+ }
885
+ if (rule.mode === 'weekly') {
886
+ var _rule$weekdays;
887
+ if (!((_rule$weekdays = rule.weekdays) !== null && _rule$weekdays !== void 0 && _rule$weekdays.length)) return true;
888
+ var freq = Math.max(1, Number(rule.frequency || 1) || 1);
889
+ if (!rule.weekdays.includes(weekdayFromLocalDate(date))) return false;
890
+ if (!rule.startDate) return true;
891
+ var targetWeekStart = dateOrdinalFromLocalDate(date) - weekdayFromLocalDate(date);
892
+ var ruleWeekStart = dateOrdinalFromLocalDate(rule.startDate) - weekdayFromLocalDate(rule.startDate);
893
+ return Math.floor((targetWeekStart - ruleWeekStart) / 7) % freq === 0;
894
+ }
895
+ if (rule.mode === 'daily' && rule.startDate) {
896
+ var _freq = Math.max(1, Number(rule.frequency || 1) || 1);
897
+ return (dateOrdinalFromLocalDate(date) - dateOrdinalFromLocalDate(rule.startDate)) % _freq === 0;
898
+ }
899
+ return true;
900
+ }
901
+ function mergeScheduleRefs(base, next) {
902
+ var map = new Map();
903
+ [].concat(_toConsumableArray(base), _toConsumableArray(next)).forEach(function (ref) {
904
+ var _ref$scheduleId, _ref$timeSlotId, _ref$ruleId, _ref$source, _ref$label;
905
+ var key = [(_ref$scheduleId = ref.scheduleId) !== null && _ref$scheduleId !== void 0 ? _ref$scheduleId : '', (_ref$timeSlotId = ref.timeSlotId) !== null && _ref$timeSlotId !== void 0 ? _ref$timeSlotId : '', (_ref$ruleId = ref.ruleId) !== null && _ref$ruleId !== void 0 ? _ref$ruleId : '', (_ref$source = ref.source) !== null && _ref$source !== void 0 ? _ref$source : '', (_ref$label = ref.label) !== null && _ref$label !== void 0 ? _ref$label : ''].join(':');
906
+ map.set(key, ref);
907
+ });
908
+ return Array.from(map.values());
909
+ }
910
+ function buildProductIndex(projection) {
911
+ return new Map(projection.products.map(function (product) {
912
+ return [String(product.id), product];
913
+ }));
914
+ }
915
+ function buildGroupResourceIndex(params) {
916
+ var _params$resourceIds;
917
+ var resourceIdSet = (_params$resourceIds = params.resourceIds) !== null && _params$resourceIds !== void 0 && _params$resourceIds.length ? new Set(params.resourceIds.map(String)) : null;
918
+ var index = new Map();
919
+ params.projection.products.forEach(function (product) {
920
+ (product.requirementGroups || []).forEach(function (requirementGroup) {
921
+ var key = [String(product.id), requirementGroup.id].join(':');
922
+ index.set(key, params.projection.resources.filter(function (resource) {
923
+ return matchesRequirementGroup(requirementGroup, resource, product, params.partySize);
924
+ }).filter(function (resource) {
925
+ return !resourceIdSet || resourceIdSet.has(String(resource.id));
926
+ }));
927
+ });
928
+ });
929
+ return index;
930
+ }
931
+ function getSessionRangesForProduct(projection, product, dateRange) {
932
+ var _extractFixedOffsetMi2;
933
+ var fixedOffsetMinutes = (_extractFixedOffsetMi2 = extractFixedOffsetMinutes(projection.meta.now)) !== null && _extractFixedOffsetMi2 !== void 0 ? _extractFixedOffsetMi2 : captureRuntimeOffsetMinutes();
934
+ var ranges = (product.sessionRanges || []).map(function (range) {
935
+ var _range$scheduleRefs2;
936
+ var normalized = normalizeAbsoluteRange(range, projection.meta.timezone);
937
+ if (!normalized) return null;
938
+ var startDate = formatDate(normalized.start, projection.meta.timezone);
939
+ if (startDate < dateRange.startDate || startDate > dateRange.endDate) {
940
+ return null;
941
+ }
942
+ return {
943
+ key: "".concat(String(product.id), ":").concat(normalized.start, ":").concat(normalized.end),
944
+ productId: product.id,
945
+ source: 'session_schedule',
946
+ startAt: formatDateTime(normalized.start, projection.meta.timezone, fixedOffsetMinutes),
947
+ endAt: formatDateTime(normalized.end, projection.meta.timezone, fixedOffsetMinutes),
948
+ bookingStartAt: formatDateTime(normalized.start, projection.meta.timezone, fixedOffsetMinutes),
949
+ bookingEndAt: formatDateTime(normalized.end, projection.meta.timezone, fixedOffsetMinutes),
950
+ date: formatDate(normalized.start, projection.meta.timezone),
951
+ startTime: formatTime(normalized.start, projection.meta.timezone),
952
+ endTime: formatTime(normalized.end, projection.meta.timezone),
953
+ timezone: projection.meta.timezone,
954
+ scheduleRefs: range.scheduleRefs || [],
955
+ primaryScheduleRef: (_range$scheduleRefs2 = range.scheduleRefs) === null || _range$scheduleRefs2 === void 0 ? void 0 : _range$scheduleRefs2[0]
956
+ };
957
+ }).filter(Boolean);
958
+ var ruleRanges = materializeScheduleRules(product.sessionRules || [], dateRange, projection.meta.timezone, fixedOffsetMinutes);
959
+ return dedupeProductRanges([].concat(_toConsumableArray(ranges), _toConsumableArray(ruleRanges.map(function (range) {
960
+ return _objectSpread(_objectSpread({}, range), {}, {
961
+ key: "".concat(String(product.id), ":").concat(range.startAt, ":").concat(range.endAt),
962
+ productId: product.id,
963
+ timezone: projection.meta.timezone
964
+ });
965
+ }))));
966
+ }
967
+ function dedupeProductRanges(ranges) {
968
+ var map = new Map();
969
+ ranges.forEach(function (range) {
970
+ var key = getProductTimeRangeIdentity(range);
971
+ if (!map.has(key)) {
972
+ map.set(key, _objectSpread(_objectSpread({}, range), {}, {
973
+ key: key
974
+ }));
975
+ return;
976
+ }
977
+ var existing = map.get(key);
978
+ existing.scheduleRefs = mergeScheduleRefs(existing.scheduleRefs, range.scheduleRefs || []);
979
+ if (!existing.primaryScheduleRef) existing.primaryScheduleRef = range.primaryScheduleRef;
980
+ });
981
+ return Array.from(map.values()).sort(function (left, right) {
982
+ return left.startAt.localeCompare(right.startAt);
983
+ });
984
+ }
985
+ function createDurationTimeRange(product, start, durationMinutes, projection, fixedOffsetMinutes) {
986
+ var bookingEnd = addMinutesZoned(start, durationMinutes, projection.meta.timezone);
987
+ return createDurationTimeRangeWithEnd(product, start, bookingEnd, projection, fixedOffsetMinutes);
988
+ }
989
+ function createDurationTimeRangeWithEnd(product, start, bookingEnd, projection, fixedOffsetMinutes) {
990
+ return {
991
+ key: "".concat(String(product.id), ":").concat(start, ":").concat(bookingEnd),
992
+ productId: product.id,
993
+ source: 'duration_window',
994
+ startAt: formatDateTime(start, projection.meta.timezone, fixedOffsetMinutes),
995
+ endAt: formatDateTime(bookingEnd, projection.meta.timezone, fixedOffsetMinutes),
996
+ bookingStartAt: formatDateTime(start, projection.meta.timezone, fixedOffsetMinutes),
997
+ bookingEndAt: formatDateTime(bookingEnd, projection.meta.timezone, fixedOffsetMinutes),
998
+ date: formatDate(start, projection.meta.timezone),
999
+ startTime: formatTime(start, projection.meta.timezone),
1000
+ endTime: formatTime(bookingEnd, projection.meta.timezone),
1001
+ timezone: projection.meta.timezone,
1002
+ scheduleRefs: []
1003
+ };
1004
+ }
1005
+ export function getProductTimeRanges(projection) {
1006
+ var _extractFixedOffsetMi3, _request$productIds;
1007
+ var request = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1008
+ var runtimeOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
1009
+ var normalizedPartySize = normalizePartySize(request.partySize);
1010
+ var dateRange = resolveProjectionDateRange(projection, request.dateRange);
1011
+ var slotStepMinutes = projection.meta.defaults.slotStepMinutes;
1012
+ var fixedOffsetMinutes = (_extractFixedOffsetMi3 = extractFixedOffsetMinutes(projection.meta.now)) !== null && _extractFixedOffsetMi3 !== void 0 ? _extractFixedOffsetMi3 : captureRuntimeOffsetMinutes();
1013
+ var resourceWindowIndex = buildResourceWindowIndex(projection);
1014
+ var filteredProductIdSet = (_request$productIds = request.productIds) !== null && _request$productIds !== void 0 && _request$productIds.length ? new Set(request.productIds.map(String)) : null;
1015
+ return projection.products.filter(function (product) {
1016
+ return !filteredProductIdSet || filteredProductIdSet.has(String(product.id));
1017
+ }).map(function (product) {
1018
+ if (product.kind === 'normal') {
1019
+ return {
1020
+ productId: product.id,
1021
+ title: product.title,
1022
+ kind: product.kind,
1023
+ slotStepMinutes: slotStepMinutes,
1024
+ ranges: []
1025
+ };
1026
+ }
1027
+ if (product.kind === 'session' || product.kind === 'venue_slot') {
1028
+ return {
1029
+ productId: product.id,
1030
+ title: product.title,
1031
+ kind: product.kind,
1032
+ slotStepMinutes: slotStepMinutes,
1033
+ ranges: getSessionRangesForProduct(projection, product, dateRange)
1034
+ };
1035
+ }
1036
+ var requiredGroups = (product.requirementGroups || []).filter(function (group) {
1037
+ return group.required !== false;
1038
+ });
1039
+ var operatingRanges = buildOperatingRanges(projection, dateRange);
1040
+ var resourceRanges = requiredGroups.length ? requiredGroups.map(function (group) {
1041
+ return buildGroupWorkingRanges({
1042
+ projection: projection,
1043
+ product: product,
1044
+ requirementGroup: group,
1045
+ dateRange: dateRange,
1046
+ resourceIds: request.resourceIds,
1047
+ partySize: normalizedPartySize,
1048
+ resourceWindowIndex: resourceWindowIndex
1049
+ });
1050
+ }).reduce(function (current, ranges) {
1051
+ return current === null ? ranges : intersectRangeLists(current, ranges);
1052
+ }, null) || [] : null;
1053
+ var baseRanges = resourceRanges === null ? operatingRanges : intersectRangeLists(resourceRanges, operatingRanges);
1054
+ var durationMinutes = resolveRequestedDurationMinutes(product, request);
1055
+ var flexibleStartAt = function () {
1056
+ if (!product.isFlexibleDuration) return undefined;
1057
+ var evaluatedAt = toDateTime(runtimeOptions.evaluatedAt || projection.meta.durationCandidateAnchorAt, projection.meta.timezone);
1058
+ if (!Number.isFinite(evaluatedAt)) {
1059
+ throw availabilityError('INVALID_TARGET_FILTER', 'runtimeOptions.evaluatedAt 必须是有效时间', {
1060
+ evaluatedAt: runtimeOptions.evaluatedAt
1061
+ });
1062
+ }
1063
+ return floorToMinute(evaluatedAt);
1064
+ }();
1065
+ var durationAnchor = ceilToTenMinutes(toDateTime(projection.meta.durationCandidateAnchorAt, projection.meta.timezone), projection.meta.timezone);
1066
+ var requestedStartTimes = resolveRequestedDurationStartTimes(product, request);
1067
+ var ranges = [];
1068
+ baseRanges.forEach(function (range) {
1069
+ if (product.isFlexibleDuration && flexibleStartAt !== undefined) {
1070
+ var start = Math.max(range.start, flexibleStartAt);
1071
+ if (start >= range.end) return;
1072
+ var bookingDate = formatDate(start);
1073
+ if (bookingDate >= dateRange.startDate && bookingDate <= dateRange.endDate) {
1074
+ ranges.push(createDurationTimeRangeWithEnd(product, start, range.end, projection, fixedOffsetMinutes));
938
1075
  }
939
- assignments.push({
940
- productId: interval.productId,
941
- resourceId: availableCell.resourceId,
942
- start_at: interval.start_at,
943
- end_at: interval.end_at,
944
- capacityRequired: interval.capacityRequired
1076
+ return;
1077
+ }
1078
+ if (range.end <= durationAnchor) return;
1079
+ var cursor = range.start;
1080
+ if (cursor < durationAnchor) {
1081
+ cursor = durationAnchor;
1082
+ }
1083
+ while (true) {
1084
+ var bookingEnd = addMinutesZoned(cursor, durationMinutes, projection.meta.timezone);
1085
+ if (bookingEnd > range.end) break;
1086
+ var _bookingDate = formatDate(cursor);
1087
+ if (_bookingDate >= dateRange.startDate && _bookingDate <= dateRange.endDate) {
1088
+ ranges.push(createDurationTimeRange(product, cursor, durationMinutes, projection, fixedOffsetMinutes));
1089
+ }
1090
+ cursor = addMinutesZoned(cursor, slotStepMinutes, projection.meta.timezone);
1091
+ }
1092
+ });
1093
+ if (dateRange.startDate === dateRange.endDate && requestedStartTimes.length > 0) {
1094
+ requestedStartTimes.forEach(function (startTime) {
1095
+ var start = toDateTimeInDate(dateRange.startDate, startTime, projection.meta.timezone);
1096
+ if (!Number.isFinite(start)) return;
1097
+ var end = addMinutesZoned(start, durationMinutes, projection.meta.timezone);
1098
+ var startsInOperatingRange = operatingRanges.some(function (range) {
1099
+ return range.start <= start && start < range.end;
945
1100
  });
946
- },
947
- _ret2;
948
- for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
949
- _ret2 = _loop3();
950
- if (_ret2 === 0) continue;
1101
+ if (!startsInOperatingRange && !request.allowOutsideOperatingHoursForRequestedStartTimes) {
1102
+ return;
1103
+ }
1104
+ ranges.push(createDurationTimeRange(product, start, durationMinutes, projection, fixedOffsetMinutes));
1105
+ });
1106
+ }
1107
+ return {
1108
+ productId: product.id,
1109
+ title: product.title,
1110
+ kind: product.kind,
1111
+ slotStepMinutes: slotStepMinutes,
1112
+ ranges: dedupeProductRanges(ranges)
1113
+ };
1114
+ });
1115
+ }
1116
+ function aggregateMatchedCells(productId, requirementGroupId, resourceId, candidateRange, indexedCells) {
1117
+ var _extractFixedOffsetMi4, _extractFixedOffsetMi5;
1118
+ var candidateStartMs = candidateRange.start;
1119
+ var candidateEndMs = candidateRange.end;
1120
+ var left = 0;
1121
+ var right = indexedCells.length;
1122
+ while (left < right) {
1123
+ var middle = Math.floor((left + right) / 2);
1124
+ if (indexedCells[middle].range.endMs <= candidateStartMs) {
1125
+ left = middle + 1;
1126
+ } else {
1127
+ right = middle;
1128
+ }
1129
+ }
1130
+ var overlapped = [];
1131
+ for (var index = Math.max(0, left - 1); index < indexedCells.length; index += 1) {
1132
+ var _indexedCells$index = indexedCells[index],
1133
+ cell = _indexedCells$index.cell,
1134
+ range = _indexedCells$index.range;
1135
+ if (range.startMs >= candidateEndMs) break;
1136
+ if (range.endMs <= candidateStartMs) continue;
1137
+ var clipped = clipRange(range, candidateRange);
1138
+ if (clipped) overlapped.push({
1139
+ cell: cell,
1140
+ range: clipped
1141
+ });
1142
+ }
1143
+ if (!overlapped.length) return undefined;
1144
+ var cursor = candidateRange.start;
1145
+ for (var _i = 0, _overlapped = overlapped; _i < _overlapped.length; _i++) {
1146
+ var item = _overlapped[_i];
1147
+ if (item.range.start > cursor) return undefined;
1148
+ if (item.range.end > cursor) {
1149
+ cursor = item.range.end;
1150
+ }
1151
+ if (cursor >= candidateRange.end) break;
1152
+ }
1153
+ if (cursor < candidateRange.end) return undefined;
1154
+ var cells = overlapped.map(function (item) {
1155
+ return item.cell;
1156
+ });
1157
+ var statuses = cells.map(function (cell) {
1158
+ return cell.status;
1159
+ });
1160
+ var nonPastStatuses = statuses.filter(function (status) {
1161
+ return status !== 'past';
1162
+ });
1163
+ var aggregatedStatus = function () {
1164
+ if (nonPastStatuses.some(function (status) {
1165
+ return status === 'unavailable';
1166
+ })) return 'unavailable';
1167
+ if (nonPastStatuses.some(function (status) {
1168
+ return status === 'full';
1169
+ })) return 'full';
1170
+ if (nonPastStatuses.some(function (status) {
1171
+ return status === 'limited';
1172
+ })) return 'limited';
1173
+ if (nonPastStatuses.every(function (status) {
1174
+ return status === 'available';
1175
+ })) return 'available';
1176
+ if (statuses.every(function (status) {
1177
+ return status === 'past';
1178
+ })) return 'past';
1179
+ return 'available';
1180
+ }();
1181
+ var capacityCells = cells.filter(function (cell) {
1182
+ return cell.remainingCapacity !== undefined || cell.maxCapacity !== undefined;
1183
+ });
1184
+ var reasonCodes = Array.from(new Set(cells.flatMap(function (cell) {
1185
+ return cell.reasonCodes;
1186
+ })));
1187
+ var occupancyRefs = Array.from(new Map(cells.flatMap(function (cell) {
1188
+ return cell.occupancyRefs || [];
1189
+ }).map(function (ref) {
1190
+ var _ref$id, _ref$resourceId, _ref$serverEventId, _ref$bookingUid, _ref$productUid, _ref$scheduleEventId, _ref$source2;
1191
+ return [[(_ref$id = ref.id) !== null && _ref$id !== void 0 ? _ref$id : '', (_ref$resourceId = ref.resourceId) !== null && _ref$resourceId !== void 0 ? _ref$resourceId : '', (_ref$serverEventId = ref.serverEventId) !== null && _ref$serverEventId !== void 0 ? _ref$serverEventId : '', (_ref$bookingUid = ref.bookingUid) !== null && _ref$bookingUid !== void 0 ? _ref$bookingUid : '', (_ref$productUid = ref.productUid) !== null && _ref$productUid !== void 0 ? _ref$productUid : '', (_ref$scheduleEventId = ref.scheduleEventId) !== null && _ref$scheduleEventId !== void 0 ? _ref$scheduleEventId : '', (_ref$source2 = ref.source) !== null && _ref$source2 !== void 0 ? _ref$source2 : ''].join(':'), ref];
1192
+ })).values());
1193
+ var firstCell = cells[0];
1194
+ return _objectSpread(_objectSpread({}, firstCell), {}, {
1195
+ key: buildCellKey({
1196
+ productId: productId,
1197
+ groupId: requirementGroupId,
1198
+ resourceId: resourceId,
1199
+ start: candidateRange.start,
1200
+ end: candidateRange.end
1201
+ }),
1202
+ startAt: formatDateTime(candidateRange.start, firstCell.timezone, (_extractFixedOffsetMi4 = extractFixedOffsetMinutes(firstCell.startAt)) !== null && _extractFixedOffsetMi4 !== void 0 ? _extractFixedOffsetMi4 : captureRuntimeOffsetMinutes()),
1203
+ endAt: formatDateTime(candidateRange.end, firstCell.timezone, (_extractFixedOffsetMi5 = extractFixedOffsetMinutes(firstCell.startAt)) !== null && _extractFixedOffsetMi5 !== void 0 ? _extractFixedOffsetMi5 : captureRuntimeOffsetMinutes()),
1204
+ date: formatDate(candidateRange.start, firstCell.timezone),
1205
+ startTime: formatTime(candidateRange.start, firstCell.timezone),
1206
+ endTime: formatTime(candidateRange.end, firstCell.timezone),
1207
+ status: aggregatedStatus,
1208
+ remainingCapacity: capacityCells.length ? Math.min.apply(Math, _toConsumableArray(capacityCells.map(function (cell) {
1209
+ var _cell$remainingCapaci;
1210
+ return (_cell$remainingCapaci = cell.remainingCapacity) !== null && _cell$remainingCapaci !== void 0 ? _cell$remainingCapaci : Number.MAX_SAFE_INTEGER;
1211
+ }))) : undefined,
1212
+ maxCapacity: capacityCells.length ? Math.min.apply(Math, _toConsumableArray(capacityCells.map(function (cell) {
1213
+ var _cell$maxCapacity;
1214
+ return (_cell$maxCapacity = cell.maxCapacity) !== null && _cell$maxCapacity !== void 0 ? _cell$maxCapacity : Number.MAX_SAFE_INTEGER;
1215
+ }))) : undefined,
1216
+ reasonCodes: reasonCodes,
1217
+ occupancyRefs: occupancyRefs
1218
+ });
1219
+ }
1220
+ function aggregateMatchedCellSummary(candidateRange, indexedCells) {
1221
+ var candidateStartMs = candidateRange.startMs;
1222
+ var candidateEndMs = candidateRange.endMs;
1223
+ var left = 0;
1224
+ var right = indexedCells.length;
1225
+ while (left < right) {
1226
+ var middle = Math.floor((left + right) / 2);
1227
+ if (indexedCells[middle].range.endMs <= candidateStartMs) {
1228
+ left = middle + 1;
1229
+ } else {
1230
+ right = middle;
951
1231
  }
952
- } catch (err) {
953
- _iterator5.e(err);
954
- } finally {
955
- _iterator5.f();
956
1232
  }
1233
+ var cells = [];
1234
+ var cursorMs = candidateStartMs;
1235
+ for (var index = Math.max(0, left - 1); index < indexedCells.length; index += 1) {
1236
+ var _indexedCells$index2 = indexedCells[index],
1237
+ cell = _indexedCells$index2.cell,
1238
+ range = _indexedCells$index2.range;
1239
+ if (range.startMs >= candidateEndMs) break;
1240
+ if (range.endMs <= candidateStartMs) continue;
1241
+ var clippedStartMs = Math.max(range.startMs, candidateStartMs);
1242
+ var clippedEndMs = Math.min(range.endMs, candidateEndMs);
1243
+ if (clippedEndMs <= clippedStartMs) continue;
1244
+ if (clippedStartMs > cursorMs) return undefined;
1245
+ cells.push(cell);
1246
+ if (clippedEndMs > cursorMs) cursorMs = clippedEndMs;
1247
+ if (cursorMs >= candidateEndMs) break;
1248
+ }
1249
+ if (!cells.length || cursorMs < candidateEndMs) return undefined;
1250
+ var statuses = cells.map(function (cell) {
1251
+ return cell.status;
1252
+ });
1253
+ var nonPastStatuses = statuses.filter(function (status) {
1254
+ return status !== 'past';
1255
+ });
1256
+ var status = function () {
1257
+ if (nonPastStatuses.some(function (cellStatus) {
1258
+ return cellStatus === 'unavailable';
1259
+ })) return 'unavailable';
1260
+ if (nonPastStatuses.some(function (cellStatus) {
1261
+ return cellStatus === 'full';
1262
+ })) return 'full';
1263
+ if (nonPastStatuses.some(function (cellStatus) {
1264
+ return cellStatus === 'limited';
1265
+ })) return 'limited';
1266
+ if (nonPastStatuses.every(function (cellStatus) {
1267
+ return cellStatus === 'available';
1268
+ })) return 'available';
1269
+ if (statuses.every(function (cellStatus) {
1270
+ return cellStatus === 'past';
1271
+ })) return 'past';
1272
+ return 'available';
1273
+ }();
1274
+ var capacityCells = cells.filter(function (cell) {
1275
+ return cell.remainingCapacity !== undefined || cell.maxCapacity !== undefined;
1276
+ });
1277
+ return {
1278
+ status: status,
1279
+ remainingCapacity: capacityCells.length ? Math.min.apply(Math, _toConsumableArray(capacityCells.map(function (cell) {
1280
+ var _cell$remainingCapaci2;
1281
+ return (_cell$remainingCapaci2 = cell.remainingCapacity) !== null && _cell$remainingCapaci2 !== void 0 ? _cell$remainingCapaci2 : Number.MAX_SAFE_INTEGER;
1282
+ }))) : undefined,
1283
+ maxCapacity: capacityCells.length ? Math.min.apply(Math, _toConsumableArray(capacityCells.map(function (cell) {
1284
+ var _cell$maxCapacity2;
1285
+ return (_cell$maxCapacity2 = cell.maxCapacity) !== null && _cell$maxCapacity2 !== void 0 ? _cell$maxCapacity2 : Number.MAX_SAFE_INTEGER;
1286
+ }))) : undefined,
1287
+ reasonCodes: Array.from(new Set(cells.flatMap(function (cell) {
1288
+ return cell.reasonCodes;
1289
+ })))
1290
+ };
1291
+ }
1292
+ function buildAvailabilityCellIndex(projection) {
1293
+ var index = new Map();
1294
+ projection.cells.forEach(function (cell) {
1295
+ if (cell.requirementGroupId == null || cell.resourceId == null) return;
1296
+ var key = [String(cell.productId), cell.requirementGroupId, String(cell.resourceId)].join(':');
1297
+ var entries = index.get(key) || [];
1298
+ entries.push({
1299
+ cell: cell,
1300
+ range: toIndexedRange({
1301
+ start: toDateTime(cell.startAt, projection.meta.timezone),
1302
+ end: toDateTime(cell.endAt, projection.meta.timezone)
1303
+ })
1304
+ });
1305
+ index.set(key, entries);
1306
+ });
1307
+ index.forEach(function (entries, key) {
1308
+ index.set(key, entries.sort(function (left, right) {
1309
+ return left.range.startMs - right.range.startMs;
1310
+ }));
1311
+ });
1312
+ return index;
1313
+ }
1314
+ function buildEvaluationContext(params) {
1315
+ var _params$resourceIds2;
1316
+ var evaluatedAt = toDateTime(params.evaluatedAt || params.projection.meta.now, params.projection.meta.timezone);
1317
+ var selectionsByGroupKey = new Map();
1318
+ (params.requirementSelections || []).forEach(function (selection) {
1319
+ var _selection$formId;
1320
+ var key = selection.requirementGroupId || String((_selection$formId = selection.formId) !== null && _selection$formId !== void 0 ? _selection$formId : '');
1321
+ selectionsByGroupKey.set(key, selection);
1322
+ });
1323
+ return {
1324
+ evaluatedAt: evaluatedAt,
1325
+ evaluatedAtMs: evaluatedAt,
1326
+ resourceIdSet: (_params$resourceIds2 = params.resourceIds) !== null && _params$resourceIds2 !== void 0 && _params$resourceIds2.length ? new Set(params.resourceIds.map(String)) : null,
1327
+ selectionsByGroupKey: selectionsByGroupKey
1328
+ };
1329
+ }
1330
+ function summarizeStatuses(statuses) {
1331
+ if (!statuses.length) return 'unavailable';
1332
+ if (statuses.every(function (status) {
1333
+ return status === 'past';
1334
+ })) return 'past';
1335
+ if (statuses.some(function (status) {
1336
+ return status === 'unavailable';
1337
+ })) return 'unavailable';
1338
+ if (statuses.some(function (status) {
1339
+ return status === 'full';
1340
+ })) return 'full';
1341
+ if (statuses.some(function (status) {
1342
+ return status === 'limited';
1343
+ })) return 'limited';
1344
+ if (statuses.every(function (status) {
1345
+ return status === 'available';
1346
+ })) return 'available';
1347
+ if (statuses.some(function (status) {
1348
+ return status === 'past';
1349
+ })) return 'past';
1350
+ return statuses[0];
1351
+ }
1352
+ function createStatusAggregate() {
1353
+ return {
1354
+ count: 0,
1355
+ hasAvailable: false,
1356
+ hasLimited: false,
1357
+ hasFull: false,
1358
+ allPast: true
1359
+ };
1360
+ }
1361
+ function pushStatusAggregate(aggregate, status) {
1362
+ aggregate.count += 1;
1363
+ if (status !== 'past') aggregate.allPast = false;
1364
+ if (status === 'available') aggregate.hasAvailable = true;
1365
+ if (status === 'limited') aggregate.hasLimited = true;
1366
+ if (status === 'full') aggregate.hasFull = true;
1367
+ }
1368
+ function summarizeStatusAggregate(aggregate) {
1369
+ if (aggregate.count === 0) return 'unavailable';
1370
+ if (aggregate.hasAvailable) return 'available';
1371
+ if (aggregate.hasLimited) return 'limited';
1372
+ if (aggregate.hasFull) return 'full';
1373
+ if (aggregate.allPast) return 'past';
1374
+ return 'unavailable';
1375
+ }
1376
+ function evaluateMatchedResourceSummary(params) {
1377
+ var _ref6, _params$matchedCell$m;
1378
+ var isPast = params.candidateRange.endMs <= params.evaluationContext.evaluatedAtMs;
1379
+ if (!params.matchedCell) {
1380
+ return {
1381
+ status: isPast ? 'past' : 'unavailable',
1382
+ maxPartySize: 0,
1383
+ remainingCapacity: 0,
1384
+ maxCapacity: 0,
1385
+ reasonCodes: isPast ? ['past'] : ['outside_resource_window']
1386
+ };
1387
+ }
1388
+ var remainingCapacity = Math.max(0, params.matchedCell.remainingCapacity || 0);
1389
+ var maxCapacity = Math.max(0, (_ref6 = (_params$matchedCell$m = params.matchedCell.maxCapacity) !== null && _params$matchedCell$m !== void 0 ? _params$matchedCell$m : params.resourceCapacity) !== null && _ref6 !== void 0 ? _ref6 : 0);
1390
+ var maxPartySize = isPast ? 0 : remainingCapacity;
1391
+ var status = function () {
1392
+ if (isPast) return 'past';
1393
+ if (params.matchedCell.status === 'unavailable') return 'unavailable';
1394
+ if (remainingCapacity < params.requiredCapacity) return 'full';
1395
+ if (remainingCapacity < maxCapacity) return 'limited';
1396
+ return 'available';
1397
+ }();
1398
+ return {
1399
+ status: status,
1400
+ remainingCapacity: remainingCapacity,
1401
+ maxCapacity: maxCapacity,
1402
+ maxPartySize: maxPartySize,
1403
+ reasonCodes: isPast ? Array.from(new Set([].concat(_toConsumableArray(params.matchedCell.reasonCodes || []), ['past']))) : params.matchedCell.reasonCodes
1404
+ };
1405
+ }
1406
+ function evaluateRequirementGroupSummary(params) {
1407
+ var _requirementGroup$min2, _requirementGroup$max, _params$groupResource;
1408
+ var projection = params.projection,
1409
+ product = params.product,
1410
+ requirementGroup = params.requirementGroup,
1411
+ candidateRange = params.candidateRange,
1412
+ requestedPartySize = params.requestedPartySize,
1413
+ evaluationContext = params.evaluationContext;
1414
+ var required = requirementGroup.required !== false;
1415
+ var minimum = Math.max(required ? 1 : 0, Number((_requirementGroup$min2 = requirementGroup.min) !== null && _requirementGroup$min2 !== void 0 ? _requirementGroup$min2 : required ? 1 : 0) || 0);
1416
+ var maximum = Math.max(minimum, Number((_requirementGroup$max = requirementGroup.max) !== null && _requirementGroup$max !== void 0 ? _requirementGroup$max : Math.max(minimum, 1)) || Math.max(minimum, 1));
1417
+ var matchedResources = ((_params$groupResource = params.groupResourceIndex) === null || _params$groupResource === void 0 ? void 0 : _params$groupResource.get([String(product.id), requirementGroup.id].join(':'))) || projection.resources.filter(function (resource) {
1418
+ return matchesRequirementGroup(requirementGroup, resource, product, requestedPartySize !== null && requestedPartySize !== void 0 ? requestedPartySize : 1);
1419
+ }).filter(function (resource) {
1420
+ return !evaluationContext.resourceIdSet || evaluationContext.resourceIdSet.has(String(resource.id));
1421
+ });
1422
+ var requiredCapacity = resolveRequiredCapacity(requirementGroup, product, requestedPartySize);
1423
+ var optionStatusAggregate = createStatusAggregate();
1424
+ var reasonCodeSet = new Set();
1425
+ var totalResourceCount = 0;
1426
+ var availableResourceCount = 0;
1427
+ var groupMaxPartySize = 0;
1428
+ var groupRemainingCapacity = 0;
1429
+ var groupMaxCapacity = 0;
1430
+ matchedResources.forEach(function (resource) {
1431
+ var _params$cellIndex;
1432
+ totalResourceCount += 1;
1433
+ var matchedCell = aggregateMatchedCellSummary(candidateRange, ((_params$cellIndex = params.cellIndex) === null || _params$cellIndex === void 0 ? void 0 : _params$cellIndex.get([String(product.id), requirementGroup.id, String(resource.id)].join(':'))) || []);
1434
+ var option = evaluateMatchedResourceSummary({
1435
+ matchedCell: matchedCell,
1436
+ resourceCapacity: resource.capacity,
1437
+ requiredCapacity: requiredCapacity,
1438
+ candidateRange: candidateRange,
1439
+ evaluationContext: evaluationContext
1440
+ });
1441
+ pushStatusAggregate(optionStatusAggregate, option.status);
1442
+ option.reasonCodes.forEach(function (reasonCode) {
1443
+ return reasonCodeSet.add(reasonCode);
1444
+ });
1445
+ groupRemainingCapacity = Math.max(groupRemainingCapacity, option.remainingCapacity);
1446
+ groupMaxCapacity = Math.max(groupMaxCapacity, option.maxCapacity);
1447
+ if (option.status === 'available' || option.status === 'limited') {
1448
+ availableResourceCount += 1;
1449
+ groupMaxPartySize = Math.max(groupMaxPartySize, option.maxPartySize);
1450
+ }
1451
+ });
1452
+ var status = 'available';
1453
+ if (candidateRange.endMs <= evaluationContext.evaluatedAtMs) {
1454
+ status = 'past';
1455
+ } else if (availableResourceCount < minimum) {
1456
+ status = optionStatusAggregate.hasAvailable || optionStatusAggregate.hasLimited || optionStatusAggregate.hasFull ? 'full' : 'unavailable';
1457
+ } else if (availableResourceCount === totalResourceCount && !optionStatusAggregate.hasLimited && !optionStatusAggregate.hasFull && !optionStatusAggregate.allPast) {
1458
+ status = 'available';
1459
+ } else {
1460
+ status = 'limited';
1461
+ }
1462
+ return {
1463
+ required: required,
1464
+ min: minimum,
1465
+ max: maximum,
1466
+ status: status,
1467
+ maxPartySize: groupMaxPartySize,
1468
+ remainingCapacity: groupRemainingCapacity,
1469
+ maxCapacity: groupMaxCapacity,
1470
+ reasonCodes: Array.from(reasonCodeSet)
1471
+ };
1472
+ }
1473
+ function evaluateCandidateSummary(projection, candidate, request) {
1474
+ var _request$productIndex, _request$candidateRan;
1475
+ var product = ((_request$productIndex = request.productIndex) === null || _request$productIndex === void 0 ? void 0 : _request$productIndex.get(String(candidate.productId))) || projection.products.find(function (item) {
1476
+ return sameId(item.id, candidate.productId);
1477
+ });
1478
+ if (!product) {
1479
+ throw availabilityError('INVALID_TARGET_FILTER', 'candidate.productId 不存在于 projection.products', {
1480
+ candidate: candidate
1481
+ });
1482
+ }
1483
+ var candidateRangeCacheKey = getCandidateRangeCacheKey(candidate);
1484
+ var candidateRange = ((_request$candidateRan = request.candidateRangeCache) === null || _request$candidateRan === void 0 ? void 0 : _request$candidateRan.get(candidateRangeCacheKey)) || function () {
1485
+ var normalized = normalizeAbsoluteRange(getCandidateBookingRange(candidate), projection.meta.timezone);
1486
+ return normalized ? toIndexedRange(normalized) : null;
1487
+ }();
1488
+ if (!candidateRange) {
1489
+ throw availabilityError('INVALID_TARGET_FILTER', 'candidate 时间范围非法', {
1490
+ candidate: candidate
1491
+ });
1492
+ }
1493
+ if (request.candidateRangeCache && !request.candidateRangeCache.has(candidateRangeCacheKey)) {
1494
+ request.candidateRangeCache.set(candidateRangeCacheKey, candidateRange);
1495
+ }
1496
+ var evaluationContext = request.evaluationContext || buildEvaluationContext({
1497
+ projection: projection,
1498
+ evaluatedAt: request.evaluatedAt,
1499
+ resourceIds: request.resourceIds
1500
+ });
1501
+ var requestedPartySize = normalizePartySize(request.partySize);
1502
+ var requiredGroups = (product.requirementGroups || []).filter(function (group) {
1503
+ return group.required !== false;
1504
+ }).map(function (requirementGroup) {
1505
+ return evaluateRequirementGroupSummary({
1506
+ projection: projection,
1507
+ product: product,
1508
+ requirementGroup: requirementGroup,
1509
+ candidateRange: candidateRange,
1510
+ requestedPartySize: requestedPartySize,
1511
+ evaluationContext: evaluationContext,
1512
+ cellIndex: request.cellIndex,
1513
+ groupResourceIndex: request.groupResourceIndex
1514
+ });
1515
+ });
1516
+ var gate = evaluateCandidateTimeGate({
1517
+ product: product,
1518
+ candidateRange: candidateRange,
1519
+ evaluationContext: evaluationContext,
1520
+ timezoneName: projection.meta.timezone
1521
+ });
1522
+ var candidateStatus = requiredGroups.length ? summarizeStatuses(requiredGroups.map(function (group) {
1523
+ return group.status;
1524
+ })) : candidateRange.endMs <= evaluationContext.evaluatedAtMs ? 'past' : 'available';
1525
+ var maxPartySize = requiredGroups.length ? requiredGroups.reduce(function (min, group) {
1526
+ return Math.min(min, group.maxPartySize);
1527
+ }, Number.MAX_SAFE_INTEGER) : 0;
1528
+ var remainingCapacity = requiredGroups.length ? requiredGroups.reduce(function (min, group) {
1529
+ return Math.min(min, group.remainingCapacity);
1530
+ }, Number.MAX_SAFE_INTEGER) : 0;
1531
+ var maxCapacity = requiredGroups.length ? requiredGroups.reduce(function (min, group) {
1532
+ return Math.min(min, group.maxCapacity);
1533
+ }, Number.MAX_SAFE_INTEGER) : 0;
1534
+ var status = (gate === null || gate === void 0 ? void 0 : gate.status) || candidateStatus;
957
1535
  return {
958
- assignments: assignments,
959
- conflicts: conflicts
1536
+ status: status,
1537
+ maxPartySize: gate ? 0 : Number.isFinite(maxPartySize) ? maxPartySize : 0,
1538
+ remainingCapacity: status === 'past' || status === 'unavailable' ? 0 : Number.isFinite(remainingCapacity) ? remainingCapacity : 0,
1539
+ maxCapacity: Number.isFinite(maxCapacity) ? maxCapacity : 0,
1540
+ reasonCodes: (gate === null || gate === void 0 ? void 0 : gate.reasonCodes) || Array.from(new Set(requiredGroups.flatMap(function (group) {
1541
+ return group.reasonCodes;
1542
+ })))
960
1543
  };
961
1544
  }
962
- function withAssignmentEvents(context, assignments) {
963
- if (!assignments.length) return context;
964
- return _objectSpread(_objectSpread({}, context), {}, {
965
- externalEvents: [].concat(_toConsumableArray(context.externalEvents), _toConsumableArray(assignments.map(function (assignment) {
1545
+ function evaluateCandidate(projection, candidate, request) {
1546
+ var _request$productIndex2, _request$candidateRan2;
1547
+ var product = ((_request$productIndex2 = request.productIndex) === null || _request$productIndex2 === void 0 ? void 0 : _request$productIndex2.get(String(candidate.productId))) || projection.products.find(function (item) {
1548
+ return sameId(item.id, candidate.productId);
1549
+ });
1550
+ if (!product) {
1551
+ throw availabilityError('INVALID_TARGET_FILTER', 'candidate.productId 不存在于 projection.products', {
1552
+ candidate: candidate
1553
+ });
1554
+ }
1555
+ var candidateRangeCacheKey = getCandidateRangeCacheKey(candidate);
1556
+ var candidateRange = ((_request$candidateRan2 = request.candidateRangeCache) === null || _request$candidateRan2 === void 0 ? void 0 : _request$candidateRan2.get(candidateRangeCacheKey)) || function () {
1557
+ var normalized = normalizeAbsoluteRange(getCandidateBookingRange(candidate), projection.meta.timezone);
1558
+ return normalized ? toIndexedRange(normalized) : null;
1559
+ }();
1560
+ if (!candidateRange) {
1561
+ throw availabilityError('INVALID_TARGET_FILTER', 'candidate 时间范围非法', {
1562
+ candidate: candidate
1563
+ });
1564
+ }
1565
+ if (request.candidateRangeCache && !request.candidateRangeCache.has(candidateRangeCacheKey)) {
1566
+ request.candidateRangeCache.set(candidateRangeCacheKey, candidateRange);
1567
+ }
1568
+ var evaluationContext = request.evaluationContext || buildEvaluationContext({
1569
+ projection: projection,
1570
+ evaluatedAt: request.evaluatedAt,
1571
+ resourceIds: request.resourceIds,
1572
+ requirementSelections: request.requirementSelections
1573
+ });
1574
+ var requestedPartySize = normalizePartySize(request.partySize);
1575
+ var partySize = requestedPartySize !== null && requestedPartySize !== void 0 ? requestedPartySize : 1;
1576
+ var selectionsByGroupKey = evaluationContext.selectionsByGroupKey;
1577
+ var requirementGroups = (product.requirementGroups || []).map(function (requirementGroup) {
1578
+ var _requirementGroup$min3, _requirementGroup$max2, _requirementGroup$for2, _request$groupResourc;
1579
+ var required = requirementGroup.required !== false;
1580
+ var minimum = Math.max(required ? 1 : 0, Number((_requirementGroup$min3 = requirementGroup.min) !== null && _requirementGroup$min3 !== void 0 ? _requirementGroup$min3 : required ? 1 : 0) || 0);
1581
+ var maximum = Math.max(minimum, Number((_requirementGroup$max2 = requirementGroup.max) !== null && _requirementGroup$max2 !== void 0 ? _requirementGroup$max2 : Math.max(minimum, 1)) || Math.max(minimum, 1));
1582
+ var selection = selectionsByGroupKey.get(requirementGroup.id) || selectionsByGroupKey.get(String((_requirementGroup$for2 = requirementGroup.formId) !== null && _requirementGroup$for2 !== void 0 ? _requirementGroup$for2 : ''));
1583
+ var matchedResources = (((_request$groupResourc = request.groupResourceIndex) === null || _request$groupResourc === void 0 ? void 0 : _request$groupResourc.get([String(product.id), requirementGroup.id].join(':'))) || projection.resources.filter(function (resource) {
1584
+ return matchesRequirementGroup(requirementGroup, resource, product, partySize);
1585
+ }).filter(function (resource) {
1586
+ return !evaluationContext.resourceIdSet || evaluationContext.resourceIdSet.has(String(resource.id));
1587
+ })).filter(function (resource) {
1588
+ return !selection || selection.resourceIds.some(function (resourceId) {
1589
+ return sameId(resourceId, resource.id);
1590
+ });
1591
+ });
1592
+ var options = matchedResources.map(function (resource) {
1593
+ var _request$cellIndex, _resource$raw2;
1594
+ var matchedCell = aggregateMatchedCells(product.id, requirementGroup.id, resource.id, candidateRange, ((_request$cellIndex = request.cellIndex) === null || _request$cellIndex === void 0 ? void 0 : _request$cellIndex.get([String(product.id), requirementGroup.id, String(resource.id)].join(':'))) || []);
1595
+ if (!matchedCell) {
1596
+ var _resource$raw;
1597
+ var _isPast = candidateRange.endMs <= evaluationContext.evaluatedAtMs;
1598
+ return {
1599
+ resourceId: resource.id,
1600
+ resourceName: resource.name,
1601
+ resourceFormId: resource.formId,
1602
+ policyId: (_resource$raw = resource.raw) === null || _resource$raw === void 0 ? void 0 : _resource$raw.policy_id,
1603
+ resourceType: resource.resourceType,
1604
+ status: _isPast ? 'past' : 'unavailable',
1605
+ maxPartySize: 0,
1606
+ reasonCodes: _isPast ? ['past'] : ['outside_resource_window']
1607
+ };
1608
+ }
1609
+ var remainingCapacity = matchedCell.remainingCapacity;
1610
+ var maxCapacity = matchedCell.maxCapacity;
1611
+ var isPast = candidateRange.endMs <= evaluationContext.evaluatedAtMs;
1612
+ var maxPartySize = isPast ? 0 : Math.max(0, remainingCapacity || 0);
1613
+ var requiredCapacity = resolveRequiredCapacity(requirementGroup, product, requestedPartySize);
1614
+ var status = function () {
1615
+ if (isPast) return 'past';
1616
+ if (matchedCell.status === 'unavailable') return 'unavailable';
1617
+ if ((remainingCapacity || 0) < requiredCapacity) return 'full';
1618
+ if ((remainingCapacity || 0) < (maxCapacity || resource.capacity)) return 'limited';
1619
+ return 'available';
1620
+ }();
966
1621
  return {
967
- resourceId: assignment.resourceId,
968
- start_at: assignment.start_at,
969
- end_at: assignment.end_at,
970
- pax: assignment.capacityRequired || 1,
971
- source: 'selection'
1622
+ resourceId: resource.id,
1623
+ resourceName: resource.name,
1624
+ resourceFormId: resource.formId,
1625
+ policyId: (_resource$raw2 = resource.raw) === null || _resource$raw2 === void 0 ? void 0 : _resource$raw2.policy_id,
1626
+ resourceType: resource.resourceType,
1627
+ status: status,
1628
+ remainingCapacity: remainingCapacity,
1629
+ maxCapacity: maxCapacity,
1630
+ maxPartySize: maxPartySize,
1631
+ reasonCodes: isPast ? Array.from(new Set([].concat(_toConsumableArray(matchedCell.reasonCodes), ['past']))) : matchedCell.reasonCodes,
1632
+ matchedCell: matchedCell
972
1633
  };
973
- })))
1634
+ });
1635
+ var availableOptions = options.filter(function (option) {
1636
+ return option.status === 'available' || option.status === 'limited';
1637
+ });
1638
+ var totalResourceCount = options.length;
1639
+ var availableResourceCount = availableOptions.length;
1640
+ var groupMaxPartySize = availableOptions.reduce(function (max, option) {
1641
+ return Math.max(max, option.maxPartySize);
1642
+ }, 0);
1643
+ var status = 'available';
1644
+ if (candidateRange.endMs <= evaluationContext.evaluatedAtMs) {
1645
+ status = 'past';
1646
+ } else if (availableResourceCount < minimum) {
1647
+ status = options.some(function (option) {
1648
+ return option.status === 'full' || option.status === 'limited' || option.status === 'available';
1649
+ }) ? 'full' : 'unavailable';
1650
+ } else if (options.every(function (option) {
1651
+ return option.status === 'available';
1652
+ })) {
1653
+ status = 'available';
1654
+ } else {
1655
+ status = 'limited';
1656
+ }
1657
+ return {
1658
+ groupId: requirementGroup.id,
1659
+ formId: requirementGroup.formId,
1660
+ title: requirementGroup.title,
1661
+ code: requirementGroup.code,
1662
+ resourceType: requirementGroup.resourceType,
1663
+ required: required,
1664
+ min: minimum,
1665
+ max: maximum,
1666
+ status: status,
1667
+ availableResourceCount: availableResourceCount,
1668
+ totalResourceCount: totalResourceCount,
1669
+ maxPartySize: groupMaxPartySize,
1670
+ remainingCapacity: Math.max.apply(Math, _toConsumableArray(availableOptions.map(function (option) {
1671
+ return option.remainingCapacity || 0;
1672
+ })).concat([0])),
1673
+ maxCapacity: Math.max.apply(Math, _toConsumableArray(availableOptions.map(function (option) {
1674
+ return option.maxCapacity || 0;
1675
+ })).concat([0])),
1676
+ reasonCodes: Array.from(new Set(options.flatMap(function (option) {
1677
+ return option.reasonCodes;
1678
+ }))),
1679
+ options: options
1680
+ };
1681
+ });
1682
+ var requiredGroups = requirementGroups.filter(function (group) {
1683
+ return group.required;
1684
+ });
1685
+ var requiredStatuses = requiredGroups.map(function (group) {
1686
+ return group.status;
1687
+ });
1688
+ var gate = evaluateCandidateTimeGate({
1689
+ product: product,
1690
+ candidateRange: candidateRange,
1691
+ evaluationContext: evaluationContext,
1692
+ timezoneName: projection.meta.timezone
974
1693
  });
1694
+ var candidateStatus = requiredGroups.length ? summarizeStatuses(requiredStatuses) : candidateRange.endMs <= evaluationContext.evaluatedAtMs ? 'past' : 'available';
1695
+ var maxPartySize = requiredGroups.length ? requiredGroups.reduce(function (min, group) {
1696
+ return Math.min(min, group.maxPartySize);
1697
+ }, Number.MAX_SAFE_INTEGER) : 0;
1698
+ var selectedAssignments = requirementGroups.flatMap(function (group) {
1699
+ var _group$formId;
1700
+ var selection = selectionsByGroupKey.get(group.groupId) || selectionsByGroupKey.get(String((_group$formId = group.formId) !== null && _group$formId !== void 0 ? _group$formId : ''));
1701
+ if (!selection && !(request.allowImplicitSelection && group.required)) return [];
1702
+ var availableOptions = group.options.filter(function (option) {
1703
+ return option.status === 'available' || option.status === 'limited';
1704
+ });
1705
+ var selectedOptions = selection ? availableOptions.slice(0, group.max) : availableOptions.slice(0, Math.max(group.min, 1));
1706
+ var originalGroup = (product.requirementGroups || []).find(function (item) {
1707
+ return item.id === group.groupId;
1708
+ }) || {
1709
+ id: group.groupId,
1710
+ resourceType: group.resourceType,
1711
+ min: group.min,
1712
+ max: group.max
1713
+ };
1714
+ return selectedOptions.map(function (selectedOption) {
1715
+ var _candidate$primarySch, _candidate$primarySch2;
1716
+ return {
1717
+ productId: product.id,
1718
+ resourceId: selectedOption.resourceId,
1719
+ formId: group.formId,
1720
+ startAt: candidate.bookingStartAt || candidate.startAt,
1721
+ endAt: candidate.bookingEndAt || candidate.endAt,
1722
+ capacityRequired: resolveRequiredCapacity(originalGroup, product, requestedPartySize),
1723
+ scheduleId: (_candidate$primarySch = candidate.primaryScheduleRef) === null || _candidate$primarySch === void 0 ? void 0 : _candidate$primarySch.scheduleId,
1724
+ timeSlotId: (_candidate$primarySch2 = candidate.primaryScheduleRef) === null || _candidate$primarySch2 === void 0 ? void 0 : _candidate$primarySch2.timeSlotId
1725
+ };
1726
+ });
1727
+ });
1728
+ return {
1729
+ item: _objectSpread(_objectSpread({}, candidate), {}, {
1730
+ status: (gate === null || gate === void 0 ? void 0 : gate.status) || candidateStatus,
1731
+ maxPartySize: gate ? 0 : Number.isFinite(maxPartySize) ? maxPartySize : 0,
1732
+ reasonCodes: (gate === null || gate === void 0 ? void 0 : gate.reasonCodes) || Array.from(new Set(requiredGroups.flatMap(function (group) {
1733
+ return group.reasonCodes;
1734
+ }))),
1735
+ requirementGroups: requirementGroups
1736
+ }),
1737
+ assignments: gate ? [] : selectedAssignments,
1738
+ gate: gate || undefined
1739
+ };
975
1740
  }
976
- export function validateAssignments(contextInput, selection) {
977
- if (selection.assignments.length > 0) {
978
- var context = normalizePlannerContext(contextInput);
979
- var _conflicts = [];
980
- var _loop4 = function _loop4() {
981
- var assignment = selection.assignments[index];
982
- var product = context.products.find(function (item) {
983
- return sameId(item.id, assignment.productId);
984
- });
985
- var resource = context.resources.find(function (item) {
986
- return sameId(item.id, assignment.resourceId);
1741
+ function assertTimeCandidates(request) {
1742
+ if (request.target === 'time' && (!Array.isArray(request.candidates) || request.candidates.length === 0)) {
1743
+ throw availabilityError('MISSING_TIME_CANDIDATES', 'target=time 必须传非空 candidates', {
1744
+ request: request
1745
+ });
1746
+ }
1747
+ }
1748
+ function assertSelectedRanges(request) {
1749
+ if (request.target === 'resource' && (!Array.isArray(request.selectedRanges) || request.selectedRanges.length === 0)) {
1750
+ throw availabilityError('MISSING_SELECTED_RANGE', 'target=resource 必须传非空 selectedRanges', {
1751
+ request: request
1752
+ });
1753
+ }
1754
+ }
1755
+ function assertRangesWithinProjection(projection, ranges) {
1756
+ var coverage = {
1757
+ start: toDateTime(projection.meta.coverage.startAt, projection.meta.timezone),
1758
+ end: toDateTime(projection.meta.coverage.endAt, projection.meta.timezone)
1759
+ };
1760
+ ranges.forEach(function (range) {
1761
+ var displayRange = normalizeAbsoluteRange(range, projection.meta.timezone);
1762
+ var bookingRange = normalizeAbsoluteRange(getCandidateBookingRange(range), projection.meta.timezone);
1763
+ if (!displayRange || !bookingRange) {
1764
+ throw availabilityError('INVALID_TARGET_FILTER', '候选时间范围非法', {
1765
+ range: range
1766
+ });
1767
+ }
1768
+ if (!contains(coverage, displayRange) || !contains(coverage, bookingRange)) {
1769
+ throw availabilityError('OUT_OF_PROJECTION_COVERAGE', '候选时间范围超出 projection.coverage', {
1770
+ range: range,
1771
+ coverage: projection.meta.coverage
1772
+ });
1773
+ }
1774
+ });
1775
+ }
1776
+ export function queryAvailabilityProjection(projection, request) {
1777
+ var _extractFixedOffsetMi6, _request$productIds2;
1778
+ var normalizedPartySize = normalizePartySize(request.partySize);
1779
+ var evaluatedAt = formatDateTime(localDateToScalar(new Date()), projection.meta.timezone, (_extractFixedOffsetMi6 = extractFixedOffsetMinutes(projection.meta.now)) !== null && _extractFixedOffsetMi6 !== void 0 ? _extractFixedOffsetMi6 : captureRuntimeOffsetMinutes());
1780
+ if (request.target === 'time') {
1781
+ assertTimeCandidates(request);
1782
+ assertRangesWithinProjection(projection, request.candidates);
1783
+ }
1784
+ if (request.target === 'resource') {
1785
+ assertSelectedRanges(request);
1786
+ assertRangesWithinProjection(projection, request.selectedRanges);
1787
+ }
1788
+ var productIdSet = (_request$productIds2 = request.productIds) !== null && _request$productIds2 !== void 0 && _request$productIds2.length ? new Set(request.productIds.map(String)) : null;
1789
+ var productIndex = buildProductIndex(projection);
1790
+ var cellIndex = buildAvailabilityCellIndex(projection);
1791
+ var groupResourceIndex = buildGroupResourceIndex({
1792
+ projection: projection,
1793
+ resourceIds: request.resourceIds,
1794
+ partySize: normalizedPartySize
1795
+ });
1796
+ var candidateRangeCache = new Map();
1797
+ var evaluationContext = buildEvaluationContext({
1798
+ projection: projection,
1799
+ evaluatedAt: evaluatedAt,
1800
+ resourceIds: request.resourceIds
1801
+ });
1802
+ if (request.target === 'time') {
1803
+ var candidates = dedupeProductRanges(request.candidates);
1804
+ var candidatesByProduct = new Map();
1805
+ candidates.forEach(function (candidate) {
1806
+ var key = String(candidate.productId);
1807
+ candidatesByProduct.set(key, [].concat(_toConsumableArray(candidatesByProduct.get(key) || []), [candidate]));
1808
+ });
1809
+ var _data = projection.products.filter(function (product) {
1810
+ return !productIdSet || productIdSet.has(String(product.id));
1811
+ }).map(function (product) {
1812
+ return {
1813
+ productId: product.id,
1814
+ title: product.title,
1815
+ times: (candidatesByProduct.get(String(product.id)) || []).map(function (candidate) {
1816
+ return evaluateCandidate(projection, candidate, {
1817
+ partySize: normalizedPartySize,
1818
+ resourceIds: request.resourceIds,
1819
+ evaluatedAt: evaluatedAt,
1820
+ cellIndex: cellIndex,
1821
+ productIndex: productIndex,
1822
+ groupResourceIndex: groupResourceIndex,
1823
+ candidateRangeCache: candidateRangeCache,
1824
+ evaluationContext: evaluationContext
1825
+ }).item;
1826
+ })
1827
+ };
1828
+ }).filter(function (group) {
1829
+ return group.times.length > 0;
1830
+ });
1831
+ return {
1832
+ target: 'time',
1833
+ evaluatedAt: evaluatedAt,
1834
+ snapshotId: projection.meta.snapshotId,
1835
+ data: _data
1836
+ };
1837
+ }
1838
+ if (request.target === 'resource') {
1839
+ var selectedRanges = dedupeProductRanges(request.selectedRanges);
1840
+ var rangesByProduct = new Map();
1841
+ selectedRanges.forEach(function (candidate) {
1842
+ var key = String(candidate.productId);
1843
+ rangesByProduct.set(key, [].concat(_toConsumableArray(rangesByProduct.get(key) || []), [candidate]));
1844
+ });
1845
+ var _data2 = projection.products.filter(function (product) {
1846
+ return !productIdSet || productIdSet.has(String(product.id));
1847
+ }).map(function (product) {
1848
+ return {
1849
+ productId: product.id,
1850
+ title: product.title,
1851
+ ranges: (rangesByProduct.get(String(product.id)) || []).map(function (candidate) {
1852
+ var evaluated = evaluateCandidate(projection, candidate, {
1853
+ partySize: normalizedPartySize,
1854
+ resourceIds: request.resourceIds,
1855
+ evaluatedAt: evaluatedAt,
1856
+ cellIndex: cellIndex,
1857
+ productIndex: productIndex,
1858
+ groupResourceIndex: groupResourceIndex,
1859
+ candidateRangeCache: candidateRangeCache,
1860
+ evaluationContext: evaluationContext
1861
+ }).item;
1862
+ return {
1863
+ range: candidate,
1864
+ status: evaluated.status,
1865
+ maxPartySize: evaluated.maxPartySize,
1866
+ reasonCodes: evaluated.reasonCodes,
1867
+ requirementGroups: evaluated.requirementGroups
1868
+ };
1869
+ })
1870
+ };
1871
+ }).filter(function (group) {
1872
+ return group.ranges.length > 0;
1873
+ });
1874
+ return {
1875
+ target: 'resource',
1876
+ evaluatedAt: evaluatedAt,
1877
+ snapshotId: projection.meta.snapshotId,
1878
+ data: _data2
1879
+ };
1880
+ }
1881
+ var dateRange = resolveProjectionDateRange(projection, request.dateRange);
1882
+ var candidateGroups = getProductTimeRanges(projection, {
1883
+ productIds: request.productIds,
1884
+ dateRange: dateRange,
1885
+ resourceIds: request.resourceIds,
1886
+ partySize: normalizedPartySize
1887
+ }, {
1888
+ evaluatedAt: evaluatedAt
1889
+ });
1890
+ var candidateGroupMap = new Map(candidateGroups.map(function (group) {
1891
+ return [String(group.productId), group];
1892
+ }));
1893
+ if (request.target === 'date') {
1894
+ var _data3 = projection.products.filter(function (product) {
1895
+ return !productIdSet || productIdSet.has(String(product.id));
1896
+ }).map(function (product) {
1897
+ var group = candidateGroupMap.get(String(product.id)) || {
1898
+ productId: product.id,
1899
+ title: product.title,
1900
+ kind: product.kind,
1901
+ ranges: []
1902
+ };
1903
+ var byDate = new Map();
1904
+ group.ranges.forEach(function (candidate) {
1905
+ var evaluated = evaluateCandidateSummary(projection, candidate, {
1906
+ partySize: normalizedPartySize,
1907
+ resourceIds: request.resourceIds,
1908
+ evaluatedAt: evaluatedAt,
1909
+ cellIndex: cellIndex,
1910
+ productIndex: productIndex,
1911
+ groupResourceIndex: groupResourceIndex,
1912
+ candidateRangeCache: candidateRangeCache,
1913
+ evaluationContext: evaluationContext
987
1914
  });
988
- if (!product) {
989
- _conflicts.push({
990
- type: 'missing_resource',
991
- message: 'assignment 对应的商品不存在',
992
- productId: assignment.productId
993
- });
994
- return 0; // continue
995
- }
996
- if (!resource) {
997
- _conflicts.push({
998
- type: 'missing_resource',
999
- message: 'assignment 对应的资源不存在',
1000
- productId: assignment.productId,
1001
- resourceId: assignment.resourceId
1002
- });
1003
- return 0; // continue
1004
- }
1005
- var start = dayjs(assignment.start_at);
1006
- var end = dayjs(assignment.end_at);
1007
- var interval = {
1008
- id: "".concat(assignment.productId, ":").concat(assignment.resourceId, ":").concat(assignment.start_at, ":").concat(assignment.end_at),
1009
- productId: assignment.productId,
1010
- source: product.kind === 'session' ? 'session' : product.kind === 'venue_slot' ? 'venue_slot' : 'duration',
1011
- date: start.format('YYYY-MM-DD'),
1012
- start_at: assignment.start_at,
1013
- end_at: assignment.end_at,
1014
- durationMinutes: Math.max(1, end.diff(start, 'minute')),
1015
- quantity: Math.max(1, Number(product.quantity || 1)),
1016
- capacityRequired: Math.max(1, Number(assignment.capacityRequired || product.capacityRequired || 1)),
1017
- resourceRequirements: product.resourceRequirements || []
1915
+ var aggregate = byDate.get(candidate.date) || {
1916
+ availableCount: 0,
1917
+ totalCount: 0,
1918
+ maxPartySize: 0,
1919
+ remainingCapacity: 0,
1920
+ maxCapacity: 0,
1921
+ reasonCodeSet: new Set(),
1922
+ statusAggregate: createStatusAggregate()
1018
1923
  };
1019
- var cell = evaluateResourceForInterval({
1020
- resource: resource,
1021
- interval: interval,
1022
- context: context
1023
- });
1024
- if (cell.status !== 'available' && cell.status !== 'limited') {
1025
- _conflicts.push.apply(_conflicts, _toConsumableArray(cell.conflicts));
1924
+ aggregate.totalCount += 1;
1925
+ if (evaluated.status === 'available' || evaluated.status === 'limited') {
1926
+ aggregate.availableCount += 1;
1026
1927
  }
1027
- var priorContext = withAssignmentEvents(context, selection.assignments.slice(0, index));
1028
- var selfConflictCell = evaluateResourceForInterval({
1029
- resource: resource,
1030
- interval: interval,
1031
- context: priorContext
1928
+ aggregate.maxPartySize = Math.max(aggregate.maxPartySize, evaluated.maxPartySize);
1929
+ aggregate.remainingCapacity += evaluated.remainingCapacity;
1930
+ aggregate.maxCapacity += evaluated.maxCapacity;
1931
+ pushStatusAggregate(aggregate.statusAggregate, evaluated.status);
1932
+ evaluated.reasonCodes.forEach(function (reasonCode) {
1933
+ return aggregate.reasonCodeSet.add(reasonCode);
1032
1934
  });
1033
- if (selfConflictCell.status !== 'available' && selfConflictCell.status !== 'limited') {
1034
- _conflicts.push.apply(_conflicts, _toConsumableArray(selfConflictCell.conflicts.map(function (conflict) {
1035
- return _objectSpread(_objectSpread({}, conflict), {}, {
1036
- type: 'cart_overlap',
1037
- message: '本次选择中存在资源时间冲突'
1038
- });
1039
- })));
1040
- }
1041
- },
1042
- _ret3;
1043
- for (var index = 0; index < selection.assignments.length; index += 1) {
1044
- _ret3 = _loop4();
1045
- if (_ret3 === 0) continue;
1046
- }
1935
+ byDate.set(candidate.date, aggregate);
1936
+ });
1937
+ var dates = enumerateDates(dateRange.startDate, dateRange.endDate).map(function (date) {
1938
+ var aggregate = byDate.get(date);
1939
+ return {
1940
+ date: date,
1941
+ status: aggregate ? summarizeStatusAggregate(aggregate.statusAggregate) : 'unavailable',
1942
+ availableCount: (aggregate === null || aggregate === void 0 ? void 0 : aggregate.availableCount) || 0,
1943
+ totalCount: (aggregate === null || aggregate === void 0 ? void 0 : aggregate.totalCount) || 0,
1944
+ maxPartySize: (aggregate === null || aggregate === void 0 ? void 0 : aggregate.maxPartySize) || 0,
1945
+ remainingCapacity: (aggregate === null || aggregate === void 0 ? void 0 : aggregate.remainingCapacity) || 0,
1946
+ maxCapacity: (aggregate === null || aggregate === void 0 ? void 0 : aggregate.maxCapacity) || 0,
1947
+ reasonCodes: aggregate ? Array.from(aggregate.reasonCodeSet) : []
1948
+ };
1949
+ });
1950
+ return {
1951
+ productId: product.id,
1952
+ title: product.title,
1953
+ dates: dates
1954
+ };
1955
+ });
1047
1956
  return {
1048
- ok: _conflicts.length === 0,
1049
- conflicts: _conflicts
1957
+ target: 'date',
1958
+ evaluatedAt: evaluatedAt,
1959
+ snapshotId: projection.meta.snapshotId,
1960
+ data: _data3
1050
1961
  };
1051
1962
  }
1052
- var projection = queryPlannerAvailability(contextInput, {
1053
- productIds: selection.productIds,
1054
- date: selection.date,
1055
- startTime: selection.startTime,
1056
- endTime: selection.endTime,
1057
- resourceIds: selection.resourceIds,
1058
- mode: selection.mode
1963
+ var data = projection.products.filter(function (product) {
1964
+ return !productIdSet || productIdSet.has(String(product.id));
1965
+ }).map(function (product) {
1966
+ var group = candidateGroupMap.get(String(product.id)) || {
1967
+ productId: product.id,
1968
+ title: product.title,
1969
+ kind: product.kind,
1970
+ ranges: []
1971
+ };
1972
+ var productStatusAggregate = createStatusAggregate();
1973
+ var productReasonCodeSet = new Set();
1974
+ var dateStatusMap = new Map();
1975
+ var availableTimeCount = 0;
1976
+ var totalTimeCount = 0;
1977
+ var maxPartySize = 0;
1978
+ group.ranges.forEach(function (candidate) {
1979
+ var evaluated = evaluateCandidateSummary(projection, candidate, {
1980
+ partySize: normalizedPartySize,
1981
+ resourceIds: request.resourceIds,
1982
+ evaluatedAt: evaluatedAt,
1983
+ cellIndex: cellIndex,
1984
+ productIndex: productIndex,
1985
+ groupResourceIndex: groupResourceIndex,
1986
+ candidateRangeCache: candidateRangeCache,
1987
+ evaluationContext: evaluationContext
1988
+ });
1989
+ totalTimeCount += 1;
1990
+ if (evaluated.status === 'available' || evaluated.status === 'limited') {
1991
+ availableTimeCount += 1;
1992
+ }
1993
+ maxPartySize = Math.max(maxPartySize, evaluated.maxPartySize);
1994
+ pushStatusAggregate(productStatusAggregate, evaluated.status);
1995
+ evaluated.reasonCodes.forEach(function (reasonCode) {
1996
+ return productReasonCodeSet.add(reasonCode);
1997
+ });
1998
+ var dateAggregate = dateStatusMap.get(candidate.date) || createStatusAggregate();
1999
+ pushStatusAggregate(dateAggregate, evaluated.status);
2000
+ dateStatusMap.set(candidate.date, dateAggregate);
2001
+ });
2002
+ var availableDateCount = Array.from(dateStatusMap.values()).filter(function (aggregate) {
2003
+ var status = summarizeStatusAggregate(aggregate);
2004
+ return status === 'available' || status === 'limited';
2005
+ }).length;
2006
+ var totalDateCount = dateStatusMap.size;
2007
+ var status = summarizeStatusAggregate(productStatusAggregate);
2008
+ var item = {
2009
+ productId: group.productId,
2010
+ title: group.title,
2011
+ status: status,
2012
+ reasonCodes: Array.from(productReasonCodeSet),
2013
+ availableDateCount: availableDateCount,
2014
+ totalDateCount: totalDateCount,
2015
+ availableTimeCount: availableTimeCount,
2016
+ totalTimeCount: totalTimeCount,
2017
+ maxPartySize: maxPartySize
2018
+ };
2019
+ return {
2020
+ productId: product.id,
2021
+ title: product.title,
2022
+ item: item
2023
+ };
1059
2024
  });
1060
- var conflicts = projection.cells.filter(function (cell) {
1061
- return cell.status !== 'available' && cell.status !== 'limited';
1062
- }).flatMap(function (cell) {
1063
- return cell.conflicts;
2025
+ return {
2026
+ target: 'product',
2027
+ evaluatedAt: evaluatedAt,
2028
+ snapshotId: projection.meta.snapshotId,
2029
+ data: data
2030
+ };
2031
+ }
2032
+ function summarizeDateStatus(statuses) {
2033
+ if (!statuses.length) return 'unavailable';
2034
+ if (statuses.some(function (status) {
2035
+ return status === 'available';
2036
+ })) return 'available';
2037
+ if (statuses.some(function (status) {
2038
+ return status === 'limited';
2039
+ })) return 'limited';
2040
+ if (statuses.some(function (status) {
2041
+ return status === 'full';
2042
+ })) return 'full';
2043
+ if (statuses.every(function (status) {
2044
+ return status === 'past';
2045
+ })) return 'past';
2046
+ return 'unavailable';
2047
+ }
2048
+ function summarizeProductStatus(statuses) {
2049
+ if (!statuses.length) return 'unavailable';
2050
+ if (statuses.some(function (status) {
2051
+ return status === 'available';
2052
+ })) return 'available';
2053
+ if (statuses.some(function (status) {
2054
+ return status === 'limited';
2055
+ })) return 'limited';
2056
+ if (statuses.some(function (status) {
2057
+ return status === 'full';
2058
+ })) return 'full';
2059
+ if (statuses.every(function (status) {
2060
+ return status === 'past';
2061
+ })) return 'past';
2062
+ return 'unavailable';
2063
+ }
2064
+ export function validateAvailabilitySelection(projection, params) {
2065
+ var _extractFixedOffsetMi7;
2066
+ var normalizedPartySize = normalizePartySize(params.partySize);
2067
+ var normalizedBookingCount = normalizeBookingCount(params.bookingCount);
2068
+ assertRangesWithinProjection(projection, [params.candidate]);
2069
+ var productIndex = buildProductIndex(projection);
2070
+ var evaluatedAt = formatDateTime(localDateToScalar(new Date()), projection.meta.timezone, (_extractFixedOffsetMi7 = extractFixedOffsetMinutes(projection.meta.now)) !== null && _extractFixedOffsetMi7 !== void 0 ? _extractFixedOffsetMi7 : captureRuntimeOffsetMinutes());
2071
+ var evaluated = evaluateCandidate(projection, params.candidate, {
2072
+ partySize: normalizedPartySize,
2073
+ evaluatedAt: evaluatedAt,
2074
+ requirementSelections: params.requirementSelections,
2075
+ allowImplicitSelection: false,
2076
+ cellIndex: buildAvailabilityCellIndex(projection),
2077
+ productIndex: productIndex,
2078
+ groupResourceIndex: buildGroupResourceIndex({
2079
+ projection: projection,
2080
+ partySize: normalizedPartySize
2081
+ }),
2082
+ candidateRangeCache: new Map(),
2083
+ evaluationContext: buildEvaluationContext({
2084
+ projection: projection,
2085
+ evaluatedAt: evaluatedAt,
2086
+ requirementSelections: params.requirementSelections
2087
+ })
2088
+ });
2089
+ var conflicts = [];
2090
+ if (evaluated.gate) {
2091
+ conflicts.push({
2092
+ code: evaluated.gate.conflictCode,
2093
+ message: evaluated.gate.status === 'past' ? '候选时间已过期' : '候选时间不满足预订时间门禁',
2094
+ reasonCodes: evaluated.gate.reasonCodes,
2095
+ productId: params.candidate.productId,
2096
+ candidateKey: params.candidate.key
2097
+ });
2098
+ return {
2099
+ ok: false,
2100
+ status: evaluated.gate.status === 'past' ? 'past' : 'conflict',
2101
+ assignments: [],
2102
+ conflicts: conflicts,
2103
+ availability: evaluated.item
2104
+ };
2105
+ }
2106
+ var requiredGroups = evaluated.item.requirementGroups.filter(function (group) {
2107
+ return group.required;
2108
+ });
2109
+ var findSelectionForGroup = function findSelectionForGroup(group) {
2110
+ return params.requirementSelections.find(function (selection) {
2111
+ return selection.requirementGroupId && selection.requirementGroupId === group.groupId || selection.formId !== undefined && sameId(selection.formId, group.formId);
2112
+ });
2113
+ };
2114
+ requiredGroups.forEach(function (group) {
2115
+ var selection = findSelectionForGroup(group);
2116
+ var selectedCount = new Set(((selection === null || selection === void 0 ? void 0 : selection.resourceIds) || []).map(String)).size;
2117
+ if (selectedCount < group.min) {
2118
+ conflicts.push({
2119
+ code: 'missing_resource',
2120
+ message: "\u8D44\u6E90\u7EC4 ".concat(group.groupId, " \u7F3A\u5C11\u5DF2\u9009\u8D44\u6E90"),
2121
+ productId: params.candidate.productId,
2122
+ requirementGroupId: group.groupId,
2123
+ formId: group.formId,
2124
+ candidateKey: params.candidate.key
2125
+ });
2126
+ return;
2127
+ }
2128
+ if (selectedCount > group.max) {
2129
+ conflicts.push({
2130
+ code: 'invalid_selection_count',
2131
+ message: "\u8D44\u6E90\u7EC4 ".concat(group.groupId, " \u5DF2\u9009\u8D44\u6E90\u6570\u91CF\u8D85\u51FA\u4E0A\u9650"),
2132
+ productId: params.candidate.productId,
2133
+ requirementGroupId: group.groupId,
2134
+ formId: group.formId,
2135
+ candidateKey: params.candidate.key
2136
+ });
2137
+ return;
2138
+ }
2139
+ if (group.status === 'available' || group.status === 'limited') return;
2140
+ conflicts.push({
2141
+ code: group.status === 'past' ? 'past' : group.status === 'full' ? 'resource_full' : 'missing_resource',
2142
+ message: "\u8D44\u6E90\u7EC4 ".concat(group.groupId, " \u5F53\u524D\u4E0D\u53EF\u63D0\u4EA4"),
2143
+ productId: params.candidate.productId,
2144
+ requirementGroupId: group.groupId,
2145
+ formId: group.formId,
2146
+ candidateKey: params.candidate.key
2147
+ });
2148
+ });
2149
+ params.requirementSelections.forEach(function (selection) {
2150
+ var matchedGroup = evaluated.item.requirementGroups.find(function (group) {
2151
+ return selection.requirementGroupId ? group.groupId === selection.requirementGroupId : sameId(group.formId, selection.formId);
2152
+ });
2153
+ if (!matchedGroup) {
2154
+ conflicts.push({
2155
+ code: 'invalid_requirement_group',
2156
+ message: '选择的资源组不属于当前商品',
2157
+ productId: params.candidate.productId,
2158
+ requirementGroupId: selection.requirementGroupId,
2159
+ formId: selection.formId,
2160
+ candidateKey: params.candidate.key
2161
+ });
2162
+ return;
2163
+ }
2164
+ var selectedCount = new Set(selection.resourceIds.map(String)).size;
2165
+ if (!matchedGroup.required && (selectedCount < matchedGroup.min || selectedCount > matchedGroup.max)) {
2166
+ conflicts.push({
2167
+ code: 'invalid_selection_count',
2168
+ message: "\u8D44\u6E90\u7EC4 ".concat(matchedGroup.groupId, " \u5DF2\u9009\u8D44\u6E90\u6570\u91CF\u4E0D\u6EE1\u8DB3 ").concat(matchedGroup.min, "-").concat(matchedGroup.max),
2169
+ productId: params.candidate.productId,
2170
+ requirementGroupId: matchedGroup.groupId,
2171
+ formId: matchedGroup.formId,
2172
+ candidateKey: params.candidate.key
2173
+ });
2174
+ }
2175
+ selection.resourceIds.forEach(function (resourceId) {
2176
+ var option = matchedGroup === null || matchedGroup === void 0 ? void 0 : matchedGroup.options.find(function (item) {
2177
+ return sameId(item.resourceId, resourceId);
2178
+ });
2179
+ var matchedCell = option === null || option === void 0 ? void 0 : option.matchedCell;
2180
+ var remainingCapacity = matchedCell === null || matchedCell === void 0 ? void 0 : matchedCell.remainingCapacity;
2181
+ if (option && (option.status === 'available' || option.status === 'limited') && remainingCapacity !== undefined) {
2182
+ var capacityPerBooking = normalizedPartySize !== null && normalizedPartySize !== void 0 ? normalizedPartySize : 1;
2183
+ var availableQuantity = Math.max(0, Math.floor(remainingCapacity / capacityPerBooking));
2184
+ if (normalizedBookingCount > availableQuantity) {
2185
+ conflicts.push({
2186
+ code: 'resource_capacity_exceeded',
2187
+ message: "\u6240\u9009\u8D44\u6E90\u300C".concat(option.resourceName || String(resourceId), "\u300D\u5F53\u524D\u6700\u591A\u53EF\u9884\u8BA2 ").concat(availableQuantity, " \u4E2A"),
2188
+ productId: params.candidate.productId,
2189
+ requirementGroupId: matchedGroup === null || matchedGroup === void 0 ? void 0 : matchedGroup.groupId,
2190
+ formId: matchedGroup === null || matchedGroup === void 0 ? void 0 : matchedGroup.formId,
2191
+ resourceId: resourceId,
2192
+ resourceName: option.resourceName,
2193
+ candidateKey: params.candidate.key,
2194
+ requestedQuantity: normalizedBookingCount,
2195
+ availableQuantity: availableQuantity,
2196
+ remainingCapacity: remainingCapacity,
2197
+ capacityPerBooking: capacityPerBooking
2198
+ });
2199
+ return;
2200
+ }
2201
+ }
2202
+ if (!option || option.status !== 'available' && option.status !== 'limited') {
2203
+ conflicts.push({
2204
+ code: 'resource_unavailable',
2205
+ message: "\u8D44\u6E90 ".concat(String(resourceId), " \u5F53\u524D\u4E0D\u53EF\u63D0\u4EA4"),
2206
+ productId: params.candidate.productId,
2207
+ requirementGroupId: matchedGroup === null || matchedGroup === void 0 ? void 0 : matchedGroup.groupId,
2208
+ formId: matchedGroup === null || matchedGroup === void 0 ? void 0 : matchedGroup.formId,
2209
+ resourceId: resourceId,
2210
+ candidateKey: params.candidate.key
2211
+ });
2212
+ }
2213
+ });
1064
2214
  });
1065
2215
  return {
1066
- ok: conflicts.length === 0,
1067
- conflicts: conflicts
2216
+ ok: conflicts.length === 0 && (evaluated.item.status === 'available' || evaluated.item.status === 'limited'),
2217
+ status: conflicts.length === 0 ? evaluated.item.status : 'conflict',
2218
+ assignments: evaluated.assignments,
2219
+ conflicts: conflicts,
2220
+ availability: evaluated.item
1068
2221
  };
1069
2222
  }