Prior to the release of Developer Extensibility (Embedded Steampunk) integration scenario developers working with S/4HANA Cloud public edition were restricted to the use of released APIs.

A recent blog post and tutorial published by Dennis Leslie demonstrates how it’s now quite straightforward for customers to build their own Custom APIs. In this blog post I will share some experience from implementing this concept on a recent project. If you have not already, I strongly recommend following the tutorial on creating Custom HTTP Services. It covers some of the basic prerequisites involved in enable inbound communication to custom APIs that will not be covered in this blog. The example case we will explore will also require creation of a custom RAP business object. If you are not already familiar I would strongly recommend the tutorial on Creating Custom RAP Business Objects

Understanding the Use Case

We currently have a wide variety of released APIs, detailed on the API Hub, that can be used to read and manipulate many standard business objects from an external system. However, in some cases the options available may not meet our requirements and some extension may be required. Let’s take an abstract example using the available API for sales orders. This API provides a range of functionality for manipulating sales order data. Let’s imagine we, as the S/4HANA Cloud system owners, want to open up a way for our customers to manipulate the sales order using this API but we want to apply some custom logic to ensure they can only update the quantity of their sales orders. On top of this, we are required to cross reference the customer against a custom business object where we can maintain tolerance limits for quantity changes on a per transaction basis.

With those factors in mind, the process flow could look something like this:

Sales%20Order%20Update%202%20Step%20Process

Sales Order Update 2 Step Process

This process could work, but has some major drawbacks.

  • The middleware would be required to interpret the results of the response from the custom business object ZSales_Order_Tolerance and determine if the request is valid forcing us to build some logic in the middleware.
  • There are 2 API calls in the process flow so which may negatively impact performance in the middleware.

This design might be completely acceptable, however the drawbacks are evident. In many cases it makes sense to limit custom logic in the middleware as much as possible and like to push as much processing logic as possible into S/4HANA Cloud to minimize the number of requests made. Using a custom API your process could look a little more like the below.

Sales%20Order%20Update%20Using%20Custom%20API

Sales Order Update Using Custom API

By moving the read of the custom business object and the update to the sales order into a custom API we can now limit the process to a single inbound request and handle all of the processing inside the custom logic. This significantly streamlines the process and simplifies the maintenance of the custom solution. In theory, we could eliminate the middleware from the solution; however, for security and monitoring purposes it often makes sense to keep it in place. Let’s now explore how this design can be built.

Building the Solution

Overview

Before we dive into building the solution, let us outline at a high level the required steps we need to complete to fully realize the custom solution.

  1. Create the custom RAP Business Object for holding tolerance values
  2. Create an HTTP service in ADT
  3. Create an inbound communication scenario and add the HTTP service
  4. Create a communication arrangement
  5. Create a communication system
  6. Create a communication user and assign it to the created communication system
  7. Implemented request and response handling in the handler class
  8. Implement the custom logic in the handler class

Creating the RAP Business Object

Here we are covering step 1. For this we need to define a table structure in ABAP Development Tools (ADT). In the example scenario we simply want to maintain a Business Partner key field and a configurable update tolerance value. The structure below meets these requirements.

@EndUserText.label : 'Sales Order Tolerance'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zsales_order_tol {

  key client            : abap.clnt;
  key customer          : idtw_kunnr;
  @Semantics.quantity.unitOfMeasure : 'zsales_order_tol.unit_field'
  change_tolerance      : abap.quan(13,2);
  unit_field            : abap.unit(2);
  local_created_by      : abp_creation_user;
  local_created_at      : abp_creation_tstmpl;
  local_last_changed_by : abp_locinst_lastchange_user;
  local_last_changed_at : abp_locinst_lastchange_tstmpl;
  last_changed_at       : abp_lastchange_tstmpl;

}

With this table created we can now generate the required repository objects following the same steps as defined in the tutorial on Creating Custom RAP Business Objects. Once completed we have a UI service available where we can maintain some data relevant to a test customer.

Custom%20Business%20Object%20UI%20Service%20Preview

Custom Business Object UI Service Preview

This completes step 1 of the process.

Creating the Custom HTTP Service

The custom API takes the form of a custom class that implements the handle request method of interface IF_HTTP_SERVICE_EXTENSION. Following the steps outlined in the tutorial on creating  custom APIs, we can create a handler class with an empty implementation of HTTP handle requests interface that will be executed upon receiving external requests and complete all necessary configuration steps to enable external requests to the custom API. This tutorial covers steps 2-6 of the process outlined and will leave us with an empty handler class like the below (you can skip the add method ‘get_html’ of the tutorial).

