diff --git a/src/app/core/facades/product-context.facade.spec.ts b/src/app/core/facades/product-context.facade.spec.ts index ae80ad4a96..14e9c431ec 100644 --- a/src/app/core/facades/product-context.facade.spec.ts +++ b/src/app/core/facades/product-context.facade.spec.ts @@ -3,7 +3,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { omit, pick } from 'lodash-es'; import { BehaviorSubject, EMPTY, Observable, Subject, of } from 'rxjs'; import { map, mapTo, switchMapTo } from 'rxjs/operators'; -import { anyString, anything, instance, mock, when } from 'ts-mockito'; +import { anyString, anything, instance, mock, verify, when } from 'ts-mockito'; import { AttributeGroup } from 'ish-core/models/attribute-group/attribute-group.model'; import { AttributeGroupTypes } from 'ish-core/models/attribute-group/attribute-group.types'; @@ -33,6 +33,7 @@ describe('Product Context Facade', () => { beforeEach(() => { shoppingFacade = mock(ShoppingFacade); + when(shoppingFacade.productLinks$(anything())).thenReturn(of({})); when(shoppingFacade.productParts$(anything())).thenReturn(EMPTY); when(shoppingFacade.category$(anything())).thenReturn(of(undefined)); @@ -54,6 +55,8 @@ describe('Product Context Facade', () => { "requiredCompletenessLevel": 2, } `); + verify(shoppingFacade.productLinks$(anything())).never(); + verify(shoppingFacade.promotions$(anything())).never(); expect(context.get('loading')).toBeFalsy(); }); @@ -289,6 +292,17 @@ describe('Product Context Facade', () => { }); }); + describe('lazy property handling', () => { + it('should not load product links until subscription', done => { + verify(shoppingFacade.productLinks$(anything())).never(); + + context.select('links').subscribe(() => { + verify(shoppingFacade.productLinks$(anything())).once(); + done(); + }); + }); + }); + it('should set correct display properties for product', () => { expect(context.get('displayProperties')).toMatchInlineSnapshot(` Object { @@ -425,19 +439,22 @@ describe('Product Context Facade', () => { context.set('sku', () => '123'); }); - it('should set parts property for retail set', () => { - expect(context.get('parts')).toMatchInlineSnapshot(` - Array [ - Object { - "quantity": 1, - "sku": "p1", - }, - Object { - "quantity": 1, - "sku": "p2", - }, - ] - `); + it('should set parts property for retail set', done => { + context.select('parts').subscribe(parts => { + expect(parts).toMatchInlineSnapshot(` + Array [ + Object { + "quantity": 1, + "sku": "p1", + }, + Object { + "quantity": 1, + "sku": "p2", + }, + ] + `); + done(); + }); }); it('should set correct display properties for retail set', () => { @@ -492,19 +509,22 @@ describe('Product Context Facade', () => { context.set('sku', () => '123'); }); - it('should set parts property for bundle', () => { - expect(context.get('parts')).toMatchInlineSnapshot(` - Array [ - Object { - "quantity": 1, - "sku": "p1", - }, - Object { - "quantity": 2, - "sku": "p2", - }, - ] - `); + it('should set parts property for bundle', done => { + context.select('parts').subscribe(parts => { + expect(parts).toMatchInlineSnapshot(` + Array [ + Object { + "quantity": 1, + "sku": "p1", + }, + Object { + "quantity": 2, + "sku": "p2", + }, + ] + `); + done(); + }); }); it('should set correct display properties for bundle', () => { diff --git a/src/app/core/facades/product-context.facade.ts b/src/app/core/facades/product-context.facade.ts index 2375f12e51..fa58e717e9 100644 --- a/src/app/core/facades/product-context.facade.ts +++ b/src/app/core/facades/product-context.facade.ts @@ -7,9 +7,11 @@ import { debounceTime, distinctUntilChanged, filter, first, map, skip, startWith import { AttributeGroupTypes } from 'ish-core/models/attribute-group/attribute-group.types'; import { Image } from 'ish-core/models/image/image.model'; +import { ProductLinksDictionary } from 'ish-core/models/product-links/product-links.model'; import { ProductVariationHelper } from 'ish-core/models/product-variation/product-variation.helper'; import { ProductView } from 'ish-core/models/product-view/product-view.model'; import { ProductCompletenessLevel, ProductHelper, SkuQuantityType } from 'ish-core/models/product/product.model'; +import { Promotion } from 'ish-core/models/promotion/promotion.model'; import { generateProductUrl } from 'ish-core/routing/product/product.route'; import { whenTruthy } from 'ish-core/utils/operators'; @@ -80,9 +82,13 @@ export interface ProductContext { loading: boolean; label: string; categoryId: string; - displayProperties: Partial; + // lazy + links: ProductLinksDictionary; + promotions: Promotion[]; + parts: SkuQuantityType[]; + // variation handling variationCount: number; @@ -99,7 +105,6 @@ export interface ProductContext { hasQuantityError: boolean; // child contexts - parts: SkuQuantityType[]; propagateActive: boolean; children: ProductContext[]; } @@ -108,6 +113,7 @@ export interface ProductContext { export class ProductContextFacade extends RxState { private privateConfig$ = new BehaviorSubject>({}); private loggingActive = false; + private lazyFieldsInitialized: string[] = []; set config(config: Partial) { this.privateConfig$.next(config); @@ -231,14 +237,6 @@ export class ProductContextFacade extends RxState { ) ); - this.connect( - 'parts', - this.select('sku').pipe( - whenTruthy(), - switchMap(sku => this.shoppingFacade.productParts$(sku)) - ) - ); - const externalDisplayPropertyProviders = injector .get(EXTERNAL_DISPLAY_PROPERTY_PROVIDER, []) .map(edp => edp.setup(this.select('product'))); @@ -275,6 +273,49 @@ export class ProductContextFacade extends RxState { } } + select(): Observable; + select(k1: K1): Observable; + select( + k1: K1, + k2: K2 + ): Observable; + + select(k1?: K1, k2?: K2) { + const wrap = (key: K, obs: Observable) => { + if (!this.lazyFieldsInitialized.includes(key)) { + this.connect(key, obs); + this.lazyFieldsInitialized.push(key); + } + }; + + switch (k1) { + case 'links': + wrap('links', this.shoppingFacade.productLinks$(this.select('sku'))); + break; + case 'promotions': + wrap( + 'promotions', + combineLatest([this.select('displayProperties', 'promotions'), this.select('product', 'promotionIds')]).pipe( + filter(([visible]) => !!visible), + switchMap(([, ids]) => this.shoppingFacade.promotions$(ids)) + ) + ); + break; + case 'parts': + wrap( + 'parts', + combineLatest([ + this.select('displayProperties', 'bundleParts'), + this.select('displayProperties', 'retailSetParts'), + ]).pipe( + filter(([a, b]) => a || b), + switchMap(() => this.shoppingFacade.productParts$(this.select('sku'))) + ) + ); + } + return k2 ? super.select(k1, k2) : k1 ? super.select(k1) : super.select(); + } + log(val: boolean) { if (!this.loggingActive) { this.hold(this.select().pipe(filter(() => !!val)), ctx => { @@ -341,12 +382,4 @@ export class ProductContextFacade extends RxState { ) ); } - - productLinks$() { - return this.shoppingFacade.productLinks$(this.select('sku')); - } - - productPromotions$() { - return this.select('product', 'promotionIds').pipe(switchMap(ids => this.shoppingFacade.promotions$(ids))); - } } diff --git a/src/app/core/facades/shopping.facade.ts b/src/app/core/facades/shopping.facade.ts index 70aeb5876d..2f630203f7 100644 --- a/src/app/core/facades/shopping.facade.ts +++ b/src/app/core/facades/shopping.facade.ts @@ -36,6 +36,7 @@ import { loadProduct, loadProductIfNotLoaded, loadProductLinks, + loadProductParts, } from 'ish-core/store/shopping/products'; import { getPromotion, getPromotions, loadPromotion } from 'ish-core/store/shopping/promotions'; import { @@ -119,6 +120,7 @@ export class ShoppingFacade { productLinks$(sku: string | Observable) { return toObservable(sku).pipe( + whenTruthy(), tap(plainSKU => { this.store.dispatch(loadProductLinks({ sku: plainSKU })); }), @@ -126,10 +128,16 @@ export class ShoppingFacade { ); } - // PRODUCT RETAILSET / BUNDLES + // PRODUCT RETAIL SET / BUNDLES - productParts$(sku: string) { - return this.store.pipe(select(getProductParts(sku))); + productParts$(sku: string | Observable) { + return toObservable(sku).pipe( + whenTruthy(), + tap(plainSKU => { + this.store.dispatch(loadProductParts({ sku: plainSKU })); + }), + switchMap(plainSKU => this.store.pipe(select(getProductParts(plainSKU)))) + ); } // SEARCH diff --git a/src/app/core/store/shopping/products/products.actions.ts b/src/app/core/store/shopping/products/products.actions.ts index a99d533e7f..b622e1297a 100644 --- a/src/app/core/store/shopping/products/products.actions.ts +++ b/src/app/core/store/shopping/products/products.actions.ts @@ -53,14 +53,11 @@ export const loadProductVariationsSuccess = createAction( payload<{ sku: string; variations: string[]; defaultVariation: string }>() ); -export const loadProductBundlesSuccess = createAction( - '[Products API] Load Product Bundles Success', - payload<{ sku: string; bundledProducts: SkuQuantityType[] }>() -); +export const loadProductParts = createAction('[Products API] Load Product Parts', payload<{ sku: string }>()); -export const loadRetailSetSuccess = createAction( - '[Products API] Load Retail Set Success', - payload<{ sku: string; parts: string[] }>() +export const loadProductPartsSuccess = createAction( + '[Products API] Load Product Parts Success', + payload<{ sku: string; parts: SkuQuantityType[] }>() ); export const loadProductLinks = createAction('[Products Internal] Load Product Links', payload<{ sku: string }>()); diff --git a/src/app/core/store/shopping/products/products.effects.spec.ts b/src/app/core/store/shopping/products/products.effects.spec.ts index 805f325595..ad8ec9d1a1 100644 --- a/src/app/core/store/shopping/products/products.effects.spec.ts +++ b/src/app/core/store/shopping/products/products.effects.spec.ts @@ -25,6 +25,7 @@ import { loadProductLinks, loadProductLinksFail, loadProductLinksSuccess, + loadProductParts, loadProductSuccess, loadProductVariationsFail, loadProductVariationsIfNotLoaded, @@ -54,14 +55,6 @@ describe('Products Effects', () => { } }); - when(productsServiceMock.getProductBundles(anything())).thenCall((sku: string) => { - if (!sku) { - return throwError(makeHttpError({ message: 'invalid' })); - } else { - return of({ product: { sku }, stubs: [] }); - } - }); - when(productsServiceMock.getCategoryProducts('123', anyNumber(), anything())).thenReturn( of({ sortableAttributes: [{ name: 'name-asc' }, { name: 'name-desc' }], @@ -96,19 +89,6 @@ describe('Products Effects', () => { store$.dispatch(setProductListingPageSize({ itemsPerPage: TestBed.inject(PRODUCT_LISTING_ITEMS_PER_PAGE) })); }); - describe('loadProductBundles$', () => { - it('should call the productsService for LoadProductBundles action', done => { - const sku = 'P123'; - const action = loadProductSuccess({ product: { sku, type: 'Bundle' } as Product }); - actions$ = of(action); - - effects.loadProductBundles$.subscribe(() => { - verify(productsServiceMock.getProductBundles(sku)).once(); - done(); - }); - }); - }); - describe('loadProduct$', () => { it('should call the productsService for LoadProduct action', done => { const sku = 'P123'; @@ -410,52 +390,76 @@ describe('Products Effects', () => { }); }); - describe('loadProductBundles$', () => { - it('should load stubs and bundle reference when queried', done => { - when(productsServiceMock.getProductBundles('ABC')).thenReturn( - of({ - stubs: [{ sku: 'A' }, { sku: 'B' }], - bundledProducts: [ - { sku: 'A', quantity: 1 }, - { sku: 'B', quantity: 1 }, - ], - }) - ); + describe('loadProductParts$', () => { + describe('with bundle', () => { + beforeEach(() => { + store$.dispatch(loadProductSuccess({ product: { sku: 'ABC', type: 'Bundle' } as Product })); - actions$ = of(loadProductSuccess({ product: { sku: 'ABC', type: 'Bundle' } as Product })); + when(productsServiceMock.getProductBundles('ABC')).thenReturn(of({ stubs: [], bundledProducts: [] })); + }); - effects.loadProductBundles$.pipe(toArray()).subscribe(actions => { - expect(actions).toMatchInlineSnapshot(` - [Products API] Load Product Success: - product: {"sku":"A"} - [Products API] Load Product Success: - product: {"sku":"B"} - [Products API] Load Product Bundles Success: - sku: "ABC" - bundledProducts: [{"sku":"A","quantity":1},{"sku":"B","quantity":1}] - `); - done(); + it('should call the products service for loading bundle information', done => { + const action = loadProductParts({ sku: 'ABC' }); + actions$ = of(action); + + effects.loadProductParts$.subscribe(() => { + verify(productsServiceMock.getProductBundles(anything())).once(); + const [sku] = capture(productsServiceMock.getProductBundles).last(); + expect(sku).toMatchInlineSnapshot(`"ABC"`); + done(); + }); + }); + + it('should load stubs and bundle reference when queried', done => { + when(productsServiceMock.getProductBundles('ABC')).thenReturn( + of({ + stubs: [{ sku: 'A' }, { sku: 'B' }], + bundledProducts: [ + { sku: 'A', quantity: 1 }, + { sku: 'B', quantity: 1 }, + ], + }) + ); + + actions$ = of(loadProductParts({ sku: 'ABC' })); + + effects.loadProductParts$.pipe(toArray()).subscribe(actions => { + expect(actions).toMatchInlineSnapshot(` + [Products API] Load Product Success: + product: {"sku":"A"} + [Products API] Load Product Success: + product: {"sku":"B"} + [Products API] Load Product Parts Success: + sku: "ABC" + parts: [{"sku":"A","quantity":1},{"sku":"B","quantity":1}] + `); + done(); + }); }); }); - }); - describe('loadPartsOfRetailSet$', () => { - it('should load stubs and retail set reference when queried', done => { - when(productsServiceMock.getRetailSetParts('ABC')).thenReturn(of([{ sku: 'A' }, { sku: 'B' }])); + describe('with retail set', () => { + beforeEach(() => { + store$.dispatch(loadProductSuccess({ product: { sku: 'ABC', type: 'RetailSet' } as Product })); + }); + + it('should load stubs and retail set reference when queried', done => { + when(productsServiceMock.getRetailSetParts('ABC')).thenReturn(of([{ sku: 'A' }, { sku: 'B' }])); - actions$ = of(loadProductSuccess({ product: { sku: 'ABC', type: 'RetailSet' } as Product })); + actions$ = of(loadProductParts({ sku: 'ABC' })); - effects.loadPartsOfRetailSet$.pipe(toArray()).subscribe(actions => { - expect(actions).toMatchInlineSnapshot(` - [Products API] Load Product Success: - product: {"sku":"A"} - [Products API] Load Product Success: - product: {"sku":"B"} - [Products API] Load Retail Set Success: - sku: "ABC" - parts: ["A","B"] - `); - done(); + effects.loadProductParts$.pipe(toArray()).subscribe(actions => { + expect(actions).toMatchInlineSnapshot(` + [Products API] Load Product Success: + product: {"sku":"A"} + [Products API] Load Product Success: + product: {"sku":"B"} + [Products API] Load Product Parts Success: + sku: "ABC" + parts: [{"sku":"A","quantity":1},{"sku":"B","quantity":1}] + `); + done(); + }); }); }); }); diff --git a/src/app/core/store/shopping/products/products.effects.ts b/src/app/core/store/shopping/products/products.effects.ts index 9a884997c3..ee3defd005 100644 --- a/src/app/core/store/shopping/products/products.effects.ts +++ b/src/app/core/store/shopping/products/products.effects.ts @@ -18,6 +18,7 @@ import { mergeMap, switchMap, switchMapTo, + take, throttleTime, withLatestFrom, } from 'rxjs/operators'; @@ -41,12 +42,13 @@ import { import { loadProduct, - loadProductBundlesSuccess, loadProductFail, loadProductIfNotLoaded, loadProductLinks, loadProductLinksFail, loadProductLinksSuccess, + loadProductParts, + loadProductPartsSuccess, loadProductSuccess, loadProductVariationsFail, loadProductVariationsIfNotLoaded as loadProductVariationsIfNotLoaded, @@ -55,11 +57,12 @@ import { loadProductsForCategoryFail, loadProductsForMaster, loadProductsForMasterFail, - loadRetailSetSuccess, } from './products.actions'; import { getBreadcrumbForProductPage, + getProduct, getProductEntities, + getProductParts, getProductVariationSKUs, getSelectedProduct, } from './products.selectors'; @@ -169,18 +172,44 @@ export class ProductsEffects { ) ); - loadProductBundles$ = createEffect(() => + loadProductParts$ = createEffect(() => this.actions$.pipe( - ofType(loadProductSuccess), - mapToPayloadProperty('product'), - filter(product => ProductHelper.isProductBundle(product)), - mergeMap(({ sku }) => - this.productsService.getProductBundles(sku).pipe( - mergeMap(({ stubs, bundledProducts }) => [ - ...stubs.map((product: Product) => loadProductSuccess({ product })), - loadProductBundlesSuccess({ sku, bundledProducts }), - ]), - mapErrorToAction(loadProductFail, { sku }) + ofType(loadProductParts), + mapToPayloadProperty('sku'), + groupBy(identity), + mergeMap(group$ => + group$.pipe( + this.throttleOnBrowser(), + mergeMap(sku => + this.store.pipe( + select(getProductParts(sku)), + first(), + filter(parts => !parts?.length), + switchMap(() => this.store.pipe(select(getProduct(sku)))), + filter(product => ProductHelper.isProductBundle(product) || ProductHelper.isRetailSet(product)), + take(1) + ) + ), + exhaustMap(product => + ProductHelper.isProductBundle(product) + ? this.productsService.getProductBundles(product.sku).pipe( + mergeMap(({ stubs, bundledProducts: parts }) => [ + ...stubs.map((stub: Product) => loadProductSuccess({ product: stub })), + loadProductPartsSuccess({ sku: product.sku, parts }), + ]), + mapErrorToAction(loadProductFail, { sku: product.sku }) + ) + : this.productsService.getRetailSetParts(product.sku).pipe( + mergeMap(stubs => [ + ...stubs.map((stub: Product) => loadProductSuccess({ product: stub })), + loadProductPartsSuccess({ + sku: product.sku, + parts: stubs.map(stub => ({ sku: stub.sku, quantity: 1 })), + }), + ]), + mapErrorToAction(loadProductFail, { sku: product.sku }) + ) + ) ) ) ) @@ -260,25 +289,6 @@ export class ProductsEffects { ) ); - loadPartsOfRetailSet$ = createEffect(() => - this.actions$.pipe( - ofType(loadProductSuccess), - mapToPayloadProperty('product'), - filter(ProductHelper.isRetailSet), - mapToProperty('sku'), - mergeMap(sku => - this.productsService - .getRetailSetParts(sku) - .pipe( - mergeMap(stubs => [ - ...stubs.map((product: Product) => loadProductSuccess({ product })), - loadRetailSetSuccess({ sku, parts: stubs.map(p => p.sku) }), - ]) - ) - ) - ) - ); - redirectIfErrorInProducts$ = createEffect( () => combineLatest([ diff --git a/src/app/core/store/shopping/products/products.reducer.ts b/src/app/core/store/shopping/products/products.reducer.ts index b702f88028..74fef148bb 100644 --- a/src/app/core/store/shopping/products/products.reducer.ts +++ b/src/app/core/store/shopping/products/products.reducer.ts @@ -5,13 +5,12 @@ import { ProductLinksDictionary } from 'ish-core/models/product-links/product-li import { AllProductTypes, SkuQuantityType } from 'ish-core/models/product/product.model'; import { - loadProductBundlesSuccess, loadProductFail, loadProductLinksSuccess, + loadProductPartsSuccess, loadProductSuccess, loadProductVariationsFail, loadProductVariationsSuccess, - loadRetailSetSuccess, productSpecialUpdate, } from './products.actions'; @@ -68,13 +67,9 @@ export const productsReducer = createReducer( variations: { ...state.variations, [action.payload.sku]: action.payload.variations }, defaultVariation: { ...state.defaultVariation, [action.payload.sku]: action.payload.defaultVariation }, })), - on(loadProductBundlesSuccess, (state, action) => ({ + on(loadProductPartsSuccess, (state, action) => ({ ...state, - parts: { ...state.parts, [action.payload.sku]: action.payload.bundledProducts }, - })), - on(loadRetailSetSuccess, (state, action) => ({ - ...state, - parts: { ...state.parts, [action.payload.sku]: action.payload.parts.map(sku => ({ sku, quantity: 1 })) }, + parts: { ...state.parts, [action.payload.sku]: action.payload.parts }, })), on(loadProductLinksSuccess, (state, action) => ({ ...state, diff --git a/src/app/core/store/shopping/products/products.selectors.spec.ts b/src/app/core/store/shopping/products/products.selectors.spec.ts index bde88f82c8..d5999c3db6 100644 --- a/src/app/core/store/shopping/products/products.selectors.spec.ts +++ b/src/app/core/store/shopping/products/products.selectors.spec.ts @@ -14,14 +14,13 @@ import { categoryTree } from 'ish-core/utils/dev/test-data-utils'; import { loadProduct, - loadProductBundlesSuccess, loadProductFail, loadProductLinksSuccess, + loadProductPartsSuccess, loadProductSuccess, loadProductVariationsFail, loadProductVariationsIfNotLoaded, loadProductVariationsSuccess, - loadRetailSetSuccess, } from './products.actions'; import { getBreadcrumbForProductPage, @@ -235,9 +234,9 @@ describe('Products Selectors', () => { it('should contain information for the product bundle', () => { store$.dispatch(loadProductSuccess({ product: { sku: 'ABC' } as Product })); store$.dispatch( - loadProductBundlesSuccess({ + loadProductPartsSuccess({ sku: 'ABC', - bundledProducts: [ + parts: [ { sku: 'A', quantity: 1 }, { sku: 'B', quantity: 2 }, ], @@ -263,9 +262,12 @@ describe('Products Selectors', () => { it('should contain information for the product retail set', () => { store$.dispatch(loadProductSuccess({ product: { sku: 'ABC' } as Product })); store$.dispatch( - loadRetailSetSuccess({ + loadProductPartsSuccess({ sku: 'ABC', - parts: ['A', 'B'], + parts: [ + { sku: 'A', quantity: 1 }, + { sku: 'B', quantity: 1 }, + ], }) ); diff --git a/src/app/pages/product/product-links/product-links.component.ts b/src/app/pages/product/product-links/product-links.component.ts index ec6114fe55..3720ae90a9 100644 --- a/src/app/pages/product/product-links/product-links.component.ts +++ b/src/app/pages/product/product-links/product-links.component.ts @@ -25,6 +25,6 @@ export class ProductLinksComponent implements OnInit { constructor(private context: ProductContextFacade) {} ngOnInit() { - this.links$ = this.context.productLinks$(); + this.links$ = this.context.select('links'); } } diff --git a/src/app/shared/components/product/product-promotion/product-promotion.component.spec.ts b/src/app/shared/components/product/product-promotion/product-promotion.component.spec.ts index 529ecfc385..68bff067bf 100644 --- a/src/app/shared/components/product/product-promotion/product-promotion.component.spec.ts +++ b/src/app/shared/components/product/product-promotion/product-promotion.component.spec.ts @@ -19,7 +19,7 @@ describe('Product Promotion Component', () => { beforeEach(async () => { context = mock(ProductContextFacade); when(context.select('displayProperties', 'promotions')).thenReturn(of(true)); - when(context.productPromotions$()).thenReturn(EMPTY); + when(context.select('promotions')).thenReturn(EMPTY); await TestBed.configureTestingModule({ declarations: [ @@ -44,7 +44,7 @@ describe('Product Promotion Component', () => { }); it('should display the promotion when supplied', () => { - when(context.productPromotions$()).thenReturn( + when(context.select('promotions')).thenReturn( of([ { id: 'PROMO_UUID', diff --git a/src/app/shared/components/product/product-promotion/product-promotion.component.ts b/src/app/shared/components/product/product-promotion/product-promotion.component.ts index 40620b5cfa..34406c9674 100644 --- a/src/app/shared/components/product/product-promotion/product-promotion.component.ts +++ b/src/app/shared/components/product/product-promotion/product-promotion.component.ts @@ -19,6 +19,6 @@ export class ProductPromotionComponent implements OnInit { ngOnInit() { this.visible$ = this.context.select('displayProperties', 'promotions'); - this.promotions$ = this.context.productPromotions$(); + this.promotions$ = this.context.select('promotions'); } }