Process OData $batch errors in SAP Cloud Integration

Starting from Marty McCormick’s blog , I designed a method of extracting error messages from the OData batch call responses and create attachments for local error handling.

This method is especially useful when you use an API that does not really provide much information in the error response text and you cannot identify the line that failed. I will explain in the following chapter the scenario I used in order to develop iFlow.

Let’s assume a 3rd party system sends the following payload to SAP Cloud Integration via HTTP call :

Time,Sales Entity,Month,Invoice Customer Code,Article Code,Allocation Code,Planning Type,Preferred Site,Preferred Warehouse,Vendor Account,Final Commercial Forecast,Commercial Forecast Lag 3M,Commercial Forecast Production Program
22-Mar,BE03,202205,300612,10000155,,STS,0241,27,BE03,500,1049,648
22-Mar,BE03,202205,300612,20000155,,STS,0241,27,BE03,501,1049,648
22-Mar,BE03,202208,300612,10000157,,STS,0241,27,BE03,500,1049,648
22-Mar,BE03,202209,300612,10000158,,STS,0241,27,BE03,501,1049,648

It’s a csv format payload, comma delimited, and consists of forecast values for different products.

This forecast will be sent in SAP S/4HANA using the Planned Independent Requirements API via POST or PATCH methods, depending on several filters ( f.e. the forecast period which determines if PATCH will also be used ).

In SAP Cloud Integration, by using the CSV to XML Converter we generate this payload which will be used for the rest of the iFlow :

<root> <row> <Product>10000155</Product> <Plant>0241</Plant> <PlndIndepRqmtType>VSF</PlndIndepRqmtType> <PlndIndepRqmtVersion>00</PlndIndepRqmtVersion> <RequirementPlan/> <RequirementSegment/> <RequirementPlanIsExternal>false</RequirementPlanIsExternal> <PlndIndepRqmtIsActive>X</PlndIndepRqmtIsActive> <PlndIndepRqmtPeriod>202205</PlndIndepRqmtPeriod> <PeriodType>M</PeriodType> <PlannedQuantity>500</PlannedQuantity> <WithdrawalQuantity>0</WithdrawalQuantity> <UnitOfMeasure>KG</UnitOfMeasure> </row> <row> <Product>10000155</Product> <Plant>0241</Plant> <PlndIndepRqmtType>VSF</PlndIndepRqmtType> <PlndIndepRqmtVersion>00</PlndIndepRqmtVersion> <RequirementPlan/> <RequirementSegment/> <RequirementPlanIsExternal>false</RequirementPlanIsExternal> <PlndIndepRqmtIsActive>X</PlndIndepRqmtIsActive> <PlndIndepRqmtPeriod>202205</PlndIndepRqmtPeriod> <PeriodType>M</PeriodType> <PlannedQuantity>501</PlannedQuantity> <WithdrawalQuantity>0</WithdrawalQuantity> <UnitOfMeasure>KG</UnitOfMeasure> </row>
</root>

Because some of the lines maybe are not fit to be further processed, or maybe we want to post/patch specific lines, this means we need to find a way on how we can link the Product number with the possible error messages.

In order to do that, we would do the following steps :

Local%20Process%20used%20for%20creating%20the%20product%20list

Local Process used for creating the product list

We would create a Message Mapping which creates a the following XML :

<?xml version="1.0" encoding="UTF-8"?> <root>
<row>
<Product>10000155</Product>
</row>
<row>
<Product>10000155</Product>
</row>
</root>

This XML will be saved as a property for future use.

In the next Message Mapping we will use the original payload for generating the proper batch message structure :

