@pisell/pisellos 2.3.72 → 2.3.74

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 (74) hide show
  1. package/dist/core/index.d.ts +7 -0
  2. package/dist/core/index.js +109 -65
  3. package/dist/modules/OpenData/types.d.ts +1 -0
  4. package/dist/modules/Order/index.d.ts +1 -1
  5. package/dist/modules/Order/utils.js +6 -1
  6. package/dist/modules/Quotation/index.d.ts +6 -0
  7. package/dist/modules/Quotation/index.js +75 -19
  8. package/dist/modules/ResourcePlanner/index.d.ts +0 -22
  9. package/dist/modules/ResourcePlanner/index.js +1 -180
  10. package/dist/modules/ResourcePlanner/localClock.d.ts +33 -0
  11. package/dist/modules/ResourcePlanner/localClock.js +183 -0
  12. package/dist/modules/ResourcePlanner/planner.d.ts +5 -14
  13. package/dist/modules/ResourcePlanner/planner.js +2019 -936
  14. package/dist/modules/ResourcePlanner/types.d.ts +346 -187
  15. package/dist/modules/ResourcePlanner/types.js +33 -1
  16. package/dist/plugins/request.d.ts +1 -0
  17. package/dist/server/index.d.ts +8 -2
  18. package/dist/server/index.js +206 -140
  19. package/dist/server/modules/floor-plan/index.d.ts +4 -0
  20. package/dist/server/modules/floor-plan/index.js +240 -98
  21. package/dist/server/modules/menu/index.js +48 -23
  22. package/dist/server/modules/order/index.d.ts +17 -7
  23. package/dist/server/modules/order/index.js +419 -207
  24. package/dist/server/modules/products/index.d.ts +8 -2
  25. package/dist/server/modules/products/index.js +167 -75
  26. package/dist/server/modules/resource/index.js +21 -13
  27. package/dist/solution/BookingByStep/index.d.ts +1 -1
  28. package/dist/solution/BookingTicket/index.js +4 -2
  29. package/dist/solution/BookingTicket/utils/addProductDecision.d.ts +2 -1
  30. package/dist/solution/BookingTicket/utils/addProductDecision.js +3 -2
  31. package/dist/solution/UnifiedBookingSales/index.d.ts +110 -73
  32. package/dist/solution/UnifiedBookingSales/index.js +3349 -971
  33. package/dist/solution/UnifiedBookingSales/remoteOccupancyCache.d.ts +49 -0
  34. package/dist/solution/UnifiedBookingSales/remoteOccupancyCache.js +430 -0
  35. package/dist/solution/UnifiedBookingSales/types.d.ts +133 -76
  36. package/dist/solution/UnifiedBookingSales/types.js +4 -9
  37. package/lib/core/index.d.ts +7 -0
  38. package/lib/core/index.js +20 -2
  39. package/lib/model/strategy/adapter/promotion/index.js +0 -51
  40. package/lib/modules/OpenData/types.d.ts +1 -0
  41. package/lib/modules/Order/index.d.ts +1 -1
  42. package/lib/modules/Order/utils.js +5 -1
  43. package/lib/modules/Quotation/index.d.ts +6 -0
  44. package/lib/modules/Quotation/index.js +49 -5
  45. package/lib/modules/ResourcePlanner/index.d.ts +0 -22
  46. package/lib/modules/ResourcePlanner/index.js +0 -123
  47. package/lib/modules/ResourcePlanner/localClock.d.ts +33 -0
  48. package/lib/modules/ResourcePlanner/localClock.js +270 -0
  49. package/lib/modules/ResourcePlanner/planner.d.ts +5 -14
  50. package/lib/modules/ResourcePlanner/planner.js +1903 -798
  51. package/lib/modules/ResourcePlanner/types.d.ts +346 -187
  52. package/lib/modules/ResourcePlanner/types.js +19 -0
  53. package/lib/plugins/request.d.ts +1 -0
  54. package/lib/server/index.d.ts +8 -2
  55. package/lib/server/index.js +52 -17
  56. package/lib/server/modules/floor-plan/index.d.ts +4 -0
  57. package/lib/server/modules/floor-plan/index.js +54 -6
  58. package/lib/server/modules/menu/index.js +31 -5
  59. package/lib/server/modules/order/index.d.ts +17 -7
  60. package/lib/server/modules/order/index.js +197 -40
  61. package/lib/server/modules/products/index.d.ts +8 -2
  62. package/lib/server/modules/products/index.js +97 -24
  63. package/lib/server/modules/resource/index.js +7 -2
  64. package/lib/solution/BookingByStep/index.d.ts +1 -1
  65. package/lib/solution/BookingTicket/index.js +2 -1
  66. package/lib/solution/BookingTicket/utils/addProductDecision.d.ts +2 -1
  67. package/lib/solution/BookingTicket/utils/addProductDecision.js +2 -0
  68. package/lib/solution/UnifiedBookingSales/index.d.ts +110 -73
  69. package/lib/solution/UnifiedBookingSales/index.js +2289 -374
  70. package/lib/solution/UnifiedBookingSales/remoteOccupancyCache.d.ts +49 -0
  71. package/lib/solution/UnifiedBookingSales/remoteOccupancyCache.js +376 -0
  72. package/lib/solution/UnifiedBookingSales/types.d.ts +133 -76
  73. package/lib/solution/UnifiedBookingSales/types.js +3 -2
  74. package/package.json +1 -1
@@ -30,25 +30,37 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/solution/UnifiedBookingSales/index.ts
31
31
  var UnifiedBookingSales_exports = {};
32
32
  __export(UnifiedBookingSales_exports, {
33
+ AvailabilityError: () => import_ResourcePlanner2.AvailabilityError,
33
34
  UnifiedBookingSales: () => UnifiedBookingSales,
34
35
  UnifiedBookingSalesImpl: () => UnifiedBookingSalesImpl,
35
- buildBookingFromAssignment: () => buildBookingFromAssignment,
36
- buildBookingFromAssignments: () => buildBookingFromAssignments,
36
+ buildAvailabilityLineIdentity: () => buildAvailabilityLineIdentity,
37
+ buildAvailabilityResourcesFromV2: () => buildAvailabilityResourcesFromV2,
38
+ buildBookingFromAvailabilityAssignment: () => buildBookingFromAvailabilityAssignment,
39
+ buildBookingFromAvailabilityAssignments: () => buildBookingFromAvailabilityAssignments,
37
40
  buildPlannerDiscountResult: () => buildPlannerDiscountResult,
38
- buildPlannerLineIdentity: () => buildPlannerLineIdentity,
39
- buildPlannerResourcesFromV2: () => buildPlannerResourcesFromV2,
40
- collectPlannerResourceIds: () => collectPlannerResourceIds,
41
- normalizeProductForPlanner: () => normalizeProductForPlanner
41
+ collectAvailabilityResourceIds: () => collectAvailabilityResourceIds,
42
+ normalizeProductForAvailability: () => normalizeProductForAvailability
42
43
  });
43
44
  module.exports = __toCommonJS(UnifiedBookingSales_exports);
44
45
  var import_dayjs = __toESM(require("dayjs"));
46
+ var import_lodash_es = require("lodash-es");
45
47
  var import_modules = require("../../modules");
46
48
  var import_getDateIsInSchedule = require("../../modules/Schedule/getDateIsInSchedule");
49
+ var import_ResourcePlanner = require("../../modules/ResourcePlanner");
47
50
  var import_types = require("./types");
51
+ var import_remoteOccupancyCache = require("./remoteOccupancyCache");
48
52
  var import_BookingTicket = require("../BookingTicket");
53
+ var import_localClock = require("../../modules/ResourcePlanner/localClock");
49
54
  __reExport(UnifiedBookingSales_exports, require("./types"), module.exports);
50
- var OPEN_DATA_SECTION_CODES = ["sale", "reservation", "fulfillment", "menu", "workflow"];
55
+ var import_ResourcePlanner2 = require("../../modules/ResourcePlanner");
56
+ var OPEN_DATA_SECTION_CODES = ["sale", "reservation", "fulfillment", "menu", "workflow", "basic", "checkout", "availability"];
51
57
  var OPEN_DATA_CACHE_TTL = 5 * 60 * 1e3;
58
+ function normalizePositiveInteger(value) {
59
+ const normalized = Number(value);
60
+ if (!Number.isInteger(normalized) || normalized <= 0)
61
+ return void 0;
62
+ return normalized;
63
+ }
52
64
  function isRecord(value) {
53
65
  return !!value && typeof value === "object" && !Array.isArray(value);
54
66
  }
@@ -166,88 +178,473 @@ function filterCustomerBoundDiscounts(discountList) {
166
178
  return [];
167
179
  return discountList.filter((discount) => !isCustomerBoundDiscount(discount));
168
180
  }
