@pisell/pisellos 0.10.21 → 0.10.23

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 (73) hide show
  1. package/dist/modules/BookingContext/index.d.ts +3 -0
  2. package/dist/modules/BookingContext/index.js +42 -13
  3. package/dist/modules/BookingContext/types.d.ts +4 -4
  4. package/dist/modules/BookingContext/types.js +3 -3
  5. package/dist/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +10 -7
  6. package/dist/modules/OpenData/index.d.ts +1 -0
  7. package/dist/modules/OpenData/index.js +28 -9
  8. package/dist/modules/Order/index.d.ts +5 -0
  9. package/dist/modules/Order/index.js +148 -84
  10. package/dist/modules/ProductList/clientDataVariants.d.ts +50 -0
  11. package/dist/modules/ProductList/clientDataVariants.js +216 -0
  12. package/dist/modules/ProductList/index.d.ts +14 -13
  13. package/dist/modules/ProductList/index.js +105 -77
  14. package/dist/modules/Quotation/index.d.ts +16 -0
  15. package/dist/modules/Quotation/index.js +240 -27
  16. package/dist/modules/ResourcePlanner/planner.d.ts +2 -2
  17. package/dist/modules/ResourcePlanner/planner.js +88 -21
  18. package/dist/modules/ResourcePlanner/types.d.ts +12 -0
  19. package/dist/modules/Rules/index.d.ts +5 -0
  20. package/dist/modules/Rules/index.js +27 -12
  21. package/dist/solution/BaseSales/index.d.ts +16 -1
  22. package/dist/solution/BaseSales/index.js +667 -349
  23. package/dist/solution/BookingByStep/index.d.ts +1 -1
  24. package/dist/solution/BookingTicket/index.d.ts +1 -1
  25. package/dist/solution/Sales/index.js +5 -3
  26. package/dist/solution/UnifiedBookingSales/index.d.ts +1 -1
  27. package/dist/solution/UnifiedBookingSales/index.js +290 -81
  28. package/dist/solution/UnifiedBookingSales/types.d.ts +4 -0
  29. package/dist/solution/VenueBooking/index.d.ts +2 -0
  30. package/dist/solution/VenueBooking/index.js +542 -494
  31. package/dist/solution/VenueBooking/utils/dateSummary.d.ts +1 -0
  32. package/dist/solution/VenueBooking/utils/dateSummary.js +3 -1
  33. package/dist/solution/VenueBooking/utils/slotMerge.d.ts +2 -0
  34. package/dist/solution/VenueBooking/utils/slotMerge.js +3 -1
  35. package/dist/utils/platform.d.ts +8 -0
  36. package/dist/utils/platform.js +15 -0
  37. package/lib/modules/BookingContext/index.d.ts +3 -0
  38. package/lib/modules/BookingContext/index.js +35 -8
  39. package/lib/modules/BookingContext/types.d.ts +4 -4
  40. package/lib/modules/BookingContext/types.js +3 -3
  41. package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +4 -0
  42. package/lib/modules/OpenData/index.d.ts +1 -0
  43. package/lib/modules/OpenData/index.js +13 -0
  44. package/lib/modules/Order/index.d.ts +5 -0
  45. package/lib/modules/Order/index.js +65 -8
  46. package/lib/modules/ProductList/clientDataVariants.d.ts +50 -0
  47. package/lib/modules/ProductList/clientDataVariants.js +204 -0
  48. package/lib/modules/ProductList/index.d.ts +14 -13
  49. package/lib/modules/ProductList/index.js +77 -58
  50. package/lib/modules/Quotation/index.d.ts +16 -0
  51. package/lib/modules/Quotation/index.js +112 -9
  52. package/lib/modules/ResourcePlanner/planner.d.ts +2 -2
  53. package/lib/modules/ResourcePlanner/planner.js +80 -20
  54. package/lib/modules/ResourcePlanner/types.d.ts +12 -0
  55. package/lib/modules/Rules/index.d.ts +5 -0
  56. package/lib/modules/Rules/index.js +20 -11
  57. package/lib/solution/BaseSales/index.d.ts +16 -1
  58. package/lib/solution/BaseSales/index.js +293 -66
  59. package/lib/solution/BookingByStep/index.d.ts +1 -1
  60. package/lib/solution/BookingTicket/index.d.ts +1 -1
  61. package/lib/solution/Sales/index.js +2 -1
  62. package/lib/solution/UnifiedBookingSales/index.d.ts +1 -1
  63. package/lib/solution/UnifiedBookingSales/index.js +225 -20
  64. package/lib/solution/UnifiedBookingSales/types.d.ts +4 -0
  65. package/lib/solution/VenueBooking/index.d.ts +2 -0
  66. package/lib/solution/VenueBooking/index.js +28 -8
  67. package/lib/solution/VenueBooking/utils/dateSummary.d.ts +1 -0
  68. package/lib/solution/VenueBooking/utils/dateSummary.js +3 -1
  69. package/lib/solution/VenueBooking/utils/slotMerge.d.ts +2 -0
  70. package/lib/solution/VenueBooking/utils/slotMerge.js +3 -1
  71. package/lib/utils/platform.d.ts +8 -0
  72. package/lib/utils/platform.js +22 -0
  73. package/package.json +1 -1
@@ -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生效
@@ -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;