@pisell/pisellos 0.10.41 → 0.10.43

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 (56) hide show
  1. package/dist/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +58 -19
  2. package/dist/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +24 -12
  3. package/dist/modules/Discount/entitlementPass.d.ts +34 -0
  4. package/dist/modules/Discount/entitlementPass.js +147 -0
  5. package/dist/modules/Discount/index.d.ts +2 -0
  6. package/dist/modules/Discount/index.js +39 -13
  7. package/dist/modules/Discount/types.d.ts +26 -5
  8. package/dist/modules/Order/index.d.ts +5 -0
  9. package/dist/modules/Order/index.js +283 -118
  10. package/dist/modules/Order/utils/orderCollectionIdentity.js +16 -12
  11. package/dist/modules/Order/utils.js +17 -1
  12. package/dist/modules/ResourcePlanner/planner.d.ts +2 -2
  13. package/dist/modules/ResourcePlanner/planner.js +163 -46
  14. package/dist/modules/ResourcePlanner/types.d.ts +17 -0
  15. package/dist/modules/Rules/index.js +312 -135
  16. package/dist/solution/BaseSales/index.d.ts +2 -0
  17. package/dist/solution/BaseSales/index.js +9 -1
  18. package/dist/solution/BaseSales/utils/cartPromotion.js +141 -126
  19. package/dist/solution/BookingTicket/types.d.ts +5 -1
  20. package/dist/solution/BookingTicket/utils/scan/applyGlobalScan.js +171 -96
  21. package/dist/solution/BookingTicket/utils/scan/cloudSearch.js +3 -1
  22. package/dist/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +2 -0
  23. package/dist/solution/BookingTicket/utils/scan/formatGlobalScan.js +12 -3
  24. package/dist/solution/ShopDiscount/index.d.ts +1 -0
  25. package/dist/solution/ShopDiscount/index.js +41 -21
  26. package/dist/solution/ShopDiscount/utils.js +3 -2
  27. package/dist/solution/UnifiedBookingSales/index.js +94 -21
  28. package/lib/model/strategy/adapter/promotion/index.js +1 -46
  29. package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +35 -6
  30. package/lib/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +13 -6
  31. package/lib/modules/Discount/entitlementPass.d.ts +34 -0
  32. package/lib/modules/Discount/entitlementPass.js +141 -0
  33. package/lib/modules/Discount/index.d.ts +2 -0
  34. package/lib/modules/Discount/index.js +25 -9
  35. package/lib/modules/Discount/types.d.ts +26 -5
  36. package/lib/modules/Order/index.d.ts +5 -0
  37. package/lib/modules/Order/index.js +172 -28
  38. package/lib/modules/Order/utils/orderCollectionIdentity.js +14 -11
  39. package/lib/modules/Order/utils.js +19 -1
  40. package/lib/modules/ResourcePlanner/planner.d.ts +2 -2
  41. package/lib/modules/ResourcePlanner/planner.js +137 -35
  42. package/lib/modules/ResourcePlanner/types.d.ts +17 -0
  43. package/lib/modules/Rules/index.js +232 -97
  44. package/lib/solution/BaseSales/index.d.ts +2 -0
  45. package/lib/solution/BaseSales/index.js +7 -1
  46. package/lib/solution/BaseSales/utils/cartPromotion.js +12 -4
  47. package/lib/solution/BookingTicket/types.d.ts +5 -1
  48. package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.js +65 -18
  49. package/lib/solution/BookingTicket/utils/scan/cloudSearch.js +3 -1
  50. package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +2 -0
  51. package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.js +5 -1
  52. package/lib/solution/ShopDiscount/index.d.ts +1 -0
  53. package/lib/solution/ShopDiscount/index.js +26 -6
  54. package/lib/solution/ShopDiscount/utils.js +3 -2
  55. package/lib/solution/UnifiedBookingSales/index.js +86 -22
  56. package/package.json +1 -1