181
+ var DEFAULT_AVAILABILITY_SLOT_STEP_MINUTES = 30;
182
+ var DEFAULT_AVAILABILITY_BUSINESS_HOURS = {
183
+ startTime: "09:00",
184
+ endTime: "18:00"
185
+ };
186
+ var MAX_AVAILABILITY_RANGE_DAYS = 90;
187
+ var MAX_AVAILABILITY_CONTEXT_REGISTRY = 128;
188
+ var MAX_AVAILABILITY_PROJECTION_POOL = 16;
189
+ var MAX_REMOTE_OCCUPANCY_RESOURCE_DATE_CELLS = 4096;
190
+ function isValidClockTime(value) {
191
+ const match = /^(\d{2}):(\d{2})$/.exec(value);
192
+ if (!match)
193
+ return false;
194
+ return Number(match[1]) < 24 && Number(match[2]) < 60;
195
+ }
196
+ function assertValidAvailabilityTimezone(timezoneName) {
197
+ if (!String(timezoneName || "").trim()) {
198
+ throw new import_ResourcePlanner.AvailabilityError("INVALID_TARGET_FILTER", "timezone label 不能为空", {
199
+ timezone: timezoneName
200
+ });
201
+ }
202
+ }
203
+ function normalizeAvailabilityLocalDateTime(value, fixedOffsetMinutes) {
204
+ if (value == null || value === "")
205
+ return null;
206
+ try {
207
+ const localText = typeof (value == null ? void 0 : value.format) === "function" ? value.format("YYYY-MM-DDTHH:mm:ss") : String(value);
208
+ return (0, import_localClock.formatLocalDateTime)((0, import_localClock.parseLocalDateTime)(localText), fixedOffsetMinutes);
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+ function normalizeAvailabilityLocalDateTimeRange(params) {
214
+ const startText = normalizeAvailabilityLocalDateTime(params.startAt, params.fixedOffsetMinutes);
215
+ const endText = normalizeAvailabilityLocalDateTime(params.endAt, params.fixedOffsetMinutes);
216
+ if (!startText || !endText)
217
+ return null;
218
+ const startScalar = (0, import_localClock.localDateTimeToScalar)((0, import_localClock.parseLocalDateTime)(startText));
219
+ let endScalar = (0, import_localClock.localDateTimeToScalar)((0, import_localClock.parseLocalDateTime)(endText));
220
+ if (endScalar <= startScalar && params.rollOverEnd) {
221
+ endScalar = (0, import_localClock.addLocalCalendarDays)(endScalar, 1);
222
+ }
223
+ if (endScalar <= startScalar)
224
+ return null;
225
+ return {
226
+ startAt: startText,
227
+ endAt: (0, import_localClock.formatLocalDateTime)(endScalar, params.fixedOffsetMinutes),
228
+ startScalar,
229
+ endScalar
230
+ };
231
+ }
232
+ function sameAvailabilityId(left, right) {
233
+ return left !== void 0 && left !== null && right !== void 0 && right !== null && String(left) === String(right);
234
+ }
235
+ function uniqueAvailabilityIds(values) {
236
+ const result = /* @__PURE__ */ new Map();
237
+ values.forEach((value) => {
238
+ const id = toNumberId(value);
239
+ if (id !== void 0)
240
+ result.set(String(id), id);
241
+ });
242
+ return Array.from(result.values());
243
+ }
244
+ function assertAvailabilityCatalogProductIds(productIds, context) {
245
+ if (productIds == null)
246
+ return;
247
+ if (!Array.isArray(productIds)) {
248
+ throw new import_ResourcePlanner.AvailabilityError("INVALID_TARGET_FILTER", `${context} 必须是 number[]`, {
249
+ context,
250
+ productIds
251
+ });
252
+ }
253
+ const invalidIds = productIds.filter((id) => typeof id !== "number" || !Number.isFinite(id));
254
+ if (invalidIds.length > 0) {
255
+ throw new import_ResourcePlanner.AvailabilityError("INVALID_TARGET_FILTER", `${context} 只能包含有限数值商品 ID`, {
256
+ context,
257
+ invalidIds,
258
+ productIds
259
+ });
260
+ }
261
+ }
262
+ function normalizeResourceType(_value) {
263
+ return "single";
264
+ }
169
265
  function resolveProductKind(product) {
170
266
  var _a, _b, _c;
171
- if (product.duration)
267
+ if (product.duration || product.service_duration)
172
268
  return "duration";
269
+ if (product.extension_type === "venue_slot")
270
+ return "venue_slot";
173
271
  if (["session_product", "session_ticket"].includes(product.extension_type))
174
272
  return "session";
175
- if (((_a = product.cartDetailValue) == null ? void 0 : _a.session) || ((_c = (_b = product._extend) == null ? void 0 : _b.other) == null ? void 0 : _c.session))
273
+ if (((_a = product.cartDetailValue) == null ? void 0 : _a.session) || ((_c = (_b = product._extend) == null ? void 0 : _b.other) == null ? void 0 : _c.session) || product.session) {
176
274
  return "session";
177
- if (Array.isArray(product["schedule.ids"]) && product["schedule.ids"].length > 0)
275
+ }
276
+ const scheduleIds = product["schedule.ids"] || product.schedule_ids;
277
+ if (Array.isArray(scheduleIds) && scheduleIds.length > 0)
178
278
  return "session";
179
279
  return "normal";
180
280
  }
181
- function normalizeSchedule(product) {
182
- var _a, _b, _c;
183
- const session = ((_a = product.cartDetailValue) == null ? void 0 : _a.session) || ((_c = (_b = product._extend) == null ? void 0 : _b.other) == null ? void 0 : _c.session) || product.session;
184
- if ((session == null ? void 0 : session.start_at) && (session == null ? void 0 : session.end_at)) {
185
- return [{ start_at: session.start_at, end_at: session.end_at }];
186
- }
187
- const schedules = product.schedules || product.schedule || product._schedule;
188
- if (Array.isArray(schedules)) {
189
- return schedules.map((item) => ({
190
- start_at: item.start_at || item.start || item.start_time,
191
- end_at: item.end_at || item.end || item.end_time
192
- })).filter((item) => item.start_at && item.end_at);
193
- }
194
- return [];
281
+ function expandScheduleDateRanges(values) {
282
+ if (!Array.isArray(values))
283
+ return [];
284
+ const dates = /* @__PURE__ */ new Set();
285
+ values.forEach((value) => {
286
+ if (typeof value === "string") {
287
+ try {
288
+ dates.add((0, import_localClock.formatLocalDate)((0, import_localClock.parseLocalDate)(value.slice(0, 10))));
289
+ } catch {
290
+ }
291
+ return;
292
+ }
293
+ const startValue = (value == null ? void 0 : value.start) || (value == null ? void 0 : value.start_date) || (value == null ? void 0 : value.date);
294
+ const endValue = (value == null ? void 0 : value.end) || (value == null ? void 0 : value.end_date) || startValue;
295
+ let start;
296
+ let end;
297
+ try {
298
+ start = (0, import_localClock.localDateToScalarStart)((0, import_localClock.parseLocalDate)(String(startValue).slice(0, 10)));
299
+ end = (0, import_localClock.localDateToScalarStart)((0, import_localClock.parseLocalDate)(String(endValue).slice(0, 10)));
300
+ } catch {
301
+ return;
302
+ }
303
+ if (end < start)
304
+ return;
305
+ let cursor = start;
306
+ let guard = 0;
307
+ while (cursor <= end && guard < MAX_AVAILABILITY_RANGE_DAYS) {
308
+ dates.add((0, import_localClock.formatLocalDate)(cursor));
309
+ cursor = (0, import_localClock.addLocalCalendarDays)(cursor, 1);
310
+ guard += 1;
311
+ }
312
+ });
313
+ return Array.from(dates).sort();
195
314
  }
196
- function normalizeRequirements(product) {
315
+ function normalizeRequirementGroups(product) {
197
316
  var _a;
198
317
  const resources = (_a = product.product_resource) == null ? void 0 : _a.resources;
199
318
  if (!Array.isArray(resources))
200
319
  return [];
201
- return resources.filter((resource) => (resource == null ? void 0 : resource.status) !== 0).map((resource) => {
202
- var _a2, _b;
320
+ return resources.filter((resource) => (resource == null ? void 0 : resource.status) !== 0).map((resource, index) => {
321
+ var _a2, _b, _c;
322
+ const formId = toNumberId(resource.id ?? resource.form_id);
323
+ const required = resource.required ?? resource.status === 1;
324
+ const resourceType = normalizeResourceType(resource.type);
325
+ const combinedIds = ((_a2 = resource.combined_resource) == null ? void 0 : _a2.status) === 1 ? (_b = resource.combined_resource) == null ? void 0 : _b.resource_ids : [];
326
+ const resourceIds = uniqueAvailabilityIds([
327
+ ...Array.isArray(resource.default_resource) ? resource.default_resource : [],
328
+ ...Array.isArray(resource.optional_resource) ? resource.optional_resource : [],
329
+ ...Array.isArray(combinedIds) ? combinedIds : []
330
+ ]);
331
+ const min = required ? 1 : 0;
332
+ const max = 1;
203
333
  return {
204
- formId: toNumberId(resource.id),
205
- resourceIds: [
206
- ...Array.isArray(resource.default_resource) ? resource.default_resource : [],
207
- ...Array.isArray(resource.optional_resource) ? resource.optional_resource : []
208
- ].map((id) => toNumberId(id)).filter((id) => id !== void 0),
209
- resourceType: resource.type || ((_a2 = resource.select_type) == null ? void 0 : _a2.type) || "single",
210
- required: resource.status === 1,
211
- capacityRequired: Number(((_b = product.capacity) == null ? void 0 : _b.default) || product.capacity || 1) || 1
334
+ id: String(formId ?? resource.code ?? `requirement-${index}`),
335
+ formId,
336
+ title: pickLocalizedText(resource.title || resource.name || resource.main_field),
337
+ code: resource.code,
338
+ resourceType,
339
+ required: Boolean(required),
340
+ min,
341
+ max,
342
+ resourceIds,
343
+ capacityRequired: Math.max(1, Number(
344
+ resource.capacity_required ?? ((_c = product.capacity) == null ? void 0 : _c.default) ?? product.capacity ?? 1
345
+ ) || 1),
346
+ raw: resource
212
347
  };
213
348
  });
214
349
  }
215
- function normalizeProductForPlanner(product) {
350
+ function normalizeScheduleRepeatType(value) {
351
+ if (value === "daily" || value === 1 || value === "1")
352
+ return "daily";
353
+ if (value === "weekly" || value === 2 || value === "2")
354
+ return "weekly";
355
+ return "none";
356
+ }
357
+ function normalizeAbsoluteSessionRange(params) {
358
+ const fixedOffsetMinutes = params.fixedOffsetMinutes ?? (0, import_localClock.captureRuntimeOffsetMinutes)();
359
+ const range = normalizeAvailabilityLocalDateTimeRange({
360
+ startAt: params.startAt,
361
+ endAt: params.endAt,
362
+ fixedOffsetMinutes,
363
+ rollOverEnd: true
364
+ });
365
+ if (!range)
366
+ return null;
367
+ return {
368
+ startAt: range.startAt,
369
+ endAt: range.endAt,
370
+ scheduleRefs: [{
371
+ scheduleId: params.scheduleId,
372
+ timeSlotId: params.timeSlotId,
373
+ source: params.source
374
+ }]
375
+ };
376
+ }
377
+ function normalizeScheduleItem(schedule, fixedOffsetMinutes) {
216
378
  var _a, _b;
379
+ const rules = [];
380
+ const ranges = [];
381
+ const scheduleId = toNumberId(schedule.id ?? schedule.schedule_id);
382
+ const repeatType = normalizeScheduleRepeatType(schedule.repeat_type);
383
+ const repeatRule = schedule.repeat_rule || {};
384
+ const startAt = schedule.start_at || schedule.start || schedule.start_time;
385
+ const endAt = schedule.end_at || schedule.end || schedule.end_time;
386
+ const startDate = startAt ? String(startAt).slice(0, 10) : void 0;
387
+ const configuredEndDate = ((_a = repeatRule.end) == null ? void 0 : _a.type) === "date" ? (_b = repeatRule.end) == null ? void 0 : _b.end_date : void 0;
388
+ const endDate = configuredEndDate ? String(configuredEndDate).slice(0, 10) : void 0;
389
+ const includedDates = expandScheduleDateRanges(repeatRule.included_date);
390
+ const excludedDates = expandScheduleDateRanges(repeatRule.excluded_date);
391
+ const addRule = (params) => {
392
+ const mode = repeatType === "weekly" ? "weekly" : "daily";
393
+ rules.push({
394
+ id: `${String(scheduleId ?? "schedule")}:${params.idSuffix}`,
395
+ mode,
396
+ startTime: params.startTime.slice(0, 5),
397
+ endTime: params.endTime.slice(0, 5),
398
+ startDate,
399
+ endDate,
400
+ frequency: Math.max(1, Number(repeatRule.frequency || 1)),
401
+ weekdays: repeatType === "weekly" ? (repeatRule.frequency_date || []).map(Number).filter(Number.isFinite) : void 0,
402
+ includeDates: includedDates,
403
+ excludeDates: excludedDates,
404
+ scheduleRef: {
405
+ scheduleId,
406
+ timeSlotId: params.timeSlotId,
407
+ source: `schedule_${schedule.type || "standard"}`
408
+ }
409
+ });
410
+ };
411
+ if (schedule.type === "designation") {
412
+ const designations = Array.isArray(schedule.designation) ? schedule.designation : schedule.designation ? [schedule.designation] : [];
413
+ designations.forEach((designation) => {
414
+ const range = normalizeAbsoluteSessionRange({
415
+ startAt: `${designation.start_date} ${designation.start_time}`,
416
+ endAt: `${designation.end_date || designation.start_date} ${designation.end_time}`,
417
+ scheduleId,
418
+ source: "schedule_designation",
419
+ fixedOffsetMinutes
420
+ });
421
+ if (range)
422
+ ranges.push(range);
423
+ });
424
+ return { rules, ranges };
425
+ }
426
+ if (schedule.type === "time-slots" && Array.isArray(schedule.time_slot)) {
427
+ schedule.time_slot.forEach((slot, index) => {
428
+ if (!slot.start_time || !slot.end_time)
429
+ return;
430
+ if (repeatType === "none" && startDate) {
431
+ const range = normalizeAbsoluteSessionRange({
432
+ startAt: `${startDate} ${slot.start_time}`,
433
+ endAt: `${startDate} ${slot.end_time}`,
434
+ scheduleId,
435
+ timeSlotId: toNumberId(slot.id),
436
+ source: "schedule_time_slot",
437
+ fixedOffsetMinutes
438
+ });
439
+ if (range)
440
+ ranges.push(range);
441
+ return;
442
+ }
443
+ addRule({
444
+ idSuffix: `slot-${String(slot.id ?? index)}`,
445
+ startTime: slot.start_time,
446
+ endTime: slot.end_time,
447
+ timeSlotId: toNumberId(slot.id)
448
+ });
449
+ });
450
+ return { rules, ranges };
451
+ }
452
+ if (startAt && endAt) {
453
+ if (repeatType === "none") {
454
+ const range = normalizeAbsoluteSessionRange({
455
+ startAt,
456
+ endAt,
457
+ scheduleId,
458
+ source: "schedule_standard",
459
+ fixedOffsetMinutes
460
+ });
461
+ if (range)
462
+ ranges.push(range);
463
+ } else {
464
+ try {
465
+ addRule({
466
+ idSuffix: "standard",
467
+ startTime: (0, import_localClock.formatLocalTime)(String(startAt), false),
468
+ endTime: (0, import_localClock.formatLocalTime)(String(endAt), false)
469
+ });
470
+ } catch {
471
+ }
472
+ }
473
+ }
474
+ return { rules, ranges };
475
+ }
476
+ function resolveProductScheduleItems(product, scheduleList) {
477
+ const scheduleMap = new Map(scheduleList.map((schedule) => [String(schedule.id), schedule]));
478
+ const rawSchedules = [product.schedules, product.schedule, product._schedule].flatMap((value) => Array.isArray(value) ? value : value ? [value] : []);
479
+ const explicitIds = uniqueAvailabilityIds([
480
+ ...Array.isArray(product["schedule.ids"]) ? product["schedule.ids"] : [],
481
+ ...Array.isArray(product.schedule_ids) ? product.schedule_ids : [],
482
+ ...rawSchedules.filter((item) => typeof item !== "object")
483
+ ]);
484
+ const result = /* @__PURE__ */ new Map();
485
+ rawSchedules.filter((item) => item && typeof item === "object").forEach((schedule) => {
486
+ result.set(String(schedule.id ?? JSON.stringify(schedule)), schedule);
487
+ });
488
+ explicitIds.forEach((id) => {
489
+ const schedule = scheduleMap.get(String(id));
490
+ if (schedule)
491
+ result.set(String(id), schedule);
492
+ });
493
+ return Array.from(result.values());
494
+ }
495
+ function normalizeProductForAvailability(product, scheduleList = [], fixedOffsetMinutes = (0, import_localClock.captureRuntimeOffsetMinutes)()) {
496
+ var _a, _b, _c, _d, _e;
217
497
  const raw = product;
218
498
  const kind = resolveProductKind(raw);
499
+ const sessionRules = [];
500
+ const sessionRanges = [];
501
+ const explicitSession = ((_a = raw.cartDetailValue) == null ? void 0 : _a.session) || ((_c = (_b = raw._extend) == null ? void 0 : _b.other) == null ? void 0 : _c.session) || raw.session;
502
+ const explicitRange = normalizeAbsoluteSessionRange({
503
+ startAt: explicitSession == null ? void 0 : explicitSession.start_at,
504
+ endAt: explicitSession == null ? void 0 : explicitSession.end_at,
505
+ scheduleId: toNumberId(explicitSession == null ? void 0 : explicitSession.schedule_id),
506
+ timeSlotId: toNumberId(explicitSession == null ? void 0 : explicitSession.time_slot_id),
507
+ source: "product_session",
508
+ fixedOffsetMinutes
509
+ });
510
+ if (explicitRange)
511
+ sessionRanges.push(explicitRange);
512
+ resolveProductScheduleItems(raw, scheduleList).forEach((schedule) => {
513
+ const normalized = normalizeScheduleItem(schedule, fixedOffsetMinutes);
514
+ sessionRules.push(...normalized.rules);
515
+ sessionRanges.push(...normalized.ranges);
516
+ });
219
517
  return {
220
518
  id: raw.id ?? raw.product_id,
221
519
  title: pickLocalizedText(raw.title || raw.name || raw.product_title),
222
520
  kind,
223
- durationMinutes: Number(((_a = raw.duration) == null ? void 0 : _a.value) || raw.duration || raw.service_duration || 30) || 30,
224
- schedule: normalizeSchedule(raw),
225
- resourceRequirements: normalizeRequirements(raw),
226
- capacityRequired: Number(((_b = raw.capacity) == null ? void 0 : _b.default) || raw.capacity || 1) || 1,
227
- quantity: Number(raw.num || raw.quantity || 1) || 1,
228
- price: raw.price ?? raw.selling_price ?? raw.base_price,
521
+ durationMinutes: kind === "duration" ? Math.max(1, Number(((_d = raw.duration) == null ? void 0 : _d.value) || raw.duration || raw.service_duration || 30) || 30) : void 0,
522
+ requirementGroups: normalizeRequirementGroups(raw),
523
+ sessionRules,
524
+ sessionRanges,
525
+ capacityRequired: Math.max(1, Number(((_e = raw.capacity) == null ? void 0 : _e.default) || raw.capacity || 1) || 1),
526
+ quantity: Math.max(1, Number(raw.num || raw.quantity || 1) || 1),
527
+ cutOffPolicy: normalizeAvailabilityCutOffPolicy(raw.cut_off_time),
229
528
  raw
230
529
  };
231
530
  }
232
- function normalizeResourceType(value) {
233
- if (value === "multiple" || value === "capacity")
234
- return value;
235
- return "single";
531
+ function normalizeAvailabilityCutOffNumber(value, context) {
532
+ if (value == null) {
533
+ return { reason: `${context} 缺失` };
534
+ }
535
+ if (typeof value === "string" && !value.trim()) {
536
+ return { reason: `${context} 缺失` };
537
+ }
538
+ const normalized = Number(value);
539
+ if (!Number.isFinite(normalized) || normalized < 0) {
540
+ return { reason: `${context} 必须是非负有限数` };
541
+ }
542
+ return { value: normalized };
236
543
  }
237
- function normalizeResource(raw) {
238
- var _a, _b;
544
+ function normalizeAvailabilityCutOffInteger(value, context) {
545
+ const numeric = normalizeAvailabilityCutOffNumber(value, context);
546
+ if (numeric.reason)
547
+ return numeric;
548
+ if (!Number.isInteger(numeric.value)) {
549
+ return { reason: `${context} 必须是非负整数` };
550
+ }
551
+ return { value: numeric.value };
552
+ }
553
+ function invalidAvailabilityCutOffPolicy(reason) {
554
+ return { type: "invalid", reason };
555
+ }
556
+ function normalizeAvailabilityFutureDay(raw) {
557
+ if (!Object.prototype.hasOwnProperty.call(raw, "future_day"))
558
+ return {};
559
+ const futureDay = normalizeAvailabilityCutOffInteger(raw.future_day, "cut_off_time.future_day");
560
+ return futureDay.reason ? { reason: futureDay.reason } : { futureDay: futureDay.value };
561
+ }
562
+ function normalizeAvailabilityCutOffPolicy(raw) {
563
+ if (raw == null || raw === "")
564
+ return void 0;
565
+ if (!isRecord(raw)) {
566
+ return invalidAvailabilityCutOffPolicy("cut_off_time 必须是对象");
567
+ }
568
+ const futureDay = normalizeAvailabilityFutureDay(raw);
569
+ if (futureDay.reason) {
570
+ return invalidAvailabilityCutOffPolicy(futureDay.reason);
571
+ }
572
+ const type = String(raw.type ?? "").trim();
573
+ if (!type) {
574
+ return invalidAvailabilityCutOffPolicy("cut_off_time.type 缺失");
575
+ }
576
+ if (type === "ongoing") {
577
+ if (!isRecord(raw.ongoing)) {
578
+ return invalidAvailabilityCutOffPolicy("cut_off_time.ongoing 缺失");
579
+ }
580
+ const ongoingType = String(raw.ongoing.type ?? "").trim();
581
+ if (ongoingType === "before_end") {
582
+ return {
583
+ type: "ongoing",
584
+ mode: "before_end",
585
+ ...futureDay.futureDay === void 0 ? {} : { futureDay: futureDay.futureDay }
586
+ };
587
+ }
588
+ if (ongoingType === "after_start") {
589
+ const graceMinutes = normalizeAvailabilityCutOffNumber(
590
+ raw.ongoing.unit,
591
+ "cut_off_time.ongoing.unit"
592
+ );
593
+ return graceMinutes.reason ? invalidAvailabilityCutOffPolicy(graceMinutes.reason) : {
594
+ type: "ongoing",
595
+ mode: "after_start",
596
+ graceMinutes: graceMinutes.value,
597
+ ...futureDay.futureDay === void 0 ? {} : { futureDay: futureDay.futureDay }
598
+ };
599
+ }
600
+ return invalidAvailabilityCutOffPolicy(`未知的 cut_off_time.ongoing.type: ${ongoingType}`);
601
+ }
602
+ if (type === "before_start") {
603
+ return {
604
+ type: "before_start",
605
+ ...futureDay.futureDay === void 0 ? {} : { futureDay: futureDay.futureDay }
606
+ };
607
+ }
608
+ if (type === "advance") {
609
+ const unit = normalizeAvailabilityCutOffNumber(raw.unit, "cut_off_time.unit");
610
+ if (unit.reason)
611
+ return invalidAvailabilityCutOffPolicy(unit.reason);
612
+ const unitType = String(raw.unit_type ?? "").trim();
613
+ if (!["days", "hours", "minutes"].includes(unitType)) {
614
+ return invalidAvailabilityCutOffPolicy("cut_off_time.unit_type 必须是 days|hours|minutes");
615
+ }
616
+ return {
617
+ type: "advance",
618
+ unit: unit.value,
619
+ unitType,
620
+ ...futureDay.futureDay === void 0 ? {} : { futureDay: futureDay.futureDay }
621
+ };
622
+ }
623
+ return invalidAvailabilityCutOffPolicy(`未知的 cut_off_time.type: ${type}`);
624
+ }
625
+ function normalizeResource(raw, fixedOffsetMinutes = (0, import_localClock.captureRuntimeOffsetMinutes)()) {
239
626
  const id = raw.resourceId ?? raw.resource_id ?? raw.id;
240
627
  if (id === void 0 || id === null || id === "")
241
628
  return null;
242
- const combinedResourceIds = (_a = raw.combined_resource) == null ? void 0 : _a.resource_ids;
629
+ const rawWindows = raw.windows || raw.times || [];
243
630
  return {
244
631
  id,
245
632
  formId: raw.formId ?? raw.form_id ?? raw.resource_form_id,
246
633
  name: raw.resourceName || raw.main_field || raw.name || raw.title,
247
- resourceType: normalizeResourceType(raw.type || raw.resourceType || ((_b = raw.select_type) == null ? void 0 : _b.type)),
248
- capacity: Number(raw.capacity || raw.max_capacity || 1) || 1,
249
- times: Array.isArray(raw.times) ? raw.times : [],
250
- childResourceIds: Array.isArray(combinedResourceIds) ? combinedResourceIds : void 0,
634
+ code: raw.code,
635
+ resourceType: normalizeResourceType(raw.type || raw.resourceType),
636
+ capacity: Math.max(1, Number(raw.capacity || raw.max_capacity || 1) || 1),
637
+ windows: (Array.isArray(rawWindows) ? rawWindows : []).flatMap((window) => {
638
+ const range = normalizeAvailabilityLocalDateTimeRange({
639
+ startAt: window.startAt || window.start_at || window.start,
640
+ endAt: window.endAt || window.end_at || window.end,
641
+ fixedOffsetMinutes,
642
+ rollOverEnd: true
643
+ });
644
+ if (!range)
645
+ return [];
646
+ return [{ startAt: range.startAt, endAt: range.endAt, source: window.source, raw: window }];
647
+ }),
251
648
  raw
252
649
  };
253
650
  }
@@ -255,97 +652,81 @@ function normalizeScheduleIds(raw) {
255
652
  const ids = raw.schedule ?? raw.schedule_ids ?? raw.scheduleIds ?? [];
256
653
  return (Array.isArray(ids) ? ids : [ids]).map((id) => toNumberId(id)).filter((id) => id !== void 0);
257
654
  }
258
- function collectRawScheduleIds(rawResources) {
259
- const ids = /* @__PURE__ */ new Map();
260
- (rawResources || []).forEach((resource) => {
261
- normalizeScheduleIds(resource).forEach((id) => {
262
- ids.set(String(id), id);
263
- });
264
- });
265
- return Array.from(ids.values());
266
- }
267
- function scheduleListHasIds(scheduleList, scheduleIds) {
268
- if (!scheduleIds.length)
269
- return true;
270
- const existingIds = new Set((scheduleList || []).map((schedule) => String(schedule.id)));
271
- return scheduleIds.every((id) => existingIds.has(String(id)));
272
- }
273
655
  function dateRangeDays(dateRange) {
274
- if (!(dateRange == null ? void 0 : dateRange.start) || !(dateRange == null ? void 0 : dateRange.end))
656
+ if (!(dateRange == null ? void 0 : dateRange.startDate) || !(dateRange == null ? void 0 : dateRange.endDate))
275
657
  return [];
276
658
  const dates = [];
277
- let cursor = (0, import_dayjs.default)(dateRange.start);
278
- const end = (0, import_dayjs.default)(dateRange.end);
279
- if (!cursor.isValid() || !end.isValid() || end.isBefore(cursor, "day"))
659
+ const startScalar = (0, import_localClock.localDateToScalarStart)((0, import_localClock.parseLocalDate)(dateRange.startDate));
660
+ const endScalar = (0, import_localClock.localDateToScalarStart)((0, import_localClock.parseLocalDate)(dateRange.endDate));
661
+ if (endScalar < startScalar)
280
662
  return dates;
281
- while (cursor.isSame(end, "day") || cursor.isBefore(end, "day")) {
282
- dates.push(cursor.format("YYYY-MM-DD"));
283
- cursor = cursor.add(1, "day");
663
+ for (let cursor = startScalar; cursor <= endScalar; cursor = (0, import_localClock.addLocalCalendarDays)(cursor, 1)) {
664
+ dates.push((0, import_localClock.formatLocalDate)(cursor));
284
665
  }
285
666
  return dates;
286
667
  }
287
- function normalizePlannerEvent(rawEvent, resourceId) {
288
- const startAt = rawEvent.start_at ?? rawEvent.startAt ?? rawEvent.start_time ?? rawEvent.startTime;
289
- const endAt = rawEvent.end_at ?? rawEvent.endAt ?? rawEvent.end_time ?? rawEvent.endTime;
290
- if (!startAt || !endAt)
668
+ function normalizeResourceOccupancy(rawEvent, resourceId, source = "remote", fixedOffsetMinutes = (0, import_localClock.captureRuntimeOffsetMinutes)()) {
669
+ var _a, _b;
670
+ const range = normalizeAvailabilityLocalDateTimeRange({
671
+ startAt: rawEvent.start_at ?? rawEvent.startAt ?? rawEvent.start_time ?? rawEvent.startTime,
672
+ endAt: rawEvent.end_at ?? rawEvent.endAt ?? rawEvent.end_time ?? rawEvent.endTime,
673
+ fixedOffsetMinutes,
674
+ rollOverEnd: false
675
+ });
676
+ if (!range)
291
677
  return null;
292
- const rawSource = rawEvent.source;
293
- const source = rawSource === "remote" || rawSource === "cart" || rawSource === "selection" ? rawSource : "remote";
294
678
  return {
295
- ...rawEvent,
679
+ id: rawEvent.id == null ? void 0 : String(rawEvent.id),
296
680
  resourceId,
297
- resource_id: rawEvent.resource_id ?? rawEvent.resourceId ?? resourceId,
298
- start_at: startAt,
299
- end_at: endAt,
681
+ startAt: range.startAt,
682
+ endAt: range.endAt,
300
683
  pax: Number(rawEvent.pax ?? rawEvent.number ?? rawEvent.capacity ?? 1) || 1,
301
- source
684
+ source: rawEvent.source === "cart" || rawEvent.source === "selection" ? rawEvent.source : source,
685
+ scheduleEventId: toNumberId(rawEvent.schedule_event_id ?? rawEvent.scheduleEventId),
686
+ serverEventId: toNumberId(rawEvent.schedule_event_id ?? rawEvent.server_event_id ?? rawEvent.id),
687
+ bookingUid: rawEvent.booking_uid ?? rawEvent.bookingUid ?? ((_a = rawEvent.metadata) == null ? void 0 : _a.unique_identification_number),
688
+ productUid: rawEvent.product_uid ?? rawEvent.productUid ?? ((_b = rawEvent.metadata) == null ? void 0 : _b.product_uid),
689
+ raw: rawEvent
302
690
  };
303
691
  }
304
- function normalizeResourceV2(raw) {
305
- const normalized = normalizeResource({
306
- ...raw,
307
- times: []
308
- });
309
- if (!normalized)
310
- return null;
311
- return normalized;
312
- }
313
692
  function buildResourceWindowFallbackTimes(params) {
314
693
  const { raw, dates, scheduleIds } = params;
315
- const rawStart = raw.start_time || raw.start_at || raw.startTime;
316
- const rawEnd = raw.end_time || raw.end_at || raw.endTime;
317
- if (!rawStart || !rawEnd)
318
- return [];
319
- const startBoundary = (0, import_dayjs.default)(rawStart);
320
- const endBoundary = (0, import_dayjs.default)(rawEnd);
321
- if (!startBoundary.isValid() || !endBoundary.isValid() || !endBoundary.isAfter(startBoundary)) {
694
+ const fixedOffsetMinutes = params.fixedOffsetMinutes ?? (0, import_localClock.captureRuntimeOffsetMinutes)();
695
+ const boundary = normalizeAvailabilityLocalDateTimeRange({
696
+ startAt: raw.start_time || raw.start_at || raw.startTime,
697
+ endAt: raw.end_time || raw.end_at || raw.endTime,
698
+ fixedOffsetMinutes,
699
+ rollOverEnd: true
700
+ });
701
+ if (!boundary)
322
702
  return [];
323
- }
324
703
  return dates.flatMap((date) => {
325
- const dayStart = (0, import_dayjs.default)(`${date} 00:00:00`);
326
- const dayEnd = (0, import_dayjs.default)(`${date} 23:59:59`);
327
- const start = startBoundary.isAfter(dayStart) ? startBoundary : dayStart;
328
- const end = endBoundary.isBefore(dayEnd) ? endBoundary : dayEnd;
329
- if (!end.isAfter(start))
704
+ const dayStart = (0, import_localClock.localDateTimeToScalar)((0, import_localClock.parseLocalDateTime)(`${date}T00:00:00`));
705
+ const dayEnd = (0, import_localClock.localDateTimeToScalar)((0, import_localClock.parseLocalDateTime)(`${date}T23:59:59`));
706
+ const start = Math.max(boundary.startScalar, dayStart);
707
+ const end = Math.min(boundary.endScalar, dayEnd);
708
+ if (end <= start)
330
709
  return [];
331
710
  return [{
332
- start_at: start.format("YYYY-MM-DD HH:mm:ss"),
333
- end_at: end.format("YYYY-MM-DD HH:mm:ss"),
711
+ start_at: (0, import_localClock.formatLocalDateTime)(start, fixedOffsetMinutes),
712
+ end_at: (0, import_localClock.formatLocalDateTime)(end, fixedOffsetMinutes),
334
713
  schedule_ids: scheduleIds,
335
714
  source: "resource_window_fallback"
336
715
  }];
337
716
  });
338
717
  }
339
- function buildPlannerResourcesFromV2(params) {
718
+ function buildAvailabilityResourcesFromV2(params) {
340
719
  const scheduleMap = new Map(
341
720
  (params.scheduleList || []).map((schedule) => [String(schedule.id), schedule])
342
721
  );
343
722
  const dates = dateRangeDays(params.dateRange);
344
- const externalEvents = [];
723
+ const resourceOccupancies = [];
724
+ const fixedOffsetMinutes = params.fixedOffsetMinutes ?? (0, import_localClock.captureRuntimeOffsetMinutes)();
345
725
  const resources = (params.rawResources || []).map((raw) => {
346
- const resource = normalizeResourceV2(raw);
726
+ const resource = normalizeResource(raw, fixedOffsetMinutes);
347
727
  if (!resource)
348
728
  return null;
729
+ const explicitWindows = [...resource.windows];
349
730
  const scheduleIds = normalizeScheduleIds(raw);
350
731
  const schedules = scheduleIds.map((id) => scheduleMap.get(String(id))).filter(Boolean);
351
732
  const matchedScheduleIds = new Set(schedules.map((schedule) => String(schedule.id)));
@@ -353,35 +734,53 @@ function buildPlannerResourcesFromV2(params) {
353
734
  if (missingScheduleIds.length > 0) {
354
735
  resource.raw = {
355
736
  ...resource.raw || raw,
356
- _plannerMissingScheduleIds: missingScheduleIds
737
+ _availabilityMissingScheduleIds: missingScheduleIds
357
738
  };
358
739
  }
359
- resource.times = dates.flatMap((date) => (0, import_getDateIsInSchedule.getScheduleStartEndTimePoints)(date, schedules).map((slot) => ({
360
- start_at: (0, import_dayjs.default)(slot.start_at).format("YYYY-MM-DD HH:mm:ss"),
361
- end_at: (0, import_dayjs.default)(slot.end_at).format("YYYY-MM-DD HH:mm:ss"),
362
- schedule_ids: scheduleIds
740
+ const materializedWindows = dates.flatMap((date) => schedules.flatMap((schedule) => (0, import_getDateIsInSchedule.getScheduleStartEndTimePoints)(date, [schedule]).flatMap((slot) => {
741
+ const range = normalizeAvailabilityLocalDateTimeRange({
742
+ startAt: slot.start_at,
743
+ endAt: slot.end_at,
744
+ fixedOffsetMinutes,
745
+ rollOverEnd: true
746
+ });
747
+ if (!range)
748
+ return [];
749
+ return [{
750
+ startAt: range.startAt,
751
+ endAt: range.endAt,
752
+ source: `resource_schedule:${schedule.id}`,
753
+ raw: { scheduleId: schedule.id }
754
+ }];
363
755
  })));
364
- if (resource.times.length === 0 && missingScheduleIds.length > 0) {
365
- resource.times = buildResourceWindowFallbackTimes({
756
+ resource.windows = materializedWindows.length > 0 ? materializedWindows : explicitWindows;
757
+ if (resource.windows.length === 0 && missingScheduleIds.length > 0) {
758
+ resource.windows = buildResourceWindowFallbackTimes({
366
759
  raw,
367
760
  dates,
368
- scheduleIds
369
- });
761
+ scheduleIds,
762
+ fixedOffsetMinutes
763
+ }).map((window) => ({
764
+ startAt: window.start_at,
765
+ endAt: window.end_at,
766
+ source: window.source,
767
+ raw: window
768
+ }));
370
769
  }
371
770
  const eventList = Array.isArray(raw.event_list) ? raw.event_list : [];
372
771
  eventList.forEach((event) => {
373
- const normalizedEvent = normalizePlannerEvent(event, resource.id);
374
- if (normalizedEvent)
375
- externalEvents.push(normalizedEvent);
772
+ const occupancy = normalizeResourceOccupancy(event, resource.id, "remote", fixedOffsetMinutes);
773
+ if (occupancy)
774
+ resourceOccupancies.push(occupancy);
376
775
  });
377
776
  return resource;
378
777
  }).filter((resource) => resource !== null);
379
778
  return {
380
779
  resources,
381
- externalEvents
780
+ resourceOccupancies
382
781
  };
383
782
  }
384
- function collectPlannerResourceIds(products, explicitResourceIds = []) {
783
+ function collectAvailabilityResourceIds(products, explicitResourceIds = []) {
385
784
  const ids = /* @__PURE__ */ new Map();
386
785
  const add = (value) => {
387
786
  if (value === void 0 || value === null || value === "")
@@ -391,76 +790,199 @@ function collectPlannerResourceIds(products, explicitResourceIds = []) {
391
790
  explicitResourceIds.forEach(add);
392
791
  products.forEach((product) => {
393
792
  var _a;
394
- (_a = product.resourceRequirements) == null ? void 0 : _a.forEach((requirement) => {
793
+ (_a = product.requirementGroups) == null ? void 0 : _a.forEach((requirement) => {
395
794
  var _a2;
396
795
  (_a2 = requirement.resourceIds) == null ? void 0 : _a2.forEach(add);
397
796
  });
398
797
  });
399
798
  return Array.from(ids.values());
400
799
  }
401
- function samePlannerId(left, right) {
402
- if (left === void 0 || right === void 0)
403
- return false;
404
- return String(left) === String(right);
800
+ function getProductLineUid(product) {
801
+ var _a;
802
+ return String(
803
+ ((_a = product == null ? void 0 : product.metadata) == null ? void 0 : _a.unique_identification_number) ?? (product == null ? void 0 : product.unique_identification_number) ?? ""
804
+ );
405
805
  }
406
- function findRequirementForAssignment(product, assignment) {
407
- const requirements = product.resourceRequirements || [];
408
- return requirements.find((requirement) => (requirement.resourceIds || []).some((id) => samePlannerId(id, assignment.resourceId))) || (requirements.length === 1 ? requirements[0] : void 0) || requirements.find((requirement) => requirement.required) || requirements[0];
806
+ function buildAvailabilityLineIdentity(product) {
807
+ return {
808
+ product_id: product.product_id,
809
+ product_variant_id: product.product_variant_id ?? 0,
810
+ unique_identification_number: getProductLineUid(product),
811
+ product_sku: product.product_sku,
812
+ product_bundle: product.product_bundle
813
+ };
409
814
  }
410
- function buildBookingFromAssignment(params) {
411
- return buildBookingFromAssignments({
815
+ function buildBookingFromAvailabilityAssignment(params) {
816
+ return buildBookingFromAvailabilityAssignments({
817
+ product: params.product,
412
818
  assignments: [params.assignment],
819
+ timezone: params.timezone,
820
+ fixedOffsetMinutes: params.fixedOffsetMinutes
821
+ });
822
+ }
823
+ function buildBookingFromAvailabilityAssignments(params) {
824
+ const normalizedAssignments = (params.assignments || []).map((assignment) => {
825
+ const resourceId = assignment.resourceId ?? assignment.resource_id;
826
+ const matchedGroup = (params.product.requirementGroups || []).find((group) => {
827
+ var _a;
828
+ return (_a = group.resourceIds) == null ? void 0 : _a.some((id) => sameAvailabilityId(id, resourceId));
829
+ });
830
+ return {
831
+ productId: assignment.productId ?? assignment.product_id,
832
+ resourceId,
833
+ formId: assignment.formId ?? assignment.form_id ?? (matchedGroup == null ? void 0 : matchedGroup.formId),
834
+ startAt: assignment.startAt ?? assignment.start_at,
835
+ endAt: assignment.endAt ?? assignment.end_at,
836
+ capacityRequired: assignment.capacityRequired ?? assignment.capacity_required ?? (matchedGroup == null ? void 0 : matchedGroup.capacityRequired),
837
+ scheduleId: assignment.scheduleId ?? assignment.schedule_id,
838
+ timeSlotId: assignment.timeSlotId ?? assignment.time_slot_id
839
+ };
840
+ });
841
+ const first = normalizedAssignments[0];
842
+ const fixedOffsetMinutes = params.fixedOffsetMinutes ?? (0, import_localClock.captureRuntimeOffsetMinutes)();
843
+ const range = normalizeAvailabilityLocalDateTimeRange({
844
+ startAt: first.startAt,
845
+ endAt: first.endAt,
846
+ fixedOffsetMinutes,
847
+ rollOverEnd: false
848
+ });
849
+ if (!range) {
850
+ throw new import_ResourcePlanner.AvailabilityError("INVALID_TARGET_FILTER", "assignment 时间范围非法", {
851
+ assignments: params.assignments
852
+ });
853
+ }
854
+ const candidate = {
855
+ key: `${String(first.productId)}:${range.startAt}:${range.endAt}`,
856
+ productId: first.productId,
857
+ source: "duration_window",
858
+ startAt: range.startAt,
859
+ endAt: range.endAt,
860
+ bookingStartAt: range.startAt,
861
+ bookingEndAt: range.endAt,
862
+ date: range.startAt.slice(0, 10),
863
+ startTime: (0, import_localClock.formatLocalTime)(range.startAt, false),
864
+ endTime: (0, import_localClock.formatLocalTime)(range.endAt, false),
865
+ timezone: String(params.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || "runtime"),
866
+ scheduleRefs: [],
867
+ primaryScheduleRef: void 0
868
+ };
869
+ return buildBookingFromAvailabilitySelection({
870
+ assignments: normalizedAssignments,
871
+ candidate,
413
872
  product: params.product
414
873
  });
415
874
  }
416
- function buildBookingFromAssignments(params) {
417
- const { assignments, product } = params;
418
- const firstAssignment = assignments[0];
419
- const start = (0, import_dayjs.default)(firstAssignment.start_at);
420
- const end = (0, import_dayjs.default)(firstAssignment.end_at);
875
+ function getBookingUid(booking) {
876
+ var _a;
877
+ return String(
878
+ ((_a = booking == null ? void 0 : booking.metadata) == null ? void 0 : _a.unique_identification_number) ?? (booking == null ? void 0 : booking.booking_uid) ?? ""
879
+ );
880
+ }
881
+ function stableValue(value) {
882
+ if (Array.isArray(value))
883
+ return value.map(stableValue);
884
+ if (!value || typeof value !== "object")
885
+ return value;
886
+ return Object.keys(value).sort().reduce((result, key) => {
887
+ if (value[key] !== void 0 && typeof value[key] !== "function") {
888
+ result[key] = stableValue(value[key]);
889
+ }
890
+ return result;
891
+ }, {});
892
+ }
893
+ function hashText(value) {
894
+ let hash = 2166136261;
895
+ for (let index = 0; index < value.length; index += 1) {
896
+ hash ^= value.charCodeAt(index);
897
+ hash = Math.imul(hash, 16777619);
898
+ }
899
+ return (hash >>> 0).toString(36);
900
+ }
901
+ function resolveOrderRevision(tempOrder) {
902
+ if (!tempOrder)
903
+ return "order:none";
904
+ const products = (Array.isArray(tempOrder.products) ? tempOrder.products : []).map((product) => {
905
+ var _a;
906
+ return {
907
+ uid: getProductLineUid(product),
908
+ productId: product.product_id ?? product.id,
909
+ variantId: product.product_variant_id ?? 0,
910
+ num: product.num ?? product.quantity ?? 1,
911
+ bookingUid: product.booking_uid ?? ((_a = product.metadata) == null ? void 0 : _a.booking_uid),
912
+ productSku: product.product_sku,
913
+ productBundle: product.product_bundle
914
+ };
915
+ }).sort((left, right) => left.uid.localeCompare(right.uid));
916
+ const bookings = (Array.isArray(tempOrder.bookings) ? tempOrder.bookings : []).map((booking) => ({
917
+ uid: getBookingUid(booking),
918
+ scheduleEventId: booking.schedule_event_id,
919
+ productUid: booking.product_uid,
920
+ scheduleId: booking.schedule_id,
921
+ startDate: booking.start_date,
922
+ startTime: booking.start_time,
923
+ endDate: booking.end_date,
924
+ endTime: booking.end_time,
925
+ resources: booking.resources
926
+ })).sort((left, right) => left.uid.localeCompare(right.uid));
927
+ return `order:${hashText(JSON.stringify(stableValue({ products, bookings })))}`;
928
+ }
929
+ function buildBookingFromAvailabilitySelection(params) {
930
+ var _a, _b, _c, _d;
931
+ const {
932
+ assignments,
933
+ candidate,
934
+ product,
935
+ resources = [],
936
+ bookingUid
937
+ } = params;
938
+ const bookingStartAt = candidate.bookingStartAt || candidate.startAt;
939
+ const bookingEndAt = candidate.bookingEndAt || candidate.endAt;
940
+ const startScalar = (0, import_localClock.localDateTimeToScalar)((0, import_localClock.parseLocalDateTime)(bookingStartAt));
941
+ const endScalar = (0, import_localClock.localDateTimeToScalar)((0, import_localClock.parseLocalDateTime)(bookingEndAt));
942
+ const scheduleId = ((_a = candidate.primaryScheduleRef) == null ? void 0 : _a.scheduleId) ?? ((_b = assignments.find((assignment) => assignment.scheduleId != null)) == null ? void 0 : _b.scheduleId);
943
+ const timeSlotId = ((_c = candidate.primaryScheduleRef) == null ? void 0 : _c.timeSlotId) ?? ((_d = assignments.find((assignment) => assignment.timeSlotId != null)) == null ? void 0 : _d.timeSlotId);
944
+ if (product.kind === "session" && scheduleId == null) {
945
+ throw new import_ResourcePlanner.AvailabilityError("REVALIDATION_FAILED", "session 候选缺少真实 schedule identity", {
946
+ productId: product.id,
947
+ candidateKey: candidate.key,
948
+ scheduleRefs: candidate.scheduleRefs
949
+ });
950
+ }
421
951
  return {
422
- start_date: start.format("YYYY-MM-DD"),
423
- start_time: start.format("HH:mm"),
424
- end_date: end.format("YYYY-MM-DD"),
425
- end_time: end.format("HH:mm"),
426
- duration: Math.max(1, end.diff(start, "minute")),
952
+ start_date: bookingStartAt.slice(0, 10),
953
+ start_time: bookingStartAt.slice(11, 16),
954
+ end_date: bookingEndAt.slice(0, 10),
955
+ end_time: bookingEndAt.slice(11, 16),
956
+ duration: Math.max(1, Math.trunc((endScalar - startScalar) / (60 * 1e3))),
427
957
  like_status: "common",
428
- schedule_id: 0,
958
+ schedule_id: scheduleId ?? 0,
429
959
  resources: assignments.map((assignment) => {
430
- const requirement = findRequirementForAssignment(product, assignment);
960
+ const matchedResource = resources.find((resource) => sameAvailabilityId(resource.id, assignment.resourceId));
961
+ const resourceName = String((matchedResource == null ? void 0 : matchedResource.name) || "").trim();
431
962
  return {
432
963
  id: assignment.resourceId,
433
964
  resource_id: assignment.resourceId,
434
- form_id: requirement == null ? void 0 : requirement.formId,
965
+ form_id: assignment.formId,
435
966
  relation_type: "form",
436
967
  relation_id: assignment.resourceId,
437
968
  like_status: "common",
438
- capacity: assignment.capacityRequired || product.capacityRequired || 1
969
+ capacity: assignment.capacityRequired || 1,
970
+ ...resourceName ? { metadata: { resource_name: resourceName } } : {}
439
971
  };
440
972
  }),
441
973
  metadata: {
974
+ ...bookingUid ? { unique_identification_number: bookingUid } : {},
442
975
  unified_booking_sales: true,
443
- planner_source: product.kind
976
+ availability_candidate_key: candidate.key,
977
+ availability_source: candidate.source,
978
+ availability_slice_start_at: candidate.startAt,
979
+ availability_slice_end_at: candidate.endAt,
980
+ availability_schedule_refs: candidate.scheduleRefs,
981
+ ...timeSlotId == null ? {} : { time_slot_id: timeSlotId },
982
+ product_kind: product.kind
444
983
  }
445
984
  };
446
985
  }
447
- function buildAssignmentGroupKey(assignment) {
448
- return [
449
- assignment.productId,
450
- assignment.start_at,
451
- assignment.end_at
452
- ].join(":");
453
- }
454
- function buildPlannerLineIdentity(product) {
455
- var _a;
456
- return {
457
- product_id: product.product_id,
458
- product_variant_id: product.product_variant_id ?? 0,
459
- unique_identification_number: ((_a = product.metadata) == null ? void 0 : _a.unique_identification_number) || product.unique_identification_number,
460
- product_sku: product.product_sku,
461
- product_bundle: product.product_bundle
462
- };
463
- }
464
986
  var UnifiedBookingSalesImpl = class extends import_BookingTicket.BookingTicket {
465
987
  constructor() {
466
988
  super(...arguments);
@@ -471,12 +993,334 @@ var UnifiedBookingSalesImpl = class extends import_BookingTicket.BookingTicket {
471
993
  this.openDataTarget = null;
472
994
  this.loadOpenDataInFlight = null;
473
995
  this.loadOpenDataInFlightTarget = null;
996
+ this.availabilityRuntimeOffsetMinutes = null;
997
+ this.availabilityScheduleCatalog = null;
998
+ this.availabilityProjectionSequence = 0;
999
+ this.availabilityContextSequence = 0;
1000
+ this.availabilityLifecycleGeneration = 0;
1001
+ this.availabilityContextRegistry = /* @__PURE__ */ new Map();
1002
+ this.availabilityProjectionPool = /* @__PURE__ */ new Map();
1003
+ this.availabilityPrepareInFlight = /* @__PURE__ */ new Map();
1004
+ this.availabilityProjectionRefreshInFlight = /* @__PURE__ */ new Map();
1005
+ this.availabilityRemoteOccupancyCache = new import_remoteOccupancyCache.AvailabilityRemoteOccupancyCache(
1006
+ MAX_REMOTE_OCCUPANCY_RESOURCE_DATE_CELLS
1007
+ );
1008
+ this.availabilityRemoteResourceQueryInFlight = /* @__PURE__ */ new Map();
1009
+ this.availabilityRemoteResourceQuerySequence = 0;
1010
+ this.availabilityRemoteResourceEpoch = 0;
1011
+ this.availabilityCommitLocks = /* @__PURE__ */ new Set();
1012
+ }
1013
+ async initialize(core, options = {}) {
1014
+ var _a, _b, _c, _d;
1015
+ this.availabilityScheduleCatalog = null;
1016
+ this.openDataTarget = null;
1017
+ this.availabilityRuntimeOffsetMinutes = (0, import_localClock.captureRuntimeOffsetMinutes)();
1018
+ this.clearAvailabilityRemoteResourceState();
1019
+ await super.initialize(core, options);
1020
+ const openDataBusinessCode = String(
1021
+ ((_a = this.otherParams) == null ? void 0 : _a.businessCode) ?? ((_b = this.otherParams) == null ? void 0 : _b.business_code) ?? ""
1022
+ ).trim();
1023
+ const openDataChannel = String(((_c = this.otherParams) == null ? void 0 : _c.channel) ?? "").trim();
1024
+ if (((_d = this.otherParams) == null ? void 0 : _d.openData) && openDataBusinessCode && openDataChannel) {
1025
+ this.openDataTarget = `${openDataBusinessCode}+${openDataChannel}`;
1026
+ }
1027
+ this.captureAvailabilityScheduleCatalog();
474
1028
  }
475
1029
  getSubmitOrderSalesChannel() {
476
1030
  var _a;
477
1031
  const channel = String(((_a = this.otherParams) == null ? void 0 : _a.channel) ?? "").trim();
478
1032
  return channel || super.getSubmitOrderSalesChannel();
479
1033
  }
1034
+ createAvailabilityFacadeError(code, message, details) {
1035
+ return new import_ResourcePlanner.AvailabilityError(code, message, details);
1036
+ }
1037
+ getAvailabilityScopeDescriptor(otherParams = this.otherParams || {}) {
1038
+ return {
1039
+ cacheId: this.cacheId || "",
1040
+ businessCode: String(otherParams.businessCode ?? otherParams.business_code ?? ""),
1041
+ channel: String(otherParams.channel ?? ""),
1042
+ platform: String(otherParams.platform ?? ""),
1043
+ businessTimezone: String(otherParams.businessTimezone ?? "")
1044
+ };
1045
+ }
1046
+ normalizeAvailabilityIdList(values) {
1047
+ if (!Array.isArray(values) || values.length === 0)
1048
+ return void 0;
1049
+ const uniqueMap = /* @__PURE__ */ new Map();
1050
+ values.forEach((value) => {
1051
+ uniqueMap.set(String(value), value);
1052
+ });
1053
+ return Array.from(uniqueMap.entries()).sort(([left], [right]) => left.localeCompare(right)).map(([, value]) => value);
1054
+ }
1055
+ normalizeAvailabilityStringList(values) {
1056
+ if (!Array.isArray(values) || values.length === 0)
1057
+ return void 0;
1058
+ return Array.from(new Set(values.map(String))).sort((left, right) => left.localeCompare(right));
1059
+ }
1060
+ normalizeAvailabilityPrepareDescriptor(params) {
1061
+ var _a;
1062
+ this.assertAvailabilityPrepareParams(params);
1063
+ const timezone = String(
1064
+ params.timezone || ((_a = this.otherParams) == null ? void 0 : _a.businessTimezone) || Intl.DateTimeFormat().resolvedOptions().timeZone || "Asia/Shanghai"
1065
+ );
1066
+ assertValidAvailabilityTimezone(timezone);
1067
+ const dateRange = this.normalizeAvailabilityDateRange(params, timezone);
1068
+ const slotStepMinutes = params.slotStepMinutes || DEFAULT_AVAILABILITY_SLOT_STEP_MINUTES;
1069
+ const businessHours = params.businessHours || DEFAULT_AVAILABILITY_BUSINESS_HOURS;
1070
+ const mode = params.mode || "appointment";
1071
+ if (!isValidClockTime(businessHours.startTime) || !isValidClockTime(businessHours.endTime)) {
1072
+ throw new import_ResourcePlanner.AvailabilityError("INVALID_TARGET_FILTER", "businessHours 必须使用 HH:mm", {
1073
+ businessHours
1074
+ });
1075
+ }
1076
+ return this.cloneAvailabilityPrepareParams({
1077
+ ...params,
1078
+ productIds: this.normalizeAvailabilityIdList(params.productIds),
1079
+ resourceIds: this.normalizeAvailabilityIdList(params.resourceIds),
1080
+ editingProductLineUids: this.normalizeAvailabilityStringList(params.editingProductLineUids),
1081
+ dateRange,
1082
+ timezone,
1083
+ slotStepMinutes,
1084
+ businessHours: { ...businessHours },
1085
+ _availabilityOperatingHours: params._availabilityOperatingHours ? {
1086
+ source: params._availabilityOperatingHours.source,
1087
+ scheduleIds: [...params._availabilityOperatingHours.scheduleIds]
1088
+ } : void 0,
1089
+ mode
1090
+ });
1091
+ }
1092
+ async resolveAvailabilityPrepareDefaults(params) {
1093
+ var _a, _b, _c, _d;
1094
+ const needsOperatingHours = params.businessHours === void 0;
1095
+ const needsTimeSlice = params.slotStepMinutes === void 0;
1096
+ if (!needsOperatingHours && !needsTimeSlice) {
1097
+ return {
1098
+ ...params,
1099
+ _availabilityOperatingHours: {
1100
+ source: "prepare_business_hours",
1101
+ scheduleIds: []
1102
+ }
1103
+ };
1104
+ }
1105
+ const businessCode = String(
1106
+ ((_a = this.otherParams) == null ? void 0 : _a.businessCode) ?? ((_b = this.otherParams) == null ? void 0 : _b.business_code) ?? ""
1107
+ ).trim();
1108
+ const channel = String(((_c = this.otherParams) == null ? void 0 : _c.channel) ?? "").trim();
1109
+ const currentTarget = businessCode && channel ? `${businessCode}+${channel}` : null;
1110
+ let openData = !currentTarget || this.openDataTarget === currentTarget ? (_d = this.otherParams) == null ? void 0 : _d.openData : null;
1111
+ if (!openData && this.store.openData) {
1112
+ try {
1113
+ openData = await this.getOpenData();
1114
+ } catch (error) {
1115
+ if (needsOperatingHours) {
1116
+ throw this.createAvailabilityFacadeError(
1117
+ "OPERATING_HOURS_RESOLVE_FAILED",
1118
+ "无法读取店铺营业时间配置,请重试",
1119
+ {
1120
+ businessCode,
1121
+ channel,
1122
+ cause: error instanceof Error ? error.message : String(error)
1123
+ }
1124
+ );
1125
+ }
1126
+ console.warn("[UnifiedBookingSales] OpenData 时间切片配置加载失败,回退为默认值", error);
1127
+ }
1128
+ }
1129
+ const operatingScheduleIds = this.normalizeAvailabilityIdList(
1130
+ Array.isArray(openData == null ? void 0 : openData["availability.operating_hours"]) ? openData == null ? void 0 : openData["availability.operating_hours"] : void 0
1131
+ ) || [];
1132
+ const configuredTimeSlice = needsTimeSlice ? normalizePositiveInteger(openData == null ? void 0 : openData["reservation.time_slice"]) : void 0;
1133
+ return {
1134
+ ...params,
1135
+ ...configuredTimeSlice ? { slotStepMinutes: configuredTimeSlice } : {},
1136
+ _availabilityOperatingHours: needsOperatingHours ? {
1137
+ source: operatingScheduleIds.length > 0 ? "open_data_schedule" : "default_business_hours",
1138
+ scheduleIds: operatingScheduleIds
1139
+ } : {
1140
+ source: "prepare_business_hours",
1141
+ scheduleIds: []
1142
+ }
1143
+ };
1144
+ }
1145
+ buildAvailabilityPrepareKey(params) {
1146
+ const normalizedParams = this.normalizeAvailabilityPrepareDescriptor(params);
1147
+ return JSON.stringify(stableValue({
1148
+ scope: this.getAvailabilityScopeDescriptor(),
1149
+ prepare: normalizedParams
1150
+ }));
1151
+ }
1152
+ clearAvailabilityRemoteResourceState() {
1153
+ this.availabilityRemoteResourceEpoch += 1;
1154
+ this.availabilityRemoteOccupancyCache.clear();
1155
+ this.availabilityRemoteResourceQueryInFlight.clear();
1156
+ this.availabilityRemoteResourceQuerySequence = 0;
1157
+ }
1158
+ buildAvailabilityRemoteResourceScopeKey(timezoneName) {
1159
+ return JSON.stringify(stableValue({
1160
+ scope: this.getAvailabilityScopeDescriptor(),
1161
+ timezone: timezoneName
1162
+ }));
1163
+ }
1164
+ buildAvailabilityRemoteResourceQueryKey(params) {
1165
+ return JSON.stringify(stableValue({
1166
+ scope: this.getAvailabilityScopeDescriptor(),
1167
+ timezone: params.timezone,
1168
+ dateRange: params.dateRange,
1169
+ resourceIds: this.normalizeAvailabilityIdList(params.resourceIds) || []
1170
+ }));
1171
+ }
1172
+ async setOtherParams(params, { cover = false } = {}) {
1173
+ var _a;
1174
+ const previousScope = this.getAvailabilityScopeDescriptor(this.otherParams || {});
1175
+ const explicitlySetsOpenData = Object.prototype.hasOwnProperty.call(params, "openData");
1176
+ await super.setOtherParams(params, { cover });
1177
+ const nextScope = this.getAvailabilityScopeDescriptor(this.otherParams || {});
1178
+ if (explicitlySetsOpenData) {
1179
+ this.openDataTarget = ((_a = this.otherParams) == null ? void 0 : _a.openData) && nextScope.businessCode && nextScope.channel ? `${nextScope.businessCode}+${nextScope.channel}` : null;
1180
+ }
1181
+ const shouldResetAvailabilityState = previousScope.cacheId !== nextScope.cacheId || previousScope.businessCode !== nextScope.businessCode || previousScope.channel !== nextScope.channel || previousScope.platform !== nextScope.platform || previousScope.businessTimezone !== nextScope.businessTimezone;
1182
+ if (shouldResetAvailabilityState) {
1183
+ await this.resetAvailabilityProjectionState("set_other_params_reset");
1184
+ }
1185
+ }
1186
+ async destroy() {
1187
+ this.availabilityScheduleCatalog = null;
1188
+ this.availabilityRuntimeOffsetMinutes = null;
1189
+ await this.resetAvailabilityProjectionState("destroy");
1190
+ await super.destroy();
1191
+ }
1192
+ createAvailabilityContextId() {
1193
+ this.availabilityContextSequence += 1;
1194
+ return `${this.name}:availability:${Date.now()}:${this.availabilityContextSequence}`;
1195
+ }
1196
+ buildAvailabilityProjectionKey(contextId, version) {
1197
+ return `${contextId}:${version}`;
1198
+ }
1199
+ getAvailabilityRuntimeOffsetMinutes() {
1200
+ if (this.availabilityRuntimeOffsetMinutes === null) {
1201
+ this.availabilityRuntimeOffsetMinutes = (0, import_localClock.captureRuntimeOffsetMinutes)();
1202
+ }
1203
+ return this.availabilityRuntimeOffsetMinutes;
1204
+ }
1205
+ getAvailabilityRuntimeDateTime(value = /* @__PURE__ */ new Date()) {
1206
+ return (0, import_localClock.formatLocalDateTime)((0, import_localClock.localDateToScalar)(value), this.getAvailabilityRuntimeOffsetMinutes());
1207
+ }
1208
+ cloneAvailabilityPrepareParams(params) {
1209
+ var _a, _b;
1210
+ return (0, import_lodash_es.cloneDeep)({
1211
+ ...params,
1212
+ productIds: params.productIds ? [...params.productIds] : void 0,
1213
+ resourceIds: params.resourceIds ? [...params.resourceIds] : void 0,
1214
+ rawProducts: params.rawProducts ? (0, import_lodash_es.cloneDeep)(params.rawProducts) : void 0,
1215
+ rawResources: params.rawResources ? (0, import_lodash_es.cloneDeep)(params.rawResources) : void 0,
1216
+ rawOccupancies: params.rawOccupancies ? (0, import_lodash_es.cloneDeep)(params.rawOccupancies) : void 0,
1217
+ editingProductLineUids: params.editingProductLineUids ? [...params.editingProductLineUids] : void 0,
1218
+ dateRange: params.dateRange ? { ...params.dateRange } : void 0,
1219
+ businessHours: params.businessHours ? { ...params.businessHours } : void 0,
1220
+ _availabilityOperatingHours: params._availabilityOperatingHours ? {
1221
+ source: params._availabilityOperatingHours.source,
1222
+ scheduleIds: [...params._availabilityOperatingHours.scheduleIds]
1223
+ } : void 0,
1224
+ catalogScope: ((_a = params.catalogScope) == null ? void 0 : _a.type) === "schedule" ? { ...params.catalogScope } : ((_b = params.catalogScope) == null ? void 0 : _b.type) === "productList" ? { type: "productList", params: { ...params.catalogScope.params } } : void 0
1225
+ });
1226
+ }
1227
+ getAvailabilityDurationCandidateAnchorAt(projection) {
1228
+ const value = projection == null ? void 0 : projection.meta.durationCandidateAnchorAt;
1229
+ return typeof value === "string" && value.trim() ? value : void 0;
1230
+ }
1231
+ createAvailabilityContextRef(context) {
1232
+ return {
1233
+ contextId: context.contextId
1234
+ };
1235
+ }
1236
+ async emitAvailabilityDiagnostic(payload) {
1237
+ var _a, _b, _c;
1238
+ await ((_c = (_b = (_a = this.core) == null ? void 0 : _a.effects) == null ? void 0 : _b.emit) == null ? void 0 : _c.call(
1239
+ _b,
1240
+ `${this.name}:${import_types.UnifiedBookingSalesHooks.onAvailabilityProjectionDiagnostic}`,
1241
+ {
1242
+ ...payload,
1243
+ registrySize: this.availabilityContextRegistry.size,
1244
+ projectionPoolSize: this.availabilityProjectionPool.size
1245
+ }
1246
+ ));
1247
+ }
1248
+ touchAvailabilityContext(context) {
1249
+ context.lastAccessedAt = (/* @__PURE__ */ new Date()).toISOString();
1250
+ this.availabilityContextRegistry.delete(context.contextId);
1251
+ this.availabilityContextRegistry.set(context.contextId, context);
1252
+ }
1253
+ touchAvailabilityProjection(entry) {
1254
+ this.availabilityProjectionPool.delete(entry.projectionKey);
1255
+ this.availabilityProjectionPool.set(entry.projectionKey, entry);
1256
+ return entry;
1257
+ }
1258
+ removeAvailabilityProjection(projectionKey) {
1259
+ if (!projectionKey)
1260
+ return;
1261
+ this.availabilityProjectionPool.delete(projectionKey);
1262
+ }
1263
+ removeAvailabilityContext(contextId) {
1264
+ const context = this.availabilityContextRegistry.get(contextId);
1265
+ if (!context)
1266
+ return;
1267
+ this.removeAvailabilityProjection(context.activeProjectionKey);
1268
+ this.availabilityProjectionRefreshInFlight.delete(contextId);
1269
+ this.availabilityCommitLocks.delete(contextId);
1270
+ this.availabilityContextRegistry.delete(contextId);
1271
+ }
1272
+ evictAvailabilityProjectionPool() {
1273
+ while (this.availabilityProjectionPool.size > MAX_AVAILABILITY_PROJECTION_POOL) {
1274
+ const oldestEntry = this.availabilityProjectionPool.entries().next().value;
1275
+ if (!oldestEntry)
1276
+ break;
1277
+ const [projectionKey, projectionEntry] = oldestEntry;
1278
+ this.availabilityProjectionPool.delete(projectionKey);
1279
+ const context = this.availabilityContextRegistry.get(projectionEntry.contextId);
1280
+ if ((context == null ? void 0 : context.activeProjectionKey) === projectionKey) {
1281
+ context.activeProjectionKey = null;
1282
+ context.refreshNonce += 1;
1283
+ }
1284
+ }
1285
+ }
1286
+ evictAvailabilityContextRegistry() {
1287
+ while (this.availabilityContextRegistry.size > MAX_AVAILABILITY_CONTEXT_REGISTRY) {
1288
+ const oldestContext = this.availabilityContextRegistry.entries().next().value;
1289
+ if (!oldestContext)
1290
+ break;
1291
+ this.removeAvailabilityContext(oldestContext[0]);
1292
+ }
1293
+ }
1294
+ invalidateAvailabilityContextsAfterCommit() {
1295
+ this.availabilityLifecycleGeneration += 1;
1296
+ const nextOrderRevision = this.getCurrentOrderRevision();
1297
+ this.availabilityProjectionPool.clear();
1298
+ this.availabilityPrepareInFlight.clear();
1299
+ this.availabilityProjectionRefreshInFlight.clear();
1300
+ this.availabilityContextRegistry.forEach((context) => {
1301
+ context.activeVersion += 1;
1302
+ context.activeProjectionKey = null;
1303
+ context.lastKnownOrderRevision = nextOrderRevision;
1304
+ context.refreshNonce += 1;
1305
+ context.lifecycleGeneration = this.availabilityLifecycleGeneration;
1306
+ });
1307
+ }
1308
+ async resetAvailabilityProjectionState(action) {
1309
+ this.availabilityLifecycleGeneration += 1;
1310
+ this.availabilityContextRegistry.clear();
1311
+ this.availabilityProjectionPool.clear();
1312
+ this.availabilityPrepareInFlight.clear();
1313
+ this.availabilityProjectionRefreshInFlight.clear();
1314
+ this.availabilityCommitLocks.clear();
1315
+ this.clearAvailabilityRemoteResourceState();
1316
+ await this.emitAvailabilityDiagnostic({
1317
+ action,
1318
+ contextId: null,
1319
+ contextVersion: null,
1320
+ reason: action,
1321
+ projection: null
1322
+ });
1323
+ }
480
1324
  isSessionStoragePersistEnabled() {
481
1325
  var _a;
482
1326
  return Boolean(
@@ -545,12 +1389,9 @@ var UnifiedBookingSalesImpl = class extends import_BookingTicket.BookingTicket {
545
1389
  return 1;
546
1390
  }
547
1391
  getRegisteredModuleNames() {
548
- return [...super.getRegisteredModuleNames(), "resourcePlanner", "openData"];
1392
+ return [...super.getRegisteredModuleNames(), "openData"];
549
1393
  }
550
1394
  createSubModule(moduleName) {
551
- if (moduleName === "resourcePlanner") {
552
- return new import_modules.ResourcePlannerModule(`${this.name}_resourcePlanner`);
553
- }
554
1395
  if (moduleName === "openData") {
555
1396
  return new import_modules.OpenDataModule(`${this.name}_openData`);
556
1397
  }
@@ -896,22 +1737,16 @@ var UnifiedBookingSalesImpl = class extends import_BookingTicket.BookingTicket {
896
1737
  preferredDiscountFound: preferredDiscountId === void 0 || preferredDiscountId === null ? void 0 : Boolean(preferredDiscount)
897
1738
  });
898
1739
  }
899
- /** ResourcePlanner is a required child module; fail early instead of producing partial availability. */
900
- getPlanner() {
901
- if (!this.store.resourcePlanner) {
902
- throw new Error("[UnifiedBookingSales] resourcePlanner 模块未初始化");
903
- }
904
- return this.store.resourcePlanner;
905
- }
906
1740
  buildUnifiedOrderProduct(sourceProduct, ownershipKey, product) {
1741
+ const normalizedProductRaw = (product == null ? void 0 : product.raw) || {};
907
1742
  const orderProduct = this.transformBaseProductToOrderProduct({
908
1743
  payload: {
909
1744
  ...sourceProduct,
910
1745
  id: sourceProduct.id ?? (product == null ? void 0 : product.id),
911
1746
  product_id: sourceProduct.product_id ?? sourceProduct.id ?? (product == null ? void 0 : product.id),
912
1747
  quantity: sourceProduct.quantity ?? sourceProduct.num ?? (product == null ? void 0 : product.quantity) ?? 1,
913
- price: sourceProduct.price ?? (product == null ? void 0 : product.price),
914
- selling_price: sourceProduct.selling_price ?? sourceProduct.price ?? (product == null ? void 0 : product.price),
1748
+ price: sourceProduct.price ?? normalizedProductRaw.price,
1749
+ selling_price: sourceProduct.selling_price ?? sourceProduct.price ?? normalizedProductRaw.selling_price ?? normalizedProductRaw.price,
915
1750
  metadata: {
916
1751
  ...sourceProduct.metadata || {},
917
1752
  [ownershipKey]: true
@@ -935,7 +1770,7 @@ var UnifiedBookingSalesImpl = class extends import_BookingTicket.BookingTicket {
935
1770
  return ((_a2 = product == null ? void 0 : product.metadata) == null ? void 0 : _a2[ownershipKey]) === true;
936
1771
  });
937
1772
  if (existingLines.length > 0) {
938
- await this.removeProductsFromOrder(existingLines.map(buildPlannerLineIdentity));
1773
+ await this.removeProductsFromOrder(existingLines.map(buildAvailabilityLineIdentity));
939
1774
  }
940
1775
  }
941
1776
  async stageProducts(params, ownershipKey) {
@@ -966,15 +1801,8 @@ var UnifiedBookingSalesImpl = class extends import_BookingTicket.BookingTicket {
966
1801
  return this.getTempOrder();
967
1802
  }
968
1803
  /**
969
- * Adds the current product choice to an otherwise clean planner cart without creating bookings.
970
- * This separates "what the customer wants" from the later time/resource confirmation step.
971
- */
972
- async stagePlannerProducts(params) {
973
- return this.stageProducts(params, "unified_booking_sales");
974
- }
975
- /**
976
- * Adds ordinary retail products without asking ResourcePlanner for booking facts.
977
- * Its staged lines have separate ownership so planner and retail skins can coexist safely.
1804
+ * Adds ordinary retail products without asking ResourcePlanner for booking facts.
1805
+ * Its staged lines have separate ownership so planner and retail skins can coexist safely.
978
1806
  */
979
1807
  async stageRetailProducts(params) {
980
1808
  return this.stageProducts(params, "unified_booking_sales_retail");
@@ -1021,57 +1849,200 @@ var UnifiedBookingSalesImpl = class extends import_BookingTicket.BookingTicket {
1021
1849
  }
1022
1850
  return this.addProductToOrder(orderProduct);
1023
1851
  }
1024
- /** Normalizes caller-provided products or loads the catalog before any resource request is made. */
1025
- async loadPlannerProducts(params) {
1026
- var _a, _b, _c, _d, _e, _f;
1027
- if ((_b = (_a = params.context) == null ? void 0 : _a.products) == null ? void 0 : _b.length)
1028
- return params.context.products;
1029
- if ((_c = params.rawProducts) == null ? void 0 : _c.length)
1030
- return params.rawProducts.map(normalizeProductForPlanner);
1031
- const productIds = params.productIds || ((_d = params.productLoadParams) == null ? void 0 : _d.product_ids) || [];
1032
- if (productIds.length > 0 || params.productLoadParams) {
1033
- await this.loadProducts({
1034
- ...params.date ? { schedule_date: params.date } : {},
1035
- ...params.productLoadParams || {},
1036
- product_ids: productIds.length ? productIds.map(Number).filter(Number.isFinite) : (_e = params.productLoadParams) == null ? void 0 : _e.product_ids
1852
+ normalizeAvailabilityDateRange(params, timezone) {
1853
+ var _a, _b;
1854
+ const today = (0, import_localClock.formatLocalDate)((0, import_localClock.localDateToScalar)(/* @__PURE__ */ new Date()));
1855
+ const startDate = ((_a = params.dateRange) == null ? void 0 : _a.startDate) || today;
1856
+ const endDate = ((_b = params.dateRange) == null ? void 0 : _b.endDate) || (0, import_localClock.formatLocalDate)((0, import_localClock.addLocalCalendarDays)((0, import_localClock.localDateToScalarStart)((0, import_localClock.parseLocalDate)(startDate)), 31));
1857
+ let startScalar;
1858
+ let endScalar;
1859
+ try {
1860
+ startScalar = (0, import_localClock.localDateToScalarStart)((0, import_localClock.parseLocalDate)(startDate));
1861
+ endScalar = (0, import_localClock.localDateToScalarStart)((0, import_localClock.parseLocalDate)(endDate));
1862
+ } catch (error) {
1863
+ throw new import_ResourcePlanner.AvailabilityError("INVALID_DATE_RANGE", "日期范围必须是有效的 YYYY-MM-DD 且结束日期不早于开始日期", {
1864
+ dateRange: { startDate, endDate },
1865
+ timezone,
1866
+ cause: error instanceof Error ? error.message : String(error)
1037
1867
  });
1038
1868
  }
1039
- return (((_f = this.getProductCatalog) == null ? void 0 : _f.call(this).products) || []).map(normalizeProductForPlanner);
1869
+ if (endScalar < startScalar) {
1870
+ throw new import_ResourcePlanner.AvailabilityError("INVALID_DATE_RANGE", "日期范围必须是有效的 YYYY-MM-DD 且结束日期不早于开始日期", {
1871
+ dateRange: { startDate, endDate },
1872
+ timezone
1873
+ });
1874
+ }
1875
+ const inclusiveDays = Math.trunc((endScalar - startScalar) / (24 * 60 * 60 * 1e3)) + 1;
1876
+ if (inclusiveDays > MAX_AVAILABILITY_RANGE_DAYS) {
1877
+ throw new import_ResourcePlanner.AvailabilityError("RANGE_TOO_LARGE", "日期范围最多包含 90 个自然日", {
1878
+ dateRange: { startDate, endDate },
1879
+ inclusiveDays
1880
+ });
1881
+ }
1882
+ return { startDate, endDate };
1040
1883
  }
1041
- /** Loads the reusable store-level schedule catalog on demand. */
1042
- async ensurePlannerSchedulesLoaded() {
1043
- var _a, _b, _c, _d, _e, _f;
1044
- if (!this.store.schedule)
1045
- throw new Error("[UnifiedBookingSales] schedule 模块未初始化");
1046
- const scheduleList = ((_b = (_a = this.store.schedule).getScheduleList) == null ? void 0 : _b.call(_a)) || [];
1047
- if (!((_d = (_c = this.store.schedule).isScheduleListLoaded) == null ? void 0 : _d.call(_c)) || scheduleList.length === 0) {
1048
- await this.store.schedule.loadAllSchedule();
1884
+ assertAvailabilityPrepareParams(params) {
1885
+ var _a, _b;
1886
+ assertAvailabilityCatalogProductIds(params.productIds, "prepare.productIds");
1887
+ const hasProductAnchor = Array.isArray(params.productIds) && params.productIds.length > 0;
1888
+ const hasResourceAnchor = Array.isArray(params.resourceIds) && params.resourceIds.length > 0;
1889
+ const hasDateAnchor = Boolean(((_a = params.dateRange) == null ? void 0 : _a.startDate) || ((_b = params.dateRange) == null ? void 0 : _b.endDate));
1890
+ if (!hasProductAnchor && !hasResourceAnchor && !hasDateAnchor) {
1891
+ throw new import_ResourcePlanner.AvailabilityError("MISSING_ANCHOR", "prepare 至少需要 productIds、resourceIds 或 dateRange 之一", {
1892
+ params
1893
+ });
1894
+ }
1895
+ if (!hasProductAnchor && !params.catalogScope) {
1896
+ throw new import_ResourcePlanner.AvailabilityError("MISSING_CATALOG_SCOPE", "缺少 productIds 时必须提供 catalogScope", {
1897
+ params
1898
+ });
1899
+ }
1900
+ if (params.slotStepMinutes !== void 0) {
1901
+ const step = Number(params.slotStepMinutes);
1902
+ if (!Number.isInteger(step) || step <= 0) {
1903
+ throw new import_ResourcePlanner.AvailabilityError("INVALID_TARGET_FILTER", "slotStepMinutes 必须是正整数", {
1904
+ slotStepMinutes: params.slotStepMinutes
1905
+ });
1906
+ }
1049
1907
  }
1050
- return ((_f = (_e = this.store.schedule).getScheduleList) == null ? void 0 : _f.call(_e)) || [];
1051
1908
  }
1052
- /**
1053
- * v2 resources reference schedule ids rather than expanded work times. Refresh once when the
1054
- * cached schedule catalog cannot materialize all referenced ids.
1055
- */
1056
- async ensurePlannerSchedulesForResources(rawResources) {
1909
+ captureAvailabilityScheduleCatalog() {
1057
1910
  var _a, _b;
1058
- let scheduleList = await this.ensurePlannerSchedulesLoaded();
1059
- const scheduleIds = collectRawScheduleIds(rawResources);
1060
- if (!scheduleListHasIds(scheduleList, scheduleIds)) {
1061
- await this.store.schedule.loadAllSchedule({ useCache: false });
1062
- scheduleList = ((_b = (_a = this.store.schedule).getScheduleList) == null ? void 0 : _b.call(_a)) || [];
1911
+ if (!this.store.schedule) {
1912
+ throw new Error("[UnifiedBookingSales] schedule 模块未初始化");
1913
+ }
1914
+ this.availabilityScheduleCatalog = (0, import_lodash_es.cloneDeep)(
1915
+ ((_b = (_a = this.store.schedule).getScheduleList) == null ? void 0 : _b.call(_a)) || []
1916
+ );
1917
+ }
1918
+ /** Returns the solution-lifetime schedule snapshot captured after initialization. */
1919
+ getAvailabilityScheduleCatalog() {
1920
+ if (this.availabilityScheduleCatalog === null) {
1921
+ throw new Error("[UnifiedBookingSales] schedule 快照未初始化");
1063
1922
  }
1064
- return scheduleList;
1923
+ return this.availabilityScheduleCatalog;
1924
+ }
1925
+ buildAvailabilityOperatingHoursSnapshot(params) {
1926
+ const descriptor = params.prepare._availabilityOperatingHours || {
1927
+ source: params.prepare.businessHours ? "prepare_business_hours" : "default_business_hours",
1928
+ scheduleIds: []
1929
+ };
1930
+ if (descriptor.source !== "open_data_schedule") {
1931
+ const windows2 = dateRangeDays(params.dateRange).flatMap((date) => {
1932
+ const range = normalizeAvailabilityLocalDateTimeRange({
1933
+ startAt: `${date}T${params.businessHours.startTime}:00`,
1934
+ endAt: `${date}T${params.businessHours.endTime}:00`,
1935
+ fixedOffsetMinutes: this.getAvailabilityRuntimeOffsetMinutes(),
1936
+ rollOverEnd: true
1937
+ });
1938
+ return range ? [{
1939
+ startAt: range.startAt,
1940
+ endAt: range.endAt,
1941
+ source: descriptor.source
1942
+ }] : [];
1943
+ });
1944
+ return {
1945
+ source: descriptor.source,
1946
+ scheduleIds: [],
1947
+ missingScheduleIds: [],
1948
+ windows: windows2
1949
+ };
1950
+ }
1951
+ const scheduleMap = new Map(
1952
+ params.scheduleList.map((schedule) => [String(schedule.id), schedule])
1953
+ );
1954
+ const schedules = descriptor.scheduleIds.map((scheduleId) => scheduleMap.get(String(scheduleId))).filter((schedule) => Boolean(schedule));
1955
+ const matchedScheduleIds = new Set(schedules.map((schedule) => String(schedule.id)));
1956
+ const missingScheduleIds = descriptor.scheduleIds.filter(
1957
+ (scheduleId) => !matchedScheduleIds.has(String(scheduleId))
1958
+ );
1959
+ const windows = dateRangeDays(params.dateRange).flatMap((date) => (0, import_getDateIsInSchedule.getScheduleStartEndTimePoints)(date, schedules).flatMap((slot) => {
1960
+ const range = normalizeAvailabilityLocalDateTimeRange({
1961
+ startAt: slot.start_at,
1962
+ endAt: slot.end_at,
1963
+ fixedOffsetMinutes: this.getAvailabilityRuntimeOffsetMinutes(),
1964
+ rollOverEnd: true
1965
+ });
1966
+ return range ? [{
1967
+ startAt: range.startAt,
1968
+ endAt: range.endAt,
1969
+ source: `operating_schedule:${descriptor.scheduleIds.map(String).join(",")}`
1970
+ }] : [];
1971
+ }));
1972
+ return {
1973
+ source: descriptor.source,
1974
+ scheduleIds: [...descriptor.scheduleIds],
1975
+ missingScheduleIds,
1976
+ windows
1977
+ };
1978
+ }
1979
+ async loadAvailabilityProducts(params) {
1980
+ var _a, _b, _c, _d, _e, _f, _g;
1981
+ const { prepare, dateRange } = params;
1982
+ const explicitProductIds = prepare.productIds || [];
1983
+ let rawProducts = [];
1984
+ if (Array.isArray(prepare.rawProducts)) {
1985
+ rawProducts = prepare.rawProducts;
1986
+ this.unifiedProductCatalog = [...rawProducts];
1987
+ } else if (explicitProductIds.length > 0) {
1988
+ rawProducts = await this.loadProducts({
1989
+ product_ids: explicitProductIds,
1990
+ schedule_date: dateRange.startDate
1991
+ });
1992
+ } else if (((_a = prepare.catalogScope) == null ? void 0 : _a.type) === "schedule") {
1993
+ await this.loadScheduleAvailableDate({
1994
+ startDate: dateRange.startDate,
1995
+ endDate: dateRange.endDate,
1996
+ custom_page_id: Number(prepare.catalogScope.customPageId) || void 0,
1997
+ channel: prepare.catalogScope.channel
1998
+ });
1999
+ const dateSet = new Set(dateRangeDays(dateRange));
2000
+ const scheduleProductIds = (((_c = (_b = this.store.schedule).getAvailabilityScheduleDateList) == null ? void 0 : _c.call(_b)) || []).filter((item) => dateSet.has(item.date)).flatMap((item) => item.product_ids || []);
2001
+ const productIds = uniqueAvailabilityIds([
2002
+ ...scheduleProductIds,
2003
+ ...((_e = (_d = this.store.schedule).getOtherProductsIds) == null ? void 0 : _e.call(_d)) || []
2004
+ ]);
2005
+ if (productIds.length > 0) {
2006
+ assertAvailabilityCatalogProductIds(productIds, "prepare.catalogScope.scheduleProductIds");
2007
+ rawProducts = await this.loadProducts({
2008
+ product_ids: productIds,
2009
+ schedule_date: dateRange.startDate
2010
+ });
2011
+ } else {
2012
+ this.unifiedProductCatalog = [];
2013
+ }
2014
+ } else if (((_f = prepare.catalogScope) == null ? void 0 : _f.type) === "productList") {
2015
+ rawProducts = await this.loadProducts({
2016
+ ...prepare.catalogScope.params,
2017
+ schedule_date: prepare.catalogScope.params.schedule_date || dateRange.startDate
2018
+ });
2019
+ }
2020
+ const normalized = rawProducts.map((product) => normalizeProductForAvailability(
2021
+ product,
2022
+ params.scheduleList,
2023
+ this.getAvailabilityRuntimeOffsetMinutes()
2024
+ ));
2025
+ if (!explicitProductIds.length && ((_g = prepare.resourceIds) == null ? void 0 : _g.length)) {
2026
+ return normalized.filter((product) => {
2027
+ var _a2;
2028
+ return (_a2 = product.requirementGroups) == null ? void 0 : _a2.some((group) => {
2029
+ var _a3;
2030
+ return (_a3 = group.resourceIds) == null ? void 0 : _a3.some((resourceId) => prepare.resourceIds.some((id) => sameAvailabilityId(id, resourceId)));
2031
+ });
2032
+ });
2033
+ }
2034
+ const productIdSet = explicitProductIds.length ? new Set(explicitProductIds.map(String)) : null;
2035
+ return productIdSet ? normalized.filter((product) => productIdSet.has(String(product.id))) : normalized;
1065
2036
  }
1066
2037
  /**
1067
2038
  * Fetches resource metadata plus flat occupied events. Work windows are intentionally derived
1068
- * locally in buildPlannerResourcesFromV2 from /schedule, rather than requested from the legacy
2039
+ * locally in buildAvailabilityResourcesFromV2 from /schedule, rather than requested from the legacy
1069
2040
  * resource dates endpoint.
1070
2041
  */
1071
- async fetchPlannerResourcesV2(params) {
2042
+ async fetchAvailabilityResourcesV2(params) {
1072
2043
  const response = await this.request.get("/schedule/resource/list/v2", {
1073
- start_date: params.dateRange.start,
1074
- end_date: params.dateRange.end,
2044
+ start_date: params.dateRange.startDate,
2045
+ end_date: params.dateRange.endDate,
1075
2046
  resource_ids: params.resourceIds,
1076
2047
  front_end_cache_id: false
1077
2048
  }, {
@@ -1084,190 +2055,1134 @@ var UnifiedBookingSalesImpl = class extends import_BookingTicket.BookingTicket {
1084
2055
  return data.list;
1085
2056
  return [];
1086
2057
  }
1087
- /**
1088
- * Resolves the Planner context in precedence order: explicit normalized context, test raw data,
1089
- * then the v2 resource API inferred from selected product requirements and any URL lock.
1090
- */
1091
- async loadPlannerResources(params, products = []) {
1092
- var _a, _b, _c, _d, _e, _f;
1093
- if ((_b = (_a = params.context) == null ? void 0 : _a.resources) == null ? void 0 : _b.length) {
1094
- return {
1095
- resources: params.context.resources,
1096
- externalEvents: params.context.externalEvents || []
1097
- };
2058
+ async fetchAndPublishAvailabilityResourcesV2(params) {
2059
+ const normalizedResourceIds = this.normalizeAvailabilityIdList(params.resourceIds) || [];
2060
+ if (normalizedResourceIds.length === 0)
2061
+ return [];
2062
+ const scopeKey = this.buildAvailabilityRemoteResourceScopeKey(params.timezone);
2063
+ const cacheParams = {
2064
+ scopeKey,
2065
+ resourceIds: normalizedResourceIds,
2066
+ dateRange: params.dateRange
2067
+ };
2068
+ const cachedRawResources = this.availabilityRemoteOccupancyCache.read(cacheParams);
2069
+ if (cachedRawResources) {
2070
+ if (params.useCacheOnHit) {
2071
+ void this.refreshAvailabilityRemoteResourceQuery({
2072
+ timezone: params.timezone,
2073
+ dateRange: params.dateRange,
2074
+ resourceIds: normalizedResourceIds
2075
+ }).catch((error) => {
2076
+ console.warn("[UnifiedBookingSales] Remote occupancy background refresh failed", error);
2077
+ });
2078
+ }
2079
+ return cachedRawResources;
2080
+ }
2081
+ return await this.refreshAvailabilityRemoteResourceQuery({
2082
+ timezone: params.timezone,
2083
+ dateRange: params.dateRange,
2084
+ resourceIds: normalizedResourceIds
2085
+ });
2086
+ }
2087
+ async refreshAvailabilityRemoteResourceQuery(params) {
2088
+ const normalizedResourceIds = this.normalizeAvailabilityIdList(params.resourceIds) || [];
2089
+ if (normalizedResourceIds.length === 0)
2090
+ return [];
2091
+ const queryKey = this.buildAvailabilityRemoteResourceQueryKey({
2092
+ timezone: params.timezone,
2093
+ dateRange: params.dateRange,
2094
+ resourceIds: normalizedResourceIds
2095
+ });
2096
+ const epoch = this.availabilityRemoteResourceEpoch;
2097
+ const existing = this.availabilityRemoteResourceQueryInFlight.get(queryKey);
2098
+ if ((existing == null ? void 0 : existing.epoch) === epoch) {
2099
+ return await existing.promise;
2100
+ }
2101
+ this.availabilityRemoteResourceQuerySequence += 1;
2102
+ const writeVersion = this.availabilityRemoteResourceQuerySequence;
2103
+ const promise = this.fetchAvailabilityResourcesV2({
2104
+ dateRange: params.dateRange,
2105
+ resourceIds: normalizedResourceIds
2106
+ });
2107
+ this.availabilityRemoteResourceQueryInFlight.set(queryKey, {
2108
+ epoch,
2109
+ promise
2110
+ });
2111
+ try {
2112
+ const rawResources = await promise;
2113
+ if (epoch === this.availabilityRemoteResourceEpoch) {
2114
+ this.availabilityRemoteOccupancyCache.publish({
2115
+ scopeKey: this.buildAvailabilityRemoteResourceScopeKey(params.timezone),
2116
+ timezoneName: params.timezone,
2117
+ resourceIds: normalizedResourceIds,
2118
+ dateRange: params.dateRange,
2119
+ rawResources,
2120
+ version: writeVersion
2121
+ });
2122
+ }
2123
+ return rawResources;
2124
+ } finally {
2125
+ const current = this.availabilityRemoteResourceQueryInFlight.get(queryKey);
2126
+ if ((current == null ? void 0 : current.promise) === promise) {
2127
+ this.availabilityRemoteResourceQueryInFlight.delete(queryKey);
2128
+ }
1098
2129
  }
1099
- if ((_c = params.rawResources) == null ? void 0 : _c.length) {
2130
+ }
2131
+ async loadAvailabilityResources(params) {
2132
+ const resourceIds = collectAvailabilityResourceIds(params.products, params.resourceIds || []);
2133
+ if (resourceIds.length === 0) {
1100
2134
  return {
1101
- resources: params.rawResources.map(normalizeResource).filter((resource) => resource !== null),
1102
- externalEvents: ((_d = params.context) == null ? void 0 : _d.externalEvents) || []
2135
+ resources: [],
2136
+ resourceOccupancies: []
1103
2137
  };
1104
2138
  }
1105
- const resourceIds = collectPlannerResourceIds(products, params.resourceIds || []);
1106
- if (!params.useResourceDateApi || !params.dateRange || resourceIds.length === 0) {
1107
- return {
2139
+ if (Array.isArray(params.rawResources)) {
2140
+ return buildAvailabilityResourcesFromV2({
2141
+ rawResources: params.rawResources,
2142
+ scheduleList: params.scheduleList,
2143
+ dateRange: params.dateRange,
2144
+ fixedOffsetMinutes: this.getAvailabilityRuntimeOffsetMinutes()
2145
+ });
2146
+ }
2147
+ if (params.fetchPolicy === "cache_only") {
2148
+ const cachedRawResources = this.availabilityRemoteOccupancyCache.read({
2149
+ scopeKey: this.buildAvailabilityRemoteResourceScopeKey(params.timezone),
2150
+ resourceIds,
2151
+ dateRange: params.dateRange
2152
+ });
2153
+ if (cachedRawResources) {
2154
+ return buildAvailabilityResourcesFromV2({
2155
+ rawResources: cachedRawResources,
2156
+ scheduleList: params.scheduleList,
2157
+ dateRange: params.dateRange,
2158
+ fixedOffsetMinutes: this.getAvailabilityRuntimeOffsetMinutes()
2159
+ });
2160
+ }
2161
+ return params.fallbackResources ? (0, import_lodash_es.cloneDeep)(params.fallbackResources) : {
1108
2162
  resources: [],
1109
- externalEvents: ((_e = params.context) == null ? void 0 : _e.externalEvents) || []
2163
+ resourceOccupancies: []
1110
2164
  };
1111
2165
  }
1112
- const rawResources = await this.fetchPlannerResourcesV2({
2166
+ const rawResources = await this.fetchAndPublishAvailabilityResourcesV2({
2167
+ timezone: params.timezone,
1113
2168
  dateRange: params.dateRange,
1114
- resourceIds
2169
+ resourceIds,
2170
+ useCacheOnHit: true
1115
2171
  });
1116
- const scheduleList = await this.ensurePlannerSchedulesForResources(rawResources);
1117
- const materialized = buildPlannerResourcesFromV2({
2172
+ return buildAvailabilityResourcesFromV2({
1118
2173
  rawResources,
1119
- scheduleList,
1120
- dateRange: params.dateRange
2174
+ scheduleList: params.scheduleList,
2175
+ dateRange: params.dateRange,
2176
+ fixedOffsetMinutes: this.getAvailabilityRuntimeOffsetMinutes()
1121
2177
  });
1122
- return {
1123
- resources: materialized.resources,
1124
- externalEvents: [
1125
- ...((_f = params.context) == null ? void 0 : _f.externalEvents) || [],
1126
- ...materialized.externalEvents
1127
- ]
1128
- };
1129
2178
  }
1130
- /**
1131
- * Boundary between IO and pure planning. Once this returns, all later availability/slot calls
1132
- * operate on the ResourcePlanner snapshot and do not issue resource requests themselves.
1133
- */
1134
- async loadPlannerContext(params = {}) {
1135
- const products = await this.loadPlannerProducts(params);
1136
- const plannerResources = await this.loadPlannerResources(params, products);
1137
- const context = {
1138
- ...params.context || {},
1139
- products,
1140
- resources: plannerResources.resources,
1141
- externalEvents: plannerResources.externalEvents
1142
- };
1143
- const snapshot = this.getPlanner().setContext(context);
1144
- this.getPlanner().setSelectionPatch({
1145
- mode: params.mode,
1146
- productIds: params.productIds || [],
1147
- date: params.date,
1148
- startTime: params.startTime,
1149
- endTime: params.endTime,
1150
- resourceIds: params.resourceIds || []
2179
+ resolveEditingOccupancyExclusions(tempOrder, productLineUids = []) {
2180
+ const productUids = /* @__PURE__ */ new Set();
2181
+ const bookingUids = /* @__PURE__ */ new Set();
2182
+ const scheduleEventIds = /* @__PURE__ */ new Set();
2183
+ if (!productLineUids.length)
2184
+ return { productUids, bookingUids, scheduleEventIds };
2185
+ const products = Array.isArray(tempOrder == null ? void 0 : tempOrder.products) ? tempOrder.products : [];
2186
+ const bookings = Array.isArray(tempOrder == null ? void 0 : tempOrder.bookings) ? tempOrder.bookings : [];
2187
+ productLineUids.forEach((requestedUid) => {
2188
+ var _a;
2189
+ const product = products.find((line) => getProductLineUid(line) === String(requestedUid));
2190
+ if (!product) {
2191
+ throw new import_ResourcePlanner.AvailabilityError(
2192
+ "SELF_EXCLUSION_RESOLVE_FAILED",
2193
+ "无法解析编辑商品行,不能安全排除自身占用",
2194
+ { productLineUid: requestedUid }
2195
+ );
2196
+ }
2197
+ const productUid = getProductLineUid(product);
2198
+ const linkedBookingUid = String(product.booking_uid ?? ((_a = product.metadata) == null ? void 0 : _a.booking_uid) ?? "");
2199
+ const linkedBookings = bookings.filter((booking) => linkedBookingUid && getBookingUid(booking) === linkedBookingUid || productUid && String(booking.product_uid ?? "") === productUid);
2200
+ if (!productUid || !linkedBookings.length) {
2201
+ throw new import_ResourcePlanner.AvailabilityError(
2202
+ "SELF_EXCLUSION_RESOLVE_FAILED",
2203
+ "编辑商品行缺少可验证的 booking 关联",
2204
+ { productLineUid: requestedUid, linkedBookingUid }
2205
+ );
2206
+ }
2207
+ productUids.add(productUid);
2208
+ linkedBookings.forEach((booking) => {
2209
+ const bookingUid = getBookingUid(booking);
2210
+ if (bookingUid)
2211
+ bookingUids.add(bookingUid);
2212
+ if (booking.schedule_event_id != null) {
2213
+ scheduleEventIds.add(String(booking.schedule_event_id));
2214
+ }
2215
+ });
1151
2216
  });
1152
- await this.core.effects.emit(`${this.name}:${import_types.UnifiedBookingSalesHooks.onPlannerContextLoaded}`, snapshot);
1153
- return this.getPlanner().getSnapshot();
2217
+ return { productUids, bookingUids, scheduleEventIds };
1154
2218
  }
1155
- /** Returns cells, date summaries and a derived grid; cells are continuous ranges, not UI slots. */
1156
- async queryPlannerAvailability(query = {}) {
1157
- return this.getPlanner().queryAvailability(query);
2219
+ occupancyIsExcluded(occupancy, exclusions) {
2220
+ if (occupancy.scheduleEventId != null) {
2221
+ return exclusions.scheduleEventIds.has(String(occupancy.scheduleEventId));
2222
+ }
2223
+ if (occupancy.bookingUid != null) {
2224
+ return exclusions.bookingUids.has(String(occupancy.bookingUid));
2225
+ }
2226
+ if (occupancy.productUid != null) {
2227
+ return exclusions.productUids.has(String(occupancy.productUid));
2228
+ }
2229
+ return false;
1158
2230
  }
1159
- /** Turns compatible availability ranges into user-selectable intervals and ready-to-commit assignments. */
1160
- resolveAssignableSlots(query = {}) {
1161
- return this.getPlanner().resolveAssignableSlots(query);
2231
+ collectCartResourceOccupancies(tempOrder, exclusions) {
2232
+ const bookings = Array.isArray(tempOrder == null ? void 0 : tempOrder.bookings) ? tempOrder.bookings : [];
2233
+ return bookings.flatMap((booking) => {
2234
+ var _a;
2235
+ const bookingUid = getBookingUid(booking);
2236
+ const productUid = String(booking.product_uid ?? ((_a = booking.metadata) == null ? void 0 : _a.product_uid) ?? "");
2237
+ const scheduleEventId = toNumberId(booking.schedule_event_id);
2238
+ const startAt = booking.start_at || `${booking.start_date || ""} ${booking.start_time || ""}`.trim();
2239
+ const endAt = booking.end_at || `${booking.end_date || booking.start_date || ""} ${booking.end_time || ""}`.trim();
2240
+ const resources = Array.isArray(booking.resources) ? booking.resources : [];
2241
+ return resources.flatMap((resource) => {
2242
+ const resourceId = resource.resource_id ?? resource.relation_id ?? resource.id;
2243
+ if (resourceId == null || !startAt || !endAt)
2244
+ return [];
2245
+ const range = normalizeAvailabilityLocalDateTimeRange({
2246
+ startAt,
2247
+ endAt,
2248
+ fixedOffsetMinutes: this.getAvailabilityRuntimeOffsetMinutes(),
2249
+ rollOverEnd: false
2250
+ });
2251
+ if (!range)
2252
+ return [];
2253
+ const occupancy = {
2254
+ resourceId,
2255
+ startAt: range.startAt,
2256
+ endAt: range.endAt,
2257
+ pax: Math.max(1, Number(resource.capacity ?? booking.capacity ?? 1) || 1),
2258
+ source: "cart",
2259
+ scheduleEventId,
2260
+ bookingUid: bookingUid || void 0,
2261
+ productUid: productUid || void 0,
2262
+ raw: { booking, resource }
2263
+ };
2264
+ return this.occupancyIsExcluded(occupancy, exclusions) ? [] : [occupancy];
2265
+ });
2266
+ });
1162
2267
  }
1163
- /** Stores a UI choice only. It is deliberately side-effect free with respect to tempOrder. */
1164
- setPlannerSelection(patch) {
1165
- return this.getPlanner().setSelectionPatch(patch);
2268
+ getCurrentOrderRevision() {
2269
+ return resolveOrderRevision(this.getTempOrder());
1166
2270
  }
1167
- /**
1168
- * Legacy-friendly automatic assignment for already fixed intervals. Duration products without a
1169
- * start time intentionally remain unresolved; callers offering a picker should use slots.
1170
- */
1171
- async autoAssignPlanner(query = {}) {
1172
- const result = this.getPlanner().autoAssign(query);
1173
- if (result.conflicts.length) {
1174
- console.warn("[UnifiedBookingSales] autoAssignPlanner 存在未分配项", result.conflicts);
2271
+ getAvailabilityContextEntry(contextId) {
2272
+ const context = this.availabilityContextRegistry.get(contextId);
2273
+ if (!context) {
2274
+ throw this.createAvailabilityFacadeError(
2275
+ "AVAILABILITY_CONTEXT_NOT_FOUND",
2276
+ "availability context 不存在或已失效,请重新 prepare",
2277
+ {
2278
+ contextId,
2279
+ lifecycleGeneration: this.availabilityLifecycleGeneration
2280
+ }
2281
+ );
1175
2282
  }
1176
- return this.getPlanner().getSnapshot();
2283
+ this.touchAvailabilityContext(context);
2284
+ return context;
1177
2285
  }
1178
- /** Rechecks remote events, capacity and conflicts among the current selection before committing. */
1179
- validatePlannerSelection() {
1180
- return this.getPlanner().validateSelection();
2286
+ getAvailabilityProjectionByKey(projectionKey) {
2287
+ var _a;
2288
+ if (!projectionKey)
2289
+ return null;
2290
+ return ((_a = this.availabilityProjectionPool.get(projectionKey)) == null ? void 0 : _a.projection) || null;
2291
+ }
2292
+ getActiveAvailabilityProjectionEntry(context) {
2293
+ if (!context.activeProjectionKey)
2294
+ return null;
2295
+ const entry = this.availabilityProjectionPool.get(context.activeProjectionKey);
2296
+ if (!entry)
2297
+ return null;
2298
+ return this.touchAvailabilityProjection(entry);
1181
2299
  }
1182
- /** Read-only snapshot for UI diagnostics and progressive flow steps. */
1183
- getPlannerSnapshot() {
1184
- return this.getPlanner().getSnapshot();
2300
+ decorateAvailabilityCandidate(contextId, contextVersion, candidate) {
2301
+ return {
2302
+ ...candidate,
2303
+ availabilityContextId: contextId,
2304
+ availabilityContextVersion: contextVersion
2305
+ };
1185
2306
  }
1186
- /**
1187
- * Validates then writes assignments to tempOrder. Real order submission is opt-in so UI callers
1188
- * can safely preview the cart; pass submitAfterCommit only after an explicit confirmation.
1189
- */
1190
- async commitPlannerSelection(params = {}) {
1191
- if (params.selection) {
1192
- this.setPlannerSelection(params.selection);
2307
+ assertAvailabilityCandidateFreshness(context, candidate) {
2308
+ if (candidate.availabilityContextId !== context.contextId) {
2309
+ throw this.createAvailabilityFacadeError(
2310
+ "AVAILABILITY_SELECTION_STALE",
2311
+ "候选与 availability context 不匹配,请重新获取候选",
2312
+ {
2313
+ candidateContextId: candidate.availabilityContextId,
2314
+ contextId: context.contextId
2315
+ }
2316
+ );
2317
+ }
2318
+ const currentContextVersion = context.activeVersion;
2319
+ if (candidate.availabilityContextVersion !== currentContextVersion) {
2320
+ throw this.createAvailabilityFacadeError(
2321
+ "AVAILABILITY_SELECTION_STALE",
2322
+ "候选已过期,请重新获取最新时间段",
2323
+ {
2324
+ contextId: context.contextId,
2325
+ candidateContextVersion: candidate.availabilityContextVersion,
2326
+ currentContextVersion,
2327
+ snapshotId: context.lastSnapshotId
2328
+ }
2329
+ );
1193
2330
  }
1194
- if (params.assignments) {
1195
- this.setPlannerSelection({ assignments: params.assignments });
2331
+ }
2332
+ assertContextualQueryFreshness(context, request) {
2333
+ if (request.target === "time") {
2334
+ request.candidates.forEach((candidate) => {
2335
+ this.assertAvailabilityCandidateFreshness(context, candidate);
2336
+ });
2337
+ return;
1196
2338
  }
1197
- const snapshot = this.getPlannerSnapshot();
1198
- const assignments = snapshot.selection.assignments || [];
1199
- if (assignments.length === 0) {
1200
- return {
1201
- tempOrder: this.getTempOrder(),
1202
- assignments,
1203
- validation: {
1204
- ok: false,
1205
- conflicts: [{
1206
- type: "missing_assignment",
1207
- message: "没有可提交的 planner assignment,请先执行 autoAssignPlanner 或传入 assignments"
1208
- }]
2339
+ if (request.target === "resource") {
2340
+ request.selectedRanges.forEach((candidate) => {
2341
+ this.assertAvailabilityCandidateFreshness(context, candidate);
2342
+ });
2343
+ }
2344
+ }
2345
+ storeAvailabilityProjectionEntry(params) {
2346
+ const previousContext = this.availabilityContextRegistry.get(params.contextId);
2347
+ const contextVersion = previousContext ? previousContext.activeVersion + (params.incrementVersion ? 1 : 0) : 1;
2348
+ const projectionKey = this.buildAvailabilityProjectionKey(params.contextId, contextVersion);
2349
+ const entry = {
2350
+ contextId: params.contextId,
2351
+ contextVersion,
2352
+ projectionKey,
2353
+ projection: params.projection,
2354
+ orderRevision: params.orderRevision,
2355
+ preparedAt: params.projection.meta.preparedAt,
2356
+ snapshotId: params.projection.meta.snapshotId
2357
+ };
2358
+ if ((previousContext == null ? void 0 : previousContext.activeProjectionKey) && previousContext.activeProjectionKey !== projectionKey) {
2359
+ this.removeAvailabilityProjection(previousContext.activeProjectionKey);
2360
+ }
2361
+ this.availabilityProjectionPool.set(projectionKey, entry);
2362
+ this.evictAvailabilityProjectionPool();
2363
+ const nextContext = {
2364
+ contextId: params.contextId,
2365
+ prepareParams: this.cloneAvailabilityPrepareParams(params.prepareParams),
2366
+ prepareKey: params.prepareKey,
2367
+ activeVersion: contextVersion,
2368
+ activeProjectionKey: projectionKey,
2369
+ createdAt: (previousContext == null ? void 0 : previousContext.createdAt) ?? params.projection.meta.preparedAt,
2370
+ lastAccessedAt: (/* @__PURE__ */ new Date()).toISOString(),
2371
+ lastKnownOrderRevision: params.orderRevision,
2372
+ lastPreparedAt: params.projection.meta.preparedAt,
2373
+ lastSnapshotId: params.projection.meta.snapshotId,
2374
+ durationCandidateAnchorAt: (previousContext == null ? void 0 : previousContext.durationCandidateAnchorAt) ?? this.getAvailabilityDurationCandidateAnchorAt(params.projection),
2375
+ refreshNonce: (previousContext == null ? void 0 : previousContext.refreshNonce) ?? 0,
2376
+ lifecycleGeneration: this.availabilityLifecycleGeneration
2377
+ };
2378
+ this.touchAvailabilityContext(nextContext);
2379
+ this.evictAvailabilityContextRegistry();
2380
+ return this.touchAvailabilityProjection(entry);
2381
+ }
2382
+ async buildAvailabilityProjectionSnapshot(params, options = {}) {
2383
+ const normalizedPrepareParams = this.normalizeAvailabilityPrepareDescriptor(params);
2384
+ const timezoneLabel = String(normalizedPrepareParams.timezone);
2385
+ const dateRange = normalizedPrepareParams.dateRange;
2386
+ const slotStepMinutes = normalizedPrepareParams.slotStepMinutes || DEFAULT_AVAILABILITY_SLOT_STEP_MINUTES;
2387
+ const businessHours = normalizedPrepareParams.businessHours || DEFAULT_AVAILABILITY_BUSINESS_HOURS;
2388
+ const mode = normalizedPrepareParams.mode || "appointment";
2389
+ const scheduleList = this.getAvailabilityScheduleCatalog();
2390
+ const operatingHours = this.buildAvailabilityOperatingHoursSnapshot({
2391
+ prepare: normalizedPrepareParams,
2392
+ dateRange,
2393
+ scheduleList,
2394
+ businessHours
2395
+ });
2396
+ const products = await this.loadAvailabilityProducts({
2397
+ prepare: normalizedPrepareParams,
2398
+ dateRange,
2399
+ scheduleList
2400
+ });
2401
+ const loadedResources = await this.loadAvailabilityResources({
2402
+ timezone: timezoneLabel,
2403
+ dateRange,
2404
+ resourceIds: normalizedPrepareParams.resourceIds,
2405
+ products,
2406
+ rawResources: normalizedPrepareParams.rawResources,
2407
+ scheduleList,
2408
+ fetchPolicy: options.resourceFetchPolicy || "default",
2409
+ fallbackResources: options.fallbackResources
2410
+ });
2411
+ const tempOrder = this.getTempOrder();
2412
+ const exclusions = this.resolveEditingOccupancyExclusions(
2413
+ tempOrder,
2414
+ normalizedPrepareParams.editingProductLineUids
2415
+ );
2416
+ const injectedRawOccupancies = (normalizedPrepareParams.rawOccupancies || []).flatMap((occupancy) => {
2417
+ const range = normalizeAvailabilityLocalDateTimeRange({
2418
+ startAt: occupancy.startAt,
2419
+ endAt: occupancy.endAt,
2420
+ fixedOffsetMinutes: this.getAvailabilityRuntimeOffsetMinutes(),
2421
+ rollOverEnd: false
2422
+ });
2423
+ if (!range)
2424
+ return [];
2425
+ return [{
2426
+ ...occupancy,
2427
+ startAt: range.startAt,
2428
+ endAt: range.endAt
2429
+ }];
2430
+ });
2431
+ const remoteOccupancies = [
2432
+ ...loadedResources.resourceOccupancies,
2433
+ ...loadedResources.includesInjectedRawOccupancies ? [] : injectedRawOccupancies
2434
+ ].filter((occupancy) => !this.occupancyIsExcluded(occupancy, exclusions));
2435
+ const cartOccupancies = this.collectCartResourceOccupancies(tempOrder, exclusions);
2436
+ const preparedAt = this.getAvailabilityRuntimeDateTime();
2437
+ const durationCandidateAnchorAt = options.durationCandidateAnchorAt || preparedAt;
2438
+ this.availabilityProjectionSequence += 1;
2439
+ const orderRevision = this.getCurrentOrderRevision();
2440
+ const projection = (0, import_ResourcePlanner.createAvailabilityProjection)({
2441
+ products,
2442
+ resources: loadedResources.resources,
2443
+ resourceOccupancies: [...remoteOccupancies, ...cartOccupancies],
2444
+ mode,
2445
+ timezone: timezoneLabel,
2446
+ coverage: dateRange,
2447
+ slotStepMinutes,
2448
+ businessHours,
2449
+ operatingHours,
2450
+ preparedAt,
2451
+ now: preparedAt,
2452
+ orderRevision,
2453
+ snapshotId: `${this.name}:${Date.now()}:${this.availabilityProjectionSequence}`,
2454
+ anchors: {
2455
+ productIds: normalizedPrepareParams.productIds,
2456
+ resourceIds: normalizedPrepareParams.resourceIds,
2457
+ dateRange
2458
+ },
2459
+ prepareParams: normalizedPrepareParams,
2460
+ durationCandidateAnchorAt
2461
+ });
2462
+ return {
2463
+ projection,
2464
+ prepareParams: normalizedPrepareParams,
2465
+ orderRevision
2466
+ };
2467
+ }
2468
+ async getSharedPreparedAvailabilitySnapshot(params) {
2469
+ const lifecycleGeneration = this.availabilityLifecycleGeneration;
2470
+ const normalizedParams = this.normalizeAvailabilityPrepareDescriptor(params);
2471
+ const hasRawInputs = params.rawProducts !== void 0 || params.rawResources !== void 0 || params.rawOccupancies !== void 0;
2472
+ const prepareKey = hasRawInputs ? JSON.stringify(stableValue({
2473
+ scope: this.getAvailabilityScopeDescriptor(),
2474
+ prepare: {
2475
+ ...normalizedParams,
2476
+ rawProducts: void 0,
2477
+ rawResources: void 0,
2478
+ rawOccupancies: void 0
2479
+ },
2480
+ rawInputs: {
2481
+ products: params.rawProducts !== void 0,
2482
+ resources: params.rawResources !== void 0,
2483
+ occupancies: params.rawOccupancies !== void 0
2484
+ }
2485
+ })) : this.buildAvailabilityPrepareKey(normalizedParams);
2486
+ if (hasRawInputs) {
2487
+ const snapshot = await this.buildAvailabilityProjectionSnapshot(normalizedParams);
2488
+ if (lifecycleGeneration !== this.availabilityLifecycleGeneration) {
2489
+ throw this.createAvailabilityFacadeError(
2490
+ "AVAILABILITY_CONTEXT_NOT_FOUND",
2491
+ "prepare 期间业务作用域或实例生命周期已变化,请重新 prepare",
2492
+ {
2493
+ prepareKey,
2494
+ expectedLifecycleGeneration: lifecycleGeneration,
2495
+ currentLifecycleGeneration: this.availabilityLifecycleGeneration
2496
+ }
2497
+ );
2498
+ }
2499
+ return { prepareKey, snapshot };
2500
+ }
2501
+ const existing = this.availabilityPrepareInFlight.get(prepareKey);
2502
+ const promise = (existing == null ? void 0 : existing.lifecycleGeneration) === lifecycleGeneration ? existing.promise : this.buildAvailabilityProjectionSnapshot(normalizedParams);
2503
+ if (!existing || existing.lifecycleGeneration !== lifecycleGeneration) {
2504
+ this.availabilityPrepareInFlight.set(prepareKey, {
2505
+ lifecycleGeneration,
2506
+ prepareKey,
2507
+ promise
2508
+ });
2509
+ }
2510
+ try {
2511
+ const snapshot = await promise;
2512
+ if (lifecycleGeneration !== this.availabilityLifecycleGeneration) {
2513
+ throw this.createAvailabilityFacadeError(
2514
+ "AVAILABILITY_CONTEXT_NOT_FOUND",
2515
+ "prepare 期间业务作用域或实例生命周期已变化,请重新 prepare",
2516
+ {
2517
+ prepareKey,
2518
+ expectedLifecycleGeneration: lifecycleGeneration,
2519
+ currentLifecycleGeneration: this.availabilityLifecycleGeneration
2520
+ }
2521
+ );
2522
+ }
2523
+ return { prepareKey, snapshot };
2524
+ } finally {
2525
+ const current = this.availabilityPrepareInFlight.get(prepareKey);
2526
+ if ((current == null ? void 0 : current.promise) === promise) {
2527
+ this.availabilityPrepareInFlight.delete(prepareKey);
2528
+ }
2529
+ }
2530
+ }
2531
+ async refreshAvailabilityContextProjection(params) {
2532
+ const existingInFlight = this.availabilityProjectionRefreshInFlight.get(params.contextId);
2533
+ if (existingInFlight)
2534
+ return await existingInFlight;
2535
+ const context = this.getAvailabilityContextEntry(params.contextId);
2536
+ const expectedVersion = context.activeVersion;
2537
+ const expectedRefreshNonce = context.refreshNonce;
2538
+ const expectedLifecycleGeneration = this.availabilityLifecycleGeneration;
2539
+ const refreshPromise = (async () => {
2540
+ const snapshot = await this.buildAvailabilityProjectionSnapshot(
2541
+ this.cloneAvailabilityPrepareParams(context.prepareParams),
2542
+ {
2543
+ durationCandidateAnchorAt: context.durationCandidateAnchorAt
1209
2544
  }
1210
- };
2545
+ );
2546
+ const currentContext = this.availabilityContextRegistry.get(params.contextId);
2547
+ if (!currentContext)
2548
+ return null;
2549
+ if (expectedLifecycleGeneration !== this.availabilityLifecycleGeneration || currentContext.activeVersion !== expectedVersion || currentContext.refreshNonce !== expectedRefreshNonce || currentContext.lifecycleGeneration !== expectedLifecycleGeneration) {
2550
+ return null;
2551
+ }
2552
+ const projectionEntry = this.storeAvailabilityProjectionEntry({
2553
+ contextId: currentContext.contextId,
2554
+ prepareParams: snapshot.prepareParams,
2555
+ prepareKey: currentContext.prepareKey,
2556
+ projection: snapshot.projection,
2557
+ orderRevision: snapshot.orderRevision,
2558
+ incrementVersion: true
2559
+ });
2560
+ await this.emitAvailabilityDiagnostic({
2561
+ action: "refresh",
2562
+ contextId: currentContext.contextId,
2563
+ contextVersion: projectionEntry.contextVersion,
2564
+ projection: projectionEntry.projection,
2565
+ snapshotId: projectionEntry.snapshotId,
2566
+ orderRevision: projectionEntry.orderRevision,
2567
+ previousOrderRevision: params.previousOrderRevision,
2568
+ reason: params.reason
2569
+ });
2570
+ return projectionEntry;
2571
+ })();
2572
+ this.availabilityProjectionRefreshInFlight.set(params.contextId, refreshPromise);
2573
+ try {
2574
+ return await refreshPromise;
2575
+ } finally {
2576
+ const current = this.availabilityProjectionRefreshInFlight.get(params.contextId);
2577
+ if (current === refreshPromise) {
2578
+ this.availabilityProjectionRefreshInFlight.delete(params.contextId);
2579
+ }
1211
2580
  }
1212
- const validation = this.validatePlannerSelection();
1213
- if (!validation.ok) {
2581
+ }
2582
+ async ensureAvailabilityContextProjection(contextId, reason) {
2583
+ let retries = 0;
2584
+ while (retries < 3) {
2585
+ const context = this.getAvailabilityContextEntry(contextId);
2586
+ const currentOrderRevision = this.getCurrentOrderRevision();
2587
+ const projectionEntry = this.getActiveAvailabilityProjectionEntry(context);
2588
+ if (projectionEntry && projectionEntry.orderRevision === currentOrderRevision) {
2589
+ return { context, projectionEntry, refreshed: false };
2590
+ }
2591
+ const refreshedProjectionEntry = await this.refreshAvailabilityContextProjection({
2592
+ contextId,
2593
+ reason: projectionEntry ? reason : `${reason}:projection_cache_miss`,
2594
+ previousOrderRevision: context.lastKnownOrderRevision
2595
+ });
2596
+ if (!refreshedProjectionEntry) {
2597
+ retries += 1;
2598
+ continue;
2599
+ }
2600
+ if (refreshedProjectionEntry.orderRevision !== this.getCurrentOrderRevision()) {
2601
+ retries += 1;
2602
+ continue;
2603
+ }
1214
2604
  return {
1215
- tempOrder: this.getTempOrder(),
1216
- assignments,
1217
- validation
2605
+ context: this.getAvailabilityContextEntry(contextId),
2606
+ projectionEntry: refreshedProjectionEntry,
2607
+ refreshed: true
1218
2608
  };
1219
2609
  }
1220
- const productsById = new Map(
1221
- snapshot.context.products.map((product) => [String(product.id), product])
2610
+ throw new import_ResourcePlanner.AvailabilityError("STALE_PROJECTION", "availability projection 刷新期间购物车持续变化,请重新 prepare", {
2611
+ contextId,
2612
+ reason
2613
+ });
2614
+ }
2615
+ async tryAcquireAvailabilityCommitLock(contextId) {
2616
+ if (this.availabilityCommitLocks.has(contextId)) {
2617
+ const waitingContext = this.availabilityContextRegistry.get(contextId);
2618
+ await this.emitAvailabilityDiagnostic({
2619
+ action: "commit_lock_wait",
2620
+ contextId,
2621
+ contextVersion: (waitingContext == null ? void 0 : waitingContext.activeVersion) ?? null,
2622
+ projection: this.getAvailabilityProjectionByKey(waitingContext == null ? void 0 : waitingContext.activeProjectionKey),
2623
+ snapshotId: waitingContext == null ? void 0 : waitingContext.lastSnapshotId,
2624
+ orderRevision: waitingContext == null ? void 0 : waitingContext.lastKnownOrderRevision,
2625
+ reason: "AVAILABILITY_COMMIT_IN_PROGRESS"
2626
+ });
2627
+ throw this.createAvailabilityFacadeError(
2628
+ "AVAILABILITY_COMMIT_IN_PROGRESS",
2629
+ "同一 availability context 已有 commit 正在进行中",
2630
+ { contextId }
2631
+ );
2632
+ }
2633
+ this.availabilityCommitLocks.add(contextId);
2634
+ const currentContext = this.availabilityContextRegistry.get(contextId);
2635
+ await this.emitAvailabilityDiagnostic({
2636
+ action: "commit_lock_acquired",
2637
+ contextId,
2638
+ contextVersion: (currentContext == null ? void 0 : currentContext.activeVersion) ?? null,
2639
+ projection: this.getAvailabilityProjectionByKey(currentContext == null ? void 0 : currentContext.activeProjectionKey),
2640
+ snapshotId: currentContext == null ? void 0 : currentContext.lastSnapshotId,
2641
+ orderRevision: currentContext == null ? void 0 : currentContext.lastKnownOrderRevision,
2642
+ reason: "commit_start"
2643
+ });
2644
+ }
2645
+ async releaseAvailabilityCommitLock(contextId) {
2646
+ this.availabilityCommitLocks.delete(contextId);
2647
+ const releasedContext = this.availabilityContextRegistry.get(contextId);
2648
+ await this.emitAvailabilityDiagnostic({
2649
+ action: "commit_lock_released",
2650
+ contextId,
2651
+ contextVersion: (releasedContext == null ? void 0 : releasedContext.activeVersion) ?? null,
2652
+ projection: this.getAvailabilityProjectionByKey(releasedContext == null ? void 0 : releasedContext.activeProjectionKey),
2653
+ snapshotId: releasedContext == null ? void 0 : releasedContext.lastSnapshotId,
2654
+ orderRevision: releasedContext == null ? void 0 : releasedContext.lastKnownOrderRevision,
2655
+ reason: "commit_end"
2656
+ });
2657
+ }
2658
+ /** IO boundary: loads catalog, schedules, resources, occupancies, and registers one context. */
2659
+ async prepareAvailabilityProjection(params) {
2660
+ this.assertAvailabilityPrepareParams(params);
2661
+ const paramsWithDefaults = await this.resolveAvailabilityPrepareDefaults(params);
2662
+ const { prepareKey, snapshot } = await this.getSharedPreparedAvailabilitySnapshot(paramsWithDefaults);
2663
+ const contextId = this.createAvailabilityContextId();
2664
+ const projectionEntry = this.storeAvailabilityProjectionEntry({
2665
+ contextId,
2666
+ prepareParams: snapshot.prepareParams,
2667
+ prepareKey,
2668
+ projection: snapshot.projection,
2669
+ orderRevision: snapshot.orderRevision,
2670
+ incrementVersion: false
2671
+ });
2672
+ const context = this.getAvailabilityContextEntry(contextId);
2673
+ const handle = this.createAvailabilityContextRef(context);
2674
+ const payload = {
2675
+ context: handle,
2676
+ contextVersion: context.activeVersion,
2677
+ projection: projectionEntry.projection,
2678
+ refreshed: false,
2679
+ reason: "prepare"
2680
+ };
2681
+ await this.emitAvailabilityDiagnostic({
2682
+ action: "prepare",
2683
+ contextId,
2684
+ contextVersion: context.activeVersion,
2685
+ projection: projectionEntry.projection,
2686
+ snapshotId: context.lastSnapshotId,
2687
+ orderRevision: context.lastKnownOrderRevision,
2688
+ reason: "prepare"
2689
+ });
2690
+ await this.core.effects.emit(
2691
+ `${this.name}:${import_types.UnifiedBookingSalesHooks.onAvailabilityProjectionPrepared}`,
2692
+ payload
2693
+ );
2694
+ return handle;
2695
+ }
2696
+ /** Pure configuration calculation over the current context projection. */
2697
+ async getProductTimeRanges(contextId, request = {}) {
2698
+ const { context, projectionEntry } = await this.ensureAvailabilityContextProjection(
2699
+ contextId,
2700
+ "get_product_time_ranges"
1222
2701
  );
1223
- await this.removeExistingStagedLines("unified_booking_sales");
1224
- const assignmentGroups = /* @__PURE__ */ new Map();
1225
- assignments.forEach((assignment) => {
1226
- const key = buildAssignmentGroupKey(assignment);
1227
- assignmentGroups.set(key, [...assignmentGroups.get(key) || [], assignment]);
2702
+ const contextVersion = context.activeVersion;
2703
+ return (0, import_ResourcePlanner.getProductTimeRanges)(projectionEntry.projection, request).map((group) => ({
2704
+ ...group,
2705
+ ranges: group.ranges.map((candidate) => this.decorateAvailabilityCandidate(context.contextId, contextVersion, candidate))
2706
+ }));
2707
+ }
2708
+ /** Thin facade: auto-refresh on cart drift, then delegate to the pure projection query. */
2709
+ async queryBookingAvailability(contextId, request) {
2710
+ const { context, projectionEntry, refreshed } = await this.ensureAvailabilityContextProjection(
2711
+ contextId,
2712
+ "query_booking_availability"
2713
+ );
2714
+ this.assertContextualQueryFreshness(context, request);
2715
+ const result = (0, import_ResourcePlanner.queryAvailabilityProjection)(
2716
+ projectionEntry.projection,
2717
+ request
2718
+ );
2719
+ const contextVersion = context.activeVersion;
2720
+ const contextualResult = result.target === "time" ? {
2721
+ ...result,
2722
+ data: result.data.map((group) => ({
2723
+ ...group,
2724
+ times: group.times.map((item) => ({
2725
+ ...item,
2726
+ availabilityContextId: context.contextId,
2727
+ availabilityContextVersion: contextVersion
2728
+ }))
2729
+ }))
2730
+ } : result.target === "resource" ? {
2731
+ ...result,
2732
+ data: result.data.map((group) => ({
2733
+ ...group,
2734
+ ranges: group.ranges.map((rangeGroup) => ({
2735
+ ...rangeGroup,
2736
+ range: {
2737
+ ...rangeGroup.range,
2738
+ availabilityContextId: context.contextId,
2739
+ availabilityContextVersion: contextVersion
2740
+ }
2741
+ }))
2742
+ }))
2743
+ } : result;
2744
+ return {
2745
+ ...contextualResult,
2746
+ contextId: context.contextId,
2747
+ contextVersion,
2748
+ refreshed
2749
+ };
2750
+ }
2751
+ releaseAvailabilityContext(contextId) {
2752
+ var _a;
2753
+ const context = this.availabilityContextRegistry.get(contextId);
2754
+ if (!context)
2755
+ return;
2756
+ this.removeAvailabilityContext(contextId);
2757
+ void this.emitAvailabilityDiagnostic({
2758
+ action: "release",
2759
+ contextId,
2760
+ contextVersion: context.activeVersion,
2761
+ projection: ((_a = this.getActiveAvailabilityProjectionEntry(context)) == null ? void 0 : _a.projection) || null,
2762
+ snapshotId: context.lastSnapshotId,
2763
+ orderRevision: context.lastKnownOrderRevision,
2764
+ reason: "explicit_release"
1228
2765
  });
1229
- for (const groupAssignments of assignmentGroups.values()) {
1230
- const product = productsById.get(String(groupAssignments[0].productId));
1231
- if (!product)
1232
- continue;
1233
- const sourceProduct = product.raw || { id: product.id, title: product.title, price: product.price };
1234
- const productInput = this.buildUnifiedOrderProduct(
1235
- sourceProduct,
1236
- "unified_booking_sales",
1237
- product
2766
+ }
2767
+ assertAvailabilityCommitWriteCAS(params) {
2768
+ const context = this.availabilityContextRegistry.get(params.contextId);
2769
+ if (!context) {
2770
+ throw this.createAvailabilityFacadeError(
2771
+ "AVAILABILITY_CONTEXT_NOT_FOUND",
2772
+ "availability context 不存在或已失效,请重新 prepare",
2773
+ { contextId: params.contextId }
1238
2774
  );
1239
- if (!productInput)
1240
- continue;
1241
- await this.addProductToOrder(productInput, buildBookingFromAssignments({
1242
- assignments: groupAssignments,
1243
- product
1244
- }));
1245
2775
  }
1246
- let submitResult;
1247
- if (params.submitAfterCommit) {
1248
- submitResult = await this.submitSalesOrder();
2776
+ if (this.availabilityLifecycleGeneration !== params.expectedLifecycleGeneration || context.lifecycleGeneration !== params.expectedLifecycleGeneration || context.activeVersion !== params.expectedVersion || context.refreshNonce !== params.expectedRefreshNonce || this.getCurrentOrderRevision() !== params.expectedOrderRevision) {
2777
+ throw this.createAvailabilityFacadeError(
2778
+ "AVAILABILITY_SELECTION_STALE",
2779
+ "availability context 已变化,当前选择已失效,请重新获取候选",
2780
+ {
2781
+ contextId: params.contextId,
2782
+ expectedVersion: params.expectedVersion,
2783
+ currentVersion: context.activeVersion,
2784
+ expectedRefreshNonce: params.expectedRefreshNonce,
2785
+ currentRefreshNonce: context.refreshNonce,
2786
+ expectedLifecycleGeneration: params.expectedLifecycleGeneration,
2787
+ currentLifecycleGeneration: this.availabilityLifecycleGeneration,
2788
+ expectedOrderRevision: params.expectedOrderRevision,
2789
+ currentOrderRevision: this.getCurrentOrderRevision()
2790
+ }
2791
+ );
2792
+ }
2793
+ return context;
2794
+ }
2795
+ findProductLineByUid(productLineUid) {
2796
+ var _a;
2797
+ return (((_a = this.getTempOrder()) == null ? void 0 : _a.products) || []).find((product) => getProductLineUid(product) === String(productLineUid));
2798
+ }
2799
+ findLinkedBooking(product) {
2800
+ var _a, _b;
2801
+ const productUid = getProductLineUid(product);
2802
+ const bookingUid = String(product.booking_uid ?? ((_a = product.metadata) == null ? void 0 : _a.booking_uid) ?? "");
2803
+ return (((_b = this.getTempOrder()) == null ? void 0 : _b.bookings) || []).find((booking) => bookingUid && getBookingUid(booking) === bookingUid || productUid && String(booking.product_uid ?? "") === productUid);
2804
+ }
2805
+ /** Revalidates against the latest landed occupancy cache, then mutates exactly one product line. */
2806
+ async commitAvailabilitySelection(contextId, params) {
2807
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
2808
+ const normalizedContextId = String(contextId || "").trim();
2809
+ if (!normalizedContextId) {
2810
+ throw new import_ResourcePlanner.AvailabilityError("INVALID_TARGET_FILTER", "commitAvailabilitySelection 缺少 contextId", {
2811
+ candidate: params.candidate
2812
+ });
2813
+ }
2814
+ await this.tryAcquireAvailabilityCommitLock(normalizedContextId);
2815
+ try {
2816
+ const bookingCount = Number(params.bookingCount ?? 1);
2817
+ if (!Number.isInteger(bookingCount) || bookingCount <= 0) {
2818
+ throw new import_ResourcePlanner.AvailabilityError("INVALID_RESERVATION_QUANTITY", "bookingCount 必须是正整数", {
2819
+ bookingCount: params.bookingCount
2820
+ });
2821
+ }
2822
+ if (params.mode === "edit" && bookingCount !== 1) {
2823
+ throw new import_ResourcePlanner.AvailabilityError("INVALID_RESERVATION_QUANTITY", "编辑预约商品时 bookingCount 必须为 1", {
2824
+ bookingCount
2825
+ });
2826
+ }
2827
+ const orderProductQuantity = ((_a = params.orderProduct) == null ? void 0 : _a.num) ?? ((_b = params.orderProduct) == null ? void 0 : _b.quantity) ?? ((_d = (_c = params.orderProduct) == null ? void 0 : _c._extend) == null ? void 0 : _d.quantity);
2828
+ if (orderProductQuantity !== void 0 && orderProductQuantity !== null && Number(orderProductQuantity) !== bookingCount) {
2829
+ throw new import_ResourcePlanner.AvailabilityError("INVALID_RESERVATION_QUANTITY", "orderProduct 数量与 bookingCount 不一致", {
2830
+ bookingCount,
2831
+ orderProductQuantity
2832
+ });
2833
+ }
2834
+ const partySize = Number(params.partySize);
2835
+ if (!Number.isInteger(partySize) || partySize <= 0) {
2836
+ throw new import_ResourcePlanner.AvailabilityError("INVALID_RESERVATION_QUANTITY", "partySize 必须是正整数", {
2837
+ partySize: params.partySize
2838
+ });
2839
+ }
2840
+ const {
2841
+ context,
2842
+ projectionEntry,
2843
+ refreshed
2844
+ } = await this.ensureAvailabilityContextProjection(
2845
+ normalizedContextId,
2846
+ "commit_precheck"
2847
+ );
2848
+ if (refreshed) {
2849
+ throw this.createAvailabilityFacadeError(
2850
+ "AVAILABILITY_SELECTION_STALE",
2851
+ "availability context 已刷新,请重新获取候选",
2852
+ {
2853
+ contextId: normalizedContextId,
2854
+ currentContextVersion: context.activeVersion,
2855
+ candidateContextVersion: params.candidate.availabilityContextVersion
2856
+ }
2857
+ );
2858
+ }
2859
+ const boundEditingUids = context.prepareParams.editingProductLineUids || [];
2860
+ if (params.mode === "edit") {
2861
+ if (!params.productLineUid || !boundEditingUids.includes(String(params.productLineUid))) {
2862
+ throw this.createAvailabilityFacadeError(
2863
+ "AVAILABILITY_SELECTION_STALE",
2864
+ "edit context 与当前商品行不匹配,请重新 prepare 编辑上下文",
2865
+ {
2866
+ contextId: normalizedContextId,
2867
+ productLineUid: params.productLineUid,
2868
+ editingProductLineUids: boundEditingUids
2869
+ }
2870
+ );
2871
+ }
2872
+ } else if (boundEditingUids.length > 0) {
2873
+ throw this.createAvailabilityFacadeError(
2874
+ "AVAILABILITY_SELECTION_STALE",
2875
+ "edit context 不能用于 add,请重新 prepare 新增上下文",
2876
+ {
2877
+ contextId: normalizedContextId,
2878
+ editingProductLineUids: boundEditingUids
2879
+ }
2880
+ );
2881
+ }
2882
+ if (projectionEntry.contextVersion !== context.activeVersion) {
2883
+ throw this.createAvailabilityFacadeError(
2884
+ "AVAILABILITY_SELECTION_STALE",
2885
+ "availability context 已刷新,请重新获取候选",
2886
+ {
2887
+ contextId: normalizedContextId,
2888
+ currentContextVersion: context.activeVersion,
2889
+ projectionContextVersion: projectionEntry.contextVersion
2890
+ }
2891
+ );
2892
+ }
2893
+ if (projectionEntry.orderRevision !== this.getCurrentOrderRevision()) {
2894
+ throw this.createAvailabilityFacadeError(
2895
+ "AVAILABILITY_SELECTION_STALE",
2896
+ "购物车已变化,请重新获取候选",
2897
+ {
2898
+ contextId: normalizedContextId,
2899
+ currentContextVersion: context.activeVersion,
2900
+ projectionOrderRevision: projectionEntry.orderRevision,
2901
+ currentOrderRevision: this.getCurrentOrderRevision()
2902
+ }
2903
+ );
2904
+ }
2905
+ if (projectionEntry.contextVersion !== params.candidate.availabilityContextVersion) {
2906
+ throw this.createAvailabilityFacadeError(
2907
+ "AVAILABILITY_SELECTION_STALE",
2908
+ "候选已过期,请重新获取最新时间段",
2909
+ {
2910
+ contextId: normalizedContextId,
2911
+ candidateContextVersion: params.candidate.availabilityContextVersion,
2912
+ currentContextVersion: context.activeVersion
2913
+ }
2914
+ );
2915
+ }
2916
+ this.assertAvailabilityCandidateFreshness(context, params.candidate);
2917
+ const orderProductId = ((_e = params.orderProduct) == null ? void 0 : _e.product_id) ?? ((_f = params.orderProduct) == null ? void 0 : _f.id);
2918
+ if (!sameAvailabilityId(orderProductId, params.candidate.productId)) {
2919
+ throw new import_ResourcePlanner.AvailabilityError("INVALID_TARGET_FILTER", "orderProduct 与 candidate 商品不一致", {
2920
+ orderProductId,
2921
+ candidateProductId: params.candidate.productId
2922
+ });
2923
+ }
2924
+ if (params.mode === "edit" && !params.productLineUid) {
2925
+ throw new import_ResourcePlanner.AvailabilityError("PRODUCT_LINE_NOT_FOUND", "edit 模式必须提供 productLineUid");
2926
+ }
2927
+ const editingLine = params.mode === "edit" ? this.findProductLineByUid(String(params.productLineUid)) : void 0;
2928
+ if (params.mode === "edit" && !editingLine) {
2929
+ throw new import_ResourcePlanner.AvailabilityError("PRODUCT_LINE_NOT_FOUND", "找不到要编辑的预约商品行", {
2930
+ productLineUid: params.productLineUid
2931
+ });
2932
+ }
2933
+ if (editingLine && !sameAvailabilityId(editingLine.product_id ?? editingLine.id, params.candidate.productId)) {
2934
+ throw new import_ResourcePlanner.AvailabilityError("INVALID_TARGET_FILTER", "编辑商品行与 candidate 商品不一致", {
2935
+ productLineUid: params.productLineUid,
2936
+ productLineProductId: editingLine.product_id ?? editingLine.id,
2937
+ candidateProductId: params.candidate.productId
2938
+ });
2939
+ }
2940
+ if (!Array.isArray(params.requirementSelections)) {
2941
+ throw new import_ResourcePlanner.AvailabilityError("INVALID_TARGET_FILTER", "requirementSelections 必须是数组", {
2942
+ requirementSelections: params.requirementSelections
2943
+ });
2944
+ }
2945
+ const baseProjection = projectionEntry.projection;
2946
+ let commitWriteGuard = {
2947
+ contextId: normalizedContextId,
2948
+ expectedVersion: context.activeVersion,
2949
+ expectedRefreshNonce: context.refreshNonce,
2950
+ expectedLifecycleGeneration: this.availabilityLifecycleGeneration,
2951
+ expectedOrderRevision: String(baseProjection.meta.orderRevision ?? "")
2952
+ };
2953
+ const prepareParams = baseProjection.meta.prepareParams;
2954
+ if (!prepareParams) {
2955
+ throw new import_ResourcePlanner.AvailabilityError("REVALIDATION_FAILED", "projection 缺少 prepareParams,无法执行缓存重校验");
2956
+ }
2957
+ const fallbackResources = {
2958
+ resources: (0, import_lodash_es.cloneDeep)([...baseProjection.resources || []]),
2959
+ resourceOccupancies: (0, import_lodash_es.cloneDeep)(
2960
+ [...baseProjection.resourceOccupancies || []].filter((occupancy) => occupancy.source !== "cart")
2961
+ ),
2962
+ includesInjectedRawOccupancies: true
2963
+ };
2964
+ const commitSnapshot = await this.buildAvailabilityProjectionSnapshot({
2965
+ ...this.cloneAvailabilityPrepareParams(prepareParams),
2966
+ editingProductLineUids: params.mode === "edit" && params.productLineUid ? [params.productLineUid] : []
2967
+ }, {
2968
+ resourceFetchPolicy: "cache_only",
2969
+ fallbackResources,
2970
+ durationCandidateAnchorAt: this.getAvailabilityDurationCandidateAnchorAt(baseProjection) || context.durationCandidateAnchorAt
2971
+ });
2972
+ const refreshedProjection = commitSnapshot.projection;
2973
+ if (String(refreshedProjection.meta.orderRevision ?? "") !== String(baseProjection.meta.orderRevision ?? "")) {
2974
+ await this.refreshAvailabilityContextProjection({
2975
+ contextId: normalizedContextId,
2976
+ reason: "commit_order_revision_changed",
2977
+ previousOrderRevision: String(baseProjection.meta.orderRevision ?? "")
2978
+ });
2979
+ const latestContext = this.getAvailabilityContextEntry(normalizedContextId);
2980
+ throw this.createAvailabilityFacadeError(
2981
+ "AVAILABILITY_SELECTION_STALE",
2982
+ "最终缓存重校验期间购物车已变化,请重新获取候选",
2983
+ {
2984
+ contextId: normalizedContextId,
2985
+ candidateContextVersion: params.candidate.availabilityContextVersion,
2986
+ currentContextVersion: latestContext.activeVersion,
2987
+ projectionOrderRevision: baseProjection.meta.orderRevision,
2988
+ refreshedOrderRevision: refreshedProjection.meta.orderRevision,
2989
+ snapshotId: baseProjection.meta.snapshotId,
2990
+ refreshedSnapshotId: refreshedProjection.meta.snapshotId
2991
+ }
2992
+ );
2993
+ }
2994
+ this.assertAvailabilityCommitWriteCAS(commitWriteGuard);
2995
+ const refreshedRanges = (0, import_ResourcePlanner.getProductTimeRanges)(refreshedProjection, {
2996
+ productIds: [params.candidate.productId],
2997
+ dateRange: {
2998
+ startDate: params.candidate.startAt.slice(0, 10),
2999
+ endDate: params.candidate.startAt.slice(0, 10)
3000
+ }
3001
+ }).flatMap((group) => group.ranges);
3002
+ const refreshedCandidateBase = refreshedRanges.find((candidate) => candidate.startAt === params.candidate.startAt && candidate.endAt === params.candidate.endAt && candidate.bookingStartAt === params.candidate.bookingStartAt && candidate.bookingEndAt === params.candidate.bookingEndAt);
3003
+ if (!refreshedCandidateBase) {
3004
+ throw new import_ResourcePlanner.AvailabilityError("REVALIDATION_FAILED", "缓存重校验后候选时间已不在商品配置中", {
3005
+ contextId: normalizedContextId,
3006
+ candidate: params.candidate,
3007
+ refreshedSnapshotId: refreshedProjection.meta.snapshotId
3008
+ });
3009
+ }
3010
+ const validation = (0, import_ResourcePlanner.validateAvailabilitySelection)(refreshedProjection, {
3011
+ candidate: refreshedCandidateBase,
3012
+ requirementSelections: params.requirementSelections,
3013
+ partySize,
3014
+ bookingCount
3015
+ });
3016
+ if (!validation.ok) {
3017
+ const capacityConflict = validation.conflicts.find((conflict) => conflict.code === "resource_capacity_exceeded");
3018
+ throw new import_ResourcePlanner.AvailabilityError(
3019
+ "REVALIDATION_FAILED",
3020
+ (capacityConflict == null ? void 0 : capacityConflict.message) || "缓存重校验后资源已不可提交",
3021
+ {
3022
+ validation,
3023
+ refreshedSnapshotId: refreshedProjection.meta.snapshotId
3024
+ }
3025
+ );
3026
+ }
3027
+ const revalidationEntry = this.storeAvailabilityProjectionEntry({
3028
+ contextId: normalizedContextId,
3029
+ prepareParams: commitSnapshot.prepareParams,
3030
+ prepareKey: context.prepareKey,
3031
+ projection: refreshedProjection,
3032
+ orderRevision: commitSnapshot.orderRevision,
3033
+ incrementVersion: true
3034
+ });
3035
+ const revalidatedContext = this.getAvailabilityContextEntry(normalizedContextId);
3036
+ await this.emitAvailabilityDiagnostic({
3037
+ action: "refresh",
3038
+ contextId: normalizedContextId,
3039
+ contextVersion: revalidatedContext.activeVersion,
3040
+ projection: refreshedProjection,
3041
+ snapshotId: revalidationEntry.snapshotId,
3042
+ orderRevision: revalidationEntry.orderRevision,
3043
+ previousOrderRevision: baseProjection.meta.orderRevision,
3044
+ reason: "commit_cached_occupancy_revalidation"
3045
+ });
3046
+ commitWriteGuard = {
3047
+ contextId: normalizedContextId,
3048
+ expectedVersion: revalidatedContext.activeVersion,
3049
+ expectedRefreshNonce: revalidatedContext.refreshNonce,
3050
+ expectedLifecycleGeneration: this.availabilityLifecycleGeneration,
3051
+ expectedOrderRevision: revalidationEntry.orderRevision
3052
+ };
3053
+ const refreshedCandidate = this.decorateAvailabilityCandidate(
3054
+ normalizedContextId,
3055
+ revalidatedContext.activeVersion,
3056
+ refreshedCandidateBase
3057
+ );
3058
+ const product = refreshedProjection.products.find((item) => sameAvailabilityId(item.id, refreshedCandidate.productId));
3059
+ if (!product) {
3060
+ throw new import_ResourcePlanner.AvailabilityError("REVALIDATION_FAILED", "缓存重校验后商品不在 projection 中", {
3061
+ productId: refreshedCandidate.productId
3062
+ });
3063
+ }
3064
+ const existingBooking = editingLine ? this.findLinkedBooking(editingLine) : void 0;
3065
+ const existingBookingUid = existingBooking ? getBookingUid(existingBooking) : "";
3066
+ if (params.mode === "edit" && !existingBookingUid) {
3067
+ throw new import_ResourcePlanner.AvailabilityError("PRODUCT_LINE_NOT_FOUND", "编辑商品行缺少关联 booking", {
3068
+ productLineUid: params.productLineUid
3069
+ });
3070
+ }
3071
+ this.assertAvailabilityCommitWriteCAS(commitWriteGuard);
3072
+ const booking = buildBookingFromAvailabilitySelection({
3073
+ assignments: validation.assignments,
3074
+ candidate: refreshedCandidate,
3075
+ product,
3076
+ resources: refreshedProjection.resources,
3077
+ bookingUid: existingBookingUid || void 0
3078
+ });
3079
+ const transformed = this.buildUnifiedOrderProduct({
3080
+ ...params.orderProduct,
3081
+ num: bookingCount,
3082
+ quantity: bookingCount
3083
+ }, "unified_booking_sales", product);
3084
+ if (!transformed) {
3085
+ throw new import_ResourcePlanner.AvailabilityError("INVALID_TARGET_FILTER", "orderProduct 无法转换为订单行", {
3086
+ orderProduct: params.orderProduct
3087
+ });
3088
+ }
3089
+ transformed.num = bookingCount;
3090
+ transformed.metadata = {
3091
+ ...transformed.metadata || {},
3092
+ unified_booking_sales: true,
3093
+ availability_candidate_key: refreshedCandidate.key,
3094
+ availability_context_id: normalizedContextId,
3095
+ availability_context_version: refreshedCandidate.availabilityContextVersion
3096
+ };
3097
+ let productLineUid = "";
3098
+ let bookingUid = "";
3099
+ let productLineUids = [];
3100
+ let bookingUids = [];
3101
+ if (params.mode === "edit" && editingLine && params.productLineUid) {
3102
+ transformed.product_sku = transformed.product_sku ?? editingLine.product_sku;
3103
+ transformed.product_bundle = transformed.product_bundle ?? editingLine.product_bundle;
3104
+ transformed.booking_uid = existingBookingUid;
3105
+ transformed.metadata = {
3106
+ ...editingLine.metadata || {},
3107
+ ...transformed.metadata || {},
3108
+ unique_identification_number: params.productLineUid,
3109
+ booking_uid: existingBookingUid
3110
+ };
3111
+ await this.updateOrderProduct({
3112
+ product_id: transformed.product_id ?? editingLine.product_id,
3113
+ product_variant_id: transformed.product_variant_id ?? editingLine.product_variant_id ?? 0,
3114
+ unique_identification_number: params.productLineUid,
3115
+ updates: transformed,
3116
+ booking
3117
+ });
3118
+ productLineUid = params.productLineUid;
3119
+ bookingUid = existingBookingUid;
3120
+ productLineUids = [productLineUid];
3121
+ bookingUids = [bookingUid];
3122
+ } else {
3123
+ const beforeProductLineUids = new Set(
3124
+ (((_g = this.getTempOrder()) == null ? void 0 : _g.products) || []).map((line) => getProductLineUid(line)).filter(Boolean)
3125
+ );
3126
+ const beforeBookingUids = new Set(
3127
+ (((_h = this.getTempOrder()) == null ? void 0 : _h.bookings) || []).map((item) => getBookingUid(item)).filter(Boolean)
3128
+ );
3129
+ const products = await this.addProductToOrder(transformed, booking);
3130
+ const addedLines = products.filter((line) => {
3131
+ var _a2;
3132
+ return sameAvailabilityId(line.product_id ?? line.id, refreshedCandidate.productId) && ((_a2 = line.metadata) == null ? void 0 : _a2.availability_candidate_key) === refreshedCandidate.key && !beforeProductLineUids.has(getProductLineUid(line));
3133
+ });
3134
+ productLineUids = addedLines.map((line) => getProductLineUid(line)).filter(Boolean);
3135
+ const addedBookings = (((_i = this.getTempOrder()) == null ? void 0 : _i.bookings) || []).filter((item) => {
3136
+ var _a2;
3137
+ return ((_a2 = item.metadata) == null ? void 0 : _a2.availability_candidate_key) === refreshedCandidate.key && !beforeBookingUids.has(getBookingUid(item));
3138
+ });
3139
+ bookingUids = addedBookings.map((item) => getBookingUid(item)).filter(Boolean);
3140
+ if (bookingUids.length === 0) {
3141
+ bookingUids = addedLines.map((line) => {
3142
+ var _a2;
3143
+ return String(
3144
+ line.booking_uid ?? ((_a2 = line.metadata) == null ? void 0 : _a2.booking_uid) ?? ""
3145
+ );
3146
+ }).filter(Boolean);
3147
+ }
3148
+ productLineUid = productLineUids[productLineUids.length - 1] || "";
3149
+ bookingUid = bookingUids[bookingUids.length - 1] || "";
3150
+ }
3151
+ this.invalidateAvailabilityContextsAfterCommit();
3152
+ const nextContext = this.getAvailabilityContextEntry(normalizedContextId);
3153
+ const result = {
3154
+ contextId: normalizedContextId,
3155
+ contextVersion: nextContext.activeVersion,
3156
+ tempOrder: this.getTempOrder(),
3157
+ validation,
3158
+ candidate: refreshedCandidate,
3159
+ productLineUid,
3160
+ bookingUid,
3161
+ productLineUids,
3162
+ bookingUids
3163
+ };
3164
+ await this.core.effects.emit(
3165
+ `${this.name}:${import_types.UnifiedBookingSalesHooks.onAvailabilitySelectionCommitted}`,
3166
+ result
3167
+ );
3168
+ return result;
3169
+ } finally {
3170
+ await this.releaseAvailabilityCommitLock(normalizedContextId);
1249
3171
  }
1250
- const result = {
1251
- tempOrder: this.getTempOrder(),
1252
- assignments,
1253
- validation,
1254
- submitResult
1255
- };
1256
- await this.core.effects.emit(`${this.name}:${import_types.UnifiedBookingSalesHooks.onPlannerCommitted}`, result);
1257
- return result;
1258
3172
  }
1259
3173
  };
1260
3174
  var UnifiedBookingSales = UnifiedBookingSalesImpl;
1261
3175
  // Annotate the CommonJS export names for ESM import in node:
1262
3176
  0 && (module.exports = {
3177
+ AvailabilityError,
1263
3178
  UnifiedBookingSales,
1264
3179
  UnifiedBookingSalesImpl,
1265
- buildBookingFromAssignment,
1266
- buildBookingFromAssignments,
3180
+ buildAvailabilityLineIdentity,
3181
+ buildAvailabilityResourcesFromV2,
3182
+ buildBookingFromAvailabilityAssignment,
3183
+ buildBookingFromAvailabilityAssignments,
1267
3184
  buildPlannerDiscountResult,
1268
- buildPlannerLineIdentity,
1269
- buildPlannerResourcesFromV2,
1270
- collectPlannerResourceIds,
1271
- normalizeProductForPlanner,
3185
+ collectAvailabilityResourceIds,
3186
+ normalizeProductForAvailability,
1272
3187
  ...require("./types")
1273
3188
  });