@pisell/pisellos 2.2.309 → 2.2.310

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 (121) hide show
  1. package/dist/model/strategy/adapter/walletPass/evaluator.d.ts +2 -0
  2. package/dist/model/strategy/adapter/walletPass/evaluator.js +4 -2
  3. package/dist/modules/BookingContext/index.js +4 -4
  4. package/dist/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +58 -19
  5. package/dist/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +24 -12
  6. package/dist/modules/Customer/index.js +6 -3
  7. package/dist/modules/Customer/types.d.ts +2 -0
  8. package/dist/modules/Discount/availability.d.ts +7 -0
  9. package/dist/modules/Discount/availability.js +49 -0
  10. package/dist/modules/Discount/entitlementPass.d.ts +34 -0
  11. package/dist/modules/Discount/entitlementPass.js +147 -0
  12. package/dist/modules/Discount/index.d.ts +2 -2
  13. package/dist/modules/Discount/index.js +37 -151
  14. package/dist/modules/Discount/types.d.ts +36 -5
  15. package/dist/modules/OpenData/index.d.ts +1 -1
  16. package/dist/modules/OpenData/index.js +21 -12
  17. package/dist/modules/Order/index.d.ts +32 -4
  18. package/dist/modules/Order/index.js +733 -373
  19. package/dist/modules/Order/types.d.ts +17 -0
  20. package/dist/modules/Order/utils/orderCollectionIdentity.js +16 -12
  21. package/dist/modules/Order/utils.d.ts +14 -2
  22. package/dist/modules/Order/utils.js +275 -76
  23. package/dist/modules/Payment/index.d.ts +0 -5
  24. package/dist/modules/Payment/index.js +35 -85
  25. package/dist/modules/Payment/types.d.ts +21 -1
  26. package/dist/modules/Payment/types.js +2 -0
  27. package/dist/modules/Payment/walletpass.d.ts +1 -1
  28. package/dist/modules/Payment/walletpass.js +80 -47
  29. package/dist/modules/ProductList/index.js +18 -5
  30. package/dist/modules/Rules/index.d.ts +2 -2
  31. package/dist/modules/Rules/index.js +480 -186
  32. package/dist/modules/Rules/types.d.ts +4 -0
  33. package/dist/server/index.js +8 -5
  34. package/dist/server/modules/order/index.js +6 -0
  35. package/dist/server/modules/order/types.d.ts +12 -0
  36. package/dist/server/modules/payment/index.d.ts +25 -6
  37. package/dist/server/modules/payment/index.js +660 -133
  38. package/dist/server/utils/small-ticket.d.ts +8 -5
  39. package/dist/server/utils/small-ticket.js +1269 -199
  40. package/dist/solution/BaseSales/index.d.ts +11 -2
  41. package/dist/solution/BaseSales/index.js +1463 -1173
  42. package/dist/solution/BaseSales/types.d.ts +7 -3
  43. package/dist/solution/BaseSales/utils/cartPromotion.js +141 -126
  44. package/dist/solution/BookingByStep/index.d.ts +1 -1
  45. package/dist/solution/BookingTicket/index.d.ts +12 -3
  46. package/dist/solution/BookingTicket/index.js +663 -542
  47. package/dist/solution/BookingTicket/types.d.ts +5 -1
  48. package/dist/solution/BookingTicket/utils/scan/applyGlobalScan.d.ts +2 -0
  49. package/dist/solution/BookingTicket/utils/scan/applyGlobalScan.js +338 -147
  50. package/dist/solution/BookingTicket/utils/scan/cloudSearch.js +8 -4
  51. package/dist/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +6 -0
  52. package/dist/solution/BookingTicket/utils/scan/formatGlobalScan.js +19 -6
  53. package/dist/solution/ShopDiscount/index.d.ts +2 -0
  54. package/dist/solution/ShopDiscount/index.js +99 -42
  55. package/dist/solution/ShopDiscount/utils.d.ts +4 -2
  56. package/dist/solution/ShopDiscount/utils.js +43 -23
  57. package/dist/solution/UnifiedBookingSales/index.d.ts +2 -2
  58. package/dist/solution/UnifiedBookingSales/index.js +5 -5
  59. package/dist/utils/os-private-payload.d.ts +5 -0
  60. package/dist/utils/os-private-payload.js +17 -0
  61. package/lib/model/strategy/adapter/walletPass/evaluator.d.ts +2 -0
  62. package/lib/model/strategy/adapter/walletPass/evaluator.js +2 -0
  63. package/lib/modules/BookingContext/index.js +3 -3
  64. package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +35 -6
  65. package/lib/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +13 -6
  66. package/lib/modules/Customer/index.js +5 -1
  67. package/lib/modules/Customer/types.d.ts +2 -0
  68. package/lib/modules/Discount/availability.d.ts +7 -0
  69. package/lib/modules/Discount/availability.js +43 -0
  70. package/lib/modules/Discount/entitlementPass.d.ts +34 -0
  71. package/lib/modules/Discount/entitlementPass.js +141 -0
  72. package/lib/modules/Discount/index.d.ts +2 -2
  73. package/lib/modules/Discount/index.js +23 -129
  74. package/lib/modules/Discount/types.d.ts +36 -5
  75. package/lib/modules/OpenData/index.d.ts +1 -1
  76. package/lib/modules/OpenData/index.js +11 -2
  77. package/lib/modules/Order/index.d.ts +32 -4
  78. package/lib/modules/Order/index.js +341 -71
  79. package/lib/modules/Order/types.d.ts +17 -0
  80. package/lib/modules/Order/utils/orderCollectionIdentity.js +14 -11
  81. package/lib/modules/Order/utils.d.ts +14 -2
  82. package/lib/modules/Order/utils.js +186 -3
  83. package/lib/modules/Payment/index.d.ts +0 -5
  84. package/lib/modules/Payment/index.js +3 -28
  85. package/lib/modules/Payment/types.d.ts +21 -1
  86. package/lib/modules/Payment/types.js +1 -0
  87. package/lib/modules/Payment/walletpass.d.ts +1 -1
  88. package/lib/modules/Payment/walletpass.js +29 -9
  89. package/lib/modules/ProductList/index.js +14 -1
  90. package/lib/modules/Rules/index.d.ts +2 -2
  91. package/lib/modules/Rules/index.js +357 -110
  92. package/lib/modules/Rules/types.d.ts +4 -0
  93. package/lib/server/index.js +8 -5
  94. package/lib/server/modules/order/index.js +6 -0
  95. package/lib/server/modules/order/types.d.ts +12 -0
  96. package/lib/server/modules/payment/index.d.ts +25 -6
  97. package/lib/server/modules/payment/index.js +350 -78
  98. package/lib/server/utils/small-ticket.d.ts +8 -5
  99. package/lib/server/utils/small-ticket.js +1009 -121
  100. package/lib/solution/BaseSales/index.d.ts +11 -2
  101. package/lib/solution/BaseSales/index.js +164 -30
  102. package/lib/solution/BaseSales/types.d.ts +7 -3
  103. package/lib/solution/BaseSales/utils/cartPromotion.js +12 -4
  104. package/lib/solution/BookingByStep/index.d.ts +1 -1
  105. package/lib/solution/BookingTicket/index.d.ts +12 -3
  106. package/lib/solution/BookingTicket/index.js +103 -41
  107. package/lib/solution/BookingTicket/types.d.ts +5 -1
  108. package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.d.ts +2 -0
  109. package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.js +174 -42
  110. package/lib/solution/BookingTicket/utils/scan/cloudSearch.js +8 -4
  111. package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +6 -0
  112. package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.js +11 -3
  113. package/lib/solution/ShopDiscount/index.d.ts +2 -0
  114. package/lib/solution/ShopDiscount/index.js +55 -9
  115. package/lib/solution/ShopDiscount/utils.d.ts +4 -2
  116. package/lib/solution/ShopDiscount/utils.js +42 -25
  117. package/lib/solution/UnifiedBookingSales/index.d.ts +2 -2
  118. package/lib/solution/UnifiedBookingSales/index.js +5 -5
  119. package/lib/utils/os-private-payload.d.ts +5 -0
  120. package/lib/utils/os-private-payload.js +22 -0
  121. package/package.json +1 -1
@@ -9,15 +9,31 @@ var _dayjs = _interopRequireDefault(require("dayjs"));
9
9
  var _decimal = _interopRequireDefault(require("decimal.js"));
10
10
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
11
11
  const DEFAULT_LOCALES = ['en', 'zh_CN'];
