@pisell/pisellos 2.3.144 → 2.3.146
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/modules/BookingContext/utils/productExtend.d.ts +7 -0
- package/dist/modules/BookingContext/utils/productExtend.js +62 -18
- package/dist/modules/Order/index.d.ts +2 -1
- package/dist/modules/Order/index.js +380 -198
- package/dist/modules/Order/types.d.ts +9 -0
- package/dist/modules/ResourcePlanner/planner.d.ts +2 -0
- package/dist/modules/ResourcePlanner/planner.js +327 -22
- package/dist/modules/ResourcePlanner/types.d.ts +29 -0
- package/dist/solution/BookingByStep/index.d.ts +1 -1
- package/dist/solution/BookingTicket/utils/addProductDecision.d.ts +0 -1
- package/dist/solution/BookingTicket/utils/addProductDecision.js +1 -1
- package/dist/solution/BookingTicket/utils/scan/applyGlobalScan.js +6 -2
- package/dist/solution/UnifiedBookingSales/index.d.ts +3 -0
- package/dist/solution/UnifiedBookingSales/index.js +432 -163
- package/dist/solution/UnifiedBookingSales/types.d.ts +14 -2
- package/lib/modules/BookingContext/utils/productExtend.d.ts +7 -0
- package/lib/modules/BookingContext/utils/productExtend.js +45 -2
- package/lib/modules/Order/index.d.ts +2 -1
- package/lib/modules/Order/index.js +92 -10
- package/lib/modules/Order/types.d.ts +9 -0
- package/lib/modules/ResourcePlanner/planner.d.ts +2 -0
- package/lib/modules/ResourcePlanner/planner.js +247 -7
- package/lib/modules/ResourcePlanner/types.d.ts +29 -0
- package/lib/solution/BookingByStep/index.d.ts +1 -1
- package/lib/solution/BookingTicket/utils/addProductDecision.d.ts +0 -1
- package/lib/solution/BookingTicket/utils/addProductDecision.js +1 -1
- package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.js +7 -1
- package/lib/solution/UnifiedBookingSales/index.d.ts +3 -0
- package/lib/solution/UnifiedBookingSales/index.js +220 -27
- package/lib/solution/UnifiedBookingSales/types.d.ts +14 -2
- package/package.json +1 -1
|
@@ -33,6 +33,7 @@ var _dayjs = _interopRequireDefault(require("dayjs"));
|
|
|
33
33
|
var _lodashEs = require("lodash-es");
|
|
34
34
|
var _modules = require("../../modules");
|
|
35
35
|
var _utils = require("../../modules/BookingContext/utils");
|
|
36
|
+
var _cacheItemToBookingInput = require("../../modules/BookingContext/utils/cacheItemToBookingInput");
|
|
36
37
|
var _OpenData = require("../../modules/OpenData");
|
|
37
38
|
var _getDateIsInSchedule = require("../../modules/Schedule/getDateIsInSchedule");
|
|
38
39
|
var _ResourcePlanner = require("../../modules/ResourcePlanner");
|
|
@@ -1306,6 +1307,26 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
1306
1307
|
businessTimezone: String(otherParams.businessTimezone ?? '')
|
|
1307
1308
|
};
|
|
1308
1309
|
}
|
|
1310
|
+
usesDurationTimeGrid(projection) {
|
|
1311
|
+
const platform = String(this.otherParams?.platform ?? '').trim().toLowerCase();
|
|
1312
|
+
return (platform === 'pos' || platform === 'kiosk') && (!projection || projection.products.some(product => product.kind === 'duration'));
|
|
1313
|
+
}
|
|
1314
|
+
getCurrentAvailabilityBusinessDate(projection) {
|
|
1315
|
+
return (0, _ResourcePlanner.getDurationBusinessDate)(projection, this.getAvailabilityRuntimeDateTime());
|
|
1316
|
+
}
|
|
1317
|
+
getAvailabilityBusinessDateRange(projection) {
|
|
1318
|
+
const prepare = projection.meta.prepareParams;
|
|
1319
|
+
const range = prepare?.dateRange;
|
|
1320
|
+
if (!this.usesDurationTimeGrid(projection) || !range?.startDate || !range.endDate) {
|
|
1321
|
+
return projection.meta.coverage;
|
|
1322
|
+
}
|
|
1323
|
+
const businessDate = this.getCurrentAvailabilityBusinessDate(projection);
|
|
1324
|
+
const today = this.getAvailabilityRuntimeDateTime().slice(0, 10);
|
|
1325
|
+
return {
|
|
1326
|
+
startDate: range.startDate === today && businessDate < today ? businessDate : range.startDate,
|
|
1327
|
+
endDate: range.endDate
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1309
1330
|
normalizeAvailabilityIdList(values) {
|
|
1310
1331
|
if (!Array.isArray(values) || values.length === 0) return undefined;
|
|
1311
1332
|
const uniqueMap = new Map();
|
|
@@ -1509,8 +1530,15 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
1509
1530
|
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
1510
1531
|
}
|
|
1511
1532
|
createAvailabilityContextRef(context) {
|
|
1533
|
+
const projection = this.getActiveAvailabilityProjectionEntry(context)?.projection;
|
|
1512
1534
|
return {
|
|
1513
|
-
contextId: context.contextId
|
|
1535
|
+
contextId: context.contextId,
|
|
1536
|
+
...(projection && this.usesDurationTimeGrid(projection) ? {
|
|
1537
|
+
durationTimeSelection: {
|
|
1538
|
+
version: 1,
|
|
1539
|
+
currentBusinessDate: this.getCurrentAvailabilityBusinessDate(projection)
|
|
1540
|
+
}
|
|
1541
|
+
} : {})
|
|
1514
1542
|
};
|
|
1515
1543
|
}
|
|
1516
1544
|
async emitAvailabilityDiagnostic(payload) {
|
|
@@ -2413,7 +2441,9 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
2413
2441
|
return range ? [{
|
|
2414
2442
|
startAt: range.startAt,
|
|
2415
2443
|
endAt: range.endAt,
|
|
2416
|
-
source: descriptor.source
|
|
2444
|
+
source: descriptor.source,
|
|
2445
|
+
businessDate: date,
|
|
2446
|
+
openingAnchorAt: range.startAt
|
|
2417
2447
|
}] : [];
|
|
2418
2448
|
});
|
|
2419
2449
|
return {
|
|
@@ -2437,7 +2467,8 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
2437
2467
|
return range ? [{
|
|
2438
2468
|
startAt: range.startAt,
|
|
2439
2469
|
endAt: range.endAt,
|
|
2440
|
-
source: `operating_schedule:${descriptor.scheduleIds.map(String).join(',')}
|
|
2470
|
+
source: `operating_schedule:${descriptor.scheduleIds.map(String).join(',')}`,
|
|
2471
|
+
businessDate: date
|
|
2441
2472
|
}] : [];
|
|
2442
2473
|
}));
|
|
2443
2474
|
const excludedWindows = dateRangeDays(params.dateRange).flatMap(date => schedules.flatMap(schedule => getExcludedScheduleDateOverrideTimePoints(date, schedule).flatMap(slot => {
|
|
@@ -2451,12 +2482,23 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
2451
2482
|
startAt: range.startAt,
|
|
2452
2483
|
endAt: range.endAt,
|
|
2453
2484
|
source: `operating_schedule_excluded:${schedule.id}`,
|
|
2485
|
+
businessDate: date,
|
|
2454
2486
|
raw: {
|
|
2455
2487
|
scheduleId: schedule.id,
|
|
2456
2488
|
excludedDate: date
|
|
2457
2489
|
}
|
|
2458
2490
|
}] : [];
|
|
2459
2491
|
})));
|
|
2492
|
+
// All lunch-break windows share the first opening of their business day.
|
|
2493
|
+
const openings = new Map();
|
|
2494
|
+
[...windows, ...excludedWindows].forEach(window => {
|
|
2495
|
+
const date = window.businessDate || window.startAt.slice(0, 10);
|
|
2496
|
+
const previous = openings.get(date);
|
|
2497
|
+
if (!previous || window.startAt < previous) openings.set(date, window.startAt);
|
|
2498
|
+
});
|
|
2499
|
+
[...windows, ...excludedWindows].forEach(window => {
|
|
2500
|
+
window.openingAnchorAt = openings.get(window.businessDate || window.startAt.slice(0, 10));
|
|
2501
|
+
});
|
|
2460
2502
|
return {
|
|
2461
2503
|
source: descriptor.source,
|
|
2462
2504
|
scheduleIds: [...descriptor.scheduleIds],
|
|
@@ -2846,24 +2888,34 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
2846
2888
|
const normalizedPrepareParams = this.normalizeAvailabilityPrepareDescriptor(params);
|
|
2847
2889
|
const timezoneLabel = String(normalizedPrepareParams.timezone);
|
|
2848
2890
|
const dateRange = normalizedPrepareParams.dateRange;
|
|
2891
|
+
const scheduleList = this.getAvailabilityScheduleCatalog();
|
|
2892
|
+
const products = await this.loadAvailabilityProducts({
|
|
2893
|
+
prepare: normalizedPrepareParams,
|
|
2894
|
+
dateRange,
|
|
2895
|
+
scheduleList
|
|
2896
|
+
});
|
|
2897
|
+
// Coverage is physical local time; the caller's date range remains business
|
|
2898
|
+
// dates. Keep yesterday's opening and the last business day's overnight tail.
|
|
2899
|
+
const useDurationTimeGrid = this.usesDurationTimeGrid() && products.some(product => product.kind === 'duration');
|
|
2900
|
+
const coverageDateRange = useDurationTimeGrid ? {
|
|
2901
|
+
startDate: (0, _localClock.formatLocalDate)((0, _localClock.addLocalCalendarDays)((0, _localClock.localDateToScalarStart)((0, _localClock.parseLocalDate)(dateRange.startDate)), -1)),
|
|
2902
|
+
endDate: (0, _localClock.formatLocalDate)((0, _localClock.addLocalCalendarDays)((0, _localClock.localDateToScalarStart)((0, _localClock.parseLocalDate)(dateRange.endDate)), 1))
|
|
2903
|
+
} : dateRange;
|
|
2849
2904
|
const slotStepMinutes = normalizedPrepareParams.slotStepMinutes || DEFAULT_AVAILABILITY_SLOT_STEP_MINUTES;
|
|
2850
2905
|
const businessHours = normalizedPrepareParams.businessHours || DEFAULT_AVAILABILITY_BUSINESS_HOURS;
|
|
2851
2906
|
const mode = normalizedPrepareParams.mode || 'appointment';
|
|
2852
|
-
const scheduleList = this.getAvailabilityScheduleCatalog();
|
|
2853
2907
|
const operatingHours = this.buildAvailabilityOperatingHoursSnapshot({
|
|
2854
2908
|
prepare: normalizedPrepareParams,
|
|
2855
|
-
dateRange
|
|
2909
|
+
dateRange: useDurationTimeGrid ? {
|
|
2910
|
+
startDate: coverageDateRange.startDate,
|
|
2911
|
+
endDate: dateRange.endDate
|
|
2912
|
+
} : dateRange,
|
|
2856
2913
|
scheduleList,
|
|
2857
2914
|
businessHours
|
|
2858
2915
|
});
|
|
2859
|
-
const products = await this.loadAvailabilityProducts({
|
|
2860
|
-
prepare: normalizedPrepareParams,
|
|
2861
|
-
dateRange,
|
|
2862
|
-
scheduleList
|
|
2863
|
-
});
|
|
2864
2916
|
const loadedResources = await this.loadAvailabilityResources({
|
|
2865
2917
|
timezone: timezoneLabel,
|
|
2866
|
-
dateRange,
|
|
2918
|
+
dateRange: coverageDateRange,
|
|
2867
2919
|
resourceIds: normalizedPrepareParams.resourceIds,
|
|
2868
2920
|
products,
|
|
2869
2921
|
rawResources: normalizedPrepareParams.rawResources,
|
|
@@ -2899,7 +2951,7 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
2899
2951
|
resourceOccupancies: [...remoteOccupancies, ...cartOccupancies],
|
|
2900
2952
|
mode,
|
|
2901
2953
|
timezone: timezoneLabel,
|
|
2902
|
-
coverage:
|
|
2954
|
+
coverage: coverageDateRange,
|
|
2903
2955
|
slotStepMinutes,
|
|
2904
2956
|
businessHours,
|
|
2905
2957
|
operatingHours,
|
|
@@ -3154,14 +3206,47 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
3154
3206
|
} = await this.ensureAvailabilityContextProjection(contextId, 'get_product_time_ranges');
|
|
3155
3207
|
const contextVersion = context.activeVersion;
|
|
3156
3208
|
const isPosAvailabilityRequest = String(this.otherParams?.platform ?? '').trim().toLowerCase() === 'pos';
|
|
3157
|
-
const
|
|
3209
|
+
const isEditingContext = Boolean(context.prepareParams.editingProductLineUids?.length);
|
|
3210
|
+
const editingOffset = (0, _localClock.extractFixedOffsetMinutes)(projectionEntry.projection.meta.now) ?? this.getAvailabilityRuntimeOffsetMinutes();
|
|
3211
|
+
// This new editing input represents an absolute instant. Normalize it once
|
|
3212
|
+
// to the projection's fixed business clock before the wall-clock planner.
|
|
3213
|
+
const editingStartAtByProductId = isEditingContext && request.editingStartAtByProductId ? Object.fromEntries(Object.entries(request.editingStartAtByProductId).map(([productId, startAt]) => {
|
|
3214
|
+
try {
|
|
3215
|
+
const scalar = (0, _localClock.localDateTimeToScalar)((0, _localClock.parseLocalDateTime)(startAt));
|
|
3216
|
+
const inputOffset = (0, _localClock.extractFixedOffsetMinutes)(startAt) ?? editingOffset;
|
|
3217
|
+
return [productId, (0, _localClock.formatLocalDateTime)(scalar + (editingOffset - inputOffset) * 60000, editingOffset)];
|
|
3218
|
+
} catch {
|
|
3219
|
+
throw new _ResourcePlanner.AvailabilityError('INVALID_TARGET_FILTER', '编辑开始时间必须是完整有效日期时间');
|
|
3220
|
+
}
|
|
3221
|
+
})) : undefined;
|
|
3222
|
+
let dateRange = request.dateRange || this.getAvailabilityBusinessDateRange(projectionEntry.projection);
|
|
3223
|
+
// Older edit drafts carry the real calendar date, not a business-date label.
|
|
3224
|
+
// The explicit absolute start disambiguates the previous day's overnight tail.
|
|
3225
|
+
const editingStarts = Object.values(editingStartAtByProductId || {});
|
|
3226
|
+
if (this.usesDurationTimeGrid(projectionEntry.projection) && editingStarts.length === 1 && dateRange.startDate === dateRange.endDate && dateRange.startDate === (0, _localClock.formatLocalDate)((0, _localClock.parseLocalDateTime)(editingStarts[0]))) {
|
|
3227
|
+
const businessDate = (0, _ResourcePlanner.getDurationBusinessDate)(projectionEntry.projection, editingStarts[0]);
|
|
3228
|
+
dateRange = {
|
|
3229
|
+
startDate: businessDate,
|
|
3230
|
+
endDate: businessDate
|
|
3231
|
+
};
|
|
3232
|
+
}
|
|
3233
|
+
const rememberedStartAtByProductId = isPosAvailabilityRequest && !isEditingContext ? request.rememberedStartAtByProductId : undefined;
|
|
3234
|
+
const allowOutsideOperatingHoursForRequestedStartTimes = isPosAvailabilityRequest && !rememberedStartAtByProductId && (editingStarts.length > 0 || Object.values(request.durationStartTimesByProductId || {}).some(startTimes => Array.isArray(startTimes) && startTimes.length > 0));
|
|
3158
3235
|
return (0, _ResourcePlanner.getProductTimeRanges)(projectionEntry.projection, {
|
|
3159
3236
|
...request,
|
|
3237
|
+
dateRange,
|
|
3238
|
+
editingStartAtByProductId,
|
|
3239
|
+
rememberedStartAtByProductId,
|
|
3240
|
+
...(rememberedStartAtByProductId ? {
|
|
3241
|
+
allowOutsideOperatingHoursForRequestedStartTimes: false
|
|
3242
|
+
} : {}),
|
|
3160
3243
|
...(allowOutsideOperatingHoursForRequestedStartTimes ? {
|
|
3161
3244
|
allowOutsideOperatingHoursForRequestedStartTimes: true
|
|
3162
3245
|
} : {})
|
|
3163
3246
|
}, {
|
|
3164
3247
|
evaluatedAt: this.getAvailabilityRuntimeDateTime(),
|
|
3248
|
+
useDurationTimeGrid: this.usesDurationTimeGrid(projectionEntry.projection),
|
|
3249
|
+
enforceSaleCutoff: Boolean(rememberedStartAtByProductId),
|
|
3165
3250
|
useExactDurationCandidateMinute: isPosAvailabilityRequest,
|
|
3166
3251
|
allowSessionCustomRanges: isPosAvailabilityRequest,
|
|
3167
3252
|
allowExcludedScheduleDateOverride: isPosAvailabilityRequest
|
|
@@ -3180,7 +3265,13 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
3180
3265
|
} = await this.ensureAvailabilityContextProjection(contextId, 'query_booking_availability');
|
|
3181
3266
|
this.assertContextualQueryFreshness(context, request);
|
|
3182
3267
|
const isPosAvailabilityRequest = String(this.otherParams?.platform ?? '').trim().toLowerCase() === 'pos';
|
|
3183
|
-
const
|
|
3268
|
+
const queryRequest = request.target === 'date' || request.target === 'product' ? {
|
|
3269
|
+
...request,
|
|
3270
|
+
dateRange: request.dateRange || this.getAvailabilityBusinessDateRange(projectionEntry.projection)
|
|
3271
|
+
} : request;
|
|
3272
|
+
const result = (0, _ResourcePlanner.queryAvailabilityProjection)(projectionEntry.projection, queryRequest, {
|
|
3273
|
+
useDurationTimeGrid: this.usesDurationTimeGrid(projectionEntry.projection),
|
|
3274
|
+
enforceSaleCutoff: isPosAvailabilityRequest && !context.prepareParams.editingProductLineUids?.length && request.selectionIntent === 'remembered',
|
|
3184
3275
|
useExactDurationCandidateMinute: isPosAvailabilityRequest,
|
|
3185
3276
|
allowExcludedScheduleDateOverride: isPosAvailabilityRequest
|
|
3186
3277
|
});
|
|
@@ -3269,6 +3360,25 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
3269
3360
|
candidate: params.candidate
|
|
3270
3361
|
});
|
|
3271
3362
|
}
|
|
3363
|
+
const isRememberedSelection = params.selectionIntent === 'remembered';
|
|
3364
|
+
if (isRememberedSelection && (String(this.otherParams?.platform ?? '').trim().toLowerCase() !== 'pos' || params.mode !== 'add')) {
|
|
3365
|
+
throw new _ResourcePlanner.AvailabilityError('INVALID_TARGET_FILTER', '记忆时间自动提交仅支持 POS 新增 duration 商品');
|
|
3366
|
+
}
|
|
3367
|
+
const hasCommitGuard = isRememberedSelection || Boolean(params.availabilityCommitGuard);
|
|
3368
|
+
params.availabilityCommitGuard?.();
|
|
3369
|
+
// Stabilize the locally generated sale identity before taking the guard snapshot.
|
|
3370
|
+
if (hasCommitGuard) this.store.order?.ensureTempOrder?.();
|
|
3371
|
+
const rememberedOrder = hasCommitGuard ? this.getTempOrder() : null;
|
|
3372
|
+
const getRememberedCommitIdentity = () => JSON.stringify(stableValue({
|
|
3373
|
+
scope: this.getAvailabilityScopeDescriptor(),
|
|
3374
|
+
shopId: this.otherParams?.shop_id ?? this.otherParams?.shopId,
|
|
3375
|
+
contextShopId: this.core?.context?.shop_id,
|
|
3376
|
+
orderId: this.getTempOrder()?.order_id,
|
|
3377
|
+
externalSaleNumber: this.getTempOrder()?.external_sale_number
|
|
3378
|
+
}));
|
|
3379
|
+
const rememberedCommitIdentity = hasCommitGuard ? getRememberedCommitIdentity() : '';
|
|
3380
|
+
const availabilityCommitTargetGuard = () => this.getTempOrder() === rememberedOrder && getRememberedCommitIdentity() === rememberedCommitIdentity;
|
|
3381
|
+
let guardedCommitPublished = false;
|
|
3272
3382
|
await this.tryAcquireAvailabilityCommitLock(normalizedContextId);
|
|
3273
3383
|
try {
|
|
3274
3384
|
const bookingCount = Number(params.bookingCount ?? 1);
|
|
@@ -3374,6 +3484,10 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
3374
3484
|
});
|
|
3375
3485
|
}
|
|
3376
3486
|
const baseProjection = projectionEntry.projection;
|
|
3487
|
+
const selectedProduct = baseProjection.products.find(item => sameAvailabilityId(item.id, params.candidate.productId));
|
|
3488
|
+
if (isRememberedSelection && (String(this.otherParams?.platform ?? '').trim().toLowerCase() !== 'pos' || params.mode !== 'add' || selectedProduct?.kind !== 'duration')) {
|
|
3489
|
+
throw new _ResourcePlanner.AvailabilityError('INVALID_TARGET_FILTER', '记忆时间自动提交仅支持 POS 新增 duration 商品');
|
|
3490
|
+
}
|
|
3377
3491
|
let commitWriteGuard = {
|
|
3378
3492
|
contextId: normalizedContextId,
|
|
3379
3493
|
expectedVersion: context.activeVersion,
|
|
@@ -3419,17 +3533,32 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
3419
3533
|
this.assertAvailabilityCommitWriteCAS(commitWriteGuard);
|
|
3420
3534
|
const candidateDurationMinutes = getCandidateDurationMinutes(params.candidate);
|
|
3421
3535
|
const candidateStartTime = getCandidateStartTime(params.candidate);
|
|
3422
|
-
const allowPosAvailabilityOverride = String(this.otherParams?.platform ?? '').trim().toLowerCase() === 'pos';
|
|
3536
|
+
const allowPosAvailabilityOverride = String(this.otherParams?.platform ?? '').trim().toLowerCase() === 'pos' && !isRememberedSelection;
|
|
3537
|
+
const candidateBusinessDate = params.candidate.businessDate || (selectedProduct?.kind === 'duration' && this.usesDurationTimeGrid(refreshedProjection) ? (0, _ResourcePlanner.getDurationBusinessDate)(refreshedProjection, params.candidate.startAt) : params.candidate.startAt.slice(0, 10));
|
|
3423
3538
|
const refreshedRanges = (0, _ResourcePlanner.getProductTimeRanges)(refreshedProjection, {
|
|
3424
3539
|
productIds: [params.candidate.productId],
|
|
3425
3540
|
dateRange: {
|
|
3426
|
-
startDate:
|
|
3427
|
-
endDate:
|
|
3541
|
+
startDate: candidateBusinessDate,
|
|
3542
|
+
endDate: candidateBusinessDate
|
|
3428
3543
|
},
|
|
3429
|
-
...(
|
|
3544
|
+
...(isRememberedSelection ? {
|
|
3545
|
+
rememberedStartAtByProductId: {
|
|
3546
|
+
[String(params.candidate.productId)]: params.candidate.bookingStartAt
|
|
3547
|
+
},
|
|
3548
|
+
...(!selectedProduct?.isFlexibleDuration && candidateDurationMinutes ? {
|
|
3549
|
+
durationMinutesByProductId: {
|
|
3550
|
+
[String(params.candidate.productId)]: candidateDurationMinutes
|
|
3551
|
+
}
|
|
3552
|
+
} : {})
|
|
3553
|
+
} : candidateDurationMinutes ? {
|
|
3430
3554
|
durationMinutesByProductId: {
|
|
3431
3555
|
[String(params.candidate.productId)]: candidateDurationMinutes
|
|
3432
3556
|
},
|
|
3557
|
+
...(params.mode === 'edit' && selectedProduct?.kind === 'duration' ? {
|
|
3558
|
+
editingStartAtByProductId: {
|
|
3559
|
+
[String(params.candidate.productId)]: params.candidate.bookingStartAt
|
|
3560
|
+
}
|
|
3561
|
+
} : {}),
|
|
3433
3562
|
...(candidateStartTime ? {
|
|
3434
3563
|
durationStartTimesByProductId: {
|
|
3435
3564
|
[String(params.candidate.productId)]: [candidateStartTime]
|
|
@@ -3440,6 +3569,9 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
3440
3569
|
} : {})
|
|
3441
3570
|
} : {})
|
|
3442
3571
|
}, {
|
|
3572
|
+
evaluatedAt: this.getAvailabilityRuntimeDateTime(),
|
|
3573
|
+
useDurationTimeGrid: this.usesDurationTimeGrid(refreshedProjection),
|
|
3574
|
+
enforceSaleCutoff: isRememberedSelection,
|
|
3443
3575
|
useExactDurationCandidateMinute: allowPosAvailabilityOverride,
|
|
3444
3576
|
allowSessionCustomRanges: allowPosAvailabilityOverride,
|
|
3445
3577
|
allowExcludedScheduleDateOverride: allowPosAvailabilityOverride
|
|
@@ -3464,6 +3596,8 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
3464
3596
|
partySize,
|
|
3465
3597
|
bookingCount
|
|
3466
3598
|
}, {
|
|
3599
|
+
useDurationTimeGrid: this.usesDurationTimeGrid(refreshedProjection),
|
|
3600
|
+
enforceSaleCutoff: isRememberedSelection,
|
|
3467
3601
|
useExactDurationCandidateMinute: allowPosAvailabilityOverride,
|
|
3468
3602
|
allowExcludedScheduleDateOverride: allowPosAvailabilityOverride
|
|
3469
3603
|
});
|
|
@@ -3524,6 +3658,30 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
3524
3658
|
expectedOrderRevision: revalidationEntry.orderRevision
|
|
3525
3659
|
};
|
|
3526
3660
|
const refreshedCandidate = this.decorateAvailabilityCandidate(normalizedContextId, revalidatedContext.activeVersion, refreshedCandidateBase);
|
|
3661
|
+
// Order may await line/runtime setup. Its guarded transaction invokes this
|
|
3662
|
+
// again immediately before publishing, while the live cart is unchanged.
|
|
3663
|
+
const availabilityCommitGuard = hasCommitGuard ? () => {
|
|
3664
|
+
params.availabilityCommitGuard?.();
|
|
3665
|
+
this.assertAvailabilityCommitWriteCAS(commitWriteGuard);
|
|
3666
|
+
if (!availabilityCommitTargetGuard()) {
|
|
3667
|
+
throw this.createAvailabilityFacadeError('AVAILABILITY_SELECTION_STALE', '记忆时间加购期间订单或业务已变化');
|
|
3668
|
+
}
|
|
3669
|
+
if (!isRememberedSelection) return;
|
|
3670
|
+
const currentValidation = (0, _ResourcePlanner.validateAvailabilitySelection)(refreshedProjection, {
|
|
3671
|
+
candidate: refreshedCandidateBase,
|
|
3672
|
+
requirementSelections: params.requirementSelections,
|
|
3673
|
+
partySize,
|
|
3674
|
+
bookingCount
|
|
3675
|
+
}, {
|
|
3676
|
+
useDurationTimeGrid: true,
|
|
3677
|
+
enforceSaleCutoff: true
|
|
3678
|
+
});
|
|
3679
|
+
if (!currentValidation.ok) {
|
|
3680
|
+
throw new _ResourcePlanner.AvailabilityError('REVALIDATION_FAILED', '记忆时间在加购前已不可用,请重新选择', {
|
|
3681
|
+
validation: currentValidation
|
|
3682
|
+
});
|
|
3683
|
+
}
|
|
3684
|
+
} : undefined;
|
|
3527
3685
|
const existingBooking = editingLine ? this.findLinkedBooking(editingLine) : undefined;
|
|
3528
3686
|
const existingBookingUid = existingBooking ? getBookingUid(existingBooking) : '';
|
|
3529
3687
|
if (params.mode === 'edit' && !existingBookingUid) {
|
|
@@ -3541,6 +3699,23 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
3541
3699
|
holder: params.orderProduct?._extend?.holder ?? existingBooking?.holder ?? existingBooking?.metadata?.holder,
|
|
3542
3700
|
holderIds: params.orderProduct?._extend?.holder_id ?? params.orderProduct?._extend?.holder?.form_record ?? existingBooking?.metadata?.holder_id ?? existingBooking?.holder?.form_record
|
|
3543
3701
|
});
|
|
3702
|
+
if (isRememberedSelection && params.orderProduct?._extend?.resource) {
|
|
3703
|
+
// AutoClose 与编辑共用旧资源协议。仅复用其结构,实际资源及每份容量
|
|
3704
|
+
// 必须与本次严格校验结果一致;时间、Holder 和校验元数据仍使用上面的结果。
|
|
3705
|
+
const legacyBooking = (0, _cacheItemToBookingInput.cacheItemToBookingInput)(params.orderProduct);
|
|
3706
|
+
const legacyResources = legacyBooking.resources || [];
|
|
3707
|
+
const selectedResources = legacyResources.filter(resource => Number(resource.relation_id) !== 0);
|
|
3708
|
+
const resourceKeys = resources => resources.map(resource => JSON.stringify([String(resource.form_id), String(resource.relation_id), resource.relation_type, Number(resource.capacity)])).sort();
|
|
3709
|
+
if (JSON.stringify(resourceKeys(selectedResources)) !== JSON.stringify(resourceKeys(booking.resources)) || legacyResources.some(resource => Number(resource.relation_id) === 0 && !selectedResources.some(selected => sameAvailabilityId(selected.form_id, resource.form_id)))) {
|
|
3710
|
+
throw new _ResourcePlanner.AvailabilityError('REVALIDATION_FAILED', '记忆时间的资源与本次校验结果不一致');
|
|
3711
|
+
}
|
|
3712
|
+
Object.assign(booking, {
|
|
3713
|
+
relation_id: legacyBooking.relation_id,
|
|
3714
|
+
form_id: legacyBooking.form_id,
|
|
3715
|
+
relation_type: legacyBooking.relation_type,
|
|
3716
|
+
resources: (0, _lodashEs.cloneDeep)(legacyResources)
|
|
3717
|
+
});
|
|
3718
|
+
}
|
|
3544
3719
|
const transformed = this.buildUnifiedOrderProduct({
|
|
3545
3720
|
...params.orderProduct,
|
|
3546
3721
|
num: bookingCount,
|
|
@@ -3593,12 +3768,20 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
3593
3768
|
} else {
|
|
3594
3769
|
const beforeProductLineUids = new Set((this.getTempOrder()?.products || []).map(line => getProductLineUid(line)).filter(Boolean));
|
|
3595
3770
|
const beforeBookingUids = new Set((this.getTempOrder()?.bookings || []).map(item => getBookingUid(item)).filter(Boolean));
|
|
3596
|
-
|
|
3597
|
-
|
|
3771
|
+
availabilityCommitGuard?.();
|
|
3772
|
+
const products = await this.addProductToOrder(transformed, booking, params.salesNotes?.length || availabilityCommitGuard ? {
|
|
3773
|
+
...(params.salesNotes?.length ? {
|
|
3774
|
+
salesNotes: params.salesNotes
|
|
3775
|
+
} : {}),
|
|
3776
|
+
...(availabilityCommitGuard ? {
|
|
3777
|
+
availabilityCommitGuard,
|
|
3778
|
+
availabilityCommitTargetGuard
|
|
3779
|
+
} : {})
|
|
3598
3780
|
} : undefined);
|
|
3781
|
+
guardedCommitPublished = hasCommitGuard;
|
|
3599
3782
|
const addedLines = products.filter(line => sameAvailabilityId(line.product_id ?? line.id, refreshedCandidate.productId) && line.metadata?.availability_candidate_key === refreshedCandidate.key && !beforeProductLineUids.has(getProductLineUid(line)));
|
|
3600
3783
|
productLineUids = addedLines.map(line => getProductLineUid(line)).filter(Boolean);
|
|
3601
|
-
const addedBookings = (this.getTempOrder()?.bookings || []).filter(item => item.metadata?.availability_candidate_key === refreshedCandidate.key && !beforeBookingUids.has(getBookingUid(item)));
|
|
3784
|
+
const addedBookings = ((hasCommitGuard ? rememberedOrder : this.getTempOrder())?.bookings || []).filter(item => item.metadata?.availability_candidate_key === refreshedCandidate.key && !beforeBookingUids.has(getBookingUid(item)));
|
|
3602
3785
|
bookingUids = addedBookings.map(item => getBookingUid(item)).filter(Boolean);
|
|
3603
3786
|
if (bookingUids.length === 0) {
|
|
3604
3787
|
bookingUids = addedLines.map(line => String(line.booking_uid ?? line.metadata?.booking_uid ?? '')).filter(Boolean);
|
|
@@ -3606,12 +3789,13 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
3606
3789
|
productLineUid = productLineUids[productLineUids.length - 1] || '';
|
|
3607
3790
|
bookingUid = bookingUids[bookingUids.length - 1] || '';
|
|
3608
3791
|
}
|
|
3609
|
-
|
|
3610
|
-
|
|
3792
|
+
const targetStillCurrent = !hasCommitGuard || availabilityCommitTargetGuard();
|
|
3793
|
+
if (targetStillCurrent) this.invalidateAvailabilityContextsAfterCommit();else this.removeAvailabilityContext(normalizedContextId);
|
|
3794
|
+
const nextContext = hasCommitGuard ? this.availabilityContextRegistry.get(normalizedContextId) : this.getAvailabilityContextEntry(normalizedContextId);
|
|
3611
3795
|
const result = {
|
|
3612
3796
|
contextId: normalizedContextId,
|
|
3613
|
-
contextVersion: nextContext.
|
|
3614
|
-
tempOrder: this.getTempOrder(),
|
|
3797
|
+
contextVersion: nextContext?.activeVersion ?? refreshedCandidate.availabilityContextVersion,
|
|
3798
|
+
tempOrder: hasCommitGuard ? rememberedOrder : this.getTempOrder(),
|
|
3615
3799
|
validation,
|
|
3616
3800
|
candidate: refreshedCandidate,
|
|
3617
3801
|
productLineUid,
|
|
@@ -3619,10 +3803,19 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
3619
3803
|
productLineUids,
|
|
3620
3804
|
bookingUids
|
|
3621
3805
|
};
|
|
3622
|
-
|
|
3806
|
+
if (targetStillCurrent) {
|
|
3807
|
+
// A notification failure after publication must not invite a duplicate add.
|
|
3808
|
+
const emit = () => this.core.effects.emit(`${this.name}:${_types.UnifiedBookingSalesHooks.onAvailabilitySelectionCommitted}`, result);
|
|
3809
|
+
if (guardedCommitPublished) {
|
|
3810
|
+
try {
|
|
3811
|
+
await emit();
|
|
3812
|
+
} catch {/* The cart mutation has already succeeded. */}
|
|
3813
|
+
} else await emit();
|
|
3814
|
+
}
|
|
3623
3815
|
return result;
|
|
3624
3816
|
} finally {
|
|
3625
|
-
|
|
3817
|
+
const release = this.releaseAvailabilityCommitLock(normalizedContextId);
|
|
3818
|
+
if (guardedCommitPublished) await release.catch(() => undefined);else await release;
|
|
3626
3819
|
}
|
|
3627
3820
|
}
|
|
3628
3821
|
}
|
|
@@ -72,6 +72,11 @@ export interface UnifiedBookingSalesPrepareAvailabilityProjectionParams {
|
|
|
72
72
|
}
|
|
73
73
|
export interface AvailabilityContextRef {
|
|
74
74
|
contextId: string;
|
|
75
|
+
/** POS/Kiosk duration protocol capability and business-day default. */
|
|
76
|
+
durationTimeSelection?: {
|
|
77
|
+
version: 1;
|
|
78
|
+
currentBusinessDate: string;
|
|
79
|
+
};
|
|
75
80
|
}
|
|
76
81
|
export interface ContextualProductTimeRange extends ProductTimeRange {
|
|
77
82
|
availabilityContextId: string;
|
|
@@ -80,7 +85,7 @@ export interface ContextualProductTimeRange extends ProductTimeRange {
|
|
|
80
85
|
export interface ContextualProductTimeRangeGroup extends Omit<ProductTimeRangeGroup, 'ranges'> {
|
|
81
86
|
ranges: ContextualProductTimeRange[];
|
|
82
87
|
}
|
|
83
|
-
export type ContextualAvailabilityQuery = Omit<Extract<AvailabilityQuery, {
|
|
88
|
+
export type ContextualAvailabilityQuery = (Omit<Extract<AvailabilityQuery, {
|
|
84
89
|
target: 'product';
|
|
85
90
|
}>, never> | Omit<Extract<AvailabilityQuery, {
|
|
86
91
|
target: 'date';
|
|
@@ -92,7 +97,10 @@ export type ContextualAvailabilityQuery = Omit<Extract<AvailabilityQuery, {
|
|
|
92
97
|
target: 'resource';
|
|
93
98
|
}>, 'selectedRanges'> & {
|
|
94
99
|
selectedRanges: ContextualProductTimeRange[];
|
|
95
|
-
})
|
|
100
|
+
})) & {
|
|
101
|
+
/** Automatic remembered selection must not inherit POS manual exceptions. */
|
|
102
|
+
selectionIntent?: 'remembered' | 'manual';
|
|
103
|
+
};
|
|
96
104
|
export type ContextualTimeAvailabilityItem = Omit<TimeAvailabilityItem, keyof ProductTimeRange> & ContextualProductTimeRange;
|
|
97
105
|
export interface ContextualTimeAvailabilityGroup extends Omit<TimeAvailabilityGroup, 'times'> {
|
|
98
106
|
times: ContextualTimeAvailabilityItem[];
|
|
@@ -155,6 +163,10 @@ export interface UnifiedBookingSalesAddRetailProductParams {
|
|
|
155
163
|
}
|
|
156
164
|
export interface ContextualCommitAvailabilitySelectionParams {
|
|
157
165
|
mode: 'add' | 'edit';
|
|
166
|
+
/** Only POS duration add supports automatic remembered-time commits. */
|
|
167
|
+
selectionIntent?: 'remembered' | 'manual';
|
|
168
|
+
/** Local UI intent guard, checked again immediately before the Order mutation. */
|
|
169
|
+
availabilityCommitGuard?: () => void;
|
|
158
170
|
productLineUid?: string;
|
|
159
171
|
orderProduct: Record<string, any>;
|
|
160
172
|
candidate: ContextualProductTimeRange;
|