@pisell/pisellos 0.10.68 → 0.10.70
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.
- package/dist/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +5 -4
- package/dist/modules/Holder/utils.d.ts +2 -0
- package/dist/modules/Holder/utils.js +32 -2
- package/dist/modules/Rules/index.js +59 -58
- package/dist/server/modules/products/index.js +3 -2
- package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +3 -2
- package/dist/solution/BookingByStep/index.d.ts +1 -1
- package/dist/solution/BookingTicket/index.js +177 -112
- package/dist/solution/BookingTicket/types.d.ts +14 -4
- package/dist/solution/BookingTicket/utils/scan/applyGlobalScan.js +94 -58
- package/dist/solution/BookingTicket/utils/scan/applyProductVoucherCollectionScan.d.ts +10 -0
- package/dist/solution/BookingTicket/utils/scan/applyProductVoucherCollectionScan.js +117 -0
- package/lib/model/strategy/adapter/promotion/index.js +46 -1
- package/lib/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +2 -1
- package/lib/modules/Holder/utils.d.ts +2 -0
- package/lib/modules/Holder/utils.js +27 -1
- package/lib/modules/Rules/index.js +2 -1
- package/lib/server/modules/products/index.js +2 -1
- package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +9 -8
- package/lib/solution/BookingByStep/index.d.ts +1 -1
- package/lib/solution/BookingTicket/index.js +22 -2
- package/lib/solution/BookingTicket/types.d.ts +14 -4
- package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.js +14 -2
- package/lib/solution/BookingTicket/utils/scan/applyProductVoucherCollectionScan.d.ts +10 -0
- package/lib/solution/BookingTicket/utils/scan/applyProductVoucherCollectionScan.js +71 -0
- package/package.json +1 -1
|
@@ -5,8 +5,9 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.transformBaseProductToOrderProduct = transformBaseProductToOrderProduct;
|
|
7
7
|
var _weighedProduct = require("../../../modules/Order/utils/weighedProduct");
|
|
8
|
+
var _utils = require("../../../modules/Holder/utils");
|
|
8
9
|
var _serviceTimes = require("../../../modules/BookingContext/utils/serviceTimes");
|
|
9
|
-
var
|
|
10
|
+
var _utils2 = require("../../../modules/Order/utils");
|
|
10
11
|
var _manualProductDiscount = require("../../../modules/Order/utils/manualProductDiscount");
|
|
11
12
|
function toNumber(value, fallback = 0) {
|
|
12
13
|
const parsedValue = Number(value);
|
|
@@ -109,7 +110,7 @@ function normalizeBundleItems(bundleItems, preferPriceField = false, preferBundl
|
|
|
109
110
|
discount_list: preferPriceField ? [] : bundleItem.discount_list
|
|
110
111
|
};
|
|
111
112
|
// bundle_selling_price 落「正净额」:markdown = |price| − Σoptions;markup/原价维持原值。
|
|
112
|
-
normalizedItem.bundle_selling_price = (0,
|
|
113
|
+
normalizedItem.bundle_selling_price = (0, _utils2.getBundleSellingMagnitude)(magnitudeSource).toFixed(2);
|
|
113
114
|
if ((normalizedItem.price_type ?? normalizedItem.custom_price_type) === 'markdown' && (normalizedItem.price_type_ext === '' || normalizedItem.price_type_ext === undefined || normalizedItem.price_type_ext === null)) {
|
|
114
115
|
normalizedItem.custom_price = normalizedItem.custom_price ?? normalizedItem.price;
|
|
115
116
|
normalizedItem.price = normalizedItem.bundle_selling_price;
|
|
@@ -159,14 +160,14 @@ function transformBaseProductToOrderProduct(params) {
|
|
|
159
160
|
const explicitLineTotal = payload.line_total ?? payload.total ?? payload._extend?.total;
|
|
160
161
|
const hasExplicitLineTotal = Number.isFinite(Number(explicitLineTotal));
|
|
161
162
|
const lineCompositeTotal = toPriceString(explicitLineTotal, sourceProductPrice);
|
|
162
|
-
const productOptionItem = (0,
|
|
163
|
+
const productOptionItem = (0, _utils2.normalizeProductSkuOptions)(normalizeOptionItems(payload.product_sku?.option ?? payload.option ?? payload.options ?? payload.product_option_item, hasPriceOverride || weighedSourcePrice !== undefined));
|
|
163
164
|
const productBundle = normalizeBundleItems(payload.bundle ?? payload.product_bundle, hasPriceOverride, hasPriceOverride && weighedSourcePrice !== undefined);
|
|
164
165
|
// `payload.unique` 是业务侧商品/票务标识,可能在连续加同一商品时重复;
|
|
165
166
|
// 行级唯一值只接受显式协议字段,缺省交给 OrderModule 生成。
|
|
166
167
|
const uniqueIdentificationNumber = payload.unique_identification_number;
|
|
167
168
|
const resolvedQuantity = toSafePositiveInt(payload.quantity ?? payload.num, 1);
|
|
168
|
-
const originTotal = weighedSourcePrice !== undefined ? Number((0,
|
|
169
|
-
mainPrice: Number(weighedSourcePrice) + (0,
|
|
169
|
+
const originTotal = weighedSourcePrice !== undefined ? Number((0, _utils2.composeLinePrice)({
|
|
170
|
+
mainPrice: Number(weighedSourcePrice) + (0, _utils2.sumOptionUnitPrice)(productOptionItem).toNumber(),
|
|
170
171
|
bundle: hasPriceOverride ? (0, _weighedProduct.withOriginalBundleOptionPrices)(productBundle) : productBundle,
|
|
171
172
|
useOriginalBundle: true
|
|
172
173
|
})) : (0, _manualProductDiscount.resolveManualDiscountOriginTotal)({
|
|
@@ -198,9 +199,9 @@ function transformBaseProductToOrderProduct(params) {
|
|
|
198
199
|
})) : payload.discount_list || [];
|
|
199
200
|
|
|
200
201
|
// v2:source 不含 option;main_product_* 含 option(与 normalizeOrderProduct 一致)
|
|
201
|
-
const optionSum = (0,
|
|
202
|
+
const optionSum = (0, _utils2.sumOptionUnitPrice)(productOptionItem).toNumber();
|
|
202
203
|
const mainProductOriginalPrice = toPriceString(Number(sourceProductPrice) + optionSum);
|
|
203
|
-
const bundleOnlyComposite = (0,
|
|
204
|
+
const bundleOnlyComposite = (0, _utils2.composeLinePrice)({
|
|
204
205
|
mainPrice: 0,
|
|
205
206
|
bundle: productBundle
|
|
206
207
|
});
|
|
@@ -221,7 +222,7 @@ function transformBaseProductToOrderProduct(params) {
|
|
|
221
222
|
main_product_selling_price: mainProductSellingPrice,
|
|
222
223
|
main_product_original_price: mainProductOriginalPrice,
|
|
223
224
|
price_schema_version: 2,
|
|
224
|
-
holder_config: sourceProduct
|
|
225
|
+
holder_config: (0, _utils.resolveProductHolderConfig)(sourceProduct) ?? undefined,
|
|
225
226
|
...(dataVariantIds !== undefined ? {
|
|
226
227
|
data_variant_ids: dataVariantIds
|
|
227
228
|
} : {}),
|
|
@@ -326,7 +326,7 @@ export declare class BookingByStepImpl extends BaseModule implements Module {
|
|
|
326
326
|
date: string;
|
|
327
327
|
status: string;
|
|
328
328
|
week: string;
|
|
329
|
-
weekNum: 0 |
|
|
329
|
+
weekNum: 0 | 2 | 1 | 6 | 4 | 5 | 3;
|
|
330
330
|
}[]>;
|
|
331
331
|
submitTimeSlot(timeSlots: TimeSliceItem): void;
|
|
332
332
|
private getScheduleDataByIds;
|
|
@@ -30,6 +30,7 @@ var _handleScan = require("./utils/scan/handleScan");
|
|
|
30
30
|
var _scanCache = _interopRequireDefault(require("./utils/scan/scanCache"));
|
|
31
31
|
var _formatGlobalScan = require("./utils/scan/formatGlobalScan");
|
|
32
32
|
var _applyGlobalScan = require("./utils/scan/applyGlobalScan");
|
|
33
|
+
var _applyProductVoucherCollectionScan = require("./utils/scan/applyProductVoucherCollectionScan");
|
|
33
34
|
var _cartView = require("./utils/cartView");
|
|
34
35
|
var _addProductDecision = require("./utils/addProductDecision");
|
|
35
36
|
var _buildCacheItemFromOrderLine = require("../../modules/BookingContext/utils/buildCacheItemFromOrderLine");
|
|
@@ -1847,7 +1848,7 @@ class BookingTicketImpl extends _BaseSales.BaseSalesImpl {
|
|
|
1847
1848
|
reason: 'not_found'
|
|
1848
1849
|
};
|
|
1849
1850
|
}
|
|
1850
|
-
const
|
|
1851
|
+
const host = {
|
|
1851
1852
|
setOrderCustomer: customer => {
|
|
1852
1853
|
beforeScanCommit();
|
|
1853
1854
|
this.setOrderCustomer(customer);
|
|
@@ -1868,10 +1869,29 @@ class BookingTicketImpl extends _BaseSales.BaseSalesImpl {
|
|
|
1868
1869
|
beforeScanCommit();
|
|
1869
1870
|
return this.scanWalletAsset(scanCode);
|
|
1870
1871
|
}
|
|
1871
|
-
}
|
|
1872
|
+
};
|
|
1873
|
+
const result = guardedBridge?.scanScope === 'product-voucher-collection' ? await (0, _applyProductVoucherCollectionScan.applyProductVoucherCollectionScan)({
|
|
1874
|
+
...host,
|
|
1875
|
+
waitForCustomerDiscounts: () => this.orderCustomerDiscountRefreshInFlight || Promise.resolve(),
|
|
1876
|
+
loadCustomerDiscounts: async () => {
|
|
1877
|
+
const customerId = Number(this.getOrderCustomer()?.customer_id || 0);
|
|
1878
|
+
if (customerId <= 1) return [];
|
|
1879
|
+
await this.refreshDiscountConfigAfterOrderCustomerChange(customerId);
|
|
1880
|
+
return this.getDiscountList();
|
|
1881
|
+
}
|
|
1882
|
+
}, formatted, guardedBridge) : await (0, _applyGlobalScan.applyGlobalScan)(host, formatted, guardedBridge);
|
|
1872
1883
|
bridge?.refresh?.();
|
|
1873
1884
|
return result;
|
|
1874
1885
|
} catch (error) {
|
|
1886
|
+
if (bridge?.scanScope === 'product-voucher-collection') {
|
|
1887
|
+
if (bridge.isCurrent?.() === false) return {
|
|
1888
|
+
status: 'cancelled'
|
|
1889
|
+
};
|
|
1890
|
+
return {
|
|
1891
|
+
status: 'failed',
|
|
1892
|
+
reason: 'entry_resolution_failed'
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1875
1895
|
console.error('[BookingTicket] handleGlobalScanCode failed', error);
|
|
1876
1896
|
bridge?.onNotify?.('fail', 'pisell2.text.scan-global-failed');
|
|
1877
1897
|
return {
|
|
@@ -208,8 +208,10 @@ export interface AddAddTimeProductsToBookingInput {
|
|
|
208
208
|
}
|
|
209
209
|
/** 扫码宿主桥接:SalesSdk 在 pubsub 订阅回调里注入,OS 需要 UI 时直接调用 */
|
|
210
210
|
export interface GlobalScanHostBridge {
|
|
211
|
-
/**
|
|
212
|
-
scanScope?: 'all' | 'product-voucher';
|
|
211
|
+
/** product-voucher handles voucher codes only; product-voucher-collection also resolves wallet customer vouchers. */
|
|
212
|
+
scanScope?: 'all' | 'product-voucher' | 'product-voucher-collection';
|
|
213
|
+
/** A one-shot entry may outlive its page while requests are in flight. */
|
|
214
|
+
isCurrent?: () => boolean;
|
|
213
215
|
openProductDetail?: (payload: Record<string, any>) => Promise<any | null>;
|
|
214
216
|
openBookingEdit?: (payload: Record<string, any>) => Promise<any | null>;
|
|
215
217
|
confirmDetail?: (input: {
|
|
@@ -234,11 +236,13 @@ export interface GlobalScanHostBridge {
|
|
|
234
236
|
*/
|
|
235
237
|
applyPromotionCode?: (code: string, customerId?: number, options?: {
|
|
236
238
|
suppressFailureNotification?: boolean;
|
|
239
|
+
productVouchersOnly?: boolean;
|
|
237
240
|
}) => Promise<{
|
|
238
241
|
isAvailable?: boolean;
|
|
239
242
|
isAccepted?: boolean;
|
|
240
243
|
type?: string;
|
|
241
244
|
unavailableReason?: string;
|
|
245
|
+
scannedDiscountList?: Record<string, any>[];
|
|
242
246
|
}>;
|
|
243
247
|
/** 当前订单已有其他客户时,由宿主复用客户组件完成确认及切换。 */
|
|
244
248
|
requestCustomerSwitch?: (customer: ICustomer) => Promise<boolean | null>;
|
|
@@ -246,7 +250,7 @@ export interface GlobalScanHostBridge {
|
|
|
246
250
|
refresh?: () => void;
|
|
247
251
|
}
|
|
248
252
|
/** handleGlobalScanCode 结构化返回 */
|
|
249
|
-
export type GlobalScanHandleResult = {
|
|
253
|
+
export type GlobalScanHandleResult = ({
|
|
250
254
|
status: 'success';
|
|
251
255
|
kind: 'customer' | 'products' | 'promotion';
|
|
252
256
|
meta?: {
|
|
@@ -259,5 +263,11 @@ export type GlobalScanHandleResult = {
|
|
|
259
263
|
status: 'cancelled';
|
|
260
264
|
} | {
|
|
261
265
|
status: 'failed';
|
|
262
|
-
reason: 'empty_code' | 'not_found' | 'invalid_data' | 'no_bridge';
|
|
266
|
+
reason: 'empty_code' | 'not_found' | 'invalid_data' | 'no_bridge' | 'entry_resolution_failed';
|
|
267
|
+
}) & {
|
|
268
|
+
/** product-voucher-collection returns voucher candidates; the caller decides how to use them. */
|
|
269
|
+
redemption?: {
|
|
270
|
+
source: 'wallet' | 'scan';
|
|
271
|
+
vouchers: Record<string, any>[];
|
|
272
|
+
};
|
|
263
273
|
};
|
|
@@ -289,6 +289,18 @@ async function applyGlobalScan(host, formatted, bridge) {
|
|
|
289
289
|
// batch-search 可能返回来源门店客户;先映射到当前门店,确保 Wallet
|
|
290
290
|
// 支付资产查询使用本店客户上下文。
|
|
291
291
|
const bindResult = await bindWalletPassCustomer();
|
|
292
|
+
// Voucher collection prioritizes goods coupons resolved from the wristband/order.
|
|
293
|
+
// Only this opt-in flow checks them before selecting a payment wallet asset.
|
|
294
|
+
const collectionPromotion = bridge?.scanScope === 'product-voucher-collection' && bridge.applyPromotionCode ? await bridge.applyPromotionCode(scanCode, getRealCustomerId(activeCustomer), {
|
|
295
|
+
suppressFailureNotification: true,
|
|
296
|
+
productVouchersOnly: true
|
|
297
|
+
}) : undefined;
|
|
298
|
+
if (collectionPromotion?.isAccepted ?? collectionPromotion?.isAvailable) {
|
|
299
|
+
return {
|
|
300
|
+
status: 'success',
|
|
301
|
+
kind: 'promotion'
|
|
302
|
+
};
|
|
303
|
+
}
|
|
292
304
|
const walletSelection = await trySelectWalletAsset();
|
|
293
305
|
if (walletSelection) return walletSelection;
|
|
294
306
|
if (!bridge?.applyPromotionCode) {
|
|
@@ -298,9 +310,9 @@ async function applyGlobalScan(host, formatted, bridge) {
|
|
|
298
310
|
};
|
|
299
311
|
}
|
|
300
312
|
const promotionCustomerId = getRealCustomerId(activeCustomer);
|
|
301
|
-
const promotionResult = bindResult.customerBound ? await bridge.applyPromotionCode(scanCode, promotionCustomerId, {
|
|
313
|
+
const promotionResult = collectionPromotion ?? (bindResult.customerBound ? await bridge.applyPromotionCode(scanCode, promotionCustomerId, {
|
|
302
314
|
suppressFailureNotification: true
|
|
303
|
-
}) : await bridge.applyPromotionCode(scanCode, promotionCustomerId);
|
|
315
|
+
}) : await bridge.applyPromotionCode(scanCode, promotionCustomerId));
|
|
304
316
|
if (promotionResult?.isAccepted ?? promotionResult?.isAvailable) {
|
|
305
317
|
return {
|
|
306
318
|
status: 'success',
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { GlobalScanHandleResult, GlobalScanHostBridge } from '../../types';
|
|
2
|
+
import { type GlobalScanApplyHost } from './applyGlobalScan';
|
|
3
|
+
import type { GlobalScanFormattedResult } from './formatGlobalScan';
|
|
4
|
+
export interface ProductVoucherCollectionScanHost extends GlobalScanApplyHost {
|
|
5
|
+
waitForCustomerDiscounts(): Promise<void>;
|
|
6
|
+
loadCustomerDiscounts(): Promise<Record<string, any>[]>;
|
|
7
|
+
}
|
|
8
|
+
/** Collect voucher candidates after customer binding and coupon loading finish.
|
|
9
|
+
* Never expand a wristband's result to other orders of the same customer. */
|
|
10
|
+
export declare function applyProductVoucherCollectionScan(host: ProductVoucherCollectionScanHost, formatted: GlobalScanFormattedResult, bridge: GlobalScanHostBridge): Promise<GlobalScanHandleResult>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.applyProductVoucherCollectionScan = applyProductVoucherCollectionScan;
|
|
7
|
+
var _applyGlobalScan = require("./applyGlobalScan");
|
|
8
|
+
/** Collect voucher candidates after customer binding and coupon loading finish.
|
|
9
|
+
* Never expand a wristband's result to other orders of the same customer. */
|
|
10
|
+
async function applyProductVoucherCollectionScan(host, formatted, bridge) {
|
|
11
|
+
const assertCurrent = () => {
|
|
12
|
+
if (bridge.isCurrent?.() === false) {
|
|
13
|
+
throw new DOMException('Scan session ended', 'AbortError');
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
let vouchers = [];
|
|
17
|
+
assertCurrent();
|
|
18
|
+
const result = await (0, _applyGlobalScan.applyGlobalScan)({
|
|
19
|
+
...host,
|
|
20
|
+
setOrderCustomer: customer => {
|
|
21
|
+
assertCurrent();
|
|
22
|
+
host.setOrderCustomer(customer);
|
|
23
|
+
},
|
|
24
|
+
addProductToOrder: (product, booking) => {
|
|
25
|
+
assertCurrent();
|
|
26
|
+
return host.addProductToOrder(product, booking);
|
|
27
|
+
}
|
|
28
|
+
}, formatted, {
|
|
29
|
+
...bridge,
|
|
30
|
+
applyPromotionCode: bridge.applyPromotionCode && (async (code, customerId, options) => {
|
|
31
|
+
// Customer binding starts prepare/config in the background. Let it finish
|
|
32
|
+
// before adding scanned coupons so its response cannot overwrite them.
|
|
33
|
+
await host.waitForCustomerDiscounts();
|
|
34
|
+
assertCurrent();
|
|
35
|
+
const promotion = await bridge.applyPromotionCode(code, customerId, {
|
|
36
|
+
...options,
|
|
37
|
+
productVouchersOnly: true,
|
|
38
|
+
suppressFailureNotification: true
|
|
39
|
+
});
|
|
40
|
+
assertCurrent();
|
|
41
|
+
// An empty successful search is different from a swallowed request error.
|
|
42
|
+
if (!Array.isArray(promotion.scannedDiscountList)) {
|
|
43
|
+
throw new Error('Could not resolve scanned vouchers');
|
|
44
|
+
}
|
|
45
|
+
vouchers = promotion.scannedDiscountList;
|
|
46
|
+
return promotion;
|
|
47
|
+
})
|
|
48
|
+
});
|
|
49
|
+
assertCurrent();
|
|
50
|
+
if (formatted.searchType === 'wallet' && result.status === 'success') {
|
|
51
|
+
vouchers = await host.loadCustomerDiscounts();
|
|
52
|
+
assertCurrent();
|
|
53
|
+
return {
|
|
54
|
+
...result,
|
|
55
|
+
redemption: {
|
|
56
|
+
source: 'wallet',
|
|
57
|
+
vouchers
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (formatted.searchType === 'walletPass') {
|
|
62
|
+
return {
|
|
63
|
+
...result,
|
|
64
|
+
redemption: {
|
|
65
|
+
source: 'scan',
|
|
66
|
+
vouchers
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|