Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
Marcel_Wahl
Advisor
Advisor

🔥 Updates


2023-08-08
Thank you colleagues for the feedback. As per our discussion we could simplify this guide even more by using a managed RAP implementation with an "unmanaged save". The solution does now no  longer require a custom workspace buffer but is using the RAP default implementation.

Introduction


During the last months we often received the question, how to wrap SAP S/4HANA BAPIs for use in side-by-side scenarios, for example, from SAP Cloud Application Programming Model (CAP).

There are multiple ways to achieve that on a purely technical level basically enabling the module to be called over HTTPS. But the overwhelming interface only lead to meeting after meeting clarifying the purpose of fields of which 90% are not needed for the business use case.

To avoid that we came up with the approach to model facades in ABAP RESTful Programming Model (RAP) that reduce the surface of the API to the minimum and split by the need-to-know principle. The functional experts in the SAP S/4HANA business system could easily tell what input is expect from the end-user / consumer. Anything else was hidden in the RAP facade.

This is an end-to-end "how-to" guide with code snippets focusing on the main features needed to achieve this.

  1. Modelling + implementing RAP

  2. Calling the BAPI

  3. Error handling

  4. Testing in ABAP + POSTMAN

  5. Remarks on transaction handling


Credits


This blog is based on "Using BAPIs in RAP" and was written with support of marcel.hermanns and renzo.colle. Thanks a lot for for the insights.

Sample Use Case


This is a real use case of an incentive / bonus payment for managers used in the performance review cycles.

The managers only want to specify the amount and employee it is for.

The finance experts in the backend know that they have to create a finance posting with debit on corporate benefits money pool and credit to an employee payout account.

Solution Overview



Solution Overview



Entity Model


The bonus payment is modelled as simple as possible on top of the SAP standard accounting document.

The item structure of an accounting document is hidden by using the a unique main item projection.

 
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'RAP Facade: Bonus Payment main item'
@Metadata.ignorePropagatedAnnotations: true
define view entity ZDemo_BonusPaymentItem
as select from I_OperationalAcctgDocItem
association [1] to I_OperationalAcctgDocItem as _AcctDocItem on _AcctDocItem.CompanyCode = $projection.CompanyCode
and _AcctDocItem.FiscalYear = $projection.FiscalYear
and _AcctDocItem.AccountingDocument = $projection.AccountingDocument
and _AcctDocItem.AccountingDocumentItem = $projection.MainItem
{
key CompanyCode, //Access control
key FiscalYear,
key AccountingDocument,
min( AccountingDocumentItem ) as MainItem,

_AcctDocItem
}
// Assumption:
// every bonus payment has exactly 1 debit item with the total bonus amount
// there may be multiple credit items
where
DebitCreditCode = 'S' //Debit line
group by
CompanyCode,
FiscalYear,
AccountingDocument

 

The main entity for the WebAPI is a simplified projection on the accounting document mixed with the figures of the main item.

 
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'RAP Facade: Bonus Payment'
define root view entity ZDemo_BonusPayment
as select from I_AccountingDocument
//Main item
association [0..1] to ZDemo_BonusPaymentItem as _MainItem on _MainItem.CompanyCode = $projection.CompanyCode
and _MainItem.FiscalYear = $projection.FiscalYear
and _MainItem.AccountingDocument = $projection.AccountingDocument
{
key CompanyCode, // Access control
key FiscalYear,
key AccountingDocument,

//main attributes
DocumentDate,
PostingDate,

// main item figures - currency relationship inherited
_MainItem._AcctDocItem.CompanyCodeCurrency as Currency,
_MainItem._AcctDocItem.AmountInCompanyCodeCurrency as Amount,

//Administrative data
CreationTime as CreatedAt,
AccountingDocumentCreationDate as CreatedOn,
AccountingDocCreatedByUser as CreatedBy,

//Document reference used for employee field
DocumentReferenceID as BonusRecipient
}
where
AccountingDocumentType = 'WA'
and IsReversed != 'X'

 

RAP Facade


Creating a behavior definition


To enable creation of new bonus payouts we are defining a behavior definition for this entity with minimal information.

We will use a "managed" implementation with "unmanaged save" to get the transaction workspace handling for free.

As with all BAPIs, the final key will only be available after the real posting run.
Hence "late numbering" must be activated.

 
managed implementation in class zbp_demo_bonuspayment unique;
strict ( 2 );

