Skip to content

feat/CXSPA-9270 CXSPA-7795: Order code and Quote code link in details page #20030

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Mar 12, 2025
Merged
2 changes: 2 additions & 0 deletions feature-libs/order/assets/translations/en/order.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"placed": "Placed",
"placedBy": "Placed By",
"unit": "Unit",
"quoteCode": "Quote ID {{id}}",
"quoteDetail": "Quote Detail",
"costCenter": "Cost Center",
"costCenterAndUnit": "Cost Center / Unit",
"costCenterAndUnitValue": "{{costCenterName}} / {{unitName}}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@
[content]="getOrderStatusCardContent(order.statusDisplay) | async"
></cx-card>

<ng-container
*ngTemplateOutlet="
showQuoteCode;
context: { sapQuoteCode: order.sapQuoteCode }
"
></ng-container>

<ng-template
[cxOutlet]="cartOutlets.ORDER_OVERVIEW"
[cxOutletContext]="{ item: order, readonly: true }"
Expand Down Expand Up @@ -153,6 +160,13 @@
[content]="getOrderStatusCardContent(order.statusDisplay) | async"
></cx-card>

<ng-container
*ngTemplateOutlet="
showQuoteCode;
context: { sapQuoteCode: order.sapQuoteCode }
"
></ng-container>

<ng-template
[cxOutlet]="cartOutlets.ORDER_OVERVIEW"
[cxOutletContext]="{ item: order, readonly: true }"
Expand All @@ -168,3 +182,21 @@
</div>
</ng-container>
</div>

