Showing posts with label D365FO. Show all posts
Showing posts with label D365FO. Show all posts

Monday, May 12, 2025

Consuming External Web Services in D365FO


Enhancing Dynamics 365 Finance and Operations with Real-Time External Data

Integrating external web services into Dynamics 365 Finance and Operations (D365FO) can greatly enhance functionality, allowing for the use of real-time data from various sources. In this article, we will demonstrate how to consume an external web service in X++ using a practical example: retrieving currency exchange rates from a publicly available API.


💡 Use Case: Currency Conversion in the Sales Order Form

We aim to build a class that fetches the exchange rate between two currencies. The chosen API for this example is ExchangeRate-API, which provides conversion rates between multiple currencies as well as many other services.

Our specific implementation will:

  • Integrate into the Sales order form
  • Add a new button labeled "Show price in USD" (we assume that the company currency is USD)
  • Display both the conversion rate and the price in USD via infolog
  • Convert prices from Sales order currency to USD in real-time

Additionally, we'll leverage existing D365FO tables (Regulatory Service EMWebApplication and EMWebService) to securely store connection parameters and API keys. This approach promotes reuse and simplifies maintenance of external service configurations.

📌 Pro Tip: Although these tables are part of the Electronic Messaging (EM) functionality mainly intended for regulatory reporting, they can also be conveniently utilized for storing external web service configurations.


📋 Prerequisites

  • D365FO environment with development access
  • Access to a publicly available API (e.g., ExchangeRate-API)
  • API key for authentication: you can get it for free on the API web site

Implementation Guide

Step 1: Storing Connection Information

D365FO provides the EMWebApplication and EMWebService tables within the Regulatory Service module to manage external service connections. These tables allow you to store connection parameters securely.

  1. Add records for your application (e.g., 'wzhExchangeRateAPI')
  2. Create service records (e.g., 'wzhCurrencyPairConversion')
  3. Store the base URL and request method in these tables

Understanding the table structure:

  • Web Application table: Stores main connection information, such as base URL and general settings

  • Web Service table: Stores different API endpoints under the same application (like methods for Pair Conversion and Latest Rates)

Step 2: API response class wzhExchangeRateResponseDC

Create a new class named wzhExchangeRateResponseDC. This class contains methods to receive the API response.

// to be used as a response from wzhExchangeRateServiceConvert
[DataContract]
public class wzhExchangeRateResponseDC
{
    str         result;
    real        rate;

    [DataMember("result")]
    public str parmResult(str _result = result)
    {
        result = _result;
        return result;
    }

    [DataMember("conversion_rate")]
    public real parmRate(real _rate = rate)
    {
        rate = _rate;
        return rate;
    }

}

Step 3: Main logic class wzhExchangeRateService

Create a new class named wzhExchangeRateService. This class contains methods to initialize the service, retrieve conversion rates, and display the converted price. The class is designed to be triggered from a button click in the Sales order form. The construct method initializes the service object and sets the sales line parameter.

/// <summary>
/// Consumes external Web service for currency conversions
/// </summary>
class wzhExchangeRateService
{
    SalesLine salesLine;
    // to be run from a button
    static void main(Args _args)
    {
        SalesLine salesLine = _args.record();

        wzhExchangeRateService service = wzhExchangeRateService::construct(salesLine);
        service.run();
    }

    public SalesLine parmSalesLine(SalesLine _salesLine = salesLine)
    {
        salesLine = _salesLine;
        return salesLine;
    }

    static private wzhExchangeRateService construct(SalesLine _salesLine)
    {
        wzhExchangeRateService service = new wzhExchangeRateService();
        
        service.parmSalesLine(_salesLine);

        return  service;
    }

    public void run()
    {
        CurrencyCode currencyFrom   = salesline.CurrencyCode;
        CurrencyCode currencyTo     = Ledger::accountingCurrency();
        real rate = this.getConversionRate(currencyFrom, currencyTo);
        info(strFmt("Conversion rate from %1 to %2: %3", currencyFrom, currencyTo, rate));
        info(strFmt("Price in %1 is %2", currencyTo, salesLine.SalesPrice * rate));
    }