define behavior for ZDemo_BonusPayment alias BonusPayment
with unmanaged save
late numbering // <<<< must have when working with the BAPI
lock master
authorization master ( instance ) // based on company code field
etag master CreatedAt
{
field ( readonly ) AccountingDocument, FiscalYear; //Key fields filled by API
field ( readonly : update ) CompanyCode; //Key fields provided from external
field ( readonly ) CreatedAt, CreatedBy, CreatedOn; //Admin fields

create;
delete; //Cancellation - Not implemented yet
}

 

Notes:

  1. We do not need draft capabilities because WebAPIs do synchronous calls and dont need it.

  2. We do not require a behavior definition for the item view since it serves only as a filter.

  3. Method "Delete" is included for demo purposes but not fully implemented


Overview of behavior runtime


A RAP facade for a BAPI requires only the minimal methods for writing in the saver class:

  1. Filling the BAPI call by merging requested data with hidden internal control fields
    e.g. document type

  2. Calling the BAPI and handling all errors

  3. Returning the final keys


Save sequence using a BAPI


The BAPI call is part of the save sequence going through the following methods:

  1. "Adjust_Numbers"

    • The BAPI must be called here, since later you can no longer handle errors or return the keys



  2. "Save_Modified"

    • This method is not required because the BAPI has already written the data. It can be left empty.




Important:



  1. To handle the BAPI errors, you must change the inheritance of your local saver class definition from "cl_abap_behavior_saver" to "cl_abap_behavior_saver_failed".
    This will enable the additional parameter "failed" in method "adjust_numbers".See RAP Saver Classes and Methods in ABAP Keyword Documentation for more details

  2. BAPIs often use "CALL FUNCTION ... IN UPDATE TASK" which is forbidden during RAP runtime except in "adjust numbers" and the save methods.


Internal control fields and defaults


BAPIs require a set of control parameters such as document types, company code context and so on.

The RAP Facade requires access to these values which can be implemented in various ways:

  1. BRF+ function returning a structure with all fields

  2. Configuration class reading from a configuration table

  3. Member structure with hard coded defaults --- FOR DEMO PURPOSES ONLY !


For the sake of a copy + paste demo, we defined our configuration in the saver class private section:

 
  PRIVATE SECTION.
DATA:
"! <p class="shorttext synchronized" lang="en">Configuration</p>
"! <p>hard coded just for demo purposes only </p>
"! <p>Use a configuration table or BRF+ rule to fill
"! the values in a real use case </p>
BEGIN OF ms_config,
company_code TYPE bapiache09-comp_code VALUE '1010',
document_type TYPE bapiache09-doc_type VALUE 'WA',
BEGIN OF debit,
transaction TYPE bapiacgl09-acct_key VALUE 'BSX',
glaccount TYPE bapiacgl09-gl_account VALUE '0054070000',
END OF debit,
BEGIN OF credit,
transaction TYPE bapiacgl09-acct_key VALUE 'GBB',
glaccount TYPE bapiacgl09-gl_account VALUE '0013600000',
END OF credit,
END OF ms_config ##no_text.

 

Filling BAPI Requests


To keep the code as easy as possible, it is a good a practice to have all BAPI parameters in a request structure type and build a request table based on it.

 
    TYPES:
"! <p class="shorttext synchronized" lang="en">Accounting document request</p>
BEGIN OF ty_s_bapi_request,
pid TYPE abp_behv_pid,
header TYPE bapiache09,
amounts TYPE STANDARD TABLE OF bapiaccr09 WITH DEFAULT KEY,
accounts TYPE STANDARD TABLE OF bapiacgl09 WITH DEFAULT KEY,
END OF ty_s_bapi_request.

 

The methods "fill_header" and "fill_amounts" follow the same pattern:

  1. Map fields from RAP structure to BAPI structure

  2. Fill control fields from configuration


Example for header:

 
  METHOD fill_header.
"basics
CLEAR es_header.
es_header-comp_code = is_bonus-CompanyCode.
es_header-fisc_year = is_bonus-FiscalYear.
"dates
es_header-doc_date = is_bonus-DocumentDate.
es_header-pstng_date = is_bonus-PostingDate.
"document references
es_header-ref_doc_no = is_bonus-BonusRecipient.
"determination of internal attributes
det_header_defaults( EXPORTING is_bonus = is_bonus
CHANGING cs_header = es_header
cs_reported = cs_reported ).
ENDMETHOD.

 

The example code for control field determination can be found in the appendix below.

Optional: determinations and validations before save


You can use RAP determinations and validations to do any custom logic before saving the data record, for example, filling field defaults that havent been provided by the caller.

Avoid coding any validation here that is anyway covered by ther BAPI logic.

BAPI call and final keys


