@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
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.GOOD_PASS_TYPE = exports.ENTITLEMENT_PASS_TYPE = void 0;
7
+ exports.compareEntitlementPassPriority = compareEntitlementPassPriority;
8
+ exports.getRemainingEntitlementUses = getRemainingEntitlementUses;
9
+ exports.hasAvailableEntitlementUses = hasAvailableEntitlementUses;
10
+ exports.isEntitlementPass = isEntitlementPass;
11
+ exports.isFullCoveragePass = isFullCoveragePass;
12
+ exports.resolveEntitlementQuota = resolveEntitlementQuota;
13
+ var _dayjs = _interopRequireDefault(require("dayjs"));
14
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
+ const ENTITLEMENT_PASS_TYPE = exports.ENTITLEMENT_PASS_TYPE = 'entitlement_pass';
16
+ const GOOD_PASS_TYPE = exports.GOOD_PASS_TYPE = 'good_pass';
17
+ function getDiscountType(discount) {
18
+ const nestedDiscount = discount?.discount;
19
+ return String(discount?.tag || discount?.type || nestedDiscount?.tag || nestedDiscount?.type || '');
20
+ }
21
+ function isEntitlementPass(discount) {
22
+ return getDiscountType(discount) === ENTITLEMENT_PASS_TYPE;
23
+ }
24
+ function isFullCoveragePass(discount) {
25
+ const type = getDiscountType(discount);
26
+ return type === GOOD_PASS_TYPE || type === ENTITLEMENT_PASS_TYPE;
27
+ }
28
+ function resolveIncludedUses(discount) {
29
+ const metadata = discount.metadata;
30
+ const hasMetadataIncludedUses = !!metadata && typeof metadata === 'object' && Object.prototype.hasOwnProperty.call(metadata, 'included_uses');
31
+ const rawValue = hasMetadataIncludedUses ? metadata?.included_uses : Array.isArray(discount.extension_data) ? discount.extension_data.find(item => item?.field_key === 'included_uses')?.value : undefined;
32
+ if (!rawValue || typeof rawValue !== 'object' || Array.isArray(rawValue)) {
33
+ return null;
34
+ }
35
+ const includedUses = rawValue;
36
+ if (includedUses.type === 'unlimited' && includedUses.value === null) {
37
+ return {
38
+ type: 'unlimited',
39
+ value: null
40
+ };
41
+ }
42
+ if (includedUses.type === 'limited' && typeof includedUses.value === 'number' && Number.isInteger(includedUses.value) && includedUses.value >= 1 && includedUses.value <= 9999) {
43
+ return {
44
+ type: 'limited',
45
+ value: includedUses.value
46
+ };
47
+ }
48
+ return null;
49
+ }
50
+ function resolveNonNegativeInteger(value) {
51
+ if (typeof value !== 'number' && typeof value !== 'string' || typeof value === 'string' && value.trim() === '') {
52
+ return null;
53
+ }
54
+ const normalizedValue = Number(value);
55
+ if (!Number.isInteger(normalizedValue) || normalizedValue < 0) return null;
56
+ return normalizedValue;
57
+ }
58
+
59
+ /**
60
+ * Resolve the authoritative total-use contract for an Entitlement Pass.
61
+ * Invalid or missing configuration fails closed. Legacy mode/total_uses fields
62
+ * are intentionally unsupported.
63
+ */
64
+ function resolveEntitlementQuota(discount) {
65
+ if (!isEntitlementPass(discount)) return null;
66
+ const includedUses = resolveIncludedUses(discount);
67
+ if (!includedUses) return null;
68
+ if (includedUses.type === 'unlimited') {
69
+ return {
70
+ type: 'unlimited',
71
+ value: null,
72
+ balance: null
73
+ };
74
+ }
75
+ const balance = resolveNonNegativeInteger(discount?.balance);
76
+ if (balance === null) return null;
77
+ return {
78
+ type: 'limited',
79
+ value: includedUses.value,
80
+ balance
81
+ };
82
+ }
83
+ function getRemainingEntitlementUses(discount, usedCount, requestedCount) {
84
+ const quota = resolveEntitlementQuota(discount);
85
+ const normalizedRequestedCount = Math.max(Math.floor(Number(requestedCount) || 0), 0);
86
+ if (!quota || normalizedRequestedCount <= 0) return 0;
87
+ if (quota.type === 'unlimited') return normalizedRequestedCount;
88
+ const normalizedUsedCount = Math.max(Math.floor(Number(usedCount) || 0), 0);
89
+ return Math.min(Math.max(quota.balance - normalizedUsedCount, 0), normalizedRequestedCount);
90
+ }
91
+ function hasAvailableEntitlementUses(discount) {
92
+ const quota = resolveEntitlementQuota(discount);
93
+ return !!quota && (quota.type === 'unlimited' || quota.balance > 0);
94
+ }
95
+ function resolveExpireTimestamp(value) {
96
+ if (typeof value !== 'string' || !value.trim()) return Number.POSITIVE_INFINITY;
97
+ const normalizedValue = value.trim();
98
+ const calendarParts = normalizedValue.match(/^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2}))?)?/);
99
+ if (calendarParts) {
100
+ const [, year, month, day, hour = '0', minute = '0', second = '0'] = calendarParts;
101
+ const yearNumber = Number(year);
102
+ const monthNumber = Number(month);
103
+ const dayNumber = Number(day);
104
+ const hourNumber = Number(hour);
105
+ const minuteNumber = Number(minute);
106
+ const secondNumber = Number(second);
107
+ const daysInMonth = new Date(Date.UTC(yearNumber, monthNumber, 0)).getUTCDate();
108
+ if (yearNumber <= 0 || monthNumber < 1 || monthNumber > 12 || dayNumber < 1 || dayNumber > daysInMonth || hourNumber < 0 || hourNumber > 23 || minuteNumber < 0 || minuteNumber > 59 || secondNumber < 0 || secondNumber > 59) {
109
+ return Number.POSITIVE_INFINITY;
110
+ }
111
+ }
112
+ const date = (0, _dayjs.default)(normalizedValue);
113
+ return date.isValid() ? date.valueOf() : Number.POSITIVE_INFINITY;
114
+ }
115
+ function compareIds(left, right) {
116
+ const leftNumber = Number(left);
117
+ const rightNumber = Number(right);
118
+ if (Number.isFinite(leftNumber) && Number.isFinite(rightNumber)) {
119
+ return leftNumber - rightNumber;
120
+ }
121
+ return String(left ?? '').localeCompare(String(right ?? ''), undefined, {
122
+ numeric: true
123
+ });
124
+ }
125
+
126
+ /** Sort Entitlement Passes by expiry, finite balance, then card instance id. */
127
+ function compareEntitlementPassPriority(left, right) {
128
+ const expireDifference = resolveExpireTimestamp(left.expire_time) - resolveExpireTimestamp(right.expire_time);
129
+ if (!Number.isNaN(expireDifference) && expireDifference !== 0) {
130
+ return expireDifference;
131
+ }
132
+ const leftQuota = resolveEntitlementQuota(left);
133
+ const rightQuota = resolveEntitlementQuota(right);
134
+ const leftBalance = leftQuota?.type === 'limited' ? leftQuota.balance : Number.POSITIVE_INFINITY;
135
+ const rightBalance = rightQuota?.type === 'limited' ? rightQuota.balance : Number.POSITIVE_INFINITY;
136
+ const balanceDifference = leftBalance - rightBalance;
137
+ if (!Number.isNaN(balanceDifference) && balanceDifference !== 0) {
138
+ return balanceDifference;
139
+ }
140
+ return compareIds(left.id, right.id);
141
+ }
@@ -26,6 +26,7 @@ export declare class DiscountModule extends BaseModule implements Module, Discou
26
26
  with_available_wallet_flag?: 0 | 1;
