CX Works

A single portal for curated, field-tested and SAP-verified expertise for your SAP C/4HANA suite. Whether it's a new implementation, adding new features, or getting additional value from an existing deployment, get it here, at CX Works.

Custom SAP Commerce Cloud Events for SAP Cloud Platform Extension Factory

Extend your use case with custom events

SAP Cloud Platform Extension Factory provides a flexible way to extend SAP Commerce Cloud using events - the list of events supplied out-of-the-box can be found here. However, your use case may require events that are not implemented in the current list. This article describes how to create custom events in SAP Commerce for SAP Cloud Platform Extension Factory. A custom SAP Commerce Cloud extension is created to expose a custom event in SAP Commerce Cloud to SAP Cloud Platform Extension Factory.

Table of Contents


Introduction

SAP Commerce Cloud events are simple Java classes without further metadata, which extend the SAP Commerce Cloud AbstractEvent.

There are three steps to creating a custom event:

  1. Define the event bean.
  2. Register the event with the application connector.
  3. Trigger the event from the business logic.

Example Use Case

Users can write product reviews on our SAP Commerce Cloud website. We would like to analyze these reviews for customer sentiment based on what they write. Currently, when a user submits a product review, there is no out-of-the-box event. Once we create one, we can write a serverless function in SAP Cloud Platform Extension Factory to send the review comments to a text analytics service which will determine whether the user's sentiment is positive or negative. Based on that result, we can trigger other actions like notify our customer service representative through a ticketing system or create a marketing interaction to follow up with the user. The example event included in this article is product.reviewsubmitted.

Process Flow


Implementation

Custom Extension Setup

  1. Create a custom SAP Commerce Cloud extension, my-custom-events, using the yempty template with ant extgen.

    ant extgen -Dtemplate=yempty -Dname=my-custom-events
  2. Register your new extension in localextensions.xml.

    localextensions.xml
      <extensions>
    	...
        <extension name="my-custom-events"/>
      </extensions>


  3. Add the following extension dependencies to your my-custom-events extension's extensioninfo.xml file.

    extensioninfo.xml
    <requires-extension name="commercefacades"/>
    <requires-extension name="kymaintegrationservices"/>
    <requires-extension name="kymaintegrationsampledata"/>

Extend CustomerReview Type

The example event in this article is triggered when a customer product review is submitted. This leverages the customerreview extension in SAP Commerce Cloud. Out-of-the-box, the CustomerReview type does not have a unique key. For this example, the CustomerReview type is extended and a unique reviewcode attribute is added. This allows you to model the CustomerReview type as an Integration Object.

  1. Extend the type in *items.xml.

    my-custom-events-items.xml
    <itemtype code="CustomerReview" autocreate="false" generate="false" >
       <attributes>
          <attribute type="java.lang.String" qualifier="code">
             <description>
                Unique id for the customer review
             </description>
             <persistence type="property" />
             <modifiers read="true" write="true" search="true" initial="true" optional="false" unique="true"/>
          </attribute>
       </attributes>
    </itemtype>
  2. Run ant all.
  3. Extend the CustomerReviewService to populate and save the new code attribute.

    my-custom-events-spring.xml
    <bean id="mycustomerreviewService" class="com.example.my-custom-events.service.impl.MyCustomerReviewService" parent="defaultCustomerReviewService">
        <property name="modelService" ref="modelService"/>
        <property name="customerReviewDao" ref="customerReviewDao"/>
    </bean>
    <alias alias="customerReviewService" name="mycustomerreviewService"/>
  4. Implement the MyCustomerReviewService.java.

    MyCustomerReviewService.java
    package com.example.my-custom-events.service.impl;
    import de.hybris.platform.core.model.product.ProductModel;
    import de.hybris.platform.core.model.user.UserModel;
    import de.hybris.platform.customerreview.CustomerReviewService;
    import de.hybris.platform.customerreview.impl.DefaultCustomerReviewService;
    import de.hybris.platform.customerreview.model.CustomerReviewModel;
    import org.apache.commons.lang.StringUtils;
    import java.util.Date;
    import java.util.UUID;
    public class MyCustomerReviewService extends DefaultCustomerReviewService implements CustomerReviewService {
        @Override
        public CustomerReviewModel createCustomerReview(final Double rating, final String headline, final String comment,
                                                        final UserModel user, final ProductModel product)
        {
            UUID code = UUID.randomUUID();
            return createCustomerReview(rating,headline,comment,user,product,code.toString());
        }
        public CustomerReviewModel createCustomerReview(final Double rating, final String headline, final String comment,
                                                        final UserModel user, final ProductModel product, String code)
        {
            final CustomerReviewModel review = getModelService().create(CustomerReviewModel.class);
            review.setUser(user);
            review.setProduct(product);
            review.setRating(rating);
            review.setHeadline(headline);
            review.setComment(comment);
            review.setCode(code);
            getModelService().save(review);
            return review;
        }
    }