    /// <summary>
    /// Fetches the conversion rate between two given currencies.
    /// </summary>
    /// <param name="_currencyFrom">Base currency code (e.g., USD)</param>
    /// <param name="_currencyTo">Target currency code (e.g., EUR)</param>
    public real getConversionRate(CurrencyCode _currencyFrom, CurrencyCode _currencyTo)
    {
        // these constants can be saved in a separate parameters table
        const str                       appName     = 'wzhExchangeRateAPI';
        const str                       serviceId   = 'wzhCurrencyPairConversion';
        System.Net.HttpWebRequest       request;
        System.Net.HttpWebResponse      response;
        System.IO.StreamReader          reader;
        System.Net.WebHeaderCollection  httpHeader;
        str                             responseData, url, apiKey;
        EMWebApplication                webApp;
        EMWebService                    webService;
        container                       jsonContainer;
        Object                          jsonObject;
        Real                            conversionRate;
        System.String                   netString;
        System.Exception                netExcepn;
        str                             msg;
        str                             errMsg;
        wzhExchangeRateResponseDC       resp;

        try
        {
            // Step 1: Find the Web Application and Web Service records
            select firstonly webApp
                where webApp.Name == appName;

            if (!webApp.RecId)
            {
                throw error("Web application for currency conversions not found.");
            }

            select firstonly webService
                where webService.WebApplication == webApp.RecId
                   && webService.WebServiceId == serviceId;

            if (!webService.RecId)
            {
                throw error("Web Service for currency conversion rates not found.");
            }

            // Step 2: Retrieve the API key securely (from Azure Key Vault or encrypted table); check other methods getDigitalCertificate, getPrivateKey
            apiKey              = KeyVaultCertificateHelper::getManualSecretValue(webService.KeyVaultCertificateRef, false);
            apiKey              = '111914749114a4117ca989d2'; // for the test's sake

            // Step 3: Construct the API URL dynamically
            url                 = strFmt("%1/%2/%3/%4", webApp.BaseURL, webService.URL, _currencyFrom, _currencyTo);

            // Step 4: Create the HTTP request
            request             = System.Net.WebRequest::Create(url);
            request.Method      = webService.RequestMethod; // GET
            request.ContentType = webService.RequestContentType; // application/json
            httpHeader          = new System.Net.WebHeaderCollection();
            httpHeader.Add("Authorization", 'Bearer ' + apiKey); // security info
            request.set_Headers(httpHeader);

            // Step 5: Get the response
            response            = request.GetResponse();
            reader              = new System.IO.StreamReader(response.GetResponseStream());
            responseData        = reader.ReadToEnd();

            // Step 6: Parse the JSON response to a custom data contract bearing Success code and rate
            resp                = FormJsonSerializer::deserializeObject(classNum(wzhExchangeRateResponseDC), responseData);

            //if (resp && resp.parmResult() == webService.SuccessfulResponseCode) // this table success code is int and normally meant to be 200;
            if (resp && resp.parmResult() == webService.AdditionalSuccessfulResponseCodes) // but here we use this parameter as 'success'
            {
                conversionRate = resp.parmRate();
            }
            else
            {
                throw error("Error: Failed to parse the conversion rate.");
            }
        }
        catch (Exception::CLRError)
        {
            netExcepn   = CLRInterop::getLastException();

            if(netExcepn)
            {
                // something goes wrong with the service call
                // let's grab all details
                errMsg      = strFmt("Error: %1", netExcepn.InnerException.Message);
                msg         = strFmt("Failed to retrieve conversion rate %1->%2. \n%3", _currencyFrom, _currencyTo, errMsg);
            }
            throw error(strFmt("Error:%1", msg));
        }
        return conversionRate;
    }

}

Step 4: Fetch Conversion Rate

The core functionality lies in the getConversionRate method. It follows these steps:

  1. Finds the web application and service records using EMWebApplication and EMWebService tables
  2. Retrieves the API key securely, ideally using Azure Key Vault
  3. Constructs the URL dynamically based on input parameters
  4. Sends an HTTP request to the API and reads the response
  5. Parses the response using a custom data contract (wzhExchangeRateResponseDC)

🛡️ Handling Errors

The getConversionRate method includes robust error handling to manage connectivity issues or invalid API responses. Any errors are captured, and detailed messages are displayed to the user.


▶️ Running the Service

The run method prints the conversion rate and calculates the converted price based on the current sales line's currency and the accounting currency. This feature can be executed by clicking the "Show price in USD" button on the Sales order form.


🌟 Advantages of This Approach

  • Configuration-Driven: Uses Web Application and Web Service records instead of hardcoding URLs
  • Dynamic URL Construction: Retrieves settings directly from the D365FO database
  • Flexible: If the API URL changes, you only need to update the Web Application record
  • Efficient Management: The Web Application table holds the main connection information, while the Web Service table can manage multiple API endpoints under a single application

Final Thoughts

Consuming external web services in X++ allows D365FO to leverage real-time data from third-party sources. By using D365FO's existing EMWebApplication and EMWebService tables for storing connection data, we can enhance security and maintainability. Following this structured approach enables developers to integrate external services efficiently and securely.

Feel free to customize the class and extend its functionality according to your project requirements. Happy AXing!

Friday, August 30, 2024

Dual-write setup from Lifecycle Services fails at Application step

If you receive the issue with applying Dual-write for your D365FO environment following the standard procedure, you may need just to change one database parameter.



 Open SQL MS and switch this flip to True and keep default values for your database.


Now you can resume the process.

How to get back Import users button in D365FO

 

 You may start experiencing the following issue with button Import users.