12
+ const SERVICE_TYPE_LABEL_KEYS = {
13
+ takeaway: 'takeAway',
14
+ pick_up: 'pickUp',
15
+ table_service: 'tableService',
16
+ delivery: 'delivery',
17
+ room_service: 'roomService',
18
+ dine_in: 'dineIn',
19
+ delivery_by_uber: 'deliveryByUber',
20
+ delivery_by_panda: 'deliveryByPanda',
21
+ delivery_by_merchant: 'deliveryByMerchant'
22
+ };
12
23
  const LABELS = {
13
24
  en: {
14
25
  contactName: 'Contact name',
15
26
  contactMobile: 'Contact Mobile',
16
27
  email: 'email',
17
- dineIn: 'Dine-in',
18
- takeAway: 'Take away',
28
+ dineIn: 'Dine in',
29
+ takeAway: 'Takeaway',
19
30
  delivery: 'Delivery',
20
- pickUp: 'Pick-up',
31
+ pickUp: 'Pickup',
32
+ tableService: 'Table Service',
33
+ roomService: 'Room Service',
34
+ deliveryByUber: 'Uber Eats Delivery',
35
+ deliveryByPanda: 'HungryPanda Delivery',
36
+ deliveryByMerchant: 'Merchant Delivery',
21
37
  shopService: 'Dine-in',
22
38
  pickUpTime: 'Pick-up time',
23
39
  asap: 'ASAP',
@@ -27,13 +43,21 @@ const LABELS = {
27
43
  eftpos: 'Eftpos',
28
44
  wallet: 'Wallet',
29
45
  giftCard: 'Gift card',
46
+ pointCard: 'Point card',
30
47
  balance: 'Balance',
48
+ validity: 'Validity',
31
49
  remainingAmount: 'Remaining amount',
32
50
  redeemPoints: 'Redeem points',
33
- remainingPoints: 'Remaining points',
51
+ remainingPoints: 'Points balance',
52
+ entitlementRemaining: 'Remaining',
34
53
  table: 'Table',
54
+ bookingId: 'Booking ID',
55
+ bookingDateTime: 'Date & time',
56
+ bookingResource: 'Resource',
57
+ bookingHolder: 'Holder',
35
58
  orderDiscount: 'Order discount',
36
59
  manualDiscount: 'Manual discount',
60
+ manualMarkup: 'Manual markup',
37
61
  payProcessingFee: 'Pay processing fee',
38
62
  surchargeIncluded: 'Surcharge(Included)',
39
63
  rounding: 'Rounding',
@@ -49,7 +73,12 @@ const LABELS = {
49
73
  dineIn: '堂食',
50
74
  takeAway: '外带',
51
75
  delivery: '配送',
52
- pickUp: '自提',
76
+ pickUp: '自取',
77
+ tableService: '送餐到桌',
78
+ roomService: '客房服务',
79
+ deliveryByUber: 'Uber 配送',
80
+ deliveryByPanda: '熊猫外卖配送',
81
+ deliveryByMerchant: '商家配送',
53
82
  shopService: '堂食',
54
83
  pickUpTime: '取餐时间',
55
84
  asap: '尽快',
@@ -59,13 +88,21 @@ const LABELS = {
59
88
  eftpos: '刷卡支付',
60
89
  wallet: '钱包支付',
61
90
  giftCard: '礼品卡',
91
+ pointCard: '积分卡',
62
92
  balance: '余额',
93
+ validity: '有效期',
63
94
  remainingAmount: '剩余金额',
64
95
  redeemPoints: '本次使用积分',
65
- remainingPoints: '剩余积分',
96
+ remainingPoints: '积分余额',
97
+ entitlementRemaining: '剩余次数',
66
98
  table: '桌台',
99
+ bookingId: '预约编号',
100
+ bookingDateTime: '日期和时间',
101
+ bookingResource: '资源',
102
+ bookingHolder: '预约人',
67
103
  orderDiscount: '整单折扣',
68
104
  manualDiscount: '手动折扣',
105
+ manualMarkup: '手动加价',
69
106
  payProcessingFee: '支付手续费',
70
107
  surchargeIncluded: '手续费(已含)',
71
108
  rounding: '抹零',
@@ -82,6 +119,11 @@ const LABELS = {
82
119
  takeAway: '外帶',
83
120
  delivery: '配送',
84
121
  pickUp: '自取',
122
+ tableService: '送餐到桌',
123
+ roomService: '客房服務',
124
+ deliveryByUber: 'Uber 配送',
125
+ deliveryByPanda: '熊貓外賣配送',
126
+ deliveryByMerchant: '商家配送',
85
127
  shopService: '堂食',
86
128
  pickUpTime: '取餐時間',
87
129
  asap: '盡快',
@@ -91,13 +133,21 @@ const LABELS = {
91
133
  eftpos: '刷卡支付',
92
134
  wallet: '錢包支付',
93
135
  giftCard: '禮品卡',
136
+ pointCard: '積分卡',
94
137
  balance: '餘額',
138
+ validity: '有效期',
95
139
  remainingAmount: '剩餘金額',
96
140
  redeemPoints: '本次使用積分',
97
- remainingPoints: '剩餘積分',
141
+ remainingPoints: '積分餘額',
142
+ entitlementRemaining: '剩餘次數',
98
143
  table: '桌台',
144
+ bookingId: '預約編號',
145
+ bookingDateTime: '日期和時間',
146
+ bookingResource: '資源',
147
+ bookingHolder: '預約人',
99
148
  orderDiscount: '整單折扣',
100
149
  manualDiscount: '手動折扣',
150
+ manualMarkup: '手動加價',
101
151
  payProcessingFee: '支付手續費',
102
152
  surchargeIncluded: '手續費(已含)',
103
153
  rounding: '抹零',
@@ -114,6 +164,11 @@ const LABELS = {
114
164
  takeAway: 'テイクアウト',
115
165
  delivery: '配達',
116
166
  pickUp: '受け取り',
167
+ tableService: 'テーブルサービス',
168
+ roomService: 'ルームサービス',
169
+ deliveryByUber: 'Uber Eats 配達',
170
+ deliveryByPanda: 'HungryPanda 配達',
171
+ deliveryByMerchant: '店舗による配達',
117
172
  shopService: '店内飲食',
118
173
  pickUpTime: '受取時間',
119
174
  asap: 'できるだけ早く',
@@ -121,13 +176,21 @@ const LABELS = {
121
176
  eftpos: 'カード',
122
177
  wallet: 'ウォレット',
123
178
  giftCard: 'ギフトカード',
179
+ pointCard: 'ポイントカード',
124
180
  balance: '残高',
181
+ validity: '有効期限',
125
182
  remainingAmount: '残高',
126
183
  redeemPoints: '利用ポイント',
127
- remainingPoints: '残りポイント',
184
+ remainingPoints: 'ポイント残高',
185
+ entitlementRemaining: '残り回数',
128
186
  table: 'テーブル',
187
+ bookingId: '予約ID',
188
+ bookingDateTime: '日時',
189
+ bookingResource: 'リソース',
190
+ bookingHolder: '予約者',
129
191
  orderDiscount: '注文割引',
130
192
  manualDiscount: '手動割引',
193
+ manualMarkup: '手動値上げ',
131
194
  payProcessingFee: '決済手数料',
132
195
  rounding: '端数調整',
133
196
  gross: '総重量',
@@ -145,6 +208,11 @@ const LABELS = {
145
208
  takeAway: 'Para levar',
146
209
  delivery: 'Entrega',
147
210
  pickUp: 'Recolha',
211
+ tableService: 'Serviço à mesa',
212
+ roomService: 'Serviço de quarto',
213
+ deliveryByUber: 'Entrega pela Uber Eats',
214
+ deliveryByPanda: 'Entrega pela HungryPanda',
215
+ deliveryByMerchant: 'Entrega pelo comerciante',
148
216
  shopService: 'Consumo no local',
149
217
  pickUpTime: 'Hora de recolha',
150
218
  asap: 'Assim que possível',
@@ -152,13 +220,21 @@ const LABELS = {
152
220
  eftpos: 'Cartão',
153
221
  wallet: 'Carteira',
154
222
  giftCard: 'Cartão-presente',
223
+ pointCard: 'Cartão de pontos',
155
224
  balance: 'Saldo',
225
+ validity: 'Validade',
156
226
  remainingAmount: 'Saldo restante',
157
227
  redeemPoints: 'Pontos utilizados',
158
- remainingPoints: 'Pontos restantes',
228
+ remainingPoints: 'Saldo de pontos',
229
+ entitlementRemaining: 'Utilizações restantes',
159
230
  table: 'Mesa',
231
+ bookingId: 'ID da reserva',
232
+ bookingDateTime: 'Data e hora',
233
+ bookingResource: 'Recurso',
234
+ bookingHolder: 'Titular',
160
235
  orderDiscount: 'Desconto do pedido',
161
236
  manualDiscount: 'Desconto manual',
237
+ manualMarkup: 'Acréscimo manual',
162
238
  payProcessingFee: 'Taxa de processamento',
163
239
  rounding: 'Arredondamento',
164
240
  gross: 'Peso bruto',
@@ -264,9 +340,7 @@ function buildBaseData(params) {
264
340
  payment_status: financials.paymentStatus,
265
341
  total_amount: formatMoney(financials.totalAmount, currencySymbol),
266
342
  has_pay: formatMoney(financials.netPaymentApplied, currencySymbol),
267
- ...(isPresentId(financials.amountGap) ? {
268
- amount_gap: formatMoney(financials.amountGap, currencySymbol)
269
- } : {}),
343
+ amount_gap: formatCalculatedMoney(financials.amountGap ?? 0, currencySymbol),
270
344
  is_repeat: 0,
271
345
  buzzer: stringify(order.buzzer || order.pickup_number || order.metadata?.buzzer),
272
346
  order_no: orderNumber,
@@ -278,7 +352,9 @@ function buildBaseData(params) {
278
352
  }
279
353
  function buildProducts(order, locale, productMap) {
280
354
  const products = Array.isArray(order.products) ? order.products : [];
281
- return products.map(product => {
355
+ const allergyByProductUid = buildProductAllergyMap(order, locale);
356
+ return products.map(rawProduct => {
357
+ const product = restoreReceiptDiscountHierarchy(rawProduct);
282
358
  const quantity = toNumber(product.product_quantity ?? product.quantity ?? product.num ?? 1, 1);
283
359
  const originalPrice = product.original_price ?? product.selling_price ?? product.payment_price ?? product.price ?? 0;
284
360
  const sellingPrice = product.selling_price ?? product.payment_price ?? product.price ?? originalPrice;
@@ -317,16 +393,26 @@ function buildProducts(order, locale, productMap) {
317
393
  originalPrice
318
394
  });
319
395
  const productNote = resolveProductNote(product, locale);
396
+ const allergy = isPresentId(productUid) ? allergyByProductUid.get(String(productUid).trim()) || '' : '';
320
397
  const basePrice = resolveProductBasePrice(product);
398
+ const resolvedCombinations = Array.isArray(product.combinations) && product.combinations.length ? normalizeExistingCombinations(product.combinations, locale, currencySymbol) : combinations;
399
+ const shouldIncludeBasePrice = options.length > 0 || resolvedCombinations.length > 0 || hasProductDiscount(product, extensionList);
321
400
  return {
322
401
  product_title: productTitle,
323
402
  product_quantity: quantity,
324
403
  selling_price: formatMoney(sellingPrice, currencySymbol),
404
+ total_selling_price: formatCalculatedMoney(new _decimal.default(toMoneyNumber(sellingPrice)).mul(quantity), currencySymbol),
325
405
  original_price: formatMoney(originalPrice, currencySymbol),
326
- ...(isPresentId(basePrice) ? {
327
- base_price: formatMoney(basePrice, currencySymbol)
406
+ ...(shouldIncludeBasePrice ? {
407
+ base_price: formatCalculatedMoney(basePrice ?? 0, currencySymbol)
328
408
  } : {}),
329
409
  total_original_price: formatMoney(new _decimal.default(toMoneyNumber(originalPrice)).mul(quantity), currencySymbol),
410
+ ...(options.length ? {
411
+ options_total_price: sumNormalizedOptionTotals(options, currencySymbol)
412
+ } : {}),
413
+ ...(resolvedCombinations.length ? {
414
+ package_items_total_price: sumCombinationTotals(resolvedCombinations, currencySymbol)
415
+ } : {}),
330
416
  extension_list: extensionList,
331
417
  ...(isPresentId(productUid) ? {
332
418
  product_uid: productUid
@@ -337,27 +423,104 @@ function buildProducts(order, locale, productMap) {
337
423
  ...(productNote ? {
338
424
  product_note: productNote
339
425
  } : {}),
426
+ ...(allergy ? {
427
+ allergy
428
+ } : {}),
340
429
  ...(options.length ? {
341
430
  options
342
431
  } : {}),
343
432
  ...(resolvedVariant ? {
344
433
  variant: resolvedVariant
345
434
  } : {}),
346
- ...(Array.isArray(product.combinations) && product.combinations.length ? {
347
- combinations: normalizeExistingCombinations(product.combinations, locale)
348
- } : combinations.length ? {
349
- combinations
435
+ ...(resolvedCombinations.length ? {
436
+ combinations: resolvedCombinations
350
437
  } : {})
351
438
  };
352
439
  });
353
440
  }
441
+ const RECEIPT_BUNDLE_MAP_ID_KEY = 'custom_product_bundle_map_id';
442
+ function restoreReceiptDiscountHierarchy(source) {
443
+ if (!source || typeof source !== 'object' || Array.isArray(source)) {
444
+ return source;
445
+ }
446
+ const childKey = Array.isArray(source.combinations) && source.combinations.length ? 'combinations' : Array.isArray(source.product_bundle) && source.product_bundle.length ? 'product_bundle' : null;
447
+ if (!childKey) return {
448
+ ...source
449
+ };
450
+ const children = source[childKey].map(child => child && typeof child === 'object' && !Array.isArray(child) ? restoreReceiptDiscountHierarchy(child) : child);
451
+ const restored = {
452
+ ...source,
453
+ [childKey]: children
454
+ };
455
+ if (!Array.isArray(source.discount_list) || !source.discount_list.length) {
456
+ return restored;
457
+ }
458
+ const descendantsByMapId = new Map();
459
+ indexReceiptDiscountTargets(children, descendantsByMapId);
460
+ if (!descendantsByMapId.size) return restored;
461
+ const ownDiscounts = [];
462
+ for (const discount of source.discount_list) {
463
+ const mapId = getReceiptDiscountBundleMapId(discount);
464
+ const target = mapId ? descendantsByMapId.get(mapId) : undefined;
465
+ if (!target) {
466
+ ownDiscounts.push(discount);
467
+ continue;
468
+ }
469
+ const targetDiscounts = Array.isArray(target.discount_list) ? target.discount_list : [];
470
+ if (!targetDiscounts.some(item => isSameReceiptDiscount(item, discount))) {
471
+ target.discount_list = [...targetDiscounts, discount];
472
+ }
473
+ }
474
+ restored.discount_list = ownDiscounts;
475
+ return restored;
476
+ }
477
+ function indexReceiptDiscountTargets(nodes, targets) {
478
+ for (const node of nodes) {
479
+ if (!node || typeof node !== 'object' || Array.isArray(node)) continue;
480
+ const mapId = getReceiptBundleMapId(node);
481
+ if (mapId && !targets.has(mapId)) targets.set(mapId, node);
482
+ const children = Array.isArray(node.combinations) && node.combinations.length ? node.combinations : Array.isArray(node.product_bundle) ? node.product_bundle : [];
483
+ indexReceiptDiscountTargets(children, targets);
484
+ }
485
+ }
486
+ function normalizeReceiptId(value) {
487
+ if (value === undefined || value === null) return null;
488
+ const normalized = String(value).trim();
489
+ return normalized ? normalized : null;
490
+ }
491
+ function getReceiptBundleMapId(node) {
492
+ return normalizeReceiptId(node.metadata?.[RECEIPT_BUNDLE_MAP_ID_KEY] ?? node._id);
493
+ }
494
+ function getReceiptDiscountBundleMapId(discount) {
495
+ return normalizeReceiptId(discount?.metadata?.[RECEIPT_BUNDLE_MAP_ID_KEY]);
496
+ }
497
+ function isSameReceiptDiscount(left, right) {
498
+ const leftOrderDiscountId = normalizeReceiptId(left?.order_discount_id);
499
+ const rightOrderDiscountId = normalizeReceiptId(right?.order_discount_id);
500
+ if (leftOrderDiscountId && rightOrderDiscountId) {
501
+ return leftOrderDiscountId === rightOrderDiscountId;
502
+ }
503
+ const leftResourceId = normalizeReceiptId(left?.discount?.resource_id ?? left?.resource_id ?? left?.discount_id);
504
+ const rightResourceId = normalizeReceiptId(right?.discount?.resource_id ?? right?.resource_id ?? right?.discount_id);
505
+ if (!leftResourceId || !rightResourceId) return false;
506
+ return leftResourceId === rightResourceId && getReceiptDiscountBundleMapId(left) === getReceiptDiscountBundleMapId(right) && String(left?.amount ?? left?.value ?? left?.discount_amount ?? '') === String(right?.amount ?? right?.value ?? right?.discount_amount ?? '');
507
+ }
354
508
  function resolveProductBasePrice(product) {
355
509
  const metadata = product.metadata || {};
356
510
  const origin = product._origin || metadata.origin || {};
357
511
  const variantId = product.product_variant_id ?? product.variant_id;
358
512
  const variant = Array.isArray(origin.variant) && isPresentId(variantId) ? origin.variant.find(item => isSameId(item?.id, variantId)) : undefined;
359
513
  const isV2 = metadata.price_schema_version === 2 || metadata.price_schema_version === '2';
360
- return firstPresentValue(metadata.source_product_price, variant?.price, product._origin?.price, product._origin?.base_price, !isV2 ? metadata.main_product_original_price : undefined, product.base_price, product.price);
514
+ const quantity = toNumber(product.product_quantity ?? product.quantity ?? product.num ?? 1, 1);
515
+ const hasEntitlementPass = normalizeArray(product.discount_list).some(discount => getProductWalletDiscountType(discount) === 'entitlement_pass');
516
+ const hasOptions = normalizeArray(product.options).length > 0 || normalizeArray(product.product_sku?.option).length > 0;
517
+ const hasCombinations = normalizeArray(product.combinations).length > 0 || normalizeArray(product.product_bundle).length > 0 || normalizeArray(product.bundle).length > 0;
518
+ const aggregateSourcePrice = firstPresentValue(metadata.source_product_price, product.price);
519
+ const originalPrice = product.original_price;
520
+ // 部分旧 entitlement 商品把 source/price 持久化成了整行总价,但
521
+ // original_price 仍是单件价。仅在等式能明确证明该口径时做窄幅回退。
522
+ const useOriginalPriceAsLegacyUnitPrice = hasEntitlementPass && quantity > 1 && !hasOptions && !hasCombinations && isNumericAmount(aggregateSourcePrice) && isNumericAmount(originalPrice) && new _decimal.default(aggregateSourcePrice).eq(new _decimal.default(originalPrice).mul(quantity));
523
+ return firstPresentValue(useOriginalPriceAsLegacyUnitPrice ? originalPrice : undefined, metadata.source_product_price, variant?.price, product._origin?.price, product._origin?.base_price, !isV2 ? metadata.main_product_original_price : undefined, product.base_price, product.price);
361
524
  }
362
525
  function normalizeProductDiscounts(params) {
363
526
  const {
@@ -366,15 +529,37 @@ function normalizeProductDiscounts(params) {
366
529
  currencySymbol,
367
530
  quantity
368
531
  } = params;
369
- return normalizeArray(product.discount_list).map(discount => {
532
+ const rows = normalizeArray(product.discount_list).filter(discount => {
533
+ const amount = discount.amount ?? discount.value ?? discount.discount_amount;
534
+ return isNonZero(amount);
535
+ }).map(discount => {
370
536
  const title = resolveDiscountTitleWithPercent(discount, locale);
371
537
  const amount = discount.amount ?? discount.value ?? discount.discount_amount;
372
- const formattedAmount = formatNegativeMoney(new _decimal.default(toMoneyNumber(amount)).mul(quantity), currencySymbol);
538
+ const isMarkup = isManualMarkup(discount);
539
+ const isAggregatedBundleDiscount = Boolean(getReceiptDiscountBundleMapId(discount));
540
+ const isEntitlementPass = getProductWalletDiscountType(discount) === 'entitlement_pass';
541
+ const totalAmount = new _decimal.default(toMoneyNumber(amount)).mul(isAggregatedBundleDiscount || isEntitlementPass ? 1 : quantity);
542
+ const formattedAmount = isMarkup ? formatMoney(totalAmount.abs(), currencySymbol) : formatNegativeMoney(totalAmount, currencySymbol);
373
543
  return {
374
544
  item: title,
375
- value: formattedAmount
545
+ value: formattedAmount,
546
+ isMarkup
376
547
  };
377
548
  });
549
+ return rows.sort((left, right) => Number(right.isMarkup) - Number(left.isMarkup)).map(({
550
+ isMarkup: _isMarkup,
551
+ ...row
552
+ }) => row);
553
+ }
554
+ function isManualMarkup(discount) {
555
+ if (discount.type !== 'product') return false;
556
+ if (discount.discount?.discount_type === 'change_price') return true;
557
+ const amount = discount.amount ?? discount.value ?? discount.discount_amount;
558
+ return new _decimal.default(toMoneyNumber(amount)).isNegative();
559
+ }
560
+ function hasProductDiscount(product, extensionList = []) {
561
+ const hasDiscountListAmount = normalizeArray(product.discount_list).some(discount => isNonZero(discount.amount ?? discount.value ?? discount.discount_amount));
562
+ return hasDiscountListAmount || extensionList.some(row => /^\s*-/.test(stringify(row?.value)));
378
563
  }
379
564
  function buildProductExtensionList(params) {
380
565
  const {
@@ -446,6 +631,9 @@ function buildProductQuantityPriceText(params) {
446
631
  }
447
632
  function resolveDiscountTitleWithPercent(discount, locale) {
448
633
  const title = resolveDiscountTitle(discount, locale);
634
+ if (getProductWalletDiscountType(discount) === 'entitlement_pass') {
635
+ return title;
636
+ }
449
637
  const percentRaw = discount.discount?.percent;
450
638
  const cardType = discount.discount?.discount_card_type ?? 'percent';
451
639
  const percent = percentRaw !== undefined && percentRaw !== null ? Math.trunc(Number(percentRaw)) : NaN;
@@ -457,7 +645,9 @@ function resolveDiscountTitleWithPercent(discount, locale) {
457
645
  function resolveDiscountTitle(discount, locale) {
458
646
  const title = getLocalizedValue(discount.discount?.name ?? discount.discount?.title ?? discount.discount?.message ?? discount.name ?? discount.title ?? discount.message, locale);
459
647
  if (title) return title;
460
- if (discount.type === 'product') return label(locale, 'manualDiscount');
648
+ if (discount.type === 'product') {
649
+ return label(locale, isManualMarkup(discount) ? 'manualMarkup' : 'manualDiscount');
650
+ }
461
651
  return '';
462
652
  }
463
653
  function resolveProductTitle(params) {
@@ -483,6 +673,7 @@ function normalizeProductOptions(params) {
483
673
  const rawOptions = Array.isArray(params.options) ? params.options : Array.isArray(product.options) ? product.options : Array.isArray(product.product_sku?.option) ? product.product_sku.option : [];
484
674
  return rawOptions.map(option => {
485
675
  const price = option.original_price ?? option.add_price ?? option.price;
676
+ const quantity = toNumber(option.num ?? option.quantity ?? 1, 1);
486
677
  const value = option.value ?? option.option ?? option.item;
487
678
  const name = getProductTitleSource(option, locale) || resolveOptionTitleFromParentProduct({
488
679
  option,
@@ -492,11 +683,12 @@ function normalizeProductOptions(params) {
492
683
  return {
493
684
  name,
494
685
  ...(option.num !== undefined || option.quantity !== undefined ? {
495
- num: toNumber(option.num ?? option.quantity, 1)
686
+ num: quantity
496
687
  } : {}),
497
688
  ...(price !== undefined && price !== null && price !== '' ? {
498
689
  price: formatMoney(price, currencySymbol)
499
690
  } : {}),
691
+ total_price: formatCalculatedMoney(new _decimal.default(toMoneyNumber(price ?? 0)).mul(quantity), currencySymbol),
500
692
  ...(value !== undefined && value !== null && value !== '' ? {
501
693
  value: getLocalizedValue(value, locale)
502
694
  } : {})
@@ -527,49 +719,149 @@ function normalizeProductCombinations(params) {
527
719
  parentProduct
528
720
  });
529
721
  const productNote = resolveProductNote(bundle, locale);
530
- const nestedCombinations = Array.isArray(bundle.combinations) ? normalizeExistingCombinations(bundle.combinations, locale) : [];
722
+ const nestedCombinations = Array.isArray(bundle.combinations) ? normalizeExistingCombinations(bundle.combinations, locale, currencySymbol) : [];
723
+ const quantity = toNumber(bundle.num ?? bundle.quantity ?? 1, 1);
724
+ const formattedPrice = formatBundlePrice(bundle, currencySymbol);
725
+ const optionsTotal = sumNormalizedOptionTotals(combinationsOptions, currencySymbol);
726
+ const explicitBasePrice = resolveCombinationBasePrice(bundle);
727
+ const basePrice = isPresentId(explicitBasePrice) ? explicitBasePrice : 0;
728
+ const extensionList = buildProductExtensionList({
729
+ product: bundle,
730
+ locale,
731
+ currencySymbol,
732
+ quantity,
733
+ originalPrice: formattedPrice
734
+ });
735
+ const shouldIncludeBasePrice = combinationsOptions.length > 0 || nestedCombinations.length > 0 || hasProductDiscount(bundle, extensionList);
531
736
  return {
532
737
  product_title: baseTitle,
533
- product_quantity: toNumber(bundle.num ?? bundle.quantity ?? 1, 1),
534
- price: formatBundlePrice(bundle, currencySymbol),
738
+ product_quantity: quantity,
739
+ price: formattedPrice,
740
+ total_selling_price: formatCalculatedMoney(new _decimal.default(toMoneyNumber(formattedPrice)).mul(quantity), currencySymbol),
741
+ ...(shouldIncludeBasePrice ? {
742
+ base_price: formatCalculatedMoney(basePrice, currencySymbol)
743
+ } : {}),
535
744
  combinations_options: combinationsOptions,
745
+ ...(combinationsOptions.length ? {
746
+ options_total_price: optionsTotal
747
+ } : {}),
748
+ ...(nestedCombinations.length ? {
749
+ package_items_total_price: sumCombinationTotals(nestedCombinations, currencySymbol)
750
+ } : {}),
536
751
  ...(productNote ? {
537
752
  product_note: productNote
538
753
  } : {}),
754
+ ...(extensionList.length ? {
755
+ extension_list: extensionList
756
+ } : {}),
539
757
  ...(nestedCombinations.length ? {
540
758
  combinations: nestedCombinations
541
759
  } : {})
542
760
  };
543
761
  });
544
762
  }
545
- function normalizeExistingCombinations(combinations, locale) {
763
+ function normalizeExistingCombinations(combinations, locale, currencySymbol) {
546
764
  return combinations.map(combination => {
547
765
  if (!combination || typeof combination !== 'object' || Array.isArray(combination)) {
548
766
  return combination;
549
767
  }
550
768
  const productNote = resolveProductNote(combination, locale);
551
- const nestedCombinations = Array.isArray(combination.combinations) ? normalizeExistingCombinations(combination.combinations, locale) : undefined;
769
+ const rawOptions = Array.isArray(combination.combinations_options) ? combination.combinations_options : Array.isArray(combination.options) ? combination.options : [];
770
+ const normalizedOptions = normalizeProductOptions({
771
+ product: {},
772
+ locale,
773
+ currencySymbol,
774
+ options: rawOptions
775
+ });
776
+ const nestedCombinations = Array.isArray(combination.combinations) ? normalizeExistingCombinations(combination.combinations, locale, currencySymbol) : [];
777
+ const productTitle = getBundleProductTitleSource(combination, locale);
552
778
  const snapshot = {
553
779
  ...combination
554
780
  };
781
+ delete snapshot.product_title;
555
782
  delete snapshot.product_note;
556
783
  delete snapshot.note;
784
+ delete snapshot.combinations;
785
+ delete snapshot.discount_list;
786
+ const quantity = toNumber(combination.product_quantity ?? combination.num ?? combination.quantity ?? 1, 1);
787
+ const unitPrice = firstPresentValue(combination.selling_price, combination.price, combination.original_price) ?? 0;
788
+ const optionsAmount = sumMoneyValues(normalizedOptions.map(option => option.total_price));
789
+ const explicitBasePrice = resolveCombinationBasePrice(combination, true);
790
+ const basePrice = isPresentId(explicitBasePrice) ? explicitBasePrice : 0;
791
+ const extensionList = buildProductExtensionList({
792
+ product: combination,
793
+ locale,
794
+ currencySymbol,
795
+ quantity,
796
+ originalPrice: unitPrice
797
+ });
798
+ const shouldIncludeBasePrice = normalizedOptions.length > 0 || nestedCombinations.length > 0 || hasProductDiscount(combination, extensionList);
557
799
  return {
558
800
  ...snapshot,
801
+ ...(productTitle ? {
802
+ product_title: productTitle
803
+ } : {}),
804
+ product_quantity: quantity,
805
+ total_selling_price: formatCalculatedMoney(new _decimal.default(toMoneyNumber(unitPrice)).mul(quantity), currencySymbol),
806
+ ...(shouldIncludeBasePrice ? {
807
+ base_price: formatCalculatedMoney(basePrice, currencySymbol)
808
+ } : {}),
809
+ ...(Array.isArray(combination.combinations_options) ? {
810
+ combinations_options: normalizedOptions
811
+ } : Array.isArray(combination.options) ? {
812
+ options: normalizedOptions
813
+ } : {}),
814
+ ...(normalizedOptions.length ? {
815
+ options_total_price: formatCalculatedMoney(optionsAmount, currencySymbol)
816
+ } : {}),
817
+ ...(nestedCombinations.length ? {
818
+ package_items_total_price: sumCombinationTotals(nestedCombinations, currencySymbol)
819
+ } : {}),
559
820
  ...(productNote ? {
560
821
  product_note: productNote
561
822
  } : {}),
562
- ...(nestedCombinations ? {
823
+ ...(extensionList.length ? {
824
+ extension_list: extensionList
825
+ } : {}),
826
+ ...(nestedCombinations.length ? {
563
827
  combinations: nestedCombinations
564
828
  } : {})
565
829
  };
566
830
  });
567
831
  }
832
+ function sumMoneyValues(values) {
833
+ return values.reduce((sum, value) => sum.plus(toMoneyNumber(value)), new _decimal.default(0));
834
+ }
835
+ function resolveCombinationBasePrice(combination, preferOriginalPrice = false) {
836
+ const fallbackPrices = preferOriginalPrice ? [combination.original_price, combination.product_price, combination.price] : [combination.price, combination.original_price, combination.product_price];
837
+ return firstPresentValue(combination.base_price, combination.metadata?.source_product_price, ...fallbackPrices);
838
+ }
839
+ function sumNormalizedOptionTotals(options, currencySymbol) {
840
+ return formatCalculatedMoney(sumMoneyValues(options.map(option => option.total_price)), currencySymbol);
841
+ }
842
+ function sumCombinationTotals(combinations, currencySymbol) {
843
+ return formatCalculatedMoney(sumMoneyValues(combinations.map(combination => combination.total_selling_price)), currencySymbol);
844
+ }
568
845
  function resolveProductNote(product, locale) {
569
846
  const productNote = getLocalizedValue(product.product_note, locale).trim();
570
847
  if (productNote) return productNote;
571
848
  return getLocalizedValue(product.note, locale).trim();
572
849
  }
850
+ function buildProductAllergyMap(order, locale) {
851
+ const allergyByProductUid = new Map();
852
+ for (const note of normalizeArray(order.notes)) {
853
+ if (!note || typeof note !== 'object' || Array.isArray(note) || String(note.note_type || '').trim() !== 'allergy') {
854
+ continue;
855
+ }
856
+ const content = getLocalizedValue(note.content?.text, locale).trim();
857
+ if (!content) continue;
858
+ const relation = normalizeArray(note.note_relations).find(item => item && typeof item === 'object' && !Array.isArray(item) && item.relation_type === 'about' && item.target_type === 'order_detail' && Number(item.is_primary) === 1);
859
+ const productUid = String(relation?.target_uuid ?? '').trim();
860
+ if (!productUid || allergyByProductUid.has(productUid)) continue;
861
+ allergyByProductUid.set(productUid, content);
862
+ }
863
+ return allergyByProductUid;
864
+ }
573
865
  function resolveBundleProductTitle(params) {
574
866
  const {
575
867
  bundle,
@@ -577,18 +869,18 @@ function resolveBundleProductTitle(params) {
577
869
  locale,
578
870
  parentProduct
579
871
  } = params;
580
- const ownTitle = getProductTitleSource(bundle, locale);
872
+ const ownTitle = getBundleProductTitleSource(bundle, locale);
581
873
  if (ownTitle) return ownTitle;
582
874
  const parentBundleItem = resolveBundleItemFromParentProduct({
583
875
  bundle,
584
876
  parentProduct
585
877
  });
586
- const parentBundleTitle = parentBundleItem ? getProductTitleSource(parentBundleItem, locale) : '';
878
+ const parentBundleTitle = parentBundleItem ? getBundleProductTitleSource(parentBundleItem, locale) : '';
587
879
  if (parentBundleTitle) return parentBundleTitle;
588
880
  const productId = bundle.bundle_product_id ?? bundle._bundle_product_id ?? bundle.product_id ?? bundle.id;
589
881
  const fallbackProduct = resolveProductFromMap(productId, productMap);
590
882
  if (!fallbackProduct) return '';
591
- return getProductTitleSource(fallbackProduct, locale);
883
+ return getBundleProductTitleSource(fallbackProduct, locale);
592
884
  }
593
885
  function resolveProductFromMap(productId, productMap) {
594
886
  if (productId === undefined || productId === null || productId === '') return null;
@@ -660,7 +952,11 @@ function resolveVariantItem(group, itemPositionId) {
660
952
  }
661
953
  function getProductTitleSource(source, locale) {
662
954
  if (!source || typeof source !== 'object') return '';
663
- return resolveTitleValue(source.product_title, locale) || resolveTitleValue(source.title, locale) || resolveTitleValue(source.name, locale);
955
+ return resolveTitleValue(source.product_title, locale) || resolveTitleValue(source.title_i18n, locale) || resolveTitleValue(source.title, locale) || resolveTitleValue(source.name_i18n, locale) || resolveTitleValue(source.name, locale);
956
+ }
957
+ function getBundleProductTitleSource(source, locale) {
958
+ if (!source || typeof source !== 'object') return '';
959
+ return resolveTitleValue(source.bundle_title, locale) || resolveTitleValue(source.trans_title, locale) || resolveTitleValue(source.title_i18n, locale) || resolveTitleValue(source.product_title, locale) || resolveTitleValue(source.title, locale) || resolveTitleValue(source.name, locale);
664
960
  }
665
961
  function resolveTitleValue(value, locale) {
666
962
  if (value === undefined || value === null) return '';
@@ -795,12 +1091,6 @@ function buildFees(params) {
795
1091
  });
796
1092
  const isPriceIncludeTax = firstPresentValue(summary.is_price_include_tax, order.is_price_include_tax, shopInfo.is_price_include_tax);
797
1093
  const roundingAmount = firstPresentValue(summary.rounding_amount, order.rounding_amount);
798
- if (isNonZero(roundingAmount)) {
799
- fees.push({
800
- item: label(locale, 'rounding'),
801
- value: formatMoney(roundingAmount, currencySymbol)
802
- });
803
- }
804
1094
  const payProcessingFee = financials.payProcessingFee;
805
1095
  if (isNonZero(payProcessingFee)) {
806
1096
  fees.push({
@@ -811,12 +1101,14 @@ function buildFees(params) {
811
1101
  const productExpectAmount = firstPresentValue(summary.product_expect_amount, summary.product_amount, order.product_expect_amount);
812
1102
  const productOriginalAmount = firstPresentValue(summary.product_original_amount, order.product_original_amount) ?? calculateProductOriginalAmount(order);
813
1103
  const totalSavingsAmount = new _decimal.default(toMoneyNumber(productOriginalAmount)).minus(toMoneyNumber(productExpectAmount)).plus(toMoneyNumber(discountAmount));
1104
+ const itemDiscountAmount = new _decimal.default(toMoneyNumber(productExpectAmount ?? 0)).minus(toMoneyNumber(productOriginalAmount));
814
1105
  return {
815
1106
  fees,
816
1107
  countFee: {
817
1108
  total_amount: formatMoney(firstPresentValue(summary.total_amount, order.total_amount) ?? 0, currencySymbol),
818
1109
  original_item_amount: formatMoney(productOriginalAmount, currencySymbol),
819
1110
  product_expect_amount: formatMoney(productExpectAmount ?? 0, currencySymbol),
1111
+ item_discount_amount: formatCalculatedMoney(itemDiscountAmount, currencySymbol),
820
1112
  expect_amount: formatMoney(financials.totalAmount, currencySymbol),
821
1113
  product_quantity: toNumber(firstPresentValue(summary.product_quantity, products.reduce((sum, product) => sum + product.product_quantity, 0)), 0),
822
1114
  has_pay: formatMoney(financials.orderPaidAmount, currencySymbol),
@@ -900,15 +1192,13 @@ function normalizeCustomerDisplayValue(value) {
900
1192
  return text;
901
1193
  }
902
1194
  function buildDelivery(order, locale) {
903
- const deliveryType = stringify(order.delivery_type).trim();
904
- const serviceType = deliveryType || stringify(firstPresentValue(order.metadata?.service_type, order.service_type)).trim();
905
- const serviceTypeLabelKey = resolveServiceTypeLabelKey(serviceType, order);
1195
+ const serviceType = stringify(order.metadata?.shop_service_data?.service_type).trim();
1196
+ const serviceTypeLabelKey = resolveServiceTypeLabelKey(serviceType);
906
1197
  if (!serviceTypeLabelKey) return [];
907
1198
  const items = [{
908
- item: '',
909
- value: label(locale, serviceTypeLabelKey),
910
- size: 2,
911
- align: 'left'
1199
+ item: label(locale, serviceTypeLabelKey),
1200
+ value: '',
1201
+ size: 2
912
1202
  }];
913
1203
  const handoverTime = firstPresentValue(order.handover_time, order.handover_at, order.pickup_time, order.pickup_at, order.scheduled_pickup_time, order.delivery_time, order.delivery_at, order.scheduled_delivery_time, order.fulfillment_time, order.fulfillment_at, order.metadata?.handover_time, order.metadata?.handover_at, order.metadata?.pickup_time, order.metadata?.pickup_at, order.metadata?.scheduled_pickup_time, order.metadata?.delivery_time, order.metadata?.delivery_at, order.metadata?.scheduled_delivery_time, order.metadata?.fulfillment_time, order.metadata?.fulfillment_at);
914
1204
  if (isPresentId(handoverTime)) {
@@ -920,26 +1210,16 @@ function buildDelivery(order, locale) {
920
1210
  return items;
921
1211
  }
922
1212
  function buildAppointment(order, locale) {
923
- const bookings = sortBookings(normalizeArray(order.bookings));
1213
+ const parentScheduleId = order.metadata?.appointment_booking_parent_schedule_id;
1214
+ const bookings = sortBookings(normalizeArray(order.bookings).filter(booking => isReceiptAppointmentBooking(booking, parentScheduleId)));
924
1215
  if (bookings.length) {
925
- return bookings.map(booking => {
926
- const snapshot = {
927
- ...booking
928
- };
929
- delete snapshot.product_uid;
930
- delete snapshot.booking_uid;
931
- const productUid = firstPresentValue(booking.product_uid, booking.metadata?.product_uid);
932
- const bookingUid = firstPresentValue(booking.metadata?.unique_identification_number, booking.booking_uid);
933
- return {
934
- ...snapshot,
935
- ...(isPresentId(productUid) ? {
936
- product_uid: productUid
937
- } : {}),
938
- ...(isPresentId(bookingUid) ? {
939
- booking_uid: bookingUid
940
- } : {})
941
- };
942
- });
1216
+ const allResources = normalizeArray(order.resources || order.resource_list);
1217
+ return bookings.flatMap(booking => buildAppointmentRows({
1218
+ booking,
1219
+ allResources,
1220
+ bookingCount: bookings.length,
1221
+ locale
1222
+ }));
943
1223
  }
944
1224
  const tableName = order.table_name || order.metadata?.table_name || order.metadata?.table?.name || order.form_data?.table_name;
945
1225
  if (!tableName) return [];
@@ -948,6 +1228,95 @@ function buildAppointment(order, locale) {
948
1228
  value: getLocalizedValue(tableName, locale)
949
1229
  }];
950
1230
  }
1231
+ function isReceiptAppointmentBooking(booking, parentScheduleId) {
1232
+ if (!booking || typeof booking !== 'object' || Array.isArray(booking)) {
1233
+ return false;
1234
+ }
1235
+ if (isPresentId(parentScheduleId) && isSameId(booking.schedule_event_id, parentScheduleId)) {
1236
+ return false;
1237
+ }
1238
+ const parentId = booking.parent_id;
1239
+ if (parentId === undefined || parentId === null || parentId === '') {
1240
+ return true;
1241
+ }
1242
+ return Number(parentId) !== 0;
1243
+ }
1244
+ function buildAppointmentRows(params) {
1245
+ const {
1246
+ booking,
1247
+ allResources,
1248
+ bookingCount,
1249
+ locale
1250
+ } = params;
1251
+ const productUid = stringify(firstPresentValue(booking.product_uid, booking.metadata?.product_uid)).trim();
1252
+ const bookingUid = stringify(firstPresentValue(booking.metadata?.unique_identification_number, booking.booking_uid)).trim();
1253
+ const association = {
1254
+ product_uid: productUid,
1255
+ booking_uid: bookingUid
1256
+ };
1257
+ const bookingId = stringify(firstPresentValue(booking.booking_id, booking.schedule_event_id, bookingUid)).trim();
1258
+ const bookingTime = formatBookingRange(booking, locale);
1259
+ const resourceText = formatBookingResources(resolveBookingResourcesForReceipt(booking, allResources, bookingCount), locale);
1260
+ const holder = getLocalizedValue(booking.holder?.name ?? booking.metadata?.holder?.name, locale).trim();
1261
+ return [{
1262
+ item: label(locale, 'bookingId'),
1263
+ value: bookingId,
1264
+ ...association
1265
+ }, {
1266
+ item: label(locale, 'bookingDateTime'),
1267
+ value: bookingTime,
1268
+ ...association
1269
+ }, {
1270
+ item: label(locale, 'bookingResource'),
1271
+ value: resourceText,
1272
+ ...association
1273
+ }, {
1274
+ item: label(locale, 'bookingHolder'),
1275
+ value: holder,
1276
+ ...association
1277
+ }].filter(row => Boolean(row.value));
1278
+ }
1279
+ function formatBookingRange(booking, locale) {
1280
+ const startDate = stringify(booking.start_date);
1281
+ const startTime = stringify(booking.start_time);
1282
+ const endDate = stringify(booking.end_date || startDate);
1283
+ const endTime = stringify(booking.end_time);
1284
+ const start = firstValidBookingDate(booking.start_at, booking.start_datetime, booking.start_date_time, `${startDate} ${startTime}`.trim());
1285
+ const end = firstValidBookingDate(booking.end_at, booking.end_datetime, booking.end_date_time, endTime ? `${endDate} ${endTime}`.trim() : '');
1286
+ if (!start) {
1287
+ return [startDate, startTime, endTime ? `- ${endDate} ${endTime}` : ''].filter(Boolean).join(' ');
1288
+ }
1289
+ const dateFormat = locale === 'en' ? 'DD/MM/YYYY' : 'YYYY/MM/DD';
1290
+ const startText = start.format(`${dateFormat} HH:mm`);
1291
+ if (!end) return startText;
1292
+ return `${startText} - ${end.isSame(start, 'day') ? end.format('HH:mm') : end.format(`${dateFormat} HH:mm`)}`;
1293
+ }
1294
+ function firstValidBookingDate(...values) {
1295
+ for (const value of values) {
1296
+ if (!isPresentId(value)) continue;
1297
+ const parsed = (0, _dayjs.default)(value);
1298
+ if (parsed.isValid()) return parsed;
1299
+ }
1300
+ return null;
1301
+ }
1302
+ function resolveBookingResourcesForReceipt(booking, allResources, bookingCount) {
1303
+ const embedded = normalizeArray(booking.resources).filter(resource => Boolean(resource) && typeof resource === 'object' && !Array.isArray(resource));
1304
+ const resourceIds = new Set([booking.resource_id, booking.relation_id, ...normalizeArray(booking.resource_ids), ...normalizeArray(booking.metadata?.resource_ids)].map(value => stringify(value).trim()).filter(Boolean));
1305
+ for (const resource of embedded) {
1306
+ const id = stringify(resource.id ?? resource.resource_id ?? resource.relation_id).trim();
1307
+ if (id) resourceIds.add(id);
1308
+ }
1309
+ if (resourceIds.size) {
1310
+ const matched = allResources.filter(resource => resourceIds.has(stringify(resource.id ?? resource.resource_id ?? resource.relation_id).trim()));
1311
+ if (matched.length) return matched;
1312
+ }
1313
+ if (embedded.length) return embedded;
1314
+ if (bookingCount === 1 && allResources.length) return allResources;
1315
+ return [];
1316
+ }
1317
+ function formatBookingResources(resources, locale) {
1318
+ return resources.map(resource => [getLocalizedValue(resource.form_title || resource.metadata?.form_name || resource.relation_form_name || resource.metadata?.relation_form_name, locale), getLocalizedValue(resource.title || resource.name || resource.main_field || resource.metadata?.main_field || resource.metadata?.resource_name, locale)].filter(Boolean).join(' ')).filter(Boolean).join(', ');
1319
+ }
951
1320
  function sortBookings(bookings) {
952
1321
  return bookings.map((booking, index) => ({
953
1322
  booking,
@@ -999,7 +1368,9 @@ function buildPaymentData(params) {
999
1368
  payment,
1000
1369
  locale: params.locale,
1001
1370
  currencySymbol: params.currencySymbol,
1002
- includeFundRecordBalance: false
1371
+ includeFundRecordBalance: false,
1372
+ includeFundRecordUsage: true,
1373
+ includePaymentAmount: true
1003
1374
  }));
1004
1375
  }
1005
1376
  function buildRefundData(params) {
@@ -1083,28 +1454,454 @@ function resolveRefundPayment(refund, payments) {
1083
1454
  return matchedPayment || refund;
1084
1455
  }
1085
1456
  function buildWallets(params) {
1086
- const paymentWallets = normalizeArray(params.order.payments).filter(payment => isPrintablePayment(payment) && !!getFundRecord(payment)).map(payment => buildPaymentItems({
1087
- payment,
1457
+ const virtualCurrencySnapshots = collectVirtualCurrencySnapshots(params.order);
1458
+ const displayedWalletPayments = normalizeArray(params.order.payments).filter(payment => isPrintablePayment(payment) && !!getFundRecord(payment));
1459
+ const paymentWallets = displayedWalletPayments.map(payment => {
1460
+ const virtualCurrencySnapshot = findVirtualCurrencySnapshotForPayment(payment, virtualCurrencySnapshots.byId);
1461
+ return buildPaymentItems({
1462
+ payment,
1463
+ locale: params.locale,
1464
+ currencySymbol: params.currencySymbol,
1465
+ includeFundRecordBalance: true,
1466
+ includeFundRecordUsage: false,
1467
+ includePaymentAmount: false,
1468
+ virtualCurrencyUnit: virtualCurrencySnapshot ? getVirtualCurrencySnapshotUnit(virtualCurrencySnapshot, params.locale) : undefined
1469
+ });
1470
+ });
1471
+ const displayedWalletAssetIdentities = collectDisplayedWalletAssetIdentities(displayedWalletPayments);
1472
+ const voucherWallets = buildVoucherAssets({
1473
+ ...params,
1474
+ displayedWalletAssetIdentities
1475
+ });
1476
+ const productDiscountWallets = buildProductDiscountAssets({
1477
+ ...params,
1478
+ displayedWalletAssetIdentities
1479
+ });
1480
+ return [...paymentWallets, ...voucherWallets, ...productDiscountWallets, ...buildPointCardSnapshotAssets({
1481
+ order: params.order,
1088
1482
  locale: params.locale,
1089
- currencySymbol: params.currencySymbol,
1090
- includeFundRecordBalance: true
1091
- }));
1092
- return [...paymentWallets, ...buildVoucherAssets(params)];
1483
+ displayedWalletAssetIdentities
1484
+ }), ...buildVirtualCurrencySnapshotAssets({
1485
+ snapshots: virtualCurrencySnapshots.items,
1486
+ order: params.order,
1487
+ locale: params.locale,
1488
+ displayedWalletAssetIdentities
1489
+ })];
1490
+ }
1491
+ function collectDisplayedWalletAssetIdentities(payments) {
1492
+ const identities = [];
1493
+ payments.forEach(payment => {
1494
+ const fundRecord = getFundRecord(payment);
1495
+ const ids = Array.from(new Set([fundRecord?.voucher_id, payment.voucher_id].map(value => normalizeWalletPassId(value)).filter(value => !!value)));
1496
+ const codes = normalizeWalletAssetCodes(fundRecord?.code, fundRecord?.encoded);
1497
+ if (!ids.length) {
1498
+ registerWalletAssetIdentity(identities, {
1499
+ id: null,
1500
+ codes
1501
+ });
1502
+ return;
1503
+ }
1504
+ ids.forEach(id => {
1505
+ registerWalletAssetIdentity(identities, {
1506
+ id,
1507
+ codes
1508
+ });
1509
+ });
1510
+ });
1511
+ return identities;
1512
+ }
1513
+ const PRODUCT_WALLET_DISCOUNT_TYPES = new Set(['good_pass', 'discount_card', 'product_discount_card', 'entitlement_pass']);
1514
+ function buildProductDiscountAssets(params) {
1515
+ const fallbackAssetTime = firstPresentValue(params.order.updated_at, params.order.created_at);
1516
+ return collectProductWalletDiscountAssets(params.order).flatMap(asset => {
1517
+ if (!claimWalletAssetIdentity(params.displayedWalletAssetIdentities, {
1518
+ id: asset.id,
1519
+ codes: asset.codes
1520
+ })) return [];
1521
+ const name = getProductWalletDiscountName(asset, params.locale);
1522
+ const assetTime = getProductWalletDiscountTime(asset) ?? fallbackAssetTime;
1523
+ const timeRow = isPresentId(assetTime) ? {
1524
+ item: formatDateTime(assetTime, params.locale),
1525
+ value: ''
1526
+ } : null;
1527
+ if (asset.type === 'entitlement_pass') {
1528
+ const remaining = getProductEntitlementBalance(asset);
1529
+ return [[{
1530
+ item: name,
1531
+ value: ''
1532
+ }, ...(remaining ? [{
1533
+ item: label(params.locale, 'entitlementRemaining'),
1534
+ value: formatEntitlementUses(remaining, params.locale)
1535
+ }] : []), ...(timeRow ? [timeRow] : [])]];
1536
+ }
1537
+ const percent = getProductWalletDiscountPercent(asset);
1538
+ if (percent) {
1539
+ return [[{
1540
+ item: appendPercentToVoucherName(name, percent),
1541
+ value: formatVoucherDiscount(percent, params.locale)
1542
+ }, ...(timeRow ? [timeRow] : [])]];
1543
+ }
1544
+ const balance = getProductWalletMoneyBalance(asset);
1545
+ return [[{
1546
+ item: name,
1547
+ value: ''
1548
+ }, ...(balance ? [{
1549
+ item: label(params.locale, 'balance'),
1550
+ value: formatMoney(balance, params.currencySymbol)
1551
+ }] : []), ...(timeRow ? [timeRow] : [])]];
1552
+ });
1553
+ }
1554
+ function collectProductWalletDiscountAssets(order) {
1555
+ const assets = [];
1556
+ const assetsById = new Map();
1557
+ normalizeArray(order.products).forEach(rawProduct => {
1558
+ if (!rawProduct || typeof rawProduct !== 'object') return;
1559
+ visitProductWalletDiscounts(restoreReceiptDiscountHierarchy(rawProduct), assets, assetsById);
1560
+ });
1561
+ return assets;
1562
+ }
1563
+ function visitProductWalletDiscounts(product, assets, assetsById) {
1564
+ normalizeArray(product.discount_list).forEach(record => {
1565
+ if (!record || typeof record !== 'object' || Array.isArray(record)) return;
1566
+ const type = getProductWalletDiscountType(record);
1567
+ if (!type) return;
1568
+ const id = getProductWalletDiscountAssetId(record);
1569
+ if (!id) return;
1570
+ const codes = getProductWalletDiscountCodes(record);
1571
+ const existing = assetsById.get(id);
1572
+ if (existing) {
1573
+ existing.records.push(record);
1574
+ existing.codes = Array.from(new Set([...existing.codes, ...codes]));
1575
+ return;
1576
+ }
1577
+ const asset = {
1578
+ id,
1579
+ type,
1580
+ codes,
1581
+ records: [record]
1582
+ };
1583
+ assetsById.set(id, asset);
1584
+ assets.push(asset);
1585
+ });
1586
+ for (const childKey of ['combinations', 'product_bundle', 'bundle']) {
1587
+ normalizeArray(product[childKey]).forEach(child => {
1588
+ if (!child || typeof child !== 'object' || Array.isArray(child)) return;
1589
+ visitProductWalletDiscounts(child, assets, assetsById);
1590
+ });
1591
+ }
1592
+ }
1593
+ function getProductWalletDiscountType(record) {
1594
+ for (const value of [record.tag, record.type, record.discount?.tag, record.discount?.type]) {
1595
+ const type = stringify(value).trim().toLowerCase();
1596
+ if (PRODUCT_WALLET_DISCOUNT_TYPES.has(type)) return type;
1597
+ }
1598
+ return null;
1599
+ }
1600
+ function getProductWalletDiscountAssetId(record) {
1601
+ for (const value of [record.discount?.resource_id, record.resource_id, record.id, record.discount_id]) {
1602
+ const id = normalizeWalletPassId(value);
1603
+ if (id && id !== '0') return id;
1604
+ }
1605
+ return null;
1606
+ }
1607
+ function getProductWalletDiscountCodes(record) {
1608
+ return normalizeWalletAssetCodes(record.code, record.encoded, record.discount?.code, record.discount?.encoded);
1609
+ }
1610
+ function getProductWalletDiscountName(asset, locale) {
1611
+ for (const record of asset.records) {
1612
+ for (const value of [record.discount?.title, record.discount?.name, record.discount?.message, record.product_name, record.format_title, record.title, record.name, record.message]) {
1613
+ const name = getLocalizedValue(value, locale).trim();
1614
+ if (name) return name;
1615
+ }
1616
+ }
1617
+ return asset.codes[0] || label(locale, 'giftCard');
1618
+ }
1619
+ function getProductWalletDiscountTime(asset) {
1620
+ for (const field of ['updated_at', 'created_at', 'collection_time']) {
1621
+ for (const record of asset.records) {
1622
+ const value = firstPresentValue(record[field], record.discount?.[field]);
1623
+ if (isPresentId(value)) return value;
1624
+ }
1625
+ }
1626
+ return undefined;
1627
+ }
1628
+ function getProductEntitlementBalance(asset) {
1629
+ for (const record of asset.records) {
1630
+ const balance = getProductDiscountEntitlementBalance(record);
1631
+ if (balance) return balance;
1632
+ }
1633
+ return null;
1634
+ }
1635
+ function getProductDiscountEntitlementBalance(record) {
1636
+ return getEntitlementBalance(record.metadata?.balance, record.balance, record.discount?.metadata?.balance, record.discount?.balance);
1637
+ }
1638
+ function getEntitlementBalance(...values) {
1639
+ for (const value of values) {
1640
+ if (!isNumericAmount(value)) continue;
1641
+ const balance = new _decimal.default(value);
1642
+ if (balance.isFinite() && balance.isInteger() && balance.gte(0)) {
1643
+ return balance;
1644
+ }
1645
+ }
1646
+ return null;
1647
+ }
1648
+ function formatEntitlementUses(value, locale) {
1649
+ const count = value.toFixed(0);
1650
+ switch (normalizeLocaleKey(locale)) {
1651
+ case 'zh_CN':
1652
+ case 'zh_HK':
1653
+ return `${count}次`;
1654
+ case 'ja':
1655
+ return `${count}回`;
1656
+ case 'pt':
1657
+ return `${count} ${value.eq(1) ? 'utilização' : 'utilizações'}`;
1658
+ default:
1659
+ return `${count} ${value.eq(1) ? 'use' : 'uses'}`;
1660
+ }
1661
+ }
1662
+ function getProductWalletDiscountPercent(asset) {
1663
+ if (asset.type !== 'discount_card' && asset.type !== 'product_discount_card') return null;
1664
+ for (const record of asset.records) {
1665
+ const discountType = stringify(firstPresentValue(record.discount_card_type, record.metadata?.discount_card_type, record.discount?.discount_card_type, record.discount?.metadata?.discount_card_type)).trim().toLowerCase();
1666
+ if (discountType && discountType !== 'percent' && discountType !== 'percentage') continue;
1667
+ const candidates = [record.percent, record.discount?.percent];
1668
+ if (discountType === 'percent' || discountType === 'percentage') {
1669
+ candidates.push(record.par_value, record.discount?.par_value);
1670
+ }
1671
+ for (const value of candidates) {
1672
+ if (!isNumericAmount(value)) continue;
1673
+ const percent = new _decimal.default(value);
1674
+ if (percent.isFinite() && percent.gt(0) && percent.lte(100)) {
1675
+ return percent;
1676
+ }
1677
+ }
1678
+ }
1679
+ return null;
1680
+ }
1681
+ function getProductWalletMoneyBalance(asset) {
1682
+ for (const record of asset.records) {
1683
+ for (const value of [record.metadata?.balance, record.balance, record.discount?.metadata?.balance, record.discount?.balance]) {
1684
+ if (!isNumericAmount(value)) continue;
1685
+ return _decimal.default.max(new _decimal.default(value), 0);
1686
+ }
1687
+ }
1688
+ for (const record of asset.records) {
1689
+ const parValue = firstPresentValue(record.par_value, record.discount?.par_value);
1690
+ const usedParValue = firstPresentValue(record.used_par_value, record.discount?.used_par_value);
1691
+ if (!isNumericAmount(parValue) || !isNumericAmount(usedParValue)) continue;
1692
+ return _decimal.default.max(new _decimal.default(parValue).minus(new _decimal.default(usedParValue)), 0);
1693
+ }
1694
+ return null;
1695
+ }
1696
+ function buildPointCardSnapshotAssets(params) {
1697
+ const snapshots = params.order.order_meta_list?.point_cards_snapshot;
1698
+ if (!Array.isArray(snapshots)) return [];
1699
+ const snapshotTime = firstPresentValue(params.order.updated_at, params.order.created_at);
1700
+ return snapshots.flatMap(snapshot => {
1701
+ if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) return [];
1702
+ const pointCard = snapshot;
1703
+ if (stringify(pointCard.tag).trim().toLowerCase() !== 'point_card') return [];
1704
+ const assetId = normalizeWalletPassId(pointCard.id);
1705
+ if (!assetId || !claimWalletAssetIdentity(params.displayedWalletAssetIdentities, {
1706
+ id: assetId,
1707
+ codes: []
1708
+ })) return [];
1709
+ const rows = [{
1710
+ item: appendMaskedCardCode(stringify(pointCard.product_name).trim() || label(params.locale, 'pointCard'), pointCard.code),
1711
+ value: ''
1712
+ }];
1713
+ const remainingPoints = getPointCardSnapshotRemainingPoints(pointCard);
1714
+ if (remainingPoints) {
1715
+ rows.push({
1716
+ item: label(params.locale, 'remainingPoints'),
1717
+ value: trimDecimal(remainingPoints)
1718
+ });
1719
+ }
1720
+ if (isPresentId(snapshotTime)) {
1721
+ rows.push({
1722
+ item: formatDateTime(snapshotTime, params.locale),
1723
+ value: ''
1724
+ });
1725
+ }
1726
+ return [rows];
1727
+ });
1728
+ }
1729
+ function collectVirtualCurrencySnapshots(order) {
1730
+ const items = [];
1731
+ const byId = new Map();
1732
+ const snapshots = order.order_meta_list?.virtual_currency_snapshot;
1733
+ if (!Array.isArray(snapshots)) return {
1734
+ items,
1735
+ byId
1736
+ };
1737
+ snapshots.forEach(snapshot => {
1738
+ if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) return;
1739
+ const virtualCurrency = snapshot;
1740
+ if (stringify(virtualCurrency.tag).trim().toLowerCase() !== 'virtual_currency') return;
1741
+ const id = normalizeWalletPassId(virtualCurrency.id);
1742
+ if (!id || byId.has(id)) return;
1743
+ byId.set(id, virtualCurrency);
1744
+ items.push(virtualCurrency);
1745
+ });
1746
+ return {
1747
+ items,
1748
+ byId
1749
+ };
1750
+ }
1751
+ function findVirtualCurrencySnapshotForPayment(payment, snapshotsById) {
1752
+ const fundRecord = getFundRecord(payment);
1753
+ for (const value of [fundRecord?.voucher_id, payment.voucher_id]) {
1754
+ const id = normalizeWalletPassId(value);
1755
+ if (!id) continue;
1756
+ const snapshot = snapshotsById.get(id);
1757
+ if (snapshot) return snapshot;
1758
+ }
1759
+ return undefined;
1760
+ }
1761
+ function buildVirtualCurrencySnapshotAssets(params) {
1762
+ const snapshotTime = firstPresentValue(params.order.updated_at, params.order.created_at);
1763
+ return params.snapshots.flatMap(virtualCurrency => {
1764
+ const assetId = normalizeWalletPassId(virtualCurrency.id);
1765
+ if (!assetId || !claimWalletAssetIdentity(params.displayedWalletAssetIdentities, {
1766
+ id: assetId,
1767
+ codes: []
1768
+ })) return [];
1769
+ const rows = [{
1770
+ item: stringify(virtualCurrency.product_name).trim(),
1771
+ value: ''
1772
+ }];
1773
+ const balance = getVirtualCurrencySnapshotBalance(virtualCurrency);
1774
+ if (balance) {
1775
+ rows.push({
1776
+ item: label(params.locale, 'balance'),
1777
+ value: formatVirtualCurrencyBalance(balance, getVirtualCurrencySnapshotUnit(virtualCurrency, params.locale))
1778
+ });
1779
+ }
1780
+ if (isPresentId(snapshotTime)) {
1781
+ rows.push({
1782
+ item: formatDateTime(snapshotTime, params.locale),
1783
+ value: ''
1784
+ });
1785
+ }
1786
+ return [rows];
1787
+ });
1788
+ }
1789
+ function getVirtualCurrencySnapshotBalance(snapshot) {
1790
+ if (!isNumericAmount(snapshot.par_value) || !isNumericAmount(snapshot.used_par_value)) {
1791
+ return null;
1792
+ }
1793
+ return _decimal.default.max(new _decimal.default(snapshot.par_value).minus(new _decimal.default(snapshot.used_par_value)), 0);
1794
+ }
1795
+ function getVirtualCurrencySnapshotUnit(snapshot, locale) {
1796
+ return getNonEmptyLocalizedValue(snapshot.name, locale);
1797
+ }
1798
+ function getNonEmptyLocalizedValue(value, locale) {
1799
+ if (value === undefined || value === null) return '';
1800
+ if (typeof value !== 'object' || Array.isArray(value)) return stringify(value).trim();
1801
+ const dashedLocale = locale.replace('_', '-');
1802
+ const underscoredLocale = locale.replace('-', '_');
1803
+ for (const candidate of [value[locale], value[dashedLocale], value[underscoredLocale], value.original, value.auto, value.default, value.en]) {
1804
+ const text = stringify(candidate).trim();
1805
+ if (text) return text;
1806
+ }
1807
+ return '';
1808
+ }
1809
+ function formatVirtualCurrencyBalance(value, unit) {
1810
+ const amount = trimDecimal(value);
1811
+ const normalizedUnit = unit.trim();
1812
+ return normalizedUnit ? `${amount} ${normalizedUnit}` : amount;
1813
+ }
1814
+ function normalizeWalletPassId(value) {
1815
+ if (typeof value === 'number') return Number.isFinite(value) ? String(value) : null;
1816
+ if (typeof value !== 'string') return null;
1817
+ return value.trim() || null;
1818
+ }
1819
+ function firstNormalizedWalletAssetId(...values) {
1820
+ for (const value of values) {
1821
+ const id = normalizeWalletPassId(value);
1822
+ if (id) return id;
1823
+ }
1824
+ return null;
1825
+ }
1826
+ function normalizeWalletAssetCodes(...values) {
1827
+ return Array.from(new Set(values.map(value => normalizeWalletPassId(value)).filter(value => !!value)));
1828
+ }
1829
+ function registerWalletAssetIdentity(identities, candidate) {
1830
+ if (!candidate.id && !candidate.codes.length) return;
1831
+ const existing = identities.find(identity => isSameWalletAssetIdentity(identity, candidate));
1832
+ if (existing) {
1833
+ mergeWalletAssetIdentity(existing, candidate);
1834
+ return;
1835
+ }
1836
+ identities.push({
1837
+ id: candidate.id,
1838
+ codes: [...candidate.codes]
1839
+ });
1840
+ }
1841
+ function claimWalletAssetIdentity(identities, candidate) {
1842
+ if (!candidate.id && !candidate.codes.length) return true;
1843
+ const existing = identities.find(identity => isSameWalletAssetIdentity(identity, candidate));
1844
+ if (existing) {
1845
+ mergeWalletAssetIdentity(existing, candidate);
1846
+ return false;
1847
+ }
1848
+ identities.push({
1849
+ id: candidate.id,
1850
+ codes: [...candidate.codes]
1851
+ });
1852
+ return true;
1853
+ }
1854
+ function isSameWalletAssetIdentity(left, right) {
1855
+ if (left.id && right.id) return left.id === right.id;
1856
+ return left.codes.some(code => right.codes.includes(code));
1857
+ }
1858
+ function mergeWalletAssetIdentity(target, source) {
1859
+ if (!target.id && source.id) target.id = source.id;
1860
+ target.codes = Array.from(new Set([...target.codes, ...source.codes]));
1861
+ }
1862
+ function getPointCardSnapshotRemainingPoints(snapshot) {
1863
+ if (!isNumericAmount(snapshot.par_value) || !isNumericAmount(snapshot.used_par_value)) {
1864
+ return null;
1865
+ }
1866
+ return _decimal.default.max(new _decimal.default(snapshot.par_value).minus(new _decimal.default(snapshot.used_par_value)), 0);
1093
1867
  }
1094
1868
  function buildVoucherAssets(params) {
1095
- const seenVoucherKeys = new Set();
1096
- return normalizeArray(params.order.vouchers).flatMap((voucher, index) => {
1869
+ const fallbackAssetTime = firstPresentValue(params.order.updated_at, params.order.created_at);
1870
+ return normalizeArray(params.order.vouchers).flatMap(voucher => {
1097
1871
  if (!voucher || typeof voucher !== 'object' || Array.isArray(voucher)) return [];
1098
- const voucherKey = getVoucherAssetKey(voucher, index);
1099
- if (seenVoucherKeys.has(voucherKey)) return [];
1100
- seenVoucherKeys.add(voucherKey);
1872
+ if (!claimWalletAssetIdentity(params.displayedWalletAssetIdentities, getVoucherAssetIdentity(voucher))) return [];
1101
1873
  const name = getVoucherAssetName(voucher, params.locale);
1874
+ const assetTime = firstPresentValue(voucher.updated_at, voucher.created_at, voucher.collection_time, fallbackAssetTime);
1875
+ const timeRow = isPresentId(assetTime) ? {
1876
+ item: formatDateTime(assetTime, params.locale),
1877
+ value: ''
1878
+ } : null;
1879
+ if (isEntitlementVoucher(voucher)) {
1880
+ const remaining = getEntitlementBalance(voucher.metadata?.balance, voucher.balance);
1881
+ return [[{
1882
+ item: name,
1883
+ value: ''
1884
+ }, ...(remaining ? [{
1885
+ item: label(params.locale, 'entitlementRemaining'),
1886
+ value: formatEntitlementUses(remaining, params.locale)
1887
+ }] : []), ...(timeRow ? [timeRow] : [])]];
1888
+ }
1889
+ if (isTicketVoucher(voucher)) {
1890
+ const validity = getVoucherValidity(voucher, params.locale);
1891
+ return [[{
1892
+ item: name,
1893
+ value: ''
1894
+ }, ...(validity ? [{
1895
+ item: label(params.locale, 'validity'),
1896
+ value: validity
1897
+ }] : [])]];
1898
+ }
1102
1899
  const percent = getVoucherDiscountPercent(voucher);
1103
1900
  if (percent) {
1104
1901
  return [[{
1105
1902
  item: appendPercentToVoucherName(name, percent),
1106
1903
  value: formatVoucherDiscount(percent, params.locale)
1107
- }]];
1904
+ }, ...(timeRow ? [timeRow] : [])]];
1108
1905
  }
1109
1906
  const balance = getVoucherBalance(voucher);
1110
1907
  return [[{
@@ -1113,12 +1910,92 @@ function buildVoucherAssets(params) {
1113
1910
  }, {
1114
1911
  item: label(params.locale, 'balance'),
1115
1912
  value: formatMoney(balance, params.currencySymbol)
1116
- }]];
1913
+ }, ...(timeRow ? [timeRow] : [])]];
1914
+ });
1915
+ }
1916
+ const TICKET_VOUCHER_TAGS = new Set(['ticket', 'product_package_ticket', 'appointment_ticket', 'session_ticket']);
1917
+ function isEntitlementVoucher(voucher) {
1918
+ return [voucher.tag, voucher.type].some(value => stringify(value).trim().toLowerCase() === 'entitlement_pass');
1919
+ }
1920
+ function isTicketVoucher(voucher) {
1921
+ return TICKET_VOUCHER_TAGS.has(stringify(voucher.tag).trim().toLowerCase());
1922
+ }
1923
+ function getVoucherValidity(voucher, locale) {
1924
+ const availableTime = firstPresentValue(voucher.available_time, voucher.avaliable_time, voucher.metadata?.available_time, voucher.metadata?.avaliable_time);
1925
+ const availableTimeText = formatVoucherAvailableTime(availableTime, locale);
1926
+ if (availableTimeText) return availableTimeText;
1927
+ const startDate = firstPresentValue(voucher.valid_from, voucher.start_at, voucher.start_date_time, voucher.start_date, voucher.metadata?.valid_from, voucher.metadata?.start_at, voucher.metadata?.start_date);
1928
+ const endDate = firstPresentValue(voucher.valid_to, voucher.end_at, voucher.end_date_time, voucher.expire_date, voucher.end_date, voucher.expire_time, voucher.metadata?.valid_to, voucher.metadata?.end_at, voucher.metadata?.expire_date, voucher.metadata?.end_date, voucher.metadata?.expire_time);
1929
+ return formatVoucherValidityRange(startDate, endDate, locale, {
1930
+ startTime: voucher.start_time ?? voucher.metadata?.start_time,
1931
+ endTime: voucher.end_time ?? voucher.metadata?.end_time
1117
1932
  });
1118
1933
  }
1119
- function getVoucherAssetKey(voucher, index) {
1120
- const identity = firstPresentValue(voucher.voucher_id, voucher.id, voucher.code, voucher.encoded);
1121
- return identity === undefined ? `voucher-index:${index}` : `voucher:${String(identity)}`;
1934
+ function formatVoucherAvailableTime(value, locale) {
1935
+ if (!isPresentId(value)) return '';
1936
+ if (Array.isArray(value)) {
1937
+ return value.map(item => formatVoucherAvailableTime(item, locale)).filter(Boolean).join(', ');
1938
+ }
1939
+ if (typeof value !== 'object') return stringify(value).trim();
1940
+ const start = firstPresentValue(value.start, value.start_at, value.start_date_time, value.start_date);
1941
+ const end = firstPresentValue(value.end, value.end_at, value.end_date_time, value.expire_time, value.expire_date, value.end_date);
1942
+ if (isPresentId(start) || isPresentId(end)) {
1943
+ return formatVoucherValidityRange(start, end, locale, {
1944
+ startTime: value.start_time,
1945
+ endTime: value.end_time
1946
+ });
1947
+ }
1948
+ return getLocalizedValue(value, locale).trim();
1949
+ }
1950
+ function formatVoucherValidityRange(startValue, endValue, locale, times = {}) {
1951
+ const start = combineVoucherDateAndTime(startValue, times.startTime);
1952
+ const end = combineVoucherDateAndTime(endValue, times.endTime);
1953
+ const startPart = formatVoucherValidityDate(start, locale);
1954
+ const endPart = formatVoucherValidityDate(end, locale);
1955
+ if (startPart.text && endPart.text) {
1956
+ const parsedStart = (0, _dayjs.default)(start);
1957
+ const parsedEnd = (0, _dayjs.default)(end);
1958
+ const sameDay = parsedStart.isValid() && parsedEnd.isValid() && parsedStart.isSame(parsedEnd, 'day');
1959
+ if (sameDay && startPart.time && endPart.time) {
1960
+ return `${startPart.time} - ${endPart.time} ${startPart.date}`;
1961
+ }
1962
+ return `${startPart.text} - ${endPart.text}`;
1963
+ }
1964
+ return startPart.text || endPart.text;
1965
+ }
1966
+ function combineVoucherDateAndTime(dateValue, timeValue) {
1967
+ if (!isPresentId(dateValue) || !isPresentId(timeValue)) return dateValue;
1968
+ const dateText = stringify(dateValue).trim();
1969
+ if (/\d{1,2}:\d{2}/.test(dateText)) return dateValue;
1970
+ return `${dateText} ${stringify(timeValue).trim()}`;
1971
+ }
1972
+ function formatVoucherValidityDate(value, locale) {
1973
+ if (!isPresentId(value)) return {
1974
+ date: '',
1975
+ time: '',
1976
+ text: ''
1977
+ };
1978
+ const source = stringify(value).trim();
1979
+ const parsed = (0, _dayjs.default)(source);
1980
+ if (!parsed.isValid()) return {
1981
+ date: '',
1982
+ time: '',
1983
+ text: source
1984
+ };
1985
+ const hasTime = /\d{1,2}:\d{2}/.test(source);
1986
+ const date = parsed.format(locale === 'en' ? 'DD/MM/YYYY' : 'YYYY/MM/DD');
1987
+ const time = hasTime ? parsed.format(/\d{1,2}:\d{2}:\d{2}/.test(source) ? 'HH:mm:ss' : 'HH:mm') : '';
1988
+ return {
1989
+ date,
1990
+ time,
1991
+ text: time ? `${time} ${date}` : date
1992
+ };
1993
+ }
1994
+ function getVoucherAssetIdentity(voucher) {
1995
+ return {
1996
+ id: firstNormalizedWalletAssetId(voucher.voucher_id, voucher.id),
1997
+ codes: normalizeWalletAssetCodes(voucher.code, voucher.encoded)
1998
+ };
1122
1999
  }
1123
2000
  function getVoucherAssetName(voucher, locale) {
1124
2001
  return getLocalizedValue(voucher.product_name || voucher.name || voucher.title, locale) || stringify(voucher.encoded || voucher.code || label(locale, 'giftCard'));
@@ -1144,6 +2021,10 @@ function formatVoucherDiscount(percent, locale) {
1144
2021
  return normalizedLocale === 'en' ? `Discount ${trimDecimal(percent)}% OFF` : `${trimDecimal(percent)}% OFF`;
1145
2022
  }
1146
2023
  function getVoucherBalance(voucher) {
2024
+ for (const value of [voucher.metadata?.balance, voucher.balance]) {
2025
+ if (!isNumericAmount(value)) continue;
2026
+ return _decimal.default.max(new _decimal.default(value), 0);
2027
+ }
1147
2028
  const parValue = new _decimal.default(toMoneyNumber(voucher.par_value));
1148
2029
  const usedParValue = new _decimal.default(toMoneyNumber(voucher.used_par_value));
1149
2030
  return _decimal.default.max(parValue.minus(usedParValue), 0);
@@ -1153,12 +2034,15 @@ function buildPaymentItems(params) {
1153
2034
  payment,
1154
2035
  locale,
1155
2036
  currencySymbol,
1156
- includeFundRecordBalance
2037
+ includeFundRecordBalance,
2038
+ includeFundRecordUsage,
2039
+ includePaymentAmount,
2040
+ virtualCurrencyUnit
1157
2041
  } = params;
1158
2042
  const serviceFee = getPaymentServiceFee(payment);
1159
2043
  const items = [{
1160
2044
  item: paymentMethodName(payment, locale),
1161
- value: formatMoney(resolveReceiptPaymentAmount(payment, serviceFee), currencySymbol)
2045
+ value: includePaymentAmount ? formatMoney(resolveReceiptPaymentAmount(payment, serviceFee), currencySymbol) : ''
1162
2046
  }];
1163
2047
  items.push(...buildCashChangePaymentItems({
1164
2048
  payment,
@@ -1178,7 +2062,9 @@ function buildPaymentItems(params) {
1178
2062
  fundRecord,
1179
2063
  locale,
1180
2064
  currencySymbol,
1181
- includeBalance: includeFundRecordBalance
2065
+ includeBalance: includeFundRecordBalance,
2066
+ includeUsage: includeFundRecordUsage,
2067
+ virtualCurrencyUnit
1182
2068
  }));
1183
2069
  } else {
1184
2070
  const remainingAmount = payment.remaining_amount ?? payment.balance ?? payment.metadata?.remaining_amount;
@@ -1242,17 +2128,21 @@ function buildFundRecordPaymentItems(params) {
1242
2128
  fundRecord,
1243
2129
  locale,
1244
2130
  currencySymbol,
1245
- includeBalance
2131
+ includeBalance,
2132
+ includeUsage,
2133
+ virtualCurrencyUnit
1246
2134
  } = params;
1247
2135
  const items = [];
1248
2136
  const usingBeforeValue = fundRecord.using_before_value;
1249
2137
  const usingAfterValue = fundRecord.using_after_value;
1250
2138
  if (isPointCardPayment(payment) && usingBeforeValue !== undefined && usingAfterValue !== undefined) {
1251
2139
  const usedPoints = new _decimal.default(toMoneyNumber(usingBeforeValue)).minus(toMoneyNumber(usingAfterValue));
1252
- items.push({
1253
- item: label(locale, 'redeemPoints'),
1254
- value: trimDecimal(usedPoints)
1255
- });
2140
+ if (includeUsage) {
2141
+ items.push({
2142
+ item: label(locale, 'redeemPoints'),
2143
+ value: trimDecimal(usedPoints)
2144
+ });
2145
+ }
1256
2146
  if (includeBalance) {
1257
2147
  items.push({
1258
2148
  item: label(locale, 'remainingPoints'),
@@ -1262,8 +2152,16 @@ function buildFundRecordPaymentItems(params) {
1262
2152
  return items;
1263
2153
  }
1264
2154
  if (includeBalance && usingAfterValue !== undefined && usingAfterValue !== null) {
2155
+ if (virtualCurrencyUnit !== undefined) {
2156
+ if (!isNumericAmount(usingAfterValue)) return items;
2157
+ items.push({
2158
+ item: label(locale, 'balance'),
2159
+ value: formatVirtualCurrencyBalance(new _decimal.default(usingAfterValue), virtualCurrencyUnit)
2160
+ });
2161
+ return items;
2162
+ }
1265
2163
  items.push({
1266
- item: label(locale, 'remainingAmount'),
2164
+ item: label(locale, 'balance'),
1267
2165
  value: formatMoney(usingAfterValue, currencySymbol)
1268
2166
  });
1269
2167
  }
@@ -1281,9 +2179,14 @@ function isPointCardPayment(payment) {
1281
2179
  return code === 'POINTCARD' || type === 'point_card';
1282
2180
  }
1283
2181
  function maskMachineCode(code) {
1284
- const text = stringify(code);
1285
- if (text.length <= 6) return text;
1286
- return `${text.slice(0, 2)}**${text.slice(-4)}`;
2182
+ const text = stringify(code).trim();
2183
+ if (!text) return '';
2184
+ if (text.length <= 6) return '*'.repeat(text.length);
2185
+ return `${text.slice(0, 2)}****${text.slice(-4)}`;
2186
+ }
2187
+ function appendMaskedCardCode(name, code) {
2188
+ const maskedCode = maskMachineCode(code);
2189
+ return maskedCode ? `${name}(${maskedCode})` : name;
1287
2190
  }
1288
2191
  function trimDecimal(value) {
1289
2192
  return value.toDecimalPlaces(2).toFixed(2).replace(/\.?0+$/, '');
@@ -1292,16 +2195,13 @@ function getFundRecordPaymentName(payment, locale) {
1292
2195
  const fundRecord = getFundRecord(payment);
1293
2196
  if (!fundRecord) return '';
1294
2197
  const name = getLocalizedValue(fundRecord.wallet_pass_name, locale);
1295
- const code = maskMachineCode(fundRecord.code);
1296
- if (!name && !code) return '';
1297
- if (!code) return name;
2198
+ if (!name && !maskMachineCode(fundRecord.code)) return '';
1298
2199
  const fallbackName = getLocalizedValue(payment.name || payment.title || payment.payment_method_name || payment.method_name || payment.code, locale);
1299
- return `${name || fallbackName}(${code})`;
2200
+ return appendMaskedCardCode(name || fallbackName, fundRecord.code);
1300
2201
  }
1301
2202
  function appendMachineCodeToName(name, payment) {
1302
2203
  const fundRecord = getFundRecord(payment);
1303
- if (!fundRecord?.code) return name;
1304
- return `${name}(${maskMachineCode(fundRecord.code)})`;
2204
+ return appendMaskedCardCode(name, fundRecord?.code);
1305
2205
  }
1306
2206
  function paymentMethodName(payment, locale) {
1307
2207
  const fundRecordName = getFundRecordPaymentName(payment, locale);
@@ -1338,25 +2238,8 @@ function getLocalizedValue(value, locale) {
1338
2238
  const underscoredLocale = locale.replace('-', '_');
1339
2239
  return stringify(value[locale] ?? value[dashedLocale] ?? value[underscoredLocale] ?? value.original ?? value.auto ?? value.default ?? value.en ?? '');
1340
2240
  }
1341
- function resolveServiceTypeLabelKey(serviceType, order) {
1342
- const normalized = serviceType.trim().toLowerCase().replace(/[\s-]+/g, '_');
1343
- if (['', 'nil', 'none', 'unknown'].includes(normalized)) return null;
1344
- if (['pickup', 'pick_up', 'self_pickup'].includes(normalized)) return 'pickUp';
1345
- if (['delivery', 'shipping', 'local_delivery'].includes(normalized)) return 'delivery';
1346
- if (['takeaway', 'take_away'].includes(normalized)) return 'takeAway';
1347
- if (['dine_in', 'dinein'].includes(normalized)) return 'dineIn';
1348
- if (['shop_service', 'shop_server'].includes(normalized)) {
1349
- return resolveShopServiceLabelKey(order);
1350
- }
1351
- return null;
1352
- }
1353
- function resolveShopServiceLabelKey(order) {
1354
- const rawShopServiceType = firstPresentValue(order.metadata?.shop_service_type?.server_type, order.shop_service_type?.server_type, typeof order.shop_service_type === 'string' ? order.shop_service_type : undefined);
1355
- if (!isPresentId(rawShopServiceType)) return 'dineIn';
1356
- const normalized = stringify(rawShopServiceType).trim().toLowerCase().replace(/[\s-]+/g, '_');
1357
- if (['takeaway', 'take_away'].includes(normalized)) return 'takeAway';
1358
- if (['dine_in', 'dinein'].includes(normalized)) return 'dineIn';
1359
- return null;
2241
+ function resolveServiceTypeLabelKey(serviceType) {
2242
+ return SERVICE_TYPE_LABEL_KEYS[serviceType] || null;
1360
2243
  }
1361
2244
  function normalizeBooleanFlag(value) {
1362
2245
  if (value === true || value === 1 || stringify(value).trim() === '1') return 1;
@@ -1377,6 +2260,11 @@ function formatMoney(value, currencySymbol) {
1377
2260
  const amount = new _decimal.default(toMoneyNumber(value));
1378
2261
  return `${currencySymbol}${amount.toFixed(2)}`;
1379
2262
  }
2263
+ function formatCalculatedMoney(value, currencySymbol) {
2264
+ const amount = new _decimal.default(toMoneyNumber(value));
2265
+ const sign = amount.isNegative() ? '-' : '';
2266
+ return `${sign}${currencySymbol}${amount.abs().toFixed(2)}`;
2267
+ }
1380
2268
  function formatNegativeMoney(value, currencySymbol) {
1381
2269
  const amount = new _decimal.default(toMoneyNumber(value)).abs();
1382
2270
  return `-${currencySymbol}${amount.toFixed(2)}`;