@pisell/pisellos 0.10.53 → 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 (68) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +3 -1
  3. package/dist/model/strategy/adapter/promotion/index.js +9 -0
  4. package/dist/modules/BusinessAvailability/index.d.ts +30 -0
  5. package/dist/modules/BusinessAvailability/index.js +204 -0
  6. package/dist/modules/OpenData/index.js +1 -1
  7. package/dist/modules/OpenData/types.d.ts +2 -0
  8. package/dist/modules/Order/index.d.ts +5 -2
  9. package/dist/modules/Order/index.js +320 -247
  10. package/dist/modules/Order/types.d.ts +8 -0
  11. package/dist/modules/Payment/index.js +9 -10
  12. package/dist/plugins/request.d.ts +3 -0
  13. package/dist/server/utils/small-ticket.d.ts +1 -85
  14. package/dist/server/utils/small-ticket.js +2 -2576
  15. package/dist/shared/payment/loadPaymentMethods.d.ts +9 -0
  16. package/dist/shared/payment/loadPaymentMethods.js +66 -0
  17. package/dist/shared/receipt/small-ticket.d.ts +85 -0
  18. package/dist/shared/receipt/small-ticket.js +2576 -0
  19. package/dist/shared/sales/loadSalesOrderSnapshot.d.ts +9 -0
  20. package/dist/shared/sales/loadSalesOrderSnapshot.js +21 -0
  21. package/dist/solution/BaseSales/index.d.ts +4 -15
  22. package/dist/solution/BaseSales/index.js +131 -86
  23. package/dist/solution/BaseSales/types.d.ts +11 -0
  24. package/dist/solution/BookingByStep/index.d.ts +1 -1
  25. package/dist/solution/BookingTicket/index.js +41 -13
  26. package/dist/solution/OrderInvoicePreview/index.d.ts +56 -0
  27. package/dist/solution/OrderInvoicePreview/index.js +269 -0
  28. package/dist/solution/UnifiedBookingSales/businessAvailability.d.ts +27 -0
  29. package/dist/solution/UnifiedBookingSales/businessAvailability.js +292 -0
  30. package/dist/solution/UnifiedBookingSales/index.d.ts +11 -0
  31. package/dist/solution/UnifiedBookingSales/index.js +154 -75
  32. package/dist/solution/UnifiedBookingSales/types.d.ts +2 -0
  33. package/dist/solution/index.d.ts +1 -0
  34. package/dist/solution/index.js +2 -1
  35. package/lib/index.d.ts +2 -0
  36. package/lib/index.js +21 -1
  37. package/lib/modules/BusinessAvailability/index.d.ts +30 -0
  38. package/lib/modules/BusinessAvailability/index.js +146 -0
  39. package/lib/modules/OpenData/index.js +1 -1
  40. package/lib/modules/OpenData/types.d.ts +2 -0
  41. package/lib/modules/Order/index.d.ts +5 -2
  42. package/lib/modules/Order/index.js +67 -13
  43. package/lib/modules/Order/types.d.ts +8 -0
  44. package/lib/modules/Payment/index.js +3 -4
  45. package/lib/plugins/request.d.ts +3 -0
  46. package/lib/server/utils/small-ticket.d.ts +1 -85
  47. package/lib/server/utils/small-ticket.js +9 -2335
  48. package/lib/shared/payment/loadPaymentMethods.d.ts +9 -0
  49. package/lib/shared/payment/loadPaymentMethods.js +40 -0
  50. package/lib/shared/receipt/small-ticket.d.ts +85 -0
  51. package/lib/shared/receipt/small-ticket.js +2342 -0
  52. package/lib/shared/sales/loadSalesOrderSnapshot.d.ts +9 -0
  53. package/lib/shared/sales/loadSalesOrderSnapshot.js +22 -0
  54. package/lib/solution/BaseSales/index.d.ts +4 -15
  55. package/lib/solution/BaseSales/index.js +45 -24
  56. package/lib/solution/BaseSales/types.d.ts +11 -0
  57. package/lib/solution/BookingByStep/index.d.ts +1 -1
  58. package/lib/solution/BookingTicket/index.js +37 -4
  59. package/lib/solution/OrderInvoicePreview/index.d.ts +56 -0
  60. package/lib/solution/OrderInvoicePreview/index.js +194 -0
  61. package/lib/solution/UnifiedBookingSales/businessAvailability.d.ts +27 -0
  62. package/lib/solution/UnifiedBookingSales/businessAvailability.js +138 -0
  63. package/lib/solution/UnifiedBookingSales/index.d.ts +11 -0
  64. package/lib/solution/UnifiedBookingSales/index.js +54 -0
  65. package/lib/solution/UnifiedBookingSales/types.d.ts +2 -0
  66. package/lib/solution/index.d.ts +1 -0
  67. package/lib/solution/index.js +11 -0
  68. 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>;