class ZCL_DEMO_SO_UPDATE definition
  public
  create public .

public section.

  interfaces IF_HTTP_SERVICE_EXTENSION .

protected section.
private section.

ENDCLASS.



CLASS ZCL_DEMO_SO_UPDATE IMPLEMENTATION.


  method IF_HTTP_SERVICE_EXTENSION~HANDLE_REQUEST.

 
  endmethod.
ENDCLASS.

 

 

Request and Response Handling

Moving on to step 7, with the handler class in place, we now need to define a data structure that we expect the request payload to match. The XCO library provides a very useful JSON deserialization tool. We can reuse this with the input defined structure to map a JSON payload received to ABAP. In this example we designed the method to handle multiple sales order items. It is very important that the ABAP structure nesting matches the JSON payload exactly, otherwise you will face errors in parsing the request (note the nested items table of type ty_demo_req in type definition ty_inputs, this will be clearer when the example payload is shown later).

 TYPES:
      BEGIN OF ty_demo_req,
        sales_order(10)       TYPE c,
        sales_order_item(6)    TYPE c,
        requested_quantity(13)    TYPE p DECIMALS 2,
      END OF ty_demo_req,

      BEGIN OF ty_demo_response,
        sales_order(10)       TYPE c,
        sales_order_item(6)    TYPE c,
        previous_quantity(13)   TYPE p DECIMALS 2,
        updated_quantity(13) TYPE p DECIMALS 2,
      END OF ty_demo_response.

    TYPES:
      BEGIN OF ty_inputs,
        customer_number  TYPE idtw_kunnr,
        items TYPE STANDARD TABLE OF ty_demo_req WITH EMPTY KEY,
      END OF ty_inputs,

      BEGIN OF ty_output,
        items TYPE STANDARD TABLE OF ty_demo_response WITH EMPTY KEY,
      END OF ty_output.

In the private section we declare some class variables and constants we will need during execution.

DATA: lt_req       TYPE ty_inputs,
      lv_error(1)     TYPE c,
      lv_text         TYPE string,
      ls_response       TYPE ty_demo_response,
      es_response       TYPE ty_output,
      lc_header_content TYPE string VALUE 'content-type',
      lc_content_type   TYPE string VALUE 'text/json'.

Then at the very start of the implementation of the handle request interface we retrieve the payload and deserialize it into the ABAP structure lt_req

  DATA(lv_req_body) = request->get_text( ).

"first deserialize the request
    xco_cp_json=>data->from_string( lv_req_body )->apply( VALUE #(
        ( xco_cp_json=>transformation->pascal_case_to_underscore )
      ) )->write_to( REF #( lt_req ) ).

We will also need a way to send a response to the requestor. Fortunately, XCO allows us to parse ABAP to JSON and populate the Body of the handle request interface’s response parameter. Some systems might also require a response status code and header fields detailing the content, so we include those in the successful response case.

      response->set_status('200').

       DATA(lv_json_string) = xco_cp_json=>data->from_abap( es_response )->apply( VALUE #(
    ( xco_cp_json=>transformation->underscore_to_pascal_case )
  ) )->to_string( ).
  response->set_text( lv_json_string ).
        response->set_header_field( i_name = lc_header_content
       i_value = lc_content_type ).

Implementing the Custom Logic

Now that we have a framework in place to communicate with the sending system, we can move to step 8 and focus on building the custom logic. The first step in the process flow was to check the tolerance assigned to the customer making the request in the custom business object. The RAP generator created a CDS data definition ZI_SALES_ORDER_TOL. We can read the tolerance value directly from this and the conditionally proceed or terminate processing.

"check the customer number supplied against our custom business object
SELECT SINGLE ChangeTolerance, UnitField FROM zi_sales_order_tol WHERE Customer = @lt_req-customer_number INTO @DATA(ls_tolerance).

