@pisell/pisellos 0.10.57 → 0.10.58
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/model/strategy/adapter/promotion/index.js +9 -0
- package/dist/modules/Order/index.d.ts +1 -1
- package/dist/modules/Order/utils.d.ts +5 -3
- package/dist/modules/Order/utils.js +32 -11
- package/dist/modules/ProductList/index.d.ts +3 -1
- package/dist/modules/ProductList/index.js +187 -92
- package/dist/modules/ProductList/types.d.ts +8 -0
- package/dist/modules/ProductList/types.js +2 -0
- package/dist/solution/BookingByStep/index.d.ts +1 -1
- package/dist/solution/UnifiedBookingSales/index.d.ts +7 -1
- package/dist/solution/UnifiedBookingSales/index.js +548 -388
- package/lib/modules/Order/index.d.ts +1 -1
- package/lib/modules/Order/utils.d.ts +5 -3
- package/lib/modules/Order/utils.js +29 -10
- package/lib/modules/ProductList/index.d.ts +3 -1
- package/lib/modules/ProductList/index.js +31 -5
- package/lib/modules/ProductList/types.d.ts +8 -0
- package/lib/modules/ProductList/types.js +1 -0
- package/lib/solution/BookingByStep/index.d.ts +1 -1
- package/lib/solution/UnifiedBookingSales/index.d.ts +7 -1
- package/lib/solution/UnifiedBookingSales/index.js +80 -1
- package/package.json +1 -1
|
@@ -592,7 +592,7 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
|
|
|
592
592
|
generateIdempotencyToken(): string;
|
|
593
593
|
syncPaymentsToOrder<T = any>(params: SyncPaymentsToOrderParams): Promise<SyncPaymentsToOrderResult<T>>;
|
|
594
594
|
createOrder(params: CommitOrderParams['query']): {
|
|
595
|
-
type: "
|
|
595
|
+
type: "appointment_booking" | "virtual";
|
|
596
596
|
platform: string;
|
|
597
597
|
sales_channel: string;
|
|
598
598
|
order_sales_channel: string;
|
|
@@ -121,8 +121,9 @@ export declare function getBundleSellingMagnitude(bundle: Record<string, any> |
|
|
|
121
121
|
* = `metadata.main_product_original_price` + Σ(bundle 原价 × num)。
|
|
122
122
|
* - `metadata.source_product_price`:主商品/variant 基础价(已应用报价单),不含 option、
|
|
123
123
|
* 不含折扣。**权威源**。
|
|
124
|
-
* - `
|
|
125
|
-
* - `metadata.
|
|
124
|
+
* - `product_sku.option[].add_price` 保留单规格折前单价;Rules 的 option.price 只用于本次计算。
|
|
125
|
+
* - `metadata.main_product_original_price` = source + Σ(option.add_price × option.num)(含 option、不含折扣)。
|
|
126
|
+
* - `metadata.main_product_selling_price` = main_original − 行内 per-unit amount(含主商品及 option 优惠)。
|
|
126
127
|
* - `payment_price`:出站 payload 中为行 composite 已减整单折扣均摊价(无折扣时与 `selling_price` 同义)。
|
|
127
128
|
*
|
|
128
129
|
* Rules 钩子契约(hook_adapt 方案,Rules 内部保持不变):
|
|
@@ -131,8 +132,9 @@ export declare function getBundleSellingMagnitude(bundle: Record<string, any> |
|
|
|
131
132
|
* `getProductOriginTotalPrice` 会自行叠加 option + bundle,喂 source 可避免 option 双加。
|
|
132
133
|
* - `getProduct.total` = `selling_price × num`(行 composite × num),与 Rules 内部产出的
|
|
133
134
|
* total(source + option + bundle)对齐。
|
|
135
|
+
* - `getProduct.options` 用单规格原价参与重算;主商品折后价从 main_selling 减去 option 折后合计反推。
|
|
134
136
|
* - `setProduct` 反向流程:把 Rules 返回的 source-level `main_product_selling_price` / `price`
|
|
135
|
-
*
|
|
137
|
+
* 加回单规格原价合计,再减去 optionDiscountAmount;保留订单单规格原价供取消折扣及后续重算。
|
|
136
138
|
* 优先级:
|
|
137
139
|
* 1. `values.main_product_selling_price`(券应用分支,per-unit 券后 source-level 主价)
|
|
138
140
|
* 2. `values.price`(还原分支 `restoredPrice`、good_pass 归零)
|
|
@@ -633,8 +633,9 @@ function getBundleSellingMagnitude(bundle) {
|
|
|
633
633
|
* = `metadata.main_product_original_price` + Σ(bundle 原价 × num)。
|
|
634
634
|
* - `metadata.source_product_price`:主商品/variant 基础价(已应用报价单),不含 option、
|
|
635
635
|
* 不含折扣。**权威源**。
|
|
636
|
-
* - `
|
|
637
|
-
* - `metadata.
|
|
636
|
+
* - `product_sku.option[].add_price` 保留单规格折前单价;Rules 的 option.price 只用于本次计算。
|
|
637
|
+
* - `metadata.main_product_original_price` = source + Σ(option.add_price × option.num)(含 option、不含折扣)。
|
|
638
|
+
* - `metadata.main_product_selling_price` = main_original − 行内 per-unit amount(含主商品及 option 优惠)。
|
|
638
639
|
* - `payment_price`:出站 payload 中为行 composite 已减整单折扣均摊价(无折扣时与 `selling_price` 同义)。
|
|
639
640
|
*
|
|
640
641
|
* Rules 钩子契约(hook_adapt 方案,Rules 内部保持不变):
|
|
@@ -643,8 +644,9 @@ function getBundleSellingMagnitude(bundle) {
|
|
|
643
644
|
* `getProductOriginTotalPrice` 会自行叠加 option + bundle,喂 source 可避免 option 双加。
|
|
644
645
|
* - `getProduct.total` = `selling_price × num`(行 composite × num),与 Rules 内部产出的
|
|
645
646
|
* total(source + option + bundle)对齐。
|
|
647
|
+
* - `getProduct.options` 用单规格原价参与重算;主商品折后价从 main_selling 减去 option 折后合计反推。
|
|
646
648
|
* - `setProduct` 反向流程:把 Rules 返回的 source-level `main_product_selling_price` / `price`
|
|
647
|
-
*
|
|
649
|
+
* 加回单规格原价合计,再减去 optionDiscountAmount;保留订单单规格原价供取消折扣及后续重算。
|
|
648
650
|
* 优先级:
|
|
649
651
|
* 1. `values.main_product_selling_price`(券应用分支,per-unit 券后 source-level 主价)
|
|
650
652
|
* 2. `values.price`(还原分支 `restoredPrice`、good_pass 归零)
|
|
@@ -843,6 +845,14 @@ function createDefaultOrderRulesHooks(getBookings) {
|
|
|
843
845
|
const effectiveNum = Number(num) > 0 ? Number(num) : 1;
|
|
844
846
|
return new _decimal.default(Number(totalLike) || 0).dividedBy(effectiveNum).toDecimalPlaces(2).toString();
|
|
845
847
|
};
|
|
848
|
+
|
|
849
|
+
// optionDiscountAmount 已是每件主商品内的单规格优惠合计,不再乘商品数量。
|
|
850
|
+
const getOptionDiscountAmount = discountList => {
|
|
851
|
+
return (discountList || []).reduce((total, discount) => {
|
|
852
|
+
if (discount?.metadata?.custom_product_bundle_map_id) return total;
|
|
853
|
+
return total.plus(Number(discount?.metadata?.optionDiscountAmount) || 0);
|
|
854
|
+
}, new _decimal.default(0));
|
|
855
|
+
};
|
|
846
856
|
const getBundleIdentity = bundle => {
|
|
847
857
|
return [bundle.bundle_id ?? bundle.id ?? '', bundle.bundle_product_id ?? bundle._bundle_product_id ?? bundle.product_id ?? '', bundle.bundle_variant_id ?? bundle.variant_id ?? bundle.product_variant_id ?? 0, bundle.bundle_group_id ?? bundle.group_id ?? ''].join('|');
|
|
848
858
|
};
|
|
@@ -885,12 +895,14 @@ function createDefaultOrderRulesHooks(getBookings) {
|
|
|
885
895
|
getProduct: product => {
|
|
886
896
|
// source_product_price 是权威源;兜底链:source → mainSelling − options(反推)→ 行 selling_price
|
|
887
897
|
const metadataAny = product.metadata || {};
|
|
888
|
-
const
|
|
898
|
+
const productOptions = getProductSkuOptions(product);
|
|
899
|
+
const optionSum = sumOptionUnitPrice(productOptions);
|
|
900
|
+
const optionSellingSum = optionSum.minus(getOptionDiscountAmount(product.discount_list));
|
|
889
901
|
let sourcePrice;
|
|
890
902
|
if (metadataAny.source_product_price !== undefined) {
|
|
891
903
|
sourcePrice = String(metadataAny.source_product_price);
|
|
892
904
|
} else if (metadataAny.main_product_selling_price !== undefined) {
|
|
893
|
-
sourcePrice = new _decimal.default(Number(metadataAny.main_product_selling_price) || 0).minus(
|
|
905
|
+
sourcePrice = new _decimal.default(Number(metadataAny.main_product_selling_price) || 0).minus(optionSellingSum).toDecimalPlaces(2).toString();
|
|
894
906
|
} else {
|
|
895
907
|
sourcePrice = String(product.selling_price ?? 0);
|
|
896
908
|
}
|
|
@@ -920,6 +932,11 @@ function createDefaultOrderRulesHooks(getBookings) {
|
|
|
920
932
|
num: product.num || 1,
|
|
921
933
|
discount_list: product.discount_list || [],
|
|
922
934
|
bundle: product.product_bundle || [],
|
|
935
|
+
// 仅在 Rules 入参中映射 price,避免折后价污染订单的 add_price。
|
|
936
|
+
options: productOptions.map(option => ({
|
|
937
|
+
...option,
|
|
938
|
+
price: option.add_price
|
|
939
|
+
})),
|
|
923
940
|
booking_id: product.metadata?.booking_id || null,
|
|
924
941
|
isClient: false,
|
|
925
942
|
// 勿写死 false:否则 Rules 不会走 total≠origin_total 的手动价推断,
|
|
@@ -927,7 +944,7 @@ function createDefaultOrderRulesHooks(getBookings) {
|
|
|
927
944
|
isManualDiscount: resolveRulesManualDiscountFlag(metadataAny),
|
|
928
945
|
holder_id: product.metadata?.holder_id,
|
|
929
946
|
startDate: getProductStartDate(product),
|
|
930
|
-
main_product_selling_price: metadataAny.main_product_selling_price !== undefined ? new _decimal.default(Number(metadataAny.main_product_selling_price) || 0).minus(
|
|
947
|
+
main_product_selling_price: metadataAny.main_product_selling_price !== undefined ? new _decimal.default(Number(metadataAny.main_product_selling_price) || 0).minus(optionSellingSum).toDecimalPlaces(2).toString() : undefined,
|
|
931
948
|
inPromotion: metadataAny._promotion?.inPromotion === true,
|
|
932
949
|
_promotion: metadataAny._promotion
|
|
933
950
|
};
|
|
@@ -938,8 +955,10 @@ function createDefaultOrderRulesHooks(getBookings) {
|
|
|
938
955
|
const existedSource = metadataAny.source_product_price ?? product.selling_price ?? '0';
|
|
939
956
|
const nextOptions = getProductSkuOptions(product);
|
|
940
957
|
const optionSum = sumOptionUnitPrice(nextOptions);
|
|
958
|
+
const nextDiscountList = values.discount_list ?? product.discount_list;
|
|
959
|
+
const optionSellingSum = optionSum.minus(getOptionDiscountAmount(nextDiscountList));
|
|
941
960
|
|
|
942
|
-
// Rules 返回的 main_product_selling_price / price
|
|
961
|
+
// Rules 返回的 main_product_selling_price / price 为不含 option 的主商品价。
|
|
943
962
|
// 优先级:main_product_selling_price(per-unit 券后 source)> price(还原/good_pass 分支)>
|
|
944
963
|
// total/num(仅当 Rules 只给 total 时的兜底)> 保留当前 source。
|
|
945
964
|
const nextSourceSellingPrice = values.main_product_selling_price !== undefined ? String(values.main_product_selling_price) : values.price !== undefined ? String(values.price) : values.total !== undefined ? toUnitPriceString(values.total, nextNum) : String(existedSource);
|
|
@@ -948,8 +967,8 @@ function createDefaultOrderRulesHooks(getBookings) {
|
|
|
948
967
|
// 缺省时保留现有 source(券前 = 当前 source)。
|
|
949
968
|
const nextSourceOriginalPrice = values.original_price !== undefined ? String(values.original_price) : String(existedSource);
|
|
950
969
|
|
|
951
|
-
//
|
|
952
|
-
const nextMainSellingPrice = new _decimal.default(Number(nextSourceSellingPrice) || 0).plus(
|
|
970
|
+
// 成交价只加回 option 折后合计;原价仍使用订单中保存的 option 原价。
|
|
971
|
+
const nextMainSellingPrice = new _decimal.default(Number(nextSourceSellingPrice) || 0).plus(optionSellingSum).toDecimalPlaces(2).toFixed(2);
|
|
953
972
|
const nextMainOriginalPrice = new _decimal.default(Number(nextSourceOriginalPrice) || 0).plus(optionSum).toDecimalPlaces(2).toFixed(2);
|
|
954
973
|
const nextBundle = values.bundle !== undefined ? mergeRulesBundlePrices(product.product_bundle, values.bundle) : product.product_bundle;
|
|
955
974
|
const composedSellingPrice = composeLinePrice({
|
|
@@ -970,7 +989,7 @@ function createDefaultOrderRulesHooks(getBookings) {
|
|
|
970
989
|
...product,
|
|
971
990
|
selling_price: composedSellingPrice,
|
|
972
991
|
original_price: finalOriginalPrice,
|
|
973
|
-
discount_list:
|
|
992
|
+
discount_list: nextDiscountList,
|
|
974
993
|
num: values.quantity ?? product.num,
|
|
975
994
|
product_bundle: nextBundle,
|
|
976
995
|
metadata: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Module, PisellCore } from '../../types';
|
|
2
2
|
import { BaseModule } from '../BaseModule';
|
|
3
|
-
import { ProductListLoadProductsParams, ProductListStoreQueryParams } from './types';
|
|
3
|
+
import { ProductListLoadProductsParams, ProductListStoreQueryParams, ProductEnrichmentContext } from './types';
|
|
4
4
|
import { ProductData } from '../Product/types';
|
|
5
5
|
export * from './types';
|
|
6
6
|
export declare class ProductList extends BaseModule implements Module {
|
|
@@ -19,6 +19,8 @@ export declare class ProductList extends BaseModule implements Module {
|
|
|
19
19
|
* productList.updateOtherParams({ channel: 'pos' });
|
|
20
20
|
*/
|
|
21
21
|
updateOtherParams(params: Record<string, any>): void;
|
|
22
|
+
enrichProducts(products: ProductData[], context: ProductEnrichmentContext): Promise<ProductData[]>;
|
|
23
|
+
private enrichCatalog;
|
|
22
24
|
storeChange(path?: string, value?: any): Promise<void>;
|
|
23
25
|
/**
|
|
24
26
|
* 获取加时商品列表。
|
|
@@ -68,6 +68,30 @@ class ProductList extends _BaseModule.BaseModule {
|
|
|
68
68
|
updateOtherParams(params) {
|
|
69
69
|
this.otherParams = params || {};
|
|
70
70
|
}
|
|
71
|
+
async enrichProducts(products, context) {
|
|
72
|
+
const enrich = this.otherParams.enrichProducts;
|
|
73
|
+
if (!enrich) return products;
|
|
74
|
+
if (context.signal?.aborted) throw new Error('Product enrichment cancelled');
|
|
75
|
+
const result = await enrich((0, _lodashEs.cloneDeep)(products), {
|
|
76
|
+
businessCode: this.otherParams.businessCode,
|
|
77
|
+
channel: this.otherParams.channel,
|
|
78
|
+
...context
|
|
79
|
+
});
|
|
80
|
+
if (context.signal?.aborted) throw new Error('Product enrichment cancelled');
|
|
81
|
+
if (!Array.isArray(result)) throw new Error('Product enrichment must return an array');
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
async enrichCatalog(products, reason) {
|
|
85
|
+
try {
|
|
86
|
+
return await this.enrichProducts(products, {
|
|
87
|
+
reason
|
|
88
|
+
});
|
|
89
|
+
} catch {
|
|
90
|
+
// An unavailable supplementary source is not zero stock or an empty catalog.
|
|
91
|
+
console.warn('[ProductList] Supplementary product data could not be refreshed');
|
|
92
|
+
return products;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
71
95
|
async storeChange(path, value) {
|
|
72
96
|
// No longer needed - products are stored as plain objects instead of Product instances
|
|
73
97
|
}
|
|
@@ -197,12 +221,13 @@ class ProductList extends _BaseModule.BaseModule {
|
|
|
197
221
|
queryPayload,
|
|
198
222
|
handledByOsServer
|
|
199
223
|
});
|
|
200
|
-
await this.
|
|
201
|
-
|
|
224
|
+
const enrichedCallbackList = await this.enrichCatalog(resolvedCallbackList, 'catalog-update');
|
|
225
|
+
await this.addProduct(enrichedCallbackList);
|
|
226
|
+
originalCallback(enrichedCallbackList === callbackList ? result : {
|
|
202
227
|
...result,
|
|
203
228
|
data: {
|
|
204
229
|
...result.data,
|
|
205
|
-
list:
|
|
230
|
+
list: enrichedCallbackList
|
|
206
231
|
}
|
|
207
232
|
});
|
|
208
233
|
} : undefined;
|
|
@@ -222,7 +247,8 @@ class ProductList extends _BaseModule.BaseModule {
|
|
|
222
247
|
queryPayload,
|
|
223
248
|
handledByOsServer
|
|
224
249
|
});
|
|
225
|
-
const
|
|
250
|
+
const enrichedList = await this.enrichCatalog(resolvedList, 'catalog');
|
|
251
|
+
const sortedList = enrichedList.slice().sort((a, b) => Number(b.sort) - Number(a.sort));
|
|
226
252
|
// if (sortedList.length) {
|
|
227
253
|
// sortedList.forEach((n: any) => {
|
|
228
254
|
// if (n.is_eject !== 1 && n['schedule.ids'] && n['schedule.ids'].length) {
|
|
@@ -230,7 +256,7 @@ class ProductList extends _BaseModule.BaseModule {
|
|
|
230
256
|
// }
|
|
231
257
|
// })
|
|
232
258
|
// }
|
|
233
|
-
this.addProduct(sortedList);
|
|
259
|
+
await this.addProduct(sortedList);
|
|
234
260
|
return sortedList;
|
|
235
261
|
}
|
|
236
262
|
async loadProductsPrice({
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { ProductData } from '../Product/types';
|
|
2
|
+
/** Runtime-only enrichment; never serialized into a product query. */
|
|
3
|
+
export interface ProductEnrichmentContext {
|
|
4
|
+
reason: 'catalog' | 'catalog-update' | 'detail-open';
|
|
5
|
+
businessCode?: string;
|
|
6
|
+
channel?: string;
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
}
|
|
9
|
+
export type ProductEnricher = (products: ProductData[], context: ProductEnrichmentContext) => Promise<ProductData[]>;
|
|
2
10
|
export interface ProductListLoadProductsParams {
|
|
3
11
|
category_ids?: number[];
|
|
4
12
|
product_ids?: number[];
|
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.ProductListHooks = void 0;
|
|
7
|
+
/** Runtime-only enrichment; never serialized into a product query. */
|
|
7
8
|
let ProductListHooks = exports.ProductListHooks = /*#__PURE__*/function (ProductListHooks) {
|
|
8
9
|
ProductListHooks["onGetProducts"] = "productList:onGetProducts";
|
|
9
10
|
ProductListHooks["onGetProduct"] = "productList:onGetProduct";
|
|
@@ -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 | 2 | 1 |
|
|
329
|
+
weekNum: 0 | 2 | 1 | 6 | 4 | 5 | 3;
|
|
330
330
|
}[]>;
|
|
331
331
|
submitTimeSlot(timeSlots: TimeSliceItem): void;
|
|
332
332
|
private getScheduleDataByIds;
|
|
@@ -2,7 +2,7 @@ import type { Module, ModuleOptions, PisellCore } from '../../types';
|
|
|
2
2
|
import type { ProductData } from '../../modules';
|
|
3
3
|
import { type InvoiceLayoutConfig, type InvoiceTemplateSettings, type OpenDataConfig } from '../../modules/OpenData';
|
|
4
4
|
import type { LoadScheduleAvailableDateParams, ScheduleItem } from '../../modules/Schedule/types';
|
|
5
|
-
import type { ProductListLoadProductsParams, ProductListStoreQueryParams } from '../../modules/ProductList';
|
|
5
|
+
import type { ProductListLoadProductsParams, ProductListStoreQueryParams, ProductEnrichmentContext } from '../../modules/ProductList';
|
|
6
6
|
import type { AvailabilityDateRange, AvailabilityOccupancy, AvailabilityProduct, AvailabilityResource, GetProductTimeRangesRequest } from '../../modules/ResourcePlanner';
|
|
7
7
|
import type { AvailabilityContextRef, ContextualAvailabilityQuery, ContextualAvailabilityQueryResult, ContextualCommitAvailabilitySelectionParams, ContextualProductTimeRangeGroup, UnifiedBookingSalesAddRetailProductParams, UnifiedBookingSalesApplyPlannerDiscountsParams, UnifiedBookingSalesCommitAvailabilitySelectionResult, UnifiedBookingSalesDiscountResult, UnifiedBookingSalesLoadProductsByScheduleDateParams, UnifiedBookingSalesLoadOpenDataParams, UnifiedBookingSalesPrepareAvailabilityProjectionParams, UnifiedBookingSalesPolicyResponse, UnifiedBookingSalesResolveProductAllergyRequirementParams, UnifiedBookingSalesStageRetailProductsParams, UnifiedBookingSalesState } from './types';
|
|
8
8
|
import type { BusinessAvailability } from '../../modules/BusinessAvailability';
|
|
@@ -205,6 +205,12 @@ export declare class UnifiedBookingSalesImpl extends BookingTicket {
|
|
|
205
205
|
getProductCatalog(): {
|
|
206
206
|
products: ProductData[];
|
|
207
207
|
};
|
|
208
|
+
/** Detail reads share the catalog adapter, including when basic detail is cached. */
|
|
209
|
+
enrichProducts(products: ProductData[], context: ProductEnrichmentContext): Promise<ProductData[]>;
|
|
210
|
+
/** Apply only enrichment changes, never the cart's selection/price snapshot. */
|
|
211
|
+
applyProductEnrichment(before: ProductData, after: ProductData, context?: {
|
|
212
|
+
signal?: AbortSignal;
|
|
213
|
+
}): Promise<void>;
|
|
208
214
|
/**
|
|
209
215
|
* Read-only compatibility view for salesSdk skins. It reconnects a booking to its product but
|
|
210
216
|
* must not be used to mutate tempOrder; all writes go through BaseSales / planner APIs.
|
|
@@ -2033,7 +2033,17 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
2033
2033
|
schedule_date,
|
|
2034
2034
|
schedule_datetime,
|
|
2035
2035
|
cacheId: this.cacheId
|
|
2036
|
-
}, options
|
|
2036
|
+
}, options?.callback && typeof this.otherParams.enrichProducts === 'function' ? {
|
|
2037
|
+
...options,
|
|
2038
|
+
callback: response => {
|
|
2039
|
+
const updates = response?.data?.list;
|
|
2040
|
+
if (Array.isArray(updates)) {
|
|
2041
|
+
// Subscription responses are the current query's complete visible list.
|
|
2042
|
+
this.unifiedProductCatalog = [...updates];
|
|
2043
|
+
}
|
|
2044
|
+
options.callback?.(response);
|
|
2045
|
+
}
|
|
2046
|
+
} : options);
|
|
2037
2047
|
this.unifiedProductCatalog = Array.isArray(result) ? [...result] : [];
|
|
2038
2048
|
await this.core.effects.emit(`${this.name}:onProductsLoaded`, result);
|
|
2039
2049
|
return result;
|
|
@@ -2114,6 +2124,75 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
2114
2124
|
};
|
|
2115
2125
|
}
|
|
2116
2126
|
|
|
2127
|
+
/** Detail reads share the catalog adapter, including when basic detail is cached. */
|
|
2128
|
+
async enrichProducts(products, context) {
|
|
2129
|
+
if (!this.store.products) throw new Error('Products module is not ready');
|
|
2130
|
+
return this.store.products.enrichProducts(products, context);
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
/** Apply only enrichment changes, never the cart's selection/price snapshot. */
|
|
2134
|
+
async applyProductEnrichment(before, after, context = {}) {
|
|
2135
|
+
const id = String(before.id ?? before.product_id);
|
|
2136
|
+
if (id !== String(after.id ?? after.product_id)) {
|
|
2137
|
+
throw new Error('Product enrichment changed product identity');
|
|
2138
|
+
}
|
|
2139
|
+
const patch = {};
|
|
2140
|
+
Object.keys(after).forEach(key => {
|
|
2141
|
+
if (['id', 'product_id', '_extend'].includes(key)) return;
|
|
2142
|
+
if (!(0, _lodashEs.isEqual)(before[key], after[key])) patch[key] = (0, _lodashEs.cloneDeep)(after[key]);
|
|
2143
|
+
});
|
|
2144
|
+
if (!Object.keys(patch).length) return;
|
|
2145
|
+
const cached = (await this.store.products?.queryProducts({
|
|
2146
|
+
includeIds: [id]
|
|
2147
|
+
})) || [];
|
|
2148
|
+
if (context.signal?.aborted) return;
|
|
2149
|
+
const applyPatch = product => {
|
|
2150
|
+
const next = {
|
|
2151
|
+
...product,
|
|
2152
|
+
...patch
|
|
2153
|
+
};
|
|
2154
|
+
// A detail can contain only the selected variants; keep the catalog's other SKUs
|
|
2155
|
+
// and update only fields that the adapter actually changed.
|
|
2156
|
+
if (Array.isArray(patch.variant) && Array.isArray(product.variant)) {
|
|
2157
|
+
const variantId = variant => {
|
|
2158
|
+
const value = variant?.id ?? variant?.product_variant_id;
|
|
2159
|
+
return value == null ? null : String(value);
|
|
2160
|
+
};
|
|
2161
|
+
next.variant = product.variant.map(variant => {
|
|
2162
|
+
if (variantId(variant) === null) return variant;
|
|
2163
|
+
const previous = before.variant?.find(item => variantId(item) === variantId(variant));
|
|
2164
|
+
const enriched = after.variant?.find(item => variantId(item) === variantId(variant));
|
|
2165
|
+
if (!previous || !enriched) return variant;
|
|
2166
|
+
const variantPatch = {};
|
|
2167
|
+
Object.keys(enriched).forEach(key => {
|
|
2168
|
+
if (key !== 'id' && key !== 'product_variant_id' && !(0, _lodashEs.isEqual)(previous[key], enriched[key])) {
|
|
2169
|
+
variantPatch[key] = (0, _lodashEs.cloneDeep)(enriched[key]);
|
|
2170
|
+
}
|
|
2171
|
+
});
|
|
2172
|
+
return {
|
|
2173
|
+
...variant,
|
|
2174
|
+
...variantPatch
|
|
2175
|
+
};
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
return next;
|
|
2179
|
+
};
|
|
2180
|
+
const catalogUpdates = [];
|
|
2181
|
+
this.unifiedProductCatalog = this.unifiedProductCatalog.map(product => {
|
|
2182
|
+
if (String(product.id ?? product.product_id) !== id) return product;
|
|
2183
|
+
const next = applyPatch(product);
|
|
2184
|
+
catalogUpdates.push(next);
|
|
2185
|
+
return next;
|
|
2186
|
+
});
|
|
2187
|
+
// A cart product may no longer be in the visible menu but still have a shared cache entry.
|
|
2188
|
+
const updates = cached.length ? cached.map(applyPatch) : catalogUpdates;
|
|
2189
|
+
if (updates.length) {
|
|
2190
|
+
await this.store.products?.addProduct(updates);
|
|
2191
|
+
if (context.signal?.aborted) return;
|
|
2192
|
+
await this.core.effects.emit(`${this.name}:onProductsLoaded`, this.getProductCatalog().products);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2117
2196
|
/**
|
|
2118
2197
|
* Read-only compatibility view for salesSdk skins. It reconnects a booking to its product but
|
|
2119
2198
|
* must not be used to mutate tempOrder; all writes go through BaseSales / planner APIs.
|