@pisell/pisellos 0.10.20 → 0.10.22

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 (88) hide show
  1. package/dist/model/strategy/adapter/promotion/index.js +0 -9
  2. package/dist/modules/BookingContext/index.d.ts +3 -0
  3. package/dist/modules/BookingContext/index.js +42 -13
  4. package/dist/modules/BookingContext/types.d.ts +4 -4
  5. package/dist/modules/BookingContext/types.js +3 -3
  6. package/dist/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +10 -7
  7. package/dist/modules/OpenData/index.d.ts +10 -1
  8. package/dist/modules/OpenData/index.js +268 -14
  9. package/dist/modules/OpenData/types.d.ts +14 -1
  10. package/dist/modules/OpenData/utils.d.ts +1 -0
  11. package/dist/modules/OpenData/utils.js +8 -5
  12. package/dist/modules/Order/index.d.ts +6 -1
  13. package/dist/modules/Order/index.js +148 -84
  14. package/dist/modules/ProductList/clientDataVariants.d.ts +50 -0
  15. package/dist/modules/ProductList/clientDataVariants.js +216 -0
  16. package/dist/modules/ProductList/index.d.ts +14 -13
  17. package/dist/modules/ProductList/index.js +105 -77
  18. package/dist/modules/Quotation/index.d.ts +16 -0
  19. package/dist/modules/Quotation/index.js +240 -27
  20. package/dist/modules/ResourcePlanner/planner.d.ts +2 -2
  21. package/dist/modules/ResourcePlanner/planner.js +88 -21
  22. package/dist/modules/ResourcePlanner/types.d.ts +12 -0
  23. package/dist/modules/Rules/index.d.ts +5 -0
  24. package/dist/modules/Rules/index.js +27 -12
  25. package/dist/plugins/request.d.ts +2 -1
  26. package/dist/plugins/request.js +4 -2
  27. package/dist/server/index.js +33 -16
  28. package/dist/solution/BaseSales/index.d.ts +16 -1
  29. package/dist/solution/BaseSales/index.js +690 -349
  30. package/dist/solution/BookingByStep/index.d.ts +1 -1
  31. package/dist/solution/BookingTicket/index.d.ts +14 -2
  32. package/dist/solution/BookingTicket/index.js +25 -18
  33. package/dist/solution/Sales/index.js +29 -22
  34. package/dist/solution/UnifiedBookingSales/index.d.ts +1 -1
  35. package/dist/solution/UnifiedBookingSales/index.js +315 -99
  36. package/dist/solution/UnifiedBookingSales/types.d.ts +4 -0
  37. package/dist/solution/VenueBooking/index.d.ts +2 -0
  38. package/dist/solution/VenueBooking/index.js +542 -494
  39. package/dist/solution/VenueBooking/utils/dateSummary.d.ts +1 -0
  40. package/dist/solution/VenueBooking/utils/dateSummary.js +3 -1
  41. package/dist/solution/VenueBooking/utils/slotMerge.d.ts +2 -0
  42. package/dist/solution/VenueBooking/utils/slotMerge.js +3 -1
  43. package/dist/utils/platform.d.ts +8 -0
  44. package/dist/utils/platform.js +15 -0
  45. package/lib/modules/BookingContext/index.d.ts +3 -0
  46. package/lib/modules/BookingContext/index.js +35 -8
  47. package/lib/modules/BookingContext/types.d.ts +4 -4
  48. package/lib/modules/BookingContext/types.js +3 -3
  49. package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +4 -0
  50. package/lib/modules/OpenData/index.d.ts +10 -1
  51. package/lib/modules/OpenData/index.js +162 -2
  52. package/lib/modules/OpenData/types.d.ts +14 -1
  53. package/lib/modules/OpenData/utils.d.ts +1 -0
  54. package/lib/modules/OpenData/utils.js +9 -5
  55. package/lib/modules/Order/index.d.ts +6 -1
  56. package/lib/modules/Order/index.js +65 -8
  57. package/lib/modules/ProductList/clientDataVariants.d.ts +50 -0
  58. package/lib/modules/ProductList/clientDataVariants.js +204 -0
  59. package/lib/modules/ProductList/index.d.ts +14 -13
  60. package/lib/modules/ProductList/index.js +77 -58
  61. package/lib/modules/Quotation/index.d.ts +16 -0
  62. package/lib/modules/Quotation/index.js +112 -9
  63. package/lib/modules/ResourcePlanner/planner.d.ts +2 -2
  64. package/lib/modules/ResourcePlanner/planner.js +80 -20
  65. package/lib/modules/ResourcePlanner/types.d.ts +12 -0
  66. package/lib/modules/Rules/index.d.ts +5 -0
  67. package/lib/modules/Rules/index.js +20 -11
  68. package/lib/plugins/request.d.ts +2 -1
  69. package/lib/plugins/request.js +3 -2
  70. package/lib/server/index.js +16 -1
  71. package/lib/solution/BaseSales/index.d.ts +16 -1
  72. package/lib/solution/BaseSales/index.js +314 -66
  73. package/lib/solution/BookingByStep/index.d.ts +1 -1
  74. package/lib/solution/BookingTicket/index.d.ts +14 -2
  75. package/lib/solution/BookingTicket/index.js +3 -0
  76. package/lib/solution/Sales/index.js +3 -0
  77. package/lib/solution/UnifiedBookingSales/index.d.ts +1 -1
  78. package/lib/solution/UnifiedBookingSales/index.js +228 -20
  79. package/lib/solution/UnifiedBookingSales/types.d.ts +4 -0
  80. package/lib/solution/VenueBooking/index.d.ts +2 -0
  81. package/lib/solution/VenueBooking/index.js +28 -8
  82. package/lib/solution/VenueBooking/utils/dateSummary.d.ts +1 -0
  83. package/lib/solution/VenueBooking/utils/dateSummary.js +3 -1
  84. package/lib/solution/VenueBooking/utils/slotMerge.d.ts +2 -0
  85. package/lib/solution/VenueBooking/utils/slotMerge.js +3 -1
  86. package/lib/utils/platform.d.ts +8 -0
  87. package/lib/utils/platform.js +22 -0
  88. package/package.json +1 -1