Now that we have retrieved the tolerance values from the custom business object we can loop through the input payload, retrieve the existing quantity from a publicly released CDS view for sales order items (I_SALESORDERITEM), perform a comparison against the tolerance limit, and then use the released behavior definition for sales orders (I_SALESORDERTP) to modify the sales order item entity. There is excellent help documentation available on the released behavior definitions from within ADT that includes sample entity manipulation logic, which you can use as a template.

     LOOP AT lt_req-items INTO DATA(ls_req).

        "retrieve the current quantity for comparison with our tolerance
        SELECT SINGLE requestedquantity FROM i_salesorderitem WITH PRIVILEGED ACCESS WHERE
        salesorder = @ls_req-sales_order AND
        salesorderitem = @ls_req-sales_order_item
        INTO @ls_response-previous_quantity.

        "check for tolerance violations at item level and terminate entire process if any violations occur

        IF abs( ls_req-requested_quantity - ls_response-previous_quantity ) > ls_tolerance-ChangeTolerance.
          lv_error = 'X'.
          lv_text = 'Tolerance Exceeded' .
          EXIT.
        ENDIF.

        "Modify the quantity
        MODIFY ENTITIES OF i_salesordertp PRIVILEGED
    ENTITY salesorderitem
      UPDATE
        SET FIELDS
         WITH VALUE #( ( requestedquantity          = ls_req-requested_quantity
                        %key-salesorder            = ls_req-sales_order
                        %key-salesorderitem        = ls_req-sales_order_item ) )
    FAILED   DATA(ls_failed)
    REPORTED DATA(ls_reported).

        IF ls_failed IS NOT INITIAL.
          lv_error = 'X'.
          lv_text = 'Error during update'.
          EXIT.
        ENDIF.

        "update our response structure

        ls_response-sales_order = ls_req-sales_order.
        ls_response-sales_order_item = ls_req-sales_order_item.
        ls_response-updated_quantity = ls_req-requested_quantity.

        APPEND ls_response TO es_response-items.
        CLEAR ls_response.



      ENDLOOP.

The core loop and conditions for tolerance violation are now in place. All that remains is some simple error handling before we commit the changes made to the sales order item entity, ensuring no errors have occurred up to this point.

      "commit our changes if no errors
      COMMIT ENTITIES BEGIN
    RESPONSE OF i_salesordertp
      FAILED   DATA(ls_save_failed)
      REPORTED DATA(ls_save_reported).
      COMMIT ENTITIES END.

Putting all of this together we have a simple method that can compare the input parameters against data in a custom business object; based on the result it will then update a standard business object and return a response to the sending system. Let’s now test this from Postman using the following payload. Note how payload is structured to match the input data type ty_inputs. The “items” table in the payload is reflected here by a JSON array.

{
    "CustomerNumber": "0010100001",
    "items": [
        {
            "SalesOrder": "0000000011",
            "SalesOrderItem": "000010",
            "RequestedQuantity": "15.00"
        },
        {
            "SalesOrder": "0000000011",
            "SalesOrderItem": "000020",
            "RequestedQuantity": "5.00"
        }
    ]
}

This returns the below response payload, indicating the updates for both line items were successful.

{
    "Items": [
        {
            "SalesOrder": "0000000011",
            "SalesOrderItem": "000010",
            "PreviousQuantity": 10.0,
            "UpdatedQuantity": 15.0
        },
        {
            "SalesOrder": "0000000011",
            "SalesOrderItem": "000020",
            "PreviousQuantity": 1.0,
            "UpdatedQuantity": 5.0
        }
    ]
}

Conclusion

Building on the well-documented core concepts of developer extensibility in S/4HANA Cloud, public edition, we could quite significantly simplify a previously complex integration scenario involving multiple API calls and some processing logic in middleware into a single custom API that is more far more easily maintainable. I hope this simple, and somewhat abstract, example, can be taken as a template for building your own custom APIs.

Appendix

Below is the custom API logic in its entirety. Please feel free to take, re-use, and improve upon it for your own requirements!

