@pisell/pisellos 0.10.21 → 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 (67) 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/Order/index.d.ts +5 -0
  8. package/dist/modules/Order/index.js +148 -84
  9. package/dist/modules/ProductList/clientDataVariants.d.ts +50 -0
  10. package/dist/modules/ProductList/clientDataVariants.js +216 -0
  11. package/dist/modules/ProductList/index.d.ts +14 -13
  12. package/dist/modules/ProductList/index.js +105 -77
  13. package/dist/modules/Quotation/index.d.ts +16 -0
  14. package/dist/modules/Quotation/index.js +240 -27
  15. package/dist/modules/ResourcePlanner/planner.d.ts +2 -2
  16. package/dist/modules/ResourcePlanner/planner.js +88 -21
  17. package/dist/modules/ResourcePlanner/types.d.ts +12 -0
  18. package/dist/modules/Rules/index.d.ts +5 -0
  19. package/dist/modules/Rules/index.js +27 -12
  20. package/dist/solution/BaseSales/index.d.ts +16 -1
  21. package/dist/solution/BaseSales/index.js +667 -349
  22. package/dist/solution/BookingByStep/index.d.ts +1 -1
  23. package/dist/solution/UnifiedBookingSales/index.d.ts +1 -1
  24. package/dist/solution/UnifiedBookingSales/index.js +290 -81
  25. package/dist/solution/UnifiedBookingSales/types.d.ts +4 -0
  26. package/dist/solution/VenueBooking/index.d.ts +2 -0
  27. package/dist/solution/VenueBooking/index.js +542 -494
  28. package/dist/solution/VenueBooking/utils/dateSummary.d.ts +1 -0
  29. package/dist/solution/VenueBooking/utils/dateSummary.js +3 -1
  30. package/dist/solution/VenueBooking/utils/slotMerge.d.ts +2 -0
  31. package/dist/solution/VenueBooking/utils/slotMerge.js +3 -1
  32. package/dist/utils/platform.d.ts +8 -0
  33. package/dist/utils/platform.js +15 -0
  34. package/lib/model/strategy/adapter/promotion/index.js +46 -1
  35. package/lib/modules/BookingContext/index.d.ts +3 -0
  36. package/lib/modules/BookingContext/index.js +35 -8
  37. package/lib/modules/BookingContext/types.d.ts +4 -4
  38. package/lib/modules/BookingContext/types.js +3 -3
  39. package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +4 -0
  40. package/lib/modules/Order/index.d.ts +5 -0
  41. package/lib/modules/Order/index.js +65 -8
  42. package/lib/modules/ProductList/clientDataVariants.d.ts +50 -0
  43. package/lib/modules/ProductList/clientDataVariants.js +204 -0
  44. package/lib/modules/ProductList/index.d.ts +14 -13
  45. package/lib/modules/ProductList/index.js +77 -58
  46. package/lib/modules/Quotation/index.d.ts +16 -0
  47. package/lib/modules/Quotation/index.js +112 -9
  48. package/lib/modules/ResourcePlanner/planner.d.ts +2 -2
  49. package/lib/modules/ResourcePlanner/planner.js +80 -20
  50. package/lib/modules/ResourcePlanner/types.d.ts +12 -0
  51. package/lib/modules/Rules/index.d.ts +5 -0
  52. package/lib/modules/Rules/index.js +20 -11
  53. package/lib/solution/BaseSales/index.d.ts +16 -1
  54. package/lib/solution/BaseSales/index.js +293 -66
  55. package/lib/solution/BookingByStep/index.d.ts +1 -1
  56. package/lib/solution/UnifiedBookingSales/index.d.ts +1 -1
  57. package/lib/solution/UnifiedBookingSales/index.js +225 -20
  58. package/lib/solution/UnifiedBookingSales/types.d.ts +4 -0
  59. package/lib/solution/VenueBooking/index.d.ts +2 -0
  60. package/lib/solution/VenueBooking/index.js +28 -8
  61. package/lib/solution/VenueBooking/utils/dateSummary.d.ts +1 -0
  62. package/lib/solution/VenueBooking/utils/dateSummary.js +3 -1
  63. package/lib/solution/VenueBooking/utils/slotMerge.d.ts +2 -0
  64. package/lib/solution/VenueBooking/utils/slotMerge.js +3 -1
  65. package/lib/utils/platform.d.ts +8 -0
  66. package/lib/utils/platform.js +22 -0
  67. package/package.json +1 -1
