@pisell/pisellos 2.3.138 → 2.3.140

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.
@@ -10,6 +10,46 @@ var _decimal = _interopRequireDefault(require("decimal.js"));
10
10
  var _pickupTiming = require("../../modules/Order/pickupTiming");
11
11
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
12
  const DEFAULT_LOCALES = ['en', 'zh_CN'];
13
+
14
+ // Attach provenance to a private copy BEFORE any locale-dependent filtering.
15
+ // Symbols survive the existing spread-based normalization, never enter JSON,
16
+ // and leave both the caller's snapshot and the default native contract intact.
17
+ const RECEIPT_SOURCE = Symbol('customerReceiptSource');
18
+ function copyReceiptSource(value, path = 'order') {
19
+ if (Array.isArray(value)) return value.map((item, index) => copyReceiptSource(item, `${path}/${index}`));
20
+ if (!value || typeof value !== 'object' || Object.getPrototypeOf(value) !== Object.prototype) return value;
21
+ const id = value.metadata?.unique_identification_number ?? value.product_uid ?? value.booking_uid ?? value.order_payment_id ?? value.payment_number ?? value.order_refund_id ?? value.refund_number ?? value.id;
22
+ const source = Object.fromEntries(Object.entries(value).map(([key, item]) => [key, copyReceiptSource(item, `${path}/${key}`)]));
23
+ // Retain the original occurrence even when a catalogue ID appears twice.
24
+ return {
25
+ ...source,
26
+ [RECEIPT_SOURCE]: `${path}${id != null ? `@${encodeURIComponent(String(id))}` : ''}`
27
+ };
28
+ }
29
+ function receiptKey(source, kind, valueKind = 'shared') {
30
+ const key = source?.[RECEIPT_SOURCE];
31
+ return typeof key === 'string' ? {
32
+ receipt_key: `${key}#${kind}`,
33
+ receipt_value_kind: valueKind
34
+ } : {};
35
+ }
36
+ function receiptSourceIndex(source, index) {
37
+ return source?.[RECEIPT_SOURCE] ? {
38
+ receipt_source_index: index
39
+ } : {};
40
+ }
41
+ function removePrivateReceiptSource(value) {
42
+ if (!value || typeof value !== 'object') return;
43
+ delete value[RECEIPT_SOURCE];
44
+ Object.values(value).forEach(removePrivateReceiptSource);
45
+ }
46
+ function receiptTimeKey(source, kind) {
47
+ const identity = receiptKey(source, kind);
48
+ return identity.receipt_key ? {
49
+ ...identity,
50
+ receipt_item_kind: 'shared'
51
+ } : {};
52
+ }
13
53
  const SERVICE_TYPE_LABEL_KEYS = {
14
54
  takeaway: 'takeAway',
15
55
  pick_up: 'pickUp',
@@ -247,11 +287,11 @@ const LABELS = {
247
287
  }
248
288
  };
