@pisell/pisellos 0.10.49 → 0.10.51

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 (62) hide show
  1. package/dist/modules/Discount/types.d.ts +2 -0
  2. package/dist/modules/FulfillmentTiming/index.d.ts +11 -0
  3. package/dist/modules/FulfillmentTiming/index.js +407 -0
  4. package/dist/modules/FulfillmentTiming/types.d.ts +67 -0
  5. package/dist/modules/FulfillmentTiming/types.js +1 -0
  6. package/dist/modules/OpenData/types.d.ts +6 -0
  7. package/dist/modules/Order/index.d.ts +19 -1
  8. package/dist/modules/Order/index.js +362 -217
  9. package/dist/modules/Order/pickupTiming.d.ts +46 -0
  10. package/dist/modules/Order/pickupTiming.js +53 -0
  11. package/dist/modules/Order/types.d.ts +11 -1
  12. package/dist/modules/Order/utils.d.ts +10 -0
  13. package/dist/modules/Order/utils.js +145 -85
  14. package/dist/modules/Rules/index.js +45 -26
  15. package/dist/modules/Rules/types.d.ts +4 -0
  16. package/dist/modules/index.d.ts +1 -0
  17. package/dist/modules/index.js +1 -0
  18. package/dist/server/modules/order/index.d.ts +8 -0
  19. package/dist/server/modules/order/index.js +506 -453
  20. package/dist/server/modules/order/types.d.ts +1 -0
  21. package/dist/server/utils/small-ticket.js +173 -95
  22. package/dist/solution/BaseSales/index.d.ts +23 -0
  23. package/dist/solution/BaseSales/index.js +1216 -1031
  24. package/dist/solution/BaseSales/pickupTiming.d.ts +37 -0
  25. package/dist/solution/BaseSales/pickupTiming.js +482 -0
  26. package/dist/solution/BaseSales/utils/parseSalesResponse.js +3 -0
  27. package/dist/solution/BookingByStep/index.d.ts +1 -1
  28. package/dist/solution/BookingTicket/utils/cartView.js +3 -1
  29. package/dist/solution/UnifiedBookingSales/index.d.ts +2 -0
  30. package/dist/solution/UnifiedBookingSales/index.js +583 -531
  31. package/lib/model/strategy/adapter/promotion/index.js +46 -1
  32. package/lib/modules/Discount/types.d.ts +2 -0
  33. package/lib/modules/FulfillmentTiming/index.d.ts +11 -0
  34. package/lib/modules/FulfillmentTiming/index.js +359 -0
  35. package/lib/modules/FulfillmentTiming/types.d.ts +67 -0
  36. package/lib/modules/FulfillmentTiming/types.js +5 -0
  37. package/lib/modules/OpenData/types.d.ts +6 -0
  38. package/lib/modules/Order/index.d.ts +19 -1
  39. package/lib/modules/Order/index.js +120 -7
  40. package/lib/modules/Order/pickupTiming.d.ts +46 -0
  41. package/lib/modules/Order/pickupTiming.js +61 -0
  42. package/lib/modules/Order/types.d.ts +11 -1
  43. package/lib/modules/Order/utils.d.ts +10 -0
  44. package/lib/modules/Order/utils.js +58 -4
  45. package/lib/modules/Rules/index.js +24 -13
  46. package/lib/modules/Rules/types.d.ts +4 -0
  47. package/lib/modules/index.d.ts +1 -0
  48. package/lib/modules/index.js +11 -0
  49. package/lib/server/modules/order/index.d.ts +8 -0
  50. package/lib/server/modules/order/index.js +19 -0
  51. package/lib/server/modules/order/types.d.ts +1 -0
  52. package/lib/server/utils/small-ticket.js +93 -22
  53. package/lib/solution/BaseSales/index.d.ts +23 -0
  54. package/lib/solution/BaseSales/index.js +104 -1
  55. package/lib/solution/BaseSales/pickupTiming.d.ts +37 -0
  56. package/lib/solution/BaseSales/pickupTiming.js +222 -0
  57. package/lib/solution/BaseSales/utils/parseSalesResponse.js +3 -0
  58. package/lib/solution/BookingByStep/index.d.ts +1 -1
  59. package/lib/solution/BookingTicket/utils/cartView.js +3 -1
  60. package/lib/solution/UnifiedBookingSales/index.d.ts +2 -0
  61. package/lib/solution/UnifiedBookingSales/index.js +20 -0
  62. package/package.json +1 -1
@@ -4,10 +4,17 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.OrderModule = void 0;
7
+ Object.defineProperty(exports, "isFulfillmentTimingBooking", {
8
+ enumerable: true,
9
+ get: function () {
10
+ return _pickupTiming.isFulfillmentTimingBooking;
11
+ }
12
+ });
7
13
  var _BaseModule = require("../BaseModule");
8
14
  var _types = require("./types");
9
15
  var _utils = require("./utils");
10
16
  var _utils2 = require("../Product/utils");
17
+ var _pickupTiming = require("./pickupTiming");
11
18
  var _dayjs = _interopRequireDefault(require("dayjs"));
12
19
  var _availability = require("../Discount/availability");
13
20
  var _osPrivatePayload = require("../../utils/os-private-payload");