<batchParts> <batchChangeSet> <batchChangeSetPart> <method>POST</method> <PlannedIndepRqmt> <PlannedIndepRqmtType> <Product>10000155</Product> <Plant>0241</Plant> <MRPArea/> <PlndIndepRqmtType>VSF</PlndIndepRqmtType> <PlndIndepRqmtVersion>00</PlndIndepRqmtVersion> <RequirementPlan/> <RequirementSegment/> <RequirementPlanIsExternal>false</RequirementPlanIsExternal> <PlndIndepRqmtIsActive>X</PlndIndepRqmtIsActive> <to_PlndIndepRqmtItem> <PlannedIndepRqmtItemType> <Product>10000155</Product> <Plant>0241</Plant> <MRPArea/> <PlndIndepRqmtType>VSF</PlndIndepRqmtType> <PlndIndepRqmtVersion>00</PlndIndepRqmtVersion> <RequirementPlan/> <RequirementSegment/> <PlndIndepRqmtPeriod>202205</PlndIndepRqmtPeriod> <PeriodType>M</PeriodType> <PlannedQuantity>500</PlannedQuantity> <WithdrawalQuantity>0</WithdrawalQuantity> <UnitOfMeasure>KG</UnitOfMeasure> </PlannedIndepRqmtItemType> </to_PlndIndepRqmtItem> <link> <to_PlndIndepRqmtItem> <PlannedIndepRqmtItemType> <Product>10000155</Product> <Plant>0241</Plant> <MRPArea/> <PlndIndepRqmtType>VSF</PlndIndepRqmtType> <PlndIndepRqmtVersion>00</PlndIndepRqmtVersion> <RequirementPlan/> <RequirementSegment/> <PlndIndepRqmtPeriod>202205</PlndIndepRqmtPeriod> <PeriodType>M</PeriodType> </PlannedIndepRqmtItemType> </to_PlndIndepRqmtItem> </link> </PlannedIndepRqmtType> </PlannedIndepRqmt> </batchChangeSetPart> </batchChangeSet> <batchChangeSet> <batchChangeSetPart> <method>POST</method> <PlannedIndepRqmt> <PlannedIndepRqmtType> <Product>10000155</Product> <Plant>0241</Plant> <MRPArea/> <PlndIndepRqmtType>VSF</PlndIndepRqmtType> <PlndIndepRqmtVersion>00</PlndIndepRqmtVersion> <RequirementPlan/> <RequirementSegment/> <RequirementPlanIsExternal>false</RequirementPlanIsExternal> <PlndIndepRqmtIsActive>X</PlndIndepRqmtIsActive> <to_PlndIndepRqmtItem> <PlannedIndepRqmtItemType> <Product>10000155</Product> <Plant>0241</Plant> <MRPArea/> <PlndIndepRqmtType>VSF</PlndIndepRqmtType> <PlndIndepRqmtVersion>00</PlndIndepRqmtVersion> <RequirementPlan/> <RequirementSegment/> <PlndIndepRqmtPeriod>202205</PlndIndepRqmtPeriod> <PeriodType>M</PeriodType> <PlannedQuantity>501</PlannedQuantity> <WithdrawalQuantity>0</WithdrawalQuantity> <UnitOfMeasure>KG</UnitOfMeasure> </PlannedIndepRqmtItemType> </to_PlndIndepRqmtItem> <link> <to_PlndIndepRqmtItem> <PlannedIndepRqmtItemType> <Product>10000155</Product> <Plant>0241</Plant> <MRPArea/> <PlndIndepRqmtType>VSF</PlndIndepRqmtType> <PlndIndepRqmtVersion>00</PlndIndepRqmtVersion> <RequirementPlan/> <RequirementSegment/> <PlndIndepRqmtPeriod>202205</PlndIndepRqmtPeriod> <PeriodType>M</PeriodType> </PlannedIndepRqmtItemType> </to_PlndIndepRqmtItem> </link> </PlannedIndepRqmtType> </PlannedIndepRqmt> </batchChangeSetPart> </batchChangeSet>
</batchParts>

In the next Local Process, we will send the message to SAP S/4HANA, and in case of any errors in the response we will process them :

POST%20to%20SAP%20S/4HANA%20and%20error%20handling

POST to SAP S/4HANA and error handling

In our case, we received the following response :