@@ -9,3 +9,4 @@ export * from './ScanOrder';
9
9
  export * from './BaseSales';
10
10
  export * from './VenueBooking';
11
11
  export * from './UnifiedBookingSales';
12
+ export * from './OrderInvoicePreview';
@@ -8,4 +8,5 @@ export * from "./Sales";
8
8
  export * from "./ScanOrder";
9
9
  export * from "./BaseSales";
10
10
  export * from "./VenueBooking";
11
- export * from "./UnifiedBookingSales";
11
+ export * from "./UnifiedBookingSales";
12
+ export * from "./OrderInvoicePreview";
package/lib/index.d.ts CHANGED
@@ -10,3 +10,5 @@ export * from './model';
10
10
  export { PisellOSCore } from './core';
11
11
  import PisellOSCore from './core';
12
12
  export default PisellOSCore;
13
+ export * from './shared/receipt/small-ticket';
14
+ export { loadSalesOrderSnapshot } from './shared/sales/loadSalesOrderSnapshot';
package/lib/index.js CHANGED
@@ -4,7 +4,8 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  var _exportNames = {
7
- PisellOSCore: true
7
+ PisellOSCore: true,
8
+ loadSalesOrderSnapshot: true
8
9
  };
9
10
  Object.defineProperty(exports, "PisellOSCore", {
10
11
  enumerable: true,
@@ -13,6 +14,12 @@ Object.defineProperty(exports, "PisellOSCore", {
13
14
  }
14
15
  });
15
16
  exports.default = void 0;
17
+ Object.defineProperty(exports, "loadSalesOrderSnapshot", {
18
+ enumerable: true,
19
+ get: function () {
20
+ return _loadSalesOrderSnapshot.loadSalesOrderSnapshot;
21
+ }
22
+ });
16
23
  var _types = require("./types");
17
24
  Object.keys(_types).forEach(function (key) {
18
25
  if (key === "default" || key === "__esModule") return;
@@ -74,6 +81,19 @@ Object.keys(_model).forEach(function (key) {
74
81
  });
75
82
  });
76
83
  var _core = _interopRequireWildcard(require("./core"));
84
+ var _smallTicket = require("./shared/receipt/small-ticket");
85
+ Object.keys(_smallTicket).forEach(function (key) {
86
+ if (key === "default" || key === "__esModule") return;
87
+ if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
88
+ if (key in exports && exports[key] === _smallTicket[key]) return;
89
+ Object.defineProperty(exports, key, {
90
+ enumerable: true,
91
+ get: function () {
92
+ return _smallTicket[key];
93
+ }
94
+ });
95
+ });
96
+ var _loadSalesOrderSnapshot = require("./shared/sales/loadSalesOrderSnapshot");
77
97
  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); }
78
98
  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; }
