diff --git a/feature-libs/product-configurator/common/components/configure-cart-entry/configure-cart-entry.component.spec.ts b/feature-libs/product-configurator/common/components/configure-cart-entry/configure-cart-entry.component.spec.ts index 25a93ae75c0..02def748fca 100644 --- a/feature-libs/product-configurator/common/components/configure-cart-entry/configure-cart-entry.component.spec.ts +++ b/feature-libs/product-configurator/common/components/configure-cart-entry/configure-cart-entry.component.spec.ts @@ -62,14 +62,23 @@ describe('ConfigureCartEntryComponent', () => { ); }); - it('should find correct owner type in case entry knows quote', () => { + it('should find correct owner type quoteEntry in case entry knows quote and it is read-only', () => { + component.readOnly = true; component.cartEntry = { quoteCode: quoteCode }; expect(component.getOwnerType()).toBe( CommonConfigurator.OwnerType.QUOTE_ENTRY ); }); + it('should find correct owner type cartEntry in case entry knows quote and it is editable', () => { + component.cartEntry = { quoteCode: quoteCode }; + expect(component.getOwnerType()).toBe( + CommonConfigurator.OwnerType.CART_ENTRY + ); + }); + it('should throw error in case both quote and order code are present', () => { + component.readOnly = true; component.cartEntry = { orderCode: orderCode, quoteCode: quoteCode }; expect(() => component.getOwnerType()).toThrowError(); }); @@ -87,6 +96,7 @@ describe('ConfigureCartEntryComponent', () => { }); it('should take order code into account in case entry is from order', () => { + component.readOnly = true; const orderCode = '01008765'; component.cartEntry = { entryNumber: 0, orderCode: orderCode }; expect(component.getEntityKey()).toBe(orderCode + '+0'); @@ -99,17 +109,26 @@ describe('ConfigureCartEntryComponent', () => { expect(component['getCode']()).toBeUndefined(); }); - it('should return a quote code', () => { + it('should return a quote code in case entry knows quote and is read-only', () => { + component.readOnly = true; component.cartEntry = { quoteCode: quoteCode }; expect(component['getCode']()).toEqual(quoteCode); }); + it('should return undefined in case entry knows quote and is editable, because then we are working on a (quote) cart', () => { + component.readOnly = false; + component.cartEntry = { quoteCode: quoteCode }; + expect(component['getCode']()).toBe(undefined); + }); + it('should return an order code', () => { + component.readOnly = true; component.cartEntry = { orderCode: orderCode }; expect(component['getCode']()).toEqual(orderCode); }); it('should throw error in case both quote and order code are present', () => { + component.readOnly = true; component.cartEntry = { orderCode: orderCode, quoteCode: quoteCode }; expect(() => component['getCode']()).toThrowError(); }); diff --git a/feature-libs/product-configurator/common/components/configure-cart-entry/configure-cart-entry.component.ts b/feature-libs/product-configurator/common/components/configure-cart-entry/configure-cart-entry.component.ts index ccc4d0c46bb..3a0fcbae9cb 100644 --- a/feature-libs/product-configurator/common/components/configure-cart-entry/configure-cart-entry.component.ts +++ b/feature-libs/product-configurator/common/components/configure-cart-entry/configure-cart-entry.component.ts @@ -38,7 +38,7 @@ export class ConfigureCartEntryComponent { * @returns - an owner type */ getOwnerType(): CommonConfigurator.OwnerType { - if (this.cartEntry.quoteCode || this.cartEntry.orderCode) { + if (this.isOrderOrQuoteRelated()) { if (!this.cartEntry.quoteCode) { return CommonConfigurator.OwnerType.ORDER_ENTRY; } @@ -76,7 +76,7 @@ export class ConfigureCartEntryComponent { * @returns Document code if order or quote bound, undefined in other cases */ protected getCode(): string | undefined { - if (this.cartEntry.quoteCode || this.cartEntry.orderCode) { + if (this.isOrderOrQuoteRelated()) { if (!this.cartEntry.quoteCode) { return this.cartEntry.orderCode; } @@ -91,6 +91,12 @@ export class ConfigureCartEntryComponent { } } + protected isOrderOrQuoteRelated(): boolean { + return this.cartEntry.quoteCode || this.cartEntry.orderCode + ? this.readOnly + : false; + } + /** * Retrieves a corresponding route depending whether the configuration is read only or not. * diff --git a/feature-libs/product-configurator/rulebased/core/facade/configurator-cart.service.spec.ts b/feature-libs/product-configurator/rulebased/core/facade/configurator-cart.service.spec.ts index b2faf775352..5feeeb58a3d 100644 --- a/feature-libs/product-configurator/rulebased/core/facade/configurator-cart.service.spec.ts +++ b/feature-libs/product-configurator/rulebased/core/facade/configurator-cart.service.spec.ts @@ -9,7 +9,6 @@ import { } from '@spartacus/checkout/base/root'; import { OCC_USER_ID_ANONYMOUS, - OCC_USER_ID_CURRENT, QueryState, StateUtils, UserIdService, @@ -37,6 +36,7 @@ let OWNER_CART_ENTRY = ConfiguratorModelUtils.createInitialOwner(); let OWNER_ORDER_ENTRY = ConfiguratorModelUtils.createInitialOwner(); let OWNER_QUOTE_ENTRY = ConfiguratorModelUtils.createInitialOwner(); let OWNER_PRODUCT = ConfiguratorModelUtils.createInitialOwner(); + const CART_CODE = '0000009336'; const CART_ENTRY_ID = '3'; const CART_GUID = 'e767605d-7336-48fd-b156-ad50d004ca10'; @@ -46,6 +46,7 @@ const ORDER_ENTRY_NUMBER = 2; const QUOTE_ENTRY_NUMBER = 4; const PRODUCT_CODE = 'CONF_LAPTOP'; const CONFIG_ID = '1234-56-7890'; +const USER_ID = 'ab3f7a08-690a-4616-b1fe-4f0847fcb79f'; const cart: Cart = { code: CART_CODE, @@ -95,7 +96,7 @@ class MockCheckoutQueryFacade { class MockUserIdService { getUserId(): Observable { - return of(OCC_USER_ID_ANONYMOUS); + return of(USER_ID); } } @@ -197,7 +198,7 @@ describe('ConfiguratorCartService', () => { owner: OWNER_CART_ENTRY, cartEntryNumber: OWNER_CART_ENTRY.id, cartId: CART_GUID, - userId: OCC_USER_ID_ANONYMOUS, + userId: USER_ID, }; const productConfigurationLoaderState: StateUtils.LoaderState = { @@ -307,7 +308,7 @@ describe('ConfiguratorCartService', () => { owner: OWNER_ORDER_ENTRY, orderEntryNumber: '' + ORDER_ENTRY_NUMBER, orderId: ORDER_ID, - userId: OCC_USER_ID_CURRENT, + userId: USER_ID, }; const productConfigurationLoaderState: StateUtils.LoaderState = { @@ -357,7 +358,7 @@ describe('ConfiguratorCartService', () => { it('should dispatch ReadQuoteEntryConfiguration action in case configuration is not present so far', () => { const params: CommonConfigurator.ReadConfigurationFromQuoteEntryParameters = { - userId: OCC_USER_ID_CURRENT, + userId: USER_ID, quoteId: QUOTE_ID, quoteEntryNumber: '' + QUOTE_ENTRY_NUMBER, owner: OWNER_QUOTE_ENTRY, @@ -391,7 +392,7 @@ describe('ConfiguratorCartService', () => { it('should get cart, create addToCartParameters and call addToCart action without setting quantity', () => { const addToCartParams: Configurator.AddToCartParameters = { cartId: CART_GUID, - userId: OCC_USER_ID_ANONYMOUS, + userId: USER_ID, productCode: PRODUCT_CODE, quantity: 1, configId: CONFIG_ID, @@ -410,7 +411,7 @@ describe('ConfiguratorCartService', () => { it('should get cart, create addToCartParameters and call addToCart action with setting quantity', () => { const addToCartParams: Configurator.AddToCartParameters = { cartId: CART_GUID, - userId: OCC_USER_ID_ANONYMOUS, + userId: USER_ID, productCode: PRODUCT_CODE, quantity: 100, configId: CONFIG_ID, @@ -431,7 +432,7 @@ describe('ConfiguratorCartService', () => { it('should create updateParameters and call updateCartEntry action', () => { const params: Configurator.UpdateConfigurationForCartEntryParameters = { cartId: CART_GUID, - userId: OCC_USER_ID_ANONYMOUS, + userId: USER_ID, cartEntryNumber: productConfiguration.owner.id, configuration: productConfiguration, }; diff --git a/feature-libs/product-configurator/rulebased/core/facade/configurator-cart.service.ts b/feature-libs/product-configurator/rulebased/core/facade/configurator-cart.service.ts index 5c858a5bebf..2d321368a4b 100644 --- a/feature-libs/product-configurator/rulebased/core/facade/configurator-cart.service.ts +++ b/feature-libs/product-configurator/rulebased/core/facade/configurator-cart.service.ts @@ -8,11 +8,7 @@ import { Injectable } from '@angular/core'; import { select, Store } from '@ngrx/store'; import { ActiveCartFacade, OrderEntry } from '@spartacus/cart/base/root'; import { CheckoutQueryFacade } from '@spartacus/checkout/base/root'; -import { - OCC_USER_ID_CURRENT, - StateUtils, - UserIdService, -} from '@spartacus/core'; +import { StateUtils, UserIdService } from '@spartacus/core'; import { CommonConfigurator, CommonConfiguratorUtilsService, @@ -122,18 +118,23 @@ export class ConfiguratorCartService { const ownerIdParts = this.commonConfigUtilsService.decomposeOwnerId( owner.id ); - const readFromOrderEntryParameters: CommonConfigurator.ReadConfigurationFromOrderEntryParameters = - { - userId: OCC_USER_ID_CURRENT, - orderId: ownerIdParts.documentId, - orderEntryNumber: ownerIdParts.entryNumber, - owner: owner, - }; - this.store.dispatch( - new ConfiguratorActions.ReadOrderEntryConfiguration( - readFromOrderEntryParameters - ) - ); + this.userIdService + .getUserId() + .pipe(take(1)) + .subscribe((userId) => { + const readFromOrderEntryParameters: CommonConfigurator.ReadConfigurationFromOrderEntryParameters = + { + userId: userId, + orderId: ownerIdParts.documentId, + orderEntryNumber: ownerIdParts.entryNumber, + owner: owner, + }; + this.store.dispatch( + new ConfiguratorActions.ReadOrderEntryConfiguration( + readFromOrderEntryParameters + ) + ); + }); } }), filter( @@ -181,18 +182,23 @@ export class ConfiguratorCartService { const ownerIdParts = this.commonConfigUtilsService.decomposeOwnerId( owner.id ); - const readFromQuoteEntryParameters: CommonConfigurator.ReadConfigurationFromQuoteEntryParameters = - { - userId: OCC_USER_ID_CURRENT, - quoteId: ownerIdParts.documentId, - quoteEntryNumber: ownerIdParts.entryNumber, - owner: owner, - }; - this.store.dispatch( - new ConfiguratorActions.ReadQuoteEntryConfiguration( - readFromQuoteEntryParameters - ) - ); + this.userIdService + .getUserId() + .pipe(take(1)) + .subscribe((userId) => { + const readFromQuoteEntryParameters: CommonConfigurator.ReadConfigurationFromQuoteEntryParameters = + { + userId: userId, + quoteId: ownerIdParts.documentId, + quoteEntryNumber: ownerIdParts.entryNumber, + owner: owner, + }; + this.store.dispatch( + new ConfiguratorActions.ReadQuoteEntryConfiguration( + readFromQuoteEntryParameters + ) + ); + }); } }), filter( diff --git a/feature-libs/quote/components/details/cart/quote-details-cart.component.html b/feature-libs/quote/components/details/cart/quote-details-cart.component.html index a5e3deaf461..5d7bfca2a6b 100644 --- a/feature-libs/quote/components/details/cart/quote-details-cart.component.html +++ b/feature-libs/quote/components/details/cart/quote-details-cart.component.html @@ -8,15 +8,22 @@ {{ 'quote.commons.cart' | cxTranslate }} - - + + + diff --git a/feature-libs/quote/components/details/cart/quote-details-cart.component.spec.ts b/feature-libs/quote/components/details/cart/quote-details-cart.component.spec.ts index b64433aea3c..2207a1a1992 100644 --- a/feature-libs/quote/components/details/cart/quote-details-cart.component.spec.ts +++ b/feature-libs/quote/components/details/cart/quote-details-cart.component.spec.ts @@ -1,6 +1,8 @@ import { Directive, Input } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { + ActiveCartFacade, + Cart, CartRemoveEntrySuccessEvent, CartUpdateEntrySuccessEvent, } from '@spartacus/cart/base/root'; @@ -20,6 +22,8 @@ import { } from '../../../core/testing/quote-test-utils'; import { CommonQuoteTestUtilsService } from '../../testing/common-quote-test-utils.service'; import { QuoteDetailsCartComponent } from './quote-details-cart.component'; +import { QuoteDetailsCartComponentService } from './quote-details-cart.component.service'; +import { tap } from 'rxjs/operators'; @Directive({ selector: '[cxOutlet]', @@ -43,18 +47,34 @@ const quote: Quote = { cartId: cartId, threshold: threshold, totalPrice: totalPrice, + isEditable: false, + entries: [{ entryNumber: 1 }], }; +const cart: Cart = {}; + const mockQuoteDetails$ = new BehaviorSubject(quote); +const mockCart$ = new BehaviorSubject(cart); +let cartObsHasFired = false; +let quoteObsHasFired = false; class MockQuoteFacade implements Partial { getQuoteDetails(): Observable { - return mockQuoteDetails$.asObservable(); + return mockQuoteDetails$ + .asObservable() + .pipe(tap(() => (quoteObsHasFired = true))); + } +} + +class MockActiveCartFacade implements Partial { + getActive(): Observable { + return mockCart$.asObservable().pipe(tap(() => (cartObsHasFired = true))); } } describe('QuoteDetailsCartComponent', () => { let mockedEventService: EventService; + let mockQuoteDetailsCartService: QuoteDetailsCartComponentService; let fixture: ComponentFixture; let htmlElem: HTMLElement; let component: QuoteDetailsCartComponent; @@ -70,10 +90,18 @@ describe('QuoteDetailsCartComponent', () => { provide: QuoteFacade, useClass: MockQuoteFacade, }, + { + provide: ActiveCartFacade, + useClass: MockActiveCartFacade, + }, { provide: EventService, useValue: mockedEventService, }, + { + provide: QuoteDetailsCartComponentService, + useValue: mockQuoteDetailsCartService, + }, ], }).compileComponents(); }) @@ -84,6 +112,7 @@ describe('QuoteDetailsCartComponent', () => { htmlElem = fixture.nativeElement; component = fixture.componentInstance; component.showCart$ = of(true); + initEmitCounters(); fixture.detectChanges(); }); @@ -92,17 +121,45 @@ describe('QuoteDetailsCartComponent', () => { 'get', 'dispatch', ]); + mockQuoteDetailsCartService = jasmine.createSpyObj( + 'quoteDetailsCartComponentService', + ['setQuoteEntriesExpanded', 'getQuoteEntriesExpanded'] + ); asSpy(mockedEventService.get).and.returnValue(EMPTY); + asSpy(mockQuoteDetailsCartService.getQuoteEntriesExpanded).and.returnValue( + true + ); } function asSpy(f: any) { return f; } + function initEmitCounters() { + cartObsHasFired = false; + quoteObsHasFired = false; + } + it('should create the component', () => { expect(component).toBeTruthy(); }); + describe('Rendering', () => { + it('should request quote for the item list outlet in case quote is read-only', () => { + expect(quoteObsHasFired).toBe(true); + expect(cartObsHasFired).toBe(false); + }); + + it('should request cart for the item list outlet in case quote is editable', () => { + quote.isEditable = true; + initEmitCounters(); + fixture.detectChanges(); + + expect(quoteObsHasFired).toBe(false); + expect(cartObsHasFired).toBe(true); + }); + }); + describe('Ghost animation', () => { it('should render view for ghost animation', () => { component.quoteDetails$ = NEVER; @@ -191,6 +248,22 @@ describe('QuoteDetailsCartComponent', () => { }); }); + describe('onToggleShowOrHideCart', () => { + it('should call quoteDetailsCartComponentService correctly if argument is true', () => { + component.onToggleShowOrHideCart(true); + expect( + mockQuoteDetailsCartService.setQuoteEntriesExpanded + ).toHaveBeenCalledWith(false); + }); + + it('should call quoteDetailsCartComponentService correctly if argument is false', () => { + component.onToggleShowOrHideCart(false); + expect( + mockQuoteDetailsCartService.setQuoteEntriesExpanded + ).toHaveBeenCalledWith(true); + }); + }); + it('should display CARET_UP per default', () => { CommonQuoteTestUtilsService.expectElementToContainText( expect, diff --git a/feature-libs/quote/components/details/cart/quote-details-cart.component.ts b/feature-libs/quote/components/details/cart/quote-details-cart.component.ts index fb5218d0b37..7412c9221b0 100644 --- a/feature-libs/quote/components/details/cart/quote-details-cart.component.ts +++ b/feature-libs/quote/components/details/cart/quote-details-cart.component.ts @@ -5,7 +5,12 @@ */ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { CartEvent, CartOutlets } from '@spartacus/cart/base/root'; +import { + CartEvent, + CartOutlets, + ActiveCartFacade, + Cart, +} from '@spartacus/cart/base/root'; import { EventService } from '@spartacus/core'; import { Quote, @@ -22,6 +27,7 @@ import { QuoteDetailsCartComponentService } from './quote-details-cart.component }) export class QuoteDetailsCartComponent implements OnInit, OnDestroy { quoteDetails$: Observable = this.quoteFacade.getQuoteDetails(); + cartDetails$: Observable = this.activeCartFacade.getActive(); showCart$ = this.quoteDetailsCartService.getQuoteEntriesExpanded(); iconTypes = ICON_TYPE; readonly cartOutlets = CartOutlets; @@ -29,6 +35,7 @@ export class QuoteDetailsCartComponent implements OnInit, OnDestroy { constructor( protected quoteFacade: QuoteFacade, + protected activeCartFacade: ActiveCartFacade, protected quoteDetailsCartService: QuoteDetailsCartComponentService, protected eventService: EventService ) {} diff --git a/feature-libs/quote/core/http-interceptors/index.ts b/feature-libs/quote/core/http-interceptors/index.ts new file mode 100644 index 00000000000..c2928558f5c --- /dev/null +++ b/feature-libs/quote/core/http-interceptors/index.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './quote-bad-request.handler'; +export * from './quote-not-found.handler'; diff --git a/feature-libs/quote/core/http-interceptors/quote-not-found.handler.spec.ts b/feature-libs/quote/core/http-interceptors/quote-not-found.handler.spec.ts new file mode 100644 index 00000000000..1f35c4f0ad6 --- /dev/null +++ b/feature-libs/quote/core/http-interceptors/quote-not-found.handler.spec.ts @@ -0,0 +1,95 @@ +import { HttpRequest, HttpErrorResponse } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { + GlobalMessageService, + HttpResponseStatus, + Priority, + RoutingService, +} from '@spartacus/core'; +import { QuoteNotFoundHandler } from './quote-not-found.handler'; + +const mockRequest = {} as HttpRequest; + +const mockQuoteNotFoundResponse = { + error: { + errors: [ + { + message: 'Quote not found', + type: 'NotFoundError', + }, + ], + }, +} as HttpErrorResponse; + +const mockNotFoundResponse = { + error: { + errors: [ + { + message: 'XX not found', + type: 'NotFoundError', + }, + ], + }, +} as HttpErrorResponse; + +const mockEmptyResponse = { + error: null, +} as HttpErrorResponse; + +class MockGlobalMessageService {} +class MockRoutingService { + go() {} +} + +describe('QuoteBadRequestHandler', () => { + let service: QuoteNotFoundHandler; + let routingService: RoutingService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + QuoteNotFoundHandler, + { + provide: GlobalMessageService, + useClass: MockGlobalMessageService, + }, + { + provide: RoutingService, + useClass: MockRoutingService, + }, + ], + }); + service = TestBed.inject(QuoteNotFoundHandler); + routingService = TestBed.inject(RoutingService); + spyOn(routingService, 'go'); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should register to 404 responseStatus', () => { + expect(service.responseStatus).toEqual(HttpResponseStatus.NOT_FOUND); + }); + + describe('handleError', () => { + it('should handle quote not found error', () => { + service.handleError(mockRequest, mockQuoteNotFoundResponse); + expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'quotes' }); + }); + + it('should do nothing in case error is not related to quote', () => { + service.handleError(mockRequest, mockNotFoundResponse); + expect(routingService.go).toHaveBeenCalledTimes(0); + }); + + it('should handle empty response', () => { + service.handleError(mockRequest, mockEmptyResponse); + expect(routingService.go).toHaveBeenCalledTimes(0); + }); + }); + + it('should carry normal priority', () => { + expect(service.getPriority()).toBe(Priority.NORMAL); + }); +}); diff --git a/feature-libs/quote/core/http-interceptors/quote-not-found.handler.ts b/feature-libs/quote/core/http-interceptors/quote-not-found.handler.ts new file mode 100644 index 00000000000..bdd9bfaa1a1 --- /dev/null +++ b/feature-libs/quote/core/http-interceptors/quote-not-found.handler.ts @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2023 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { HttpRequest, HttpErrorResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { + GlobalMessageService, + HttpErrorHandler, + ErrorModel, + HttpResponseStatus, + Priority, + RoutingService, +} from '@spartacus/core'; + +@Injectable({ + providedIn: 'root', +}) +export class QuoteNotFoundHandler extends HttpErrorHandler { + constructor( + protected globalMessageService: GlobalMessageService, + protected routingService: RoutingService + ) { + super(globalMessageService); + } + responseStatus = HttpResponseStatus.NOT_FOUND; + + handleError(_request: HttpRequest, response: HttpErrorResponse): void { + if (this.getQuoteNotFoundErrors(response).length > 0) { + this.navigateToQuoteList(); + } + } + + /** + * This situation can happen e.g. if one ends an ASM emulation while viewing a quote and afterwards emulates a session + * for a different user + * @param response HTTP response + * @returns Array of error models or empty array if no issues occured + */ + protected getQuoteNotFoundErrors(response: HttpErrorResponse): ErrorModel[] { + return (response.error?.errors ?? []).filter( + (error: ErrorModel) => + error.type === 'NotFoundError' && error.message === 'Quote not found' + ); + } + + protected navigateToQuoteList(): void { + this.routingService.go({ cxRoute: 'quotes' }); + } + + getPriority(): Priority { + return Priority.NORMAL; + } +} diff --git a/feature-libs/quote/core/public_api.ts b/feature-libs/quote/core/public_api.ts index 6bad61e2789..07b8c5f6521 100644 --- a/feature-libs/quote/core/public_api.ts +++ b/feature-libs/quote/core/public_api.ts @@ -9,3 +9,4 @@ export * from './connectors/index'; export * from './facade/index'; export * from './config/index'; export * from './services/index'; +export * from './http-interceptors/index'; diff --git a/feature-libs/quote/core/quote-core.module.ts b/feature-libs/quote/core/quote-core.module.ts index e3984e92cc2..31840d8951d 100644 --- a/feature-libs/quote/core/quote-core.module.ts +++ b/feature-libs/quote/core/quote-core.module.ts @@ -10,6 +10,7 @@ import { QuoteConnector } from './connectors/quote.connector'; import { facadeProviders } from './facade/facade-providers'; import { QuoteBadRequestHandler } from './http-interceptors/quote-bad-request.handler'; import { QuoteAddedToCartEventListener } from './event/quote-added-to-cart-event.listener'; +import { QuoteNotFoundHandler } from './http-interceptors/quote-not-found.handler'; @NgModule({ providers: [ @@ -20,6 +21,11 @@ import { QuoteAddedToCartEventListener } from './event/quote-added-to-cart-event useExisting: QuoteBadRequestHandler, multi: true, }, + { + provide: HttpErrorHandler, + useExisting: QuoteNotFoundHandler, + multi: true, + }, ], }) export class QuoteCoreModule {