@pisell/pisellos 0.10.54 → 0.10.55

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 (37) hide show
  1. package/dist/model/strategy/adapter/promotion/index.js +9 -0
  2. package/dist/modules/BusinessAvailability/index.d.ts +30 -0
  3. package/dist/modules/BusinessAvailability/index.js +204 -0
  4. package/dist/modules/OpenData/types.d.ts +2 -0
  5. package/dist/modules/Order/index.d.ts +5 -2
  6. package/dist/modules/Order/index.js +315 -241
  7. package/dist/modules/Order/types.d.ts +8 -0
  8. package/dist/solution/BaseSales/index.d.ts +4 -15
  9. package/dist/solution/BaseSales/index.js +131 -86
  10. package/dist/solution/BaseSales/types.d.ts +11 -0
  11. package/dist/solution/BookingByStep/index.d.ts +1 -1
  12. package/dist/solution/BookingTicket/index.d.ts +1 -1
  13. package/dist/solution/BookingTicket/index.js +41 -13
  14. package/dist/solution/UnifiedBookingSales/businessAvailability.d.ts +27 -0
  15. package/dist/solution/UnifiedBookingSales/businessAvailability.js +292 -0
  16. package/dist/solution/UnifiedBookingSales/index.d.ts +11 -0
  17. package/dist/solution/UnifiedBookingSales/index.js +154 -75
  18. package/dist/solution/UnifiedBookingSales/types.d.ts +2 -0
  19. package/lib/model/strategy/adapter/promotion/index.js +1 -46
  20. package/lib/modules/BusinessAvailability/index.d.ts +30 -0
  21. package/lib/modules/BusinessAvailability/index.js +146 -0
  22. package/lib/modules/OpenData/types.d.ts +2 -0
  23. package/lib/modules/Order/index.d.ts +5 -2
  24. package/lib/modules/Order/index.js +62 -6
  25. package/lib/modules/Order/types.d.ts +8 -0
  26. package/lib/solution/BaseSales/index.d.ts +4 -15
  27. package/lib/solution/BaseSales/index.js +45 -24
  28. package/lib/solution/BaseSales/types.d.ts +11 -0
  29. package/lib/solution/BookingByStep/index.d.ts +1 -1
  30. package/lib/solution/BookingTicket/index.d.ts +1 -1
  31. package/lib/solution/BookingTicket/index.js +37 -4
  32. package/lib/solution/UnifiedBookingSales/businessAvailability.d.ts +27 -0
  33. package/lib/solution/UnifiedBookingSales/businessAvailability.js +138 -0
  34. package/lib/solution/UnifiedBookingSales/index.d.ts +11 -0
  35. package/lib/solution/UnifiedBookingSales/index.js +54 -0
  36. package/lib/solution/UnifiedBookingSales/types.d.ts +2 -0
  37. package/package.json +1 -1
@@ -216,6 +216,8 @@ export interface UnifiedBookingSalesDiscountResult {
216
216
  summaryAfter?: unknown;
217
217
  }