For readable code during BAPI processing a result type and table can look like this:

 
    "there is no re-usable structure to split this key
TYPES:
BEGIN OF ty_s_acc_doc_key,
belnr TYPE belnr_d,
bukrs TYPE bukrs,
gjahr TYPE gjahr,
END OF ty_s_acc_doc_key.
TYPES:
"! <p class="shorttext synchronized" lang="en">Accounting document result</p>
BEGIN OF ty_s_bapi_result,
pid TYPE abp_behv_pid,
temp_key TYPE ty_s_acc_doc_key,
key TYPE awkey,
msg TYPE STANDARD TABLE OF bapiret2 WITH DEFAULT KEY,
END OF ty_s_bapi_result.

 

BAPI main processing is then implemented in method "adjust_numbers":

 
METHOD adjust_numbers.

DATA lt_bapi_res TYPE SORTED TABLE OF ty_s_bapi_result WITH UNIQUE KEY pid.
DATA lt_bapi_req TYPE SORTED TABLE OF ty_s_bapi_request WITH UNIQUE KEY pid.

"get all records from buffer
READ ENTITY IN LOCAL MODE ZDemo_BonusPayment\\BonusPayment
ALL FIELDS WITH VALUE #( FOR ls_Payment IN mapped-bonuspayment ( %pky = ls_payment-%pre ) )
RESULT DATA(lt_payments).

"prepare the create requests
LOOP AT mapped-bonuspayment ASSIGNING FIELD-SYMBOL(<ls_bonus_key>).
TRY.
"get the record from transaction buffer for this key
DATA(lr_bonus) = REF #( lt_payments[ KEY pid COMPONENTS %pid = <ls_bonus_key>-%pid
%key = <ls_bonus_key>-%tmp ] ).
"new BAPI request
DATA(ls_bapi_request) = VALUE ty_s_bapi_request( pid = lr_bonus->%pid ).

"fill header
fill_header(
EXPORTING is_bonus = lr_bonus->*
IMPORTING es_header = ls_bapi_request-header ).

"fill amounts and account information
"Every payment is a combination of a debit and credit item
fill_amounts(
EXPORTING is_bonus_payment = lr_bonus->%data
is_header = ls_bapi_request-header
CHANGING ct_amounts = ls_bapi_request-currency_amount
ct_accounts = ls_bapi_request-accountgl ).

"call the BAPI
DATA(ls_bapi_result) = VALUE ty_s_bapi_result( pid = ls_bapi_request-pid ).
CALL FUNCTION 'BAPI_ACC_DOCUMENT_POST'
EXPORTING
documentheader = ls_bapi_request-header
IMPORTING
obj_key = ls_bapi_result-acct_doc_key
TABLES
accountgl = ls_bapi_request-accountgl
currencyamount = ls_bapi_request-currency_amount
return = ls_bapi_result-msg.

"handle errors
handle_bapi_result( EXPORTING is_result = ls_bapi_result
CHANGING cs_reported = reported ).

"split key string
DATA(ls_acc_doc_key) = CONV ty_s_acc_doc_key( ls_bapi_result-acct_doc_key ).
<ls_bonus_key>-companycode = ls_acc_doc_key-bukrs.
<ls_bonus_key>-fiscalyear = ls_acc_doc_key-gjahr.
<ls_bonus_key>-accountingdocument = ls_acc_doc_key-belnr.

CATCH zcx_demo_rap_facade_web_api INTO DATA(lx_bapi_error).
"log error reason
APPEND VALUE #( %pky = <ls_bonus_key>-%pre
%msg = lx_bapi_error ) TO reported-bonuspayment.
"BAPI call or preparation failed
INSERT VALUE #( %pky = <ls_bonus_key>-%pre
%create = if_abap_behv=>mk-on
) INTO TABLE failed-bonuspayment.
ENDTRY.
ENDLOOP.
ENDMETHOD.

 

 

OData V4 Web API


The root entity view is exposed as OData V4 service using a new service definition:

 
@EndUserText.label: 'RAP facade demo: bonus payment'
define service ZDEMO_BONUS_PAYMENT {
expose ZDemo_BonusPayment as BonusPayments;
}

 

Service Binding is created using template OData V4 - WebAPI:

 


Service Binding


 

Testing with Postman