Create a New Custom Event

  1. Create a bean in my-custom-events-beans.xml and run ant all. This will generate the event bean class.

    my-custom-events-beans.xml
     <bean class="com.example.events.ProductReviewSubmittedEvent" type="event">
         <property name="reviewcode" type="String"/>
     </bean>
  2. Build and run the system update.

  3. Extend the relevant Facade or Service bean to invoke the eventService (ie. DefaultProductFacade).

    MyEventProductFacade.java
    final ProductReviewSubmittedEvent prsEvent = new ProductReviewSubmittedEvent();
    prsEvent.setReviewcode(customerReviewModel.getCode());
    getEventService().publishEvent(prsEvent); 
  4. Add your event configuration through the customevents.impex.

    customevents.impex
    $destination_target = Default_Template
    INSERT_UPDATE EventConfiguration;eventClass[unique=true];destinationTarget(id)[unique = true,default=$destination_target];version[unique=true,default=1];exportFlag;priority(code);exportName;mappingType(code)[default=GENERIC];converterBean;description;extensionName
                                    ; com.example.events.ProductReviewSubmittedEvent                                 ;;; true      ; MEDIUM    ; product.reviewsubmitted                               ;;; "Product Review Submitted 1"                         ; my-custom-events
    INSERT_UPDATE EventPropertyConfiguration; eventConfiguration(eventClass, destinationTarget(id[default = $destination_target]), version[default = 1])[unique = true]; propertyName[unique = true]; propertyMapping         ; title            ; description     ; examples(key, value)[map-delimiter = |]; required[default = true]; type[default = 'string'];
                                            ; com.example.events.ProductReviewSubmittedEvent                                                                  ; reviewcode                   ; "event.reviewcode" ; "Review Code"       ; Review Code - UUID ;     reviewcode->cb7b6c47-d078-4d9a-99fc-71e21972dd16                                   ;                         ;
     

Create an Integration Object

The InboundCustomerReview Integration Object allows services and functions running in SAP Cloud Platform Extension Factory to access the customer review details through the OData API.

customevents.impex
INSERT_UPDATE IntegrationObject; code[unique = true]; integrationType(code)
                               ; InboundCustomerReview; INBOUND
INSERT_UPDATE IntegrationObjectItem; integrationObject(code)[unique=true]; code[unique = true]; type(code)
                                   ; InboundCustomerReview ; Title                        ; Title
                                   ; InboundCustomerReview ; CustomerReview               ; CustomerReview
                                   ; InboundCustomerReview ; Gender                       ; Gender
                                   ; InboundCustomerReview ; Address                      ; Address
                                   ; InboundCustomerReview ; Region                       ; Region
                                   ; InboundCustomerReview ; User                         ; User
                                   ; InboundCustomerReview ; Product                      ; Product
                                   ; InboundCustomerReview ; Country                      ; Country
                                   ; InboundCustomerReview ; CustomerReviewApprovalType   ; CustomerReviewApprovalType
                                   ; InboundCustomerReview ; Category                     ; Category
                                   ; InboundCustomerReview ; Catalog                      ; Catalog
                                   ; InboundCustomerReview ; Language                     ; Language
                                   ; InboundCustomerReview ; CatalogVersion               ; CatalogVersion
INSERT_UPDATE IntegrationObjectItemAttribute; integrationObjectItem(integrationObject(code), code)[unique = true]; attributeName[unique = true]; attributeDescriptor(enclosingType(code), qualifier); returnIntegrationObjectItem(integrationObject(code), code); unique[default = false]; autoCreate[default = false]
                                            ; InboundCustomerReview:Title                      ; code                     ; Title:code                       ;                                                  ; true ;
                                            ; InboundCustomerReview:CustomerReview             ; product                  ; CustomerReview:product           ; InboundCustomerReview:Product                    ;  ;
                                            ; InboundCustomerReview:CustomerReview             ; alias                    ; CustomerReview:alias             ;                                                  ;  ;
                                            ; InboundCustomerReview:CustomerReview             ; blocked                  ; CustomerReview:blocked           ;                                                  ;  ;
                                            ; InboundCustomerReview:CustomerReview             ; code                     ; CustomerReview:code              ;                                                  ; true ;
                                            ; InboundCustomerReview:CustomerReview             ; headline                 ; CustomerReview:headline          ;                                                  ;  ;
                                            ; InboundCustomerReview:CustomerReview             ; user                     ; CustomerReview:user              ; InboundCustomerReview:User                       ;  ;
                                            ; InboundCustomerReview:CustomerReview             ; language                 ; CustomerReview:language          ; InboundCustomerReview:Language                   ;  ;
                                            ; InboundCustomerReview:CustomerReview             ; rating                   ; CustomerReview:rating            ;                                                  ;  ;
                                            ; InboundCustomerReview:CustomerReview             ; comment                  ; CustomerReview:comment           ;                                                  ;  ;
                                            ; InboundCustomerReview:CustomerReview             ; approvalStatus           ; CustomerReview:approvalStatus    ; InboundCustomerReview:CustomerReviewApprovalType ;  ;
                                            ; InboundCustomerReview:Gender                     ; code                     ; Gender:code                      ;                                                  ; true ;
                                            ; InboundCustomerReview:Address                    ; fax                      ; Address:fax                      ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; appartment               ; Address:appartment               ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; region                   ; Address:region                   ; InboundCustomerReview:Region                     ;  ;
                                            ; InboundCustomerReview:Address                    ; remarks                  ; Address:remarks                  ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; phone1                   ; Address:phone1                   ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; middlename2              ; Address:middlename2              ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; line1                    ; Address:line1                    ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; streetnumber             ; Address:streetnumber             ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; url                      ; Address:url                      ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; streetname               ; Address:streetname               ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; title                    ; Address:title                    ; InboundCustomerReview:Title                      ;  ;
                                            ; InboundCustomerReview:Address                    ; typeQualifier            ; Address:typeQualifier            ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; cellphone                ; Address:cellphone                ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; gender                   ; Address:gender                   ; InboundCustomerReview:Gender                     ;  ;
                                            ; InboundCustomerReview:Address                    ; lastname                 ; Address:lastname                 ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; middlename               ; Address:middlename               ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; line2                    ; Address:line2                    ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; building                 ; Address:building                 ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; postalcode               ; Address:postalcode               ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; district                 ; Address:district                 ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; email                    ; Address:email                    ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; department               ; Address:department               ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; company                  ; Address:company                  ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; firstname                ; Address:firstname                ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; publicKey                ; Address:publicKey                ;                                                  ; true ;
                                            ; InboundCustomerReview:Address                    ; town                     ; Address:town                     ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; phone2                   ; Address:phone2                   ;                                                  ;  ;
                                            ; InboundCustomerReview:Address                    ; country                  ; Address:country                  ; InboundCustomerReview:Country                    ;  ;
                                            ; InboundCustomerReview:Address                    ; pobox                    ; Address:pobox                    ;                                                  ;  ;
                                            ; InboundCustomerReview:Region                     ; country                  ; Region:country                   ; InboundCustomerReview:Country                    ; true ;
                                            ; InboundCustomerReview:Region                     ; isocode                  ; Region:isocode                   ;                                                  ; true ;
                                            ; InboundCustomerReview:User                       ; defaultPaymentAddress    ; User:defaultPaymentAddress       ; InboundCustomerReview:Address                    ;  ;
                                            ; InboundCustomerReview:User                       ; name                     ; User:name                        ;                                                  ;  ;
                                            ; InboundCustomerReview:User                       ; defaultShipmentAddress   ; User:defaultShipmentAddress      ; InboundCustomerReview:Address                    ;  ;
                                            ; InboundCustomerReview:User                       ; uid                      ; User:uid                         ;                                                  ; true ;
                                            ; InboundCustomerReview:Product                    ; name                     ; Product:name                     ;                                                  ;  ;
                                            ; InboundCustomerReview:Product                    ; supercategories          ; Product:supercategories          ; InboundCustomerReview:Category                   ;  ;
                                            ; InboundCustomerReview:Product                    ; catalogVersion           ; Product:catalogVersion           ; InboundCustomerReview:CatalogVersion             ; true ;
                                            ; InboundCustomerReview:Product                    ; averageRating            ; Product:averageRating            ;                                                  ;  ;
                                            ; InboundCustomerReview:Product                    ; description              ; Product:description              ;                                                  ;  ;
                                            ; InboundCustomerReview:Product                    ; code                     ; Product:code                     ;                                                  ; true ;
                                            ; InboundCustomerReview:Country                    ; isocode                  ; Country:isocode                  ;                                                  ; true ;
                                            ; InboundCustomerReview:CustomerReviewApprovalType ; code                     ; CustomerReviewApprovalType:code  ;                                                  ; true ;
                                            ; InboundCustomerReview:Category                   ; code                     ; Category:code                    ;                                                  ; true ;
                                            ; InboundCustomerReview:Category                   ; catalogVersion           ; Category:catalogVersion          ; InboundCustomerReview:CatalogVersion             ; true ;
                                            ; InboundCustomerReview:Catalog                    ; id                       ; Catalog:id                       ;                                                  ; true ;
                                            ; InboundCustomerReview:Language                   ; isocode                  ; Language:isocode                 ;                                                  ; true ;
                                            ; InboundCustomerReview:CatalogVersion             ; catalog                  ; CatalogVersion:catalog           ; InboundCustomerReview:Catalog                    ; true ;
                                            ; InboundCustomerReview:CatalogVersion             ; version                  ; CatalogVersion:version           ;                                                  ; true ;
 

SAP Cloud Platform Extension Factory Setup

  1. Connect SAP Commerce Cloud with SAP Cloud Platform Extension Factory. See SAP Commerce Help.
  2. Bind the Application to a Namespace. See Kyma Documentation.
  3. Add the following services to your namespace:

    • ec-occ-commerce-webservices-v2

    • ec-events-v1 

  4. Create the following Lambda Function

    process-review.js
    const request = require('request');
    const traceHeaders = ['x-request-id', 'x-b3-traceid', 'x-b3-spanid', 'x-b3-parentspanid', 'x-b3-sampled', 'x-b3-Flags', 'x-ot-span-context'];
    const PARAM_CODE = "code";
    var serviceurl = `${process.env.SERVICE_URL}`;
    var serviceuid = `${process.env.SERVICE_UID}`;
    var servicepw = `${process.env.SERVICE_PW}`;
    var gatewayurl = `${process.env.GATEWAY_URL}`;
    var basesite = `${process.env.BASE_SITE}`;
    var userid;
    module.exports = { main: function (event, context) {
            console.log('********** Event Data:');
            console.log(event.data);
            var reviewcode = event.data.reviewcode;
            if (serviceurl === undefined) {
                console.log('Environment variable SERVICE_URL is not defined');
            }
            var traceCtxHeaders = extractTraceHeaders(event.extensions.request.headers);
            getReviewDetails(reviewcode, traceCtxHeaders);
        }};
    function getReviewDetails(reviewcode, traceCtxHeaders) {
        console.log("********** getReviewDetails()");
        var url = `${serviceurl}/CustomerReviews('${reviewcode}')`;
        request.get({
            headers: traceCtxHeaders, url: url, json: true,
            auth: {
                user: serviceuid,
                pass: servicepw,
                'sendImmediately': false
            }
        }, function (error, response, body) {
            if (error === null) {
                console.log(`********** Response.statusCode:\n${response.statusCode}`);
                if (response.statusCode == '200') {
                    console.log('********** Response body:');
                    console.log(body);
                    console.log(`********** User reference:\n${body.d.user.__deferred.uri}`);
                    var userUri = body.d.user.__deferred.uri;
                    getReviewUser(userUri, traceCtxHeaders);
                } else {
                    console.log('Call to ODATA webservice failed with status code ' + response.statusCode);
                    console.log('********** Response body:');
                    console.log(response.body);
                }
            } else {
                console.log('********** Error:');
                console.log(error);
            }
        });
    }
    function getReviewUser(userUri, traceCtxHeaders) {
        console.log("********** getReviewUser()");
        var url = userUri;
        request.get({
            headers: traceCtxHeaders, url: url, json: true,
            auth: {
                user: serviceuid,
                pass: servicepw,
                'sendImmediately': false
            }
        }, function (error, response, body) {
            if (error === null) {
                console.log(`********** Response.statusCode:\n${response.statusCode}`);
                if (response.statusCode == '200') {
                    console.log('********** Response body:');
                    console.log(body);
                    userid = body.d.uid;
                    console.log(`********** UID:\n${userid}`);
                    getUserDetails(userid,traceCtxHeaders);
                } else {
                    console.log('Call to ODATA webservice failed with status code ' + response.statusCode);
                    console.log('********** Response body:');
                    console.log(response.body);
                }
            } else {
                console.log('********** Error:');
                console.log(error);
            }
        });
    }
    function getUserDetails(userid, traceCtxHeaders){
        console.log("********** getUserDetails()");
        console.log(`********** userid:\n${userid}`);
        var url = `${gatewayurl}/${basesite}/users/${userid}?fields=FULL`;
        request.get({headers:traceCtxHeaders, url: url, json: true}, function(error, response, body) {
            if(error === null) {
                console.log(response.statusCode);
                if(response.statusCode == '200'){
                    console.log(`****** User uid: ${userid} Name: ${body.firstName} ${body.lastName}` );
                    console.log(body);
                }else{
                    console.log('Call to EC webservice failed with status code ' + response.statusCode);
                    console.log(response.body);
                }
            } else {
                console.log(error);
            }
        });
    }
    function extractTraceHeaders(headers) {
        console.log("********** extractTraceHeaders()");
        console.log(headers);
        var map = {};
        for (var i in traceHeaders) {
            h = traceHeaders[i];
            headerVal = headers[h];
            console.log('header' + h + "-" + headerVal);
            if (headerVal !== undefined) {
                console.log('if not undefined header' + h + "-" + headerVal);
                map[h] = headerVal;
            }
        }
        return map;
    }
  5. Dependencies:

    package.json
    {
      "name": "app",
      "version": "0.0.1",
      "dependencies": {
        "request": "^2.85.0"
      }
    }
  6. Add the event trigger product.reviewsubmitted.


  7. Create a Service Binding to ec-occ-commerce-webservices-v2.


  8. Add the following environment variables:

    • SERVICE_URL - the SAP Commerce Cloud OData API URL for your InboundCustomerReview Integration Object
    • SERVICE_UID - the user ID for the OData service (our example uses BASIC auth)
    • SERVICE_PWD - the user password for the OData service
    • BASE_SITE - the base site (example, 'electronics') for the OCC API call

