From 34a1be2a194b7e0b40b4b2b60e2d2a6805ed79c4 Mon Sep 17 00:00:00 2001 From: Danilo Hoffmann Date: Tue, 29 Mar 2022 13:00:23 +0200 Subject: [PATCH] feat: introduce price update strategies --- src/app/core/configurations/injection-keys.ts | 8 ++++ .../core/facades/product-context.facade.ts | 15 +++++- src/app/core/facades/shopping.facade.ts | 24 ++++++++-- src/app/core/models/price/price.model.ts | 2 + .../products/products.effects.spec.ts | 15 ------ .../shopping/products/products.effects.ts | 11 ----- .../store/shopping/shopping-store.spec.ts | 48 ------------------- src/environments/environment.model.ts | 8 ++++ 8 files changed, 50 insertions(+), 81 deletions(-) diff --git a/src/app/core/configurations/injection-keys.ts b/src/app/core/configurations/injection-keys.ts index 491280394f..604d97560c 100644 --- a/src/app/core/configurations/injection-keys.ts +++ b/src/app/core/configurations/injection-keys.ts @@ -1,6 +1,7 @@ import { InjectionToken } from '@angular/core'; import { CookieConsentOptions } from 'ish-core/models/cookies/cookies.model'; +import { PriceUpdateType } from 'ish-core/models/price/price.model'; import { ViewType } from 'ish-core/models/viewtype/viewtype.types'; import { DataRetentionPolicy } from 'ish-core/utils/meta-reducers'; @@ -58,6 +59,13 @@ export const DATA_RETENTION_POLICY = new InjectionToken('da factory: () => environment.dataRetention, }); +/** + * the configured price update policy for the application + */ +export const PRICE_UPDATE = new InjectionToken('priceUpdate', { + factory: () => environment.priceUpdate, +}); + /** * the configured theme color */ diff --git a/src/app/core/facades/product-context.facade.ts b/src/app/core/facades/product-context.facade.ts index c908d56917..1bd0337128 100644 --- a/src/app/core/facades/product-context.facade.ts +++ b/src/app/core/facades/product-context.facade.ts @@ -359,9 +359,20 @@ export class ProductContextFacade extends RxState { case 'prices': wrap( 'prices', - combineLatest([this.select('displayProperties', 'price'), this.select('product', 'sku')]).pipe( + combineLatest([ + this.select('displayProperties', 'price'), + this.select('product').pipe( + filter(p => !!p && !p.failed), + mapToProperty('sku'), + distinctUntilChanged() + ), + this.select('requiredCompletenessLevel').pipe( + map(completeness => completeness === true), + distinctUntilChanged() + ), + ]).pipe( filter(([visible]) => !!visible), - switchMap(([, ids]) => this.shoppingFacade.productPrices$(ids)) + switchMap(([, sku, fresh]) => this.shoppingFacade.productPrices$(sku, fresh)) ) ); break; diff --git a/src/app/core/facades/shopping.facade.ts b/src/app/core/facades/shopping.facade.ts index 40be1b0793..409f321957 100644 --- a/src/app/core/facades/shopping.facade.ts +++ b/src/app/core/facades/shopping.facade.ts @@ -1,9 +1,11 @@ -import { Injectable } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; import { Store, select } from '@ngrx/store'; -import { Observable, combineLatest } from 'rxjs'; +import { Observable, combineLatest, identity } from 'rxjs'; import { debounce, filter, map, pairwise, startWith, switchMap, tap } from 'rxjs/operators'; +import { PRICE_UPDATE } from 'ish-core/configurations/injection-keys'; import { PriceItemHelper } from 'ish-core/models/price-item/price-item.helper'; +import { PriceUpdateType } from 'ish-core/models/price/price.model'; import { ProductListingID } from 'ish-core/models/product-listing/product-listing.model'; import { ProductCompletenessLevel, ProductHelper } from 'ish-core/models/product/product.model'; import { selectRouteParam } from 'ish-core/store/core/router'; @@ -32,6 +34,7 @@ import { getProductListingViewType, loadMoreProducts, } from 'ish-core/store/shopping/product-listing'; +import { loadProductPrices } from 'ish-core/store/shopping/product-prices'; import { getProductPrice } from 'ish-core/store/shopping/product-prices/product-prices.selectors'; import { getProduct, @@ -51,7 +54,7 @@ import { whenFalsy, whenTruthy } from 'ish-core/utils/operators'; /* eslint-disable @typescript-eslint/member-ordering */ @Injectable({ providedIn: 'root' }) export class ShoppingFacade { - constructor(private store: Store) {} + constructor(private store: Store, @Inject(PRICE_UPDATE) private priceUpdate: PriceUpdateType) {} // CATEGORY @@ -113,11 +116,22 @@ export class ShoppingFacade { ); } - productPrices$(sku: string | Observable) { + productPrices$(sku: string | Observable, fresh = false) { return toObservable(sku).pipe( + whenTruthy(), switchMap(plainSKU => combineLatest([ - this.store.pipe(select(getProductPrice(plainSKU))), + this.store.pipe( + select(getProductPrice(plainSKU)), + // reset state when updates are forced + this.priceUpdate === 'always' || fresh ? startWith(undefined) : identity, + tap(prices => { + if (!prices) { + this.store.dispatch(loadProductPrices({ skus: [plainSKU] })); + } + }), + whenTruthy() + ), this.store.pipe(select(getPriceDisplayType)), ]).pipe(map(args => PriceItemHelper.selectPricing(...args))) ) diff --git a/src/app/core/models/price/price.model.ts b/src/app/core/models/price/price.model.ts index a8cb006a69..df3fea02c4 100644 --- a/src/app/core/models/price/price.model.ts +++ b/src/app/core/models/price/price.model.ts @@ -1,3 +1,5 @@ +export type PriceUpdateType = 'stable' | 'always'; + export interface Price { type: 'Money'; value: number; 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 c26b151d60..c6426bafe5 100644 --- a/src/app/core/store/shopping/products/products.effects.spec.ts +++ b/src/app/core/store/shopping/products/products.effects.spec.ts @@ -12,12 +12,10 @@ import { ProductPriceDetails } from 'ish-core/models/product-prices/product-pric import { Product, VariationProductMaster } from 'ish-core/models/product/product.model'; import { ProductsService } from 'ish-core/services/products/products.service'; import { CoreStoreModule } from 'ish-core/store/core/core-store.module'; -import { CustomerStoreModule } from 'ish-core/store/customer/customer-store.module'; import { personalizationStatusDetermined } from 'ish-core/store/customer/user/user.actions'; import { loadCategory } from 'ish-core/store/shopping/categories'; import { setProductListingPageSize } from 'ish-core/store/shopping/product-listing'; import { loadProductPricesSuccess } from 'ish-core/store/shopping/product-prices'; -import { loadProductPrices } from 'ish-core/store/shopping/product-prices/product-prices.actions'; import { ShoppingStoreModule } from 'ish-core/store/shopping/shopping-store.module'; import { makeHttpError } from 'ish-core/utils/dev/api-service-utils'; import { HttpStatusCodeService } from 'ish-core/utils/http-status-code/http-status-code.service'; @@ -66,7 +64,6 @@ describe('Products Effects', () => { TestBed.configureTestingModule({ imports: [ CoreStoreModule.forTesting(['router', 'serverConfig']), - CustomerStoreModule.forTesting('user'), RouterTestingModule.withRoutes([ { path: 'category/:categoryUniqueId/product/:sku', children: [] }, { path: 'product/:sku', children: [] }, @@ -189,18 +186,6 @@ describe('Products Effects', () => { })); }); - describe('loadProductPricesAfterProductSuccess$', () => { - it('should trigger action to load product prices after successful load product action', () => { - const sku = 'sku123'; - const action = loadProductSuccess({ product: { sku } as Product }); - const completion = loadProductPrices({ skus: [sku] }); - actions$ = hot('-a-a-a', { a: action }); - const expected$ = cold('-c-c-c', { c: completion }); - - expect(effects.loadProductPricesAfterProductSuccess$).toBeObservable(expected$); - }); - }); - describe('loadProductsForCategory$', () => { it('should call service for SKU list', done => { actions$ = of(loadProductsForCategory({ categoryId: '123', sorting: 'name-asc' })); diff --git a/src/app/core/store/shopping/products/products.effects.ts b/src/app/core/store/shopping/products/products.effects.ts index e2fdf6bf02..67e197b274 100644 --- a/src/app/core/store/shopping/products/products.effects.ts +++ b/src/app/core/store/shopping/products/products.effects.ts @@ -29,7 +29,6 @@ import { setBreadcrumbData } from 'ish-core/store/core/viewconf'; import { personalizationStatusDetermined } from 'ish-core/store/customer/user'; import { loadCategory } from 'ish-core/store/shopping/categories'; import { getProductListingItemsPerPage, setProductListingPages } from 'ish-core/store/shopping/product-listing'; -import { loadProductPrices } from 'ish-core/store/shopping/product-prices'; import { HttpStatusCodeService } from 'ish-core/utils/http-status-code/http-status-code.service'; import { delayUntil, @@ -109,16 +108,6 @@ export class ProductsEffects { ) ); - loadProductPricesAfterProductSuccess$ = createEffect(() => - this.actions$.pipe( - ofType(loadProductSuccess), - mapToPayloadProperty('product'), - mapToProperty('sku'), - whenTruthy(), - map(sku => loadProductPrices({ skus: [sku] })) - ) - ); - /** * retrieve products for category incremental respecting paging */ diff --git a/src/app/core/store/shopping/shopping-store.spec.ts b/src/app/core/store/shopping/shopping-store.spec.ts index 8f95e372d0..5a6ef7c090 100644 --- a/src/app/core/store/shopping/shopping-store.spec.ts +++ b/src/app/core/store/shopping/shopping-store.spec.ts @@ -20,7 +20,6 @@ import { ProductsService } from 'ish-core/services/products/products.service'; import { PromotionsService } from 'ish-core/services/promotions/promotions.service'; import { SuggestService } from 'ish-core/services/suggest/suggest.service'; import { CoreStoreModule } from 'ish-core/store/core/core-store.module'; -import { CustomerStoreModule } from 'ish-core/store/customer/customer-store.module'; import { personalizationStatusDetermined } from 'ish-core/store/customer/user'; import { makeHttpError } from 'ish-core/utils/dev/api-service-utils'; import { StoreWithSnapshots, provideStoreSnapshots } from 'ish-core/utils/dev/ngrx-testing'; @@ -145,7 +144,6 @@ describe('Shopping Store', () => { TestBed.configureTestingModule({ imports: [ CoreStoreModule.forTesting(['router', 'configuration', 'serverConfig'], true), - CustomerStoreModule.forTesting('user'), RouterTestingModule.withRoutes([ { path: 'home', @@ -319,10 +317,6 @@ describe('Shopping Store', () => { sortableAttributes: [] [Filter API] Load Filter Success: filterNavigation: {} - [Product Price Internal] Load Product Prices: - skus: ["P2"] - [Products API] Load Product Prices Success: - prices: [] `); })); @@ -341,11 +335,7 @@ describe('Shopping Store', () => { sku: "P2" [Products API] Load Product Success: product: {"sku":"P2","name":"nP2"} - [Product Price Internal] Load Product Prices: - skus: ["P2"] @ngrx/router-store/navigated: /product/P2 - [Products API] Load Product Prices Success: - prices: [] `); })); }); @@ -470,12 +460,6 @@ describe('Shopping Store', () => { sortableAttributes: [] [Filter API] Load Filter Success: filterNavigation: {} - [Product Price Internal] Load Product Prices: - skus: ["P1"] - [Product Price Internal] Load Product Prices: - skus: ["P2"] - [Products API] Load Product Prices Success: - prices: [] `); })); @@ -494,11 +478,7 @@ describe('Shopping Store', () => { sku: "P1" [Products API] Load Product Success: product: {"sku":"P1","name":"nP1"} - [Product Price Internal] Load Product Prices: - skus: ["P1"] @ngrx/router-store/navigated: /category/A.123.456/product/P1 - [Products API] Load Product Prices Success: - prices: [] `); })); @@ -565,10 +545,6 @@ describe('Shopping Store', () => { sortableAttributes: [] [Filter API] Load Filter Success: filterNavigation: {} - [Product Price Internal] Load Product Prices: - skus: ["P2"] - [Products API] Load Product Prices Success: - prices: [] `); })); @@ -609,17 +585,11 @@ describe('Shopping Store', () => { sortableAttributes: [] [Filter API] Load Filter Success: filterNavigation: {} - [Product Price Internal] Load Product Prices: - skus: ["P1"] - [Product Price Internal] Load Product Prices: - skus: ["P2"] @ngrx/router-store/navigated: /category/A.123.456 [Product Listing] Load More Products: id: {"type":"category","value":"A.123.456"} [Viewconf Internal] Set Breadcrumb Data: breadcrumbData: [{"text":"nA","link":"/nA-catA"},{"text":"nA123","link":"/nA... - [Products API] Load Product Prices Success: - prices: [] `); })); }); @@ -688,11 +658,7 @@ describe('Shopping Store', () => { sku: "P1" [Products API] Load Product Success: product: {"sku":"P1","name":"nP1"} - [Product Price Internal] Load Product Prices: - skus: ["P1"] @ngrx/router-store/navigated: /category/A.123.456/product/P1 - [Products API] Load Product Prices Success: - prices: [] `); })); @@ -744,17 +710,11 @@ describe('Shopping Store', () => { sortableAttributes: [] [Filter API] Load Filter Success: filterNavigation: {} - [Product Price Internal] Load Product Prices: - skus: ["P1"] - [Product Price Internal] Load Product Prices: - skus: ["P2"] @ngrx/router-store/navigated: /category/A.123.456 [Product Listing] Load More Products: id: {"type":"category","value":"A.123.456"} [Viewconf Internal] Set Breadcrumb Data: breadcrumbData: [{"text":"nA","link":"/nA-catA"},{"text":"nA123","link":"/nA... - [Products API] Load Product Prices Success: - prices: [] `); })); }); @@ -812,11 +772,7 @@ describe('Shopping Store', () => { sku: "P1" [Products API] Load Product Success: product: {"sku":"P1","name":"nP1"} - [Product Price Internal] Load Product Prices: - skus: ["P1"] @ngrx/router-store/navigated: /product/P1 - [Products API] Load Product Prices Success: - prices: [] `); })); @@ -964,10 +920,6 @@ describe('Shopping Store', () => { sortableAttributes: [] [Filter API] Load Filter Success: filterNavigation: {} - [Product Price Internal] Load Product Prices: - skus: ["P2"] - [Products API] Load Product Prices Success: - prices: [] `); })); }); diff --git a/src/environments/environment.model.ts b/src/environments/environment.model.ts index b89437a959..1bed068796 100644 --- a/src/environments/environment.model.ts +++ b/src/environments/environment.model.ts @@ -1,5 +1,6 @@ import { Auth0Config } from 'ish-core/identity-provider/auth0.identity-provider'; import { CookieConsentOptions } from 'ish-core/models/cookies/cookies.model'; +import { PriceUpdateType } from 'ish-core/models/price/price.model'; import { DeviceType, ViewType } from 'ish-core/models/viewtype/viewtype.types'; import { DataRetentionPolicy } from 'ish-core/utils/meta-reducers'; import { MultiSiteLocaleMap } from 'ish-core/utils/multi-site/multi-site.service'; @@ -112,6 +113,12 @@ export interface Environment { // enable and configure data persistence for specific stores (compare, recently, tacton) dataRetention: DataRetentionPolicy; + + /** Price update mechanism: + * - 'always': fetch fresh price information all the time + * - 'stable': only fetch prices once per application lifetime + */ + priceUpdate: PriceUpdateType; } export const ENVIRONMENT_DEFAULTS: Omit = { @@ -170,4 +177,5 @@ export const ENVIRONMENT_DEFAULTS: Omit = { recently: 60 * 24 * 7, // 1 week tacton: 'forever', }, + priceUpdate: 'always', };