@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.
- 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/Order/utils.d.ts +5 -3
- package/dist/modules/Order/utils.js +32 -11
- 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/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/Order/utils.d.ts +5 -3
- package/lib/modules/Order/utils.js +29 -10
- 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/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
|
@@ -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;
|
|
@@ -7,6 +7,13 @@ import { TimeSlicesResult, TimeSlice, DurationSlice } from './timeSlices';
|
|
|
7
7
|
* 让 OS 这层完全可在 Node 单测下运行(dayjs 在 Node 也是纯函数)。
|
|
8
8
|
*/
|
|
9
9
|
export interface BookingCalcContext {
|
|
10
|
+
/** Sales channel; remembered duration starts are accepted only by POS. */
|
|
11
|
+
channel?: string;
|
|
12
|
+
/** Start-only device preference for this decision; never reuse a cached end/resource. */
|
|
13
|
+
rememberedTime?: {
|
|
14
|
+
businessDate: string;
|
|
15
|
+
startAt: string;
|
|
16
|
+
};
|
|
10
17
|
/** 来自 `/core/board/management/config` 的整套配置;常用路径见 sliceTimeSlices / appointmentTimingPreference。 */
|
|
11
18
|
bookingConfig?: any;
|
|
12
19
|
/** 资源 origin map(id → resource origin)。 */
|
|
@@ -28,6 +28,44 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
|
|
|
28
28
|
* 让 OS 这层完全可在 Node 单测下运行(dayjs 在 Node 也是纯函数)。
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
+
/** Rebuild only the derived booking fields; SKU, quantity, notes and Holder stay intact. */
|
|
32
|
+
function seedRememberedBookingStart(cacheItem, ctx) {
|
|
33
|
+
const remembered = ctx.rememberedTime;
|
|
34
|
+
const invalid = () => Object.assign(new Error('Remembered time requires a valid POS duration start.'), {
|
|
35
|
+
code: 'INVALID_REMEMBERED_TIME'
|
|
36
|
+
});
|
|
37
|
+
const duration = cacheItem?.duration;
|
|
38
|
+
const durationType = typeof duration === 'object' && duration ? String(duration.type || '').toLowerCase() : '';
|
|
39
|
+
const durationValue = typeof duration === 'object' && duration ? duration.value : duration;
|
|
40
|
+
const isDuration = (0, _flexible.isProductFlexibleDuration)(duration) || ['', 'fixed', 'minute', 'minutes', 'hour', 'hours'].includes(durationType) && Number.isFinite(Number(durationValue)) && Number(durationValue) > 0;
|
|
41
|
+
const isCalendarDate = value => {
|
|
42
|
+
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
|
43
|
+
const date = new Date(`${value}T00:00:00.000Z`);
|
|
44
|
+
return Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value;
|
|
45
|
+
};
|
|
46
|
+
const parts = typeof remembered?.startAt === 'string' ? /^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,3})?)?(?:Z|[+-](\d{2}):(\d{2}))?$/.exec(remembered.startAt) : null;
|
|
47
|
+
if (String(ctx.channel || '').trim().toLowerCase() !== 'pos' || !isDuration || (0, _serviceTimes.isMultiDayProduct)(cacheItem) || (0, _serviceTimes.isSessionProduct)(cacheItem) || cacheItem?.cartDetailValue?.session || cacheItem?._extend?.other?.session || !isCalendarDate(remembered?.businessDate) || !parts || !isCalendarDate(parts[1]) || Number(parts[2]) > 23 || Number(parts[3]) > 59 || Number(parts[4] || 0) > 59 || Number(parts[5] || 0) > 23 || Number(parts[6] || 0) > 59) throw invalid();
|
|
48
|
+
// Parse the actual instant with the same runtime clock used by the legacy booking helpers.
|
|
49
|
+
// The business-day label must not replace the calendar date of an overnight start.
|
|
50
|
+
const start = (0, _dayjs.default)(remembered.startAt);
|
|
51
|
+
if (!start.isValid()) throw invalid();
|
|
52
|
+
const extend = {
|
|
53
|
+
...(cacheItem?._extend || {})
|
|
54
|
+
};
|
|
55
|
+
for (const key of ['start_date', 'startDate', 'start_time', 'end_date', 'endDate', 'end_time', 'duration', 'resource', 'resources', 'capacity', 'booking', 'availabilitySelection']) delete extend[key];
|
|
56
|
+
return {
|
|
57
|
+
...cacheItem,
|
|
58
|
+
new: 1,
|
|
59
|
+
autoClose: false,
|
|
60
|
+
_data: {},
|
|
61
|
+
_extend: {
|
|
62
|
+
...extend,
|
|
63
|
+
start_date: start.format('YYYY-MM-DD'),
|
|
64
|
+
start_time: start.format(start.millisecond() ? 'HH:mm:ss.SSS' : start.second() ? 'HH:mm:ss' : 'HH:mm')
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
31
69
|
/** 从 BookingContext.date 提取 HH:mm(仅当日期带有效时分时)。 */
|
|
32
70
|
function derivePresetStartTimeFromDate(date) {
|
|
33
71
|
if (date == null) return undefined;
|
|
@@ -195,10 +233,15 @@ function getServiceStartTimeAndEndTime(params) {
|
|
|
195
233
|
*/
|
|
196
234
|
function getProductExtend(params) {
|
|
197
235
|
const {
|
|
198
|
-
cacheItem,
|
|
199
|
-
ctx,
|
|
200
236
|
isDisabledTime
|
|
201
237
|
} = params;
|
|
238
|
+
const hasRememberedTime = params.ctx.rememberedTime !== undefined;
|
|
239
|
+
const cacheItem = hasRememberedTime ? seedRememberedBookingStart(params.cacheItem, params.ctx) : params.cacheItem;
|
|
240
|
+
const ctx = hasRememberedTime ? {
|
|
241
|
+
...params.ctx,
|
|
242
|
+
previousService: undefined,
|
|
243
|
+
presetStartTime: undefined
|
|
244
|
+
} : params.ctx;
|
|
202
245
|
const isNew = cacheItem.new || !cacheItem._extend?.endDate;
|
|
203
246
|
const _cacheItem = {
|
|
204
247
|
...cacheItem
|
|
@@ -332,6 +332,7 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
|
|
|
332
332
|
private applyOrderProductsDepositSummary;
|
|
333
333
|
recalculateSummary(options?: {
|
|
334
334
|
createIfMissing?: boolean;
|
|
335
|
+
isCurrentOrder?: () => boolean;
|
|
335
336
|
}): Promise<OrderSummary | null>;
|
|
336
337
|
getOrderSummary(): Promise<OrderSummary>;
|
|
337
338
|
updateTempOrderNote(note: string): string;
|
|
@@ -592,7 +593,7 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
|
|
|
592
593
|
generateIdempotencyToken(): string;
|
|
593
594
|
syncPaymentsToOrder<T = any>(params: SyncPaymentsToOrderParams): Promise<SyncPaymentsToOrderResult<T>>;
|
|
594
595
|
createOrder(params: CommitOrderParams['query']): {
|
|
595
|
-
type: "
|
|
596
|
+
type: "appointment_booking" | "virtual";
|
|
596
597
|
platform: string;
|
|
597
598
|
sales_channel: string;
|
|
598
599
|
order_sales_channel: string;
|
|
@@ -913,7 +913,8 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
913
913
|
* - 不主动调用 applyDiscount / recalculateSummary,调用方负责跟进;
|
|
914
914
|
* - 返回的 payload 只能在 applyDiscount(商品写路径还需 summary/persist)完成后发布。
|
|
915
915
|
*/
|
|
916
|
-
async evaluatePromotion() {
|
|
916
|
+
async evaluatePromotion(options) {
|
|
917
|
+
if (options?.isCurrentOrder && !options.isCurrentOrder()) return null;
|
|
917
918
|
if (this.isApplyingPromotion || this.isEmittingPromotionApplied) {
|
|
918
919
|
return null;
|
|
919
920
|
}
|
|
@@ -974,6 +975,10 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
974
975
|
continue;
|
|
975
976
|
}
|
|
976
977
|
}
|
|
978
|
+
const giftMutationBaseline = options?.isCurrentOrder ? (0, _lodashEs.cloneDeep)({
|
|
979
|
+
products: tempOrder.products,
|
|
980
|
+
bookings: tempOrder.bookings
|
|
981
|
+
}) : null;
|
|
977
982
|
try {
|
|
978
983
|
const giftResult = await this.giftSelectResolver({
|
|
979
984
|
strategyId: addAction.strategyId,
|
|
@@ -982,6 +987,11 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
982
987
|
giftOptions: addAction.giftOptions,
|
|
983
988
|
sourceProductIds: addAction.sourceProductIds
|
|
984
989
|
});
|
|
990
|
+
if (options?.isCurrentOrder && !options.isCurrentOrder()) return null;
|
|
991
|
+
if (options?.isCurrentOrder && !(0, _lodashEs.isEqual)({
|
|
992
|
+
products: tempOrder.products,
|
|
993
|
+
bookings: tempOrder.bookings
|
|
994
|
+
}, giftMutationBaseline)) return null;
|
|
985
995
|
const giftProducts = Array.isArray(giftResult) ? giftResult : giftResult?.products;
|
|
986
996
|
const giftBookings = Array.isArray(giftResult) ? [] : giftResult?.bookings || [];
|
|
987
997
|
if (Array.isArray(giftProducts) && giftProducts.length > 0) {
|
|
@@ -1002,12 +1012,18 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
1002
1012
|
}
|
|
1003
1013
|
}
|
|
1004
1014
|
} catch (error) {
|
|
1015
|
+
if (options?.isCurrentOrder && !options.isCurrentOrder()) return null;
|
|
1016
|
+
if (options?.isCurrentOrder && !(0, _lodashEs.isEqual)({
|
|
1017
|
+
products: tempOrder.products,
|
|
1018
|
+
bookings: tempOrder.bookings
|
|
1019
|
+
}, giftMutationBaseline)) return null;
|
|
1005
1020
|
// 用户取消 / 选择失败 → 视为放弃本次新增
|
|
1006
1021
|
console.warn('[OrderModule] giftSelectResolver rejected, skip add gift', addAction.strategyId, error);
|
|
1007
1022
|
}
|
|
1008
1023
|
}
|
|
1009
1024
|
}
|
|
1010
1025
|
}
|
|
1026
|
+
if (options?.isCurrentOrder && !options.isCurrentOrder()) return null;
|
|
1011
1027
|
tempOrder.products = result.products;
|
|
1012
1028
|
this.lastUnfulfilledPromotions = result.unfulfilledPromotions;
|
|
1013
1029
|
this.lastGiftActions = result.giftActions;
|
|
@@ -2469,6 +2485,7 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
2469
2485
|
return shouldUseManualDepositOverride ? nextSummary : this.applyLastOrderDepositSnapshot(tempOrder, nextSummary);
|
|
2470
2486
|
}
|
|
2471
2487
|
async recalculateSummary(options) {
|
|
2488
|
+
if (options?.isCurrentOrder && !options.isCurrentOrder()) return null;
|
|
2472
2489
|
const tempOrder = options?.createIfMissing ? this.ensureTempOrder() : this.store.tempOrder;
|
|
2473
2490
|
if (!tempOrder) return null;
|
|
2474
2491
|
this.sanitizeTempOrderProducts(tempOrder);
|
|
@@ -2478,7 +2495,7 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
2478
2495
|
const snapshotSummary = shouldReuseOriginalSnapshot ? this.restoreOriginalSnapshotIfProductsUnchanged(tempOrder, originalSnapshot) : null;
|
|
2479
2496
|
if (snapshotSummary) return snapshotSummary;
|
|
2480
2497
|
const shouldFullRecalculateFromSnapshot = Boolean(originalSnapshot);
|
|
2481
|
-
const summaryProducts = shouldFullRecalculateFromSnapshot ? (0, _lodashEs.cloneDeep)(tempOrder.products || []) : tempOrder.products;
|
|
2498
|
+
const summaryProducts = shouldFullRecalculateFromSnapshot || options?.isCurrentOrder ? (0, _lodashEs.cloneDeep)(tempOrder.products || []) : tempOrder.products;
|
|
2482
2499
|
if (shouldFullRecalculateFromSnapshot) {
|
|
2483
2500
|
this.clearPersistedAmountFieldsForFullRecalc(summaryProducts || []);
|
|
2484
2501
|
}
|
|
@@ -2516,6 +2533,10 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
2516
2533
|
});
|
|
2517
2534
|
return this.store.summary;
|
|
2518
2535
|
}
|
|
2536
|
+
const summaryMutationBaseline = options?.isCurrentOrder ? (0, _lodashEs.cloneDeep)({
|
|
2537
|
+
tempOrder,
|
|
2538
|
+
summary: this.store.summary
|
|
2539
|
+
}) : null;
|
|
2519
2540
|
const salesSummaryResult = await salesSummary.getSummary({
|
|
2520
2541
|
products: summaryProducts,
|
|
2521
2542
|
isPriceIncludeTax: tempOrder.is_price_include_tax,
|
|
@@ -2525,7 +2546,12 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
2525
2546
|
channel: this.otherParams.channel
|
|
2526
2547
|
} : {})
|
|
2527
2548
|
});
|
|
2528
|
-
if (
|
|
2549
|
+
if (options?.isCurrentOrder && !options.isCurrentOrder()) return null;
|
|
2550
|
+
if (options?.isCurrentOrder && !(0, _lodashEs.isEqual)({
|
|
2551
|
+
tempOrder,
|
|
2552
|
+
summary: this.store.summary
|
|
2553
|
+
}, summaryMutationBaseline)) return null;
|
|
2554
|
+
if (shouldFullRecalculateFromSnapshot || options?.isCurrentOrder) {
|
|
2529
2555
|
tempOrder.products = summaryProducts;
|
|
2530
2556
|
}
|
|
2531
2557
|
this.syncSummaryProductMetadata(tempOrder.products || []);
|
|
@@ -3454,20 +3480,30 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
3454
3480
|
* await this.finalizeProductOrderMutation(tempOrder);
|
|
3455
3481
|
*/
|
|
3456
3482
|
async finalizeProductOrderMutation(tempOrder, options) {
|
|
3483
|
+
const isCurrentOrder = options?.isCurrentOrder;
|
|
3484
|
+
if (isCurrentOrder && !isCurrentOrder()) return;
|
|
3457
3485
|
this.syncTempOrderHolderFromBookings(tempOrder);
|
|
3458
3486
|
this.prepareVirtualSettlementTempOrder(tempOrder);
|
|
3459
3487
|
const isVirtualSettlement = this.isActiveVirtualSettlementOrder(tempOrder);
|
|
3460
3488
|
if (!options?.skipEditDiscountConfigRefresh && !isVirtualSettlement) {
|
|
3461
3489
|
await this.ensureEditDiscountConfigForProductChange(tempOrder);
|
|
3462
3490
|
}
|
|
3463
|
-
|
|
3491
|
+
if (isCurrentOrder && !isCurrentOrder()) return;
|
|
3492
|
+
const promotionPayload = !options?.skipDiscountRecalculation && !isVirtualSettlement ? await this.evaluatePromotion(isCurrentOrder ? {
|
|
3493
|
+
isCurrentOrder
|
|
3494
|
+
} : undefined) : null;
|
|
3495
|
+
if (isCurrentOrder && !isCurrentOrder()) return;
|
|
3464
3496
|
if (!options?.skipDiscountRecalculation && !isVirtualSettlement) {
|
|
3465
3497
|
this.applyDiscount();
|
|
3466
3498
|
}
|
|
3467
3499
|
this.sanitizeTempOrderProducts(tempOrder);
|
|
3468
|
-
await this.recalculateSummary({
|
|
3469
|
-
createIfMissing: true
|
|
3500
|
+
const summary = await this.recalculateSummary({
|
|
3501
|
+
createIfMissing: true,
|
|
3502
|
+
...(isCurrentOrder ? {
|
|
3503
|
+
isCurrentOrder
|
|
3504
|
+
} : {})
|
|
3470
3505
|
});
|
|
3506
|
+
if (isCurrentOrder && (!isCurrentOrder() || !summary)) return;
|
|
3471
3507
|
this.persistTempOrder();
|
|
3472
3508
|
this.emitPromotionApplied(promotionPayload);
|
|
3473
3509
|
}
|
|
@@ -3704,8 +3740,16 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
3704
3740
|
* // → tempOrder.products 新增 3 行,每行 num=1,bookings 3 条
|
|
3705
3741
|
*/
|
|
3706
3742
|
async addProductToOrder(product, booking, options) {
|
|
3707
|
-
const
|
|
3708
|
-
|
|
3743
|
+
const availabilityCommitGuard = options?.availabilityCommitGuard;
|
|
3744
|
+
availabilityCommitGuard?.();
|
|
3745
|
+
const liveTempOrder = this.ensureTempOrder();
|
|
3746
|
+
const beforeCommit = this.createCartIntentCommitGuard('add_product', liveTempOrder);
|
|
3747
|
+
// 自动预约在草稿上准备完整批次,避免拆票期间暴露半成品,或在失败时
|
|
3748
|
+
// 用整单快照回滚而抹掉其他入口的并发修改。人工路径保持原有写入行为。
|
|
3749
|
+
const tempOrder = availabilityCommitGuard ? (0, _lodashEs.cloneDeep)(liveTempOrder) : liveTempOrder;
|
|
3750
|
+
const guardedMutationFields = ['products', 'bookings', 'notes', '_extend', 'metadata', 'currency_code', 'currency_symbol', 'currency_format'];
|
|
3751
|
+
const readGuardedMutationFields = order => Object.fromEntries(guardedMutationFields.map(key => [key, order[key]]));
|
|
3752
|
+
const guardedMutationBaseline = availabilityCommitGuard ? (0, _lodashEs.cloneDeep)(readGuardedMutationFields(liveTempOrder)) : null;
|
|
3709
3753
|
const salesNoteDrafts = (options?.salesNotes || []).map(draft => ({
|
|
3710
3754
|
note_type: String(draft.note_type || '').trim(),
|
|
3711
3755
|
text: {
|
|
@@ -3779,6 +3823,7 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
3779
3823
|
disableMerge: options?.disableMerge,
|
|
3780
3824
|
beforeCommit
|
|
3781
3825
|
});
|
|
3826
|
+
availabilityCommitGuard?.();
|
|
3782
3827
|
attachSalesNoteDrafts(committedProduct);
|
|
3783
3828
|
if (insertAtIndex !== undefined) insertAtIndex += 1;
|
|
3784
3829
|
}
|
|
@@ -3788,6 +3833,7 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
3788
3833
|
disableMerge: options?.disableMerge,
|
|
3789
3834
|
beforeCommit
|
|
3790
3835
|
});
|
|
3836
|
+
availabilityCommitGuard?.();
|
|
3791
3837
|
attachSalesNoteDrafts(committedProduct);
|
|
3792
3838
|
}
|
|
3793
3839
|
} else {
|
|
@@ -3796,18 +3842,54 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
3796
3842
|
disableMerge: options?.disableMerge,
|
|
3797
3843
|
beforeCommit
|
|
3798
3844
|
});
|
|
3845
|
+
availabilityCommitGuard?.();
|
|
3799
3846
|
attachSalesNoteDrafts(committedProduct);
|
|
3800
3847
|
}
|
|
3848
|
+
if (availabilityCommitGuard) {
|
|
3849
|
+
availabilityCommitGuard();
|
|
3850
|
+
beforeCommit();
|
|
3851
|
+
// 调用方的结构 revision 可能不包括备注、价格或结算元数据。
|
|
3852
|
+
// 本次会覆盖的字段也要保持原值,不能让草稿覆盖并发编辑。
|
|
3853
|
+
if (!(0, _lodashEs.isEqual)(readGuardedMutationFields(liveTempOrder), guardedMutationBaseline)) {
|
|
3854
|
+
throw Object.assign(new Error('预约加购期间订单已变化,请重新选择'), {
|
|
3855
|
+
code: 'AVAILABILITY_SELECTION_STALE'
|
|
3856
|
+
});
|
|
3857
|
+
}
|
|
3858
|
+
// 从最后一次门禁到真正新增之间不再 await。只发布本次新增会改写的
|
|
3859
|
+
// 字段;其余订单字段继续使用 live 对象,外部持有的订单引用也不变。
|
|
3860
|
+
Object.assign(liveTempOrder, readGuardedMutationFields(tempOrder));
|
|
3861
|
+
}
|
|
3801
3862
|
} catch (error) {
|
|
3802
3863
|
rollbackProductAndNotesMutation();
|
|
3803
3864
|
throw error;
|
|
3804
3865
|
}
|
|
3805
|
-
|
|
3866
|
+
// 发布后预约已加购;后续促销/summary 沿用既有收尾,不用自身新增后的
|
|
3867
|
+
// revision 再执行提交 CAS,也不会把已加购预约因收尾耗时而撤回。
|
|
3868
|
+
if (availabilityCommitGuard) {
|
|
3869
|
+
const committedIdentity = [liveTempOrder.order_id, liveTempOrder.external_sale_number];
|
|
3870
|
+
const isCurrentOrder = () => this.store.tempOrder === liveTempOrder && liveTempOrder.order_id === committedIdentity[0] && liveTempOrder.external_sale_number === committedIdentity[1] && (options?.availabilityCommitTargetGuard?.() ?? true);
|
|
3871
|
+
try {
|
|
3872
|
+
// 新增已发布,即使后续报价/促销收尾失败,也不能向调用方报告未加购。
|
|
3873
|
+
// 先保存已提交行;异步结果只允许写回仍属于本次提交的订单。
|
|
3874
|
+
if (isCurrentOrder()) this.persistTempOrder();
|
|
3875
|
+
await this.finalizeProductOrderMutation(liveTempOrder, {
|
|
3876
|
+
isCurrentOrder
|
|
3877
|
+
});
|
|
3878
|
+
} catch (error) {
|
|
3879
|
+
this.logWarning('addProductToOrder committed; finalization failed', {
|
|
3880
|
+
product_id: product.product_id,
|
|
3881
|
+
committed: true,
|
|
3882
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3883
|
+
});
|
|
3884
|
+
}
|
|
3885
|
+
} else {
|
|
3886
|
+
await this.finalizeProductOrderMutation(liveTempOrder);
|
|
3887
|
+
}
|
|
3806
3888
|
this.logInfo('addProductToOrder success', {
|
|
3807
3889
|
product_id: product.product_id,
|
|
3808
3890
|
with_booking: !!booking
|
|
3809
3891
|
});
|
|
3810
|
-
return
|
|
3892
|
+
return liveTempOrder.products;
|
|
3811
3893
|
}
|
|
3812
3894
|
|
|
3813
3895
|
/**
|
|
@@ -445,6 +445,15 @@ export interface AddProductToOrderOptions {
|
|
|
445
445
|
disableMerge?: boolean;
|
|
446
446
|
/** 商品行 UID 落定后,由 OrderModule 原子绑定到该行的 Sales Notes 草稿。 */
|
|
447
447
|
salesNotes?: SalesNoteDraftInput[];
|
|
448
|
+
/**
|
|
449
|
+
* 自动预约加购的同步提交门禁;抛错时不发布本次新增。
|
|
450
|
+
* 带门禁时先在私有草稿中准备全部商品/booking,异步等待后及原子发布前重新调用。
|
|
451
|
+
* 门禁须校验当前订单身份/版本和真实时间;发布前不会因本次拆行改变订单版本。
|
|
452
|
+
* 不传时保留原有人工加购行为。
|
|
453
|
+
*/
|
|
454
|
+
availabilityCommitGuard?: () => void;
|
|
455
|
+
/** 发布后的订单/业务归属检查;只用于阻止旧异步收尾写入新单,不再校验截止或 revision。 */
|
|
456
|
+
availabilityCommitTargetGuard?: () => boolean;
|
|
448
457
|
}
|
|
449
458
|
/**
|
|
450
459
|
* 删除商品后的可选位置保持策略。
|
|
@@ -121,8 +121,9 @@ export declare function getBundleSellingMagnitude(bundle: Record<string, any> |
|
|
|
121
121
|
* = `metadata.main_product_original_price` + Σ(bundle 原价 × num)。
|
|
122
122
|
* - `metadata.source_product_price`:主商品/variant 基础价(已应用报价单),不含 option、
|
|
123
123
|
* 不含折扣。**权威源**。
|
|
124
|
-
* - `
|
|
125
|
-
* - `metadata.
|
|
124
|
+
* - `product_sku.option[].add_price` 保留单规格折前单价;Rules 的 option.price 只用于本次计算。
|
|
125
|
+
* - `metadata.main_product_original_price` = source + Σ(option.add_price × option.num)(含 option、不含折扣)。
|
|
126
|
+
* - `metadata.main_product_selling_price` = main_original − 行内 per-unit amount(含主商品及 option 优惠)。
|
|
126
127
|
* - `payment_price`:出站 payload 中为行 composite 已减整单折扣均摊价(无折扣时与 `selling_price` 同义)。
|
|
127
128
|
*
|
|
128
129
|
* Rules 钩子契约(hook_adapt 方案,Rules 内部保持不变):
|
|
@@ -131,8 +132,9 @@ export declare function getBundleSellingMagnitude(bundle: Record<string, any> |
|
|
|
131
132
|
* `getProductOriginTotalPrice` 会自行叠加 option + bundle,喂 source 可避免 option 双加。
|
|
132
133
|
* - `getProduct.total` = `selling_price × num`(行 composite × num),与 Rules 内部产出的
|
|
133
134
|
* total(source + option + bundle)对齐。
|
|
135
|
+
* - `getProduct.options` 用单规格原价参与重算;主商品折后价从 main_selling 减去 option 折后合计反推。
|
|
134
136
|
* - `setProduct` 反向流程:把 Rules 返回的 source-level `main_product_selling_price` / `price`
|
|
135
|
-
*
|
|
137
|
+
* 加回单规格原价合计,再减去 optionDiscountAmount;保留订单单规格原价供取消折扣及后续重算。
|
|
136
138
|
* 优先级:
|
|
137
139
|
* 1. `values.main_product_selling_price`(券应用分支,per-unit 券后 source-level 主价)
|
|
138
140
|
* 2. `values.price`(还原分支 `restoredPrice`、good_pass 归零)
|
|
@@ -641,8 +641,9 @@ function getBundleSellingMagnitude(bundle) {
|
|
|
641
641
|
* = `metadata.main_product_original_price` + Σ(bundle 原价 × num)。
|
|
642
642
|
* - `metadata.source_product_price`:主商品/variant 基础价(已应用报价单),不含 option、
|
|
643
643
|
* 不含折扣。**权威源**。
|
|
644
|
-
* - `
|
|
645
|
-
* - `metadata.
|
|
644
|
+
* - `product_sku.option[].add_price` 保留单规格折前单价;Rules 的 option.price 只用于本次计算。
|
|
645
|
+
* - `metadata.main_product_original_price` = source + Σ(option.add_price × option.num)(含 option、不含折扣)。
|
|
646
|
+
* - `metadata.main_product_selling_price` = main_original − 行内 per-unit amount(含主商品及 option 优惠)。
|
|
646
647
|
* - `payment_price`:出站 payload 中为行 composite 已减整单折扣均摊价(无折扣时与 `selling_price` 同义)。
|
|
647
648
|
*
|
|
648
649
|
* Rules 钩子契约(hook_adapt 方案,Rules 内部保持不变):
|
|
@@ -651,8 +652,9 @@ function getBundleSellingMagnitude(bundle) {
|
|
|
651
652
|
* `getProductOriginTotalPrice` 会自行叠加 option + bundle,喂 source 可避免 option 双加。
|
|
652
653
|
* - `getProduct.total` = `selling_price × num`(行 composite × num),与 Rules 内部产出的
|
|
653
654
|
* total(source + option + bundle)对齐。
|
|
655
|
+
* - `getProduct.options` 用单规格原价参与重算;主商品折后价从 main_selling 减去 option 折后合计反推。
|
|
654
656
|
* - `setProduct` 反向流程:把 Rules 返回的 source-level `main_product_selling_price` / `price`
|
|
655
|
-
*
|
|
657
|
+
* 加回单规格原价合计,再减去 optionDiscountAmount;保留订单单规格原价供取消折扣及后续重算。
|
|
656
658
|
* 优先级:
|
|
657
659
|
* 1. `values.main_product_selling_price`(券应用分支,per-unit 券后 source-level 主价)
|
|
658
660
|
* 2. `values.price`(还原分支 `restoredPrice`、good_pass 归零)
|
|
@@ -851,6 +853,14 @@ function createDefaultOrderRulesHooks(getBookings) {
|
|
|
851
853
|
const effectiveNum = Number(num) > 0 ? Number(num) : 1;
|
|
852
854
|
return new _decimal.default(Number(totalLike) || 0).dividedBy(effectiveNum).toDecimalPlaces(2).toString();
|
|
853
855
|
};
|
|
856
|
+
|
|
857
|
+
// optionDiscountAmount 已是每件主商品内的单规格优惠合计,不再乘商品数量。
|
|
858
|
+
const getOptionDiscountAmount = discountList => {
|
|
859
|
+
return (discountList || []).reduce((total, discount) => {
|
|
860
|
+
if (discount?.metadata?.custom_product_bundle_map_id) return total;
|
|
861
|
+
return total.plus(Number(discount?.metadata?.optionDiscountAmount) || 0);
|
|
862
|
+
}, new _decimal.default(0));
|
|
863
|
+
};
|
|
854
864
|
const getBundleIdentity = bundle => {
|
|
855
865
|
return [bundle.bundle_id ?? bundle.id ?? '', bundle.bundle_product_id ?? bundle._bundle_product_id ?? bundle.product_id ?? '', bundle.bundle_variant_id ?? bundle.variant_id ?? bundle.product_variant_id ?? 0, bundle.bundle_group_id ?? bundle.group_id ?? ''].join('|');
|
|
856
866
|
};
|
|
@@ -893,12 +903,14 @@ function createDefaultOrderRulesHooks(getBookings) {
|
|
|
893
903
|
getProduct: product => {
|
|
894
904
|
// source_product_price 是权威源;兜底链:source → mainSelling − options(反推)→ 行 selling_price
|
|
895
905
|
const metadataAny = product.metadata || {};
|
|
896
|
-
const
|
|
906
|
+
const productOptions = getProductSkuOptions(product);
|
|
907
|
+
const optionSum = sumOptionUnitPrice(productOptions);
|
|
908
|
+
const optionSellingSum = optionSum.minus(getOptionDiscountAmount(product.discount_list));
|
|
897
909
|
let sourcePrice;
|
|
898
910
|
if (metadataAny.source_product_price !== undefined) {
|
|
899
911
|
sourcePrice = String(metadataAny.source_product_price);
|
|
900
912
|
} else if (metadataAny.main_product_selling_price !== undefined) {
|
|
901
|
-
sourcePrice = new _decimal.default(Number(metadataAny.main_product_selling_price) || 0).minus(
|
|
913
|
+
sourcePrice = new _decimal.default(Number(metadataAny.main_product_selling_price) || 0).minus(optionSellingSum).toDecimalPlaces(2).toString();
|
|
902
914
|
} else {
|
|
903
915
|
sourcePrice = String(product.selling_price ?? 0);
|
|
904
916
|
}
|
|
@@ -928,6 +940,11 @@ function createDefaultOrderRulesHooks(getBookings) {
|
|
|
928
940
|
num: product.num || 1,
|
|
929
941
|
discount_list: product.discount_list || [],
|
|
930
942
|
bundle: product.product_bundle || [],
|
|
943
|
+
// 仅在 Rules 入参中映射 price,避免折后价污染订单的 add_price。
|
|
944
|
+
options: productOptions.map(option => ({
|
|
945
|
+
...option,
|
|
946
|
+
price: option.add_price
|
|
947
|
+
})),
|
|
931
948
|
booking_id: product.metadata?.booking_id || null,
|
|
932
949
|
isClient: false,
|
|
933
950
|
// 勿写死 false:否则 Rules 不会走 total≠origin_total 的手动价推断,
|
|
@@ -935,7 +952,7 @@ function createDefaultOrderRulesHooks(getBookings) {
|
|
|
935
952
|
isManualDiscount: resolveRulesManualDiscountFlag(metadataAny),
|
|
936
953
|
holder_id: product.metadata?.holder_id,
|
|
937
954
|
startDate: getProductStartDate(product),
|
|
938
|
-
main_product_selling_price: metadataAny.main_product_selling_price !== undefined ? new _decimal.default(Number(metadataAny.main_product_selling_price) || 0).minus(
|
|
955
|
+
main_product_selling_price: metadataAny.main_product_selling_price !== undefined ? new _decimal.default(Number(metadataAny.main_product_selling_price) || 0).minus(optionSellingSum).toDecimalPlaces(2).toString() : undefined,
|
|
939
956
|
inPromotion: metadataAny._promotion?.inPromotion === true,
|
|
940
957
|
_promotion: metadataAny._promotion
|
|
941
958
|
};
|
|
@@ -946,8 +963,10 @@ function createDefaultOrderRulesHooks(getBookings) {
|
|
|
946
963
|
const existedSource = metadataAny.source_product_price ?? product.selling_price ?? '0';
|
|
947
964
|
const nextOptions = getProductSkuOptions(product);
|
|
948
965
|
const optionSum = sumOptionUnitPrice(nextOptions);
|
|
966
|
+
const nextDiscountList = values.discount_list ?? product.discount_list;
|
|
967
|
+
const optionSellingSum = optionSum.minus(getOptionDiscountAmount(nextDiscountList));
|
|
949
968
|
|
|
950
|
-
// Rules 返回的 main_product_selling_price / price
|
|
969
|
+
// Rules 返回的 main_product_selling_price / price 为不含 option 的主商品价。
|
|
951
970
|
// 优先级:main_product_selling_price(per-unit 券后 source)> price(还原/good_pass 分支)>
|
|
952
971
|
// total/num(仅当 Rules 只给 total 时的兜底)> 保留当前 source。
|
|
953
972
|
const nextSourceSellingPrice = values.main_product_selling_price !== undefined ? String(values.main_product_selling_price) : values.price !== undefined ? String(values.price) : values.total !== undefined ? toUnitPriceString(values.total, nextNum) : String(existedSource);
|
|
@@ -956,8 +975,8 @@ function createDefaultOrderRulesHooks(getBookings) {
|
|
|
956
975
|
// 缺省时保留现有 source(券前 = 当前 source)。
|
|
957
976
|
const nextSourceOriginalPrice = values.original_price !== undefined ? String(values.original_price) : String(existedSource);
|
|
958
977
|
|
|
959
|
-
//
|
|
960
|
-
const nextMainSellingPrice = new _decimal.default(Number(nextSourceSellingPrice) || 0).plus(
|
|
978
|
+
// 成交价只加回 option 折后合计;原价仍使用订单中保存的 option 原价。
|
|
979
|
+
const nextMainSellingPrice = new _decimal.default(Number(nextSourceSellingPrice) || 0).plus(optionSellingSum).toDecimalPlaces(2).toFixed(2);
|
|
961
980
|
const nextMainOriginalPrice = new _decimal.default(Number(nextSourceOriginalPrice) || 0).plus(optionSum).toDecimalPlaces(2).toFixed(2);
|
|
962
981
|
const nextBundle = values.bundle !== undefined ? mergeRulesBundlePrices(product.product_bundle, values.bundle) : product.product_bundle;
|
|
963
982
|
const composedSellingPrice = composeLinePrice({
|
|
@@ -978,7 +997,7 @@ function createDefaultOrderRulesHooks(getBookings) {
|
|
|
978
997
|
...product,
|
|
979
998
|
selling_price: composedSellingPrice,
|
|
980
999
|
original_price: finalOriginalPrice,
|
|
981
|
-
discount_list:
|
|
1000
|
+
discount_list: nextDiscountList,
|
|
982
1001
|
num: values.quantity ?? product.num,
|
|
983
1002
|
product_bundle: nextBundle,
|
|
984
1003
|
metadata: {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { AvailabilityProjection, AvailabilityProjectionInput, AvailabilityQuery, AvailabilityQueryResult, AvailabilitySelectionValidationParams, AvailabilitySelectionValidationResult, GetProductTimeRangesRequest, GetProductTimeRangesRuntimeOptions, ProductTimeRangeGroup } from './types';
|
|
2
2
|
export declare function createAvailabilityProjection(input: AvailabilityProjectionInput): AvailabilityProjection;
|
|
3
|
+
/** Resolve the label without changing the real calendar timestamp. */
|
|
4
|
+
export declare function getDurationBusinessDate(projection: AvailabilityProjection, startAt: string): string;
|
|
3
5
|
export declare function getProductTimeRanges(projection: AvailabilityProjection, request?: GetProductTimeRangesRequest, runtimeOptions?: GetProductTimeRangesRuntimeOptions): ProductTimeRangeGroup[];
|
|
4
6
|
export declare function queryAvailabilityProjection(projection: AvailabilityProjection, request: AvailabilityQuery, runtimeOptions?: GetProductTimeRangesRuntimeOptions): AvailabilityQueryResult;
|
|
5
7
|
export declare function validateAvailabilitySelection(projection: AvailabilityProjection, params: AvailabilitySelectionValidationParams, runtimeOptions?: GetProductTimeRangesRuntimeOptions): AvailabilitySelectionValidationResult;
|