Custom service with multi-level JSON request/response

Using data contracts it is possible to build an arbitraty structure of input arguments and return values. For example, we need to update in one request several records and receive a detailed response about success of an operation for each record.

As an input we will receive a list of record identifiers with parameters to be changed

as an output user can receive a list of operation results for each record.

Following steps need to be performed:

  1. Create data contracts for input and output and build a needed tree.

  2. Create a custom service.

  3. Add service to a service group.

  4. Build module.

  5. Run a request with a RESTful services tool (Postman.)

As an example we will take a sales order with sales order lines. Confirmed shupping date and quantity should be updated on each line.

Input contract:
/// <summary> /// Data contract for sales order Line /// </summary> [DataContract] class ruaSalesOrderLineContract { InventTransId inventTransId; SalesQty salesQty; SalesShippingDateConfirmed shippingDateConfirmed; [DataMember('InventTransId')] public InventTransId parmInventTransId(InventTransId _inventTransId = inventTransId) { inventTransId = _inventTransId; return inventTransId; } [DataMember('SalesQty')] public SalesQty parmSalesQty(SalesQty _salesQty = salesQty) { salesQty = _salesQty; return salesQty; } [DataMember('ShippingDateConfirmed')] public SalesShippingDateConfirmed parmShippingDateConfirmed(SalesShippingDateConfirmed _shippingDateConfirmed = shippingDateConfirmed) { shippingDateConfirmed = _shippingDateConfirmed; return shippingDateConfirmed; } }

====================================================================

This contract will be an item in a list of an input contract for a sales order:

/// <summary> /// Data contract for sales order service /// </summary> [DataContract] class ruaSalesOrderHeaderContract { SalesId salesId; List salesLineList = new List(Types::Class); /// <summary> /// Order ID /// </summary> /// <param name = "_salesId">Order ID</param> /// <returns></returns> [DataMember('SalesId')] public SalesId parmSalesId(SalesId _salesId = salesId) { salesId = _salesId; return salesId; } /// <summary> /// Get/set list of lines /// </summary> /// <param name = "_salesLineList">Lines list</param> /// <returns></returns> [DataMember('Lines'), AifCollectionType('_salesLineList', Types::Class, classStr(ruaSalesOrderLineContract)), AifCollectionType('return', Types::Class, classStr(ruaSalesOrderLineContract))] public List parmLines(List _salesLineList = salesLineList) { salesLineList = _salesLineList; return salesLineList; } }


===========================================================

Then we will define a response.

First we will create a response data contract for a sales line:

 

/// <summary>

/// Response object for updating/inserting sales lines

/// </summary>

[DataContract]

class ruaSalesOrderLineResponseContract

{

    InventTransId   inventTransId;

    str             message;

    str         status;


    [DataMember("InventTransId")]

    public InventTransId parmInventTransId(InventTransId _inventTransId = inventTransId)

    {

        inventTransId = _inventTransId;

        return inventTransId;

    }


    [DataMember("Status")]

    public str parmStatus(str _status = status)

    {

        status = _status;

        return status;

    }


    [DataMember("Message")]

    public str parmMessage(str _message = message)

    {

        message = _message;

        return message;

    }


    public void setFailed(str _message)

    {

        this.parmStatus("Failed");

        this.parmMessage(_message);

    }


    public void setSuccess(str _message = "success")

    {

        this.parmStatus("OK");

        this.parmMessage(_message);

    }


}



=====================================================================

Then a response contract for a service method. This method will contain a list of a sales lines' responses:

[DataMember('Lines'), AifCollectionType('_linesResponse', Types::Class, classStr(ruaSalesOrderLineResponseContract)), AifCollectionType('return', Types::Class, classStr(ruaSalesOrderLineResponseContract))] public List parmLines(List _linesResponse = linesResponse) { linesResponse = _linesResponse; return linesResponse; }


==========================================================

A response contract looks like this:

/// <summary> /// COntract class for sales order service /// </summary> [DataContract] class ruaSalesOrderServiceResponseContract { SalesId salesId; str message; str status; List linesResponse = new List(Types::Class); [DataMember("SalesId")] public SalesId parmSalesId(SalesId _salesId = salesId) { salesId = _salesId; return salesId; } [DataMember("Status")] public str parmStatus(str _status = status) { status = _status; return status; } [DataMember("Message")] public str parmMessage(str _message = message) { message = _message; return message; } [DataMember("Lines"), AifCollectionType("_linesResponse", Types::Class, classStr(ruaSalesOrderLineResponseContract)), AifCollectionType("return", Types::Class, classStr(ruaSalesOrderLineResponseContract))] public List parmLines(List _linesResponse = linesResponse) { linesResponse = _linesResponse; return linesResponse; } public void setFailed(str _message) { this.parmStatus("Failed"); this.parmMessage(_message); } public void setSuccess(str _message = "success") { this.parmStatus("OK"); this.parmMessage(_message); } }


=======================================================

Then we will create a service with 1 method updateOrderLines