27
27
  with_available_wallet_pass_flag?: 0 | 1;
28
28
  with_virtual_currency?: 0 | 1;
29
+ with_entitlement_pass?: 0 | 1;
29
30
  request_timezone: string;
30
31
  order_id?: number | string;
31
32
  }): Promise<DiscountPrepareConfigResult>;
@@ -36,6 +37,7 @@ export declare class DiscountModule extends BaseModule implements Module, Discou
36
37
  }): Promise<DiscountWithReason>;
37
38
  batchSearchByProductIds(productIds: number[]): Promise<any>;
38
39
  filterEnabledDiscountList(discountList: Discount[]): Discount[];
40
+ private hasAvailableBalance;
39
41
  private checkUsageCreditsLimit;
40
42
  filterEnabledDiscountListWithReason(discountList: Discount[]): DiscountWithReason;
41
43
  private checkUsageCreditsLimitWithFailKey;
@@ -8,6 +8,7 @@ var _utils = require("../../solution/ShopDiscount/utils");
8
8
  var _BaseModule = require("../BaseModule");
9
9
  var _types = require("./types");
10
10
  var _decimal = _interopRequireDefault(require("decimal.js"));
11
+ var _entitlementPass = require("./entitlementPass");
11
12
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
13
  class DiscountModule extends _BaseModule.BaseModule {
13
14
  defaultName = 'discount';
@@ -83,7 +84,14 @@ class DiscountModule extends _BaseModule.BaseModule {
83
84
  with_available_wallet_pass_flag: params.with_available_wallet_pass_flag ?? 1,
84
85
  with_virtual_currency: params.with_virtual_currency ?? 1
85
86
  });
86
- const rawDiscountList = [...(prepareConfig?.data?.good_pass_list || []), ...(prepareConfig?.data?.discount_card_list || [])];
87
+ const rawEntitlementPassList = prepareConfig?.data?.entitlement_pass_list;
88
+ const entitlementPassList = Array.isArray(rawEntitlementPassList) ? rawEntitlementPassList : [];
89
+ const entitlementPassIds = new Set(entitlementPassList.map(discount => discount?.id).filter(id => id !== undefined && id !== null).map(id => String(id)));
90
+ const excludeEntitlementDuplicates = discount => {
91
+ const id = discount?.id;
92
+ return id === undefined || id === null || !entitlementPassIds.has(String(id));
93
+ };
94
+ const rawDiscountList = [...entitlementPassList, ...(prepareConfig?.data?.good_pass_list || []).filter(excludeEntitlementDuplicates), ...(prepareConfig?.data?.discount_card_list || []).filter(excludeEntitlementDuplicates)];
87
95
  const availableWalletList = [...(prepareConfig?.data?.avaiable_wallet_list || []), ...(prepareConfig?.data?.available_wallet_list || [])];
