1

In our multi-currency org, we get daily updates of conversion rates in the DatedConversionRate object and these need to be copied to the CurrencyType object.

For those of you who don't know: DML is not allowed on CurrencyType, you can only update that object via REST API. So a simple Apex trigger will not do, we ended up creating an Apex batch job that performs one REST API call per currency (we have 11 of them).

However, performing those REST API calls one after the other, often results in the error message "rsf calculation in progress" (ALREADY_IN_PROCESS). It seems that changing the rate of a CurrencyType triggers recalculations in Salesforce. You cannot postpone it or influence it in any way.

As I see it, there are two solutions:

  1. Allow more time between the REST API calls.
  2. Offer all updates in one composite REST API call.

The second solution seems the cleanest. However, I cannot get it to work. For those of you new to composite REST API calls, the URL that you use ends in /composite and the body contains the actual updates. Each update must have an element called referenceId.

So I did this and the body looks like this:

{
    "compositeRequest": [
        {
            "url": "https://ourcompanysandbox.sandbox.my.salesforce.com/services/data/v52.0/sobjects/CurrencyType/01L58000000Lz7iEAC?_HttpMethod=PATCH",
            "referenceId": "AUD",
            "method": "POST",
            "body": {
                "ConversionRate": 1.4687
            }
        },
        ...

But unfortunately, it doesn't work. The REST API call for the composite returns HTTP code 200, but for each of the updates, the body reports an HTTP 400 code, with the error 'https://ourcompanysandbox.sandbox.my.salesforce.com/services/data/v52.0/sobjects/CurrencyType/01L58000000Lz7iEAC?_HttpMethod=PATCH' is not a valid url (PROCESSING_HALTED)

This is the exact same URL as we used for the individual updates, but all of a sudden it is not valid anymore? I don't understand.

UPDATE: After I had implemented the composite REST API call successfully, it turned out that the updates inside the composite call are not executed in a single transaction, as I had hoped. The call still fails, because of the "rsf calculation in progress" issue. So the real solution is to implement retries or spread the individual calls out in time.

2
  • There's .sandbox in the url. Could this be an issue with enhanced domain enabled in the org?
    – Shamina
    Commented Nov 3, 2022 at 15:58
  • 1
    not sure you need the full url in the url field on the composite request, just from /services onwards, hence probably reporting the HTTP 400 error. Commented Nov 4, 2022 at 16:20

4 Answers 4

4
+50

I just tried this out in one of my trailhead orgs with Postman, and the following setup works for me.

The following Request works fine for me:

{
  "allOrNone": false,
  "compositeRequest": [
    {
      "method": "PATCH",
      "url": "/services/data/v55.0/sobjects/CurrencyType/01L7R0000007EJeUAM",
      "referenceId": "updateCurrencyUSD",
      "body": {
        "ConversionRate": 1.11
      }
    }
  ]
}

Here is some explanation, that might help you further understand it:

Method = PATCH because you want to update a record: Source https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_update_fields.htm?q=PATCH

URL = CurrencyType -> Just the sObjectType which you can also query + RecordId (In my case "01L7R0000007EJeUAM"). I think in your URL you need to get rid of the "?_HttpMethod=PATCH" part

referenceId & body --> should be clear

Here is a screenshot of my Postman-Setup, if you are interested. enter image description here

Note: I receive a http 204 Status code, but whenever I do an SOQL on the CurrencyType object I can see, the value has changed.

EDIT: Updating multiple currencies at once also worked with this request:

{
  "allOrNone": false,
  "compositeRequest": [
   {
      "method": "PATCH",
      "url": "/services/data/v55.0/sobjects/CurrencyType/01L7R0000007EJeUAM",
      "referenceId": "updateCurrencyUSD",
      "body": {
        "ConversionRate": 1.23
      }
    },
    {
      "method": "PATCH",
      "url": "/services/data/v55.0/sobjects/CurrencyType/01L7R0000007EJZUA2",
      "referenceId": "updateCurrencyGBP",
      "body": {
        "ConversionRate": 1.46
      }
    },
    {
      "method": "PATCH",
      "url": "/services/data/v55.0/sobjects/CurrencyType/01L7R0000007EJjUAM",
      "referenceId": "updateCurrencySGD",
      "body": {
        "ConversionRate": 1.56
      }
    },
    {
      "method": "PATCH",
      "url": "/services/data/v55.0/sobjects/CurrencyType/01L7R0000007EJoUAM",
      "referenceId": "updateCurrencySTN",
      "body": {
        "ConversionRate": 1.66
      }
    }
  ]
}
2

The following is only interested in the second of your options: use of the composite REST API - more precisely its "Composite Batch" flavour:

In this case the URL ends with /composite/batch and PATCH can be used in sub requests.

private static HttpResponse doCallout(List<CurrencyType> currencies) {

    String part = '/services/data/v55.0';
    String endpoint = Url.getOrgDomainUrl().toExternalForm() 
        + part + '/composite/batch';

    HttpRequest request = new HttpRequest();
    request.setEndpoint(endpoint);
    request.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionId());
    request.setHeader('Content-Type', 'application/json');
    request.setMethod('POST');

    Map<String, List<Map<String, Object>>> requestMap 
        = new Map<String, List<Map<String, Object>>>();
    List<Map<String, Object>> subRequests = getSubRequests(currencies, part);
    requestMap.put('batchRequests', subRequests);
    String body = JSON.serialize(requestMap);
    request.setBody(body);

    HttpResponse response = new Http().send(request);
    return response;
}

private static List<Map<String, Object>> getSubRequests(
        List<CurrencyType> currencies, String part) {

    List<Map<String, Object>> subRequests = new List<Map<String, Object>>();
    for(CurrencyType currencyType : currencies) {
        Map<String, Object> parameters = new Map<String, Object>();
        parameters.put('method', 'PATCH');
        parameters.put('url', part + '/sobjects/CurrencyType/' + currencyType.Id);
        Map<String, Double> richInputs = new Map<String, Double>();
        richInputs.put('ConversionRate', currencyType.ConversionRate);
        parameters.put('richInput', richInputs);
        subRequests.add(parameters);
    }
    return subRequests;
}

From what I can tell the first attempt might not always work. Let's wrap requests in a for-loop:

public static Boolean updateCurrencies(List<CurrencyType> currencies) {
    Integer numberAttempts = 3;
    for(Integer i=0; i<numberAttempts; i++) {
        HttpResponse response;
        try {
            response = doCallout(currencies);
        } catch (Exception e) {
            // TODO: note exception
            continue;
        }

        if (response.getStatusCode() == 200) {
            String body = response.getBody();
            Map<String, Object> bodyMap 
                = (Map<String, Object>) JSON.deserializeUntyped(body);
            if (bodyMap.get('hasErrors') == false) {
                return true;
            }
            // TODO: handle mistakes in "currencies"
            return false;
        }
        // TODO: note "unexpected status code"
    }
    // TODO: handle general failure
    return false;
}

Three rounds should be enough.

Addition 06/11/2022

To facilitate comparisons with the answer of Muffinzlol, here's an example of the JSON body generated by above code. I assume two CurrencyTypes are passed to the method, one with a misconfigured, negative ConversionRate:

{
    "batchRequests": [
        {
            "richInput": {
                "ConversionRate": -42.0
            },
            "url": "/services/data/v55.0/sobjects/CurrencyType/01L550000000qhdEAA",
            "method": "PATCH"
        },
        {
            "richInput": {
                "ConversionRate": 42.0
            },
            "url": "/services/data/v55.0/sobjects/CurrencyType/01L550000000qgzEAA",
            "method": "PATCH"
        }
    ]
}

The response body would look like this:

{
    "hasErrors": true,
    "results": [
        {
            "result": [
                {
                    "errorCode": "INVALID_CURRENCY_CONV_RATE",
                    "message": "The conversion rate has to be positive"
                }
            ],
            "statusCode": 400
        },
        {
            "statusCode": 204,
            "result": null
        }
    ]
}

Whereas Salesforce's response is sent with HTTP status code 200, the individual currency types are returned with a status code indicating success (204) or failure (400) of the individual update.

0

NB: I don't have have experiance in composite api, but I am trying to help you based on my experiance working on different APIs in salesforce and other technology.

  1. Is there are a reason why you are passing httpmethod as QueryParameter.?

?_HttpMethod=PATCH'

  1. Have you tried updating the method in the body to "method": "PATCH",

  2. When you tried to call the API from postman, while doing that, have you tried using 'PATCH' httpmethod. example below,

enter image description here

1
  • PATCH is not allowed as an HTTP method for CurrencyType. Don't ask me why not, you get an error. Passing it as a parameter is a workaround that I found somewhere (probably on this forum). Commented Nov 3, 2022 at 11:46
0

Well my scenario was a bit different, but what I ended up doing is the following. 1)Receive data in an platform event -> platform event trigger runs and creates a new record(object name cdwFxRate__c). 2)When the cdwFxRate__c object trigger runs it calls a future method that looks like this

