Skip to content

Punchout Close Session button #20155

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 10 commits into from
Apr 10, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions extra-webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ module.exports = {
__dirname,
'feature-libs/product-configurator'
),
'@spartacus/product-multi-dimensional': path.join(__dirname, 'feature-libs/product-multi-dimensional'),
'@spartacus/product-multi-dimensional': path.join(
__dirname,
'feature-libs/product-multi-dimensional'
),
'@spartacus/storefinder': path.join(
__dirname,
'feature-libs/storefinder'
Expand All @@ -58,7 +61,7 @@ module.exports = {
'@spartacus/tracking': path.join(__dirname, 'feature-libs/tracking'),
'@spartacus/cart': path.join(__dirname, 'feature-libs/cart'),
'@spartacus/order': path.join(__dirname, 'feature-libs/order'),
'@spartacus/quote': path.join( __dirname, 'feature-libs/quote'),
'@spartacus/quote': path.join(__dirname, 'feature-libs/quote'),
'@spartacus/epd-visualization': path.join(
__dirname,
'integration-libs/epd-visualization'
Expand All @@ -73,8 +76,12 @@ module.exports = {
),
'@spartacus/s4om': path.join(__dirname, 'integration-libs/s4om'),
'@spartacus/opf': path.join(__dirname, 'integration-libs/opf'),
'@spartacus/s4-service': path.join(__dirname, 'integration-libs/s4-service'),
'@spartacus/s4-service': path.join(
__dirname,
'integration-libs/s4-service'
),
'@spartacus/omf': path.join(__dirname, 'integration-libs/omf'),
'@spartacus/punchout': path.join(__dirname, 'integration-libs/punchout'),
},
},
};
25 changes: 25 additions & 0 deletions integration-libs/punchout/_index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@import '@spartacus/styles/scss/core';
@import 'styles/punchout-close-session';
@import 'styles/punchout-buttons';

$punchout-components-allowlist: cx-punchout-close-session, cx-punchout-buttons !default;

$skipComponentStyles: () !default;

@each $selector in $punchout-components-allowlist {
#{$selector} {
// skip selectors if they're added to the $skipComponentStyles list
@if (index($skipComponentStyles, $selector) == null) {
@extend %#{$selector} !optional;
}
}
}

