@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.
- package/dist/model/strategy/adapter/promotion/index.js +0 -9
- package/dist/modules/BookingContext/index.d.ts +3 -0
- package/dist/modules/BookingContext/index.js +42 -13
- package/dist/modules/BookingContext/types.d.ts +4 -4
- package/dist/modules/BookingContext/types.js +3 -3
- package/dist/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +10 -7
- package/dist/modules/Order/index.d.ts +5 -0
- package/dist/modules/Order/index.js +148 -84
- package/dist/modules/ProductList/clientDataVariants.d.ts +50 -0
- package/dist/modules/ProductList/clientDataVariants.js +216 -0
- package/dist/modules/ProductList/index.d.ts +14 -13
- package/dist/modules/ProductList/index.js +105 -77
- package/dist/modules/Quotation/index.d.ts +16 -0
- package/dist/modules/Quotation/index.js +240 -27
- package/dist/modules/ResourcePlanner/planner.d.ts +2 -2
- package/dist/modules/ResourcePlanner/planner.js +88 -21
- package/dist/modules/ResourcePlanner/types.d.ts +12 -0
- package/dist/modules/Rules/index.d.ts +5 -0
- package/dist/modules/Rules/index.js +27 -12
- package/dist/solution/BaseSales/index.d.ts +16 -1
- package/dist/solution/BaseSales/index.js +667 -349
- package/dist/solution/BookingByStep/index.d.ts +1 -1
- package/dist/solution/UnifiedBookingSales/index.d.ts +1 -1
- package/dist/solution/UnifiedBookingSales/index.js +290 -81
- package/dist/solution/UnifiedBookingSales/types.d.ts +4 -0
- package/dist/solution/VenueBooking/index.d.ts +2 -0
- package/dist/solution/VenueBooking/index.js +542 -494
- package/dist/solution/VenueBooking/utils/dateSummary.d.ts +1 -0
- package/dist/solution/VenueBooking/utils/dateSummary.js +3 -1
- package/dist/solution/VenueBooking/utils/slotMerge.d.ts +2 -0
- package/dist/solution/VenueBooking/utils/slotMerge.js +3 -1
- package/dist/utils/platform.d.ts +8 -0
- package/dist/utils/platform.js +15 -0
- package/lib/model/strategy/adapter/promotion/index.js +46 -1
- package/lib/modules/BookingContext/index.d.ts +3 -0
- package/lib/modules/BookingContext/index.js +35 -8
- package/lib/modules/BookingContext/types.d.ts +4 -4
- package/lib/modules/BookingContext/types.js +3 -3
- package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +4 -0
- package/lib/modules/Order/index.d.ts +5 -0
- package/lib/modules/Order/index.js +65 -8
- package/lib/modules/ProductList/clientDataVariants.d.ts +50 -0
- package/lib/modules/ProductList/clientDataVariants.js +204 -0
- package/lib/modules/ProductList/index.d.ts +14 -13
- package/lib/modules/ProductList/index.js +77 -58
- package/lib/modules/Quotation/index.d.ts +16 -0
- package/lib/modules/Quotation/index.js +112 -9
- package/lib/modules/ResourcePlanner/planner.d.ts +2 -2
- package/lib/modules/ResourcePlanner/planner.js +80 -20
- package/lib/modules/ResourcePlanner/types.d.ts +12 -0
- package/lib/modules/Rules/index.d.ts +5 -0
- package/lib/modules/Rules/index.js +20 -11
- package/lib/solution/BaseSales/index.d.ts +16 -1
- package/lib/solution/BaseSales/index.js +293 -66
- package/lib/solution/BookingByStep/index.d.ts +1 -1
- package/lib/solution/UnifiedBookingSales/index.d.ts +1 -1
- package/lib/solution/UnifiedBookingSales/index.js +225 -20
- package/lib/solution/UnifiedBookingSales/types.d.ts +4 -0
- package/lib/solution/VenueBooking/index.d.ts +2 -0
- package/lib/solution/VenueBooking/index.js +28 -8
- package/lib/solution/VenueBooking/utils/dateSummary.d.ts +1 -0
- package/lib/solution/VenueBooking/utils/dateSummary.js +3 -1
- package/lib/solution/VenueBooking/utils/slotMerge.d.ts +2 -0
- package/lib/solution/VenueBooking/utils/slotMerge.js +3 -1
- package/lib/utils/platform.d.ts +8 -0
- package/lib/utils/platform.js +22 -0
- 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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1770
|
-
//
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
+
}
|