For the real test you can setup a Postman collection with the following requests:

  1. GET - Read the entity set


  2. POST - to Create a new payment

    • URL:  same as for above

    • Header parameter to set the CSRF Token from read
      "x-csrf-token = ...."

    • Payload raw, please choose type JSON
      {
      "Currency": "EUR",
      "Amount": 100.00,
      "BonusRecipient": "Jan Sample"
      }​




  3. Sample response JSON:

    • The response should look like this
      {
      "@odata.context": "$metadata#BonusPayments/$entity",
      "@odata.metadataEtag": "W/\"20230731082241\"",
      "@odata.etag": "W/\"SADL-303832333039~082309\"",
      "CompanyCode": "1010",
      "FiscalYear": "2023",
      "AccountingDocument": "4900010896",
      "DocumentDate": "2023-07-31",
      "PostingDate": "2023-07-31",
      "Currency": "EUR",
      "Amount": 100,
      "CreatedAt": "08:23:09",
      "CreatedOn": "2023-07-31",
      "CreatedBy": "HIDDEN",
      "BonusRecipient": "Jan Sample",
      "SAP__Messages": []
      }





 

ABAP test runner


For a quick test, create an executable class with interface "if_oo_adt_classrun".

 
"! <p class="shorttext synchronized" lang="en">RAP Facade: Bonus payment test run</p>
CLASS zcl_demo_rap_facade_test DEFINITION PUBLIC FINAL CREATE PUBLIC .
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun .
ENDCLASS.

 

The main method can run the end-to-end test with minimal data:

 
  METHOD if_oo_adt_classrun~main.

TYPES ty_s_bonus_data TYPE STRUCTURE FOR CREATE zdemo_bonuspayment\\BonusPayment.

"fill minimal data
DATA(ls_bonus) = VALUE ty_s_bonus_data(
%cid = 'CREATE_MINIMAL_001'
CompanyCode = '1010'
PostingDate = sy-datum "optional
amount = 500
Currency = 'EUR'
%control = VALUE #(
CompanyCode = if_abap_behv=>mk-on
PostingDate = if_abap_behv=>mk-on
amount = if_abap_behv=>mk-on
Currency = if_abap_behv=>mk-on
)
) ##no_text.

"run
MODIFY ENTITY zdemo_bonuspayment\\BonusPayment
CREATE FROM VALUE #( ( ls_bonus ) )
MAPPED DATA(ls_create_mapped)
FAILED DATA(ls_create_failed)
REPORTED DATA(ls_create_reported).

"check for problems
IF ls_create_failed IS NOT INITIAL.
LOOP AT ls_create_reported-bonuspayment ASSIGNING FIELD-SYMBOL(<ls_create_msg>).
out->write( <ls_create_msg>-%msg->if_message~get_text( ) ).
ENDLOOP.
out->write( 'Creation failed'(e01) ).
RETURN.
ENDIF.

"save
COMMIT ENTITIES BEGIN
RESPONSE OF zdemo_bonuspayment
FAILED DATA(ls_save_failed)
REPORTED DATA(ls_save_reported).

"check for problems
IF ls_save_failed IS NOT INITIAL.
LOOP AT ls_save_reported-bonuspayment ASSIGNING FIELD-SYMBOL(<ls_save_msg>).
out->write( <ls_save_msg>-%msg->if_message~get_text( ) ).
ENDLOOP.
out->write( 'Saving failed'(e01) ).
RETURN.
ENDIF.

LOOP AT ls_create_mapped-bonuspayment ASSIGNING FIELD-SYMBOL(<ls_temp_key>).
"get the final keys from late numbering
CONVERT KEY OF zdemo_bonuspayment\\BonusPayment
FROM TEMPORARY VALUE #( %pid = <ls_temp_key>-%pid
%tmp = <ls_temp_key>-%key ) TO DATA(ls_acc_doc_key).
"check we have a new document key
IF ls_acc_doc_key-AccountingDocument IS INITIAL.
out->write( 'Saving did not generate AccountingDocument key'(e02) ).
RETURN.
ELSE.
out->write( |Generated Accounting Document key { ls_acc_doc_key-AccountingDocument }| ).
ENDIF.
ENDLOOP.

COMMIT ENTITIES END.

"check for problems
IF ls_save_failed IS NOT INITIAL.
out->write( 'Saving failed'(e03) ).
RETURN.
ENDIF.

"re-read new record from DB
READ ENTITY zdemo_bonuspayment\\BonusPayment
FROM VALUE #( ( CORRESPONDING #( ls_acc_doc_key ) ) )
RESULT DATA(lt_new_payment_data).

"check for problems
IF lt_new_payment_data IS INITIAL.
out->write( 'Re-read accounting document failed'(e04) ).
ENDIF.

ENDMETHOD.

 

Transaction handling


Database commits