@future(callout=true)
public static void updateManagedCurrency(String data){
    cdwFxRate__c fxr = (cdwFxRate__c)JSON.deserialize(data, cdwFxRate__c.class);
    List<String> reqId = fxr.requested_ID__c.split('/');
    String requestedId = reqId[0];//on first position we are certain that the Id is populated
    Http h = new Http(); 
    HttpRequest req = new HttpRequest(); 
    req.setEndpoint(URL.getSalesforceBaseUrl().toExternalForm() + '/services/data/v51.0/sobjects/CurrencyType/'); 
    req.setMethod('POST'); 
    req.setBody('{"IsoCode" : "' + reqId[reqId.size()-2] + '", "DecimalPlaces" : 6, "ConversionRate" : ' + fxr.FxRate__c + ', "IsActive" : "true"}'); 
    System.debug('body ' + req.getBody());
    Custom_Data__c cData = [SELECT Password__c, Security_Token__c, Username__c FROM Custom_Data__c WHERE Name = :Constants.CURRENCY_TYPE_BATCH_CUSTOM_DATA_NAME];
    String user = cData.Username__c;
    String pass = cData.Password__c + (cData.Security_Token__c == null ? '' : cData.Security_Token__c);
    String sessionId = login(user, pass);
    req.setHeader('Authorization' , 'OAuth ' + sessionId); 
    System.debug('body ' + req.getHeader('Authorization'));
    req.setHeader ('Content-Type' , 'application/json'); 
    if(!Test.isRunningTest()){
        HttpResponse res = h.send(req);
    }
}

