@pisell/pisellos 0.10.54 → 0.10.56

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 (44) hide show
  1. package/dist/modules/BusinessAvailability/index.d.ts +30 -0
  2. package/dist/modules/BusinessAvailability/index.js +204 -0
  3. package/dist/modules/OpenData/types.d.ts +2 -0
  4. package/dist/modules/Order/index.d.ts +4 -1
  5. package/dist/modules/Order/index.js +315 -241
  6. package/dist/modules/Order/types.d.ts +8 -0
  7. package/dist/plugins/request.d.ts +4 -0
  8. package/dist/shared/receipt/loadCustomerInvoiceLayout.d.ts +22 -0
  9. package/dist/shared/receipt/loadCustomerInvoiceLayout.js +142 -0
  10. package/dist/solution/BaseSales/index.d.ts +4 -15
  11. package/dist/solution/BaseSales/index.js +131 -86
  12. package/dist/solution/BaseSales/types.d.ts +11 -0
  13. package/dist/solution/BookingByStep/index.d.ts +1 -1
  14. package/dist/solution/BookingTicket/index.d.ts +1 -1
  15. package/dist/solution/BookingTicket/index.js +41 -13
  16. package/dist/solution/OrderInvoicePreview/index.js +6 -6
  17. package/dist/solution/UnifiedBookingSales/businessAvailability.d.ts +27 -0
  18. package/dist/solution/UnifiedBookingSales/businessAvailability.js +292 -0
  19. package/dist/solution/UnifiedBookingSales/index.d.ts +11 -0
  20. package/dist/solution/UnifiedBookingSales/index.js +154 -75
  21. package/dist/solution/UnifiedBookingSales/types.d.ts +2 -0
  22. package/lib/model/strategy/adapter/promotion/index.js +1 -46
  23. package/lib/modules/BusinessAvailability/index.d.ts +30 -0
  24. package/lib/modules/BusinessAvailability/index.js +146 -0
  25. package/lib/modules/OpenData/types.d.ts +2 -0
  26. package/lib/modules/Order/index.d.ts +4 -1
  27. package/lib/modules/Order/index.js +62 -6
  28. package/lib/modules/Order/types.d.ts +8 -0
  29. package/lib/plugins/request.d.ts +4 -0
  30. package/lib/shared/receipt/loadCustomerInvoiceLayout.d.ts +22 -0
  31. package/lib/shared/receipt/loadCustomerInvoiceLayout.js +86 -0
  32. package/lib/solution/BaseSales/index.d.ts +4 -15
  33. package/lib/solution/BaseSales/index.js +45 -24
  34. package/lib/solution/BaseSales/types.d.ts +11 -0
  35. package/lib/solution/BookingByStep/index.d.ts +1 -1
  36. package/lib/solution/BookingTicket/index.d.ts +1 -1
  37. package/lib/solution/BookingTicket/index.js +37 -4
  38. package/lib/solution/OrderInvoicePreview/index.js +2 -4
  39. package/lib/solution/UnifiedBookingSales/businessAvailability.d.ts +27 -0
  40. package/lib/solution/UnifiedBookingSales/businessAvailability.js +138 -0
  41. package/lib/solution/UnifiedBookingSales/index.d.ts +11 -0
  42. package/lib/solution/UnifiedBookingSales/index.js +54 -0
  43. package/lib/solution/UnifiedBookingSales/types.d.ts +2 -0
  44. 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[]>;
@@ -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
  /**
@@ -56,6 +56,10 @@ export interface CacheProps {
56
56
  * 请求配置
57
57
  */