Use of statements causing database commits like "Commit", "Call function destination" or "wait" must not be used inside the RAP processing. It can lead to data inconsistencies and will be prevented by RAP runtime checks in most cases.

Mass processing


When enabling "draft", the draft persistency can be used as staging tables for mass processing.
This approach has been successfully used for initial load of millions of records in multiple projects and scales pretty well, for example, if used together with SAP Application Interface Framework (AIF).

Use of “Call function destination ‘NONE'”


It is common practice to use “Call Function Destination ‘NONE'” in mass processing and AIF interfaces for better control in error situations and memory management.

This concept must not be used inside RAP saver implementations but only as part of the load balancing outside the RAP entity processing.

 

Appendix


The following are support methods and objects just for you information.

Access control inheriting from accounting document


 
@EndUserText.label: 'Demo payment: read access control'
@MappingRole: true
//Inheriting read access from accouting document by company code
define role ZDEMO_BONUSPAYMENT {
grant select on ZDemo_BonusPayment
where inheriting conditions from entity I_AccountingDocument;

grant select on ZDemo_BonusPaymentItem
where inheriting conditions from entity I_AccountingDocument;
}

 

RAP compatible exception class


For debugging it is very useful to have exception classes instead of messages since they contain the triggering source code position.

When you enable your exception class as RAP message class they will be forwarded  throughout the stack and you jump to the trigger point even from many levels above.

Note: Make sure to default the message severity in the "Constructor".
"! <p class="shorttext synchronized" lang="en">RAP demo: error messages</p>
CLASS zcx_demo_rap_facade_web_api DEFINITION
PUBLIC
INHERITING FROM cx_static_check
FINAL
CREATE PUBLIC .

PUBLIC SECTION.
INTERFACES if_abap_behv_message .
CONSTANTS:
"! <p class="shorttext synchronized" lang="en">Unexpected technical error</p>
BEGIN OF c_tech_error,
msgid TYPE symsgid VALUE 'ZDEMO_RAP_FACADE_WEB',
msgno TYPE symsgno VALUE '002',
attr1 TYPE scx_attrname VALUE '',
attr2 TYPE scx_attrname VALUE '',
attr3 TYPE scx_attrname VALUE '',
attr4 TYPE scx_attrname VALUE '',
END OF c_tech_error ##no_text.
CONSTANTS:
"! <p class="shorttext synchronized" lang="en">Document already exists</p>
BEGIN OF c_already_exists,
msgid TYPE symsgid VALUE 'ZDEMO_RAP_FACADE_WEB',
msgno TYPE symsgno VALUE '001',
attr1 TYPE scx_attrname VALUE 'MV_ACCOUNTING_DOCUMENT',
attr2 TYPE scx_attrname VALUE 'MV_COMPANY_CODE',
attr3 TYPE scx_attrname VALUE 'MV_FISCAL_YEAR',
attr4 TYPE scx_attrname VALUE '',
END OF c_already_exists ##no_text.

"! <p class="shorttext synchronized" lang="en">Company code</p>
DATA mv_company_code TYPE ZDemo_BonusPayment-CompanyCode.
"! <p class="shorttext synchronized" lang="en">Fiscal year</p>
DATA mv_Fiscal_Year TYPE ZDemo_BonusPayment-FiscalYear.
"! <p class="shorttext synchronized" lang="en">Document number</p>
DATA mv_Accounting_Document TYPE ZDemo_BonusPayment-AccountingDocument.

"! <p class="shorttext synchronized" lang="en">CONSTRUCTOR</p>
METHODS constructor
IMPORTING
textid LIKE if_t100_message=>t100key OPTIONAL
previous LIKE previous OPTIONAL
severity TYPE if_abap_behv_message=>t_severity DEFAULT if_abap_behv_message=>severity-error
mv_company_code TYPE zdemo_bonuspayment-companycode OPTIONAL
mv_fiscal_year TYPE zdemo_bonuspayment-fiscalyear OPTIONAL
mv_accounting_document TYPE zdemo_bonuspayment-accountingdocument OPTIONAL.
ENDCLASS.

CLASS zcx_demo_rap_facade_web_api IMPLEMENTATION.
METHOD constructor ##ADT_SUPPRESS_GENERATION.
super->constructor( previous = previous ).

me->mv_company_code = mv_company_code.
me->mv_fiscal_year = mv_fiscal_year.
me->mv_accounting_document = mv_accounting_document.

"RAP message severity
me->if_abap_behv_message~m_severity = severity. //<<<<< manually added!