<batchPartResponse>
<batchChangeSetResponse>
<batchChangeSetPartResponse>
<headers>
<Accept></Accept>
<Accept-Language></Accept-Language>
<Content-Length>1594</Content-Length>
<dataserviceversion>1.0</dataserviceversion>
<Content-Type>application/xml;charset=utf-8</Content-Type>
</headers>
<statusInfo>Bad Request</statusInfo>
<contentId/>
<body>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;&lt;error xmlns=&quot;http://schemas.microsoft.com/ado/2007/08/dataservices/metadata&quot;&gt;&lt;code&gt;PPH_FCDM/033&lt;/code&gt;&lt;message xml:lang=&quot;en&quot;&gt;Material &amp;amp;000000000010040155&amp;amp; in Plant &amp;amp;0241&amp;amp; or MRP Area not defined&lt;/message&gt;&lt;innererror&gt;&lt;application&gt;&lt;component_id&gt;PP-MRP&lt;/component_id&gt;&lt;service_namespace&gt;/SAP/&lt;/service_namespace&gt;&lt;service_id&gt;API_PLND_INDEP_RQMT_SRV&lt;/service_id&gt;&lt;service_version&gt;0001&lt;/service_version&gt;&lt;/application&gt;&lt;transactionid&gt;75592a5cc25d40f2924ec2b4538ca465&lt;/transactionid&gt;&lt;timestamp/&gt;&lt;Error_Resolution&gt;&lt;SAP_Transaction/&gt;&lt;SAP_Note&gt;See SAP Note 1797736 for error analysis (https://service.sap.com/sap/support/notes/1797736)&lt;/SAP_Note&gt;&lt;Batch_SAP_Note&gt;See SAP Note 1869434 for details about working with $batch (https://service.sap.com/sap/support/notes/1869434)&lt;/Batch_SAP_Note&gt;&lt;/Error_Resolution&gt;&lt;errordetails&gt;&lt;errordetail&gt;&lt;ContentID/&gt;&lt;code&gt;PPH_FCDM/033&lt;/code&gt;&lt;message&gt;Material &amp;amp;000000000010040155&amp;amp; in Plant &amp;amp;0241&amp;amp; or MRP Area not defined&lt;/message&gt;&lt;propertyref/&gt;&lt;severity&gt;error&lt;/severity&gt;&lt;target&gt;MRPArea&lt;/target&gt;&lt;additionalTargets&gt;&lt;target&gt;Plant&lt;/target&gt;&lt;target&gt;Product&lt;/target&gt;&lt;/additionalTargets&gt;&lt;transition&gt;true&lt;/transition&gt;&lt;/errordetail&gt;&lt;errordetail&gt;&lt;ContentID/&gt;&lt;code&gt;PPH_FCDM/033&lt;/code&gt;&lt;message&gt;Material &amp;amp;000000000010040155&amp;amp; in Plant &amp;amp;0241&amp;amp; or MRP Area not defined&lt;/message&gt;&lt;propertyref/&gt;&lt;severity&gt;error&lt;/severity&gt;&lt;target&gt;MRPArea&lt;/target&gt;&lt;additionalTargets&gt;&lt;target&gt;Plant&lt;/target&gt;&lt;target&gt;Product&lt;/target&gt;&lt;/additionalTargets&gt;&lt;transition&gt;true&lt;/transition&gt;&lt;/errordetail&gt;&lt;/errordetails&gt;&lt;/innererror&gt;&lt;/error&gt;</body>
<statusCode>400</statusCode>
</batchChangeSetPartResponse>
</batchChangeSetResponse>
<batchChangeSetResponse>
<batchChangeSetPartResponse>
<headers>
<Accept></Accept>
<Accept-Language></Accept-Language>
<Content-Length>1594</Content-Length>
<dataserviceversion>1.0</dataserviceversion>
<Content-Type>application/xml;charset=utf-8</Content-Type>
</headers>
<statusInfo>Bad Request</statusInfo>
<contentId/>
<body>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;&lt;error xmlns=&quot;http://schemas.microsoft.com/ado/2007/08/dataservices/metadata&quot;&gt;&lt;code&gt;PPH_FCDM/033&lt;/code&gt;&lt;message xml:lang=&quot;en&quot;&gt;Material &amp;amp;000000000010040155&amp;amp; in Plant &amp;amp;0241&amp;amp; or MRP Area not defined&lt;/message&gt;&lt;innererror&gt;&lt;application&gt;&lt;component_id&gt;PP-MRP&lt;/component_id&gt;&lt;service_namespace&gt;/SAP/&lt;/service_namespace&gt;&lt;service_id&gt;API_PLND_INDEP_RQMT_SRV&lt;/service_id&gt;&lt;service_version&gt;0001&lt;/service_version&gt;&lt;/application&gt;&lt;transactionid&gt;75592a5cc25d40f2924ec2b4538ca465&lt;/transactionid&gt;&lt;timestamp/&gt;&lt;Error_Resolution&gt;&lt;SAP_Transaction/&gt;&lt;SAP_Note&gt;See SAP Note 1797736 for error analysis (https://service.sap.com/sap/support/notes/1797736)&lt;/SAP_Note&gt;&lt;Batch_SAP_Note&gt;See SAP Note 1869434 for details about working with $batch (https://service.sap.com/sap/support/notes/1869434)&lt;/Batch_SAP_Note&gt;&lt;/Error_Resolution&gt;&lt;errordetails&gt;&lt;errordetail&gt;&lt;ContentID/&gt;&lt;code&gt;PPH_FCDM/033&lt;/code&gt;&lt;message&gt;Material &amp;amp;000000000010040155&amp;amp; in Plant &amp;amp;0241&amp;amp; or MRP Area not defined&lt;/message&gt;&lt;propertyref/&gt;&lt;severity&gt;error&lt;/severity&gt;&lt;target&gt;MRPArea&lt;/target&gt;&lt;additionalTargets&gt;&lt;target&gt;Plant&lt;/target&gt;&lt;target&gt;Product&lt;/target&gt;&lt;/additionalTargets&gt;&lt;transition&gt;true&lt;/transition&gt;&lt;/errordetail&gt;&lt;errordetail&gt;&lt;ContentID/&gt;&lt;code&gt;PPH_FCDM/033&lt;/code&gt;&lt;message&gt;Material &amp;amp;000000000010040155&amp;amp; in Plant &amp;amp;0241&amp;amp; or MRP Area not defined&lt;/message&gt;&lt;propertyref/&gt;&lt;severity&gt;error&lt;/severity&gt;&lt;target&gt;MRPArea&lt;/target&gt;&lt;additionalTargets&gt;&lt;target&gt;Plant&lt;/target&gt;&lt;target&gt;Product&lt;/target&gt;&lt;/additionalTargets&gt;&lt;transition&gt;true&lt;/transition&gt;&lt;/errordetail&gt;&lt;/errordetails&gt;&lt;/innererror&gt;&lt;/error&gt;</body>
<statusCode>400</statusCode>
</batchChangeSetPartResponse>
</batchChangeSetResponse>
</batchPartResponse>