58
58
  export interface RequestOptions {
59
+ /** Customer-only, credential-free GET /open/document/{id} on picoding. */
60
+ publicInvoiceDocument?: boolean;
61
+ /** Route through the host's configured picoding environment. */
62
+ pisell2?: boolean;
59
63
  /** Let the caller handle read-only preview failures without host navigation/toasts. */
60
64
  manualErrorHandling?: boolean;
61
65
  skipErrorHandler?: boolean;
@@ -0,0 +1,22 @@
1
+ import type { RequestPlugin } from '../../plugins/request';
2
+ import { type InvoiceLayoutConfig } from '../../modules/OpenData';
3
+ export interface PublicInvoiceDocumentResponse {
4
+ status: boolean;
5
+ code: number;
6
+ message?: string;
7
+ data: {
8
+ id: number;
9
+ data?: {
10
+ template_settings?: unknown;
11
+ } | null;
12
+ } | null;
13
+ }
14
+ interface TemplateScope {
15
+ tenantId: string | number;
16
+ shopId: string | number;
17
+ identity: string;
18
+ businessCode: string;
19
+ channel: string;
20
+ }
21
+ export declare function loadCustomerInvoiceLayout(request: RequestPlugin, scope: TemplateScope): Promise<InvoiceLayoutConfig>;
22
+ export {};
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.loadCustomerInvoiceLayout = loadCustomerInvoiceLayout;
7
+ var _OpenData = require("../../modules/OpenData");
8
+ // Page-runtime caches: also retain DEFAULT on failure, without automatic retries.
9
+ const caches = new WeakMap();
10
+ const isRecord = value => !!value && typeof value === 'object' && !Array.isArray(value);
11
+ function documentId(value) {
12
+ if (typeof value !== 'string' && typeof value !== 'number') return null;
13
+ const text = String(value).trim();
14
+ if (!/^\d+$/.test(text)) return null;
15
+ const id = Number(text);
16
+ return Number.isSafeInteger(id) && id > 0 ? String(id) : null;
17
+ }
18
+ function loadCustomerInvoiceLayout(request, scope) {
19
+ let cache = caches.get(request);
20
+ if (!cache) {
21
+ cache = {
22
+ scopes: new Map(),
23
+ documents: new Map()
24
+ };
25
+ caches.set(request, cache);
26
+ }
27
+ const target = `${scope.businessCode}+${scope.channel}`;
28
+ const key = JSON.stringify([scope.tenantId, scope.shopId, scope.identity, scope.businessCode, scope.channel]);
29
+ const cached = cache.scopes.get(key);
30
+ if (cached) return cached;
31
+ let id = null;
32
+ const task = (async () => {
33
+ const response = await request.post(`/open/my-pisell/${encodeURIComponent(String(scope.shopId))}/setting`, {
34
+ scope: 'board',
35
+ target,
36
+ section_code: ['documents']
37
+ }, {
38
+ pisell2: true,
39
+ prefix: false,
40
+ headers: scope.tenantId ? {
41
+ 'Tenant-Id': String(scope.tenantId)
42
+ } : {},
43
+ manualErrorHandling: true,
44
+ skipErrorHandler: true,
45
+ timeout: 15000
46
+ });
47
+ if (response?.status !== true || response.code !== 200 || !isRecord(response.data)) throw new Error(response?.message || 'Invoice OpenData is unavailable');
48
+ const config = response.data;
49
+ const raw = config['documents.invoice_template'];
50
+ id = documentId(raw);
51
+ if (!id) {
52
+ // Preserve the existing non-numeric legacy template / invoice_edit contract.
53
+ const numericLike = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i;
54
+ return typeof raw === 'string' && raw.trim() && !numericLike.test(raw.trim()) ? (0, _OpenData.normalizeInvoiceLayoutFromTemplateSettings)((0, _OpenData.normalizeInvoiceTemplateSettings)(config)) : (0, _OpenData.getDefaultInvoiceLayoutConfig)();
55
+ }
56
+ const docKey = JSON.stringify([key, id]);
57
+ let pending = cache.documents.get(docKey);
58
+ if (!pending) {
59
+ const expectedId = id;
60
+ pending = (async () => {
61
+ const result = await request.get(`/open/document/${expectedId}`, {}, {
62
+ publicInvoiceDocument: true
63
+ });
64
+ if (result?.status !== true || result.code !== 200) throw new Error(result?.message || 'Invoice Document request failed');
65
+ const doc = result.data;
66
+ if (!doc || documentId(doc.id) !== expectedId) throw new Error('Invoice Document is unavailable or its ID does not match');
67
+ if (!isRecord(doc.data) || !isRecord(doc.data.template_settings)) throw new Error('Invoice Document has no template_settings');
68
+ const settings = (0, _OpenData.normalizeRawInvoiceTemplateSettings)(doc.data.template_settings);
69
+ if (settings.modules.length === 0) throw new Error('Invoice Document has no usable template modules');
70
+ return (0, _OpenData.normalizeInvoiceLayoutFromTemplateSettings)(settings);
71
+ })().catch(error => fallback(error, target, expectedId));
72
+ cache.documents.set(docKey, pending);
73
+ }
74
+ return pending;
75
+ })().catch(error => fallback(error, target, id));
76
+ cache.scopes.set(key, task);
77
+ return task;
78
+ }
79
+ function fallback(error, target, id) {
80
+ console.warn('[OrderInvoicePreview] Template unavailable; using DEFAULT', {
81
+ target,
82
+ documentId: id,
83
+ message: error instanceof Error ? error.message : 'Template request failed'
84
+ });
85
+ return (0, _OpenData.getDefaultInvoiceLayoutConfig)();
86
+ }
@@ -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;