249
289
  function buildSmallTicketData(params) {
250
- const order = params.order || {};
290
+ const order = params.includeReceiptKeys ? copyReceiptSource(params.order || {}) : params.order || {};
251
291
  const shopInfo = params.shopInfo || {};
252
292
  const locales = resolveLocales(shopInfo, params.locales);
253
293
  const productMap = params.productMap || {};
254
- return locales.reduce((data, locale) => {
294
+ const result = locales.reduce((data, locale) => {
255
295
  data[locale] = buildLocaleData({
256
296
  order,
257
297
  shopInfo,
@@ -260,6 +300,8 @@ function buildSmallTicketData(params) {
260
300
  });
261
301
  return data;
262
302
  }, {});
303
+ if (params.includeReceiptKeys) removePrivateReceiptSource(result);
304
+ return result;
263
305
  }
264
306
  function hasSmallTicketData(value) {
265
307
  return !!value && typeof value === 'object' && Object.keys(value).length > 0;
@@ -310,7 +352,10 @@ function buildLocaleData(params) {
310
352
  locale,
311
353
  currencySymbol
312
354
  }),
313
- tickets: Array.isArray(order.tickets) ? order.tickets : [],
355
+ tickets: Array.isArray(order.tickets) ? order.tickets.map(group => Array.isArray(group) ? group.map(row => row && typeof row === 'object' ? {
356
+ ...row,
357
+ ...receiptKey(row, 'ticket')
358
+ } : row) : group) : [],
314
359
  source_shop: [],
315
360
  refund_data: buildRefundData({
316
361
  order,
@@ -354,7 +399,7 @@ function buildBaseData(params) {
354
399
  function buildProducts(order, locale, productMap) {
355
400
  const products = Array.isArray(order.products) ? order.products : [];
356
401
  const allergyByProductUid = buildProductAllergyMap(order, locale);
357
- return products.map(rawProduct => {
402
+ return products.map((rawProduct, sourceIndex) => {
358
403
  const product = restoreReceiptDiscountHierarchy(rawProduct);
359
404
  const quantity = toNumber(product.product_quantity ?? product.quantity ?? product.num ?? 1, 1);
360
405
  const originalPrice = product.original_price ?? product.selling_price ?? product.payment_price ?? product.price ?? 0;
@@ -399,6 +444,10 @@ function buildProducts(order, locale, productMap) {
399
444
  const resolvedCombinations = Array.isArray(product.combinations) && product.combinations.length ? normalizeExistingCombinations(product.combinations, locale, currencySymbol) : combinations;
400
445
  const shouldIncludeBasePrice = options.length > 0 || resolvedCombinations.length > 0 || hasProductDiscount(product, extensionList);
401
446
  return {
447
+ ...receiptKey(rawProduct, 'product'),
448
+ ...(rawProduct[RECEIPT_SOURCE] ? {
449
+ receipt_source_index: sourceIndex
450
+ } : {}),
402
451
  product_title: productTitle,
403
452
  product_quantity: quantity,
404
453
  selling_price: formatMoney(sellingPrice, currencySymbol),
@@ -542,6 +591,7 @@ function normalizeProductDiscounts(params) {
542
591
  const totalAmount = new _decimal.default(toMoneyNumber(amount)).mul(isAggregatedBundleDiscount || isEntitlementPass ? 1 : quantity);
543
592
  const formattedAmount = isMarkup ? formatMoney(totalAmount.abs(), currencySymbol) : formatNegativeMoney(totalAmount, currencySymbol);
544
593
  return {
594
+ ...receiptKey(discount, 'discount'),
545
595
  item: title,
546
596
  value: formattedAmount,
547
597
  isMarkup
@@ -581,6 +631,7 @@ function buildProductExtensionList(params) {
581
631
  locale,
582
632
  currencySymbol
583
633
  }), ...(quantity > 1 ? [{
634
+ ...receiptTimeKey(product, 'quantity-price'),
584
635
  item: buildProductQuantityPriceText({
585
636
  quantity,
586
637
  currencySymbol,
@@ -588,7 +639,10 @@ function buildProductExtensionList(params) {
588
639
  }),
589
640
  value: ''
590
641
  }] : [])];
591
- const productExtensionList = extensionList.length ? extensionList : normalizeArray(product.extension_list);
642
+ const productExtensionList = extensionList.length ? extensionList : normalizeArray(product.extension_list).map(row => ({
643
+ ...row,
644
+ ...receiptKey(row, 'extension')
645
+ }));
592
646
  return [...productExtensionList, ...discountList];
593
647
  }
594
648
  function buildProductOpenWeightRows(params) {
@@ -605,10 +659,12 @@ function buildProductOpenWeightRows(params) {
605
659
  const rows = [];
606
660
  if (hasTare) {
607
661
  rows.push({
662
+ ...receiptKey(product, 'gross'),
608
663
  item: `${manualFlag}${label(locale, 'gross')}: ${productSku.weight ?? 0}${unit}`,
609
664
  value: ''
610
665
  });
611
666
  rows.push({
667
+ ...receiptKey(product, 'tare'),
612
668
  item: `${label(locale, 'tare')}: ${productSku.tare_weight}${unit}`,
613
669
  value: ''
614
670
  });
@@ -617,6 +673,7 @@ function buildProductOpenWeightRows(params) {
617
673
  const unitPrice = productSku.unit_price ?? 0;
618
674
  const netItem = `${netWeight} ${unit} NET @ ${currencySymbol}${unitPrice}/${unit}`;
619
675
  rows.push({
676
+ ...receiptTimeKey(product, 'net-weight'),
620
677
  item: hasTare ? netItem : `${manualFlag}${netItem}`,
621
678
  value: ''
622
679
  });
@@ -672,7 +729,7 @@ function normalizeProductOptions(params) {
672
729
  parentProduct
673
730
  } = params;
674
731
  const rawOptions = Array.isArray(params.options) ? params.options : Array.isArray(product.options) ? product.options : Array.isArray(product.product_sku?.option) ? product.product_sku.option : [];
675
- return rawOptions.map(option => {
732
+ return rawOptions.map((option, sourceIndex) => {
676
733
  const price = option.original_price ?? option.add_price ?? option.price;
677
734
  const quantity = toNumber(option.num ?? option.quantity ?? 1, 1);
678
735
  const value = option.value ?? option.option ?? option.item;
@@ -682,6 +739,8 @@ function normalizeProductOptions(params) {
682
739
  locale
683
740
  });
684
741
  return {
742
+ ...receiptKey(option, 'option'),
743
+ ...receiptSourceIndex(option, sourceIndex),
685
744
  name,
686
745
  ...(option.num !== undefined || option.quantity !== undefined ? {
687
746
  num: quantity
@@ -705,7 +764,7 @@ function normalizeProductCombinations(params) {
705
764
  parentProduct
706
765
  } = params;
707
766
  const bundles = Array.isArray(product.product_bundle) ? product.product_bundle : [];
708
- return bundles.map(bundle => {
767
+ return bundles.map((bundle, sourceIndex) => {
709
768
  const combinationsOptions = normalizeProductOptions({
710
769
  product: {},
711
770
  locale,
@@ -735,6 +794,8 @@ function normalizeProductCombinations(params) {
735
794
  });
736
795
  const shouldIncludeBasePrice = combinationsOptions.length > 0 || nestedCombinations.length > 0 || hasProductDiscount(bundle, extensionList);
737
796
  return {
797
+ ...receiptKey(bundle, 'combination'),
798
+ ...receiptSourceIndex(bundle, sourceIndex),
738
799
  product_title: baseTitle,
739
800
  product_quantity: quantity,
740
801
  price: formattedPrice,
@@ -762,7 +823,7 @@ function normalizeProductCombinations(params) {
762
823
  });
763
824
  }
764
825
  function normalizeExistingCombinations(combinations, locale, currencySymbol) {
765
- return combinations.map(combination => {
826
+ return combinations.map((combination, sourceIndex) => {
766
827
  if (!combination || typeof combination !== 'object' || Array.isArray(combination)) {
767
828
  return combination;
768
829
  }
@@ -799,6 +860,8 @@ function normalizeExistingCombinations(combinations, locale, currencySymbol) {
799
860
  const shouldIncludeBasePrice = normalizedOptions.length > 0 || nestedCombinations.length > 0 || hasProductDiscount(combination, extensionList);
800
861
  return {
801
862
  ...snapshot,
863
+ ...receiptKey(combination, 'combination'),
864
+ ...receiptSourceIndex(combination, sourceIndex),
802
865
  ...(productTitle ? {
803
866
  product_title: productTitle
804
867
  } : {}),
@@ -1072,6 +1135,7 @@ function buildFees(params) {
1072
1135
  const value = surcharge.amount ?? surcharge.value ?? surcharge.total_amount;
1073
1136
  if (!isNonZero(value)) continue;
1074
1137
  fees.push({
1138
+ ...receiptKey(surcharge, 'surcharge'),
1075
1139
  item: getLocalizedValue(surcharge.name || surcharge.title || surcharge.item, locale),
1076
1140
  value: formatMoney(value, currencySymbol)
1077
1141
  });
@@ -1079,6 +1143,7 @@ function buildFees(params) {
1079
1143
  const discountAmount = firstPresentValue(order.shop_discount, summary.discount_amount, order.discount_amount);
1080
1144
  if (isNonZero(discountAmount)) {
1081
1145
  fees.push({
1146
+ ...receiptKey(order, 'order-discount'),
1082
1147
  item: label(locale, 'orderDiscount'),
1083
1148
  value: formatNegativeMoney(discountAmount, currencySymbol)
1084
1149
  });
@@ -1095,6 +1160,7 @@ function buildFees(params) {
1095
1160
  const payProcessingFee = financials.payProcessingFee;
1096
1161
  if (isNonZero(payProcessingFee)) {
1097
1162
  fees.push({
1163
+ ...receiptKey(order, 'payment-processing-fee'),
1098
1164
  item: label(locale, 'payProcessingFee'),
1099
1165
  value: formatMoney(payProcessingFee, currencySymbol)
1100
1166
  });
@@ -1176,12 +1242,15 @@ function calculateProductOriginalAmount(order) {
1176
1242
  }
1177
1243
  function buildCustomer(order, locale) {
1178
1244
  return [{
1245
+ ...receiptKey(order, 'customer.contactName'),
1179
1246
  item: label(locale, 'contactName'),
1180
1247
  value: normalizeCustomerDisplayValue(order.customer_name || order.customer?.name || order.customer?.display_name)
1181
1248
  }, {
1249
+ ...receiptKey(order, 'customer.contactMobile'),
1182
1250
  item: label(locale, 'contactMobile'),
1183
1251
  value: stringify(order.phone || order.customer?.phone)
1184
1252
  }, {
1253
+ ...receiptKey(order, 'customer.email'),
1185
1254
  item: label(locale, 'email'),
1186
1255
  value: stringify(order.email || order.customer?.email)
1187
1256
  }];
@@ -1197,6 +1266,7 @@ function buildDelivery(order, locale) {
1197
1266
  const serviceTypeLabelKey = resolveServiceTypeLabelKey(serviceType);
1198
1267
  if (!serviceTypeLabelKey) return [];
1199
1268
  const items = [{
1269
+ ...receiptKey(order, 'service-type'),
1200
1270
  item: label(locale, serviceTypeLabelKey),
1201
1271
  value: '',
1202
1272
  size: 2
@@ -1205,6 +1275,7 @@ function buildDelivery(order, locale) {
1205
1275
  const pickupTime = formatPickupBookingStart(order.bookings, locale);
1206
1276
  if (pickupTime) {
1207
1277
  items.push({
1278
+ ...receiptTimeKey(order, 'pickup-time'),
1208
1279
  item: pickupTime,
1209
1280
  value: '',
1210
1281
  size: 2
@@ -1215,6 +1286,7 @@ function buildDelivery(order, locale) {
1215
1286
  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);
1216
1287
  if (isPresentId(handoverTime)) {
1217
1288
  items.push({
1289
+ ...receiptKey(order, 'handover-time'),
1218
1290
  item: label(locale, 'handoverTime'),
1219
1291
  value: formatDateTime(handoverTime, locale)
1220
1292
  });
@@ -1254,6 +1326,7 @@ function buildAppointment(order, locale) {
1254
1326
  const tableName = order.table_name || order.metadata?.table_name || order.metadata?.table?.name || order.form_data?.table_name;
1255
1327
  if (!tableName) return [];
1256
1328
  return [{
1329
+ ...receiptKey(order, 'table', 'text'),
1257
1330
  item: label(locale, 'table'),
1258
1331
  value: getLocalizedValue(tableName, locale)
1259
1332
  }];
@@ -1289,18 +1362,22 @@ function buildAppointmentRows(params) {
1289
1362
  const resourceText = formatBookingResources(resolveBookingResourcesForReceipt(booking, allResources, bookingCount), locale);
1290
1363
  const holder = getLocalizedValue(booking.holder?.name ?? booking.metadata?.holder?.name, locale).trim();
1291
1364
  return [{
1365
+ ...receiptKey(booking, 'booking-id'),
1292
1366
  item: label(locale, 'bookingId'),
1293
1367
  value: bookingId,
1294
1368
  ...association
1295
1369
  }, {
1370
+ ...receiptKey(booking, 'booking-time', 'shared'),
1296
1371
  item: label(locale, 'bookingDateTime'),
1297
1372
  value: bookingTime,
1298
1373
  ...association
1299
1374
  }, {
1375
+ ...receiptKey(booking, 'booking-resource', 'text'),
1300
1376
  item: label(locale, 'bookingResource'),
1301
1377
  value: resourceText,
1302
1378
  ...association
1303
1379
  }, {
1380
+ ...receiptKey(booking, 'booking-holder', 'text'),
1304
1381
  item: label(locale, 'bookingHolder'),
1305
1382
  value: holder,
1306
1383
  ...association
@@ -1386,6 +1463,7 @@ function compareOptionalValues(left, right) {
1386
1463
  }
1387
1464
  function buildResources(order, locale) {
1388
1465
  return normalizeArray(order.resources || order.resource_list).map(resource => ({
1466
+ ...receiptKey(resource, 'resource', 'text'),
1389
1467
  id: resource.id || resource.resource_id || '',
1390
1468
  code: '',
1391
1469
  title: getLocalizedValue(resource.title || resource.name, locale),
@@ -1431,12 +1509,14 @@ function buildRefundData(params) {
1431
1509
  const methodName = paymentMethodName(payment, params.locale) || label(params.locale, 'refund');
1432
1510
  const amount = firstPresentValue(refund.amount, refund.refund_amount, refund.total_amount) ?? 0;
1433
1511
  const items = [{
1512
+ ...receiptKey(refund, 'refund'),
1434
1513
  item: methodName,
1435
1514
  value: formatNegativeMoney(amount, params.currencySymbol)
1436
1515
  }];
1437
1516
  const occurredAt = firstPresentValue(refund.refund_time, refund.refunded_at, refund.created_at, refund.updated_at);
1438
1517
  if (isPresentId(occurredAt)) {
1439
1518
  items.push({
1519
+ ...receiptTimeKey(refund, 'refund-time'),
1440
1520
  item: formatDateTime(occurredAt, params.locale),
1441
1521
  value: ''
1442
1522
  });
@@ -1551,15 +1631,18 @@ function buildProductDiscountAssets(params) {
1551
1631
  const name = getProductWalletDiscountName(asset, params.locale);
1552
1632
  const assetTime = getProductWalletDiscountTime(asset) ?? fallbackAssetTime;
1553
1633
  const timeRow = isPresentId(assetTime) ? {
1634
+ ...receiptTimeKey(asset.records[0], 'asset-time'),
1554
1635
  item: formatDateTime(assetTime, params.locale),
1555
1636
  value: ''
1556
1637
  } : null;
1557
1638
  if (asset.type === 'entitlement_pass') {
1558
1639
  const remaining = getProductEntitlementBalance(asset);
1559
1640
  return [[{
1641
+ ...receiptKey(asset.records[0], 'asset-name'),
1560
1642
  item: name,
1561
1643
  value: ''
1562
1644
  }, ...(remaining ? [{
1645
+ ...receiptKey(asset.records[0], 'entitlementRemaining'),
1563
1646
  item: label(params.locale, 'entitlementRemaining'),
1564
1647
  value: formatEntitlementUses(remaining, params.locale)
1565
1648
  }] : []), ...(timeRow ? [timeRow] : [])]];
@@ -1567,15 +1650,18 @@ function buildProductDiscountAssets(params) {
1567
1650
  const percent = getProductWalletDiscountPercent(asset);
1568
1651
  if (percent) {
1569
1652
  return [[{
1653
+ ...receiptKey(asset.records[0], 'asset-discount'),
1570
1654
  item: appendPercentToVoucherName(name, percent),
1571
1655
  value: formatVoucherDiscount(percent, params.locale)
1572
1656
  }, ...(timeRow ? [timeRow] : [])]];
1573
1657
  }
1574
1658
  const balance = getProductWalletMoneyBalance(asset);
1575
1659
  return [[{
1660
+ ...receiptKey(asset.records[0], 'asset-name'),
1576
1661
  item: name,
1577
1662
  value: ''
1578
1663
  }, ...(balance ? [{
1664
+ ...receiptKey(asset.records[0], 'balance'),
1579
1665
  item: label(params.locale, 'balance'),
1580
1666
  value: formatMoney(balance, params.currencySymbol)
1581
1667
  }] : []), ...(timeRow ? [timeRow] : [])]];
@@ -1737,18 +1823,21 @@ function buildPointCardSnapshotAssets(params) {
1737
1823
  codes: []
1738
1824
  })) return [];
1739
1825
  const rows = [{
1826
+ ...receiptKey(pointCard, 'asset-name'),
1740
1827
  item: appendMaskedCardCode(stringify(pointCard.product_name).trim() || label(params.locale, 'pointCard'), pointCard.code),
1741
1828
  value: ''
1742
1829
  }];
1743
1830
  const remainingPoints = getPointCardSnapshotRemainingPoints(pointCard);
1744
1831
  if (remainingPoints) {
1745
1832
  rows.push({
1833
+ ...receiptKey(pointCard, 'remainingPoints'),
1746
1834
  item: label(params.locale, 'remainingPoints'),
1747
1835
  value: trimDecimal(remainingPoints)
1748
1836
  });
1749
1837
  }
1750
1838
  if (isPresentId(snapshotTime)) {
1751
1839
  rows.push({
1840
+ ...receiptTimeKey(pointCard, 'asset-time'),
1752
1841
  item: formatDateTime(snapshotTime, params.locale),
1753
1842
  value: ''
1754
1843
  });
@@ -1797,18 +1886,21 @@ function buildVirtualCurrencySnapshotAssets(params) {
1797
1886
  codes: []
1798
1887
  })) return [];
1799
1888
  const rows = [{
1889
+ ...receiptKey(virtualCurrency, 'asset-name'),
1800
1890
  item: stringify(virtualCurrency.product_name).trim(),
1801
1891
  value: ''
1802
1892
  }];
1803
1893
  const balance = getVirtualCurrencySnapshotBalance(virtualCurrency);
1804
1894
  if (balance) {
1805
1895
  rows.push({
1896
+ ...receiptKey(virtualCurrency, 'balance'),
1806
1897
  item: label(params.locale, 'balance'),
1807
1898
  value: formatVirtualCurrencyBalance(balance, getVirtualCurrencySnapshotUnit(virtualCurrency, params.locale))
1808
1899
  });
1809
1900
  }
1810
1901
  if (isPresentId(snapshotTime)) {
1811
1902
  rows.push({
1903
+ ...receiptTimeKey(virtualCurrency, 'asset-time'),
1812
1904
  item: formatDateTime(snapshotTime, params.locale),
1813
1905
  value: ''
1814
1906
  });
@@ -1903,15 +1995,18 @@ function buildVoucherAssets(params) {
1903
1995
  const name = getVoucherAssetName(voucher, params.locale);
1904
1996
  const assetTime = firstPresentValue(voucher.updated_at, voucher.created_at, voucher.collection_time, fallbackAssetTime);
1905
1997
  const timeRow = isPresentId(assetTime) ? {
1998
+ ...receiptTimeKey(voucher, 'asset-time'),
1906
1999
  item: formatDateTime(assetTime, params.locale),
1907
2000
  value: ''
1908
2001
  } : null;
1909
2002
  if (isEntitlementVoucher(voucher)) {
1910
2003
  const remaining = getEntitlementBalance(voucher.metadata?.balance, voucher.balance);
1911
2004
  return [[{
2005
+ ...receiptKey(voucher, 'asset-name'),
1912
2006
  item: name,
1913
2007
  value: ''
1914
2008
  }, ...(remaining ? [{
2009
+ ...receiptKey(voucher, 'entitlementRemaining'),
1915
2010
  item: label(params.locale, 'entitlementRemaining'),
1916
2011
  value: formatEntitlementUses(remaining, params.locale)
1917
2012
  }] : []), ...(timeRow ? [timeRow] : [])]];
@@ -1919,9 +2014,11 @@ function buildVoucherAssets(params) {
1919
2014
  if (isTicketVoucher(voucher)) {
1920
2015
  const validity = getVoucherValidity(voucher, params.locale);
1921
2016
  return [[{
2017
+ ...receiptKey(voucher, 'asset-name'),
1922
2018
  item: name,
1923
2019
  value: ''
1924
2020
  }, ...(validity ? [{
2021
+ ...receiptKey(voucher, 'validity'),
1925
2022
  item: label(params.locale, 'validity'),
1926
2023
  value: validity
1927
2024
  }] : [])]];
@@ -1929,15 +2026,18 @@ function buildVoucherAssets(params) {
1929
2026
  const percent = getVoucherDiscountPercent(voucher);
1930
2027
  if (percent) {
1931
2028
  return [[{
2029
+ ...receiptKey(voucher, 'asset-discount'),
1932
2030
  item: appendPercentToVoucherName(name, percent),
1933
2031
  value: formatVoucherDiscount(percent, params.locale)
1934
2032
  }, ...(timeRow ? [timeRow] : [])]];
1935
2033
  }
1936
2034
  const balance = getVoucherBalance(voucher);
1937
2035
  return [[{
2036
+ ...receiptKey(voucher, 'asset-name'),
1938
2037
  item: name,
1939
2038
  value: ''
1940
2039
  }, {
2040
+ ...receiptKey(voucher, 'balance'),
1941
2041
  item: label(params.locale, 'balance'),
1942
2042
  value: formatMoney(balance, params.currencySymbol)
1943
2043
  }, ...(timeRow ? [timeRow] : [])]];
@@ -2071,6 +2171,7 @@ function buildPaymentItems(params) {
2071
2171
  } = params;
2072
2172
  const serviceFee = getPaymentServiceFee(payment);
2073
2173
  const items = [{
2174
+ ...receiptKey(payment, 'payment'),
2074
2175
  item: paymentMethodName(payment, locale),
2075
2176
  value: includePaymentAmount ? formatMoney(resolveReceiptPaymentAmount(payment, serviceFee), currencySymbol) : ''
2076
2177
  }];
@@ -2081,6 +2182,7 @@ function buildPaymentItems(params) {
2081
2182
  }));
2082
2183
  if (serviceFee.gt(0)) {
2083
2184
  items.push({
2185
+ ...receiptKey(payment, 'surchargeIncluded'),
2084
2186
  item: label(locale, 'surchargeIncluded'),
2085
2187
  value: formatMoney(serviceFee, currencySymbol)
2086
2188
  });
@@ -2100,16 +2202,21 @@ function buildPaymentItems(params) {
2100
2202
  const remainingAmount = payment.remaining_amount ?? payment.balance ?? payment.metadata?.remaining_amount;
2101
2203
  if (remainingAmount !== undefined && remainingAmount !== null) {
2102
2204
  items.push({
2205
+ ...receiptKey(payment, 'remainingAmount'),
2103
2206
  item: label(locale, 'remainingAmount'),
2104
2207
  value: formatMoney(remainingAmount, currencySymbol)
2105
2208
  });
2106
2209
  }
2107
2210
  }
2108
2211
  items.push({
2212
+ ...receiptTimeKey(payment, 'payment-time'),
2109
2213
  item: formatDateTime(payment.paid_at || payment.created_at || payment.updated_at || new Date(), locale),
2110
2214
  value: ''
2111
2215
  });
2112
- return items;
2216
+ return items.map(item => item.receipt_key ? {
2217
+ ...item,
2218
+ receipt_key: `${item.receipt_key}/${includeFundRecordBalance ? 'wallet' : 'history'}`
2219
+ } : item);
2113
2220
  }
2114
2221
  function getPaymentServiceFee(payment) {
2115
2222
  return new _decimal.default(toMoneyNumber(payment.service_fee));
@@ -2130,9 +2237,11 @@ function buildCashChangePaymentItems(params) {
2130
2237
  } = params;
2131
2238
  if (!shouldDisplayCashChange(payment)) return [];
2132
2239
  return [{
2240
+ ...receiptKey(payment, 'cashReceived'),
2133
2241
  item: label(locale, 'cashReceived'),
2134
2242
  value: formatMoney(payment.metadata.actual_paid_amount, currencySymbol)
2135
2243
  }, {
2244
+ ...receiptKey(payment, 'changeDue'),
2136
2245
  item: label(locale, 'changeDue'),
2137
2246
  value: formatMoney(payment.metadata.change_given_amount, currencySymbol)
2138
2247
  }];
@@ -2169,12 +2278,14 @@ function buildFundRecordPaymentItems(params) {
2169
2278
  const usedPoints = new _decimal.default(toMoneyNumber(usingBeforeValue)).minus(toMoneyNumber(usingAfterValue));
2170
2279
  if (includeUsage) {
2171
2280
  items.push({
2281
+ ...receiptKey(payment, 'redeemPoints'),
2172
2282
  item: label(locale, 'redeemPoints'),
2173
2283
  value: trimDecimal(usedPoints)
2174
2284
  });
2175
2285
  }
2176
2286
  if (includeBalance) {
2177
2287
  items.push({
2288
+ ...receiptKey(payment, 'remainingPoints'),
2178
2289
  item: label(locale, 'remainingPoints'),
2179
2290
  value: trimDecimal(new _decimal.default(toMoneyNumber(usingAfterValue)))
2180
2291
  });
@@ -2185,12 +2296,14 @@ function buildFundRecordPaymentItems(params) {
2185
2296
  if (virtualCurrencyUnit !== undefined) {
2186
2297
  if (!isNumericAmount(usingAfterValue)) return items;
2187
2298
  items.push({
2299
+ ...receiptKey(payment, 'balance'),
2188
2300
  item: label(locale, 'balance'),
2189
2301
  value: formatVirtualCurrencyBalance(new _decimal.default(usingAfterValue), virtualCurrencyUnit)
2190
2302
  });
2191
2303
  return items;
2192
2304
  }
2193
2305
  items.push({
2306
+ ...receiptKey(payment, 'balance'),
2194
2307
  item: label(locale, 'balance'),
2195
2308
  value: formatMoney(usingAfterValue, currencySymbol)
2196
2309
  });
@@ -1,5 +1,7 @@
1
1
  import type { Module, ModuleOptions, PisellCore } from '../../types';
2
2
  import { BaseModule } from '../../modules/BaseModule';
3
+ import { type CustomerInvoiceLanguageCode, type CustomerInvoiceLanguageOption } from '../../shared/receipt/customerInvoiceLanguages';
4
+ export type { CustomerInvoiceLanguageCode, CustomerInvoiceLanguageOption, } from '../../shared/receipt/customerInvoiceLanguages';
3
5
  import type { InvoiceLayoutConfig } from '../../modules/OpenData';
4
6
  import { type SmallTicketData } from '../../shared/receipt/small-ticket';
5
7
  export interface OrderInvoicePreviewInput {
@@ -19,6 +21,9 @@ export interface OrderInvoicePreviewResult {
19
21
  sourceRevision: string;
20
22
  generatedAt: string;
21
23
  locale: string;
24
+ secondaryLocale?: string;
25
+ languageOptions: CustomerInvoiceLanguageOption[];
26
+ selectedLanguage: CustomerInvoiceLanguageCode;
22
27
  orderId: string | number;
23
28
  source: Record<string, any>;
24
29
  shop: Record<string, any>;
@@ -40,6 +45,7 @@ export declare class OrderInvoicePreviewImpl extends BaseModule implements Modul
40
45
  private disposed;
41
46
  private input?;
42
47
  private activeKey;
48
+ private languageSelection?;
43
49
  private pending?;
44
50
  private snapshotCache;
45
51
  constructor(name?: string);
@@ -51,6 +57,10 @@ export declare class OrderInvoicePreviewImpl extends BaseModule implements Modul
51
57
  private loadContext;
52
58
  private loadSource;
53
59
  prepare(input: OrderInvoicePreviewInput): Promise<OrderInvoicePreviewResult>;
60
+ private getSelectionScope;
61
+ private documentIdentity;
62
+ /** Switch only the ready receipt projection; no fetching or rebuilding data. */
63
+ selectLanguage(code: string): OrderInvoicePreviewResult;
54
64
  retry(): Promise<OrderInvoicePreviewResult>;
55
65
  destroy(): void;
56
66
  }
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.OrderInvoicePreviewImpl = void 0;
7
7
  var _BaseModule = require("../../modules/BaseModule");
8
8
  var _loadCustomerInvoiceLayout = require("../../shared/receipt/loadCustomerInvoiceLayout");
9
+ var _customerInvoiceLanguages = require("../../shared/receipt/customerInvoiceLanguages");
9
10
  var _smallTicket = require("../../shared/receipt/small-ticket");
10
11
  var _loadSalesOrderSnapshot = require("../../shared/sales/loadSalesOrderSnapshot");
11
12
  var _loadPaymentMethods = require("../../shared/payment/loadPaymentMethods");
@@ -25,6 +26,8 @@ class OrderInvoicePreviewImpl extends _BaseModule.BaseModule {
25
26
  disposed = false;
26
27
  input;
27
28
  activeKey = '';
29
+ // Hosts retain this readonly instance while the same order modal is closed.
30
+ languageSelection;
28
31
  pending;
29
32
  snapshotCache = new Map();
30
33
  constructor(name = 'orderInvoicePreview') {
@@ -56,7 +59,7 @@ class OrderInvoicePreviewImpl extends _BaseModule.BaseModule {
56
59
  cache = new Map();
57
60
  contextCaches.set(this.request, cache);
58
61
  }
59
- const key = JSON.stringify([input.tenantId, input.shopId, input.identity, input.locale]);
62
+ const key = JSON.stringify([input.tenantId, input.shopId, input.identity]);
60
63
  const cached = cache.get(key);
61
64
  if (cached) return cached;
62
65
  const task = Promise.all([this.request.get('/shop/show', {}, {
@@ -115,6 +118,7 @@ class OrderInvoicePreviewImpl extends _BaseModule.BaseModule {
115
118
  if (this.state.status === 'ready' && this.state.result) return Promise.resolve(this.state.result);
116
119
  }
117
120
  if (this.input && this.getScope(this.input) !== this.getScope(input)) this.snapshotCache.clear();
121
+ if (this.languageSelection?.scope !== this.getSelectionScope(input)) this.languageSelection = undefined;
118
122
  this.input = input;
119
123
  this.activeKey = key;
120
124
  const generation = ++this.generation;
@@ -124,7 +128,7 @@ class OrderInvoicePreviewImpl extends _BaseModule.BaseModule {
124
128
  error: null
125
129
  });
126
130
  const task = (async () => {
127
- const [snapshot, context, layout] = await Promise.all([this.loadSource(input), this.loadContext(input), (0, _loadCustomerInvoiceLayout.loadCustomerInvoiceLayout)(this.request, input)]);
131
+ const [snapshot, context, template] = await Promise.all([this.loadSource(input), this.loadContext(input), (0, _loadCustomerInvoiceLayout.loadCustomerInvoiceTemplate)(this.request, input)]);
128
132
  const source = (0, _loadPaymentMethods.withPaymentMethodNames)(snapshot, context.methods);
129
133
  const getData = this.core.getPlugin('app')?.getData?.();
130
134
  if (getData?.('shop_id') != null && String(getData('shop_id')) !== String(input.shopId)) throw new Error('Invoice shop changed');
@@ -139,21 +143,29 @@ class OrderInvoicePreviewImpl extends _BaseModule.BaseModule {
139
143
  } : {})
140
144
  };
141
145
  if (generation !== this.generation || this.disposed) throw new Error('Invoice request superseded');
142
- const requestedLocale = input.locale.toLowerCase().replace(/_/g, '-');
143
- const locale = requestedLocale.startsWith('zh-tw') || requestedLocale.startsWith('zh-hk') ? 'zh_HK' : requestedLocale.startsWith('zh') ? 'zh_CN' : requestedLocale.startsWith('ja') ? 'ja' : requestedLocale.startsWith('pt') ? 'pt' : 'en';
146
+ const languageOptions = (0, _customerInvoiceLanguages.resolveCustomerInvoiceLanguages)(template.language, input.locale);
147
+ const remembered = this.languageSelection?.code;
148
+ const selected = languageOptions.find(option => option.code === remembered) || languageOptions[0];
149
+ const locale = selected.locales[0];
144
150
  const result = {
145
- identity: key,
151
+ identity: this.documentIdentity(key, selected.code),
152
+ languageOptions,
153
+ selectedLanguage: selected.code,
154
+ ...(selected.locales[1] ? {
155
+ secondaryLocale: selected.locales[1]
156
+ } : {}),
146
157
  orderId: input.orderId,
147
158
  sourceRevision: input.sourceRevision,
148
159
  generatedAt: new Date().toISOString(),
149
160
  locale,
150
161
  source,
151
162
  shop,
152
- layout,
163
+ layout: template.layout,
153
164
  smallTicketData: (0, _smallTicket.buildSmallTicketData)({
154
165
  order: source,
155
166
  shopInfo: shop,
156
- locales: [locale]
167
+ locales: [...new Set(languageOptions.flatMap(option => option.locales))],
168
+ includeReceiptKeys: true
157
169
  })
158
170
  };
159
171
  this.publish({
@@ -177,6 +189,47 @@ class OrderInvoicePreviewImpl extends _BaseModule.BaseModule {
177
189
  this.pending = task;
178
190
  return task;
179
191
  }
192
+ getSelectionScope(input) {
193
+ return JSON.stringify([this.getScope(input), input.orderId]);
194
+ }
195
+ documentIdentity(key, language) {
196
+ return JSON.stringify([key, language]);
197
+ }
198
+
199
+ /** Switch only the ready receipt projection; no fetching or rebuilding data. */
200
+ selectLanguage(code) {
201
+ if (this.disposed) throw new Error('Invoice preview disposed');
202
+ const current = this.state.result;
203
+ if (this.state.status !== 'ready' || !current || !this.input) throw new Error('Invoice preview is not ready');
204
+ const currentShop = this.core.getPlugin('app')?.getData?.()?.('shop_id');
205
+ if (currentShop != null && String(currentShop) !== String(this.input.shopId)) throw new Error('Invoice shop changed');
206
+ const selected = current.languageOptions.find(option => option.code === code);
207
+ if (!selected) throw new Error('Invoice language is unavailable');
208
+ this.languageSelection = {
209
+ scope: this.getSelectionScope(this.input),
210
+ code: selected.code
211
+ };
212
+ if (selected.code === current.selectedLanguage) return current;
213
+ const {
214
+ secondaryLocale: _previousSecondary,
215
+ ...rest
216
+ } = current;
217
+ const result = {
218
+ ...rest,
219
+ identity: this.documentIdentity(this.activeKey, selected.code),
220
+ selectedLanguage: selected.code,
221
+ locale: selected.locales[0],
222
+ ...(selected.locales[1] ? {
223
+ secondaryLocale: selected.locales[1]
224
+ } : {})
225
+ };
226
+ this.publish({
227
+ status: 'ready',
228
+ result,
229
+ error: null
230
+ });
231
+ return result;
232
+ }
180
233
  retry() {
181
234
  if (!this.input) return Promise.reject(new Error('Invoice input is missing'));
182
235
  return this.prepare(this.input);