CLEAR me->textid.
IF textid IS INITIAL.
if_t100_message~t100key = if_t100_message=>default_textid.
ELSE.
if_t100_message~t100key = textid.
ENDIF.
ENDMETHOD.
ENDCLASS.

 

Filling document header from configuration


 
METHOD det_header_defaults.
"values from configuration - hard coded just for demo purposes!
IF cs_header-comp_code IS INITIAL.
cs_header-comp_code = ms_config-company_code.
ENDIF.

cs_header-doc_type = ms_config-document_type.
cs_header-header_txt = |Bonus payout ARE { cs_header-fisc_year } for { cs_header-ref_doc_no }| ##no_text. "demo only
cs_header-username = sy-uname.

IF cs_header-doc_date IS INITIAL. "fallback
cs_header-doc_date = sy-datum.
ENDIF.

IF cs_header-pstng_date IS INITIAL. "fallback
cs_header-pstng_date = sy-datum.
ENDIF.

"determine fiscal year if not provided
IF cs_header-fisc_year IS INITIAL.
DATA(ls_msg) = VALUE bapiret1( ).
CALL FUNCTION 'BAPI_COMPANYCODE_GET_PERIOD'
EXPORTING
companycodeid = cs_header-comp_code
posting_date = cs_header-pstng_date
IMPORTING
fiscal_year = cs_header-fisc_year
return = ls_msg.
"handle errors
IF ls_msg-type CA c_msg_type_error.
RAISE EXCEPTION new_msg_from_bapi( CORRESPONDING #( ls_msg ) ).
ENDIF.
ENDIF.
ENDMETHOD.

 

Handling BAPI messages in RAP


Definition:

 
    "! <p class="shorttext synchronized" lang="en">Late messages</p>
TYPES ty_s_reported TYPE RESPONSE FOR REPORTED LATE zdemo_bonuspayment.
"! <p class="shorttext synchronized" lang="en">Late errors</p>
TYPES ty_s_failed TYPE RESPONSE FOR FAILED LATE zdemo_bonuspayment.

"! <p class="shorttext synchronized" lang="en">Handle BAPI exceptions + messages</p>
"! @parameter is_result | BAPI result
METHODS handle_bapi_result
IMPORTING
is_result TYPE bapiret2_tab
CHANGING
cs_reported TYPE ty_s_reported
cs_failed TYPE ty_s_failed.

 

Implementation:

 
  METHOD handle_bapi_result.
"process all messages
LOOP AT is_result-msg ASSIGNING FIELD-SYMBOL(<ls_bapi_msg>).
"Convert all messages
DATA(lo_behv_msg) = new_msg_from_bapi( <ls_bapi_msg> ).
"Stop on error
IF <ls_bapi_msg>-type CA c_msg_type_error.
DATA(lx_error_msg) = lo_behv_msg.
EXIT.
ELSE.
APPEND VALUE #( %pid = is_result-pid
%msg = lo_behv_msg ) TO cs_reported-bonuspayment.
ENDIF.
ENDLOOP.
"check a key was generated
IF lx_error_msg IS NOT BOUND
AND is_result-acct_doc_key IS INITIAL.
"Unexpected technical error
RAISE EXCEPTION TYPE zcx_demo_rap_facade_web_api
EXPORTING
textid = zcx_demo_rap_facade_web_api=>c_tech_error.
ENDIF.
"If there was an error, raise it
IF lx_error_msg IS BOUND.
RAISE EXCEPTION lx_error_msg.
ENDIF.
ENDMETHOD.

 

Conversion of BAPI messages to RAP messages


 

Definition:

 
    "! <p class="shorttext synchronized" lang="en">New RAP message from bapiret</p>
"! @parameter is_message | BAPIRET message
"! @parameter ro_msg | RAP behavior message
METHODS new_msg_from_bapi
IMPORTING is_message TYPE bapiret2
RETURNING VALUE(ro_msg) TYPE REF TO zcx_demo_rap_facade_web_api.

 

Implementation:

 
  METHOD new_msg_from_bapi.
CHECK is_message IS NOT INITIAL.
"determine severity
DATA(lv_msg_type) = COND #( WHEN is_message-type IS NOT INITIAL THEN is_message-type
ELSE if_abap_behv_message=>severity-error ).
"set system variables
MESSAGE ID is_message-id TYPE lv_msg_type NUMBER is_message-number
WITH is_message-message_v1 is_message-message_v2 is_message-message_v3 is_message-message_v4
INTO DATA(lv_msg).
TRY.
"raise message from system variables
RAISE EXCEPTION TYPE zcx_demo_rap_facade_web_api USING MESSAGE.
CATCH zcx_demo_rap_facade_web_api INTO ro_msg ##no_handler.
ENDTRY.
ENDMETHOD.