@@ -32,6 +32,7 @@ exports.normalizeProductForAvailability = normalizeProductForAvailability;
32
32
  var _dayjs = _interopRequireDefault(require("dayjs"));
33
33
  var _lodashEs = require("lodash-es");
34
34
  var _modules = require("../../modules");
35
+ var _utils = require("../../modules/BookingContext/utils");
35
36
  var _OpenData = require("../../modules/OpenData");
36
37
  var _getDateIsInSchedule = require("../../modules/Schedule/getDateIsInSchedule");
37
38
  var _ResourcePlanner = require("../../modules/ResourcePlanner");
@@ -50,6 +51,7 @@ Object.keys(_types).forEach(function (key) {
50
51
  var _remoteOccupancyCache = require("./remoteOccupancyCache");
51
52
  var _BookingTicket = require("../BookingTicket");
52
53
  var _localClock = require("../../modules/ResourcePlanner/localClock");
54
+ var _platform = require("../../utils/platform");
53
55
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
54
56
  const OPEN_DATA_SECTION_CODES = ['sale', 'reservation', 'fulfillment', 'menu', 'workflow', 'basic', 'checkout', 'availability', 'documents'];
55
57
  const OPEN_DATA_CACHE_TTL = 5 * 60 * 1000;
@@ -58,6 +60,30 @@ function normalizePositiveInteger(value) {
58
60
  if (!Number.isInteger(normalized) || normalized <= 0) return undefined;
59
61
  return normalized;
60
62
  }
63
+ function getCandidateDurationMinutes(candidate) {
64
+ const startAt = candidate.bookingStartAt || candidate.startAt;
65
+ const endAt = candidate.bookingEndAt || candidate.endAt;
66
+ try {
67
+ const start = (0, _localClock.localDateTimeToScalar)((0, _localClock.parseLocalDateTime)(startAt));
68
+ const end = (0, _localClock.localDateTimeToScalar)((0, _localClock.parseLocalDateTime)(endAt));
69
+ const minutes = Math.round((end - start) / 60000);
70
+ return Number.isInteger(minutes) && minutes > 0 ? minutes : undefined;
71
+ } catch (_error) {
72
+ return undefined;
73
+ }
74
+ }
75
+ function getCandidateStartTime(candidate) {
76
+ if (/^([01]\d|2[0-3]):[0-5]\d$/.test(candidate.startTime || '')) {
77
+ return candidate.startTime;
78
+ }
79
+ const startAt = candidate.bookingStartAt || candidate.startAt;
80
+ try {
81
+ const start = (0, _localClock.parseLocalDateTime)(startAt);
82
+ return `${String(start.hour).padStart(2, '0')}:${String(start.minute).padStart(2, '0')}`;
83
+ } catch (_error) {
84
+ return undefined;
85
+ }
86
+ }
61
87
  function isRecord(value) {
62
88
  return !!value && typeof value === 'object' && !Array.isArray(value);
63
89
  }
@@ -166,8 +192,9 @@ function filterCustomerBoundDiscounts(discountList) {
166
192
  }
167
193
  const DEFAULT_AVAILABILITY_SLOT_STEP_MINUTES = 30;
168
194
  const DEFAULT_AVAILABILITY_BUSINESS_HOURS = {
169
- startTime: '09:00',
170
- endTime: '18:00'
195
+ // Same start/end is interpreted as an overnight range, i.e. 24 hours.
196
+ startTime: '00:00',
197
+ endTime: '00:00'
171
198
  };
172
199
  const MAX_AVAILABILITY_RANGE_DAYS = 90;
173
200
  const MAX_AVAILABILITY_CONTEXT_REGISTRY = 128;
@@ -461,6 +488,7 @@ function resolveProductScheduleItems(product, scheduleList) {
461
488
  function normalizeProductForAvailability(product, scheduleList = [], fixedOffsetMinutes = (0, _localClock.captureRuntimeOffsetMinutes)()) {
462
489
  const raw = product;
463
490
  const kind = resolveProductKind(raw);
491
+ const isFlexibleDuration = kind === 'duration' && (0, _utils.isProductFlexibleDuration)(raw.duration);
464
492
  const sessionRules = [];
465
493
  const sessionRanges = [];
466
494
  const explicitSession = raw.cartDetailValue?.session || raw._extend?.other?.session || raw.session;
@@ -482,7 +510,8 @@ function normalizeProductForAvailability(product, scheduleList = [], fixedOffset
482
510
  id: raw.id ?? raw.product_id,
483
511
  title: pickLocalizedText(raw.title || raw.name || raw.product_title),
484
512
  kind,
485
- durationMinutes: kind === 'duration' ? Math.max(1, Number(raw.duration?.value || raw.duration || raw.service_duration || 30) || 30) : undefined,
513
+ isFlexibleDuration: isFlexibleDuration || undefined,
514
+ durationMinutes: kind === 'duration' && !isFlexibleDuration ? Math.max(1, Number(raw.duration?.value || raw.duration || raw.service_duration || 30) || 30) : undefined,
486
515
  requirementGroups: normalizeRequirementGroups(raw),
487
516
  sessionRules,
488
517
  sessionRanges,
@@ -981,6 +1010,125 @@ function buildBookingFromAvailabilitySelection(params) {
981
1010
  }
982
1011
  };
983
1012
  }
1013
+ const POS_OVERRIDABLE_AVAILABILITY_CONFLICT_CODES = new Set(['past', 'booking_cutoff', 'resource_full', 'resource_unavailable', 'resource_capacity_exceeded',
1014
+ // A required group whose selected resource is outside its window is surfaced
1015
+ // as missing_resource at group level. Structural validation below still
1016
+ // rejects a genuinely missing selection.
1017
+ 'missing_resource']);
1018
+ function matchesForcedSelectionGroup(selection, group) {
1019
+ const hasGroupId = typeof selection.requirementGroupId === 'string' && selection.requirementGroupId.trim().length > 0;
1020
+ const hasFormId = selection.formId !== undefined && selection.formId !== null;
1021
+ if (!hasGroupId && !hasFormId) return false;
1022
+ if (hasGroupId && selection.requirementGroupId !== group.id) return false;
1023
+ if (hasFormId && !sameAvailabilityId(selection.formId, group.formId)) return false;
1024
+ return true;
1025
+ }
1026
+
1027
+ /**
1028
+ * POS may intentionally overbook a configured time/resource. Rebuild the
1029
+ * assignments from the submitted, structurally valid resource identities so
1030
+ * a full/unavailable option is not silently dropped by normal availability
1031
+ * evaluation. This deliberately ignores capacity, occupancy and resource
1032
+ * windows, while retaining product/group/resource identity and selection
1033
+ * cardinality checks.
1034
+ */
1035
+ function buildPosForcedAvailabilityAssignments(params) {
1036
+ const {
1037
+ product,
1038
+ resources,
1039
+ candidate,
1040
+ requirementSelections,
1041
+ partySize
1042
+ } = params;
1043
+ const groups = product.requirementGroups || [];
1044
+ const conflicts = [];
1045
+ const assignments = [];
1046
+ const selectionsByGroup = new Map();
1047
+ const addConflict = (code, message, details = {}) => conflicts.push({
1048
+ code,
1049
+ message,
1050
+ productId: candidate.productId,
1051
+ candidateKey: candidate.key,
1052
+ ...details
1053
+ });
1054
+ requirementSelections.forEach(selection => {
1055
+ const matchedGroups = groups.filter(group => matchesForcedSelectionGroup(selection, group));
1056
+ if (matchedGroups.length !== 1) {
1057
+ addConflict('invalid_requirement_group', '选择的资源组不属于当前商品', {
1058
+ requirementGroupId: selection.requirementGroupId,
1059
+ formId: selection.formId
1060
+ });
1061
+ return;
1062
+ }
1063
+ const group = matchedGroups[0];
1064
+ if (selectionsByGroup.has(group.id)) {
1065
+ addConflict('invalid_selection_count', `资源组 ${group.id} 被重复提交`, {
1066
+ requirementGroupId: group.id,
1067
+ formId: group.formId
1068
+ });
1069
+ return;
1070
+ }
1071
+ selectionsByGroup.set(group.id, selection);
1072
+ if (!Array.isArray(selection.resourceIds)) {
1073
+ addConflict('invalid_selection_count', `资源组 ${group.id} 的资源选择必须是数组`, {
1074
+ requirementGroupId: group.id,
1075
+ formId: group.formId
1076
+ });
1077
+ return;
1078
+ }
1079
+ const selectedResourceIds = Array.from(new Map(selection.resourceIds.map(resourceId => [String(resourceId), resourceId])).values());
1080
+ const required = group.required !== false;
1081
+ const min = Math.max(required ? 1 : 0, Number(group.min ?? (required ? 1 : 0)) || 0);
1082
+ const max = Math.max(min, Number(group.max ?? Math.max(min, 1)) || Math.max(min, 1));
1083
+ if (selectedResourceIds.length < min) {
1084
+ addConflict('missing_resource', `资源组 ${group.id} 缺少已选资源`, {
1085
+ requirementGroupId: group.id,
1086
+ formId: group.formId
1087
+ });
1088
+ return;
1089
+ }
1090
+ if (selectedResourceIds.length > max) {
1091
+ addConflict('invalid_selection_count', `资源组 ${group.id} 已选资源数量超出上限`, {
1092
+ requirementGroupId: group.id,
1093
+ formId: group.formId
1094
+ });
1095
+ return;
1096
+ }
1097
+ selectedResourceIds.forEach(resourceId => {
1098
+ const resource = resources.find(item => sameAvailabilityId(item.id, resourceId));
1099
+ const belongsToGroup = Boolean(resource && !(group.formId !== undefined && resource.formId !== undefined && !sameAvailabilityId(group.formId, resource.formId)) && !(group.resourceIds?.length && !group.resourceIds.some(id => sameAvailabilityId(id, resourceId))));
1100
+ if (!resource || !belongsToGroup) {
1101
+ addConflict('resource_unavailable', `资源 ${String(resourceId)} 不属于当前商品资源组`, {
1102
+ requirementGroupId: group.id,
1103
+ formId: group.formId,
1104
+ resourceId
1105
+ });
1106
+ return;
1107
+ }
1108
+ assignments.push({
1109
+ productId: product.id,
1110
+ resourceId,
1111
+ formId: group.formId ?? resource.formId,
1112
+ startAt: candidate.bookingStartAt || candidate.startAt,
1113
+ endAt: candidate.bookingEndAt || candidate.endAt,
1114
+ capacityRequired: Math.max(1, partySize),
1115
+ scheduleId: candidate.primaryScheduleRef?.scheduleId,
1116
+ timeSlotId: candidate.primaryScheduleRef?.timeSlotId
1117
+ });
1118
+ });
1119
+ });
1120
+ groups.filter(group => group.required !== false).forEach(group => {
1121
+ if (selectionsByGroup.has(group.id)) return;
1122
+ addConflict('missing_resource', `资源组 ${group.id} 缺少已选资源`, {
1123
+ requirementGroupId: group.id,
1124
+ formId: group.formId
1125
+ });
1126
+ });
1127
+ return {
1128
+ assignments,
1129
+ conflicts
1130
+ };
1131
+ }
984
1132
  class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
985
1133
  defaultName = 'unifiedBookingSales';
986
1134
  defaultVersion = '1.0.0';
@@ -1425,8 +1573,7 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1425
1573
  return true;
1426
1574
  }
1427
1575
  isAuthenticatedCustomerUserPlatform() {
1428
- const platform = String(this.otherParams?.platform || '').toLowerCase();
1429
- return platform === 'pc' || platform === 'h5';
1576
+ return (0, _platform.isCustomerUserPlatform)(this.otherParams?.platform);
1430
1577
  }
1431
1578
  resolveDiscountOrderIdentity(tempOrder) {
1432
1579
  return tempOrder?.order_id || this.store.order?.getOrderIdentity?.()?.orderId || null;
@@ -1558,7 +1705,11 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1558
1705
  if (!normalizedPolicyId) {
1559
1706
  throw new Error('[UnifiedBookingSales] getProtocol 需要 policyId');
1560
1707
  }
1561
- return this.request.get(`/shop-policy/${encodeURIComponent(normalizedPolicyId)}`);
1708
+ let policyUrl = 'shop-policy';
1709
+ if (this.otherParams.platform === 'kiosk' || this.otherParams.platform === 'pos' || this.otherParams.platform === 'shop') {
1710
+ policyUrl = 'policy';
1711
+ }
1712
+ return this.request.get(`/${policyUrl}/${encodeURIComponent(normalizedPolicyId)}`);
1562
1713
  }
1563
1714
 
1564
1715
  /**
@@ -1750,6 +1901,7 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1750
1901
  }
1751
1902
  buildUnifiedOrderProduct(sourceProduct, ownershipKey, product) {
1752
1903
  const normalizedProductRaw = product?.raw || {};
1904
+ const quotationShelfId = sourceProduct._extend?.quotation_shelf_id ?? sourceProduct.quotation_shelf_id ?? sourceProduct.metadata?.quotation_shelf_id;
1753
1905
  const orderProduct = this.transformBaseProductToOrderProduct({
1754
1906
  payload: {
1755
1907
  ...sourceProduct,
@@ -1766,11 +1918,14 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1766
1918
  sourceProduct
1767
1919
  });
1768
1920
  if (orderProduct && typeof orderProduct === 'object') {
1769
- // BaseSales may rebuild metadata from its price source. Stamp ownership after that transform
1770
- // so each UnifiedBookingSales entry point can replace only its own temporary lines.
1921
+ // BaseSales may rebuild metadata from its price source. Stamp ownership and the selected
1922
+ // quotation shelf after that transform so neither booking adds nor edits lose quote identity.
1771
1923
  orderProduct.metadata = {
1772
1924
  ...(orderProduct.metadata || {}),
1773
- [ownershipKey]: true
1925
+ [ownershipKey]: true,
1926
+ ...(quotationShelfId !== undefined ? {
1927
+ quotation_shelf_id: quotationShelfId
1928
+ } : {})
1774
1929
  };
1775
1930
  }
1776
1931
  return orderProduct;
@@ -2660,14 +2815,22 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
2660
2815
  return handle;
2661
2816
  }
2662
2817
 
2663
- /** Pure configuration calculation over the current context projection. */
2818
+ /** Candidate calculation over the current context projection and runtime minute. */
2664
2819
  async getProductTimeRanges(contextId, request = {}) {
2665
2820
  const {
2666
2821
  context,
2667
2822
  projectionEntry
2668
2823
  } = await this.ensureAvailabilityContextProjection(contextId, 'get_product_time_ranges');
2669
2824
  const contextVersion = context.activeVersion;
2670
- return (0, _ResourcePlanner.getProductTimeRanges)(projectionEntry.projection, request).map(group => ({
2825
+ const allowOutsideOperatingHoursForRequestedStartTimes = String(this.otherParams?.platform ?? '').trim().toLowerCase() === 'pos' && Object.values(request.durationStartTimesByProductId || {}).some(startTimes => Array.isArray(startTimes) && startTimes.length > 0);
2826
+ return (0, _ResourcePlanner.getProductTimeRanges)(projectionEntry.projection, {
2827
+ ...request,
2828
+ ...(allowOutsideOperatingHoursForRequestedStartTimes ? {
2829
+ allowOutsideOperatingHoursForRequestedStartTimes: true
2830
+ } : {})
2831
+ }, {
2832
+ evaluatedAt: this.getAvailabilityRuntimeDateTime()
2833
+ }).map(group => ({
2671
2834
  ...group,
2672
2835
  ranges: group.ranges.map(candidate => this.decorateAvailabilityCandidate(context.contextId, contextVersion, candidate))
2673
2836
  }));
@@ -2915,12 +3078,28 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
2915
3078
  });
2916
3079
  }
2917
3080
  this.assertAvailabilityCommitWriteCAS(commitWriteGuard);
3081
+ const candidateDurationMinutes = getCandidateDurationMinutes(params.candidate);
3082
+ const candidateStartTime = getCandidateStartTime(params.candidate);
3083
+ const allowPosAvailabilityOverride = String(this.otherParams?.platform ?? '').trim().toLowerCase() === 'pos';
2918
3084
  const refreshedRanges = (0, _ResourcePlanner.getProductTimeRanges)(refreshedProjection, {
2919
3085
  productIds: [params.candidate.productId],
2920
3086
  dateRange: {
2921
3087
  startDate: params.candidate.startAt.slice(0, 10),
2922
3088
  endDate: params.candidate.startAt.slice(0, 10)
2923
- }
3089
+ },
3090
+ ...(candidateDurationMinutes ? {
3091
+ durationMinutesByProductId: {
3092
+ [String(params.candidate.productId)]: candidateDurationMinutes
3093
+ },
3094
+ ...(candidateStartTime ? {
3095
+ durationStartTimesByProductId: {
3096
+ [String(params.candidate.productId)]: [candidateStartTime]
3097
+ },
3098
+ ...(allowPosAvailabilityOverride ? {
3099
+ allowOutsideOperatingHoursForRequestedStartTimes: true
3100
+ } : {})
3101
+ } : {})
3102
+ } : {})
2924
3103
  }).flatMap(group => group.ranges);
2925
3104
  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);
2926
3105
  if (!refreshedCandidateBase) {
@@ -2930,12 +3109,38 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
2930
3109
  refreshedSnapshotId: refreshedProjection.meta.snapshotId
2931
3110
  });