private static String login(String user, String password) {
    HttpRequest req = new HttpRequest();
    req.setMethod('POST');   
    req.setTimeout(60000);
    req.setEndpoint(URL.getSalesforceBaseUrl().toExternalForm()+'/services/Soap/u/51.0');
    req.setHeader('Content-Type', 'text/xml;charset=UTF-8');        
    req.setHeader('SOAPAction', '""');
    req.setBody('<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/"><Header/><Body><login xmlns="urn:partner.soap.sforce.com"><username>' + user + '</username><password>' + password + '</password></login></Body></Envelope>');        
    HttpResponse res =  new Http().send(req);
    system.debug(res);
    if(res.getStatusCode() == 200){
        Dom.Document responseDocument = res.getBodyDocument();
        Dom.Xmlnode rootElm = responseDocument.getRootElement(); // soapenv:Envelope
        Dom.Xmlnode bodyElm = rootElm.getChildElement('Body', Constants.NS_SOAP); // soapenv:Body 
        Dom.Xmlnode loginResponseElm = bodyElm.getChildElement('loginResponse', Constants.NS_SF); // loginResponse
        Dom.Xmlnode resultElm = loginResponseElm.getChildElement('result', Constants.NS_SF); // result
        Dom.Xmlnode sessionIdElm = resultElm.getChildElement('sessionId', Constants.NS_SF); // sessionId
        return sessionIdElm.getText();
    }
    return null;
}

private class LoginException extends Exception {}

The biggest issue I encountered was that I didn't had access to login data, so I had to perform a callout in order to get a valid sessionId, hence the login method.

Hope this helps, let me know if you have any questions.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .