CX Works

CX Works brings the most relevant leading practices to you.
It is a single portal of curated, field-tested and SAP-verified expertise for SAP Customer Experience solutions

Custom SAP Commerce Cloud Events for SAP Business Technology Platform, Kyma Runtime

14 min read

Extend your use case with custom events

SAP Business Technology Runtime, Kyma Runtime (SAP BTP, Kyma runtime) 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 BTP, Kyma runtime. A custom SAP Commerce Cloud extension is created to expose a custom event in SAP Commerce Cloud to SAP BTP, Kyma runtime.

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.

Note: The following code and configuration is valid as of SAP Commerce v2011 and SAP BTP, Kyma runtime v 1.17

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 BTP, Kyma runtime 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 BTP, Kyma runtime 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 Business Technology Platfrom, Kyma runtime Setup

  1. Connect SAP Commerce Cloud with SAP BTP, Kyma runtime. 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 Serverless Function.  See Kyma Serverless Documentation.

    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


Conclusion

This article described how to add a custom event in SAP Commerce Cloud to SAP Business Technology Platform, Kyma runtime. 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 Business Technology Platform, Kyma runtime

Overlay