2932
3111
  }
2933
- const validation = (0, _ResourcePlanner.validateAvailabilitySelection)(refreshedProjection, {
3112
+ const product = refreshedProjection.products.find(item => sameAvailabilityId(item.id, refreshedCandidateBase.productId));
3113
+ if (!product) {
3114
+ throw new _ResourcePlanner.AvailabilityError('REVALIDATION_FAILED', '缓存重校验后商品不在 projection 中', {
3115
+ productId: refreshedCandidateBase.productId
3116
+ });
3117
+ }
3118
+ let validation = (0, _ResourcePlanner.validateAvailabilitySelection)(refreshedProjection, {
2934
3119
  candidate: refreshedCandidateBase,
2935
3120
  requirementSelections: params.requirementSelections,
2936
3121
  partySize,
2937
3122
  bookingCount
2938
3123
  });
3124
+ if (allowPosAvailabilityOverride && !validation.ok && validation.conflicts.every(conflict => POS_OVERRIDABLE_AVAILABILITY_CONFLICT_CODES.has(conflict.code))) {
3125
+ const forced = buildPosForcedAvailabilityAssignments({
3126
+ product,
3127
+ resources: refreshedProjection.resources,
3128
+ candidate: refreshedCandidateBase,
3129
+ requirementSelections: params.requirementSelections,
3130
+ partySize
3131
+ });
3132
+ validation = forced.conflicts.length > 0 ? {
3133
+ ...validation,
3134
+ assignments: [],
3135
+ conflicts: forced.conflicts
3136
+ } : {
3137
+ ...validation,
3138
+ ok: true,
3139
+ status: validation.availability.status,
3140
+ assignments: forced.assignments,
3141
+ conflicts: []
3142
+ };
3143
+ }
2939
3144
  if (!validation.ok) {
2940
3145
  const capacityConflict = validation.conflicts.find(conflict => conflict.code === 'resource_capacity_exceeded');
2941
3146
  throw new _ResourcePlanner.AvailabilityError('REVALIDATION_FAILED', capacityConflict?.message || '缓存重校验后资源已不可提交', {
@@ -2973,12 +3178,6 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
2973
3178
  expectedOrderRevision: revalidationEntry.orderRevision
2974
3179
  };
2975
3180
  const refreshedCandidate = this.decorateAvailabilityCandidate(normalizedContextId, revalidatedContext.activeVersion, refreshedCandidateBase);
2976
- const product = refreshedProjection.products.find(item => sameAvailabilityId(item.id, refreshedCandidate.productId));
2977
- if (!product) {
2978
- throw new _ResourcePlanner.AvailabilityError('REVALIDATION_FAILED', '缓存重校验后商品不在 projection 中', {
2979
- productId: refreshedCandidate.productId
2980
- });
2981
- }
2982
3181
  const existingBooking = editingLine ? this.findLinkedBooking(editingLine) : undefined;
2983
3182
  const existingBookingUid = existingBooking ? getBookingUid(existingBooking) : '';
2984
3183
  if (params.mode === 'edit' && !existingBookingUid) {
@@ -3033,7 +3232,13 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
3033
3232
  product_variant_id: transformed.product_variant_id ?? editingLine.product_variant_id ?? 0,
3034
3233
  unique_identification_number: params.productLineUid,
3035
3234
  updates: transformed,
3036
- booking
3235
+ booking,
3236
+ ...(params.allowBookingReprice !== undefined ? {
3237
+ allow_booking_reprice: params.allowBookingReprice
3238
+ } : {}),
3239
+ ...(params.allowBookingDiscountReapply !== undefined ? {
3240
+ allow_booking_discount_reapply: params.allowBookingDiscountReapply
3241
+ } : {})
3037
3242
  });
3038
3243
  productLineUid = params.productLineUid;
3039
3244
  bookingUid = existingBookingUid;
@@ -151,6 +151,10 @@ export interface ContextualCommitAvailabilitySelectionParams {
151
151
  requirementSelections: AvailabilityRequirementSelection[];
152
152
  partySize: number;
153
153
  bookingCount?: number;
154
+ /** 编辑预约时,允许本次显式报价覆盖已落库订单行的历史价格。 */
155
+ allowBookingReprice?: boolean;
156
+ /** 编辑预约重报价后,允许订单模块重新应用已选编辑态折扣。 */
157
+ allowBookingDiscountReapply?: boolean;
154
158
  }
155
159
  export type UnifiedBookingSalesCommitAvailabilitySelectionParams = ContextualCommitAvailabilitySelectionParams;
156
160
  export interface UnifiedBookingSalesCommitAvailabilitySelectionResult {
@@ -90,6 +90,8 @@ export declare class VenueBookingImpl extends BaseSalesImpl implements Module {
90
90
  loadAddonProducts(): Promise<ProductData[]>;
91
91
  getVenueProducts(): ProductData[];
92
92
  getAddonProductsList(): ProductData[];
93
+ private ensureQuotationsLoaded;
94
+ private preloadQuotations;
93
95
  loadQuotations(params?: {
94
96
  channel?: string;
95
97
  }): Promise<void>;
@@ -370,7 +370,7 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
370
370
  });
371
371
  }
372
372
  if (this.store.quotation) {
373
- await this.store.quotation.loadQuotations({
373
+ await this.ensureQuotationsLoaded({
374
374
  channel: this.otherParams?.channel
375
375
  });
376
376
  }
@@ -566,7 +566,7 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
566
566
  if (this.store.schedule) {
567
567
  await this.store.schedule.loadAllSchedule();
568
568
  this.injectScheduleResolverToQuotation();
569
- this.store.quotation?.loadQuotations({
569
+ this.preloadQuotations({
570
570
  channel: this.otherParams?.channel
571
571
  });
572
572
  }
@@ -720,6 +720,19 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
720
720
 
721
721
  // ─── 报价单 ───
722
722
 
723
+ async ensureQuotationsLoaded(params) {
724
+ const quotation = this.store.quotation;
725
+ if (!quotation) return;
726
+ const load = typeof quotation.ensureQuotations === 'function' ? quotation.ensureQuotations.bind(quotation) : quotation.loadQuotations.bind(quotation);
727
+ await load(params);
728
+ }
729
+ preloadQuotations(params) {
730
+ void this.ensureQuotationsLoaded(params).catch(error => {
731
+ this.logMethodError('preloadQuotations', error, {
732
+ channel: params?.channel
733
+ });
734
+ });
735
+ }
723
736
  async loadQuotations(params) {
724
737
  this.logMethodStart('loadQuotations');
725
738
  try {
@@ -854,6 +867,7 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
854
867
  rawResources: this.store.rawResourceData,
855
868
  resourceProductMap: this.resourceProductMap,
856
869
  quotationModule: this.store.quotation,
870
+ channel: this.otherParams?.channel,
857
871
  resolveConfig: date => this.resolveSlotConfigForDate(date)
858
872
  });
859
873
  }
@@ -866,7 +880,8 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
866
880
  const timePoints = timeLabels.map(label => `${date} ${label}`);
867
881
  quotationPriceMap = this.store.quotation.buildProductPriceMap({
868
882
  productIds,
869
- timePoints
883
+ timePoints,
884
+ channel: this.otherParams?.channel
870
885
  });
871
886
  }
872
887
  return (0, _timeSlot.buildTimeSlotGrid)({
@@ -1185,7 +1200,8 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
1185
1200
  price_breakdown: (0, _slotMerge.buildPriceBreakdown)({
1186
1201
  group,
1187
1202
  productId: mapping.productId,
1188
- quotation: this.store.quotation
1203
+ quotation: this.store.quotation,
1204
+ channel: this.otherParams?.channel
1189
1205
  })
1190
1206
  },
1191
1207
  _origin: venueProductOrigin
@@ -1325,7 +1341,8 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
1325
1341
  productId: mapping.productId,
1326
1342
  price: this.store.quotation.getPriceForProduct({
1327
1343
  productId: mapping.productId,
1328
- datetime: slot.startTime
1344
+ datetime: slot.startTime,
1345
+ channel: this.otherParams?.channel
1329
1346
  }) ?? mapping.price
1330
1347
  }));
1331
1348
  const merged = (0, _slotMerge.mergeConsecutiveSlots)(updatedSlots);
@@ -1335,14 +1352,16 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
1335
1352
  product.metadata.price_breakdown = (0, _slotMerge.buildPriceBreakdown)({
1336
1353
  group: merged[0],
1337
1354
  productId: mapping.productId,
1338
- quotation: this.store.quotation
1355
+ quotation: this.store.quotation,
1356
+ channel: this.otherParams?.channel
1339
1357
  });
1340
1358
  }
1341
1359
  } else if (product.product_id != null) {
1342
1360
  const quotationPrice = this.store.quotation.getPriceForProduct({
1343
1361
  productId: product.product_id,
1344
1362
  variantId: product.product_variant_id ?? undefined,
1345
- datetime: now
1363
+ datetime: now,
1364
+ channel: this.otherParams?.channel
1346
1365
  });
1347
1366
  if (quotationPrice !== null) {
1348
1367
  product.selling_price = quotationPrice;
@@ -1550,7 +1569,8 @@ class VenueBookingImpl extends _BaseSales.BaseSalesImpl {
1550
1569
  const quotationPrice = this.store.quotation.getPriceForProduct({
1551
1570
  productId: product.product_id,
1552
1571
  variantId: product.product_variant_id ?? undefined,
1553
- datetime: (0, _dayjs.default)().format('YYYY-MM-DD HH:mm:ss')
1572
+ datetime: (0, _dayjs.default)().format('YYYY-MM-DD HH:mm:ss'),
1573
+ channel: this.otherParams?.channel
1554
1574
  });
1555
1575
  if (quotationPrice !== null) {
1556
1576
  product.selling_price = quotationPrice;
@@ -7,5 +7,6 @@ export declare function buildDateRangeSummary(params: {
7
7
  rawResources: VenueResourceRawData[];
8
8
  resourceProductMap: Map<number | string, ResourceProductMapping[]>;
9
9
  quotationModule?: QuotationModule;
10
+ channel?: string;
10
11
  resolveConfig?: (date: string) => VenueBookingSlotConfig;
11
12
  }): VenueDateSummaryItem[];
@@ -15,6 +15,7 @@ function buildDateRangeSummary(params) {
15
15
  rawResources,
16
16
  resourceProductMap,
17
17
  quotationModule,
18
+ channel,
18
19
  resolveConfig
19
20
  } = params;
20
21
  const result = [];
@@ -30,7 +31,8 @@ function buildDateRangeSummary(params) {
30
31
  const timePoints = timeLabels.map(label => `${date} ${label}`);
31
32
  quotationPriceMap = quotationModule.buildProductPriceMap({
32
33
  productIds,
33
- timePoints
34
+ timePoints,
35
+ channel
34
36
  });
35
37
  }
36
38
  const grid = (0, _timeSlot.buildTimeSlotGrid)({
@@ -5,10 +5,12 @@ export declare function mergeConsecutiveSlots(slots: VenueSlotSelection[]): Merg
5
5
  export declare function buildPriceBreakdown(params: {
6
6
  group: MergedSlotGroup;
7
7
  productId: number;
8
+ channel?: string;
8
9
  quotation?: {
9
10
  getQuotationShelfId: (p: {
10
11
  productId: number;
11
12
  datetime: string;
13
+ channel?: string;
12
14
  }) => number;
13
15
  };
14
16
  }): PriceBreakdownEntry[];
@@ -57,6 +57,7 @@ function buildPriceBreakdown(params) {
57
57
  const {
58
58
  group,
59
59
  productId,
60
+ channel,
60
61
  quotation
61
62
  } = params;
62
63
  if (!group.slots.length) return [];
@@ -65,7 +66,8 @@ function buildPriceBreakdown(params) {
65
66
  const unitPrice = Number(slot.price || '0');
66
67
  const shelfId = quotation ? quotation.getQuotationShelfId({
67
68
  productId,
68
- datetime: slot.startTime
69
+ datetime: slot.startTime,
70
+ channel
69
71
  }) : 0;
70
72
  const startHm = (0, _dayjs.default)(slot.startTime, 'YYYY-MM-DD HH:mm').format('YYYY-MM-DD HH:mm');
71
73
  const endHm = (0, _dayjs.default)(slot.endTime, 'YYYY-MM-DD HH:mm').format('YYYY-MM-DD HH:mm');
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Normalize the runtime platform passed through solution/module otherParams.
3
+ */
4
+ export declare function normalizeRuntimePlatform(platform: unknown): string;
5
+ /**
6
+ * PC and H5 are authenticated-customer clients rather than staff terminals.
7
+ */
8
+ export declare function isCustomerUserPlatform(platform: unknown): boolean;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.isCustomerUserPlatform = isCustomerUserPlatform;
7
+ exports.normalizeRuntimePlatform = normalizeRuntimePlatform;
8
+ const CUSTOMER_USER_PLATFORMS = new Set(['pc', 'h5']);
9
+
10
+ /**
11
+ * Normalize the runtime platform passed through solution/module otherParams.
12
+ */
13
+ function normalizeRuntimePlatform(platform) {
14
+ return String(platform ?? '').trim().toLowerCase();
15
+ }
16
+
17
+ /**
18
+ * PC and H5 are authenticated-customer clients rather than staff terminals.
19
+ */
20
+ function isCustomerUserPlatform(platform) {
21
+ return CUSTOMER_USER_PLATFORMS.has(normalizeRuntimePlatform(platform));
22
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@pisell/pisellos",
4
- "version": "0.10.21",
4
+ "version": "0.10.22",
5
5
  "description": "一个可扩展的前端模块化SDK框架,支持插件系统",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",