It happens because Microsoft changed their approach to devboxes settings:

As of November 15, 2023, certificates in your Microsoft Entra tenant are no longer installed in new one-box development environments by default. 

You can find more detail on how to install a certificate in this article, but we can use a shortcut using 
D365fo.tools.

Here are the steps:
    

   1) Go to Azure portal and select app registration and create a new one (name the app to be the same as the environment name & leave settings at default) 

   2) The app registration needs to have the following: 

       a) 2 reply URLs > https://<fno-url> & https://<fno-url>/oauth 

       b) add the following API permissions to the App registration: 



Grant above permissions.

3) On the VM for the environment, open PowerShell 

   4) Run the following commands: 

        a) Install-Module D365fo.tools, and accept all the prompts 

        b) New-D365EntraIntegration -ClientId <client-id> (client-id is the app registration id that was created) 

   5) The above command will save a certificate, which then needs to be uploaded to the app registration that was created 

 






Friday, July 28, 2023

XDS in action: How to restraint access to data based on the current user employee

Extensible Data Security (XDS) policies allow restraint access to D365FO data in a very flexible way.

Business case: a user can see those purchase orders and their related confirmations only if he or she is Requester. The whole project can basically contain three objects: role, query, and policy.



After creating a specific security role, we need to create a query with HCMWorker table so that it is filtered for the current user.





All constrained tables should be added to the policy by referencing a particular relation (PurchTable, for example, has two relations with HCMWorker table; thus, we need to pick up the required one related to Requester field)







Note: we can create a join expression if a required relation does not exist for a given table.

Once the project built and synchronized, we can assign this new role along with some standard ones to a user. For example sake, I added Ada to my user.





Thursday, April 20, 2023

X++ to run XIRR

I created a small X++ wrapper to call XIRR function from the C# project provided by https://github.com/klearlending/XIRR (Thank you!) 

You can test XIRR function in Excel, btw.

using tmxExcelFinance;
/// <summary>
/// Implements various financial functions
/// </summary>
public final class myFinanceFunction
{
    /// <summary>
    /// Calculates XIRR value
    /// </summary>
    /// <param name = "_cashFlows">List of containers kind [cashflow amount, its date]</param>
    /// <param name = "_decimals">how many decimals to calculate</param>
    /// <param name = "_maxRate">Maximum rate</param>
    /// <returns>XIRR value</returns>
    public static real calculateXIRR(List _cashFlows, int _decimals, real _maxRate = 1000000)
    { 
         
        var                                 cashFlows       = new System.Collections.Generic.List<CashFlowDates>();
        System.Collections.IEnumerable      cashFlowsI;
        ListEnumerator                      leC             = _cashFlows.getEnumerator();
        System.Double                       dbl;
        System.DateTime                     dt;

        while(leC.moveNext())
        {
            [dbl, dt] = leC.current();
            cashFlows.Add(new CashFlowDates(dbl, dt));
        }
        // convert to iterable
        cashFlowsI = cashFlows;

        return XIRR::Calc(cashFlowsI, _decimals, _maxRate);
    }

}

This is how we can call the wrapper

 public static void calculateXIRRFromInvestmentDetailsLineTmp(RefRecId _projTableRecId, myInvestmentDetailsLineTmp _myInvestmentDetailsLineTmp)
    {
        List listCashFlowsDates = new List(Types::Container);

        myInvestmentDetailsLineTmp myInvestmentDetailsLineTmpLocal;

        myInvestmentDetailsLineTmpLocal.linkPhysicalTableInstance(_myInvestmentDetailsLineTmp);

        while select myInvestmentDetailsLineTmpLocal
        {
            listCashFlowsDates.addEnd([myInvestmentDetailsLineTmpLocal.TransactionCurrencyAmount, DateTimeUtil::newDateTime(myInvestmentDetailsLineTmpLocal.LineDate, 0)]);
        }
        myProjXIRRTable.XIRRValue = myFinanceFunction::calculateXIRR(listCashFlowsDates, 4);
...

Or

 cashFlows1 = new List (Types::Container);
        
        cashFlows1.addEnd([-10000,str2Date("01/01/2008", 123)]);
        cashFlows1.addEnd([2750, str2Date("01/03/2008", 123)]);
        cashFlows1.addEnd([4250, str2Date("30/10/2008", 123)]);
        cashFlows1.addEnd([3250, str2Date("15/02/2009", 123)]);
        cashFlows1.addEnd([2750, str2Date("01/04/2009", 123)]);

res = myFinanceFunction::calculateXIRR(cashFlows1, 10);

Notes about the solution creation.



D365FO project must be <TargetFrameworkVersion>v4.6</TargetFrameworkVersion>


Last remark. I tried to implemented the same algorithm in X++, but I bumped into two limitations.

First, there is a limit for recursion depth - 400 levels; we can flatten it, though. 

Second, real loses required decimals and ends up with division by zero.