References


The concepts for wrapping APIs in SAP S/4HANA is based on the official guidelines:

ABAP Cloud API Enablement Guidelines for SAP S/4HANA Cloud, private edition, and SAP S/4HANA

Extend SAP S/4HANA in the cloud and on premise with ABAP based extensions
9 Comments
scottlawton
Explorer
Thanks for this blog, marcel.wahl, it's very informative. I do have a question, though. Using a RAP facade to expose a BAPI as an OData web API seems like significantly more work than using the ABAP REST Library to wrap the BAPI and expose it as a "basic" (i.e. not OData) REST service. What are the advantages of using a RAP facade? (For that matter, the RAP facade approach also seems more complicated than using the SAP Gateway, which in turn seems more complicated than the REST Library.)

Also, it seems like the RAP facade method to some degree defeats the purpose of using a BAPI in the first place. One of the benefits of calling a BAPI is that the BAPI knows all of the underlying tables that need to be updated, thereby ensuring that the developer doesn't miss something important. The RAP facade appears to require creating a CDS view to expose certain fields from the underlying table(s) to map to the BAPI input. This is slightly easier in your example because there is already a well-defined CDS view that maps to the BAPI and you just need a projection on top of it to expose certain fields, but I work in FSCD and there are precious few standard CDS views delivered. If such detailed knowledge of the underlying tables is necessary, why even use the BAPI at all? Why not just create a full RAP application that directly does the updating of the tables? Or why use a RAP facade when either the ABAP REST Library or creating the OData service through the SAP Gateway seem to be less complex?

Or is there something I'm missing that would the RAP facade less complicated than it appears?

Thanks again for your blog. Any insights you can provide related to the reasons/benefits for the RAP facade approach would be greatly appreciated!

Thanks,

Scott
Marcel_Wahl
Advisor
Advisor
0 Kudos
Hi Scott,

On your question regarding the REST Library, that is not related to RAP but purely about the quality level of your web service. OData has a lot of advantages compared to plain REST, most importantly the query syntax and the standardized metadata, which allows a very convenient integration into your consumer applications on BTP , any middleware or testing tool. ( without writing an yaml )

RAP programming also is the recommended + future proof way going forward for any API in SAP.
It brings clear structure to your code, handles buffering, locking and enforces a sophisticated end-2-end flow.

The RAP part of this solution implements only the write / create part, while the CDS is the most convenient way of implementing a powerful query without a single line of SQL code.

You get none of query features nor the RAP based API + metadata features with a plain REST service, and you will have to implement a ton of coding just to get it working.
In the RAP facade, it is exactly one method for the business logic that does exactly what you want.

On the last question: why using the BAPI at all? because it is an upgrade stable, released and well document API that is the best thing to use until there is an official new RAP based API delivered by SAP.

Overall, you can always code things yourself on a lower level, but on the long run ( maintenance, upgrades, extensions, re-use ) you will not find a more efficient way than this.

BR, Marcel

 
Kinsbrunner
Active Contributor
0 Kudos
Marcel,
First of all, thank you for the post, it is very well-written, clear and serves as a perfect example for many real-life examples.

Next, I'd like to add although you are not using the SAVE_MODIFIED, it is interesting to say that if we would be implementing the Delete(), it should be coded there.

Regarading switching the inheritance of the local saver class for having access to the FAILED table, this has been the top thing I am taking from this wonderfull post!

Does this mean that the no-go decision that was on the check_before_Save is being moved into theadjust_numbers method? My understanding (based on the tests I did) is Yes. Also, I am not seeing any further the need of raising the short dump if something fails upon adjust_numbers, do you agree?

Keep it up with this great posts!
Regards.
Alejandro.
matiasmiano1982
Explorer
Hi, it's very interesting, but I've a doubt: everytime that you uses a BAPI, you need to do a COMMIT WORK to make it work. Why aren't you using it? Does the BAPI work without doing a COMMIT by code?
Marcel_Wahl
Advisor
Advisor
0 Kudos
Hi Matias,

Valid question. It is not that obvious.
The OData adapter for RAP handles the logical unit of work including commit + rollback.

First it puts all requested data records into the RAP buffer using the RAP "create" operation.
Afterwards, the OData runtime triggers the RAP save sequence, which includes the BAPI call and ends with a commit when successful.

Inside the RAP save sequence Commit / Rollback are strictly prohibited and will lead to short dumps.

BR, Marcel
rishabh_pandey2
Explorer