79
99
  /**
@@ -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
+ }
@@ -153,7 +153,7 @@ class OpenDataModule extends _BaseModule.BaseModule {
153
153
  this.fatherModule = this.otherParams.fatherModule;
154
154
  }
155
155
  shouldSkipInvoiceTemplateDocument() {
156
- return String(this.otherParams?.platform || '').trim().toLowerCase() === 'h5';
156
+ return this.otherParams.invoiceDocument !== true && String(this.otherParams?.platform || '').trim().toLowerCase() === 'h5';
157
157
  }
158
158
  async fetchOpenData(params) {
159
159
  const fetchRevision = ++this.fetchRevision;
@@ -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;
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "isFulfillmentTimingBooking", {
10
10
  return _pickupTiming.isFulfillmentTimingBooking;
11
11
  }
12
12
  });
13
+ var _loadSalesOrderSnapshot = require("../../shared/sales/loadSalesOrderSnapshot");
13
14
  var _BaseModule = require("../BaseModule");
14
15
  var _types = require("./types");
15
16
  var _utils = require("./utils");
@@ -439,6 +440,29 @@ class OrderModule extends _BaseModule.BaseModule {
439
440
  this.otherParams = otherParams;
440
441
  }
441
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
+
442
466
  /** PC/H5 属于在线店铺;其它或缺失平台统一沿用 POS 行为。 */
443
467
  isOnlineStorePlatform() {
444
468
  const platform = String(this.otherParams?.platform || '').trim().toLowerCase();
@@ -667,6 +691,7 @@ class OrderModule extends _BaseModule.BaseModule {
667
691
  });
668
692
  }