218
218
  export interface UnifiedBookingSalesApi {
219
+ /** Uses cached business settings and evaluates the current store-local clock. */
220
+ getBusinessAvailability: () => Promise<import('../../modules/BusinessAvailability').BusinessAvailability>;
219
221
  loadOpenData: (params: UnifiedBookingSalesLoadOpenDataParams) => Promise<OpenDataConfig>;
220
222
  /** Returns the latest OpenData payload cached by this solution instance. */
221
223
  getOpenData: () => Promise<OpenDataConfig | null>;
@@ -1,46 +1 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- Object.defineProperty(exports, "BUY_X_GET_Y_FREE_STRATEGY", {
7
- enumerable: true,
8
- get: function () {
9
- return _examples.BUY_X_GET_Y_FREE_STRATEGY;
10
- }
11
- });
12
- Object.defineProperty(exports, "ITEM_REWARD_STRATEGY", {
13
- enumerable: true,
14
- get: function () {
15
- return _examples.ITEM_REWARD_STRATEGY;
16
- }
17
- });
18
- Object.defineProperty(exports, "PromotionAdapter", {
19
- enumerable: true,
20
- get: function () {
21
- return _adapter.PromotionAdapter;
22
- }
23
- });
24
- Object.defineProperty(exports, "PromotionEvaluator", {
25
- enumerable: true,
26
- get: function () {
27
- return _evaluator.PromotionEvaluator;
28
- }
29
- });
30
- Object.defineProperty(exports, "X_ITEMS_FOR_Y_PRICE_STRATEGY", {
31
- enumerable: true,
32
- get: function () {
33
- return _examples.X_ITEMS_FOR_Y_PRICE_STRATEGY;
34
- }
35
- });
36
- Object.defineProperty(exports, "default", {
37
- enumerable: true,
38
- get: function () {
39
- return _adapter.default;
40
- }
41
- });
42
- var _evaluator = require("./evaluator");
43
- var _adapter = _interopRequireWildcard(require("./adapter"));
44
- var _examples = require("./examples");
45
- function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
46
- function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
1
+ "use strict";
@@ -0,0 +1,30 @@
1
+ import type { MultiLangObject } from '../OpenData/types';
2
+ import type { ScheduleItem } from '../Schedule/types';
3
+ export type BusinessClosedBehavior = 'show_menu_disabled' | 'hide_all';
4
+ export interface BusinessAvailability {
5
+ status: 'open' | 'paused' | 'closed' | 'error';
6
+ behavior: BusinessClosedBehavior | null;
7
+ message: string | MultiLangObject | null;
8
+ /** Epoch milliseconds. Re-evaluate the cached configuration at this time. */
9
+ nextChangeAt: number;
10
+ requirePaymentOnSubmit: boolean;
11
+ error?: string;
12
+ }
13
+ export interface BusinessAvailabilityConfig {
14
+ paused: boolean;
15
+ pauseBehavior: BusinessClosedBehavior;
16
+ closedBehavior: BusinessClosedBehavior;
17
+ pauseMessage: BusinessAvailability['message'];
18
+ closedMessage: BusinessAvailability['message'];
19
+ scheduleIds: number[];
20
+ invalidScheduleIds: boolean;
21
+ requirePaymentOnSubmit: boolean;
22
+ }
23
+ export declare function normalizeBusinessAvailabilityConfig(data: Record<string, unknown>): BusinessAvailabilityConfig;
24
+ /** Business ordering is independent of reservation switches and Pickup slot availability. */
25
+ export declare function computeBusinessAvailability(input: {
26
+ config: BusinessAvailabilityConfig;
27
+ schedules: ScheduleItem[];
28
+ timezone: string;
29
+ now: Date;
30
+ }): BusinessAvailability;
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.computeBusinessAvailability = computeBusinessAvailability;
7
+ exports.normalizeBusinessAvailabilityConfig = normalizeBusinessAvailabilityConfig;
8
+ var _dayjs = _interopRequireDefault(require("dayjs"));
9
+ var _isSameOrAfter = _interopRequireDefault(require("dayjs/plugin/isSameOrAfter"));
10
+ var _isSameOrBefore = _interopRequireDefault(require("dayjs/plugin/isSameOrBefore"));
11
+ var _getDateIsInSchedule = require("../Schedule/getDateIsInSchedule");
12
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
13
+ _dayjs.default.extend(_isSameOrAfter.default);
14
+ _dayjs.default.extend(_isSameOrBefore.default);
15
+ const DAY = 86_400_000;
16
+ const behavior = value => value === 'show_menu_disabled' ? value : 'hide_all';
17
+ const message = value => typeof value === 'string' || value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
18
+ function normalizeBusinessAvailabilityConfig(data) {
19
+ const rawIds = data['availability.operating_hours'];
20
+ const ids = Array.isArray(rawIds) ? rawIds.map(id => typeof id === 'number' || typeof id === 'string' ? Number(id) : NaN) : [];
21
+ const validIds = ids.filter(id => Number.isInteger(id) && id > 0);
22
+ const paused = data['availability.paused'];
23
+ return {
24
+ paused: paused === true || paused === 1 || paused === 'true' || paused === '1',
25
+ pauseBehavior: behavior(data['availability.pause_behavior']),
26
+ closedBehavior: behavior(data['availability.closed_behavior']),
27
+ pauseMessage: message(data['availability.pause_message']),
28
+ closedMessage: message(data['availability.closed_message']),
29
+ scheduleIds: [...new Set(validIds)],
30
+ invalidScheduleIds: rawIds != null && (!Array.isArray(rawIds) || validIds.length !== ids.length),
31
+ // Only an explicitly disabled setting changes the existing payment flow.
32
+ requirePaymentOnSubmit: data['sale.require_payment_on_submit'] !== false
33
+ };
34
+ }
35
+ function createClock(timezone) {
36
+ if (!timezone) throw new Error('Store timezone is unavailable.');
37
+ const formatter = new Intl.DateTimeFormat('en-GB', {
38
+ timeZone: timezone,
39
+ year: 'numeric',
40
+ month: '2-digit',
41
+ day: '2-digit',
42
+ hour: '2-digit',
43
+ minute: '2-digit',
44
+ second: '2-digit',
45
+ hourCycle: 'h23'
46
+ });
47
+ const local = timestamp => {
48
+ const parts = {};
49
+ formatter.formatToParts(timestamp).forEach(part => {
50
+ parts[part.type] = part.value;
51
+ });
52
+ const date = `${parts.year}-${parts.month}-${parts.day}`;
53
+ return {
54
+ date,
55
+ scalar: Date.parse(`${date}T${parts.hour}:${parts.minute}:${parts.second}Z`)
56
+ };
57
+ };
58
+ const offsets = new Set();
59
+ const instants = scalar => {
60
+ offsets.clear();
61
+ for (let hours = -36; hours <= 36; hours += 6) {
62
+ const sample = scalar + hours * 3_600_000;
63
+ offsets.add(local(sample).scalar - sample);
64
+ }
65
+ return [...offsets].map(offset => scalar - offset).filter(instant => local(instant).scalar === scalar).sort((a, b) => a - b);
66
+ };
67
+ return {
68
+ local,
69
+ instants
70
+ };
71
+ }
72
+
73
+ /** Business ordering is independent of reservation switches and Pickup slot availability. */
74
+ function computeBusinessAvailability(input) {
75
+ const {
76
+ config,
77
+ schedules,
78
+ timezone,
79
+ now
80
+ } = input;
81
+ const nowAt = now.getTime();
82
+ const base = {
83
+ requirePaymentOnSubmit: config.requirePaymentOnSubmit,
84
+ nextChangeAt: nowAt + DAY
85
+ };
86
+ if (config.paused) {
87
+ return {
88
+ ...base,
89
+ status: 'paused',
90
+ behavior: config.pauseBehavior,
91
+ message: config.pauseMessage
92
+ };
93
+ }
94
+ if (config.invalidScheduleIds) throw new Error('Business operating-hours configuration is invalid.');
95
+ if (!config.scheduleIds.length) return {
96
+ ...base,
97
+ status: 'open',
98
+ behavior: null,
99
+ message: null
100
+ };
101
+ const selected = schedules.filter(schedule => config.scheduleIds.includes(Number(schedule.id)));
102
+ if (config.scheduleIds.some(id => !selected.some(schedule => Number(schedule.id) === id))) {
103
+ throw new Error('A configured business operating-hours schedule is unavailable.');
104
+ }
105
+ const clock = createClock(timezone);
106
+ const current = clock.local(nowAt);
107
+ const today = Date.parse(`${current.date}T00:00:00Z`);
108
+ const nextMidnight = clock.instants(today + DAY).find(instant => instant > nowAt);
109
+ let nextChangeAt = nextMidnight ?? nowAt + 60_000;
110
+ let open = false;
111
+ // Yesterday contributes the tail of overnight windows; today supplies the next boundary.
112
+ for (const dateAt of [today - DAY, today]) {
113
+ const date = new Date(dateAt).toISOString().slice(0, 10);
114
+ // Schedule's resolver merges by time-of-day, so resolve each schedule independently.
115
+ for (const schedule of selected) {
116
+ for (const window of (0, _getDateIsInSchedule.getScheduleStartEndTimePoints)(date, [schedule])) {
117
+ const toMinutes = value => {
118
+ if (!/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(value) && value !== '24:00') {
119
+ throw new Error('Business operating-hours schedule contains an invalid time.');
120
+ }
121
+ const [hours, minutes] = value.split(':').map(Number);
122
+ return hours * 60 + minutes;
123
+ };
124
+ const start = dateAt + toMinutes(window.start_time) * 60_000;
125
+ let end = dateAt + toMinutes(window.end_time) * 60_000;
126
+ if (end <= start) end += DAY;
127
+ if (current.scalar >= start && current.scalar < end) open = true;
128
+ for (const boundary of [start, end]) {
129
+ for (const instant of clock.instants(boundary)) {
130
+ if (instant > nowAt) nextChangeAt = Math.min(nextChangeAt, instant);
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
136
+ // A timezone offset change can repeat/skip a wall-clock boundary. A local-only safety
137
+ // check also covers throttled timers; it does not fetch either configuration source.
138
+ nextChangeAt = Math.min(nextChangeAt, nowAt + 60_000);
139
+ return {
140
+ ...base,
141
+ nextChangeAt,
142
+ status: open ? 'open' : 'closed',
143
+ behavior: open ? null : config.closedBehavior,
144
+ message: open ? null : config.closedMessage
145
+ };
146
+ }
@@ -69,6 +69,8 @@ export interface OpenDataConfig {
69
69
  'reservation.time_slice'?: number | string;
70
70
  'sale.allow_item_notes'?: boolean;
71
71
  'sale.allow_order_notes'?: boolean;
72
+ /** False submits an order without starting or confirming payment. */
73
+ 'sale.require_payment_on_submit'?: boolean | null;
72
74
  'sale.enable_item_rules'?: boolean;
73
75
  'sale.enable_short_number'?: boolean;
74
76
  'sale.short_number_daily_reset'?: boolean;
@@ -4,7 +4,7 @@ import { OrderModuleAPI, CommitOrderParams, UpdateOrderBookingParams, UpdateOrde
4
4
  import { CartItem } from '../Cart/types';
5
5
  import { type SubmitPayloadEnhancer } from './utils';
6
6
  import { type PickupTimingBooking, type PickupTimingBookingInput } from './pickupTiming';
7
- import type { OrderAmountSnapshot, AddProductBookingInput, AddProductToOrderOptions, OrderIdentitySnapshot, OrderPaymentSource, OrderSnapshot, OrderSyncState, SyncPaymentsToOrderParams, SyncPaymentsToOrderResult, SubmitSalesOrderParams, OrderProduct, OrderProductIdentity, OrderSummary, OrderTempOrder, OrderPaymentData, OrderPaymentUpdateOptions, OrderPaymentUpdateResult, SendCustomerPayLinkParams, UpdateTempOrderCustomerInput, OrderCustomerPatch, OrderCustomerView, ReplaceTempOrderOptions, OrderPromotionEvaluator, OrderGiftSelectResolver, OrderUnfulfilledPromotion, OrderLastGiftActions, RemoveProductsFromOrderOptions, SalesNote, SalesNotesMutationSnapshot, SetSalesNoteInput, PickupReferenceMode, PointCardSnapshotMergeContext, PointCardSnapshotItem, ShopServiceData, ShopServiceType } from './types';
7
+ import type { OrderAmountSnapshot, AddProductBookingInput, AddProductToOrderOptions, OrderIdentitySnapshot, OrderPaymentSource, OrderSnapshot, OrderSyncState, SyncPaymentsToOrderParams, SyncPaymentsToOrderResult, SubmitSalesOrderParams, OrderProduct, OrderProductIdentity, OrderSummary, OrderTempOrder, OrderPaymentData, OrderPaymentUpdateOptions, OrderPaymentUpdateResult, SendCustomerPayLinkParams, UpdateTempOrderCustomerInput, OrderCustomerPatch, OrderCustomerView, OrderCartIntentCommitContext, ReplaceTempOrderOptions, OrderPromotionEvaluator, OrderGiftSelectResolver, OrderUnfulfilledPromotion, OrderLastGiftActions, RemoveProductsFromOrderOptions, SalesNote, SalesNotesMutationSnapshot, SetSalesNoteInput, PickupReferenceMode, PointCardSnapshotMergeContext, PointCardSnapshotItem, ShopServiceData, ShopServiceType } from './types';
8
8
  import type { ICustomer } from '../AccountList/types';
9
9
  import type { Discount } from '../Discount/types';
10
10
  import { UnavailableReason } from '../Rules/types';
@@ -67,6 +67,8 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
67
67
  * orderModule.updateOtherParams({ cacheId: 'cache-2', channel: 'pos' });
68
68
  */
69
69
  updateOtherParams(params: Record<string, any>): void;
70
+ /** Internal intent boundary shared with asynchronous scanner/detail flows. */
71
+ createCartIntentCommitGuard(action: OrderCartIntentCommitContext['action'], tempOrder?: OrderTempOrder | null): () => void;
70
72
  /** PC/H5 属于在线店铺;其它或缺失平台统一沿用 POS 行为。 */
71
73
  isOnlineStorePlatform(): boolean;
72
74
  /**
@@ -501,6 +503,7 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
501
503
  private hasSameEntitlementMergePriceConfig;
502
504
  private getBundleRuntimeIdentity;
503
505
  private preservePersistedBundlePriceFields;
506
+ private isRestrictedCartProductUpdate;
504
507
  private applyOrderProductUpdateToTempOrder;
505
508
  updateOrderProduct(params: UpdateOrderProductParams): Promise<OrderProduct[]>;
506
509
  updateOrderProducts(paramsList: UpdateOrderProductParams[]): Promise<OrderProduct[]>;
@@ -588,7 +591,7 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
588
591
  generateIdempotencyToken(): string;
589
592
  syncPaymentsToOrder<T = any>(params: SyncPaymentsToOrderParams): Promise<SyncPaymentsToOrderResult<T>>;
590
593
  createOrder(params: CommitOrderParams['query']): {
591
- type: "appointment_booking" | "virtual";
594
+ type: "virtual" | "appointment_booking";
592
595
  platform: string;
593
596
  sales_channel: string;
594
597
  order_sales_channel: string;
@@ -440,6 +440,29 @@ class OrderModule extends _BaseModule.BaseModule {
440
440
  this.otherParams = otherParams;
441
441
  }
442
442
 
443
+ /** Internal intent boundary shared with asynchronous scanner/detail flows. */
444
+ createCartIntentCommitGuard(action, tempOrder = this.store.tempOrder) {
445
+ const hook = this.orderHooks?.beforeCartIntentCommit;
446
+ if (!hook) return () => {};
447
+ const getScope = this.orderHooks?.getCartIntentScope;
448
+ const scope = getScope?.();
449
+ const otherParams = this.otherParams;
450
+ let committed = false;
451
+ return () => {
452
+ if (this.store.tempOrder !== tempOrder || this.otherParams !== otherParams || this.orderHooks?.beforeCartIntentCommit !== hook || this.orderHooks?.getCartIntentScope !== getScope || getScope?.() !== scope) {
453
+ throw new Error('[Order] Cart intent belongs to a previous order or business');
454
+ }
455
+ // Derived gifts, discounts and persistence belong to the accepted intent,
456
+ // but an asynchronous continuation must never target a different cart.
457
+ if (committed) return;
458
+ hook({
459
+ action,
460
+ tempOrder
461
+ });
462
+ committed = true;
463
+ };
464
+ }
465
+
443
466
  /** PC/H5 属于在线店铺;其它或缺失平台统一沿用 POS 行为。 */
444
467
  isOnlineStorePlatform() {
445
468
  const platform = String(this.otherParams?.platform || '').trim().toLowerCase();
@@ -668,6 +691,7 @@ class OrderModule extends _BaseModule.BaseModule {
668
691
  });
669
692
  }
670
693
  async scanCode(code, customerId) {
694
+ const beforeCommit = this.createCartIntentCommitGuard('scan_promotion');
671
695
  if (this.isActiveVirtualSettlementOrder(this.store.tempOrder)) {
672
696
  return {
673
697
  type: 'clientCalc',
@@ -715,6 +739,7 @@ class OrderModule extends _BaseModule.BaseModule {
715
739
  }));
716
740
  const currentSelected = this.getDiscountList().filter(n => n.isSelected);
717
741
  if (currentSelected.length && currentSelected.some(n => withScanList.some(m => m.id === n.id))) {
742
+ beforeCommit();
718
743
  await this.syncScannedDiscountsToOriginalDiscountList(this.getDiscountList());
719
744
  return {
720
745
  type: 'clientCalc',
@@ -748,6 +773,7 @@ class OrderModule extends _BaseModule.BaseModule {
748
773
  const normalizedNewDiscountList = newDiscountList ? markScannedDiscounts(newDiscountList, withScanList) : undefined;
749
774
  let resolvedDiscountList = normalizedNewDiscountList || this.getDiscountList();
750
775
  if (isAvailable && normalizedNewDiscountList) {
776
+ beforeCommit();
751
777
  await this.store.discount?.setDiscountList(normalizedNewDiscountList);
752
778
  // 实际写回商品时沿用扫码试算的优先级,避免旧商品行的选卡覆盖本次扫码。
753
779
  this.applyDiscount({
@@ -756,6 +782,7 @@ class OrderModule extends _BaseModule.BaseModule {
756
782
  await this.syncScannedDiscountsToOriginalDiscountList(this.getDiscountList());
757
783
  resolvedDiscountList = this.getDiscountList();
758
784
  } else if (shouldRetainForLater) {
785
+ beforeCommit();
759
786
  // 只合并本次识别的卡券;不把试算产生的其他选券状态写回订单。
760
787
  const retainedDiscounts = new Map(this.getDiscountList().map(discount => [String(discount.id), discount]));
761
788
  evaluatedScannedDiscounts.forEach(discount => {
@@ -3462,7 +3489,6 @@ class OrderModule extends _BaseModule.BaseModule {
3462
3489
  }) : null;
3463
3490
  const productToAdd = linked?.product || product;
3464
3491
  const disableMerge = options?.disableMerge === true;
3465
- this.assertAndLockProductSettlement(tempOrder, productToAdd);
3466
3492
  const incomingFingerprint = disableMerge ? '' : (0, _utils3.buildProductLineFingerprint)((0, _utils.getProductSkuOptions)(productToAdd), productToAdd.product_bundle);
3467
3493
  const normalizedIncoming = this.normalizeVirtualSettlementProductIfEnabled((0, _utils.mergeOrderProductDisplayFields)(productToAdd, (0, _utils3.normalizeOrderProduct)(productToAdd)));
3468
3494
  if (disableMerge) {
@@ -3523,6 +3549,8 @@ class OrderModule extends _BaseModule.BaseModule {
3523
3549
  const matchedProduct = matchedIndex === -1 ? null : tempOrder.products[matchedIndex];
3524
3550
  const existedFingerprint = matchedProduct ? (0, _utils3.buildProductLineFingerprint)((0, _utils.getProductSkuOptions)(matchedProduct), matchedProduct.product_bundle) : '';
3525
3551
  const commitProductLine = shouldMerge => {
3552
+ options?.beforeCommit?.();
3553
+ this.assertAndLockProductSettlement(tempOrder, productToAdd);
3526
3554
  let committedProduct;
3527
3555
  if (matchedIndex === -1 || !matchedProduct || !shouldMerge) {
3528
3556
  this.captureProductRuntime(tempOrder, productToAdd, normalizedIncoming);
@@ -3675,6 +3703,7 @@ class OrderModule extends _BaseModule.BaseModule {
3675
3703
  */
3676
3704
  async addProductToOrder(product, booking, options) {
3677
3705
  const tempOrder = this.ensureTempOrder();
3706
+ const beforeCommit = this.createCartIntentCommitGuard('add_product', tempOrder);
3678
3707
  const salesNoteDrafts = (options?.salesNotes || []).map(draft => ({
3679
3708
  note_type: String(draft.note_type || '').trim(),
3680
3709
  text: {
@@ -3745,7 +3774,8 @@ class OrderModule extends _BaseModule.BaseModule {
3745
3774
  }
3746
3775
  const committedProduct = await this.appendProductLineToTempOrder(tempOrder, splitLine.product, splitLine.booking, {
3747
3776
  insertAtIndex,
3748
- disableMerge: options?.disableMerge
3777
+ disableMerge: options?.disableMerge,
3778
+ beforeCommit
3749
3779
  });
3750
3780
  attachSalesNoteDrafts(committedProduct);
3751
3781
  if (insertAtIndex !== undefined) insertAtIndex += 1;
@@ -3753,14 +3783,16 @@ class OrderModule extends _BaseModule.BaseModule {
3753
3783
  } else {
3754
3784
  const committedProduct = await this.appendProductLineToTempOrder(tempOrder, product, (0, _lodashEs.cloneDeep)(booking), {
3755
3785
  insertAtIndex,
3756
- disableMerge: options?.disableMerge
3786
+ disableMerge: options?.disableMerge,
3787
+ beforeCommit
3757
3788
  });
3758
3789
  attachSalesNoteDrafts(committedProduct);
3759
3790
  }
3760
3791
  } else {
3761
3792
  const committedProduct = await this.appendProductLineToTempOrder(tempOrder, product, undefined, {
3762
3793
  insertAtIndex,
3763
- disableMerge: options?.disableMerge
3794
+ disableMerge: options?.disableMerge,
3795
+ beforeCommit
3764
3796
  });
3765
3797
  attachSalesNoteDrafts(committedProduct);
3766
3798
  }
@@ -3788,6 +3820,7 @@ class OrderModule extends _BaseModule.BaseModule {
3788
3820
  async addProductsToOrder(items, options) {
3789
3821
  const tempOrder = this.ensureTempOrder();
3790
3822
  if (!Array.isArray(items) || items.length === 0) return tempOrder.products;
3823
+ const beforeCommit = this.createCartIntentCommitGuard('add_product', tempOrder);
3791
3824
  for (const item of items || []) {
3792
3825
  const {
3793
3826
  product,
@@ -3800,13 +3833,17 @@ class OrderModule extends _BaseModule.BaseModule {
3800
3833
  if (splitCount > 1) {
3801
3834
  for (let i = 0; i < splitCount; i += 1) {
3802
3835
  const splitLine = this.createSplitBookingLine(productRecord, booking, i, splitCount);
3803
- const appendResult = this.appendProductLineToTempOrder(tempOrder, splitLine.product, splitLine.booking);
3836
+ const appendResult = this.appendProductLineToTempOrder(tempOrder, splitLine.product, splitLine.booking, {
3837
+ beforeCommit
3838
+ });
3804
3839
  if (appendResult) await appendResult;
3805
3840
  }
3806
3841
  continue;
3807
3842
  }
3808
3843
  }
3809
- const appendResult = this.appendProductLineToTempOrder(tempOrder, product, booking);
3844
+ const appendResult = this.appendProductLineToTempOrder(tempOrder, product, booking, {
3845
+ beforeCommit
3846
+ });
3810
3847
  if (appendResult) await appendResult;
3811
3848
  }
3812
3849
  await this.finalizeProductOrderMutation(tempOrder, options);
@@ -3891,6 +3928,16 @@ class OrderModule extends _BaseModule.BaseModule {
3891
3928
  };
3892
3929
  });
3893
3930
  }
3931
+ isRestrictedCartProductUpdate(tempOrder, params) {
3932
+ if (!this.orderHooks?.beforeCartIntentCommit) return false;
3933
+ if (params.booking || Object.keys(params.updates || {}).some(key => key !== 'num')) {
3934
+ return true;
3935
+ }
3936
+ if (params.updates?.num === undefined) return false;
3937
+ const uid = params.unique_identification_number;
3938
+ const product = uid !== undefined ? tempOrder.products.find(item => String(item.metadata?.unique_identification_number || item.unique_identification_number || '') === String(uid)) : tempOrder.products[(0, _utils3.getProductIdentityIndex)(tempOrder.products, params)];
3939
+ return !product || (0, _utils3.getSafeProductNum)(params.updates.num) > product.num;
3940
+ }
3894
3941
  applyOrderProductUpdateToTempOrder(tempOrder, params) {
3895
3942
  const {
3896
3943
  product_id,
@@ -4089,6 +4136,9 @@ class OrderModule extends _BaseModule.BaseModule {
4089
4136
  }
4090
4137
  async updateOrderProduct(params) {
4091
4138
  const tempOrder = this.ensureTempOrder();
4139
+ if (this.isRestrictedCartProductUpdate(tempOrder, params)) {
4140
+ this.createCartIntentCommitGuard('update_product', tempOrder)();
4141
+ }
4092
4142
  const updatedProductUid = this.applyOrderProductUpdateToTempOrder(tempOrder, params);
4093
4143
  const promotionPayload = await this.evaluatePromotion();
4094
4144
  this.applyDiscount({
@@ -4110,6 +4160,9 @@ class OrderModule extends _BaseModule.BaseModule {
4110
4160
  if (!paramsList.length) {
4111
4161
  return tempOrder.products;
4112
4162
  }
4163
+ if (paramsList.some(params => this.isRestrictedCartProductUpdate(tempOrder, params))) {
4164
+ this.createCartIntentCommitGuard('update_product', tempOrder)();
4165
+ }
4113
4166
  const reapplyBookingDiscountProductUids = paramsList.reduce((acc, params) => {
4114
4167
  const updatedProductUid = this.applyOrderProductUpdateToTempOrder(tempOrder, params);
4115
4168
  // 批量改预约时间时,延迟到最后一次折扣计算里统一恢复已选预约折扣卡。
@@ -4192,6 +4245,9 @@ class OrderModule extends _BaseModule.BaseModule {
4192
4245
  throw new _virtualSettlement.VirtualCurrencyBalanceError(balance.toFixed(2), requiredAmount.toFixed(2));
4193
4246
  }
4194
4247
  }
4248
+ if (normalizedProduct.num > targetProduct.num) {
4249
+ this.createCartIntentCommitGuard('increase_quantity', tempOrder)();
4250
+ }
4195
4251
  tempOrder.products[productIndex] = normalizedProduct;
4196
4252
  this.prepareVirtualSettlementTempOrder(tempOrder);
4197
4253
  const isVirtualSettlement = this.isActiveVirtualSettlementOrder(tempOrder);
@@ -517,7 +517,15 @@ export type OrderProductMergeDecisionResult = boolean | {
517
517
  reason?: string;
518
518
  [key: string]: any;
519
519
  };
520
+ export interface OrderCartIntentCommitContext {
521
+ action: 'add_product' | 'update_product' | 'increase_quantity' | 'scan_promotion';
522
+ tempOrder: OrderTempOrder | null;
523
+ }
520
524
  export interface OrderHooksConfig {
525
+ /** Optional customer-order guard; called synchronously before the first cart write. */
526
+ beforeCartIntentCommit?: (context: OrderCartIntentCommitContext) => void;
527
+ /** Captures tenant/business runtime identity for asynchronous cart intents. */
528
+ getCartIntentScope?: () => unknown;
521
529
  onBeforeMergeProductToOrder?: (context: OrderProductMergeDecisionContext) => OrderProductMergeDecisionResult | Promise<OrderProductMergeDecisionResult>;
522
530
  }
523
531
  /**
@@ -1,6 +1,6 @@
1
1
  import { Module, ModuleOptions, PisellCore } from '../../types';
2
2
  import { BaseModule } from '../../modules/BaseModule';
3
- import { BaseSalesOrderProduct, BaseSalesOrderProductIdentity, BaseSalesPaymentStatus, BaseSalesUpdateOrderProductQuantityParams, BaseSalesCalculateProductBookingPriceParams, BaseSalesPriceRecalculationContextChange, BaseSalesProductBookingPriceResult, BaseSalesQuotationCustomerScopeInfo, BaseSalesScanCodeResult, BaseSalesCashPaymentConfig, BaseSalesPayCashParams, BaseSalesPayCashResult, BaseSalesCommitPaymentMethodParams, BaseSalesCommitPaymentMethodResult, BaseSalesConfirmWalletPaymentResult } from './types';
3
+ import { BaseSalesOrderProduct, BaseSalesOrderProductIdentity, BaseSalesPaymentStatus, BaseSalesSubmitSalesOrderParams, BaseSalesUpdateOrderProductQuantityParams, BaseSalesCalculateProductBookingPriceParams, BaseSalesPriceRecalculationContextChange, BaseSalesProductBookingPriceResult, BaseSalesQuotationCustomerScopeInfo, BaseSalesScanCodeResult, BaseSalesCashPaymentConfig, BaseSalesPayCashParams, BaseSalesPayCashResult, BaseSalesCommitPaymentMethodParams, BaseSalesCommitPaymentMethodResult, BaseSalesConfirmWalletPaymentResult } from './types';
4
4
  import { OrderModule } from '../../modules/Order';
5
5
  import { type PickupTimingBookingInput } from '../../modules/Order/pickupTiming';
6
6
  import type { PickupTimingAvailability, PickupTimingSelection, PickupTimingValidationResult } from '../../modules/FulfillmentTiming';
@@ -337,13 +337,8 @@ export declare class BaseSalesImpl extends BaseModule implements Module {
337
337
  productList?: Record<string, any>[];
338
338
  discountList?: Discount[];
339
339
  }): Promise<void>;
340
- submitScanOrder<T = any>(params?: {
341
- payments?: OrderPaymentSource[];
342
- paymentStatus?: BaseSalesPaymentStatus;
343
- smallTicketDataFlag?: number;
344
- confirmPendingVoucherPayments?: boolean;
345
- enhancePayload?: SubmitPayloadEnhancer;
346
- }): Promise<T>;
340
+ submitScanOrder<T = any>(params?: BaseSalesSubmitSalesOrderParams): Promise<T>;
341
+ private buildVirtualSettlementOrderData;
347
342
  private buildVirtualSettlementSubmitData;
348
343
  /**
349
344
  * 提交当前 Sales 订单。
@@ -351,13 +346,7 @@ export declare class BaseSalesImpl extends BaseModule implements Module {
351
346
  * PaymentModal 场景通过 smallTicketDataFlag=1 请求 B 端小票数据;
352
347
  * payments 会交给 OrderModule 统一映射为 Sales 协议支付项。
353
348
  */
354
- submitSalesOrder<T = any>(params?: {
355
- payments?: OrderPaymentSource[];
356
- paymentStatus?: BaseSalesPaymentStatus;
357
- smallTicketDataFlag?: number;
358
- confirmPendingVoucherPayments?: boolean;
359
- enhancePayload?: SubmitPayloadEnhancer;
360
- }): Promise<T>;
349
+ submitSalesOrder<T = any>(params?: BaseSalesSubmitSalesOrderParams): Promise<T>;
361
350
  saveSalesOrderDraft<T = any>(params?: {
362
351
  platform?: string;
363
352
  businessCode?: string;
@@ -2432,7 +2432,7 @@ class BaseSalesImpl extends _BaseModule.BaseModule {
2432
2432
  // Deprecated compatibility alias. New BaseSales callers should use submitSalesOrder.
2433
2433
  return this.submitSalesOrder(params);
2434
2434
  }
2435
- async buildVirtualSettlementSubmitData() {
2435
+ buildVirtualSettlementOrderData() {
2436
2436
  if (!this.store.order) return null;
2437
2437
  const tempOrder = this.store.order.getTempOrder();
2438
2438
  if (!this.isActiveVirtualSettlementOrder(tempOrder)) return null;
@@ -2441,6 +2441,21 @@ class BaseSalesImpl extends _BaseModule.BaseModule {
2441
2441
  if (!snapshot || !Number.isFinite(currencyProductId) || currencyProductId <= 0) {
2442
2442
  throw new Error('Virtual currency settlement configuration is invalid.');
2443
2443
  }
2444
+ return {
2445
+ snapshot,
2446
+ currencyProductId,
2447
+ currencyCode: String(tempOrder?.currency_code || ''),
2448
+ currencySymbol: String(tempOrder?.currency_symbol || ''),
2449
+ currencyFormat: String(tempOrder?.currency_format || 'symbol_first')
2450
+ };
2451
+ }
2452
+ async buildVirtualSettlementSubmitData() {
2453
+ const settlement = this.buildVirtualSettlementOrderData();
2454
+ if (!settlement || !this.store.order) return null;
2455
+ const tempOrder = this.store.order.getTempOrder();
2456
+ const {
2457
+ currencyProductId
2458
+ } = settlement;
2444
2459
  const summary = await this.store.order.recalculateSummary({
2445
2460
  createIfMissing: true
2446
2461
  });
@@ -2455,17 +2470,10 @@ class BaseSalesImpl extends _BaseModule.BaseModule {
2455
2470
  });
2456
2471
  }
2457
2472
  const asset = this.store.order.getVirtualCurrencyList().find(item => Number(item?.product_id) === currencyProductId);
2458
- const currencyCode = String(tempOrder?.currency_code || '');
2459
- const currencySymbol = String(tempOrder?.currency_symbol || '');
2460
- const currencyFormat = String(tempOrder?.currency_format || 'symbol_first');
2461
2473
  if (amount.lte(0)) {
2462
2474
  return {
2463
- payments: [],
2464
- snapshot,
2465
- currencyProductId,
2466
- currencyCode,
2467
- currencySymbol,
2468
- currencyFormat
2475
+ ...settlement,
2476
+ payments: []
2469
2477
  };
2470
2478
  }
2471
2479
  if (!asset) {
@@ -2519,12 +2527,8 @@ class BaseSalesImpl extends _BaseModule.BaseModule {
2519
2527
  };
2520
2528
  }
2521
2529
  return {
2522
- payments: [payment],
2523
- snapshot,
2524
- currencyProductId,
2525
- currencyCode,
2526
- currencySymbol,
2527
- currencyFormat
2530
+ ...settlement,
2531
+ payments: [payment]
2528
2532
  };
2529
2533
  }
2530
2534
 
@@ -2541,9 +2545,11 @@ class BaseSalesImpl extends _BaseModule.BaseModule {
2541
2545
  if (this.isPickupTimingEnabled()) await this.preparePickupTimingForCheckout({
2542
2546
  freeze: true
2543
2547
  });
2544
- const virtualSettlement = await this.buildVirtualSettlementSubmitData();
2548
+ const isDeferredPayment = params?.paymentExecution === 'deferred';
2549
+ const virtualPaymentSettlement = isDeferredPayment ? null : await this.buildVirtualSettlementSubmitData();
2550
+ const virtualSettlement = isDeferredPayment ? this.buildVirtualSettlementOrderData() : virtualPaymentSettlement;
2545
2551
  // this.store.order.persistTempOrder();
2546
- const inferredPaymentStatus = this.getSubmitPaymentStatus(params?.paymentStatus);
2552
+ const inferredPaymentStatus = isDeferredPayment ? undefined : this.getSubmitPaymentStatus(params?.paymentStatus);
2547
2553
  const smallTicketDataFlag = params?.smallTicketDataFlag ?? this.getDefaultCheckoutSmallTicketDataFlag();
2548
2554
  const virtualSettlementEnhancePayload = virtualSettlement ? (payload, ctx) => {
2549
2555
  const callerPayload = params?.enhancePayload ? params.enhancePayload(payload, ctx) : payload;
@@ -2564,7 +2570,20 @@ class BaseSalesImpl extends _BaseModule.BaseModule {
2564
2570
  currency_format: virtualSettlement.currencyFormat
2565
2571
  };
2566
2572
  } : params?.enhancePayload;
2567
- const enhancePayload = this.buildSubmitPayloadEnhancerWithOpenDataConfig(virtualSettlementEnhancePayload);
2573
+ const configuredEnhancePayload = this.buildSubmitPayloadEnhancerWithOpenDataConfig(virtualSettlementEnhancePayload);
2574
+ // Apply this last so neither a stale draft status nor a caller enhancer can
2575
+ // turn deferred submission into payment. The live wallet selection is untouched.
2576
+ const enhancePayload = isDeferredPayment ? (payload, ctx) => {
2577
+ const enhancedPayload = configuredEnhancePayload ? configuredEnhancePayload(payload, ctx) : payload;
2578
+ const {
2579
+ payment_status: _paymentStatus,
2580
+ ...orderPayload
2581
+ } = enhancedPayload;
2582
+ return {
2583
+ ...orderPayload,
2584
+ payments: []
2585
+ };
2586
+ } : configuredEnhancePayload;
2568
2587
  const submitParams = {
2569
2588
  cacheId: this.cacheId,
2570
2589
  platform: this.otherParams?.platform,
@@ -2572,8 +2591,10 @@ class BaseSalesImpl extends _BaseModule.BaseModule {
2572
2591
  channel: this.getSubmitOrderSalesChannel(),
2573
2592
  type: this.otherParams?.type,
2574
2593
  request_unique_idempotency_token: this.store.order.generateIdempotencyToken(),
2575
- ...(virtualSettlement ? {
2576
- payments: virtualSettlement.payments
2594
+ ...(isDeferredPayment ? {
2595
+ payments: []
2596
+ } : virtualPaymentSettlement ? {
2597
+ payments: virtualPaymentSettlement.payments
2577
2598
  } : params?.payments !== undefined ? {
2578
2599
  payments: params.payments
2579
2600
  } : {}),
@@ -2583,7 +2604,7 @@ class BaseSalesImpl extends _BaseModule.BaseModule {
2583
2604
  ...(smallTicketDataFlag !== undefined ? {
2584
2605
  smallTicketDataFlag
2585
2606
  } : {}),
2586
- ...(virtualSettlement ? {
2607
+ ...(isDeferredPayment || virtualSettlement ? {
2587
2608
  confirmPendingVoucherPayments: false
2588
2609
  } : params?.confirmPendingVoucherPayments !== undefined ? {
2589
2610
  confirmPendingVoucherPayments: params.confirmPendingVoucherPayments
@@ -3921,7 +3942,7 @@ class BaseSalesImpl extends _BaseModule.BaseModule {
3921
3942
  async addProductToOrder(product, booking, options) {
3922
3943
  try {
3923
3944
  if (!this.store.order) throw new Error('order 模块未初始化');
3924
- const products = await this.store.order.addProductToOrder(product, booking, options);
3945
+ const products = options === undefined ? await this.store.order.addProductToOrder(product, booking) : await this.store.order.addProductToOrder(product, booking, options);
3925
3946
  // // 需要叠加
3926
3947
  // if (this.hooks.onAddProductOverlay) {
3927
3948
  // const res = this.hooks.onAddProductOverlay(是否叠加, product)
@@ -4010,7 +4031,7 @@ class BaseSalesImpl extends _BaseModule.BaseModule {
4010
4031
  async removeProductsFromOrder(identities, options) {
4011
4032
  try {
4012
4033
  if (!this.store.order) throw new Error('order 模块未初始化');
4013
- return await this.store.order.removeProductsFromOrder(identities, options);
4034
+ return options === undefined ? await this.store.order.removeProductsFromOrder(identities) : await this.store.order.removeProductsFromOrder(identities, options);
4014
4035
  } catch (error) {
4015
4036
  throw error;
4016
4037
  }