Hi Marcel,

Thanks for this document its really helpful, I have one scenario where I am calling BAPI - MPLAN_CHANGE (in RAP Update method) but inside this BAPI one more BAPI - MPLAN_WRITE_DOCUMENT is getting called and due to this we are getting dump as BEHAVIOR_ILLEGAL_STATEMENT because BAPI - MPLAN_WRITE_DOCUMENT is update type and it is called with UPDATE_TASK.

How to deal with this?

Thanks,

Rishabh

Srdjan
Product and Topic Expert
Product and Topic Expert
0 Kudos

Thank you very much Marcel for this illustrative and insightful example. I have one question about

But the overwhelming interface only lead to meeting after meeting clarifying the purpose of fields of which 90% are not needed for the business use case.

Is this the key challenge of BAPI consumption, to be solved by RAP Facade? I am asking because it is put first, probably because of importance?

To my understanding, all BAPI fields are there because they are all needed and documented in BAPI documentation. No single customer needs them all but the sum of all customers do, it is why fields have been implemented.

If standard RAP BO shall be implemented exposing existing BAPIs, then the Facade again shall expose all fields, because they are all needed by all customers. Is that correct?

If custom RAP BOs are implemented and each customer implements own Facade, much more technical artefacts are created in installed base, reducing overall homogenisation and BO reusability among scenarios and customers. How it fits with clean core?

In mine past projects with direct BAPIs consumption on BTP, the performance, or too many fields were not the problem at all. The key challenge was translating business requirements to right BAPIs and finding relevant BAPI fields. The problem were not all fields that are there, but what is "my field", like employee id and incentive in this example. I don't see how RAP Facade can help me here.

I understand the need for standardisation and RAP qualities, just still not fully convinced when it comes to exposure of existing BAPI and other "heavier" logic. Perhaps I missed some point and would be glad to understand it better.

Thanks, Srdjan

 

Marcel_Wahl
Advisor
Advisor

Hi Srdjan,

The key challenge to be solved by a RAP facade is to close gaps in tier 2 of the SAP extensibility model.
Meaning to create an external interface where SAP is not yet offering something while staying as close as possible to the SAP golden path for developers.

BAPI´s have been invented to allow generic access to all fields of the big business objects like business partners or financial postings or sales orders.
Yes the question is tricky when to create a huge API with majority of fields or splitting it into smaller ones per purpose.

The answer has to come from your project environment and the customer strategy.
e.g. if the customer runs a big central finance project with very complex requirements and tough timeplan it can save effort to create a big "expose all" RAP Facade based on the virtual data model ( not the BAPI ), but it will also be complex and stay there forever as its hard to trace all the use cases.

If the customer is looking for a flexible and sophisticated micro-service architecture then creating more smaller RAP facades allows to split complexity and minimize the the number of fields needed.
You pay with extra technical objects but you win transparency, scalability and the option to replace or deprecate the custom development when something better or SAP standard coverage comes up.

Regarding clean core, the RAP facade is the recommended design approach in the SAP extensibility guidelines ( see chapter on "tier 2") .  The idea of clean core is not to purge all custom objects from the system but to encapsulate custom logics and behavior in a way that it does not hinder upgrades and keep the software solution flexible, resilient and future proof. 

That OData interfaces based on virtual data model (CDS) offer a lot more advanced query language, are multiple times faster and provide more data protection compared to old BAPIs is pretty obvious.
In a example of a public authority we increased the throughput in a migration project by 3 times for reading and more than doubled for writing ( using OData mass batch, getting rid of mappings, reducing buffer problems).
Also for example using a BAPI for value helps of a cloud application is simply not fast enough, while an OData + CDS based input help can even facilitate type ahead proposals.

Hope these examples help clarify your questions. 

Helpful links:
https://learning.sap.com/learning-journeys/practicing-clean-core-extensibility-for-sap-s-4hana-cloud... 

jacqueso
Explorer

Hi @Marcel_Wahl 

Thank you for the excellent blog.  However the one example I have yet seen anywhere and that I would really like to see, which is a real life use case that is required, is having the ability to post both Header and Items together at the same time.

Keeping with your example of using the BAPI: BAPI_ACC_DOCUMENT_POST, how would you go about building the ODATA to be able to post both Header and Item fields at the same time to the BAPI.

For Example to post something like:

{

CompanyCode,

FiscalYear,

FiscalPeriod, 

ITEMS: [

{

Item,

Currency,

Amount,

GLAccount

},

{

Item,

Currency,

Amount,

GLAccount

}

]

}