@pisell/pisellos 2.3.143 → 2.3.145

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 (33) hide show
  1. package/dist/modules/BookingContext/utils/productExtend.d.ts +7 -0
  2. package/dist/modules/BookingContext/utils/productExtend.js +62 -18
  3. package/dist/modules/Order/index.d.ts +2 -1
  4. package/dist/modules/Order/index.js +380 -198
  5. package/dist/modules/Order/types.d.ts +9 -0
  6. package/dist/modules/Order/utils.d.ts +5 -3
  7. package/dist/modules/Order/utils.js +32 -11
  8. package/dist/modules/ResourcePlanner/planner.d.ts +2 -0
  9. package/dist/modules/ResourcePlanner/planner.js +327 -22
  10. package/dist/modules/ResourcePlanner/types.d.ts +29 -0
  11. package/dist/solution/BookingByStep/index.d.ts +1 -1
  12. package/dist/solution/BookingTicket/utils/addProductDecision.d.ts +0 -1
  13. package/dist/solution/BookingTicket/utils/addProductDecision.js +1 -1
  14. package/dist/solution/UnifiedBookingSales/index.d.ts +3 -0
  15. package/dist/solution/UnifiedBookingSales/index.js +432 -163
  16. package/dist/solution/UnifiedBookingSales/types.d.ts +14 -2
  17. package/lib/modules/BookingContext/utils/productExtend.d.ts +7 -0
  18. package/lib/modules/BookingContext/utils/productExtend.js +45 -2
  19. package/lib/modules/Order/index.d.ts +2 -1
  20. package/lib/modules/Order/index.js +92 -10
  21. package/lib/modules/Order/types.d.ts +9 -0
  22. package/lib/modules/Order/utils.d.ts +5 -3
  23. package/lib/modules/Order/utils.js +29 -10
  24. package/lib/modules/ResourcePlanner/planner.d.ts +2 -0
  25. package/lib/modules/ResourcePlanner/planner.js +247 -7
  26. package/lib/modules/ResourcePlanner/types.d.ts +29 -0
  27. package/lib/solution/BookingByStep/index.d.ts +1 -1
  28. package/lib/solution/BookingTicket/utils/addProductDecision.d.ts +0 -1
  29. package/lib/solution/BookingTicket/utils/addProductDecision.js +1 -1
  30. package/lib/solution/UnifiedBookingSales/index.d.ts +3 -0
  31. package/lib/solution/UnifiedBookingSales/index.js +220 -27
  32. package/lib/solution/UnifiedBookingSales/types.d.ts +14 -2
  33. 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: dateRange,
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 allowOutsideOperatingHoursForRequestedStartTimes = isPosAvailabilityRequest && Object.values(request.durationStartTimesByProductId || {}).some(startTimes => Array.isArray(startTimes) && startTimes.length > 0);
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 result = (0, _ResourcePlanner.queryAvailabilityProjection)(projectionEntry.projection, request, {
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: params.candidate.startAt.slice(0, 10),
3427
- endDate: params.candidate.startAt.slice(0, 10)
3541
+ startDate: candidateBusinessDate,
3542
+ endDate: candidateBusinessDate
3428
3543
  },
3429
- ...(candidateDurationMinutes ? {
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
- const products = await this.addProductToOrder(transformed, booking, params.salesNotes?.length ? {
3597
- salesNotes: params.salesNotes
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
- this.invalidateAvailabilityContextsAfterCommit();
3610
- const nextContext = this.getAvailabilityContextEntry(normalizedContextId);
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.activeVersion,
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
- await this.core.effects.emit(`${this.name}:${_types.UnifiedBookingSalesHooks.onAvailabilitySelectionCommitted}`, result);
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
- await this.releaseAvailabilityCommitLock(normalizedContextId);
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;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@pisell/pisellos",
4
- "version": "2.3.143",
4
+ "version": "2.3.145",
5
5
  "description": "一个可扩展的前端模块化SDK框架,支持插件系统",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",