As you can see, we have 2 problems :

  1. We cannot really figure out which line failed.
  2. The error text can’t be read easily because its unescaped

Because of these 2 reasons that prevented a clean understading of what went wrong and for which line, I used Marty’s blog ( thank you ! ) for which I did some small upgrades in order to suit my needs :

import com.sap.gateway.ip.core.customdev.util.Message
import groovy.xml.MarkupBuilder
import java.time.LocalDate
import java.time.format.DateTimeFormatter Message processData(Message message) { // Access message body and properties Reader reader = message.getBody(Reader) def messageLog = messageLogFactory.getMessageLog(message); def mapProperties = message.getProperties(); def valueProperty = mapProperties.get("ListItems"); // Define XML parser and builder def feed = new XmlParser().parseText(valueProperty) def i=0; def lista = feed.row.'*'.findAll{ node-> node.name() == 'Product' }*.text() def errorLog = "<root>"; def root = new XmlSlurper().parse(reader); int y =0; root.batchChangeSetResponse.each { y = y+1; statusCode = "${it.batchChangeSetPartResponse.statusInfo}"; if(statusCode != "Created") { errorLog = errorLog + "<error>"+"<Product>"+lista[y-1]+"</Product>"+"${it.batchChangeSetPartResponse.body}"+"</error>"; ErrorFoundFlag = 'ErrorFound'; message.setProperty("ErrorFoundFlag", ErrorFoundFlag); removal = errorLog; removal=removal.replace("<?xml version=\"1.0\" encoding=\"utf-8\"?>",""); errorLog = removal; } } errorLog = errorLog + "</root>"; message.setBody(errorLog); return message; }

These are the steps followed :

  1. We read the body and the property that stores the list of products from the original input payload ( ListItems )
  2. We create a list inside the script based on the Product node. This list will be used to interate based on the different positions.
  3. We parse the input body, and based on each occurence of batchChangeSetPartResponse we create another error node.
  4. The error node is also conditioned on the statusCode, so it would get created only for the status NOT Created.
  5. In the end i removed the xml definition in order to fix the schema validation errors.
  6. For each line we fetch the proper Product ID from the List we created earlier.
<root> <error> <Product>10040155</Product> <error xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"> <code>PPH_FCDM/033</code> <message xml:lang="en">Material &amp;000000000010040155&amp; in Plant &amp;0241&amp; or MRP Area not defined</message> <innererror> <application> <component_id>PP-MRP</component_id> <service_namespace>/SAP/</service_namespace> <service_id>API_PLND_INDEP_RQMT_SRV</service_id> <service_version>0001</service_version> </application> <transactionid>75592a5cc25d40f2924ec2b4538ca465</transactionid> <timestamp/> <Error_Resolution> <SAP_Transaction/> <SAP_Note>See SAP Note 1797736 for error analysis (https://service.sap.com/sap/support/notes/1797736)</SAP_Note> <Batch_SAP_Note>See SAP Note 1869434 for details about working with $batch (https://service.sap.com/sap/support/notes/1869434)</Batch_SAP_Note> </Error_Resolution> <errordetails> <errordetail> <ContentID/> <code>PPH_FCDM/033</code> <message>Material &amp;000000000010040155&amp; in Plant &amp;0241&amp; or MRP Area not defined</message> <propertyref/> <severity>error</severity> <target>MRPArea</target> <additionalTargets> <target>Plant</target> <target>Product</target> </additionalTargets> <transition>true</transition> </errordetail> <errordetail> <ContentID/> <code>PPH_FCDM/033</code> <message>Material &amp;000000000010040155&amp; in Plant &amp;0241&amp; or MRP Area not defined</message> <propertyref/> <severity>error</severity> <target>MRPArea</target> <additionalTargets> <target>Plant</target> <target>Product</target> </additionalTargets> <transition>true</transition> </errordetail> </errordetails> </innererror> </error> </error>
</root>

At this point we have the Product IDs and message texts, but because it got a little difficult to handle the whole process in one script I decided to duplicate this one, use the above xml as an input for the next one and create my final structure. It’s basically the same, with differences coming from the final structure and the if used in order to check if it’s neccesary to generate an attachment.

 def root = new XmlSlurper().parse(reader); int y =0; def ErrorFoundFlag = ""; def status = ""; root.error.each { errorLog = errorLog + "<error>"+"<Product>"+lista[y-1]+"</Product>"+"<Message>"+"${it.error.message}"+"</Message>"+"</error>"; ErrorFoundFlag = 'ErrorFound'; message.setProperty("ErrorFoundFlag", ErrorFoundFlag); } errorLog = errorLog + "</root>"; message.setBody(errorLog); if(ErrorFoundFlag.contains('ErrorFound')) { messageLog.addAttachmentAsString("POST Call Errors", errorLog, "text/plain"); } return message; }

And the final payload which can be seen in the Message Monitoring as an attachment under the name POST Call Errors :

<root> <error> <Product>10000155</Product> <Message>Invalid period name for period type</Message> </error> <error> <Product>20000155</Product> <Message>Material &000000000020000155& in Plant &0241& or MRP Area &0241& not defined</Message> </error>
</root>

The same logic was used for the PATCH call, the only thing changed was the statusCode from Created to 204.

In this way, we will be able to troubleshoot the following scenarios :

  1. all lines were supposed to be sent and were processed succesfully.
  2. not all lines were supposed to be sent and were processed succesfully.
  3. some lines were supposed to be posted and some patched, and for each failed line we have it’s error message and the proper product code.

I hope you liked my article and feel free to reply with  any suggestions here on in the SAP S/4HANA Questions Section as I am still updating the flow.

Thank you !