<ng-template #showQuoteCode let-quoteCode="sapQuoteCode">
<ng-container *cxFeature="'showOrderQuoteLink'">
<div id="quote-container" class="card-body cx-card-body" *ngIf="quoteCode">
<div class="cx-card-title">
{{ 'orderDetails.quoteCode' | cxTranslate: { id: quoteCode } }}
</div>
<a
[routerLink]="
{ cxRoute: 'quoteDetails', params: { quoteId: quoteCode } } | cxUrl
"
class="cx-card-actions"
>
{{ 'orderDetails.quoteDetail' | cxTranslate }}
</a>
</div>
</ng-container>
</ng-template>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, Input } from '@angular/core';
import { Component, Input, Pipe, PipeTransform } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DeliveryMode } from '@spartacus/cart/base/root';
import {
Expand All @@ -14,6 +14,7 @@ import { EMPTY, Observable, of } from 'rxjs';
import { OrderDetailsService } from '../order-details.service';
import { OrderOverviewComponent } from './order-overview.component';
import { OrderOverviewComponentService } from './order-overview-component.service';
import { MockFeatureDirective } from 'projects/storefrontlib/shared/test/mock-feature-directive';

@Component({
selector: 'cx-card',
Expand All @@ -25,6 +26,14 @@ class MockCardComponent {
content: Card;
}

@Pipe({
name: 'cxUrl',
standalone: false,
})
class MockUrlPipe implements PipeTransform {
transform() {}
}

const mockDeliveryAddress: Address = {
firstName: 'John',
lastName: 'Smith',
Expand Down Expand Up @@ -154,7 +163,12 @@ describe('OrderOverviewComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [I18nTestingModule],
declarations: [OrderOverviewComponent, MockCardComponent],
declarations: [
OrderOverviewComponent,
MockCardComponent,
MockUrlPipe,
MockFeatureDirective,
],
providers: [
{ provide: TranslationService, useClass: MockTranslationService },
{
Expand Down Expand Up @@ -552,4 +566,24 @@ describe('OrderOverviewComponent', () => {
);
});
});

it('should render quote code in UI', () => {
component.order$ = of({ ...mockOrder, sapQuoteCode: '12345' });
fixture.detectChanges();
const quoteContainer =
fixture.nativeElement.querySelector('#quote-container');
expect(quoteContainer).not.toBeNull();
const quoteTemplate = quoteContainer.querySelector('.cx-card-title');
expect(quoteTemplate.textContent).toContain('12345');
const quoteLink = quoteContainer.querySelector('.cx-card-actions');
expect(quoteLink.innerText).toEqual('orderDetails.quoteDetail');
});

it('should not render quote code in UI', () => {
component.order$ = of({ ...mockOrder });
fixture.detectChanges();
const quoteContainer =
fixture.nativeElement.querySelector('#quote-container');
expect(quoteContainer).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ConverterService,
OccConfig,
OccEndpointsService,
OccFieldsService,
} from '@spartacus/core';
import {
CancellationRequestEntryInputList,
Expand Down Expand Up @@ -48,6 +49,7 @@ describe('OccOrderHistoryAdapter', () => {
let httpMock: HttpTestingController;
let converter: ConverterService;
let occEnpointsService: OccEndpointsService;
let occFieldsService: OccFieldsService;

beforeEach(() => {
TestBed.configureTestingModule({
Expand All @@ -67,6 +69,7 @@ describe('OccOrderHistoryAdapter', () => {
httpMock = TestBed.inject(HttpTestingController);
converter = TestBed.inject(ConverterService);
occEnpointsService = TestBed.inject(OccEndpointsService);
occFieldsService = TestBed.inject(OccFieldsService);
spyOn(converter, 'pipeable').and.callThrough();
spyOn(converter, 'convert').and.callThrough();
spyOn(occEnpointsService, 'buildUrl').and.callThrough();
Expand Down Expand Up @@ -122,14 +125,42 @@ describe('OccOrderHistoryAdapter', () => {
});

describe('getOrder', () => {
it('should fetch a single order without quote code', waitForAsync(() => {
spyOn(
(occOrderHistoryAdapter as any).featureConfigService,
'isEnabled'
).and.returnValue(false);
occOrderHistoryAdapter.load(userId, orderData.code).subscribe();
httpMock.expectOne((req: HttpRequest<any>) => {
return req.method === 'GET';
}, `GET a single order`);
expect(occEnpointsService.buildUrl).toHaveBeenCalledWith('orderDetail', {
urlParams: { userId, orderId: orderData.code },
});
expect(occEnpointsService.buildUrl).not.toHaveBeenCalledWith(
'quoteCode',
{
urlParams: { userId, orderId: orderData.code },
}
);
}));
it('should fetch a single order', waitForAsync(() => {
spyOn(occFieldsService, 'getOptimalUrlGroups').and.callThrough();
spyOn(
(occOrderHistoryAdapter as any).featureConfigService,
'isEnabled'
).and.returnValue(true);
occOrderHistoryAdapter.load(userId, orderData.code).subscribe();
httpMock.expectOne((req: HttpRequest<any>) => {
return req.method === 'GET';
}, `GET a single order`);
expect(occEnpointsService.buildUrl).toHaveBeenCalledWith('orderDetail', {
urlParams: { userId, orderId: orderData.code },
});
expect(occEnpointsService.buildUrl).toHaveBeenCalledWith('quoteCode', {
urlParams: { userId, orderId: orderData.code },
});
expect(occFieldsService.getOptimalUrlGroups).toHaveBeenCalled();
}));

it('should use converter', () => {
Expand Down
26 changes: 23 additions & 3 deletions feature-libs/order/occ/adapters/occ-order-history.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import {
ConverterService,
FeatureConfigService,
InterceptorUtil,
LoggerService,
OCC_USER_ID_ANONYMOUS,
OCC_USER_ID_CURRENT,
Occ,
OccEndpointsService,
OccFieldsService,
ScopedDataWithUrl,
USE_CLIENT_TOKEN,
normalizeHttpError,
} from '@spartacus/core';
Expand Down Expand Up @@ -42,6 +45,8 @@ const CONTENT_TYPE_JSON_HEADER = { 'Content-Type': 'application/json' };
@Injectable()
export class OccOrderHistoryAdapter implements OrderHistoryAdapter {
protected logger = inject(LoggerService);
private occFieldsService = inject(OccFieldsService);
private featureConfigService = inject(FeatureConfigService);

constructor(
protected http: HttpClient,
Expand All @@ -50,9 +55,24 @@ export class OccOrderHistoryAdapter implements OrderHistoryAdapter {
) {}

public load(userId: string, orderCode: string): Observable<Order> {
const url = this.occEndpoints.buildUrl('orderDetail', {
urlParams: { userId, orderId: orderCode },
});
const url = this.featureConfigService.isEnabled('showOrderQuoteLink')
? (() => {
const scopes = ['orderDetail', 'quoteCode'];
const scopedDataWithUrls: ScopedDataWithUrl[] = scopes.map(
(scope) => ({
scopedData: { scope, userId, orderCode },
url: this.occEndpoints.buildUrl(scope, {
urlParams: { userId, orderId: orderCode },
}),
})
);
const mergedUrl =
this.occFieldsService.getOptimalUrlGroups(scopedDataWithUrls);
return Object.keys(mergedUrl)[0];
})()
: this.occEndpoints.buildUrl('orderDetail', {
urlParams: { userId, orderId: orderCode },
});

let headers = new HttpHeaders();
if (userId === OCC_USER_ID_ANONYMOUS) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const defaultOccOrderConfig: OccConfig = {
/* eslint-disable max-len */
orderHistory: 'users/${userId}/orders',
orderDetail: 'users/${userId}/orders/${orderId}?fields=FULL',
quoteCode: 'users/${userId}/orders/${orderId}?fields=sapQuoteCode',
consignmentTracking:
'users/${userId}/orders/${orderCode}/consignments/${consignmentCode}/tracking',
cancelOrder: 'users/${userId}/orders/${orderId}/cancellation',
Expand Down
6 changes: 6 additions & 0 deletions feature-libs/order/occ/model/occ-order-endpoints.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export interface OrderOccEndpoints {
* @member {string}
*/
orderDetail?: string | OccEndpoint;
/**
* Endpoint for Quote Code associated with the Order. The Quote Code is present if the order was placed from a quote.
*
* @member {string}
*/
quoteCode?: string | OccEndpoint;
/**
* Endpoint for consignment tracking
*
Expand Down
1 change: 1 addition & 0 deletions feature-libs/order/root/model/order.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,5 @@ export interface Order {
user?: Principal;
returnable?: boolean;
cancellable?: boolean;
sapQuoteCode?: string;
}
4 changes: 3 additions & 1 deletion feature-libs/quote/assets/translations/en/quote.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,12 @@
"newCart": "New Cart",
"quotes": "Quotes",
"download": "Download Proposal",
"order": "Order Detail",
"a11y": {
"newCart": "Create new empty cart and navigate to it.",
"quotes": "Navigate to quote search result list.",
"download": "Start downloading a quote proposal"
"download": "Start downloading a quote proposal",
"order": "Navigate to order associated with this quote"
}
},
"list": {
Expand Down
16 changes: 16 additions & 0 deletions feature-libs/quote/components/links/quote-links.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@
{{ 'quote.links.download' | cxTranslate }}
</button>
</li>
<ng-container *cxFeature="'showOrderQuoteLink'">
<li *ngIf="quoteDetails.sapOrderCode">
<a
[attr.aria-label]="'quote.links.a11y.order' | cxTranslate"
class="link cx-action-link"
[routerLink]="
{
cxRoute: 'orderDetails',
params: { code: quoteDetails.sapOrderCode },
} | cxUrl
"
>
{{ 'quote.links.order' | cxTranslate }}</a
>
</li>
</ng-container>
</ul>
</section>
</ng-container>
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { createEmptyQuote } from '../../core/testing/quote-test-utils';
import { CommonQuoteTestUtilsService } from '../testing/common-quote-test-utils.service';
import { QuoteLinksComponent } from './quote-links.component';
import createSpy = jasmine.createSpy;
import { MockFeatureDirective } from 'projects/storefrontlib/shared/test/mock-feature-directive';

class MockCartUtilsService implements Partial<CartUtilsService> {
goToNewCart = createSpy();
Expand All @@ -57,6 +58,11 @@ const mockQuote: Quote = {
totalPrice: totalPrice,
};

const mockWithOrderCode: Quote = {
...mockQuote,
sapOrderCode: '12345',
};

const mockQuoteAttachment = (): File => {
const blob = new Blob([''], { type: 'application/pdf' });
return blob as File;
Expand Down Expand Up @@ -114,7 +120,7 @@ describe('QuoteLinksComponent', () => {
UrlTestingModule,
RouterModule.forRoot(mockRoutes),
],
declarations: [QuoteLinksComponent],
declarations: [QuoteLinksComponent, MockFeatureDirective],
providers: [
{
provide: QuoteFacade,
Expand Down Expand Up @@ -319,4 +325,29 @@ describe('QuoteLinksComponent', () => {
});
});
});

describe('order details link', () => {
it('should not show order details link when order code is present', () => {
const anchorElements =
fixture.nativeElement.querySelectorAll('a.cx-action-link');
const orderLink = Array.from(anchorElements).find(
(el: any) => el.innerText.trim() === 'quote.links.order'
);
expect(orderLink).toBeUndefined();
});
it('should show order details link when order code is present', async () => {
mockQuoteDetails$.next(mockWithOrderCode);
fixture.detectChanges();
const anchorElements =
fixture.nativeElement.querySelectorAll('a.cx-action-link');
const orderLink = Array.from(anchorElements).find(
(el: any) => el.innerText.trim() === 'quote.links.order'
);
expect(orderLink).not.toBeUndefined();
expect((orderLink as HTMLAnchorElement).href).toContain(
'cxRoute:orderDetails'
);
expect((orderLink as HTMLAnchorElement).href).toContain('code:12345');
});
});
});
9 changes: 8 additions & 1 deletion feature-libs/quote/components/links/quote-links.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,21 @@ import { RouterModule } from '@angular/router';
import {
AuthGuard,
CmsConfig,
FeaturesConfigModule,
I18nModule,
provideDefaultConfig,
UrlModule,
} from '@spartacus/core';
import { QuoteLinksComponent } from './quote-links.component';

@NgModule({
imports: [CommonModule, I18nModule, RouterModule, UrlModule],
imports: [
CommonModule,
I18nModule,
RouterModule,
UrlModule,
FeaturesConfigModule,
],
providers: [
provideDefaultConfig(<CmsConfig>{
cmsComponents: {
Expand Down
Loading
Loading