// add body specific selectors
body {
@each $selector in $punchout-components-allowlist {
@if (index($skipComponentStyles, $selector) == null) {
@extend %#{$selector}__body !optional;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"punchout": {
"backToRequisition": "Back to requisition",
"cancel": "Cancel",
"redirectToProcurementSystem": "Return to Procurement System"
"redirectToProcurementSystem": "Return to Procurement System",
"closeSession": "Close Session"
}
}
1 change: 1 addition & 0 deletions integration-libs/punchout/components/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

export * from './punchout-buttons/punchout-buttons.component';
export * from './punchout-close-session/punchout-close-session.component';
export * from './punchout-components.module';
export * from './punchout-error/punchout-error.component';
export * from './punchout-requisition/punchout-requisition.component';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<ng-container *ngIf="(isPunchoutSessionActive$ | async) === true">
<div class="cx-punchout-close-session" (click)="clickCloseSessionButton()">
{{ 'punchout.closeSession' | cxTranslate }}
</div>
</ng-container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0
*/

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { AuthService } from '@spartacus/core';
import { PunchoutFacade, PunchoutStoreService } from '@spartacus/punchout/root';
import { map, Observable, of, switchMap } from 'rxjs';

@Component({
selector: 'cx-punchout-close-session',
templateUrl: './punchout-close-session.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class PunchoutCloseSessionComponent {
protected punchoutStoreService = inject(PunchoutStoreService);
protected authService = inject(AuthService);
protected punchoutFacade = inject(PunchoutFacade);

isPunchoutSessionActive$: Observable<boolean> = this.authService
.isUserLoggedIn()
.pipe(
switchMap((isLoggedIn) => {
return isLoggedIn
? this.punchoutStoreService.getPunchoutState()
: of({ punchoutSessionId: undefined });
}),
map((punchoutState) => {
return !!punchoutState.punchoutSessionId;
})
);

clickCloseSessionButton(): void {
this.punchoutFacade.closePunchoutSession().subscribe();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this obs runs only once, no need to unsubscribe afterwards.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { CmsConfig, I18nModule, provideDefaultConfig } from '@spartacus/core';
import { PunchoutButtonsComponent } from './punchout-buttons/punchout-buttons.component';
import { PunchoutCloseSessionComponent } from './punchout-close-session/punchout-close-session.component';
import { PunchoutErrorComponent } from './punchout-error/punchout-error.component';
import { PunchoutRequisitionComponent } from './punchout-requisition/punchout-requisition.component';
import { PunchoutSessionComponent } from './punchout-session/punchout-session.component';
Expand All @@ -19,12 +20,14 @@ import { PunchoutSessionComponent } from './punchout-session/punchout-session.co
PunchoutErrorComponent,
PunchoutRequisitionComponent,
PunchoutButtonsComponent,
PunchoutCloseSessionComponent,
],
exports: [
PunchoutSessionComponent,
PunchoutErrorComponent,
PunchoutRequisitionComponent,
PunchoutButtonsComponent,
PunchoutCloseSessionComponent,
],
imports: [CommonModule, ReactiveFormsModule, I18nModule],
providers: [
Expand All @@ -42,6 +45,9 @@ import { PunchoutSessionComponent } from './punchout-session/punchout-session.co
PunchoutRequisitionComponent: {
component: PunchoutRequisitionComponent,
},
PunchoutCloseSessionComponent: {
component: PunchoutCloseSessionComponent,
},
},
}),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { PUNCHOUT_SESSION_KEY, PunchoutFacade } from '@spartacus/punchout/root';
import { take } from 'rxjs';
import { switchMap, take } from 'rxjs';
@Component({
selector: 'cx-punchout-session',
template: `<p>Punchout session loading</p> `,
Expand All @@ -24,12 +24,16 @@ export class PunchoutSessionComponent implements OnInit {
protected punchoutFacade = inject(PunchoutFacade);

ngOnInit(): void {
this.activatedRoute.queryParams.pipe(take(1)).subscribe((param: Params) => {
const punchoutSessionId = param?.[PUNCHOUT_SESSION_KEY];
this.punchoutFacade
.getPunchoutSession({ punchoutSessionId })
.pipe(take(1))
.subscribe();
});
this.activatedRoute.queryParams
.pipe(
take(1),
switchMap((param: Params) =>
this.punchoutFacade.getPunchoutSession({
punchoutSessionId: param?.[PUNCHOUT_SESSION_KEY],
})
),
take(1)
)
.subscribe();
}
}
118 changes: 116 additions & 2 deletions integration-libs/punchout/core/facade/punchout.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TestBed } from '@angular/core/testing';
import { CommandService, RoutingService } from '@spartacus/core';
import { MultiCartFacade } from '@spartacus/cart/base/root';
import { CommandService, RoutingService, UserIdService } from '@spartacus/core';
import {
PUNCHOUT_ERROR_PAGE_URL,
PunchOutLevel,
Expand Down Expand Up @@ -35,6 +36,11 @@ const mockPunchoutRequisitionResponse: PunchoutRequisition = {
orderAsCXML: 'mockCXML',
};

const mockPunchoutInitialRequisition: PunchoutRequisition = {
browseFormPostUrl: 'mockInitialFormUrl',
orderAsCXML: 'mockInitialCXML',
};

const mockSessionId = '123abc';

const mockPunchoutSession: PunchoutSession = {
Expand All @@ -51,6 +57,9 @@ const mockPunchoutSession: PunchoutSession = {
const mockPunchoutState: PunchoutState = {
punchoutSessionId: mockSessionId,
punchoutSession: mockPunchoutSession,
punchoutInitialRequisition: undefined,
cancelRequisition: undefined,
closePunchoutSession: undefined,
};

class MockPunchoutStoreService implements Partial<PunchoutStoreService> {
Expand Down Expand Up @@ -83,12 +92,21 @@ class MockRoutingService implements Partial<RoutingService> {
go = () => Promise.resolve(true);
}

class MockMultiCartFacade implements Partial<MultiCartFacade> {
loadCart = () => {};
}

class MockUserIdService implements Partial<UserIdService> {
takeUserId = () => of(mockPunchoutSession.customerId);
}

describe('Punchoutservice', () => {
let service: PunchoutService;
let connector: PunchoutConnector;
let routingService: RoutingService;
let punchoutStoreService: PunchoutStoreService;
let punchoutAuthService: PunchoutAuthService;
let multiCartFacade: MultiCartFacade;

beforeEach(() => {
TestBed.configureTestingModule({
Expand All @@ -99,13 +117,16 @@ describe('Punchoutservice', () => {
{ provide: CommandService, useValue: commandServiceMock },
{ provide: RoutingService, useClass: MockRoutingService },
{ provide: MockPunchoutStoreService, useClass: PunchoutStoreService },
{ provide: MockMultiCartFacade, useClass: MultiCartFacade },
{ provide: MockUserIdService, useClass: UserIdService },
],
});
service = TestBed.inject(PunchoutService);
connector = TestBed.inject(PunchoutConnector);
routingService = TestBed.inject(RoutingService);
punchoutStoreService = TestBed.inject(PunchoutStoreService);
punchoutAuthService = TestBed.inject(PunchoutAuthService);
multiCartFacade = TestBed.inject(MultiCartFacade);
});

it('should be created', () => {
Expand Down Expand Up @@ -170,6 +191,7 @@ describe('Punchoutservice', () => {

it('should getPunchoutSession stays on page when isPageRefresh is true', (done) => {
spyOn(routingService, 'go').and.returnValue(Promise.resolve(true));

spyOn(connector, 'getPunchoutSessionRequisition').and.returnValue(
of(mockPunchoutRequisitionResponse)
);
Expand Down Expand Up @@ -230,23 +252,31 @@ describe('Punchoutservice', () => {

it('should getPunchoutSession opens cart page when no product item and EDIT Level ', (done) => {
spyOn(routingService, 'go').and.returnValue(Promise.resolve(true));

spyOn(connector, 'getPunchoutSession').and.returnValue(
of({
...mockPunchoutSessionResponse,
selectedItem: '',
})
);

spyOn(connector, 'getPunchoutSessionRequisition').and.returnValue(
of(mockPunchoutInitialRequisition)
);
spyOn(punchoutStoreService, 'updatePunchoutState').and.callThrough();
service.getPunchoutSession(mockSessionInput).subscribe({
next: () => {
expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'cart' });
expect(punchoutStoreService.updatePunchoutState).toHaveBeenCalledWith({
punchoutInitialRequisition: { ...mockPunchoutInitialRequisition },
});
done();
},
});
});

it('should getPunchoutSession opens pdp when selectedItem is present ', (done) => {
spyOn(routingService, 'go').and.returnValue(Promise.resolve(true));

spyOn(connector, 'getPunchoutSession').and.returnValue(
of(mockPunchoutSessionResponse)
);
Expand Down Expand Up @@ -278,4 +308,88 @@ describe('Punchoutservice', () => {
},
});
});

it('should closePunchoutSession revertToInitialCart in EDIT operation ', (done) => {
const mockState: PunchoutState = {
...mockPunchoutState,
punchoutInitialRequisition: mockPunchoutInitialRequisition,
closePunchoutSession: true,
punchoutSession: {
...mockPunchoutSession,
punchOutOperation: PunchOutOperation.EDIT,
},
};
spyOn(routingService, 'go').and.returnValue(Promise.resolve(true));
spyOn(punchoutStoreService, 'getPunchoutState').and.returnValue(
of(mockState)
);
spyOn(punchoutStoreService, 'updatePunchoutState').and.callThrough();
spyOn(connector, 'getPunchoutSessionRequisition').and.callThrough();

service.closePunchoutSession().subscribe({
next: () => {
expect(punchoutStoreService.updatePunchoutState).toHaveBeenCalled();
expect(connector.getPunchoutSessionRequisition).not.toHaveBeenCalled();
done();
},
});
});

it('should closePunchoutSession set cancelRequisition in CREATE operation ', (done) => {
const mockState: PunchoutState = {
...mockPunchoutState,
punchoutSession: {
...mockPunchoutSession,
punchOutOperation: PunchOutOperation.CREATE,
},
};
spyOn(routingService, 'go').and.returnValue(Promise.resolve(true));
spyOn(punchoutStoreService, 'getPunchoutState').and.returnValue(
of(mockState)
);

spyOn(punchoutStoreService, 'updatePunchoutState').and.callThrough();
spyOn(multiCartFacade, 'addEntries').and.callThrough();
spyOn(multiCartFacade, 'removeEntry').and.callThrough();

service.closePunchoutSession().subscribe({
next: () => {
expect(punchoutStoreService.updatePunchoutState).toHaveBeenCalledWith({
cancelRequisition: true,
});
expect(multiCartFacade.addEntries).not.toHaveBeenCalled();
expect(multiCartFacade.removeEntry).not.toHaveBeenCalled();
expect(routingService.go).toHaveBeenCalled();
done();
},
});
});

it('should closePunchoutSession only go to requisition page in INSPECT operation ', (done) => {
const mockState: PunchoutState = {
...mockPunchoutState,
punchoutSession: {
...mockPunchoutSession,
punchOutOperation: PunchOutOperation.INSPECT,
},
};
spyOn(routingService, 'go').and.returnValue(Promise.resolve(true));
spyOn(punchoutStoreService, 'getPunchoutState').and.returnValue(
of(mockState)
);

spyOn(punchoutStoreService, 'updatePunchoutState').and.callThrough();
spyOn(multiCartFacade, 'addEntries').and.callThrough();
spyOn(multiCartFacade, 'removeEntry').and.callThrough();

service.closePunchoutSession().subscribe({
next: () => {
expect(punchoutStoreService.updatePunchoutState).not.toHaveBeenCalled();
expect(multiCartFacade.addEntries).not.toHaveBeenCalled();
expect(multiCartFacade.removeEntry).not.toHaveBeenCalled();
expect(routingService.go).toHaveBeenCalled();
done();
},
});
});
});
Loading
Loading