CLASS zcl_demo_so_update DEFINITION
  PUBLIC
  CREATE PUBLIC .

  PUBLIC SECTION.

    INTERFACES if_http_service_extension .

    TYPES:
      BEGIN OF ty_demo_req,
        sales_order(10)        TYPE c,
        sales_order_item(6)    TYPE c,
        requested_quantity(13) TYPE p DECIMALS 2,
      END OF ty_demo_req,

      BEGIN OF ty_demo_response,
        sales_order(10)       TYPE c,
        sales_order_item(6)   TYPE c,
        previous_quantity(13) TYPE p DECIMALS 2,
        updated_quantity(13)  TYPE p DECIMALS 2,
      END OF ty_demo_response.

    TYPES:
      BEGIN OF ty_inputs,
        customer_number TYPE idtw_kunnr,
        items           TYPE STANDARD TABLE OF ty_demo_req WITH EMPTY KEY,
      END OF ty_inputs,

      BEGIN OF ty_output,
        items TYPE STANDARD TABLE OF ty_demo_response WITH EMPTY KEY,
      END OF ty_output.

  PROTECTED SECTION.
  PRIVATE SECTION.

    DATA: lt_req            TYPE ty_inputs,
          lv_error(1)       TYPE c,
          lv_text           TYPE string,
          ls_response       TYPE ty_demo_response,
          es_response       TYPE ty_output,
          lc_header_content TYPE string VALUE 'content-type',
          lc_content_type   TYPE string VALUE 'text/json'.
ENDCLASS.



CLASS zcl_demo_so_update IMPLEMENTATION.


  METHOD if_http_service_extension~handle_request.

    DATA(lv_req_body) = request->get_text( ).

    "first deserialize the request
    xco_cp_json=>data->from_string( lv_req_body )->apply( VALUE #(
        ( xco_cp_json=>transformation->pascal_case_to_underscore )
      ) )->write_to( REF #( lt_req ) ).

    "check the customer number supplied against our custom business object
    SELECT SINGLE ChangeTolerance, UnitField FROM zi_sales_order_tol WHERE Customer = @lt_req-customer_number INTO @DATA(ls_tolerance).

    IF ls_tolerance IS NOT INITIAL.

      LOOP AT lt_req-items INTO DATA(ls_req).

        "retrieve the current quantity for comparison with our tolerance
        SELECT SINGLE requestedquantity FROM i_salesorderitem WITH PRIVILEGED ACCESS WHERE
        salesorder = @ls_req-sales_order AND
        salesorderitem = @ls_req-sales_order_item
        INTO @ls_response-previous_quantity.

        "check for tolerance violations at item level and terminate entire process if any violations occur

        IF abs( ls_req-requested_quantity - ls_response-previous_quantity ) > ls_tolerance-ChangeTolerance.
          lv_error = 'X'.
          lv_text = 'Tolerance Exceeded' .
          EXIT.
        ENDIF.

        "Modify the quantity
        MODIFY ENTITIES OF i_salesordertp PRIVILEGED
    ENTITY salesorderitem
      UPDATE
        SET FIELDS
         WITH VALUE #( ( requestedquantity          = ls_req-requested_quantity
                        %key-salesorder            = ls_req-sales_order
                        %key-salesorderitem        = ls_req-sales_order_item ) )
    FAILED   DATA(ls_failed)
    REPORTED DATA(ls_reported).

        IF ls_failed IS NOT INITIAL.
          lv_error = 'X'.
          lv_text = 'Error during update'.
          EXIT.
        ENDIF.

        "update our response structure

        ls_response-sales_order = ls_req-sales_order.
        ls_response-sales_order_item = ls_req-sales_order_item.
        ls_response-updated_quantity = ls_req-requested_quantity.

        APPEND ls_response TO es_response-items.
        CLEAR ls_response.



      ENDLOOP.

    ELSE.

      lv_error = 'X'.
      lv_text = 'Customer Not Maintained in Tolerance Table'.

    ENDIF.

    IF lv_error IS NOT INITIAL.
      "propagate any errors raised
      response->set_status('500').
      response->set_text( lv_text ).

    ELSE.

      "commit our changes if no errors
      COMMIT ENTITIES BEGIN
    RESPONSE OF i_salesordertp
      FAILED   DATA(ls_save_failed)
      REPORTED DATA(ls_save_reported).
      COMMIT ENTITIES END.

      "propagate any errors during save
      IF ls_save_failed IS NOT INITIAL.
        response->set_status('500').
        response->set_text( 'Error during save' ).

      ELSE.

        "respond with success payload
        response->set_status('200').

        DATA(lv_json_string) = xco_cp_json=>data->from_abap( es_response )->apply( VALUE #(
     ( xco_cp_json=>transformation->underscore_to_pascal_case )
   ) )->to_string( ).
        response->set_text( lv_json_string ).
        response->set_header_field( i_name = lc_header_content
       i_value = lc_content_type ).

      ENDIF.
    ENDIF.
  ENDMETHOD.
ENDCLASS.
Sara Sampaio

Sara Sampaio

Author Since: March 10, 2022

0 0 votes
Article Rating
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x