@@ -130,6 +137,8 @@ class OrderModule extends _BaseModule.BaseModule {
130
137
  sessionStorageRestoreRecord = null;
131
138
  /** 只允许当前订单最新的 prepare/config 请求回写客户资产态。 */
132
139
  customerWalletPrepareConfigEpoch = 0;
140
+ /** Payment 可能早于 prepare/config 返回;先按实例 ID 暂存卡号等待 Snapshot。 */
141
+ pointCardSnapshotCodeCandidates = new Map();
133
142
  salesSummaryModuleName;
134
143
  rulesHooksOverride;
135
144
  moduleHooksOverride;
@@ -168,6 +177,7 @@ class OrderModule extends _BaseModule.BaseModule {
168
177
  const key = Number.isFinite(id) ? id : String(resourceId);
169
178
  const title = item?.discount?.title ?? {};
170
179
  const amount = Number(item?.amount || 0);
180
+ const encoded = item?.metadata?.encoded || item?.encoded || item?.discount?.encoded || '';
171
181
  const discountCardType = item?.discount?.discount_card_type;
172
182
  const parValue = isDiscountCard ? discountCardType === 'fixed_amount' ? item?.discount?.fixed_amount ?? item?.amount : item?.discount?.percent : '0';
173
183
  const detail = {
@@ -186,6 +196,13 @@ class OrderModule extends _BaseModule.BaseModule {
186
196
  };
187
197
  const existed = discountMap.get(key);
188
198
  if (existed) {
199
+ if (!existed.encoded && encoded) {
200
+ existed.encoded = encoded;
201
+ existed.metadata = {
202
+ ...existed.metadata,
203
+ encoded
204
+ };
205
+ }
189
206
  existed.amount = Number(existed.amount || 0) + amount;
190
207
  existed.used_par_value = String(Number(existed.used_par_value || 0) + amount);
191
208
  if (!isDiscountCard) {
@@ -205,8 +222,11 @@ class OrderModule extends _BaseModule.BaseModule {
205
222
  discountMap.set(key, {
206
223
  id: key,
207
224
  product_name: '',
208
- encoded: '',
209
- code: '',
225
+ encoded,
226
+ code: item?.code || item?.discount?.code || '',
227
+ metadata: {
228
+ encoded
229
+ },
210
230
  tag: type,
211
231
  product_id: item?.discount?.discount_product_id ?? product?.product_id ?? 0,
212
232
  relation_type: '',
@@ -728,7 +748,10 @@ class OrderModule extends _BaseModule.BaseModule {
728
748
  let resolvedDiscountList = normalizedNewDiscountList || this.getDiscountList();
729
749
  if (isAvailable && normalizedNewDiscountList) {
730
750
  await this.store.discount?.setDiscountList(normalizedNewDiscountList);
731
- this.applyDiscount();
751
+ // 实际写回商品时沿用扫码试算的优先级,避免旧商品行的选卡覆盖本次扫码。
752
+ this.applyDiscount({
753
+ preferredEntitlementPassIds: withScanList.filter(discount => (0, _entitlementPass.isEntitlementPass)(discount)).map(discount => discount.id)
754
+ });
732
755
  await this.syncScannedDiscountsToOriginalDiscountList(this.getDiscountList());
733
756
  resolvedDiscountList = this.getDiscountList();
734
757
  } else if (shouldRetainForLater) {
@@ -786,7 +809,8 @@ class OrderModule extends _BaseModule.BaseModule {
786
809
  isFormSubject: product => (0, _utils.resolveIsFormSubject)(tempOrder, product),
787
810
  orderTotalAmount: Number(this.store.summary?.total_amount || 0)
788
811
  }, {
789
- reapplyBookingDiscountProductUids: options?.reapplyBookingDiscountProductUids
812
+ reapplyBookingDiscountProductUids: options?.reapplyBookingDiscountProductUids,
813
+ preferredEntitlementPassIds: options?.preferredEntitlementPassIds
790
814
  });
791
815
  if (result?.productList) {
792
816
  const previousProductsByUid = (0, _utils.indexOrderProductsByUid)(tempOrder.products);
@@ -1144,17 +1168,55 @@ class OrderModule extends _BaseModule.BaseModule {
1144
1168
  }
1145
1169
  clearCustomerWalletRuntimeState() {
1146
1170
  this.customerWalletPrepareConfigEpoch += 1;
1171
+ this.pointCardSnapshotCodeCandidates.clear();
1147
1172
  this.store.availableWalletIds = [];
1148
1173
  this.store.virtualCurrencyList = [];
1149
1174
  }
1175
+ rememberPointCardSnapshotCodeCandidates(value) {
1176
+ (0, _utils.collectPointCardSnapshotCodeCandidates)(value).forEach(({
1177
+ id,
1178
+ code
1179
+ }) => {
1180
+ if (!this.pointCardSnapshotCodeCandidates.has(id)) {
1181
+ this.pointCardSnapshotCodeCandidates.set(id, code);
1182
+ }
1183
+ });
1184
+ }
1185
+ getRememberedPointCardSnapshotCodeCandidates() {
1186
+ return [...this.pointCardSnapshotCodeCandidates].map(([id, code]) => ({
1187
+ id,
1188
+ code
1189
+ }));
1190
+ }
1191
+ getPointCardSnapshotOrderIdentityKey(identity) {
1192
+ if (identity?.externalSaleNumber) {
1193
+ return `external:${identity.externalSaleNumber}`;
1194
+ }
1195
+ if (identity?.orderId !== undefined && identity.orderId !== null) {
1196
+ return `order:${identity.orderId}`;
1197
+ }
1198
+ if (identity?.localId) return `local:${identity.localId}`;
1199
+ if (identity?.orderNumber) return `number:${identity.orderNumber}`;
1200
+ return '';
1201
+ }
1202
+ isPointCardSnapshotMergeContextCurrent(context) {
1203
+ if (!context) return true;
1204
+ if (context.customerId !== undefined && String(context.customerId ?? '') !== String(this.store.tempOrder?.customer_id ?? '')) {
1205
+ return false;
1206
+ }
1207
+ const expectedOrderKey = this.getPointCardSnapshotOrderIdentityKey(context.orderIdentity);
1208
+ return !expectedOrderKey || expectedOrderKey === this.getPointCardSnapshotOrderIdentityKey(this.getOrderIdentity());
1209
+ }
1150
1210
  writeCustomerWalletSnapshotsFromPrepareConfig(params) {
1151
1211
  const tempOrder = this.store.tempOrder;
1152
1212
  if (!tempOrder) return;
1153
1213
  if (String(tempOrder.customer_id ?? '') !== String(params.customerId)) return;
1214
+ const currentOrderMetaList = (0, _utils.normalizeOrderMetaList)(tempOrder.order_meta_list);
1154
1215
  const virtualCurrencyList = Array.isArray(params.virtualCurrencyList) ? params.virtualCurrencyList : [];
1216
+ const snapshotsWithExistingCodes = (0, _utils.mergePointCardSnapshotCodes)(params.availableWalletPassList, currentOrderMetaList?.point_cards_snapshot);
1155
1217
  tempOrder.order_meta_list = {
1156
- ...((0, _utils.normalizeOrderMetaList)(tempOrder.order_meta_list) || {}),
1157
- point_cards_snapshot: (0, _utils.buildPointCardSnapshotFromWalletPassList)(params.availableWalletPassList),
1218
+ ...(currentOrderMetaList || {}),
1219
+ point_cards_snapshot: (0, _utils.mergePointCardSnapshotCodes)(snapshotsWithExistingCodes, this.getRememberedPointCardSnapshotCodeCandidates()),
1158
1220
  virtual_currency_snapshot: (0, _utils.buildVirtualCurrencySnapshotFromList)(virtualCurrencyList)
1159
1221
  };
1160
1222
  this.store.availableWalletIds = [...params.availableWalletIds];
@@ -1162,6 +1224,31 @@ class OrderModule extends _BaseModule.BaseModule {
1162
1224
  this.persistTempOrder();
1163
1225
  this.saveDraftInBackground();
1164
1226
  }
1227
+
1228
+ /**
1229
+ * 将 Payment WalletPass 返回的实例 code 回填到当前订单 Snapshot。
1230
+ * 这里只按资产 ID 补空值,不新增卡、不覆盖 prepare/config 冻结的余额。
1231
+ */
1232
+ mergePointCardSnapshotCodes(walletPassList, context) {
1233
+ const tempOrder = this.store.tempOrder;
1234
+ const currentOrderMetaList = (0, _utils.normalizeOrderMetaList)(tempOrder?.order_meta_list);
1235
+ const currentSnapshots = currentOrderMetaList?.point_cards_snapshot || [];
1236
+ if (!tempOrder || !this.isPointCardSnapshotMergeContextCurrent(context)) {
1237
+ return currentSnapshots;
1238
+ }
1239
+ this.rememberPointCardSnapshotCodeCandidates(walletPassList);
1240
+ if (!currentSnapshots.length) return currentSnapshots;
1241
+ const nextSnapshots = (0, _utils.mergePointCardSnapshotCodes)(currentSnapshots, this.getRememberedPointCardSnapshotCodeCandidates());
1242
+ const hasChanged = nextSnapshots.some((snapshot, index) => snapshot.code !== currentSnapshots[index]?.code);
1243
+ if (!hasChanged) return currentSnapshots;
1244
+ tempOrder.order_meta_list = {
1245
+ ...(currentOrderMetaList || {}),
1246
+ point_cards_snapshot: nextSnapshots
1247
+ };
1248
+ this.persistTempOrder();
1249
+ this.saveDraftInBackground();
1250
+ return nextSnapshots;
1251
+ }
1165
1252
  createExternalSaleNumber() {
1166
1253
  const timestamp = (0, _dayjs.default)().format('YYYYMMDDHHmmssSSS');
1167
1254
  const random6 = String(Math.floor(Math.random() * 1000000)).padStart(6, '0');
@@ -2193,6 +2280,7 @@ class OrderModule extends _BaseModule.BaseModule {
2193
2280
  const bookingByBookingUid = new Map();
2194
2281
  bookings.forEach(booking => {
2195
2282
  if (!booking || typeof booking !== 'object') return;
2283
+ if ((0, _pickupTiming.isFulfillmentTimingBooking)(booking)) return;
2196
2284
  const productUid = booking.product_uid || booking.metadata?.product_uid;
2197
2285
  if (productUid !== undefined && productUid !== null && String(productUid) !== '') {
2198
2286
  bookingByProductUid.set(String(productUid), booking);
@@ -2243,6 +2331,7 @@ class OrderModule extends _BaseModule.BaseModule {
2243
2331
  let baseHolder = null;
2244
2332
  const seen = new Set();
2245
2333
  for (const booking of tempOrder.bookings || []) {
2334
+ if ((0, _pickupTiming.isFulfillmentTimingBooking)(booking)) continue;
2246
2335
  const holder = booking?.holder && typeof booking.holder === 'object' ? booking.holder : null;
2247
2336
  if (!baseHolder && holder) {
2248
2337
  baseHolder = holder;
@@ -2283,7 +2372,7 @@ class OrderModule extends _BaseModule.BaseModule {
2283
2372
  * this.syncTempOrderHolderFromBookings(tempOrder);
2284
2373
  */
2285
2374
  syncTempOrderHolderFromBookings(tempOrder) {
2286
- const bookings = tempOrder.bookings || [];
2375
+ const bookings = (tempOrder.bookings || []).filter(booking => !(0, _pickupTiming.isFulfillmentTimingBooking)(booking));
2287
2376
  const hasHolderSource = bookings.some(booking => {
2288
2377
  return booking?.holder !== undefined || booking?.metadata?.holder_id !== undefined;
2289
2378
  });
@@ -2308,6 +2397,7 @@ class OrderModule extends _BaseModule.BaseModule {
2308
2397
  const bookingUid = product?.booking_uid || product?.metadata?.booking_uid;
2309
2398
  if (!productUid && !bookingUid) return;
2310
2399
  tempOrder.bookings = (tempOrder.bookings || []).filter(booking => {
2400
+ if ((0, _pickupTiming.isFulfillmentTimingBooking)(booking)) return true;
2311
2401
  const currentBookingUid = this.getBookingUniqueIdentificationNumber(booking);
2312
2402
  if (bookingUid && currentBookingUid === String(bookingUid)) return false;
2313
2403
  if (productUid && String(booking?.product_uid || '') === String(productUid)) return false;
@@ -2721,10 +2811,33 @@ class OrderModule extends _BaseModule.BaseModule {
2721
2811
  });
2722
2812
  }
2723
2813
  updateTempOrderServiceType(serviceType) {
2814
+ if (serviceType !== 'pick_up') {
2815
+ const tempOrder = this.ensureTempOrder();
2816
+ tempOrder.bookings = (tempOrder.bookings || []).filter(booking => !(0, _pickupTiming.isFulfillmentTimingBooking)(booking));
2817
+ }
2724
2818
  return this.updateTempOrderShopServiceData({
2725
2819
  service_type: serviceType
2726
2820
  });
2727
2821
  }
2822
+ getPickupTimingBooking() {
2823
+ return (this.store.tempOrder?.bookings || []).find(_pickupTiming.isFulfillmentTimingBooking) || null;
2824
+ }
2825
+ setPickupTimingBooking(input) {
2826
+ const tempOrder = this.ensureTempOrder();
2827
+ const bookings = tempOrder.bookings || [];
2828
+ const firstIndex = bookings.findIndex(_pickupTiming.isFulfillmentTimingBooking);
2829
+ const nextBooking = (0, _pickupTiming.buildPickupTimingBooking)(input, bookings[firstIndex]);
2830
+ // Keep the first row's identity and position; repair duplicate marked rows only.
2831
+ tempOrder.bookings = firstIndex === -1 ? [...bookings, nextBooking] : bookings.flatMap((booking, index) => index === firstIndex ? [nextBooking] : (0, _pickupTiming.isFulfillmentTimingBooking)(booking) ? [] : [booking]);
2832
+ this.notifyTempOrderChanged();
2833
+ return nextBooking;
2834
+ }
2835
+ clearPickupTimingBooking() {
2836
+ const tempOrder = this.store.tempOrder;
2837
+ if (!tempOrder?.bookings?.some(_pickupTiming.isFulfillmentTimingBooking)) return;
2838
+ tempOrder.bookings = tempOrder.bookings.filter(booking => !(0, _pickupTiming.isFulfillmentTimingBooking)(booking));
2839
+ this.notifyTempOrderChanged();
2840
+ }
2728
2841
  updateTempOrderContactsInfo(contactsInfo) {
2729
2842
  const tempOrder = this.ensureTempOrder();
2730
2843
  tempOrder.contacts_info = contactsInfo && typeof contactsInfo === 'object' && !Array.isArray(contactsInfo) ? contactsInfo : null;
@@ -0,0 +1,46 @@
1
+ export type PickupTimingSelectionMode = 'asap' | 'scheduled';
2
+ export type PickupTimingSource = 'asap' | 'schedule' | 'pos_manual';
3
+ export interface PickupTimingBookingInput {
4
+ mode: PickupTimingSelectionMode;
5
+ source: PickupTimingSource;
6
+ /** Store-local calendar date and minute; the caller validates availability. */
7
+ date: string;
8
+ time: string;
9
+ /** Absolute timestamp disambiguates a repeated local minute at a DST change. */
10
+ instant?: string;
11
+ timezone?: string;
12
+ frozen?: boolean;
13
+ }
14
+ export interface PickupTimingBookingMetadata {
15
+ unique_identification_number: string;
16
+ is_fulfillment_timing: true;
17
+ fulfillment_timing: {
18
+ method: 'pick_up';
19
+ mode: PickupTimingSelectionMode;
20
+ source: PickupTimingSource;
21
+ instant?: string;
22
+ timezone?: string;
23
+ frozen?: boolean;
24
+ };
25
+ [key: string]: any;
26
+ }
27
+ export interface PickupTimingBooking {
28
+ schedule_id: number | string;
29
+ start_date: string;
30
+ end_date: string;
31
+ start_time: string;
32
+ end_time: string;
33
+ select_date: string;
34
+ duration: 0;
35
+ is_all: 0;
36
+ sub_type: 'minutes';
37
+ like_status: string;
38
+ resources: [];
39
+ metadata: PickupTimingBookingMetadata;
40
+ schedule_event_id?: number | string;
41
+ [key: string]: any;
42
+ }
43
+ /** The explicit marker is the only authority; unlinked ordinary bookings remain ordinary. */
44
+ export declare function isFulfillmentTimingBooking(booking: unknown): boolean;
45
+ /** Update an independent pickup appointment without changing its persisted row identity. */
46
+ export declare function buildPickupTimingBooking(input: PickupTimingBookingInput, existing?: Record<string, any> | null): PickupTimingBooking;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.buildPickupTimingBooking = buildPickupTimingBooking;
7
+ exports.isFulfillmentTimingBooking = isFulfillmentTimingBooking;
8
+ var _orderCollectionIdentity = require("./utils/orderCollectionIdentity");
9
+ /** The explicit marker is the only authority; unlinked ordinary bookings remain ordinary. */
10
+ function isFulfillmentTimingBooking(booking) {
11
+ return booking?.metadata?.is_fulfillment_timing === true;
12
+ }
13
+
14
+ /** Update an independent pickup appointment without changing its persisted row identity. */
15
+ function buildPickupTimingBooking(input, existing) {
16
+ const booking = {
17
+ ...(existing || {})
18
+ };
19
+ const metadata = {
20
+ ...(booking.metadata || {})
21
+ };
22
+ // A fulfillment appointment never becomes a product/resource/holder purchase line.
23
+ for (const key of ['product_uid', 'product', 'holder', 'holder_id', 'collect_pax', 'capacity']) {
24
+ delete booking[key];
25
+ delete metadata[key];
26
+ }
27
+ delete metadata.booking_uid;
28
+ return {
29
+ ...booking,
30
+ schedule_id: booking.schedule_id || 0,
31
+ start_date: input.date,
32
+ end_date: input.date,
33
+ start_time: input.time,
34
+ end_time: input.time,
35
+ select_date: input.date,
36
+ duration: 0,
37
+ is_all: 0,
38
+ sub_type: 'minutes',
39
+ like_status: booking.like_status || 'common',
40
+ resources: [],
41
+ metadata: {
42
+ ...metadata,
43
+ unique_identification_number: String(metadata.unique_identification_number || (0, _orderCollectionIdentity.createUuidV4)()),
44
+ is_fulfillment_timing: true,
45
+ fulfillment_timing: {
46
+ method: 'pick_up',
47
+ mode: input.mode,
48
+ source: input.source,
49
+ ...(input.instant !== undefined ? {
50
+ instant: input.instant
51
+ } : {}),
52
+ ...(input.timezone !== undefined ? {
53
+ timezone: input.timezone
54
+ } : {}),
55
+ ...(input.frozen !== undefined ? {
56
+ frozen: input.frozen
57
+ } : {})
58
+ }
59
+ }
60
+ };
61
+ }
@@ -7,9 +7,10 @@ import type { SubmitPayloadEnhancer } from './utils';
7
7
  import type { PaymentItem } from '../Payment/types';
8
8
  import type { ICustomer } from '../AccountList/types';
9
9
  import type { OrderProductDiscountItem } from '../../server/modules/order/types';
10
+ import type { PickupTimingBooking, PickupTimingBookingInput } from './pickupTiming';
10
11
  export type { OrderProductDiscountItem } from '../../server/modules/order/types';
11
12
  export type PickupReferenceMode = 'counter_pickup' | 'table_service';
12
- export type ShopServiceType = 'dine_in' | 'takeaway';
13
+ export type ShopServiceType = 'dine_in' | 'takeaway' | 'pick_up' | 'delivery';
13
14
  export interface ShopServiceData {
14
15
  pickup_reference_mode?: PickupReferenceMode;
15
16
  service_type?: ShopServiceType | '';
@@ -199,10 +200,15 @@ export interface PointCardSnapshotItem {
199
200
  id: number | string;
200
201
  tag: 'point_card';
201
202
  product_id?: number | string;
203
+ code?: string;
202
204
  product_name: string;
203
205
  par_value: string;
204
206
  used_par_value: string;
205
207
  }
208
+ export interface PointCardSnapshotMergeContext {
209
+ customerId?: number | string | null;
210
+ orderIdentity?: OrderIdentitySnapshot | null;
211
+ }
206
212
  export interface VirtualCurrencySnapshotItem {
207
213
  id: number | string;
208
214
  tag: 'virtual_currency';
@@ -858,6 +864,9 @@ export interface OrderModuleAPI {
858
864
  updateTempOrderCustomer: (customer: UpdateTempOrderCustomerInput) => OrderSnapshot['customer'];
859
865
  updateTempOrderPickupReferenceMode: (mode: PickupReferenceMode) => ShopServiceData;
860
866
  updateTempOrderServiceType: (serviceType: ShopServiceType) => ShopServiceData;
867
+ getPickupTimingBooking: () => PickupTimingBooking | null;
868
+ setPickupTimingBooking: (input: PickupTimingBookingInput) => PickupTimingBooking;
869
+ clearPickupTimingBooking: () => void;
861
870
  updateTempOrderBuzzer: (buzzer: string) => string;
862
871
  updateTempOrderContactsInfo: (contactsInfo: Record<string, any> | null) => Record<string, any> | null;
863
872
  addProductToOrder: (product: Partial<OrderProduct> & OrderProductIdentity, booking?: AddProductBookingInput, options?: AddProductToOrderOptions) => Promise<OrderProduct[]>;
@@ -975,6 +984,7 @@ export interface OrderModuleAPI {
975
984
  getDiscountList: () => Discount[];
976
985
  getAvailableWalletIds: () => number[];
977
986
  getCustomerWalletPrepareConfigEpoch: () => number;
987
+ mergePointCardSnapshotCodes: (walletPassList: unknown, context?: PointCardSnapshotMergeContext) => PointCardSnapshotItem[];
978
988
  applyDiscount: () => void;
979
989
  /**
980
990
  * 注入促销评估器。
@@ -18,6 +18,16 @@ export interface OrderProductsDepositSummary {
18
18
  import type { WindowPlugin } from '../../plugins';
19
19
  export declare function normalizePointCardSnapshotItem(value: unknown): PointCardSnapshotItem | null;
20
20
  export declare function buildPointCardSnapshotFromWalletPassList(value: unknown): PointCardSnapshotItem[];
21
+ export declare function collectPointCardSnapshotCodeCandidates(value: unknown): Array<{
22
+ id: string;
23
+ code: string;
24
+ }>;
25
+ /**
26
+ * Payment 的 WalletPass 列表包含实例 code,而 prepare/config 通常只提供
27
+ * 卡名称和余额。按实例 ID 仅补齐 Snapshot 缺失的 code,避免后到的运行态
28
+ * 数据覆盖下单时已经冻结的卡名称和余额。
29
+ */
30
+ export declare function mergePointCardSnapshotCodes(snapshotsValue: unknown, walletPassesValue: unknown): PointCardSnapshotItem[];
21
31
  export declare function normalizeVirtualCurrencySnapshotItem(value: unknown): VirtualCurrencySnapshotItem | null;
22
32
  export declare function buildVirtualCurrencySnapshotFromList(value: unknown): VirtualCurrencySnapshotItem[];
23
33
  export declare function normalizeOrderMetaList(value: unknown): OrderMetaList | undefined;
@@ -14,6 +14,7 @@ exports.buildSubmitPayload = buildSubmitPayload;
14
14
  exports.buildVirtualCurrencySnapshotFromList = buildVirtualCurrencySnapshotFromList;
15
15
  exports.calculateOrderProductsDeposit = calculateOrderProductsDeposit;
16
16
  exports.clearTempOrderHolderAssignments = clearTempOrderHolderAssignments;
17
+ exports.collectPointCardSnapshotCodeCandidates = collectPointCardSnapshotCodeCandidates;
17
18
  exports.composeLinePrice = composeLinePrice;
18
19
  exports.createDefaultOrderRulesHooks = createDefaultOrderRulesHooks;
19
20
  exports.createDefaultTempOrder = createDefaultTempOrder;
@@ -66,6 +67,7 @@ Object.defineProperty(exports, "mergeManualProductDiscountIntoList", {
66
67
  }
67
68
  });
68
69
  exports.mergeOrderProductDisplayFields = mergeOrderProductDisplayFields;
70
+ exports.mergePointCardSnapshotCodes = mergePointCardSnapshotCodes;
69
71
  exports.mergeRelationForms = void 0;
70
72
  exports.normalizeBookingIsAll = normalizeBookingIsAll;
71
73
  exports.normalizeBookingSubType = normalizeBookingSubType;
@@ -133,6 +135,7 @@ var _utils = require("../Holder/utils");
133
135
  var _utils2 = require("../../solution/ScanOrder/utils");
134
136
  var _orderCollectionIdentity = require("./utils/orderCollectionIdentity");
135
137
  var _salesNotes = require("./salesNotes");
138
+ var _pickupTiming = require("./pickupTiming");
136
139
  var _manualProductDiscount = require("./utils/manualProductDiscount");
137
140
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
138
141
  _dayjs.default.extend(_customParseFormat.default);
@@ -165,6 +168,10 @@ function normalizePointCardSnapshotItem(value) {
165
168
  if (productId !== undefined) {
166
169
  snapshot.product_id = productId;
167
170
  }
171
+ const code = normalizeSnapshotScalar(value.code);
172
+ if (code !== undefined) {
173
+ snapshot.code = String(code).trim();
174
+ }
168
175
  return snapshot;
169
176
  }
170
177
  function buildPointCardSnapshotFromWalletPassList(value) {
@@ -175,6 +182,46 @@ function buildPointCardSnapshotFromWalletPassList(value) {
175
182
  return result;
176
183
  }, []);
177
184
  }
185
+ function collectPointCardSnapshotCodeCandidates(value) {
186
+ if (!Array.isArray(value)) return [];
187
+ const codeById = new Map();
188
+ value.forEach(item => {
189
+ if (!isPlainObject(item)) return;
190
+ const id = normalizeSnapshotScalar(item.id ?? item.voucher_id);
191
+ const code = normalizeSnapshotScalar(item.code);
192
+ if (id === undefined || code === undefined) return;
193
+ const identity = String(id).trim();
194
+ const normalizedCode = String(code).trim();
195
+ if (!identity || !normalizedCode || codeById.has(identity)) return;
196
+ codeById.set(identity, normalizedCode);
197
+ });
198
+ return [...codeById].map(([id, code]) => ({
199
+ id,
200
+ code
201
+ }));
202
+ }
203
+
204
+ /**
205
+ * Payment 的 WalletPass 列表包含实例 code,而 prepare/config 通常只提供
206
+ * 卡名称和余额。按实例 ID 仅补齐 Snapshot 缺失的 code,避免后到的运行态
207
+ * 数据覆盖下单时已经冻结的卡名称和余额。
208
+ */
209
+ function mergePointCardSnapshotCodes(snapshotsValue, walletPassesValue) {
210
+ const snapshots = buildPointCardSnapshotFromWalletPassList(snapshotsValue);
211
+ if (!snapshots.length || !Array.isArray(walletPassesValue)) return snapshots;
212
+ const codeById = new Map(collectPointCardSnapshotCodeCandidates(walletPassesValue).map(({
213
+ id,
214
+ code
215
+ }) => [id, code]));
216
+ return snapshots.map(snapshot => {
217
+ if (snapshot.code) return snapshot;
218
+ const code = codeById.get(String(snapshot.id).trim());
219
+ return code ? {
220
+ ...snapshot,
221
+ code
222
+ } : snapshot;
223
+ });
224
+ }
178
225
  function normalizeSnapshotNameMap(value) {
179
226
  if (typeof value === 'string' || typeof value === 'number') {
180
227
  const normalizedText = String(value).trim();
@@ -1251,7 +1298,7 @@ function normalizeSubmitProduct(product, formatOptions = {}) {
1251
1298
  payment_price: formatOptions.virtualSettlementMode ? virtualPrice : submitProduct.payment_price ?? submitProduct.selling_price
1252
1299
  };
1253
1300
  }
1254
- const SUBMIT_BOOKING_METADATA_WHITELIST = ['unique_identification_number', 'collect_pax', 'capacity', 'holder_id', 'resource_select_type', 'holder'];
1301
+ const SUBMIT_BOOKING_METADATA_WHITELIST = ['unique_identification_number', 'collect_pax', 'capacity', 'holder_id', 'resource_select_type', 'holder', 'is_fulfillment_timing', 'fulfillment_timing'];
1255
1302
  function normalizeBookingIsAll(booking) {
1256
1303
  return booking.is_all === true || booking.is_all === 1 || booking.is_all === '1' || booking.is_all === 'true' ? 1 : 0;
1257
1304
  }
@@ -1270,7 +1317,9 @@ function normalizeSubmitBooking(booking) {
1270
1317
  cleanMetadata[key] = rawMetadata[key];
1271
1318
  }
1272
1319
  }
1273
- if (cleanMetadata.holder_id === undefined) cleanMetadata.holder_id = null;
1320
+ if (cleanMetadata.holder_id === undefined && !(0, _pickupTiming.isFulfillmentTimingBooking)(booking)) {
1321
+ cleanMetadata.holder_id = null;
1322
+ }
1274
1323
  const bookingRecord = booking;
1275
1324
  const startDate = bookingRecord.start_date;
1276
1325
  const shouldRestoreSelectDate = bookingRecord.select_date === undefined || bookingRecord.select_date === null || bookingRecord.select_date === '';
@@ -1282,6 +1331,10 @@ function normalizeSubmitBooking(booking) {
1282
1331
  } : {}),
1283
1332
  is_all: normalizeBookingIsAll(bookingRecord),
1284
1333
  sub_type: normalizeBookingSubType(bookingRecord),
1334
+ ...((0, _pickupTiming.isFulfillmentTimingBooking)(booking) ? {
1335
+ like_status: bookingRecord.like_status || 'common',
1336
+ schedule_id: bookingRecord.schedule_id || 0
1337
+ } : {}),
1285
1338
  metadata: cleanMetadata
1286
1339
  };
1287
1340
  }
@@ -1367,6 +1420,7 @@ function hasBoundBookingResource(bookings) {
1367
1420
  if (!Array.isArray(bookings)) return false;
1368
1421
  return bookings.some(booking => {
1369
1422
  if (!booking || typeof booking !== 'object') return false;
1423
+ if ((0, _pickupTiming.isFulfillmentTimingBooking)(booking)) return false;
1370
1424
  if (Number(booking.parent_id) === 0) return false;
1371
1425
  const resources = booking.resources;
1372
1426
  if (!Array.isArray(resources)) return false;
@@ -1402,7 +1456,7 @@ function resolveSubmitOrderType(params) {
1402
1456
  return 'table-order';
1403
1457
  }
1404
1458
  if (currentType) return currentType;
1405
- if (Array.isArray(bookings) && bookings.length > 0) {
1459
+ if (Array.isArray(bookings) && bookings.some(booking => !(0, _pickupTiming.isFulfillmentTimingBooking)(booking))) {
1406
1460
  return businessCode === 'dine_in' ? 'table-order' : 'appointment_booking';
1407
1461
  }
1408
1462
  return 'virtual';
@@ -1487,7 +1541,7 @@ function buildSubmitPayload(params) {
1487
1541
  delivery_type: tempOrder.delivery_type || 'nil',
1488
1542
  schedule_date: scheduleDate,
1489
1543
  // created_at: tempOrder.created_at || formatDateTime(now), 和后端协定这个字段先不提交到接口
1490
- bookings: (tempOrder.bookings || []).filter(booking => Number(booking.parent_id) !== 0).map(booking => normalizeSubmitBooking(booking)),
1544
+ bookings: (tempOrder.bookings || []).filter(booking => (0, _pickupTiming.isFulfillmentTimingBooking)(booking) || Number(booking.parent_id) !== 0).map(booking => normalizeSubmitBooking(booking)),
1491
1545
  payments: tempOrder.payments || [],
1492
1546
  relation_forms: tempOrder.relation_forms || [],
1493
1547
  // contacts: tempOrder.contacts || [],
@@ -104,10 +104,12 @@ class RulesModule extends _BaseModule.BaseModule {
104
104
  unavailableReason: _types.UnavailableReason.AlreadyUsed
105
105
  };
106
106
  }
107
+ const scannedEntitlementPassIds = newDiscountList.filter(discount => (0, _entitlementPass.isEntitlementPass)(discount)).map(discount => discount.id);
108
+ const scannedEntitlementPassIdSet = new Set(scannedEntitlementPassIds.map(String));
107
109
 
108
- // 合并新旧折扣列表,并计算折扣结果,注意,如果旧折扣里有 isEditMode 为 true 的优惠券,则不合并
109
- // 保留之前扫码得到的商品券(isScan: true)
110
- const filteredOldDiscountList = oldDiscountList.filter(discount => !discount.isEditMode && (!(0, _entitlementPass.isFullCoveragePass)(discount) || discount.isScan));
110
+ // 客户次卡必须保留在完整列表中;同一实例以本次扫码数据更新。
111
+ // 历史折扣不参与试算,商品券继续沿用仅保留扫码来源的规则。
112
+ const filteredOldDiscountList = oldDiscountList.filter(discount => !discount.isEditMode && (!(0, _entitlementPass.isFullCoveragePass)(discount) || (0, _entitlementPass.isEntitlementPass)(discount) || discount.isScan) && !scannedEntitlementPassIdSet.has(String(discount.id)));
111
113
  const mergedDiscountList = (0, _utils.uniqueById)([...filteredOldDiscountList, ...newDiscountList]);
112
114
  const result = this.calcDiscount({
113
115
  discountList: mergedDiscountList,
@@ -116,7 +118,8 @@ class RulesModule extends _BaseModule.BaseModule {
116
118
  holders,
117
119
  isFormSubject
118
120
  }, {
119
- scan: true
121
+ scan: true,
122
+ preferredEntitlementPassIds: scannedEntitlementPassIds
120
123
  });
121
124
 
122
125
  // 检查是否有产品使用了新折扣
@@ -216,6 +219,7 @@ class RulesModule extends _BaseModule.BaseModule {
216
219
  isFormSubject,
217
220
  orderTotalAmount
218
221
  }, options) {
222
+ const preferredEntitlementPassIds = new Set((options?.preferredEntitlementPassIds || []).map(String));
219
223
  const resolveIsFormSubject = product => typeof isFormSubject === 'function' ? Boolean(isFormSubject(product)) : Boolean(isFormSubject);
220
224
 
221
225
  // 识别出来是不是在编辑的界面里又新增了商品
@@ -969,6 +973,7 @@ class RulesModule extends _BaseModule.BaseModule {
969
973
  discount_product_id: discount.product_id
970
974
  },
971
975
  metadata: {
976
+ encoded: discount.metadata?.encoded || discount.encoded,
972
977
  num: num,
973
978
  discount_rule_uncheck_flag: discount?.discount_rule_uncheck_flag
974
979
  }
@@ -1164,7 +1169,7 @@ class RulesModule extends _BaseModule.BaseModule {
1164
1169
  return false;
1165
1170
  });
1166
1171
 
1167
- // 用户显式点击某张卡券时,本次选择优先于自动排序和扫码默认选择。
1172
+ // 识别用户本次显式选择的卡券,供次卡和折扣卡各自的优先级使用。
1168
1173
  // 目标仍必须在当前商品的 applicableDiscounts 中,因此不会绕过 Holder、时间或商品范围校验。
1169
1174
  const explicitlySelectedDiscount = options?.isSelected === true && options.discountId !== undefined ? applicableDiscounts.find(discount => {
1170
1175
  const discountType = discount.tag || discount.type || '';
@@ -1173,21 +1178,24 @@ class RulesModule extends _BaseModule.BaseModule {
1173
1178
  // 折扣卡会复用于整条商品行;商品券必须继续按单张拆分,不能复用该分支。
1174
1179
  const explicitlySelectedDiscountCard = explicitlySelectedDiscount && !(0, _entitlementPass.isFullCoveragePass)(explicitlySelectedDiscount) ? explicitlySelectedDiscount : undefined;
1175
1180
 
1176
- // 商品数量等变化会触发无 discountId 的重算。只要当前行已应用的折扣卡仍在
1181
+ // 商品数量等变化会触发无 discountId 的重算。只要当前行已应用的卡仍在
1177
1182
  // applicableDiscounts 中,就应保留该行选择,避免被自动排序中力度更大的卡替换。
1178
- const appliedDiscountCardIds = new Set((product.discount_list || []).filter(item => ['discount_card', 'product_discount_card'].includes(item?.tag || item?.type)).map(item => item?.discount?.resource_id ?? item?.resource_id ?? item?.id).filter(id => id !== undefined && id !== null).map(String));
1183
+ const appliedWalletDiscountIds = new Set((product.discount_list || []).filter(item => ['entitlement_pass', 'discount_card', 'product_discount_card'].includes(item?.tag || item?.type)).map(item => item?.discount?.resource_id ?? item?.resource_id ?? item?.id).filter(id => id !== undefined && id !== null).map(String));
1179
1184
  const appliedDiscountCard = options?.scan ? undefined : applicableDiscounts.find(discount => {
1180
1185
  const discountType = discount.tag || discount.type || '';
1181
- return discount.isSelected === true && ['discount_card', 'product_discount_card'].includes(discountType) && appliedDiscountCardIds.has(String(discount.id));
1186
+ return discount.isSelected === true && ['discount_card', 'product_discount_card'].includes(discountType) && appliedWalletDiscountIds.has(String(discount.id));
1182
1187
  });
1188
+ const appliedEntitlementPass = options?.scan || options?.preserveEntitlementSelection === false ? undefined : applicableDiscounts.find(discount => (0, _entitlementPass.isEntitlementPass)(discount) && discount.isSelected === true && appliedWalletDiscountIds.has(String(discount.id)));
1183
1189
 
1184
- // Entitlement Pass 始终按协议优先级使用;没有新卡时沿用原选择优先级。
1190
+ // 次卡内部按本次手选/扫码、当前行已应用、默认排序选择。
1185
1191
  const scannedSelectedDiscountCard = applicableDiscounts.find(n => n.isScan && n.isSelected && !(0, _entitlementPass.isFullCoveragePass)(n));
1186
1192
  const selectedDiscountCard = explicitlySelectedDiscountCard || appliedDiscountCard || scannedSelectedDiscountCard;
1187
- const highestPriorityEntitlementPass = applicableDiscounts.find(discount => (0, _entitlementPass.isEntitlementPass)(discount));
1188
- const selectedDiscount = highestPriorityEntitlementPass || explicitlySelectedDiscount || selectedDiscountCard || applicableDiscounts[0];
1189
- const prioritizedApplicableDiscounts = !highestPriorityEntitlementPass && explicitlySelectedDiscount && (0, _entitlementPass.isFullCoveragePass)(explicitlySelectedDiscount) ? [explicitlySelectedDiscount, ...applicableDiscounts.filter(discount => {
1190
- return (0, _entitlementPass.isFullCoveragePass)(discount) && String(discount.id) !== String(explicitlySelectedDiscount.id);
1193
+ const selectedEntitlementPass = (explicitlySelectedDiscount && (0, _entitlementPass.isEntitlementPass)(explicitlySelectedDiscount) ? explicitlySelectedDiscount : undefined) || applicableDiscounts.find(discount => (0, _entitlementPass.isEntitlementPass)(discount) && preferredEntitlementPassIds.has(String(discount.id))) || appliedEntitlementPass || applicableDiscounts.find(discount => (0, _entitlementPass.isEntitlementPass)(discount));
1194
+ // 手选优先;普通重算保留当前折扣卡,扫码和最优选择仍使用原有优先级。
1195
+ const selectedDiscount = explicitlySelectedDiscount || (options?.preserveEntitlementSelection !== false && preferredEntitlementPassIds.size === 0 ? appliedDiscountCard : undefined) || selectedEntitlementPass || selectedDiscountCard || applicableDiscounts[0];
1196
+ // 实际次数分配必须使用同一顺序,避免选中状态切换后仍将次数分配给默认卡。
1197
+ const prioritizedApplicableDiscounts = selectedDiscount && (0, _entitlementPass.isFullCoveragePass)(selectedDiscount) ? [selectedDiscount, ...applicableDiscounts.filter(discount => {
1198
+ return (0, _entitlementPass.isFullCoveragePass)(discount) && String(discount.id) !== String(selectedDiscount.id);
1191
1199
  }), ...applicableDiscounts.filter(discount => !(0, _entitlementPass.isFullCoveragePass)(discount))] : applicableDiscounts;
1192
1200
 
1193
1201
  // 如果是手动折扣,则不适用优惠券
@@ -1461,6 +1469,7 @@ class RulesModule extends _BaseModule.BaseModule {
1461
1469
  _num: isFullCoverage ? appliedUseCount : product.num,
1462
1470
  config: selectedDiscount?.config,
1463
1471
  metadata: {
1472
+ encoded: selectedDiscount.metadata?.encoded || selectedDiscount.encoded,
1464
1473
  num: isFullCoverage ? appliedUseCount : 1,
1465
1474
  discount_rule_uncheck_flag: selectedDiscount?.discount_rule_uncheck_flag,
1466
1475
  // 🔥 order_level 分摊差值
@@ -1551,6 +1560,7 @@ class RulesModule extends _BaseModule.BaseModule {
1551
1560
  discount_product_id: selectedDiscount.product_id
1552
1561
  },
1553
1562
  metadata: {
1563
+ encoded: selectedDiscount.metadata?.encoded || selectedDiscount.encoded,
1554
1564
  // 🔥 使用拆分后的唯一 _id
1555
1565
  custom_product_bundle_map_id: uniqueId,
1556
1566
  discount_rule_uncheck_flag: selectedDiscount?.discount_rule_uncheck_flag,
@@ -1646,6 +1656,7 @@ class RulesModule extends _BaseModule.BaseModule {
1646
1656
  discount_product_id: selectedDiscount.product_id
1647
1657
  },
1648
1658
  metadata: {
1659
+ encoded: selectedDiscount.metadata?.encoded || selectedDiscount.encoded,
1649
1660
  // 🔥 使用唯一的 _id
1650
1661
  custom_product_bundle_map_id: uniqueId,
1651
1662
  discount_rule_uncheck_flag: selectedDiscount?.discount_rule_uncheck_flag,