88
96
  const availableWalletPassList = prepareConfig?.data?.available_wallet_pass_list || [];
89
97
  const availableWalletIds = this.normalizePositiveNumberIds([...availableWalletList.map(item => item?.wallet_id), ...availableWalletPassList.map(item => item?.product_id)]);
@@ -135,7 +143,7 @@ class DiscountModule extends _BaseModule.BaseModule {
135
143
  const result = await this.request.get(`/machinecode/batch-search`, {
136
144
  code,
137
145
  translate_flag: 1,
138
- tags: ['good_pass', 'product_discount_card'],
146
+ tags: ['entitlement_pass', 'good_pass', 'product_discount_card'],
139
147
  // available: 1,
140
148
  relation_product: 1,
141
149
  with: ['extensionData', 'customScheduleSnapshot', 'holder.detail'],
@@ -154,13 +162,24 @@ class DiscountModule extends _BaseModule.BaseModule {
154
162
  const result = await this.request.get(`/machinecode/batch-search`, {
155
163
  ids: productIds,
156
164
  translate_flag: 1,
157
- tags: ['good_pass', 'product_discount_card'],
158
- relation_product: 1
165
+ tags: ['entitlement_pass', 'good_pass', 'product_discount_card'],
166
+ relation_product: 1,
167
+ with: ['extensionData', 'customScheduleSnapshot', 'holder.detail'],
168
+ unified_status_info_flag: 1,
169
+ request_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
159
170
  });
160
171
  return result?.data || [];
161
172
  }
162
173
  filterEnabledDiscountList(discountList) {
163
- return discountList.filter(discount => discount.limit_status === 'enable' && new _decimal.default(discount?.par_value || 0).minus(new _decimal.default(discount?.used_par_value || 0)).greaterThan(0) && this.checkUsageCreditsLimit(discount));
174
+ return discountList.filter(discount => discount.limit_status === 'enable' && this.hasAvailableBalance(discount) && this.checkUsageCreditsLimit(discount));
175
+ }
176
+ hasAvailableBalance(discount) {
177
+ if ((0, _entitlementPass.isEntitlementPass)(discount)) {
178
+ return (0, _entitlementPass.hasAvailableEntitlementUses)(discount);
179
+ }
180
+
181
+ // good_pass 与折扣卡继续沿用原有金额余额判断。
182
+ return new _decimal.default(discount?.par_value || 0).minus(new _decimal.default(discount?.used_par_value || 0)).greaterThan(0);
164
183
  }
165
184
 
166
185
  // 检查使用次数限制
@@ -220,10 +239,7 @@ class DiscountModule extends _BaseModule.BaseModule {
220
239
 
221
240
  // 过滤启用且可用的优惠券,并返回校验失败原因
222
241
  filterEnabledDiscountListWithReason(discountList) {
223
- let lists = discountList.filter(discount => discount.limit_status === 'enable').filter(discount => {
224
- const remaining = new _decimal.default(discount?.par_value || 0).minus(new _decimal.default(discount?.used_par_value || 0)).greaterThan(0);
225
- return remaining;
226
- }).map(discount => {
242
+ let lists = discountList.filter(discount => discount.limit_status === 'enable').filter(discount => this.hasAvailableBalance(discount)).map(discount => {
227
243
  const limitFailKey = this.checkUsageCreditsLimitWithFailKey(discount);
228
244
  return {
229
245
  ...discount,
@@ -63,6 +63,13 @@ interface UsageCreditsValue {
63
63
  max_per_week: number;
64
64
  max_per_month: number;
65
65
  }
66
+ export type IncludedUses = {
67
+ type: 'limited';
68
+ value: number;
69
+ } | {
70
+ type: 'unlimited';
71
+ value: null;
72
+ };
66
73
  export type DiscountFilterRejectKey = 'total_credits' | 'per_user_limit' | 'max_per_day' | 'max_per_week' | 'max_per_month';
67
74
  export interface DiscountWithReason {
68
75
  discountList: Discount[];
@@ -107,30 +114,36 @@ interface ExtensionData {
107
114
  }
108
115
  export interface Discount {
109
116
  id: number;
117
+ shop_id?: number;
110
118
  product_name: string;
111
119
  encoded: string;
112
120
  code: string;
113
121
  tag: string;
114
- expire_time?: string;
122
+ expire_time?: string | null;
123
+ expire_date?: string | null;
124
+ end_time?: string | null;
115
125
  product_id: number;
116
126
  relation_type: string;
117
127
  par_value: string;
118
128
  used_par_value: string;
119
129
  limit_status: string;
120
130
  limited_relation_product_data: Limitedrelationproductdata;
121
- balance: string;
131
+ balance?: string | number | null;
132
+ available_num?: number;
133
+ available_products_info?: any[];
122
134
  format_title: Formattitle;
123
135
  metadata?: {
124
136
  discount_card_type?: 'fixed_amount' | 'percent';
125
137
  custom_product_bundle_map_id?: string;
126
138
  validity_type?: 'custom_schedule_validity' | 'fixed_validity';
127
- discount_calculation_mode: 'item_level' | 'order_level';
128
- holder: {
139
+ included_uses?: IncludedUses;
140
+ discount_calculation_mode?: 'item_level' | 'order_level';
141
+ holder?: {
129
142
  type: 'customer' | 'custom';
130
143
  };
131
144
  };
132
145
  product: Product;
133
- type: 'product' | 'good_pass';
146
+ type?: 'product' | 'good_pass' | 'entitlement_pass' | 'discount_card' | 'product_discount_card';
134
147
  resource_id?: number;
135
148
  savedAmount?: number;
136
149
  isEditMode?: boolean;
@@ -163,9 +176,17 @@ export interface Discount {
163
176
  applicableProductIds?: number[];
164
177
  applicableProductDetails: ApplicableProductDetails[];
165
178
  appliedProductDetails: ApplicableProductDetails[];
179
+ /** Entitlement Pass 编辑订单时已经提交的使用明细。 */
180
+ committedAppliedProductDetails?: ApplicableProductDetails[];
166
181
  isDisabledForProductUsed?: boolean;
167
182
  amount?: number;
168
183
  customer_id?: number | string | null;
184
+ holder?: {
185
+ holder_type?: 'customer' | 'form' | string;
186
+ holder_id?: number | string | Array<number | string>;
187
+ created_at?: string | null;
188
+ [key: string]: any;
189
+ };
169
190
  extension_data?: ExtensionData[];
170
191
  total_order_behavior_count?: number;
171
192
  today_order_behavior_count?: number;
@@ -445,7 +445,12 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
445
445
  }>, options?: {
446
446
  skipEditDiscountConfigRefresh?: boolean;
447
447
  }): Promise<OrderProduct[]>;
448
+ private getFullCoveragePassDiscounts;
449
+ private hasEntitlementPassDiscount;
448
450
  private hasGoodPassDiscount;
451
+ private isPersistedProductLine;
452
+ private isRuntimeEntitlementAppend;
453
+ private hasSameEntitlementMergePriceConfig;
449
454
  private getBundleRuntimeIdentity;
450
455
  private preservePersistedBundlePriceFields;
451
456
  private applyOrderProductUpdateToTempOrder;
@@ -21,6 +21,7 @@ var _discountProductLineIdentity = require("./utils/discountProductLineIdentity"
21
21
  var _virtualSettlement = require("./utils/virtualSettlement");
22
22
  var _Discount = require("../Discount");
23
23
  var _Rules = require("../Rules");
24
+ var _entitlementPass = require("../Discount/entitlementPass");
24
25
  var _types2 = require("../Rules/types");
25
26
  var _decimal = _interopRequireDefault(require("decimal.js"));
26
27
  var _lodashEs = require("lodash-es");
@@ -40,9 +41,27 @@ function isSameDiscountList(left, right) {
40
41
  function isManualProductDiscountProduct(product) {
41
42
  return (0, _manualProductDiscount.hasManualProductDiscountFlag)(product?.metadata) || (product?.discount_list || []).some(item => item?.type === 'product');
42
43
  }
44
+ function syncReducedEntitlementPassUsageCount(discountList, previousQuantity, nextQuantity) {
45
+ if (!Array.isArray(discountList)) return discountList;
46
+ const previousCount = (0, _utils3.getSafeProductNum)(previousQuantity);
47
+ const nextCount = (0, _utils3.getSafeProductNum)(nextQuantity);
48
+ if (nextCount >= previousCount) return discountList;
49
+ return discountList.map(discount => {
50
+ if (!(0, _entitlementPass.isEntitlementPass)(discount)) return discount;
51
+ const rawUsageCount = Number(discount?.metadata?.num ?? previousCount);
52
+ const usageCount = Number.isFinite(rawUsageCount) ? Math.max(Math.floor(rawUsageCount), 0) : previousCount;
53
+ return {
54
+ ...discount,
55
+ metadata: {
56
+ ...(discount?.metadata || {}),
57
+ num: Math.min(usageCount, nextCount)
58
+ }
59
+ };
60
+ });
61
+ }
43
62
  const ORDER_PAYMENT_STATUS_PAID = 'paid';
44
63
  const ORDER_PAYMENT_STATUS_PENDING = 'payment_pending';
45
- const CUSTOMER_PROMOTION_DISCOUNT_TAGS = new Set(['good_pass', 'product_discount_card', 'discount_card']);
64
+ const CUSTOMER_PROMOTION_DISCOUNT_TAGS = new Set(['entitlement_pass', 'good_pass', 'product_discount_card', 'discount_card']);
46
65
  function isCustomerPromotionDiscount(discount) {
47
66
  return CUSTOMER_PROMOTION_DISCOUNT_TAGS.has(String(discount?.tag || discount?.type || '').toLowerCase());
48
67
  }
@@ -72,6 +91,28 @@ function getVoucherPaymentLifecycleKey(payment) {
72
91
  if (!isVoucherPaymentRecord(payment)) return null;
73
92
  return `${String(payment.voucher_id)}::${String(payment.status || '')}`;
74
93
  }
94
+ function getDiscountIdentityKey(discount) {
95
+ const id = discount?.id;
96
+ if (id === undefined || id === null || id === '') return null;
97
+ return String(id);
98
+ }
99
+ function mergeCommittedEntitlementWithRuntime(committed, runtime) {
100
+ const committedAppliedProductDetails = committed.committedAppliedProductDetails?.length ? committed.committedAppliedProductDetails : committed.appliedProductDetails || [];
101
+ return {
102
+ ...committed,
103
+ ...runtime,
104
+ id: runtime.id ?? committed.id,
105
+ tag: runtime.tag || committed.tag,
106
+ type: runtime.type || committed.type,
107
+ isEditMode: false,
108
+ isDisabled: runtime.isDisabled ?? false,
109
+ isSelected: runtime.isSelected ?? committed.isSelected,
110
+ isUsed: committed.isUsed || runtime.isUsed,
111
+ committedAppliedProductDetails: [...committedAppliedProductDetails],
112
+ applicableProductDetails: [...(committed.applicableProductDetails || []), ...(runtime.applicableProductDetails || [])],
113
+ appliedProductDetails: [...committedAppliedProductDetails, ...(runtime.appliedProductDetails || [])]
114
+ };
115
+ }
75
116
  class OrderModule extends _BaseModule.BaseModule {
76
117
  defaultName = 'order';
77
118
  defaultVersion = '1.0.0';
@@ -131,7 +172,10 @@ class OrderModule extends _BaseModule.BaseModule {
131
172
  original_amount: String(item?.discount?.original_amount ?? item?.amount ?? '0'),
132
173
  num: Number(item?.metadata?.num ?? product?.num ?? product?.product_quantity ?? 1) || 1,
133
174
  metadata: item?.metadata,
134
- discount: item?.discount,
175
+ discount: {
176
+ ...(item?.discount || {}),
177
+ product_id: item?.discount?.product_id ?? product?.product_id
178
+ },
135
179
  _num: Number(item?.metadata?.num ?? product?.num ?? product?.product_quantity ?? 1) || 1
136
180
  };
137
181
  const existed = discountMap.get(key);
@@ -463,6 +507,7 @@ class OrderModule extends _BaseModule.BaseModule {
463
507
  with_available_wallet_flag: 1,
464
508
  with_available_wallet_pass_flag: 1,
465
509
  with_virtual_currency: 1,
510
+ with_entitlement_pass: 1,
466
511
  request_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
467
512
  ...(orderId ? {
468
513
  order_id: orderId
@@ -479,17 +524,61 @@ class OrderModule extends _BaseModule.BaseModule {
479
524
  }
480
525
  if (discountList) {
481
526
  const currentDiscountList = this.store.discount?.getDiscountList() || [];
482
- const currentEditDiscounts = currentDiscountList.filter(discount => discount?.isEditMode);
527
+ const currentCommittedDiscounts = currentDiscountList.filter(discount => discount?.isEditMode || discount?.committedAppliedProductDetails?.length);
483
528
  const currentScanDiscounts = currentDiscountList.filter(discount => discount?.isScan);
484
- const currentScanDiscountIds = new Set(currentScanDiscounts.map(discount => discount?.id));
529
+ const currentScanDiscountById = new Map(currentScanDiscounts.flatMap(discount => {
530
+ const id = getDiscountIdentityKey(discount);
531
+ return id === null ? [] : [[id, discount]];
532
+ }));
533
+ const committedDiscountById = new Map(currentCommittedDiscounts.flatMap(discount => {
534
+ const id = getDiscountIdentityKey(discount);
535
+ return id === null ? [] : [[id, discount]];
536
+ }));
537
+ const mergedCommittedDiscountIds = new Set();
538
+ const refreshedScanDiscountIds = new Set();
485
539
  const runtimeDiscountIds = new Set();
486
- const runtimeDiscounts = discountList.filter(discount => {
487
- if (currentScanDiscountIds.has(discount?.id)) return false;
488
- if (runtimeDiscountIds.has(discount?.id)) return false;
489
- runtimeDiscountIds.add(discount?.id);
490
- return true;
540
+ const runtimeDiscounts = discountList.flatMap(discount => {
541
+ const id = getDiscountIdentityKey(discount);
542
+ if (id === null) return [discount];
543
+ if (runtimeDiscountIds.has(id)) return [];
544
+ runtimeDiscountIds.add(id);
545
+ const currentScanDiscount = currentScanDiscountById.get(id);
546
+ if (currentScanDiscount && (!(0, _entitlementPass.isEntitlementPass)(currentScanDiscount) || !(0, _entitlementPass.isEntitlementPass)(discount))) {
547
+ return [];
548
+ }
549
+ const committedDiscount = committedDiscountById.get(id);
550
+ let nextDiscount = discount;
551
+ if (committedDiscount && (0, _entitlementPass.isEntitlementPass)(committedDiscount) && (0, _entitlementPass.isEntitlementPass)(discount)) {
552
+ mergedCommittedDiscountIds.add(id);
553
+ nextDiscount = mergeCommittedEntitlementWithRuntime(committedDiscount, discount);
554
+ }
555
+ if (currentScanDiscount) {
556
+ refreshedScanDiscountIds.add(id);
557
+ nextDiscount = {
558
+ ...nextDiscount,
559
+ isScan: true,
560
+ isSelected: currentScanDiscount.isSelected ?? nextDiscount.isSelected,
561
+ isManualSelect: currentScanDiscount.isManualSelect ?? nextDiscount.isManualSelect
562
+ };
563
+ }
564
+ return [nextDiscount];
491
565
  });
492
- const mergedDiscountList = [...currentEditDiscounts, ...currentScanDiscounts, ...runtimeDiscounts];
566
+ const retainedCommittedDiscounts = currentCommittedDiscounts.filter(discount => {
567
+ const id = getDiscountIdentityKey(discount);
568
+ return id === null || !mergedCommittedDiscountIds.has(id);
569
+ }).map(discount => discount.committedAppliedProductDetails?.length ? {
570
+ ...discount,
571
+ isEditMode: true,
572
+ isDisabled: true,
573
+ isSelected: true,
574
+ isUsed: true,
575
+ appliedProductDetails: [...discount.committedAppliedProductDetails]
576
+ } : discount);
577
+ const retainedScanDiscounts = currentScanDiscounts.filter(discount => {
578
+ const id = getDiscountIdentityKey(discount);
579
+ return id === null || !refreshedScanDiscountIds.has(id);
580
+ });
581
+ const mergedDiscountList = [...retainedCommittedDiscounts, ...retainedScanDiscounts, ...runtimeDiscounts];
493
582
  await this.store.discount?.setOriginalDiscountList(mergedDiscountList);
494
583
  await this.store.discount?.setDiscountList(mergedDiscountList);
495
584
  }
@@ -1502,7 +1591,8 @@ class OrderModule extends _BaseModule.BaseModule {
1502
1591
  const sku = product?.product_sku || {};
1503
1592
  const metadata = product?.metadata || {};
1504
1593
  const discountTypes = (product?.discount_list || []).map(discount => discount?.type ?? discount?.tag);
1505
- const extensionType = product?.extension_type || (discountTypes.includes('good_pass') ? 'good_pass' : 'normal');
1594
+ const fullCoveragePassType = discountTypes.find(type => ['entitlement_pass', 'good_pass'].includes(type));
1595
+ const extensionType = product?.extension_type || fullCoveragePassType || 'normal';
1506
1596
  return {
1507
1597
  product_id: product?.product_id ?? null,
1508
1598
  product_variant_id: product?.product_variant_id ?? null,
@@ -1555,7 +1645,8 @@ class OrderModule extends _BaseModule.BaseModule {
1555
1645
  const sku = product?.product_sku || {};
1556
1646
  const metadata = product?.metadata || {};
1557
1647
  const discountTypes = (product?.discount_list || []).map(discount => discount?.type ?? discount?.tag);
1558
- const extensionType = product?.extension_type || (discountTypes.includes('good_pass') ? 'good_pass' : 'normal');
1648
+ const fullCoveragePassType = discountTypes.find(type => ['entitlement_pass', 'good_pass'].includes(type));
1649
+ const extensionType = product?.extension_type || fullCoveragePassType || 'normal';
1559
1650
  return {
1560
1651
  product_id: product?.product_id ?? null,
1561
1652
  product_variant_id: product?.product_variant_id ?? null,
@@ -2370,7 +2461,7 @@ class OrderModule extends _BaseModule.BaseModule {
2370
2461
  }
2371
2462
  isCustomerBoundDiscount(discount) {
2372
2463
  const type = discount?.type ?? discount?.tag;
2373
- return type === 'good_pass' || type === 'discount_card' || type === 'product_discount_card';
2464
+ return type === 'entitlement_pass' || type === 'good_pass' || type === 'discount_card' || type === 'product_discount_card';
2374
2465
  }
2375
2466
  shouldKeepCustomerBoundDiscountAfterCustomerChange(discount) {
2376
2467
  return Boolean(discount?.isScan || discount?.isEditMode);
@@ -2999,17 +3090,35 @@ class OrderModule extends _BaseModule.BaseModule {
2999
3090
  }
3000
3091
  normalizedIncoming.discount_list = (0, _utils.normalizeOrderProductDiscountList)(normalizedIncoming.discount_list);
3001
3092
  const hasIncomingGoodPass = this.hasGoodPassDiscount(normalizedIncoming);
3093
+ const hasIncomingEntitlementPass = this.hasEntitlementPassDiscount(normalizedIncoming);
3094
+ const isIncomingEntitlementAppend = this.isRuntimeEntitlementAppend(productToAdd, tempOrder);
3095
+ const isIncomingPersistedEntitlementPass = hasIncomingEntitlementPass && this.isPersistedProductLine(normalizedIncoming);
3002
3096
  const hasIncomingProductNote = this.hasOrderProductLineNote(normalizedIncoming);
3003
3097
  const hasIncomingManualProductDiscount = isManualProductDiscountProduct(normalizedIncoming);
3004
3098
  const hasIncomingBookingUid = this.hasLinkedBookingUid(productToAdd) || this.hasLinkedBookingUid(normalizedIncoming);
3005
3099
  const isIncomingCustomItem = productToAdd.metadata?.origin?.isCustomItem === true || productToAdd._origin?.isCustomItem === true;
3006
- const shouldForceNewLine = disableMerge || !!booking || hasIncomingProductNote || hasIncomingManualProductDiscount || hasIncomingBookingUid || isIncomingCustomItem || isWeighingProduct;
3007
- const matchedIndex = hasIncomingGoodPass || shouldForceNewLine ? -1 : tempOrder.products.findIndex(item => {
3100
+ const shouldForceNewLine = disableMerge || !!booking || hasIncomingGoodPass || isIncomingPersistedEntitlementPass || hasIncomingBookingUid || isIncomingCustomItem || isWeighingProduct;
3101
+ const matchedIndex = shouldForceNewLine ? -1 : tempOrder.products.findIndex(item => {
3008
3102
  if (this.hasGoodPassDiscount(item)) return false;
3009
- if (this.hasOrderProductLineNote(item)) return false;
3010
- if (isManualProductDiscountProduct(item)) return false;
3011
- if (item.order_detail_id !== undefined && item.order_detail_id !== null) return false;
3012
- if (item.metadata?.is_edit_for_runtime) return false;
3103
+ const isRuntimeEntitlementPass = this.isRuntimeEntitlementAppend(item, tempOrder);
3104
+ if (isIncomingEntitlementAppend !== isRuntimeEntitlementPass) {
3105
+ return false;
3106
+ }
3107
+ if (isIncomingEntitlementAppend && this.isPersistedProductLine(item)) {
3108
+ return false;
3109
+ }
3110
+ const hasExistingProductNote = this.hasOrderProductLineNote(item);
3111
+ if ((hasIncomingProductNote || hasExistingProductNote) && (!isRuntimeEntitlementPass || String(normalizedIncoming.note || '').trim() !== String(item.note || '').trim())) {
3112
+ return false;
3113
+ }
3114
+ const hasExistingManualProductDiscount = isManualProductDiscountProduct(item);
3115
+ if (isRuntimeEntitlementPass && !this.hasSameEntitlementMergePriceConfig(normalizedIncoming, item)) {
3116
+ return false;
3117
+ }
3118
+ if ((hasIncomingManualProductDiscount || hasExistingManualProductDiscount) && !isRuntimeEntitlementPass) {
3119
+ return false;
3120
+ }
3121
+ if (this.isPersistedProductLine(item)) return false;
3013
3122
  if (item.product_id !== productToAdd.product_id) return false;
3014
3123
  if (item.product_variant_id !== productToAdd.product_variant_id) return false;
3015
3124
  const existedFingerprint = (0, _utils3.buildProductLineFingerprint)((0, _utils.getProductSkuOptions)(item), item.product_bundle);
@@ -3242,17 +3351,51 @@ class OrderModule extends _BaseModule.BaseModule {
3242
3351
  });
3243
3352
  return tempOrder.products;
3244
3353
  }
3245
- hasGoodPassDiscount(product) {
3246
- const hasMainGoodPass = (product.discount_list || []).some(item => item?.type === 'good_pass' || item?.tag === 'good_pass');
3247
- if (hasMainGoodPass) return true;
3248
- const bundleDiscountList = [];
3354
+ getFullCoveragePassDiscounts(product) {
3355
+ const discountList = [...(product.discount_list || [])];
3249
3356
  (product.product_bundle || []).forEach(item => {
3250
- bundleDiscountList.push(...(item?.discount_list || []));
3357
+ discountList.push(...(item?.discount_list || []));
3251
3358
  });
3252
3359
  (product?._extend?.other?.bundle || []).forEach(item => {
3253
- bundleDiscountList.push(...(item?.discount_list || []));
3360
+ discountList.push(...(item?.discount_list || []));
3254
3361
  });
3255
- return bundleDiscountList.some(item => item?.type === 'good_pass' || item?.tag === 'good_pass');
3362
+ return discountList.filter(item => (0, _entitlementPass.isFullCoveragePass)(item));
3363
+ }
3364
+ hasEntitlementPassDiscount(product) {
3365
+ return this.getFullCoveragePassDiscounts(product).some(item => (0, _entitlementPass.isEntitlementPass)(item));
3366
+ }
3367
+ hasGoodPassDiscount(product) {
3368
+ return this.getFullCoveragePassDiscounts(product).some(item => !(0, _entitlementPass.isEntitlementPass)(item));
3369
+ }
3370
+ isPersistedProductLine(product) {
3371
+ return product.order_detail_id !== undefined && product.order_detail_id !== null && product.order_detail_id !== '' || product.metadata?.is_edit_for_runtime === true;
3372
+ }
3373
+ isRuntimeEntitlementAppend(product, tempOrder) {
3374
+ if (this.isPersistedProductLine(product)) return false;
3375
+ const uid = product.metadata?.unique_identification_number ?? product.unique_identification_number;
3376
+ const storedOrigin = uid !== undefined && uid !== null && uid !== '' ? tempOrder._extend?.productsByUid?.[String(uid)]?.origin : undefined;
3377
+ return Boolean(product?._origin?.__entitlementPassRuntimeAppend === true || storedOrigin?.__entitlementPassRuntimeAppend === true);
3378
+ }
3379
+ hasSameEntitlementMergePriceConfig(incoming, existing) {
3380
+ const buildPriceConfig = product => {
3381
+ const metadata = product.metadata || {};
3382
+ const bundle = (product.product_bundle || []).map(item => ({
3383
+ bundle_id: item.bundle_id ?? item.id ?? null,
3384
+ bundle_product_id: item.bundle_product_id ?? item._bundle_product_id ?? item.product_id ?? null,
3385
+ bundle_variant_id: item.bundle_variant_id ?? item.product_variant_id ?? 0,
3386
+ price_type: item.price_type ?? item.custom_price_type ?? '',
3387
+ price_type_ext: item.price_type_ext ?? item.custom_price_type_ext ?? ''
3388
+ })).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
3389
+ return JSON.stringify({
3390
+ original_price: this.normalizeFingerprintMoney(product.original_price),
3391
+ source_product_price: this.normalizeFingerprintMoney(metadata.source_product_price),
3392
+ main_product_original_price: this.normalizeFingerprintMoney(metadata.main_product_original_price),
3393
+ price_override: this.normalizeFingerprintMoney(metadata.price_override),
3394
+ is_manual_discount: Boolean(metadata.is_manual_discount),
3395
+ bundle
3396
+ });
3397
+ };
3398
+ return buildPriceConfig(incoming) === buildPriceConfig(existing);
3256
3399
  }
3257
3400
  getBundleRuntimeIdentity(bundle) {
3258
3401
  if (!bundle || typeof bundle !== 'object') return '';
@@ -3441,7 +3584,7 @@ class OrderModule extends _BaseModule.BaseModule {
3441
3584
  // normalizeOrderProduct 幂等:v2 metadata 存在 → 保留 main_product_* 折扣重算 composite;
3442
3585
  // 否则 → 把顶层 selling_price 视作 source,派生 main_product_* 并写 price_schema_version: 2。
3443
3586
  let normalizedProduct = this.normalizeVirtualSettlementProductIfEnabled((0, _utils.mergeOrderProductDisplayFields)(nextProduct, (0, _utils3.normalizeOrderProduct)(nextProduct)));
3444
- normalizedProduct.discount_list = (0, _utils.normalizeOrderProductDiscountList)(normalizedProduct.discount_list);
3587
+ normalizedProduct.discount_list = (0, _utils.normalizeOrderProductDiscountList)(isPersistedOrderLine ? syncReducedEntitlementPassUsageCount(normalizedProduct.discount_list, targetProduct.num, normalizedProduct.num) : normalizedProduct.discount_list);
3445
3588
  const preservedBookingUid = nextProduct.booking_uid || nextProduct.metadata?.booking_uid || targetProduct.booking_uid || targetProduct.metadata?.booking_uid;
3446
3589
  if (!booking && preservedBookingUid) {
3447
3590
  normalizedProduct.booking_uid = preservedBookingUid;
@@ -3557,6 +3700,7 @@ class OrderModule extends _BaseModule.BaseModule {
3557
3700
  throw new Error('[Order] 目标商品不存在,无法更新数量');
3558
3701
  }
3559
3702
  const targetProduct = tempOrder.products[productIndex];
3703
+ const isPersistedOrderLine = targetProduct.order_detail_id !== undefined && targetProduct.order_detail_id !== null;
3560
3704
  const normalizedProduct = this.normalizeVirtualSettlementProductIfEnabled((0, _utils3.normalizeOrderProduct)({
3561
3705
  ...targetProduct,
3562
3706
  num: (0, _utils3.getSafeProductNum)(num),
@@ -3565,7 +3709,7 @@ class OrderModule extends _BaseModule.BaseModule {
3565
3709
  unique_identification_number: targetProduct.metadata?.unique_identification_number || unique_identification_number
3566
3710
  }
3567
3711
  }));
3568
- normalizedProduct.discount_list = (0, _utils.normalizeOrderProductDiscountList)(normalizedProduct.discount_list);
3712
+ normalizedProduct.discount_list = (0, _utils.normalizeOrderProductDiscountList)(isPersistedOrderLine ? syncReducedEntitlementPassUsageCount(normalizedProduct.discount_list, targetProduct.num, normalizedProduct.num) : normalizedProduct.discount_list);
3569
3713
  const settlementSnapshot = (0, _virtualSettlement.getSelectedSettlementSnapshot)(normalizedProduct);
3570
3714
  if (this.isMultiCurrencySettlementEnabled() && settlementSnapshot?.settlement_type === 'virtual_currency' && tempOrder.customer_id && Number(tempOrder.customer_id) !== 1) {
3571
3715
  const currencyProductId = Number(settlementSnapshot.currency_product_id);