@@ -14,6 +14,11 @@ class QuotationModule extends _BaseModule.BaseModule {
14
14
  list: []
15
15
  };
16
16
  scheduleResolver;
17
+ quotationCache = new Map();
18
+ quotationLoadPromises = new Map();
19
+ quotationCacheVersions = new Map();
20
+ lifecycleGeneration = 0;
21
+ activeChannelKey = '';
17
22
  constructor(name, version) {
18
23
  super(name, version);
19
24
  }
@@ -24,20 +29,99 @@ class QuotationModule extends _BaseModule.BaseModule {
24
29
  this.store = {
25
30
  list: []
26
31
  };
32
+ this.quotationCache.clear();
33
+ this.quotationLoadPromises.clear();
34
+ this.quotationCacheVersions.clear();
35
+ this.activeChannelKey = '';
36
+ this.lifecycleGeneration += 1;
27
37
  }
28
38
  async loadQuotations(params) {
29
- const query = {};
30
- if (params?.channel) query.channel = params.channel;
31
- if (params?.channel === 'online_store') {
32
- query.channel = 'online-store';
39
+ const channelKey = this.normalizeChannel(params?.channel);
40
+ const cacheVersion = this.bumpQuotationCacheVersion(channelKey);
41
+ const lifecycleGeneration = this.lifecycleGeneration;
42
+ this.activateChannel(channelKey);
43
+ const loadPromise = this.fetchQuotations({
44
+ channelKey,
45
+ cacheVersion,
46
+ lifecycleGeneration
47
+ });
48
+ this.quotationLoadPromises.set(channelKey, loadPromise);
49
+ try {
50
+ await loadPromise;
51
+ } finally {
52
+ if (this.quotationLoadPromises.get(channelKey) === loadPromise) {
53
+ this.quotationLoadPromises.delete(channelKey);
54
+ }
33
55
  }
56
+ }
57
+ async ensureQuotations(params) {
58
+ const channelKey = this.normalizeChannel(params?.channel);
59
+ const lifecycleGeneration = this.lifecycleGeneration;
60
+ while (true) {
61
+ if (this.lifecycleGeneration !== lifecycleGeneration) return;
62
+ if (this.quotationCache.has(channelKey)) {
63
+ this.activateChannel(channelKey);
64
+ return;
65
+ }
66
+ const pending = this.quotationLoadPromises.get(channelKey);
67
+ if (!pending) {
68
+ try {
69
+ await this.loadQuotations(params);
70
+ } catch (error) {
71
+ if (this.quotationCache.has(channelKey)) {
72
+ this.activateChannel(channelKey);
73
+ return;
74
+ }
75
+ if (this.quotationLoadPromises.has(channelKey)) continue;
76
+ throw error;
77
+ }
78
+ continue;
79
+ }
80
+ const cacheVersion = this.getQuotationCacheVersion(channelKey);
81
+ try {
82
+ await pending;
83
+ } catch (error) {
84
+ const latestPending = this.quotationLoadPromises.get(channelKey);
85
+ if (latestPending && latestPending !== pending) continue;
86
+ if (this.getQuotationCacheVersion(channelKey) !== cacheVersion) continue;
87
+ throw error;
88
+ }
89
+ if (this.quotationCache.has(channelKey)) {
90
+ this.activateChannel(channelKey);
91
+ return;
92
+ }
93
+ const latestPending = this.quotationLoadPromises.get(channelKey);
94
+ if (latestPending && latestPending !== pending) continue;
95
+ }
96
+ }
97
+ invalidateQuotationCache(channel) {
98
+ if (channel !== undefined) {
99
+ const channelKey = this.normalizeChannel(channel);
100
+ this.bumpQuotationCacheVersion(channelKey);
101
+ this.quotationCache.delete(channelKey);
102
+ this.quotationLoadPromises.delete(channelKey);
103
+ if (this.activeChannelKey === channelKey) this.store.list = [];
104
+ return;
105
+ }
106
+ const channelKeys = new Set([...this.quotationCache.keys(), ...this.quotationLoadPromises.keys(), ...this.quotationCacheVersions.keys(), this.activeChannelKey]);
107
+ channelKeys.forEach(channelKey => this.bumpQuotationCacheVersion(channelKey));
108
+ this.quotationCache.clear();
109
+ this.quotationLoadPromises.clear();
110
+ this.store.list = [];
111
+ }
112
+ async fetchQuotations(params) {
113
+ const query = {};
114
+ if (params.channelKey) query.channel = params.channelKey;
34
115
  const res = await this.request.get('/quotation/available', query, {
35
116
  useCache: false,
36
117
  osServer: true
37
118
  });
38
- const list = res?.data?.list || res?.list || [];
39
- list.sort((a, b) => b.sort - a.sort);
40
- this.store.list = list;
119
+ const list = [...(res?.data?.list || res?.list || [])].sort((a, b) => b.sort - a.sort);
120
+ if (this.lifecycleGeneration !== params.lifecycleGeneration || this.getQuotationCacheVersion(params.channelKey) !== params.cacheVersion) {
121
+ return;
122
+ }
123
+ this.quotationCache.set(params.channelKey, list);
124
+ if (this.activeChannelKey === params.channelKey) this.store.list = list;
41
125
  }
42
126
  getQuotationList() {
43
127
  return this.store.list;
@@ -93,7 +177,8 @@ class QuotationModule extends _BaseModule.BaseModule {
93
177
  datetime,
94
178
  customer_id
95
179
  } = params;
96
- for (const quotation of this.store.list) {
180
+ const quotations = params.channel === undefined ? this.store.list : this.quotationCache.get(this.normalizeChannel(params.channel)) || [];
181
+ for (const quotation of quotations) {
97
182
  const visible = this.isQuotationVisibleForCustomer(quotation, customer_id);
98
183
  if (!visible) continue;
99
184
  const active = this.isQuotationActiveAt(quotation, datetime);
@@ -119,13 +204,15 @@ class QuotationModule extends _BaseModule.BaseModule {
119
204
  */
120
205
  buildProductPriceMap(params) {
121
206
  const map = new Map();
122
- if (!this.store.list.length) return map;
207
+ const quotations = params.channel === undefined ? this.store.list : this.quotationCache.get(this.normalizeChannel(params.channel)) || [];
208
+ if (!quotations.length) return map;
123
209
  for (const productId of params.productIds) {
124
210
  for (const timePoint of params.timePoints) {
125
211
  const key = `${productId}:${timePoint}`;
126
212
  const price = this.getPriceForProduct({
127
213
  productId,
128
214
  datetime: timePoint,
215
+ channel: params.channel,
129
216
  customer_id: params.customer_id
130
217
  });
131
218
  if (price !== null) {
@@ -138,6 +225,22 @@ class QuotationModule extends _BaseModule.BaseModule {
138
225
  setScheduleResolver(resolver) {
139
226
  this.scheduleResolver = resolver;
140
227
  }
228
+ normalizeChannel(channel) {
229
+ const normalized = channel?.trim().toLowerCase() || '';
230
+ return normalized === 'online_store' ? 'online-store' : normalized;
231
+ }
232
+ activateChannel(channelKey) {
233
+ this.activeChannelKey = channelKey;
234
+ this.store.list = this.quotationCache.get(channelKey) || [];
235
+ }
236
+ getQuotationCacheVersion(channelKey) {
237
+ return this.quotationCacheVersions.get(channelKey) || 0;
238
+ }
239
+ bumpQuotationCacheVersion(channelKey) {
240
+ const nextVersion = this.getQuotationCacheVersion(channelKey) + 1;
241
+ this.quotationCacheVersions.set(channelKey, nextVersion);
242
+ return nextVersion;
243
+ }
141
244
  isQuotationVisibleForCustomer(quotation, customerId) {
142
245
  const customers = quotation.customer || [];
143
246
  if (customers.length === 0) return true;
@@ -1,5 +1,5 @@
1
- import type { AvailabilityProjection, AvailabilityProjectionInput, AvailabilityQuery, AvailabilityQueryResult, AvailabilitySelectionValidationParams, AvailabilitySelectionValidationResult, GetProductTimeRangesRequest, ProductTimeRangeGroup } from './types';
1
+ import type { AvailabilityProjection, AvailabilityProjectionInput, AvailabilityQuery, AvailabilityQueryResult, AvailabilitySelectionValidationParams, AvailabilitySelectionValidationResult, GetProductTimeRangesRequest, GetProductTimeRangesRuntimeOptions, ProductTimeRangeGroup } from './types';
2
2
  export declare function createAvailabilityProjection(input: AvailabilityProjectionInput): AvailabilityProjection;
3
- export declare function getProductTimeRanges(projection: AvailabilityProjection, request?: GetProductTimeRangesRequest): ProductTimeRangeGroup[];
3
+ export declare function getProductTimeRanges(projection: AvailabilityProjection, request?: GetProductTimeRangesRequest, runtimeOptions?: GetProductTimeRangesRuntimeOptions): ProductTimeRangeGroup[];
4
4
  export declare function queryAvailabilityProjection(projection: AvailabilityProjection, request: AvailabilityQuery): AvailabilityQueryResult;
5
5
  export declare function validateAvailabilitySelection(projection: AvailabilityProjection, params: AvailabilitySelectionValidationParams): AvailabilitySelectionValidationResult;
@@ -11,8 +11,8 @@ var _types = require("./types");
11
11
  var _localClock = require("./localClock");
12
12
  const DEFAULT_SLOT_STEP_MINUTES = 30;
13
13
  const DEFAULT_BUSINESS_HOURS = {
14
- startTime: '09:00',
15
- endTime: '18:00'
14
+ startTime: '00:00',
15
+ endTime: '00:00'
16
16
  };
17
17
  function availabilityError(code, message, details) {
18
18
  return new _types.AvailabilityError(code, message, details);
@@ -48,6 +48,19 @@ function normalizeBookingCount(bookingCount) {
48
48
  }
49
49
  return normalized;
50
50
  }
51
+ function resolveRequestedDurationMinutes(product, request) {
52
+ const configured = Math.max(1, Number(product.durationMinutes || 30) || 30);
53
+ if (product.kind !== 'duration') return configured;
54
+ const override = request.durationMinutesByProductId?.[String(product.id)];
55
+ const normalized = Number(override);
56
+ return Number.isInteger(normalized) && normalized > 0 ? normalized : configured;
57
+ }
58
+ function resolveRequestedDurationStartTimes(product, request) {
59
+ if (product.kind !== 'duration') return [];
60
+ const values = request.durationStartTimesByProductId?.[String(product.id)];
61
+ if (!Array.isArray(values)) return [];
62
+ return Array.from(new Set(values.filter(value => typeof value === 'string' && /^([01]\d|2[0-3]):[0-5]\d$/.test(value))));
63
+ }
51
64
  function toDateTime(value, _timezoneName) {
52
65
  try {
53
66
  return (0, _localClock.localDateTimeToScalar)((0, _localClock.parseLocalDateTime)(value));
@@ -75,6 +88,9 @@ function addMinutesZoned(value, minutes, _timezoneName) {
75
88
  function ceilToTenMinutes(value, _timezoneName) {
76
89
  return (0, _localClock.ceilLocalScalarToTenMinutes)(value);
77
90
  }
91
+ function floorToMinute(value) {
92
+ return Math.floor(value / 60000) * 60000;
93
+ }
78
94
  function nextDate(value, _timezoneName) {
79
95
  return (0, _localClock.nextDate)(value);
80
96
  }
@@ -865,9 +881,30 @@ function dedupeProductRanges(ranges) {
865
881
  });
866
882
  return Array.from(map.values()).sort((left, right) => left.startAt.localeCompare(right.startAt));
867
883
  }
868
- function getProductTimeRanges(projection, request = {}) {
884
+ function createDurationTimeRange(product, start, durationMinutes, projection, fixedOffsetMinutes) {
885
+ const bookingEnd = addMinutesZoned(start, durationMinutes, projection.meta.timezone);
886
+ return createDurationTimeRangeWithEnd(product, start, bookingEnd, projection, fixedOffsetMinutes);
887
+ }
888
+ function createDurationTimeRangeWithEnd(product, start, bookingEnd, projection, fixedOffsetMinutes) {
889
+ return {
890
+ key: `${String(product.id)}:${start}:${bookingEnd}`,
891
+ productId: product.id,
892
+ source: 'duration_window',
893
+ startAt: formatDateTime(start, projection.meta.timezone, fixedOffsetMinutes),
894
+ endAt: formatDateTime(bookingEnd, projection.meta.timezone, fixedOffsetMinutes),
895
+ bookingStartAt: formatDateTime(start, projection.meta.timezone, fixedOffsetMinutes),
896
+ bookingEndAt: formatDateTime(bookingEnd, projection.meta.timezone, fixedOffsetMinutes),
897
+ date: formatDate(start, projection.meta.timezone),
898
+ startTime: formatTime(start, projection.meta.timezone),
899
+ endTime: formatTime(bookingEnd, projection.meta.timezone),
900
+ timezone: projection.meta.timezone,
901
+ scheduleRefs: []
902
+ };
903
+ }
904
+ function getProductTimeRanges(projection, request = {}, runtimeOptions = {}) {
869
905
  const normalizedPartySize = normalizePartySize(request.partySize);
870
906
  const dateRange = resolveProjectionDateRange(projection, request.dateRange);
907
+ const slotStepMinutes = projection.meta.defaults.slotStepMinutes;
871
908
  const fixedOffsetMinutes = (0, _localClock.extractFixedOffsetMinutes)(projection.meta.now) ?? (0, _localClock.captureRuntimeOffsetMinutes)();
872
909
  const resourceWindowIndex = buildResourceWindowIndex(projection);
873
910
  const filteredProductIdSet = request.productIds?.length ? new Set(request.productIds.map(String)) : null;
@@ -877,6 +914,7 @@ function getProductTimeRanges(projection, request = {}) {
877
914
  productId: product.id,
878
915
  title: product.title,
879
916
  kind: product.kind,
917
+ slotStepMinutes,
880
918
  ranges: []
881
919
  };
882
920
  }
@@ -885,6 +923,7 @@ function getProductTimeRanges(projection, request = {}) {
885
923
  productId: product.id,
886
924
  title: product.title,
887
925
  kind: product.kind,
926
+ slotStepMinutes,
888
927
  ranges: getSessionRangesForProduct(projection, product, dateRange)
889
928
  };
890
929
  }
@@ -900,11 +939,30 @@ function getProductTimeRanges(projection, request = {}) {
900
939
  resourceWindowIndex
901
940
  })).reduce((current, ranges) => current === null ? ranges : intersectRangeLists(current, ranges), null) || [] : null;
902
941
  const baseRanges = resourceRanges === null ? operatingRanges : intersectRangeLists(resourceRanges, operatingRanges);
903
- const durationMinutes = Math.max(1, Number(product.durationMinutes || 30) || 30);
904
- const timeSliceMinutes = projection.meta.defaults.slotStepMinutes;
942
+ const durationMinutes = resolveRequestedDurationMinutes(product, request);
943
+ const flexibleStartAt = (() => {
944
+ if (!product.isFlexibleDuration) return undefined;
945
+ const evaluatedAt = toDateTime(runtimeOptions.evaluatedAt || projection.meta.durationCandidateAnchorAt, projection.meta.timezone);
946
+ if (!Number.isFinite(evaluatedAt)) {
947
+ throw availabilityError('INVALID_TARGET_FILTER', 'runtimeOptions.evaluatedAt 必须是有效时间', {
948
+ evaluatedAt: runtimeOptions.evaluatedAt
949
+ });
950
+ }
951
+ return floorToMinute(evaluatedAt);
952
+ })();
905
953
  const durationAnchor = ceilToTenMinutes(toDateTime(projection.meta.durationCandidateAnchorAt, projection.meta.timezone), projection.meta.timezone);
954
+ const requestedStartTimes = resolveRequestedDurationStartTimes(product, request);
906
955
  const ranges = [];
907
956
  baseRanges.forEach(range => {
957
+ if (product.isFlexibleDuration && flexibleStartAt !== undefined) {
958
+ const start = Math.max(range.start, flexibleStartAt);
959
+ if (start >= range.end) return;
960
+ const bookingDate = formatDate(start);
961
+ if (bookingDate >= dateRange.startDate && bookingDate <= dateRange.endDate) {
962
+ ranges.push(createDurationTimeRangeWithEnd(product, start, range.end, projection, fixedOffsetMinutes));
963
+ }
964
+ return;
965
+ }
908
966
  if (range.end <= durationAnchor) return;
909
967
  let cursor = range.start;
910
968
  if (cursor < durationAnchor) {
@@ -915,28 +973,28 @@ function getProductTimeRanges(projection, request = {}) {
915
973
  if (bookingEnd > range.end) break;
916
974
  const bookingDate = formatDate(cursor);
917
975
  if (bookingDate >= dateRange.startDate && bookingDate <= dateRange.endDate) {
918
- ranges.push({
919
- key: `${String(product.id)}:${cursor}:${bookingEnd}`,
920
- productId: product.id,
921
- source: 'duration_window',
922
- startAt: formatDateTime(cursor, projection.meta.timezone, fixedOffsetMinutes),
923
- endAt: formatDateTime(bookingEnd, projection.meta.timezone, fixedOffsetMinutes),
924
- bookingStartAt: formatDateTime(cursor, projection.meta.timezone, fixedOffsetMinutes),
925
- bookingEndAt: formatDateTime(bookingEnd, projection.meta.timezone, fixedOffsetMinutes),
926
- date: formatDate(cursor, projection.meta.timezone),
927
- startTime: formatTime(cursor, projection.meta.timezone),
928
- endTime: formatTime(bookingEnd, projection.meta.timezone),
929
- timezone: projection.meta.timezone,
930
- scheduleRefs: []
931
- });
976
+ ranges.push(createDurationTimeRange(product, cursor, durationMinutes, projection, fixedOffsetMinutes));
932
977
  }
933
- cursor = addMinutesZoned(cursor, timeSliceMinutes, projection.meta.timezone);
978
+ cursor = addMinutesZoned(cursor, slotStepMinutes, projection.meta.timezone);
934
979
  }
935
980
  });
981
+ if (dateRange.startDate === dateRange.endDate && requestedStartTimes.length > 0) {
982
+ requestedStartTimes.forEach(startTime => {
983
+ const start = toDateTimeInDate(dateRange.startDate, startTime, projection.meta.timezone);
984
+ if (!Number.isFinite(start)) return;
985
+ const end = addMinutesZoned(start, durationMinutes, projection.meta.timezone);
986
+ const startsInOperatingRange = operatingRanges.some(range => range.start <= start && start < range.end);
987
+ if (!startsInOperatingRange && !request.allowOutsideOperatingHoursForRequestedStartTimes) {
988
+ return;
989
+ }
990
+ ranges.push(createDurationTimeRange(product, start, durationMinutes, projection, fixedOffsetMinutes));
991
+ });
992
+ }
936
993
  return {
937
994
  productId: product.id,
938
995
  title: product.title,
939
996
  kind: product.kind,
997
+ slotStepMinutes,
940
998
  ranges: dedupeProductRanges(ranges)
941
999
  };
942
1000
  });
@@ -1560,6 +1618,8 @@ function queryAvailabilityProjection(projection, request) {
1560
1618
  dateRange,
1561
1619
  resourceIds: request.resourceIds,
1562
1620
  partySize: normalizedPartySize
1621
+ }, {
1622
+ evaluatedAt
1563
1623
  });
1564
1624
  const candidateGroupMap = new Map(candidateGroups.map(group => [String(group.productId), group]));
1565
1625
  if (request.target === 'date') {
@@ -81,6 +81,7 @@ export interface AvailabilityProduct {
81
81
  id: AvailabilityId;
82
82
  title?: string;
83
83
  kind: AvailabilityProductKind;
84
+ isFlexibleDuration?: boolean;
84
85
  durationMinutes?: number;
85
86
  quantity?: number;
86
87
  capacityRequired?: number;
@@ -211,6 +212,7 @@ export interface ProductTimeRangeGroup {
211
212
  productId: AvailabilityId;
212
213
  title?: string;
213
214
  kind: AvailabilityProductKind;
215
+ slotStepMinutes?: number;
214
216
  ranges: ProductTimeRange[];
215
217
  }
216
218
  export interface AvailabilityAssignment extends AvailabilityAbsoluteRange {
@@ -231,6 +233,16 @@ export interface GetProductTimeRangesRequest {
231
233
  dateRange?: Partial<AvailabilityDateRange>;
232
234
  resourceIds?: AvailabilityId[];
233
235
  partySize?: number;
236
+ durationMinutesByProductId?: Record<string, number>;
237
+ durationStartTimesByProductId?: Record<string, string[]>;
238
+ /**
239
+ * POS 手工输入时间的兼容开关:只为明确请求的 start time 补候选,
240
+ * 不改变常规候选列表仍按营业时间生成的规则。
241
+ */
242
+ allowOutsideOperatingHoursForRequestedStartTimes?: boolean;
243
+ }
244
+ export interface GetProductTimeRangesRuntimeOptions {
245
+ evaluatedAt?: string;
234
246
  }
235
247
  export interface QueryAvailabilityBaseRequest {
236
248
  productIds?: AvailabilityId[];
@@ -14,6 +14,11 @@ export declare class RulesModule extends BaseModule implements Module, RulesModu
14
14
  setRulesList(rulesList: Rules[]): Promise<void>;
15
15
  getRulesList(): Rules[];
16
16
  getWalletPassEvaluator(): import("../..").WalletPassEvaluator | undefined;
17
+ private normalizeHolderRecordIds;
18
+ /**
19
+ * 特定 form Holder 卡只授权给绑定同一 Holder Record 的商品行。
20
+ * required/isNeedHolder 仅控制是否强制选择 Holder,不参与优惠资格。
21
+ */
17
22
  private checkHolderMatch;
18
23
  isDiscountListAvailable({ oldDiscountList, newDiscountList, productList, orderTotalAmount, holders, isFormSubject, }: {
19
24
  oldDiscountList: Discount[];
@@ -38,21 +38,30 @@ class RulesModule extends _BaseModule.BaseModule {
38
38
  getWalletPassEvaluator() {
39
39
  return this.window.getWalletPassEvaluator?.();
40
40
  }
41
+ normalizeHolderRecordIds(value) {
42
+ const records = Array.isArray(value) ? value : [value];
43
+ return Array.from(new Set(records.map(record => {
44
+ if (record && typeof record === 'object') {
45
+ return record.form_record_id;
46
+ }
47
+ return record;
48
+ }).filter(record => record !== undefined && record !== null && record !== '' && record !== 0 && record !== '0').map(String)));
49
+ }
41
50
 
42
- // 商品不需要holder,则不需要判断,直接返回true,商品需要holder但是还没填写,那么暂时不使用带有holder的券,直到填写才去匹配
51
+ /**
52
+ * 特定 form Holder 卡只授权给绑定同一 Holder Record 的商品行。
53
+ * required/isNeedHolder 仅控制是否强制选择 Holder,不参与优惠资格。
54
+ */
43
55
  checkHolderMatch(discount, product, holders) {
44
56
  // 非表单类型 holder 视为匹配
45
57
  if (discount.holder?.holder_type !== 'form') return true;
46
- // 主预约holder, 目前(20251124)默认只考虑单个holder的情况
47
- // 旧版 holders[0].form_record_id 新版 holders[0]
48
- const orderHolderId = Array.isArray(holders) && holders.length > 0 ? holders[0].form_record_id || holders[0] : undefined;
49
- const productHolderId = Array.isArray(product.holder_id) ? product.holder_id[0] : product.holder_id;
50
- // 商品不需要选holder,则不对比holder,直接返回true,
51
- if (!product.isNeedHolder) return true;
52
- // 商品需要holder,但父预约及商品都无holder,返回false
53
- if (!orderHolderId && !productHolderId) return false;
54
- // 最终直接匹配 holder 是否相同
55
- return (productHolderId || orderHolderId) === discount.holder.holder_id;
58
+ const discountHolderIds = this.normalizeHolderRecordIds(discount.holder.holder_id);
59
+ if (!discountHolderIds.length) return false;
60
+ const productHolderIds = this.normalizeHolderRecordIds(product.holder_id);
61
+ const candidateHolderIds = productHolderIds.length ? productHolderIds : this.normalizeHolderRecordIds(holders);
62
+ if (!candidateHolderIds.length) return false;
63
+ const discountHolderIdSet = new Set(discountHolderIds);
64
+ return candidateHolderIds.some(holderId => discountHolderIdSet.has(holderId));
56
65
  }
57
66
 
58
67
  // 判断discountList 是否可以对当前productList生效
@@ -69,6 +69,7 @@ export interface RequestOptions {
69
69
  customToast?: () => void;
70
70
  cache?: CacheProps;
71
71
  osServer?: boolean;
72
+ isNocobase?: boolean;
72
73
  prefix?: boolean;
73
74
  callback?: (res: any) => void;
74
75
  subscriberId?: string;
@@ -124,7 +125,7 @@ declare class RequestPluginImpl implements RequestPlugin {
124
125
  /**
125
126
  * GET 请求
126
127
  */
127
- get<T = any>(url: string, options?: RequestOptions): Promise<T>;
128
+ get<T = any>(url: string, dataOrOptions?: any, options?: RequestOptions): Promise<T>;
128
129
  /**
129
130
  * POST 请求
130
131
  */
@@ -99,9 +99,10 @@ class RequestPluginImpl {
99
99
  /**
100
100
  * GET 请求
101
101
  */
102
- get(url, options = {}) {
102
+ get(url, dataOrOptions = {}, options) {
103
+ const requestOptions = options ?? dataOrOptions;
103
104
  return this.request({
104
- ...options,
105
+ ...requestOptions,
105
106
  url,
106
107
  method: 'GET'
107
108
  });
@@ -4720,7 +4720,22 @@ class Server {
4720
4720
  if (!this.shouldBuildSmallTicketData(order)) return order;
4721
4721
  if (options?.requireFullyPaid && !this.isSalesOrderFullyPaid(order)) return order;
4722
4722
  try {
4723
- const enrichedOrder = await this.enrichPaymentNamesByCode(order);
4723
+ const normalizedCollections = (0, _orderCollectionIdentity.normalizeOrderCollections)(order, {
4724
+ ensureUid: false
4725
+ });
4726
+ const normalizedOrder = normalizedCollections.order;
4727
+ const collapsedDuplicateCounts = {
4728
+ products: normalizedCollections.stats.product.collapsedDuplicateCount,
4729
+ bookings: normalizedCollections.stats.booking.collapsedDuplicateCount,
4730
+ payments: normalizedCollections.stats.payment.collapsedDuplicateCount
4731
+ };
4732
+ if (Object.values(collapsedDuplicateCounts).some(count => count > 0)) {
4733
+ this.logWarning('withLocalSmallTicketData: 折叠重复订单子项', {
4734
+ order_identifier: normalizedOrder.order_id ?? normalizedOrder.external_sale_number ?? normalizedOrder.shop_order_number ?? null,
4735
+ collapsed_duplicate_counts: collapsedDuplicateCounts
4736
+ });
4737
+ }
4738
+ const enrichedOrder = await this.enrichPaymentNamesByCode(normalizedOrder);
4724
4739
  const productMap = options?.productMap ?? (await this.buildSmallTicketProductMap(enrichedOrder));
4725
4740
  const smallTicketData = (0, _smallTicket.buildSmallTicketData)({
4726
4741
  order: enrichedOrder,
@@ -129,8 +129,23 @@ export declare class BaseSalesImpl extends BaseModule implements Module {
129
129
  */
130
130
  protected preferAuthoritativeProductQueryPrice(): boolean;
131
131
  private getPriceQuerySchedule;
132
+ private isCustomerUserPriceQuery;
133
+ private getPriceQueryScheduleList;
134
+ private buildPriceQueryPayload;
135
+ private getRawProductPriceSources;
136
+ private rememberRawProductPriceSources;
137
+ private getSelectedProductVariantId;
138
+ /**
139
+ * 将目录原始商品还原为本次选择的 SKU 基线。
140
+ *
141
+ * 这里绝不能复用已评估后的目录商品,否则从命中智能价切回不命中的
142
+ * 日期时会残留旧价格。套餐仅验证所选关系存在,最终仍由 merge 方法把
143
+ * 评估后的套餐价格映射回用户的选择数据。
144
+ */
145
+ private prepareRawProductForPriceQuery;
146
+ private resolveLocalProductForPriceQuery;
132
147
  private loadProductsForPriceQuery;
133
- private loadProductForPriceQuery;
148
+ private resolveProductsForPriceQuery;
134
149
  private getAuthoritativeBundleItems;
135
150
  private findAuthoritativeBundleItem;
136
151
  private getAuthoritativeBundleUnitPrice;