669
693
  async scanCode(code, customerId) {
694
+ const beforeCommit = this.createCartIntentCommitGuard('scan_promotion');
670
695
  if (this.isActiveVirtualSettlementOrder(this.store.tempOrder)) {
671
696
  return {
672
697
  type: 'clientCalc',
@@ -714,6 +739,7 @@ class OrderModule extends _BaseModule.BaseModule {
714
739
  }));
715
740
  const currentSelected = this.getDiscountList().filter(n => n.isSelected);
716
741
  if (currentSelected.length && currentSelected.some(n => withScanList.some(m => m.id === n.id))) {
742
+ beforeCommit();
717
743
  await this.syncScannedDiscountsToOriginalDiscountList(this.getDiscountList());
718
744
  return {
719
745
  type: 'clientCalc',
@@ -747,6 +773,7 @@ class OrderModule extends _BaseModule.BaseModule {
747
773
  const normalizedNewDiscountList = newDiscountList ? markScannedDiscounts(newDiscountList, withScanList) : undefined;
748
774
  let resolvedDiscountList = normalizedNewDiscountList || this.getDiscountList();
749
775
  if (isAvailable && normalizedNewDiscountList) {
776
+ beforeCommit();
750
777
  await this.store.discount?.setDiscountList(normalizedNewDiscountList);
751
778
  // 实际写回商品时沿用扫码试算的优先级,避免旧商品行的选卡覆盖本次扫码。
752
779
  this.applyDiscount({
@@ -755,6 +782,7 @@ class OrderModule extends _BaseModule.BaseModule {
755
782
  await this.syncScannedDiscountsToOriginalDiscountList(this.getDiscountList());
756
783
  resolvedDiscountList = this.getDiscountList();
757
784
  } else if (shouldRetainForLater) {
785
+ beforeCommit();
758
786
  // 只合并本次识别的卡券;不把试算产生的其他选券状态写回订单。
759
787
  const retainedDiscounts = new Map(this.getDiscountList().map(discount => [String(discount.id), discount]));
760
788
  evaluatedScannedDiscounts.forEach(discount => {
@@ -3461,7 +3489,6 @@ class OrderModule extends _BaseModule.BaseModule {
3461
3489
  }) : null;
3462
3490
  const productToAdd = linked?.product || product;
3463
3491
  const disableMerge = options?.disableMerge === true;
3464
- this.assertAndLockProductSettlement(tempOrder, productToAdd);
3465
3492
  const incomingFingerprint = disableMerge ? '' : (0, _utils3.buildProductLineFingerprint)((0, _utils.getProductSkuOptions)(productToAdd), productToAdd.product_bundle);
3466
3493
  const normalizedIncoming = this.normalizeVirtualSettlementProductIfEnabled((0, _utils.mergeOrderProductDisplayFields)(productToAdd, (0, _utils3.normalizeOrderProduct)(productToAdd)));
3467
3494
  if (disableMerge) {
@@ -3522,6 +3549,8 @@ class OrderModule extends _BaseModule.BaseModule {
3522
3549
  const matchedProduct = matchedIndex === -1 ? null : tempOrder.products[matchedIndex];
3523
3550
  const existedFingerprint = matchedProduct ? (0, _utils3.buildProductLineFingerprint)((0, _utils.getProductSkuOptions)(matchedProduct), matchedProduct.product_bundle) : '';
3524
3551
  const commitProductLine = shouldMerge => {
3552
+ options?.beforeCommit?.();
3553
+ this.assertAndLockProductSettlement(tempOrder, productToAdd);
3525
3554
  let committedProduct;
3526
3555
  if (matchedIndex === -1 || !matchedProduct || !shouldMerge) {
3527
3556
  this.captureProductRuntime(tempOrder, productToAdd, normalizedIncoming);
@@ -3674,6 +3703,7 @@ class OrderModule extends _BaseModule.BaseModule {
3674
3703
  */
3675
3704
  async addProductToOrder(product, booking, options) {
3676
3705
  const tempOrder = this.ensureTempOrder();
3706
+ const beforeCommit = this.createCartIntentCommitGuard('add_product', tempOrder);
3677
3707
  const salesNoteDrafts = (options?.salesNotes || []).map(draft => ({
3678
3708
  note_type: String(draft.note_type || '').trim(),
3679
3709
  text: {
@@ -3744,7 +3774,8 @@ class OrderModule extends _BaseModule.BaseModule {
3744
3774
  }
3745
3775
  const committedProduct = await this.appendProductLineToTempOrder(tempOrder, splitLine.product, splitLine.booking, {
3746
3776
  insertAtIndex,
3747
- disableMerge: options?.disableMerge
3777
+ disableMerge: options?.disableMerge,
3778
+ beforeCommit
3748
3779
  });
3749
3780
  attachSalesNoteDrafts(committedProduct);
3750
3781
  if (insertAtIndex !== undefined) insertAtIndex += 1;
@@ -3752,14 +3783,16 @@ class OrderModule extends _BaseModule.BaseModule {
3752
3783
  } else {
3753
3784
  const committedProduct = await this.appendProductLineToTempOrder(tempOrder, product, (0, _lodashEs.cloneDeep)(booking), {
3754
3785
  insertAtIndex,
3755
- disableMerge: options?.disableMerge
3786
+ disableMerge: options?.disableMerge,
3787
+ beforeCommit
3756
3788
  });
3757
3789
  attachSalesNoteDrafts(committedProduct);
3758
3790
  }
3759
3791
  } else {
3760
3792
  const committedProduct = await this.appendProductLineToTempOrder(tempOrder, product, undefined, {
3761
3793
  insertAtIndex,
3762
- disableMerge: options?.disableMerge
3794
+ disableMerge: options?.disableMerge,
3795
+ beforeCommit
3763
3796
  });
3764
3797
  attachSalesNoteDrafts(committedProduct);
3765
3798
  }
@@ -3787,6 +3820,7 @@ class OrderModule extends _BaseModule.BaseModule {
3787
3820
  async addProductsToOrder(items, options) {
3788
3821
  const tempOrder = this.ensureTempOrder();
3789
3822
  if (!Array.isArray(items) || items.length === 0) return tempOrder.products;
3823
+ const beforeCommit = this.createCartIntentCommitGuard('add_product', tempOrder);
3790
3824
  for (const item of items || []) {
3791
3825
  const {
3792
3826
  product,
@@ -3799,13 +3833,17 @@ class OrderModule extends _BaseModule.BaseModule {
3799
3833
  if (splitCount > 1) {
3800
3834
  for (let i = 0; i < splitCount; i += 1) {
3801
3835
  const splitLine = this.createSplitBookingLine(productRecord, booking, i, splitCount);
3802
- const appendResult = this.appendProductLineToTempOrder(tempOrder, splitLine.product, splitLine.booking);
3836
+ const appendResult = this.appendProductLineToTempOrder(tempOrder, splitLine.product, splitLine.booking, {
3837
+ beforeCommit
3838
+ });
3803
3839
  if (appendResult) await appendResult;
3804
3840
  }
3805
3841
  continue;
3806
3842
  }
3807
3843
  }
3808
- const appendResult = this.appendProductLineToTempOrder(tempOrder, product, booking);
3844
+ const appendResult = this.appendProductLineToTempOrder(tempOrder, product, booking, {
3845
+ beforeCommit
3846
+ });
3809
3847
  if (appendResult) await appendResult;
3810
3848
  }
3811
3849
  await this.finalizeProductOrderMutation(tempOrder, options);
@@ -3890,6 +3928,16 @@ class OrderModule extends _BaseModule.BaseModule {
3890
3928
  };
3891
3929
  });
3892
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
+ }
3893
3941
  applyOrderProductUpdateToTempOrder(tempOrder, params) {
3894
3942
  const {
3895
3943
  product_id,
@@ -4088,6 +4136,9 @@ class OrderModule extends _BaseModule.BaseModule {
4088
4136
  }
4089
4137
  async updateOrderProduct(params) {
4090
4138
  const tempOrder = this.ensureTempOrder();
4139
+ if (this.isRestrictedCartProductUpdate(tempOrder, params)) {
4140
+ this.createCartIntentCommitGuard('update_product', tempOrder)();
4141
+ }
4091
4142
  const updatedProductUid = this.applyOrderProductUpdateToTempOrder(tempOrder, params);
4092
4143
  const promotionPayload = await this.evaluatePromotion();
4093
4144
  this.applyDiscount({
@@ -4109,6 +4160,9 @@ class OrderModule extends _BaseModule.BaseModule {
4109
4160
  if (!paramsList.length) {
4110
4161
  return tempOrder.products;
4111
4162
  }
4163
+ if (paramsList.some(params => this.isRestrictedCartProductUpdate(tempOrder, params))) {
4164
+ this.createCartIntentCommitGuard('update_product', tempOrder)();
4165
+ }
4112
4166
  const reapplyBookingDiscountProductUids = paramsList.reduce((acc, params) => {
4113
4167
  const updatedProductUid = this.applyOrderProductUpdateToTempOrder(tempOrder, params);
4114
4168
  // 批量改预约时间时,延迟到最后一次折扣计算里统一恢复已选预约折扣卡。
@@ -4191,6 +4245,9 @@ class OrderModule extends _BaseModule.BaseModule {
4191
4245
  throw new _virtualSettlement.VirtualCurrencyBalanceError(balance.toFixed(2), requiredAmount.toFixed(2));
4192
4246
  }
4193
4247
  }
4248
+ if (normalizedProduct.num > targetProduct.num) {
4249
+ this.createCartIntentCommitGuard('increase_quantity', tempOrder)();
4250
+ }
4194
4251
  tempOrder.products[productIndex] = normalizedProduct;
4195
4252
  this.prepareVirtualSettlementTempOrder(tempOrder);
4196
4253
  const isVirtualSettlement = this.isActiveVirtualSettlementOrder(tempOrder);
@@ -5017,13 +5074,10 @@ class OrderModule extends _BaseModule.BaseModule {
5017
5074
  if (lookup.startsWith('LOCAL_')) {
5018
5075
  params.forceRemote = false;
5019
5076
  }
5020
- const res = await this.request.get(`/order/sales/${encodeURIComponent(lookup)}`, {
5021
- with: ['products', 'bookings', 'payments', 'customer', 'summary', 'contacts_info', 'order_meta_list', 'refunds', 'notes'],
5022
- ...(params.forceRemote ? {
5023
- forceRemote: true
5024
- } : {})
5025
- }, {
5026
- osServer: true
5077
+ const res = await (0, _loadSalesOrderSnapshot.loadSalesOrderSnapshot)(this.request, {
5078
+ lookup,
5079
+ transport: 'os-server',
5080
+ forceRemote: params.forceRemote
5027
5081
  });
5028
5082
  if (res.code === 200) {
5029
5083
  this.store.lastOrderInfo = res.data;
@@ -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
  /**
@@ -9,6 +9,7 @@ var _exportNames = {
9
9
  };
10
10
  exports.PaymentModule = void 0;
11
11
  exports.generateRequestUniqueId = generateRequestUniqueId;
12
+ var _loadPaymentMethods = require("../../shared/payment/loadPaymentMethods");
12
13
  var _BaseModule = require("../BaseModule");
13
14
  var _types = require("./types");
14
15
  Object.keys(_types).forEach(function (key) {
@@ -286,12 +287,10 @@ class PaymentModule extends _BaseModule.BaseModule {
286
287
  async getPayMethodListAsync() {
287
288
  this.logInfo('Starting getPayMethodListAsync');
288
289
  try {
289
- const response = await this.request.get('/pay/custom-payment/all', {
290
+ const payMethods = await (0, _loadPaymentMethods.loadPaymentMethods)(this.request, {
291
+ transport: 'os-server',
290
292
  filterPaymentMethods: true
291
- }, {
292
- osServer: true
293
293
  });
294
- const payMethods = response?.data || response || [];
295
294
  this.logInfo('getPayMethodListAsync completed successfully', {
296
295
  payMethods
297
296
  });
@@ -56,6 +56,9 @@ export interface CacheProps {
56
56
  * 请求配置
57
57
  */
58
58
  export interface RequestOptions {
59
+ /** Let the caller handle read-only preview failures without host navigation/toasts. */
60
+ manualErrorHandling?: boolean;
61
+ skipErrorHandler?: boolean;
59
62
  url?: string;
60
63
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
61
64
  headers?: Record<string, string>;
@@ -1,85 +1 @@
1
- export interface SmallTicketShopInfo {
2
- name?: string;
3
- phone?: string;
4
- address?: string;
5
- address_street?: string;
6
- address_detail?: string;
7
- city?: string;
8
- province?: string;
9
- country?: string;
10
- zip?: string;
11
- tax_number?: string;
12
- tax_title?: string;
13
- is_price_include_tax?: 0 | 1 | boolean;
14
- currency_symbol?: string;
15
- currency_code?: string;
16
- primary_locale?: string;
17
- timezone?: string;
18
- [key: string]: any;
19
- }
20
- export interface BuildSmallTicketDataParams {
21
- order: Record<string, any>;
22
- shopInfo?: SmallTicketShopInfo | null;
23
- locales?: string[];
24
- productMap?: Record<string | number, Record<string, any>>;
25
- }
26
- export interface SmallTicketItem {
27
- item: string;
28
- value: string;
29
- size?: number;
30
- align?: string;
31
- product_uid?: string | number;
32
- booking_uid?: string | number;
33
- }
34
- export interface SmallTicketProduct {
35
- product_title: string;
36
- product_quantity: number;
37
- selling_price: string;
38
- total_selling_price: string;
39
- original_price: string;
40
- base_price?: string;
41
- total_original_price: string;
42
- options_total_price?: string;
43
- package_items_total_price?: string;
44
- extension_list: any[];
45
- product_uid?: string | number;
46
- booking_uid?: string | number;
47
- product_note?: string;
48
- allergy?: string;
49
- options?: Array<{
50
- name: string;
51
- num?: number;
52
- price?: string;
53
- total_price: string;
54
- value?: string;
55
- }>;
56
- variant?: string;
57
- combinations?: any[];
58
- }
59
- export type SmallTicketAppointment = SmallTicketItem[];
60
- export interface SmallTicketLocaleData {
61
- base_data: Record<string, any>;
62
- products: SmallTicketProduct[];
63
- fees: {
64
- fees: SmallTicketItem[];
65
- countFee: Record<string, any>;
66
- };
67
- customer: SmallTicketItem[];
68
- appointment: SmallTicketAppointment;
69
- delivery?: SmallTicketItem[];
70
- resources: Array<{
71
- id: string | number;
72
- code: string;
73
- title: string;
74
- form_title: string;
75
- }>;
76
- payment_data: SmallTicketItem[][];
77
- wallets: SmallTicketItem[][];
78
- tickets: any[];
79
- source_shop: any[];
80
- refund_data: SmallTicketItem[][];
81
- expend_data: any[];
82
- }
83
- export type SmallTicketData = Record<string, SmallTicketLocaleData>;
84
- export declare function buildSmallTicketData(params: BuildSmallTicketDataParams): SmallTicketData;
85
- export declare function hasSmallTicketData(value: any): boolean;
1
+ export * from '../../shared/receipt/small-ticket';