Managing Service Connection Parameters with Secrets

A more secure way to manage the service connection details is using Kubernetes Secrets. This can be configured using YAML configuration and the Kubernetes CLI or with the kubeless CLI.

In the last step above, omit the SERVICE_URL, SERVICE_UID and SERVICE_PW environment variables.


Create the secret like so:

kubectl create secret
kubectl create secret generic ecc-odata-review-service -n ${NAMESPACE} --type=string --from-literal=SERVICE_URL=https://${COMMERCE_API_URL}/odata2webservices/InboundCustomerReview/CustomerReviews --from-literal=SERVICE_UID=admin --from-literal=SERVICE_PW=nimda


Deploying the function with the kubeless command allows you to maintain your function code in a separate Javascript and package.json files and specify a secret to mount as a volume attached to the container running your code.

kubeless CLI
kubeless function deploy process-review --runtime nodejs8 --handler handler.main --from-file process-review.js --dependencies package.json --env BASESITE=electronics--label=app=process-review --namespace $NAMESPACE --cpu 0.1 --memory 128Mi --secrets ecc-odata-review-service

Kubeless will mount the secret as files that can be accessed.

process-review.js
var surl = fs.readFileSync('/ecc-odata-review-service/SERVICE_URL');
var suid = fs.readFileSync('/ecc-odata-review-service/SERVICE_UID');
var spwd = fs.readFileSync('/ecc-odata-review-service/SERVICE_PWD');


Another option is to inject the secret as container environment variables. This can be defined on the function through YAML as discussed in the Kubernetes documentation.

process-review-function.yaml
... 
env:
- name: SERVICE_URL
valueFrom:
secretKeyRef:
name: ecc-odata-review-service
key: SERVICE_URL
- name: SERVICE_UID
valueFrom:
secretKeyRef:
name: ecc-odata-review-service
key: SERVICE_UID
- name: SERVICE_PWD
valueFrom:
secretKeyRef:
name: ecc-odata-review-service
key: SERVICE_PWD
...

Conclusion

This article described how to add a custom event in SAP Commerce Cloud to SAP Cloud Platform Extension Factory. This will allow you to go beyond the out-of-the-box events and enable even more business functions and use cases using the flexibility of SAP Cloud Platform Extension Factory.