-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/feature-libs/quote/components/details/overview/quote-details-overview.component.spec.ts b/feature-libs/quote/components/details/overview/quote-details-overview.component.spec.ts
index c9a5bbe6b2f..7b4fbf22338 100644
--- a/feature-libs/quote/components/details/overview/quote-details-overview.component.spec.ts
+++ b/feature-libs/quote/components/details/overview/quote-details-overview.component.spec.ts
@@ -8,17 +8,15 @@ import {
QuoteActionType,
QuoteState,
} from '@spartacus/quote/root';
-import {
- I18nTestingModule,
- QueryState,
- TranslationService,
-} from '@spartacus/core';
+import { I18nTestingModule, TranslationService } from '@spartacus/core';
import { CardModule } from '@spartacus/storefront';
import { Observable, of } from 'rxjs';
import { QuoteDetailsOverviewComponent } from './quote-details-overview.component';
import createSpy = jasmine.createSpy;
+const totalPriceFormattedValue = '$20';
+
const mockCartId = '1234';
const mockAction = { type: QuoteActionType.CREATE, isPrimary: true };
const mockQuote: Quote = {
@@ -32,17 +30,17 @@ const mockQuote: Quote = {
updatedTime: new Date('2022-06-09T13:31:36+0000'),
previousEstimatedTotal: {
currencyIso: 'USD',
- formattedValue: '$0.00',
- value: 0,
+ formattedValue: '$1.00',
+ value: 1,
},
state: QuoteState.BUYER_ORDERED,
name: 'Name',
- totalPrice: { value: 20 },
+ totalPrice: { value: 20, formattedValue: totalPriceFormattedValue },
};
export class MockQuoteFacade implements Partial
{
- getQuoteDetails(): Observable> {
- return of({ data: mockQuote, loading: false, error: false });
+ getQuoteDetails(): Observable {
+ return of(mockQuote);
}
setSort = createSpy();
setCurrentPage = createSpy();
@@ -143,4 +141,36 @@ describe('QuoteDetailsOverviewComponent', () => {
expect(result).toEqual(expected);
});
});
+
+ describe('getTotalPrice', () => {
+ it('should return the total price formatted value in case it is available', () => {
+ expect(component.getTotalPrice(mockQuote)).toBe(totalPriceFormattedValue);
+ });
+
+ it('should return null in case no formatted value is available', () => {
+ const quoteWOPrices: Quote = {
+ ...mockQuote,
+ totalPrice: {},
+ };
+ expect(component.getTotalPrice(quoteWOPrices)).toBe(null);
+ });
+ });
+
+ describe('getTotalPriceDescription', () => {
+ it('should name total price as estimated as long as final status not reached', () => {
+ expect(component.getTotalPriceDescription(mockQuote)).toBe(
+ 'quote.details.estimatedTotal'
+ );
+ });
+
+ it('should name total price as total as in case final status reached, i.e. checkout action is available', () => {
+ const quoteInOfferState: Quote = {
+ ...mockQuote,
+ allowedActions: [{ type: QuoteActionType.CHECKOUT, isPrimary: true }],
+ };
+ expect(component.getTotalPriceDescription(quoteInOfferState)).toBe(
+ 'quote.details.total'
+ );
+ });
+ });
});
diff --git a/feature-libs/quote/components/details/overview/quote-details-overview.component.ts b/feature-libs/quote/components/details/overview/quote-details-overview.component.ts
index a78ec1ebe5a..7cbde9be137 100644
--- a/feature-libs/quote/components/details/overview/quote-details-overview.component.ts
+++ b/feature-libs/quote/components/details/overview/quote-details-overview.component.ts
@@ -5,7 +5,7 @@
*/
import { Component } from '@angular/core';
-import { QuoteFacade } from '@spartacus/quote/root';
+import { Quote, QuoteActionType, QuoteFacade } from '@spartacus/quote/root';
import { TranslationService } from '@spartacus/core';
import { Card } from '@spartacus/storefront';
import { Observable } from 'rxjs';
@@ -16,7 +16,7 @@ import { map } from 'rxjs/operators';
templateUrl: './quote-details-overview.component.html',
})
export class QuoteDetailsOverviewComponent {
- quoteDetails$ = this.quoteFacade.getQuoteDetails();
+ quoteDetails$: Observable = this.quoteFacade.getQuoteDetails();
constructor(
protected quoteFacade: QuoteFacade,
@@ -32,4 +32,26 @@ export class QuoteDetailsOverviewComponent {
}))
);
}
+ /**
+ * Returns total price as formatted string
+ * @param quote Quote
+ * @returns Total price formatted format, null if that is not available
+ */
+ getTotalPrice(quote: Quote): string | null {
+ return quote.totalPrice.formattedValue ?? null;
+ }
+
+ /**
+ * Returns total price description
+ * @param quote Quote
+ * @returns 'Total' price if quote is in final state, 'Estimated total' otherwise
+ */
+ getTotalPriceDescription(quote: Quote): string {
+ const readyToSubmit = quote.allowedActions.find(
+ (action) => action.type === QuoteActionType.CHECKOUT
+ );
+ return readyToSubmit
+ ? 'quote.details.total'
+ : 'quote.details.estimatedTotal';
+ }
}
diff --git a/feature-libs/quote/components/details/vendor-contact/quote-details-vendor-contact.component.ts b/feature-libs/quote/components/details/vendor-contact/quote-details-vendor-contact.component.ts
index 5ddd8137253..43d138dbe54 100644
--- a/feature-libs/quote/components/details/vendor-contact/quote-details-vendor-contact.component.ts
+++ b/feature-libs/quote/components/details/vendor-contact/quote-details-vendor-contact.component.ts
@@ -14,7 +14,7 @@ import { ICON_TYPE, MessagingConfigs } from '@spartacus/storefront';
templateUrl: './quote-details-vendor-contact.component.html',
})
export class QuoteDetailsVendorContactComponent {
- quoteDetails$ = this.quoteFacade.getQuoteDetails();
+ quoteDetails$ = this.quoteFacade.getQuoteDetailsQueryState();
showVendorContact = true;
iconTypes = ICON_TYPE;
vendorplaceHolder: string = 'Vendor Contact Component';
diff --git a/feature-libs/quote/components/quote-actions-by-role/quote-actions-by-role.component.spec.ts b/feature-libs/quote/components/quote-actions-by-role/quote-actions-by-role.component.spec.ts
index 320d0d6e390..55df53fdd87 100644
--- a/feature-libs/quote/components/quote-actions-by-role/quote-actions-by-role.component.spec.ts
+++ b/feature-libs/quote/components/quote-actions-by-role/quote-actions-by-role.component.spec.ts
@@ -10,7 +10,6 @@ import {
GlobalMessageService,
I18nTestingModule,
Price,
- QueryState,
TranslationService,
} from '@spartacus/core';
@@ -39,15 +38,8 @@ const mockQuote: Quote = {
threshold: threshold,
totalPrice: totalPrice,
};
-const mockQuoteDetailsState: QueryState = {
- loading: false,
- error: false,
- data: mockQuote,
-};
-const mockQuoteDetailsState$ = new BehaviorSubject>(
- mockQuoteDetailsState
-);
+const mockQuoteDetails$ = new BehaviorSubject(mockQuote);
const dialogClose$ = new BehaviorSubject(undefined);
class MockLaunchDialogService implements Partial {
@@ -68,8 +60,8 @@ class MockLaunchDialogService implements Partial {
}
class MockCommerceQuotesFacade implements Partial {
- getQuoteDetails(): Observable> {
- return mockQuoteDetailsState$.asObservable();
+ getQuoteDetails(): Observable {
+ return mockQuoteDetails$.asObservable();
}
performQuoteAction(
_quoteCode: string,
@@ -119,7 +111,7 @@ describe('QuoteActionsByRoleComponent', () => {
launchDialogService = TestBed.inject(LaunchDialogService);
facade = TestBed.inject(QuoteFacade);
globalMessageService = TestBed.inject(GlobalMessageService);
- mockQuoteDetailsState$.next(mockQuoteDetailsState);
+ mockQuoteDetails$.next(mockQuote);
});
it('should create component', () => {
@@ -129,7 +121,7 @@ describe('QuoteActionsByRoleComponent', () => {
it('should read quote details state', (done) => {
component.quoteDetails$.pipe(take(1)).subscribe((state) => {
- expect(state).toEqual(mockQuoteDetailsState.data);
+ expect(state).toEqual(mockQuote);
done();
});
});
@@ -193,28 +185,24 @@ describe('QuoteActionsByRoleComponent', () => {
it('should open confirmation dialog when action is SUBMIT', () => {
spyOn(launchDialogService, 'openDialog');
- const newMockQuoteWithSubmitAction: QueryState = {
- error: false,
- loading: false,
- data: {
- ...mockQuote,
- allowedActions: [
- { type: QuoteActionType.SUBMIT, isPrimary: true },
- { type: QuoteActionType.CANCEL, isPrimary: false },
- ],
- },
+ const newMockQuoteWithSubmitAction: Quote = {
+ ...mockQuote,
+ allowedActions: [
+ { type: QuoteActionType.SUBMIT, isPrimary: true },
+ { type: QuoteActionType.CANCEL, isPrimary: false },
+ ],
};
- mockQuoteDetailsState$.next(newMockQuoteWithSubmitAction);
+ mockQuoteDetails$.next(newMockQuoteWithSubmitAction);
fixture.detectChanges();
component.onClick(
QuoteActionType.SUBMIT,
- newMockQuoteWithSubmitAction.data?.code ?? ''
+ newMockQuoteWithSubmitAction.code
);
expect(launchDialogService.openDialog).toHaveBeenCalledWith(
LAUNCH_CALLER.REQUEST_CONFIRMATION,
component.element,
component['viewContainerRef'],
- { quoteCode: newMockQuoteWithSubmitAction.data?.code }
+ { quoteCode: newMockQuoteWithSubmitAction.code }
);
});
@@ -227,24 +215,17 @@ describe('QuoteActionsByRoleComponent', () => {
totalPrice: { value: threshold - 1 },
allowedActions: allowedActionsSubmit,
};
- const queryStateSubmittableQuote: QueryState = {
- error: false,
- loading: false,
- data: {
- ...mockQuote,
- allowedActions: allowedActionsSubmit,
- },
+ const submittableQuote: Quote = {
+ ...mockQuote,
+ allowedActions: allowedActionsSubmit,
};
- const queryStateCancellableQuote: QueryState = {
- ...queryStateSubmittableQuote,
- data: {
- ...quoteFailingThreshold,
- allowedActions: [{ type: QuoteActionType.CANCEL, isPrimary: true }],
- },
+ const cancellableQuote: Quote = {
+ ...quoteFailingThreshold,
+ allowedActions: [{ type: QuoteActionType.CANCEL, isPrimary: true }],
};
it('should let submit button enabled if threshold is met', () => {
- mockQuoteDetailsState$.next(queryStateSubmittableQuote);
+ mockQuoteDetails$.next(submittableQuote);
fixture.detectChanges();
const actionButtons = fixture.debugElement.queryAll(By.css('.btn'));
expect(actionButtons).toBeDefined();
@@ -253,7 +234,7 @@ describe('QuoteActionsByRoleComponent', () => {
it('should let submit button enabled if threshold is not specified', () => {
mockQuote.threshold = undefined;
- mockQuoteDetailsState$.next(queryStateSubmittableQuote);
+ mockQuoteDetails$.next(submittableQuote);
fixture.detectChanges();
const actionButtons = fixture.debugElement.queryAll(By.css('.btn'));
expect(actionButtons).toBeDefined();
@@ -262,14 +243,7 @@ describe('QuoteActionsByRoleComponent', () => {
it('should disable submit button if threshold is not met and raise message', () => {
spyOn(globalMessageService, 'add').and.callThrough();
-
- const queryStateSubmittableQuoteFailingThreshold: QueryState = {
- ...queryStateSubmittableQuote,
- data: {
- ...quoteFailingThreshold,
- },
- };
- mockQuoteDetailsState$.next(queryStateSubmittableQuoteFailingThreshold);
+ mockQuoteDetails$.next(quoteFailingThreshold);
fixture.detectChanges();
const actionButtons = fixture.debugElement.queryAll(By.css('.btn'));
@@ -281,13 +255,7 @@ describe('QuoteActionsByRoleComponent', () => {
it('should disable submit button if total price value is not provided', () => {
quoteFailingThreshold.totalPrice.value = undefined;
- const queryStateSubmittableQuoteFailingThreshold: QueryState = {
- ...queryStateSubmittableQuote,
- data: {
- ...quoteFailingThreshold,
- },
- };
- mockQuoteDetailsState$.next(queryStateSubmittableQuoteFailingThreshold);
+ mockQuoteDetails$.next(quoteFailingThreshold);
fixture.detectChanges();
const actionButtons = fixture.debugElement.queryAll(By.css('.btn'));
@@ -296,7 +264,7 @@ describe('QuoteActionsByRoleComponent', () => {
});
it('should not touch buttons other than submit', () => {
- mockQuoteDetailsState$.next(queryStateCancellableQuote);
+ mockQuoteDetails$.next(cancellableQuote);
fixture.detectChanges();
const actionButtons = fixture.debugElement.queryAll(By.css('.btn'));
@@ -306,7 +274,7 @@ describe('QuoteActionsByRoleComponent', () => {
it('should not raise message in case threshold not met and submit action not present', () => {
spyOn(globalMessageService, 'add').and.callThrough();
- mockQuoteDetailsState$.next(queryStateCancellableQuote);
+ mockQuoteDetails$.next(cancellableQuote);
fixture.detectChanges();
expect(globalMessageService.add).toHaveBeenCalledTimes(0);
@@ -315,27 +283,23 @@ describe('QuoteActionsByRoleComponent', () => {
it('should perform quote action when action is SUBMIT and confirm dialogClose reason is yes', () => {
spyOn(facade, 'performQuoteAction').and.callThrough();
- const newMockQuoteWithSubmitAction: QueryState = {
- error: false,
- loading: false,
- data: {
- ...mockQuote,
- allowedActions: [
- { type: QuoteActionType.SUBMIT, isPrimary: true },
- { type: QuoteActionType.CANCEL, isPrimary: false },
- ],
- },
+ const newMockQuoteWithSubmitAction: Quote = {
+ ...mockQuote,
+ allowedActions: [
+ { type: QuoteActionType.SUBMIT, isPrimary: true },
+ { type: QuoteActionType.CANCEL, isPrimary: false },
+ ],
};
- mockQuoteDetailsState$.next(newMockQuoteWithSubmitAction);
+ mockQuoteDetails$.next(newMockQuoteWithSubmitAction);
fixture.detectChanges();
component.onClick(
QuoteActionType.SUBMIT,
- newMockQuoteWithSubmitAction.data?.code ?? ''
+ newMockQuoteWithSubmitAction.code
);
launchDialogService.closeDialog('yes');
expect(facade.performQuoteAction).toHaveBeenCalledWith(
- newMockQuoteWithSubmitAction.data?.code,
+ newMockQuoteWithSubmitAction.code,
QuoteActionType.SUBMIT
);
});
diff --git a/feature-libs/quote/components/quote-actions-by-role/quote-actions-by-role.component.ts b/feature-libs/quote/components/quote-actions-by-role/quote-actions-by-role.component.ts
index b9c033dfcd3..79ddee32680 100644
--- a/feature-libs/quote/components/quote-actions-by-role/quote-actions-by-role.component.ts
+++ b/feature-libs/quote/components/quote-actions-by-role/quote-actions-by-role.component.ts
@@ -16,19 +16,14 @@ import { GlobalMessageService, GlobalMessageType } from '@spartacus/core';
import { QuoteFacade, QuoteActionType, Quote } from '@spartacus/quote/root';
import { LAUNCH_CALLER, LaunchDialogService } from '@spartacus/storefront';
import { Observable, Subscription } from 'rxjs';
-import { filter, map, take, tap } from 'rxjs/operators';
+import { filter, take, tap } from 'rxjs/operators';
@Component({
selector: 'cx-quote-actions-by-role',
templateUrl: './quote-actions-by-role.component.html',
})
export class QuoteActionsByRoleComponent implements OnInit, OnDestroy {
- quoteDetails$: Observable = this.quoteFacade.getQuoteDetails().pipe(
- filter((state) => !state.loading),
- filter((state) => state.data !== undefined),
- map((state) => state.data),
- map((quote) => quote as Quote)
- );
+ quoteDetails$: Observable = this.quoteFacade.getQuoteDetails();
@ViewChild('element') element: ElementRef;
diff --git a/feature-libs/quote/core/connectors/converters.ts b/feature-libs/quote/core/connectors/converters.ts
index 34383b9a1b4..14e9a43555a 100644
--- a/feature-libs/quote/core/connectors/converters.ts
+++ b/feature-libs/quote/core/connectors/converters.ts
@@ -17,14 +17,14 @@ import {
QuoteStarter,
} from '@spartacus/quote/root';
-export const QUOTE_LIST_NORMALIZER = new InjectionToken<
- Converter
->('QuoteListNormalizer');
-
export const QUOTE_NORMALIZER = new InjectionToken>(
'QuoteNormalizer'
);
+export const QUOTE_LIST_NORMALIZER = new InjectionToken<
+ Converter
+>('QuoteListNormalizer');
+
export const QUOTE_STARTER_SERIALIZER = new InjectionToken<
Converter
>('QuoteStarterSerializer');
diff --git a/feature-libs/quote/core/facade/quote.service.spec.ts b/feature-libs/quote/core/facade/quote.service.spec.ts
index 7062b237bd0..3ae52a4fa92 100644
--- a/feature-libs/quote/core/facade/quote.service.spec.ts
+++ b/feature-libs/quote/core/facade/quote.service.spec.ts
@@ -5,6 +5,7 @@ import {
Comment,
Quote,
QuoteActionType,
+ QuoteDetailsReloadQueryEvent,
QuoteList,
QuoteMetadata,
QuotesStateParams,
@@ -187,6 +188,32 @@ describe('QuoteService', () => {
});
});
+ it('should signal that quote details need to be re-read when performing search', () => {
+ service
+ .getQuotesState(mockQuotesStateParams)
+ .pipe(take(1))
+ .subscribe(() => {
+ expect(eventService.dispatch).toHaveBeenCalledWith(
+ {},
+ QuoteDetailsReloadQueryEvent
+ );
+ });
+ });
+
+ it('should return quote details query state after calling quoteConnector.getQuote', () => {
+ service
+ .getQuoteDetailsQueryState()
+ .pipe(take(1))
+ .subscribe((details) => {
+ expect(connector.getQuote).toHaveBeenCalledWith(
+ mockUserId,
+ mockParams.quoteId
+ );
+ expect(details.data).toEqual(mockQuote);
+ expect(details.loading).toBe(false);
+ });
+ });
+
it('should return quote details after calling quoteConnector.getQuote', () => {
service
.getQuoteDetails()
@@ -196,7 +223,7 @@ describe('QuoteService', () => {
mockUserId,
mockParams.quoteId
);
- expect(details.data).toEqual(mockQuote);
+ expect(details).toEqual(mockQuote);
});
});
diff --git a/feature-libs/quote/core/facade/quote.service.ts b/feature-libs/quote/core/facade/quote.service.ts
index 6b9d745593a..c35cfa85343 100644
--- a/feature-libs/quote/core/facade/quote.service.ts
+++ b/feature-libs/quote/core/facade/quote.service.ts
@@ -36,6 +36,7 @@ import { BehaviorSubject, combineLatest, Observable, of, zip } from 'rxjs';
import {
concatMap,
distinctUntilChanged,
+ filter,
map,
switchMap,
take,
@@ -228,6 +229,9 @@ export class QuoteService implements QuoteFacade {
sort,
pageSize: this.config.view?.defaultPageSize,
});
+ }),
+ tap(() => {
+ this.eventService.dispatch({}, QuoteDetailsReloadQueryEvent);
})
),
{
@@ -292,7 +296,7 @@ export class QuoteService implements QuoteFacade {
return this.getQuotesStateQuery(params).getState();
}
- getQuoteDetails(): Observable> {
+ getQuoteDetailsQueryState(): Observable> {
return combineLatest([
this.isActionPerforming$,
this.quoteDetailsState$.getState(),
@@ -303,4 +307,13 @@ export class QuoteService implements QuoteFacade {
}))
);
}
+
+ getQuoteDetails(): Observable {
+ return this.getQuoteDetailsQueryState().pipe(
+ filter((state) => !state.loading),
+ filter((state) => state.data !== undefined),
+ map((state) => state.data),
+ map((quote) => quote as Quote)
+ );
+ }
}
diff --git a/feature-libs/quote/core/testing/quote-test-utils.ts b/feature-libs/quote/core/testing/quote-test-utils.ts
index d7ce08ad7c3..20402d79453 100644
--- a/feature-libs/quote/core/testing/quote-test-utils.ts
+++ b/feature-libs/quote/core/testing/quote-test-utils.ts
@@ -9,10 +9,10 @@ import { Quote } from '@spartacus/quote/root';
/**
* Quote test utils
*/
-
+export const QUOTE_CODE = '00010000';
export function createEmptyQuote(): Quote {
return {
- code: '00010000',
+ code: QUOTE_CODE,
name: 'Quote',
allowedActions: [],
totalPrice: {},
diff --git a/feature-libs/quote/occ/adapters/occ-quote.adapter.ts b/feature-libs/quote/occ/adapters/occ-quote.adapter.ts
index 7d6f5ea7c0a..34e19e3111f 100644
--- a/feature-libs/quote/occ/adapters/occ-quote.adapter.ts
+++ b/feature-libs/quote/occ/adapters/occ-quote.adapter.ts
@@ -13,8 +13,8 @@ import {
QUOTE_DISCOUNT_SERIALIZER,
QUOTE_LIST_NORMALIZER,
QUOTE_METADATA_SERIALIZER,
- QUOTE_NORMALIZER,
QUOTE_STARTER_SERIALIZER,
+ QUOTE_NORMALIZER,
} from '@spartacus/quote/core';
import {
Comment,
diff --git a/feature-libs/quote/occ/config/default-occ-quote-config.ts b/feature-libs/quote/occ/config/default-occ-quote-config.ts
index e1684ecea26..7fc21b6e84a 100644
--- a/feature-libs/quote/occ/config/default-occ-quote-config.ts
+++ b/feature-libs/quote/occ/config/default-occ-quote-config.ts
@@ -13,7 +13,7 @@ export const defaultOccQuoteConfig: OccConfig = {
getQuotes: 'users/${userId}/quotes',
createQuote: 'users/${userId}/quotes',
getQuote:
- 'users/${userId}/quotes/${quoteCode}?fields=FULL,previousEstimatedTotal(formattedValue)',
+ 'users/${userId}/quotes/${quoteCode}?fields=FULL,previousEstimatedTotal(formattedValue),totalPrice(formattedValue),entries(FULL)',
editQuote: 'users/${userId}/quotes/${quoteCode}',
performQuoteAction: 'users/${userId}/quotes/${quoteCode}/action',
addComment: 'users/${userId}/quotes/${quoteCode}/comments',
diff --git a/feature-libs/quote/occ/converters/occ-quote-entry-normalizer.spec.ts b/feature-libs/quote/occ/converters/occ-quote-entry-normalizer.spec.ts
new file mode 100644
index 00000000000..421eb813297
--- /dev/null
+++ b/feature-libs/quote/occ/converters/occ-quote-entry-normalizer.spec.ts
@@ -0,0 +1,47 @@
+import { TestBed } from '@angular/core/testing';
+import { ConverterService, PRODUCT_NORMALIZER } from '@spartacus/core';
+import { OccQuoteEntryNormalizer } from './occ-quote-entry-normalizer';
+
+class MockConverterService {
+ convert() {}
+}
+
+describe('OccQuoteEntryNormalizer', () => {
+ let occQuoteEntryNormalizer: OccQuoteEntryNormalizer;
+ let converter: ConverterService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ OccQuoteEntryNormalizer,
+ { provide: ConverterService, useClass: MockConverterService },
+ ],
+ });
+
+ occQuoteEntryNormalizer = TestBed.inject(OccQuoteEntryNormalizer);
+ converter = TestBed.inject(ConverterService);
+ spyOn(converter, 'convert').and.callThrough();
+ });
+
+ it('should be created', () => {
+ expect(occQuoteEntryNormalizer).toBeTruthy();
+ });
+
+ it('should convert quote entries', () => {
+ const product = { code: 'testproductcode 1' };
+ const price = { value: 123 };
+ const quote = {
+ allowedActions: [],
+ code: 'testquote',
+ comments: [],
+ description: 'test description',
+ entries: [{ product }],
+ name: 'test name',
+ totalPrice: price,
+ };
+
+ const result = occQuoteEntryNormalizer.convert(quote);
+ expect(result.code).toBe(quote.code);
+ expect(converter.convert).toHaveBeenCalledWith(product, PRODUCT_NORMALIZER);
+ });
+});
diff --git a/feature-libs/quote/occ/converters/occ-quote-entry-normalizer.ts b/feature-libs/quote/occ/converters/occ-quote-entry-normalizer.ts
new file mode 100644
index 00000000000..38c7aab1326
--- /dev/null
+++ b/feature-libs/quote/occ/converters/occ-quote-entry-normalizer.ts
@@ -0,0 +1,32 @@
+/*
+ * SPDX-FileCopyrightText: 2023 SAP Spartacus team
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Injectable } from '@angular/core';
+import { OccQuote, Quote } from '@spartacus/quote/root';
+import {
+ Converter,
+ ConverterService,
+ PRODUCT_NORMALIZER,
+} from '@spartacus/core';
+
+@Injectable({ providedIn: 'root' })
+export class OccQuoteEntryNormalizer implements Converter {
+ constructor(private converter: ConverterService) {}
+
+ convert(source: OccQuote, target?: Quote): Quote {
+ if (!target) {
+ target = { ...(source as any) } as Quote;
+ }
+
+ if (source.entries) {
+ target.entries = source.entries.map((entry) => ({
+ ...entry,
+ product: this.converter.convert(entry.product, PRODUCT_NORMALIZER),
+ }));
+ }
+ return target;
+ }
+}
diff --git a/feature-libs/quote/occ/quote-occ.module.ts b/feature-libs/quote/occ/quote-occ.module.ts
index 6ac4d0781fc..0634db3238a 100644
--- a/feature-libs/quote/occ/quote-occ.module.ts
+++ b/feature-libs/quote/occ/quote-occ.module.ts
@@ -12,6 +12,7 @@ import { QuoteAdapter, QUOTE_NORMALIZER } from '@spartacus/quote/core';
import { OccQuoteAdapter } from './adapters/occ-quote.adapter';
import { defaultOccQuoteConfig } from './config/default-occ-quote-config';
import { OccQuoteActionNormalizer } from './converters/occ-quote-action-normalizer';
+import { OccQuoteEntryNormalizer } from './converters/occ-quote-entry-normalizer';
@NgModule({
imports: [CommonModule],
@@ -26,6 +27,11 @@ import { OccQuoteActionNormalizer } from './converters/occ-quote-action-normaliz
useExisting: OccQuoteActionNormalizer,
multi: true,
},
+ {
+ provide: QUOTE_NORMALIZER,
+ useExisting: OccQuoteEntryNormalizer,
+ multi: true,
+ },
],
})
export class QuoteOccModule {}
diff --git a/feature-libs/quote/root/facade/quote.facade.ts b/feature-libs/quote/root/facade/quote.facade.ts
index b25f9cca424..050328b8b17 100644
--- a/feature-libs/quote/root/facade/quote.facade.ts
+++ b/feature-libs/quote/root/facade/quote.facade.ts
@@ -25,6 +25,7 @@ import {
feature: QUOTE_FEATURE,
methods: [
'getQuotesState',
+ 'getQuoteDetailsQueryState',
'getQuoteDetails',
'createQuote',
'editQuote',
@@ -80,7 +81,14 @@ export abstract class QuoteFacade {
abstract requote(quoteCode: string): Observable;
/**
- * Returns the quote details.
+ * Returns the quote details query state.
*/
- abstract getQuoteDetails(): Observable>;
+ abstract getQuoteDetailsQueryState(): Observable<
+ QueryState
+ >;
+
+ /**
+ * Returns the quote details once it has been fully loaded.
+ */
+ abstract getQuoteDetails(): Observable;
}