/// <summary> /// Service class for processing sales order in one transaction /// </summary> class ruaSalesOrderService extends SysOperationServiceBase { /// <summary> /// Updates sales order lines /// </summary> /// <param name = "_headerContract">Sales order header contract (with lines)</param> /// <returns>Response for header and each line</returns> public ruaSalesOrderServiceResponseContract updateOrderLines(ruaSalesOrderHeaderContract _headerContract) { SalesLine salesLine; ListEnumerator salesLineListEnumerator; ruaSalesOrderLineContract salesOrderLineContract; ruaSalesOrderServiceResponseContract response = new ruaSalesOrderServiceResponseContract(); boolean ok = true, isModified; try { if(_headerContract == null) { response.setFailed("Input contract is null"); return response; } if(!SalesTable::exist(_headerContract.parmSalesId())) { response.setFailed(strFmt("Sales order %1 does not exist", _headerContract.parmSalesId())); return response; } if(_headerContract.parmLines() == null || _headerContract.parmLines().empty()) { response.setFailed("Input contract contains no lines"); return response; } salesLineListEnumerator = _headerContract.parmLines().getEnumerator(); ttsbegin; response.parmSalesId(_headerContract.parmSalesId()); while(salesLineListEnumerator.moveNext()) { isModified = false; salesOrderLineContract = salesLineListEnumerator.current(); if(salesOrderLineContract) { ruaSalesOrderLineResponseContract lineResponse = new ruaSalesOrderLineResponseContract(); lineResponse.parmInventTransId(salesOrderLineContract.parmInventTransId()); response.parmLines().addEnd(lineResponse); salesLine = SalesLine::findInventTransId(salesOrderLineContract.parmInventTransId(), true); if(!salesLine) { lineResponse.setFailed(strFmt("Sales line with ID %1 was not found", salesOrderLineContract.parmInventTransId())); ok = false; continue; } if(salesLine.SalesId != _headerContract.parmSalesId()) { lineResponse.setFailed(strFmt("Sales line with ID %1 is from another sales order (%2)", salesOrderLineContract.parmInventTransId(), salesLine.SalesId)); ok = false; continue; } if(salesOrderLineContract.parmSalesQty() != 0 && salesOrderLineContract.parmSalesQty() != salesLine.SalesQty) { salesLine.SalesQty = salesOrderLineContract.parmSalesQty(); salesLine.modifiedField(fieldNum(SalesLine, SalesQty)); isModified = true; } if(salesOrderLineContract.parmShippingDateConfirmed() != dateNull() && salesOrderLineContract.parmShippingDateConfirmed() != salesLine.ShippingDateConfirmed) { salesLine.ShippingDateConfirmed = salesOrderLineContract.parmShippingDateConfirmed(); salesLine.modifiedField(fieldNum(SalesLine, ShippingDateConfirmed)); isModified = true; } if(isModified) { if(!salesLine.validateWrite()) { lineResponse.setFailed(ruaWebServiceHelper::getErrorFromInfolog()); ok = false; } else { lineResponse.setSuccess(); salesLine.update(); } } else { lineResponse.setSuccess("No changes are acknowledged"); } infolog.clear(); } } if(!ok) { throw error("@ElectronicReportingMapping:ValidationErrorsExist"); } response.setSuccess(); ttscommit; } catch(Exception::CLRError) { response.setFailed(AifUtil::getClrErrorMessage()); } catch(Exception::Error) { response.setFailed(ruaSalesOrderService::getErrorFromInfolog()); } return response; } public static str getErrorFromInfolog() { SysInfologEnumerator enumerator; SysInfologMessageStruct msgStruct; Exception exception; str error; enumerator = SysInfologEnumerator::newData(infolog.cut()); while (enumerator.moveNext()) { msgStruct = new SysInfologMessageStruct(enumerator.currentMessage()); exception = enumerator.currentException(); error = strfmt("%1 %2", error, msgStruct.message()); } return error; } }


=========================================================================

Create a new service ruaSalesOrderService.

Add operation updateOrderLines to it.

Create new service group ruaServices

Add ruaSalesOrderService to this group.


To call a D365FO service from extern first we need to obtain an authorization token.

A detailed descriptions can be found here.

Service URL should look like this:
https://<your_instance>.cloudax.dynamics.com/api/services/<Service group name>/ruaSalesOrderService/updateOrderLines



===========================================================================

A request JSON names should exactly coinside with a parameter names:

{ "_headerContract": { "SalesId":"RUA-S000093", "Lines":[ { "InventTransId":"SOL-000562", "ShippingDateConfirmed":"2019-09-28T18:25:43.511Z", "SalesQty":6 }, { "InventTransId":"SOL-000611", "ShippingDateConfirmed":"2019-09-28T18:25:43.511Z", "SalesQty":14 }] } }

===============================================================

A response will look like this:


{
    "$id": "1",
    "SalesId": "RUA-S000093",
    "Status": "Failed",
    "Message": " Validation errors exist",
    "Lines": [
        {
            "$id": "2",
            "InventTransId": "SOL-000562",
            "Status": "Failed",
            "Message": "Sales line with ID IFR-000504 was not found"
        },
        {
            "$id": "3",
            "InventTransId": "SOL-000611",
            "Status": "Success",
            "Message": ""
        }
    ]
}



===========================================================================================================


Comments

Popular posts from this blog

Customization on Sales invoice Report in D365 F&O

75) COC - Create a coc of the table modified method

46) D365 FO: SHAREPOINT FILE UPLOAD USING X++