In this blog, we will learn how you can integrate Spartacus – a composable storefront with SAP Commerce to achieve Level 1 B2B Punchout Functionality.
What’s the need – Latest Spartacus 5.2 version does not provide any OOTB feature/library to integrate B2B Level 1 or Level 2 Punchout to SAP Commerce.
Before diving into the implementation part from Spartacus’s end, let’s understand the B2B Punchout feature in SAP Commerce.
SAP Commerce 2211 B2B Punchout
Punchout in SAP commerce allows buyers to shop for different suppliers’ online catalogs and save the items selected for purchase as a requisition in the buyer’s procurement system. Here for this blog consider the buyer’s procurement system as SAP Ariba.
B2B Punchout is not a new feature in SAP Commerce, this was present as an addon in the previous version which can be installed on the accelerator storefront.
As we know that accelerator storefront is based on the JSP storefront and which is becoming obsolete now and the SAP commerce implementations are moving towards headless architecture.
In the latest release of SAP Commerce 2211, to support the headless architecture. The Punchout functionality in Commerce is now offered through the new Punchout module and has been enhanced in the following ways:
-
- New extensions are introduced to make the overall functionality efficient and stable.
-
- A new OCC API for Punchout is introduced to expose the functionalities of setting up a session, creating a profile, and getting order information.
-
- The password requirements for Shared Secret and its storage have been strengthened.
-
- The cXML files are handled by the latest DTD structure.
-
- A cronjob to clean up the expired PunchOut sessions can now be scheduled from the backoffice administration cockpit.
If you need to understand more about the B2B Pucnhout functionality, I would strongly recommend you to go through the below link
NOTE: In this blog, I will limit my explanation on how you can configure or make necessary changes to your SPARTACUS store to achieve the Level 1 Punchout functionality.
Setting Up SAP Commerce and Requisition System Credentials
-
- SAP Commerce 2211 is installed and it’s up and running with the below extensions added to local.extension file
<extension name='b2bpunchout' />
<extension name='b2bpunchoutbackoffice' />
<extension name='b2bpunchoutocc' />
-
- Below impex is imported in the SAP Commerce to set the Punchout credentials
# ---------------------------------------------------------------------------
# Copyright (c) 2022 SAP SE or an SAP affiliate company. All rights reserved.
# ---------------------------------------------------------------------------
INSERT_UPDATE PunchOutCredential ; code[unique = true] ; domain[unique = true] ; identity[unique = true] ; sharedsecret
; NetworkId1 ; NetworkID ; AN01000002779-T ; VerySecret1234$
; AribaSupplier ; NetworkId ; AN01000865920-T ; VerySecret1234$
; DUNS1 ; DUNS ; 123456789 ; VerySecret1234$
; AribaNetworkUserId1 ; AribaNetworkUserId ; sysadmin@ariba.com ; VerySecret1234$
; DUNS2 ; DUNS ; 123 ; VerySecret1234$
INSERT_UPDATE B2BCustomerPunchOutCredentialMapping ; B2BCustomer(uid)[unique = true] ; credentials(code)
; punchout.customer@punchoutorg.com ; NetworkId1,DUNS1
; punchout.customer2@punchoutorg.com ; AribaNetworkUserId1,AribaSupplier
INSERT_UPDATE OAuthClientDetails ; clientId[unique=true] ; resourceIds ; scope ; authorizedGrantTypes ; authorities ; clientSecret ; registeredRedirectUri
; punchout_client ; hybris ; basic ; password,client_credentials ; ROLE_CLIENT ; ;
INSERT_UPDATE B2BCustomer ; description ; customerID ; uid[unique = true] ; originalUid ; email ; name ; title(code) ; groups(uid) ; loginDisabled ; password ; permissionGroups(uid) ; sessionCurrency(isocode)[default = 'USD'] ;
; PunchOut Session Sample Customer ; punchout.customer.session@punchoutorg.com ; punchout.customer.session@punchoutorg.com ; punchout.customer.session@punchoutorg.com ; punchout.customer.session@punchoutorg.com ; PunchOut Customer Session ; mr ; PunchOut Organization, b2bcustomergroup ; false ; pwd4all ; ; ;
INSERT_UPDATE PunchOutCredential ; code[unique = true] ; domain[unique = true] ; identity[unique = true] ; sharedsecret
; DUNSSession ; DUNSSession ; 888888 ; VerySecret1234$
INSERT_UPDATE B2BCustomerPunchOutCredentialMapping ; B2BCustomer(uid)[unique = true] ; credentials(code)
; punchout.customer.session@punchoutorg.com ; DUNSSession
-
- Add the Below properties in local.properties of SAP Commerce. This helps in mapping the Spartacus landing page URL with the “initiate a Punchout Session” API.
website.powertools-spa.https=http://localhost:4200 website.powertools-spa.http=http://localhost:4200
- Add the Below properties in local.properties of SAP Commerce. This helps in mapping the Spartacus landing page URL with the “initiate a Punchout Session” API.
Customer B2B Punchout Journey
1. The customer logs into the Ariba supplier site http://supplier.ariba.com/
2. Customers select any one of the available suppliers
3. Clicks on the punchout Icon.
4. Customer Lands on Spartacus Homepage as Punhout Logged In user.
5. Browse the supplier catalog and add the product to the cart.
6. Return back to with the Product added to the cart as a purchase requisition in the buyer’s procurement system
Technical User Flow
Step 1 to Step 3 remains the same as the customer journey
4. Procurement system internally invokes SAP Commerce Backend OCC POST API ({baseSiteId}/punchout/cxml/setup) to set up the punchout profile
5. SAP Commerce backend now creates a user punchout session and responds with a home page URL which includes a hex code token.
6. OCC response will include, Spartacus home page URL to which the procurement system will redirect the Customer to the landing page.
You will understand these steps more when you start testing the flow.
Swagger API Details For Punchout in 2211 version OOTB
Setting Up Spartacus Storefront 5.2 Version
Now we have just made our SAP Commerce backend ready for B2B Punchout. This was mostly configuration.
Create a new angular app with the name “myStore”. This will be used as a store name in this document. I will not go through the steps to set up a Composable storefront as it is already outlined in many other Blogs. You can also refer to the self-explanatory help link below.
http://Building the Composable Storefront From 5.2 Libraries | SAP Help Portal
NOTE: Make sure to enable the B2B features in the interactive tool while setting up the Spartacus store as we are working on the B2B Punchout.
Once you are done with the storefront setup, make sure you can access the powertools-spa website ( https://localhost:4200/powertools-spa/en/USD )
Let’s now begin with changes to be done on the SPARTACUS end to accommodate the Punchout functionality
STEP 1 :
The first step is to listen to the URL with the shape ‘punchout/cxml/session?sid=asddfsdf…..’ . This can be achieved by making use of Router configuration in angular.
Open your project in VS code or any other code editor and create a new routing module in your “myStore” app under the location “src/app/ ” with a file name “app-routing.module.ts” and inject this file in “app.module.ts”. Below is the code to be added to the file
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LoginRedirectComponent } from './demo/features/login-redirect/login-redirect.component';
const routes: Routes = [
{
path: 'punchout/cxml/session',
component: LoginRedirectComponent,
},
];
@NgModule({
declarations: [],
imports: [RouterModule.forRoot(routes)],
})
export class DemoRoutingModule {}
You will see errors in your Visual Studio Code for this class as you have not yet created “LoginRedirectComponent”
STEP 2:
The next step is to create a new angular component – LoginRedirectComponent. Create this new component under ‘src/app/demo/features/login-redirect’. (You need to create this folder structure as this will not exist).
Component holds the main functionality to extract the SID token from the URL, send the SID token to SAP Commerce OCC API “’punchout/sessions/${sid}’ and get the user-related details. Once you have created a LoginRedirectComponent. It looks like the below is an empty class.
import { Component, OnInit } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Component({
selector: 'app-login-redirect',
templateUrl: './login-redirect.component.html',
styleUrls: ['./login-redirect.component.scss'],
})
export class LoginRedirectComponent implements OnInit {
protected busy$ = new BehaviorSubject(true);
constructor() {}
ngOnInit(): void {}
}
Below is the code for LoginRedirectModule and HTML code snippet
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { SpinnerModule } from '@spartacus/storefront';
import { LoginRedirectComponent } from './login-redirect.component';
@NgModule({
declarations: [LoginRedirectComponent],
imports: [CommonModule, SpinnerModule],
exports: [LoginRedirectComponent],
})
export class LoginRedirectModule {}
<cx-spinner class="overlay" *ngIf="busy$ | async"></cx-spinner>
STEP 3:
Now we will create a service to call SAP Commerce OCC API to fetch the user session info from the “SID” token. We will read the sid value from the previous step and pass it in the below GET request as shown below.
Request : {baseSiteId}/punchout/sessions/{sid}
HTTP Method: GET
Response:
{
cartId: '00002159',
token: {
accessToken: 'adss2332ew324324ewr3432432de',
tokenType: 'bearer',
},
userId: 'punchout.customer@punchoutorg.com'
}
}
Observe the response, it consists of cartId for the user and an access token. We will use this to set the “active cart ID” and “access token” in Spartacus. So that user will be automatically logged In.
Create a new Service class “PunchoutService” in Spartacus to fetch the above details from SAP Commerce. Below is a sample code, which is created under the file structure “src/app/demo/services”
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { OccEndpointsService } from '@spartacus/core';
import { Observable } from 'rxjs';
export interface PunchOutSession {
customerId: string;
cartId: string;
punchOutLevel: string;
punchOutOperation: string;
selectedItem: string;
token: PunchOutToken;
}
export interface PunchOutToken {
accessToken: String;
tokenType: String;
}
@Injectable({
providedIn: 'root',
})
export class PunchOutService {
constructor(
protected http: HttpClient,
protected occEndpoints: OccEndpointsService
) {}
getPunchOutSessionData(sid: string): Observable<PunchOutSession> {
localStorage.setItem('punchout-sid-key', sid);
const url = this.occEndpoints.buildUrl('getPunchOutAccessToken', {
urlParams: { sid: sid },
});
return this.http.get<PunchOutSession>(url, httpOptions);
}
}
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
}),
};
Step 4:
URL “getPunchOutAccessToken” can be configured as new constant file and must be referred to in the provider’s section of the Spartacus-configuration.module.ts file as below
A new constant file to hold all the URLs should be created and imported accordingly in the Spartacus-configuration.module.ts file
Below is a sample constant file
export const DemoOccEndPoints = {
getPunchOutAccessToken: 'punchout/sessions/${sid}'
};
Below is a code snippet where you need to inject the DemoOccEndPoints constant file in your application.
provideConfig(<OccConfig>{
backend:{
occ:{
endpoints:{
…DemoOccEndPoints
}
}
}
Step 5: Set Active Cart ID and Access Token in the Spartacus Session. To achieve this let us re-write the “LoginRedirectComponent” to achieve this functionality.
import { ActivatedRoute } from '@angular/router';
import { Store } from '@ngrx/store';
import { CartActions } from '@spartacus/cart/base/core';
import {
AuthActions,
AuthStorageService,
AuthToken,
BASE_SITE_CONTEXT_ID,
OCC_USER_ID_CURRENT,
RoutingService,
SiteContextParamsService,
StatePersistenceService,
StorageSyncType,
UserIdService,
} from '@spartacus/core';
import { BehaviorSubject, of, Subscription } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
import { PunchOutService } from '../../services/punchout-service';
@Component({
selector: 'app-login-redirect',
templateUrl: './login-redirect.component.html',
styleUrls: ['./login-redirect.component.scss'],
})
export class LoginRedirectComponent implements OnInit {
protected busy$ = new BehaviorSubject(true);
protected subscription = new Subscription();
constructor(
private activateRoute: ActivatedRoute,
private userIdService: UserIdService,
private store: Store,
private punchoutService: PunchOutService,
private authStorageService: AuthStorageService,
private routing: RoutingService,
private statePersistenceService: StatePersistenceService,
protected siteContextParamsService: SiteContextParamsService
) {}
ngOnInit(): void {
this.activateRoute.queryParams
.pipe(
switchMap((params) => {
return this.punchoutService
.getPunchOutSessionData(params['sid'])
.pipe(
tap((data) => {
if (data.token.accessToken) {
this.statePersistenceService.syncWithStorage({
key: 'cart',
state$: of({
active: data.cartId,
}),
context$: this.siteContextParamsService.getValues([
BASE_SITE_CONTEXT_ID,
]),
storageType: StorageSyncType.LOCAL_STORAGE,
onRead: (state) => this.onRead(state),
});
this.authStorageService.setToken({
access_token: data.token.accessToken,
token_type: data.token.tokenType,
} as AuthToken);
this.userIdService.setUserId(OCC_USER_ID_CURRENT);
this.store.dispatch(new AuthActions.Login());
this.busy$.next(false);
this.routing.go('/');
}
})
);
})
)
.subscribe();
}
protected onRead(state: { active: string } | undefined) {
this.store.dispatch(new CartActions.ClearCartState());
if (state) {
this.store.dispatch(new CartActions.SetActiveCartId(state.active));
} else {
this.store.dispatch(new CartActions.SetActiveCartId(''));
}
}
}
STEP 6:
Start the server to check if there are any errors and resolve them accordingly. Mostly you could have missed an import or had an incorrect entry. Make sure to correct as per your application specific. Also, make sure you have injected LoginRedirectComponent and DemoRoutingModule in the app.module.ts file else it will not consider this file when the angular app is bootstrapped.
STEP 7:
If Step 6 is successful, then it’s time for testing. Here for testing purposes, I will use POSTMAN to send Request to SAP Commerce Backend OCC API to set up a login session instead of directly configuring it in the S4 Ariba system.
POST Request For Setup API: This API will be invoked from S4 to set up user sessions in SAP Commerce.
Request URL: https://localhost:9002/occ/v2/powertools-spa/punchout/cxml/setup
Request Body:
<?xml version="1.0" encoding="UTF-8"?>
<cXML payloadID="1391193486192-325467636084519232@216.109.111.62" timestamp="2013-12-19T09:07:19-08:00" xml:lang="en-US">
<Header>
<From>
<Credential domain="NetworkId">
<Identity>AN01000002779-T</Identity>
</Credential>
</From>
<To>
<Credential domain="NetworkID">
<Identity>AN01000002779-T</Identity>
</Credential>
</To>
<Sender>
<Credential domain="AribaNetworkUserId">
<Identity>sysadmin@ariba.com</Identity>
<SharedSecret>VerySecret1234#</SharedSecret>
</Credential>
<UserAgent>Buyer 14s2</UserAgent>
</Sender>
</Header>
<Request>
<PunchOutSetupRequest operation="create">
<BuyerCookie>LwT6nfFQnOvejEKnDZydKtDnG3fq5fes0.2450842054421388232</BuyerCookie>
<Extrinsic name="CostCenter">670</Extrinsic>
<Extrinsic name="UniqueName">catalog_tester</Extrinsic>
<Extrinsic name="UserEmail">catalog_tester@ariba.com</Extrinsic>
<BrowserFormPost>
<URL>https://service.ariba.com/CatalogTester.aw/6218326/ad/handlePunchOutOrder/LwT6nfFQnOvejEKnDZydKtDnG3fq5fes0.24508420544213832232?awr=2&u=bWkn28w2x&awps=ZrbRT213H7OgcFgLfW</URL>
</BrowserFormPost>
<SupplierSetup>
<URL>https://localhost:9002/yacceleratorstorefront/cxml/punchout/new?site=powertools-spa</URL>
</SupplierSetup>
<ShipTo>
<Address addressID="26">
<Name xml:lang="en-US">Catalog Tester</Name>
<PostalAddress name="_5uicbb">
<DeliverTo>Catalog Tester</DeliverTo>
<Street>1234 Catalog Tester Way</Street>
<City>Sunnyvale</City>
<State>CA</State>
<PostalCode>94089</PostalCode>
<Country isoCountryCode="US">United States</Country>
</PostalAddress>
</Address>
</ShipTo>
<SelectedItem>
<ItemID>
<SupplierPartID>AAA</SupplierPartID>
<SupplierPartAuxiliaryID />
</ItemID>
</SelectedItem>
</PunchOutSetupRequest>
</Request>
</cXML>
Response: Observe the <URL> tag in the below response. It has a landing page URL for Spartacus with a SID request parameter.The SID parameter will hold the information of the commerce user.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE cXML SYSTEM "http://xml.cXML.org/schemas/cXML/1.2.051/cXML.dtd">
<cXML payloadID="d868d40c-7bc3-4e3e-96fa-668cc6715ec8" timestamp="2023-04-14T06:29:20Z" xml:lang="en-US">
<Response>
<Status code="200" text="success"/>
<PunchOutSetupResponse>
<StartPage>
<URL>https://localhost:4200/punchout/cxml/session?sid=VeUFhPEfVVPhqNTQ2KGL7tojZkDuyCLXMwbqqqq234343HGs</URL>
</StartPage>
</PunchOutSetupResponse>
</Response>
</cXML>
Copy the URL from the Response and paste it into Browser and click enter.
https://localhost:4200/ punchout/cxml/session?sid=MdXK777UoiHOrFnYjimf89Lardkp3tQ7pInKP67DIjs