@@ -236,6 +236,8 @@ export declare class BaseSalesImpl extends BaseModule implements Module {
236
236
  updateTempOrderNote(note: string, sync: true): Promise<string>;
237
237
  updateTempOrderNote(note: string, sync: boolean): string | Promise<string>;
238
238
  updateRemoteOrderNote(orderId: number | string, note: string): void;
239
+ /** 存储用户自定义取餐标识到当前 tempOrder.buzzer。 */
240
+ setPickupRef(buzzer: string): string;
239
241
  updateTempOrderShopDiscount(amount: string | number): string;
240
242
  updateTempOrderDepositAmount(amount: string | number): OrderSummary;
241
243
  updateTempOrderContactsInfo(contactsInfo: Record<string, any> | null): Record<string, any> | null;
@@ -1531,6 +1531,12 @@ class BaseSalesImpl extends _BaseModule.BaseModule {
1531
1531
  if (!this.store.order) throw new Error('order 模块未初始化');
1532
1532
  this.store.order.syncTempOrderNoteToRemote(orderId, note);
1533
1533
  }
1534
+
1535
+ /** 存储用户自定义取餐标识到当前 tempOrder.buzzer。 */
1536
+ setPickupRef(buzzer) {
1537
+ if (!this.store.order) throw new Error('order 模块未初始化');
1538
+ return this.store.order.updateTempOrderBuzzer(buzzer);
1539
+ }
1534
1540
  updateTempOrderShopDiscount(amount) {
1535
1541
  if (!this.store.order) throw new Error('order 模块未初始化');
1536
1542
  return this.store.order.updateTempOrderShopDiscount(amount);
@@ -1680,7 +1686,7 @@ class BaseSalesImpl extends _BaseModule.BaseModule {
1680
1686
  if (!this.isPersistedOrderProduct(item)) return;
1681
1687
  (item.discount_list || []).forEach(discount => {
1682
1688
  const discountId = discount.discount?.resource_id || discount.id;
1683
- if (!discountId || !['good_pass', 'discount_card'].includes(discount.type)) return;
1689
+ if (!discountId || !['entitlement_pass', 'good_pass', 'discount_card'].includes(discount.type)) return;
1684
1690
  const quantity = item.quantity || item.num || item.product_quantity || 1;
1685
1691
  const savedAmount = new _decimal.default(discount.amount || 0).times(quantity).plus(discount?.metadata?.product_discount_difference || 0);
1686
1692
  const index = historyDiscountList.findIndex(n => n.id === discountId);
@@ -17,6 +17,7 @@ exports.resolveStrategyEligibleProducts = resolveStrategyEligibleProducts;
17
17
  exports.resolveStrategyRequiredQuantity = resolveStrategyRequiredQuantity;
18
18
  var _decimal = _interopRequireDefault(require("decimal.js"));
19
19
  var _lodashEs = require("lodash-es");
20
+ var _entitlementPass = require("../../../modules/Discount/entitlementPass");
20
21
  var _utils = require("../../../modules/Order/utils");
21
22
  var _utils2 = require("../../ScanOrder/utils");
22
23
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
@@ -395,11 +396,16 @@ function getProductLineNoteSignature(product) {
395
396
  const uid = getOrderProductUid(product);
396
397
  return uid ? `note:${uid}` : `note:${normalizedNote}`;
397
398
  }
398
- function hasGoodPassDiscount(product) {
399
+ function getFullCoveragePassDiscountType(product) {
399
400
  const productBundle = Array.isArray(product?.product_bundle) ? product.product_bundle : [];
400
401
  const extendedBundle = Array.isArray(product?._extend?.other?.bundle) ? product._extend.other.bundle : [];
401
402
  const discountLists = [product?.discount_list, ...productBundle.map(item => item?.discount_list), ...extendedBundle.map(item => item?.discount_list)];
402
- return discountLists.some(discountList => Array.isArray(discountList) && discountList.some(item => item?.type === 'good_pass' || item?.tag === 'good_pass'));
403
+ for (const discountList of discountLists) {
404
+ if (!Array.isArray(discountList)) continue;
405
+ const pass = discountList.find(item => (0, _entitlementPass.isFullCoveragePass)(item));
406
+ if (pass) return String(pass.type || pass.tag);
407
+ }
408
+ return null;
403
409
  }
404
410
 
405
411
  /**
@@ -412,8 +418,10 @@ function getPromotionLineMergeBoundary(product) {
412
418
  if (product?.product_id === undefined || product?.product_id === null) {
413
419
  return `custom:${getOrderProductUid(product)}`;
414
420
  }
415
- if (hasGoodPassDiscount(product)) {
416
- return `good-pass:${getOrderProductUid(product)}`;
421
+ const fullCoveragePassType = getFullCoveragePassDiscountType(product);
422
+ if (fullCoveragePassType) {
423
+ const boundary = fullCoveragePassType === 'good_pass' ? 'good-pass' : 'entitlement-pass';
424
+ return `${boundary}:${getOrderProductUid(product)}`;
417
425
  }
418
426
  if (Number(product?.product_sku?.open_sold_weight || 0) === 1) {
419
427
  return `weighing:${getOrderProductUid(product)}`;
@@ -230,12 +230,16 @@ export interface GlobalScanHostBridge {
230
230
  /**
231
231
  * walk-in 扫 walletPass:直接核销优惠码(对齐 SaleDetail Voucher.handleApplyCode / cart.scanCode)。
232
232
  */
233
- applyPromotionCode?: (code: string, customerId?: number) => Promise<{
233
+ applyPromotionCode?: (code: string, customerId?: number, options?: {
234
+ suppressFailureNotification?: boolean;
235
+ }) => Promise<{
234
236
  isAvailable?: boolean;
235
237
  isAccepted?: boolean;
236
238
  type?: string;
237
239
  unavailableReason?: string;
238
240
  }>;
241
+ /** 当前订单已有其他客户时,由宿主复用客户组件完成确认及切换。 */
242
+ requestCustomerSwitch?: (customer: ICustomer) => Promise<boolean | null>;
239
243
  onNotify?: (type: 'success' | 'fail' | 'info', messageKey?: string) => void;
240
244
  refresh?: () => void;
241
245
  }
@@ -106,16 +106,12 @@ async function applyGlobalScan(host, formatted, bridge) {
106
106
  data,
107
107
  scanCode,
108
108
  customerId: formattedCustomerId,
109
- isCrossShopWallet
109
+ isCrossShopWallet,
110
+ isWalletPassUnavailableAtStore
110
111
  } = formatted;
111
112
  if (searchType === 'wallet') {
112
113
  const currentCustomer = host.getOrderCustomerSnapshot();
113
- if (getRealCustomerId(currentCustomer) !== undefined) {
114
- return {
115
- status: 'success',
116
- kind: 'customer'
117
- };
118
- }
114
+ const currentCustomerId = getRealCustomerId(currentCustomer);
119
115
  let customer = data;
120
116
  if (isCrossShopWallet) {
121
117
  const sourceCustomerId = getRealCustomerId({
@@ -152,6 +148,36 @@ async function applyGlobalScan(host, formatted, bridge) {
152
148
  ...customer,
153
149
  id: customerId
154
150
  } : customer;
151
+ if (currentCustomerId === customerId) {
152
+ return {
153
+ status: 'success',
154
+ kind: 'customer'
155
+ };
156
+ }
157
+ if (currentCustomerId !== undefined) {
158
+ // 未接入新桥接的宿主保持原行为:已有客户时不直接覆盖。
159
+ if (!bridge?.requestCustomerSwitch) {
160
+ return {
161
+ status: 'success',
162
+ kind: 'customer'
163
+ };
164
+ }
165
+ try {
166
+ const switched = await bridge.requestCustomerSwitch(orderCustomer);
167
+ return switched ? {
168
+ status: 'success',
169
+ kind: 'customer'
170
+ } : {
171
+ status: 'cancelled'
172
+ };
173
+ } catch (error) {
174
+ bridge.onNotify?.('fail', getErrorMessage(error) || 'pisell2.text.scan-global-failed');
175
+ return {
176
+ status: 'failed',
177
+ reason: 'invalid_data'
178
+ };
179
+ }
180
+ }
155
181
  host.setOrderCustomer(orderCustomer);
156
182
  bridge?.onNotify?.('success', 'pisell2.text.add-client-success');
157
183
  return {
@@ -160,13 +186,20 @@ async function applyGlobalScan(host, formatted, bridge) {
160
186
  };
161
187
  }
162
188
  if (searchType === 'walletPass') {
189
+ if (isWalletPassUnavailableAtStore) {
190
+ bridge?.onNotify?.('fail', 'pisell2.text.wallet-pass-not-available-at-store');
191
+ return {
192
+ status: 'failed',
193
+ reason: 'invalid_data'
194
+ };
195
+ }
163
196
  const snapshot = host.getOrderCustomerSnapshot();
164
- const currentCustomerIsWalkIn = getRealCustomerId(snapshot) === undefined;
165
197
  let activeCustomer = snapshot;
166
198
  const bindWalletPassCustomer = async asset => {
167
- if (!currentCustomerIsWalkIn) {
199
+ if (getRealCustomerId(activeCustomer) !== undefined) {
168
200
  return {
169
- resolveFailed: false
201
+ resolveFailed: false,
202
+ customerBound: false
170
203
  };
171
204
  }
172
205
  const assetCustomerId = getWalletAssetCustomerId(asset) ?? getRealCustomerId({
@@ -177,7 +210,8 @@ async function applyGlobalScan(host, formatted, bridge) {
177
210
  if (!host.resolveCustomerById) {
178
211
  bridge?.onNotify?.('fail', 'pisell2.text.scan-global-failed');
179
212
  return {
180
- resolveFailed: true
213
+ resolveFailed: true,
214
+ customerBound: false
181
215
  };
182
216
  }
183
217
  try {
@@ -186,7 +220,8 @@ async function applyGlobalScan(host, formatted, bridge) {
186
220
  console.warn('[GlobalScan] Wallet asset customer lookup failed', error);
187
221
  bridge?.onNotify?.('fail', getErrorMessage(error) || 'pisell2.text.scan-global-failed');
188
222
  return {
189
- resolveFailed: true
223
+ resolveFailed: true,
224
+ customerBound: false
190
225
  };
191
226
  }
192
227
  }
@@ -196,7 +231,8 @@ async function applyGlobalScan(host, formatted, bridge) {
196
231
  bridge?.onNotify?.('fail', 'pisell2.text.scan-global-failed');
197
232
  }
198
233
  return {
199
- resolveFailed: assetCustomerId !== undefined
234
+ resolveFailed: assetCustomerId !== undefined,
235
+ customerBound: false
200
236
  };
201
237
  }
202
238
  const orderCustomer = customer.id === undefined || customer.id === null ? {
@@ -210,7 +246,8 @@ async function applyGlobalScan(host, formatted, bridge) {
210
246
  retainSearchResultCodes: [scanCode]
211
247
  });
212
248
  return {
213
- resolveFailed: false
249
+ resolveFailed: false,
250
+ customerBound: true
214
251
  };
215
252
  };
216
253
  const trySelectWalletAsset = async () => {
@@ -242,24 +279,34 @@ async function applyGlobalScan(host, formatted, bridge) {
242
279
  reason: 'invalid_data'
243
280
  };
244
281
  };
282
+
283
+ // batch-search 可能返回来源门店客户;先映射到当前门店,确保 Wallet
284
+ // 支付资产查询使用本店客户上下文。
285
+ const bindResult = await bindWalletPassCustomer();
245
286
  const walletSelection = await trySelectWalletAsset();
246
287
  if (walletSelection) return walletSelection;
247
-
248
- // 非支付类 Wallet Pass 仍可能由 batch-search 返回所属客户。
249
- await bindWalletPassCustomer();
250
288
  if (!bridge?.applyPromotionCode) {
251
289
  return {
252
290
  status: 'failed',
253
291
  reason: 'no_bridge'
254
292
  };
255
293
  }
256
- const promotionResult = await bridge.applyPromotionCode(scanCode, getRealCustomerId(activeCustomer));
294
+ const promotionCustomerId = getRealCustomerId(activeCustomer);
295
+ const promotionResult = bindResult.customerBound ? await bridge.applyPromotionCode(scanCode, promotionCustomerId, {
296
+ suppressFailureNotification: true
297
+ }) : await bridge.applyPromotionCode(scanCode, promotionCustomerId);
257
298
  if (promotionResult?.isAccepted ?? promotionResult?.isAvailable) {
258
299
  return {
259
300
  status: 'success',
260
301
  kind: 'promotion'
261
302
  };
262
303
  }
304
+ if (bindResult.customerBound) {
305
+ return {
306
+ status: 'success',
307
+ kind: 'customer'
308
+ };
309
+ }
263
310
  return {
264
311
  status: 'failed',
265
312
  reason: 'invalid_data'
@@ -15,7 +15,7 @@ const searchWalletPass = async (request, code) => {
15
15
  relation_product: 1,
16
16
  // 组装关联商品: 0不处理 1处理
17
17
  with_franchisor_data: 1,
18
- with: ['customer'],
18
+ with: ['customer', 'franchiseeWalletPass:shop_id,source_shop_id,product_id,use'],
19
19
  tags: ['point_card',
20
20
  // 积分卡
21
21
  'gift_card',
@@ -26,6 +26,8 @@ const searchWalletPass = async (request, code) => {
26
26
  // 折扣卡
27
27
  'good_pass',
28
28
  // 商品券
29
+ 'entitlement_pass',
30
+ // 月卡 / 年卡 / 多次使用卡
29
31
  'session_ticket',
30
32
  // 场次票
31
33
  'appointment_ticket' // 预约类门票
@@ -10,6 +10,8 @@ export interface GlobalScanFormattedResult {
10
10
  customerId?: string | number;
11
11
  /** 仅 Wallet 返回;true 时禁止直接使用响应内的来源客户。 */
12
12
  isCrossShopWallet?: boolean;
13
+ /** 总店 Wallet Pass 未共享给当前子店。 */
14
+ isWalletPassUnavailableAtStore?: boolean;
13
15
  }
14
16
  interface RawScanSearchResult {
15
17
  searchType?: GlobalScanSearchType;
@@ -43,11 +43,15 @@ function formatGlobalScanResult(raw, scanCode) {
43
43
  }
44
44
  if (searchType === 'walletPass') {
45
45
  const firstResult = resultData?.[0];
46
+ const isWalletPassUnavailableAtStore = firstResult?.is_franchisor_pass === 1 && firstResult?.franchisee_wallet_pass?.use !== 'enabled';
46
47
  return {
47
48
  searchType,
48
49
  data: firstResult?.customer,
49
50
  scanCode,
50
- customerId: firstResult?.customer_id ?? firstResult?.holder?.customer_id ?? firstResult?.customer?.id ?? firstResult?.customer?.customer_id
51
+ customerId: firstResult?.customer_id ?? firstResult?.holder?.customer_id ?? firstResult?.customer?.id ?? firstResult?.customer?.customer_id,
52
+ ...(isWalletPassUnavailableAtStore ? {
53
+ isWalletPassUnavailableAtStore: true
54
+ } : {})
51
55
  };
52
56
  }
53
57
  if (searchType === 'product' || searchType === 'local_product') {
@@ -62,6 +62,7 @@ export declare class ShopDiscountImpl extends BaseModule implements Module {
62
62
  action?: 'create' | 'edit';
63
63
  with_good_pass?: 0 | 1;
64
64
  with_discount_card?: 0 | 1;
65
+ with_entitlement_pass?: 0 | 1;
65
66
  }): Promise<void>;
66
67
  }
67
68
  export default ShopDiscountImpl;
@@ -7,6 +7,7 @@ exports.default = exports.ShopDiscountImpl = void 0;
7
7
  var _BaseModule = require("../../modules/BaseModule");
8
8
  var _Discount = require("../../modules/Discount");
9
9
  var _Rules = require("../../modules/Rules");
10
+ var _entitlementPass = require("../../modules/Discount/entitlementPass");
10
11
  var _utils = require("./utils");
11
12
  var _types = require("../../modules/Rules/types");
12
13
  var _decimal = _interopRequireDefault(require("decimal.js"));
@@ -125,7 +126,8 @@ class ShopDiscountImpl extends _BaseModule.BaseModule {
125
126
  customerId: customer.id,
126
127
  action: 'create',
127
128
  with_good_pass: 1,
128
- with_discount_card: 1
129
+ with_discount_card: 1,
130
+ with_entitlement_pass: 1
129
131
  });
130
132
  });
131
133
  }
@@ -416,7 +418,7 @@ class ShopDiscountImpl extends _BaseModule.BaseModule {
416
418
  if (item.booking_id) {
417
419
  const product = this.hooks?.getProduct(item);
418
420
  (item.discount_list || []).forEach(discount => {
419
- if (discount.id && ['good_pass', 'discount_card'].includes(discount.type)) {
421
+ if (discount.id && ['entitlement_pass', 'good_pass', 'discount_card'].includes(discount.type)) {
420
422
  const index = editModeDiscountList.findIndex(n => n.id === (discount.discount?.resource_id || discount.id));
421
423
  if (index !== -1) {
422
424
  editModeDiscountList[index] = {
@@ -522,6 +524,7 @@ class ShopDiscountImpl extends _BaseModule.BaseModule {
522
524
  with_good_pass: 1,
523
525
  with_discount_card: 1,
524
526
  with_wallet_pass_holder: 1,
527
+ with_entitlement_pass: 1,
525
528
  request_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
526
529
  ...(orderId ? {
527
530
  order_id: orderId
@@ -530,10 +533,27 @@ class ShopDiscountImpl extends _BaseModule.BaseModule {
530
533
  const goodPassList = prepareConfigResult?.discountList || [];
531
534
  const scanDiscount = this.getDiscountList()?.filter(item => item.isScan);
532
535
 
533
- // goodPassList 里可能有 scanDiscount 重合的部分,goodPassList 需要剔除
534
- const scanDiscountIds = scanDiscount.map(n => n.id);
535
- const newGoodPassList = goodPassList?.filter(n => !scanDiscountIds.includes(n.id));
536
- const newDiscountList = [...scanDiscount, ...(newGoodPassList || [])];
536
+ // 扫码 Entitlement Pass 与客户列表重复时保留扫码选择状态,但刷新实时余额。
537
+ // 其他券继续沿用原来的“保留扫码结果、剔除列表重复项”行为。
538
+ const scanDiscountById = new Map(scanDiscount.map(discount => [String(discount.id), discount]));
539
+ const refreshedScanEntitlementIds = new Set();
540
+ const newGoodPassList = goodPassList.flatMap(discount => {
541
+ const key = String(discount.id);
542
+ const scannedDiscount = scanDiscountById.get(key);
543
+ if (!scannedDiscount) return [discount];
544
+ if ((0, _entitlementPass.isEntitlementPass)(scannedDiscount) && (0, _entitlementPass.isEntitlementPass)(discount)) {
545
+ refreshedScanEntitlementIds.add(key);
546
+ return [{
547
+ ...discount,
548
+ isScan: true,
549
+ isSelected: scannedDiscount.isSelected ?? discount.isSelected,
550
+ isManualSelect: scannedDiscount.isManualSelect ?? discount.isManualSelect
551
+ }];
552
+ }
553
+ return [];
554
+ });
555
+ const retainedScanDiscounts = scanDiscount.filter(discount => !refreshedScanEntitlementIds.has(String(discount.id)));
556
+ const newDiscountList = [...retainedScanDiscounts, ...newGoodPassList];
537
557
 
538
558
  // 存储原始优惠券列表
539
559
  this.store.originalDiscountList = newDiscountList;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.uniqueById = exports.isOrderLevelFixedAmountDiscount = exports.isNormalProductByDurationSchedule = exports.isAllNormalProduct = exports.getDiscountListAmountTotal = exports.getDiscountListAmount = exports.getDiscountAmount = exports.getDateIsInSchedule = exports.filterDiscountListByBookingTime = exports.calculateOrderLevelDiscountAllocation = void 0;
7
7
  var _decimal = _interopRequireDefault(require("decimal.js"));
8
8
  var _dayjs = _interopRequireDefault(require("dayjs"));
9
+ var _entitlementPass = require("../../modules/Discount/entitlementPass");
9
10
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
10
11
  const uniqueById = (arr, key = 'id') => {
11
12
  const seen = new Set();
@@ -35,8 +36,8 @@ const isAllNormalProduct = items => {
35
36
  */
36
37
  exports.isAllNormalProduct = isAllNormalProduct;
37
38
  const getDiscountAmount = (discount, total, price) => {
38
- // 商品券
39
- if (discount.tag === 'good_pass') {
39
+ // 商品券和 Entitlement Pass 都是全额抵扣。
40
+ if ((0, _entitlementPass.isFullCoveragePass)(discount)) {
40
41
  return new _decimal.default(price).minus(new _decimal.default(price || 0)).toNumber();
41
42
  }
42
43
 
@@ -142,7 +142,7 @@ function buildPlannerDiscountResult(params) {
142
142
  const productDiscounts = [...(Array.isArray(product.discount_list) ? product.discount_list : []), ...(Array.isArray(product.product_bundle) ? product.product_bundle.flatMap(bundle => bundle?.discount_list || []) : [])];
143
143
  productDiscounts.forEach(discount => {
144
144
  const type = discount.tag || discount.type;
145
- if (!['good_pass', 'discount_card', 'product_discount_card'].includes(type)) return;
145
+ if (!['entitlement_pass', 'good_pass', 'discount_card', 'product_discount_card'].includes(type)) return;
146
146
  const discountId = resolvePlannerDiscountId(discount);
147
147
  applications.push({
148
148
  discountId,
@@ -184,7 +184,7 @@ function isLocalOrderId(orderId) {
184
184
  }
185
185
  function isCustomerBoundDiscount(discount) {
186
186
  const type = discount?.type ?? discount?.tag;
187
- return type === 'good_pass' || type === 'discount_card' || type === 'product_discount_card';
187
+ return type === 'entitlement_pass' || type === 'good_pass' || type === 'discount_card' || type === 'product_discount_card';
188
188
  }
189
189
  function filterCustomerBoundDiscounts(discountList) {
190
190
  if (!Array.isArray(discountList)) return [];
@@ -639,6 +639,22 @@ function normalizeResource(raw, fixedOffsetMinutes = (0, _localClock.captureRunt
639
639
  const id = raw.resourceId ?? raw.resource_id ?? raw.id;
640
640
  if (id === undefined || id === null || id === '') return null;
641
641
  const rawWindows = raw.windows || raw.times || [];
642
+ const rawExcludedWindows = raw.excludedWindows || raw.excluded_windows || [];
643
+ const normalizeWindows = windows => (Array.isArray(windows) ? windows : []).flatMap(window => {
644
+ const range = normalizeAvailabilityLocalDateTimeRange({
645
+ startAt: window.startAt || window.start_at || window.start,
646
+ endAt: window.endAt || window.end_at || window.end,
647
+ fixedOffsetMinutes,
648
+ rollOverEnd: true
649
+ });
650
+ if (!range) return [];
651
+ return [{
652
+ startAt: range.startAt,
653
+ endAt: range.endAt,
654
+ source: window.source,
655
+ raw: window
656
+ }];
657
+ });
642
658
  return {
643
659
  id,
644
660
  formId: raw.formId ?? raw.form_id ?? raw.resource_form_id,
@@ -646,21 +662,8 @@ function normalizeResource(raw, fixedOffsetMinutes = (0, _localClock.captureRunt
646
662
  code: raw.code,
647
663
  resourceType: normalizeResourceType(raw.type || raw.resourceType),
648
664
  capacity: Math.max(1, Number(raw.capacity || raw.max_capacity || 1) || 1),
649
- windows: (Array.isArray(rawWindows) ? rawWindows : []).flatMap(window => {
650
- const range = normalizeAvailabilityLocalDateTimeRange({
651
- startAt: window.startAt || window.start_at || window.start,
652
- endAt: window.endAt || window.end_at || window.end,
653
- fixedOffsetMinutes,
654
- rollOverEnd: true
655
- });
656
- if (!range) return [];
657
- return [{
658
- startAt: range.startAt,
659
- endAt: range.endAt,
660
- source: window.source,
661
- raw: window
662
- }];
663
- }),
665
+ windows: normalizeWindows(rawWindows),
666
+ excludedWindows: normalizeWindows(rawExcludedWindows),
664
667
  raw
665
668
  };
666
669
  }
@@ -679,6 +682,17 @@ function dateRangeDays(dateRange) {
679
682
  }
680
683
  return dates;
681
684
  }
685
+ function getExcludedScheduleDateOverrideTimePoints(date, schedule) {
686
+ if (!schedule.repeat_rule?.excluded_date?.length) return [];
687
+ if ((0, _getDateIsInSchedule.getScheduleStartEndTimePoints)(date, [schedule]).length > 0) return [];
688
+ return (0, _getDateIsInSchedule.getScheduleStartEndTimePoints)(date, [{
689
+ ...schedule,
690
+ repeat_rule: {
691
+ ...schedule.repeat_rule,
692
+ excluded_date: []
693
+ }
694
+ }]);
695
+ }
682
696
  function normalizeResourceOccupancy(rawEvent, resourceId, source = 'remote', fixedOffsetMinutes = (0, _localClock.captureRuntimeOffsetMinutes)()) {
683
697
  const range = normalizeAvailabilityLocalDateTimeRange({
684
698
  startAt: rawEvent.start_at ?? rawEvent.startAt ?? rawEvent.start_time ?? rawEvent.startTime,
@@ -771,7 +785,26 @@ function buildAvailabilityResourcesFromV2(params) {
771
785
  }
772
786
  }];
773
787
  })));
788
+ const excludedWindows = dates.flatMap(date => schedules.flatMap(schedule => getExcludedScheduleDateOverrideTimePoints(date, schedule).flatMap(slot => {
789
+ const range = normalizeAvailabilityLocalDateTimeRange({
790
+ startAt: slot.start_at,
791
+ endAt: slot.end_at,
792
+ fixedOffsetMinutes,
793
+ rollOverEnd: true
794
+ });
795
+ if (!range) return [];
796
+ return [{
797
+ startAt: range.startAt,
798
+ endAt: range.endAt,
799
+ source: `resource_schedule_excluded:${schedule.id}`,
800
+ raw: {
801
+ scheduleId: schedule.id,
802
+ excludedDate: date
803
+ }
804
+ }];
805
+ })));
774
806
  resource.windows = materializedWindows.length > 0 ? materializedWindows : explicitWindows;
807
+ resource.excludedWindows = [...(resource.excludedWindows || []), ...excludedWindows];
775
808
  if (resource.windows.length === 0 && missingScheduleIds.length > 0) {
776
809
  resource.windows = buildResourceWindowFallbackTimes({
777
810
  raw,
@@ -1012,7 +1045,7 @@ function buildBookingFromAvailabilitySelection(params) {
1012
1045
  }
1013
1046
  };
1014
1047
  }
1015
- const POS_OVERRIDABLE_AVAILABILITY_CONFLICT_CODES = new Set(['past', 'booking_cutoff', 'resource_full', 'resource_unavailable', 'resource_capacity_exceeded',
1048
+ const POS_OVERRIDABLE_AVAILABILITY_CONFLICT_CODES = new Set(['past', 'booking_cutoff', 'schedule_excluded_date', 'resource_full', 'resource_unavailable', 'resource_capacity_exceeded',
1016
1049
  // A required group whose selected resource is outside its window is surfaced
1017
1050
  // as missing_resource at group level. Structural validation below still
1018
1051
  // rejects a genuinely missing selection.
@@ -2147,11 +2180,31 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
2147
2180
  source: `operating_schedule:${descriptor.scheduleIds.map(String).join(',')}`
2148
2181
  }] : [];
2149
2182
  }));
2183
+ const excludedWindows = dateRangeDays(params.dateRange).flatMap(date => schedules.flatMap(schedule => getExcludedScheduleDateOverrideTimePoints(date, schedule).flatMap(slot => {
2184
+ const range = normalizeAvailabilityLocalDateTimeRange({
2185
+ startAt: slot.start_at,
2186
+ endAt: slot.end_at,
2187
+ fixedOffsetMinutes: this.getAvailabilityRuntimeOffsetMinutes(),
2188
+ rollOverEnd: true
2189
+ });
2190
+ return range ? [{
2191
+ startAt: range.startAt,
2192
+ endAt: range.endAt,
2193
+ source: `operating_schedule_excluded:${schedule.id}`,
2194
+ raw: {
2195
+ scheduleId: schedule.id,
2196
+ excludedDate: date
2197
+ }
2198
+ }] : [];
2199
+ })));
2150
2200
  return {
2151
2201
  source: descriptor.source,
2152
2202
  scheduleIds: [...descriptor.scheduleIds],
2153
2203
  missingScheduleIds,
2154
- windows
2204
+ windows,
2205
+ ...(excludedWindows.length > 0 ? {
2206
+ excludedWindows
2207
+ } : {})
2155
2208
  };
2156
2209
  }
2157
2210
  async loadAvailabilityProducts(params) {
@@ -2849,7 +2902,9 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
2849
2902
  } : {})
2850
2903
  }, {
2851
2904
  evaluatedAt: this.getAvailabilityRuntimeDateTime(),
2852
- allowSessionCustomRanges: isPosAvailabilityRequest
2905
+ useExactDurationCandidateMinute: isPosAvailabilityRequest,
2906
+ allowSessionCustomRanges: isPosAvailabilityRequest,
2907
+ allowExcludedScheduleDateOverride: isPosAvailabilityRequest
2853
2908
  }).map(group => ({
2854
2909
  ...group,
2855
2910
  ranges: group.ranges.map(candidate => this.decorateAvailabilityCandidate(context.contextId, contextVersion, candidate))
@@ -2864,7 +2919,11 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
2864
2919
  refreshed
2865
2920
  } = await this.ensureAvailabilityContextProjection(contextId, 'query_booking_availability');
2866
2921
  this.assertContextualQueryFreshness(context, request);
2867
- const result = (0, _ResourcePlanner.queryAvailabilityProjection)(projectionEntry.projection, request);
2922
+ const isPosAvailabilityRequest = String(this.otherParams?.platform ?? '').trim().toLowerCase() === 'pos';
2923
+ const result = (0, _ResourcePlanner.queryAvailabilityProjection)(projectionEntry.projection, request, {
2924
+ useExactDurationCandidateMinute: isPosAvailabilityRequest,
2925
+ allowExcludedScheduleDateOverride: isPosAvailabilityRequest
2926
+ });
2868
2927
  const contextVersion = context.activeVersion;
2869
2928
  const contextualResult = result.target === 'time' ? {
2870
2929
  ...result,
@@ -3121,7 +3180,9 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
3121
3180
  } : {})
3122
3181
  } : {})
3123
3182
  }, {
3124
- allowSessionCustomRanges: allowPosAvailabilityOverride
3183
+ useExactDurationCandidateMinute: allowPosAvailabilityOverride,
3184
+ allowSessionCustomRanges: allowPosAvailabilityOverride,
3185
+ allowExcludedScheduleDateOverride: allowPosAvailabilityOverride
3125
3186
  }).flatMap(group => group.ranges);
3126
3187
  const refreshedCandidateBase = refreshedRanges.find(candidate => candidate.startAt === params.candidate.startAt && candidate.endAt === params.candidate.endAt && candidate.bookingStartAt === params.candidate.bookingStartAt && candidate.bookingEndAt === params.candidate.bookingEndAt);
3127
3188
  if (!refreshedCandidateBase) {
@@ -3142,6 +3203,9 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
3142
3203
  requirementSelections: params.requirementSelections,
3143
3204
  partySize,
3144
3205
  bookingCount
3206
+ }, {
3207
+ useExactDurationCandidateMinute: allowPosAvailabilityOverride,
3208
+ allowExcludedScheduleDateOverride: allowPosAvailabilityOverride
3145
3209
  });
3146
3210
  if (allowPosAvailabilityOverride && !validation.ok && validation.conflicts.every(conflict => POS_OVERRIDABLE_AVAILABILITY_CONFLICT_CODES.has(conflict.code))) {
3147
3211
  const forced = buildPosForcedAvailabilityAssignments({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@pisell/pisellos",
4
- "version": "0.10.41",
4
+ "version": "0.10.43",
5
5
  "description": "一个可扩展的前端模块化SDK框架,支持插件系统",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",