# Dynamics 365 integration examples

Dynamics 365 - OData connections for the ERP products and the Dataverse client for customer engagement apps.

Dynamics 365 is a suite of products and each exposes its data a little differently, so the first decision is which API to use:

- **Finance and Operations** and **Business Central** - [outgoing OData connections](https://zato.io/docs/dev/odata/) via `self.odata`
- **Customer engagement apps** - Sales, Customer Service and everything else built on Dataverse - the `DataverseClient`

The [Dynamics 365 integration overview](https://zato.io/integrations/dynamics-365/) describes the suite as a whole, and the [OData system notes](https://zato.io/docs/dev/odata/systems/) cover the connection settings of each ERP product.

## Finance and Operations and Business Central {#finance-and-operations-and-business-central}

Both expose OData APIs, so a connection configured once in the Dashboard gives services full read and write access with paging and OAuth2 token handling done for you:

```python
# -*- coding: utf-8 -*-

from zato.server.service import Service

class GetCustomers(Service):
    name = 'erp.customer.get-list'

    def handle(self):

        conn = self.odata['Business Central']

        # Companies come first in Business Central
        companies = conn.read('companies')
        company = companies[0]
        company_id = company['id']

        # Ten most recent customers of that company
        customers = conn.read(f'companies({company_id})/customers', top=10)

        for customer in customers:
            number = customer['number']
            display_name = customer['displayName']
            self.logger.info(f'{number} - {display_name}')
```

The [OData query chapter](https://zato.io/docs/dev/odata/query/) covers filtering, expansion, counting and writes, including the ETag-based concurrency that Business Central enforces on updates.

## Dataverse {#dataverse}

Dataverse tables hold the data of the customer engagement apps and use their own Web API rather than plain OData connections. The `DataverseClient` wraps it, with MSAL token acquisition included:

```python
# -*- coding: utf-8 -*-

from zato.server.service import DataverseClient, Service

class GetAccounts(Service):
    name = 'crm.account.get-list'

    def handle(self):

        # Credentials are read from a config file
        config = self.config.dataverse.api

        client = DataverseClient(
            tenant_id=config.tenant_id,
            client_id=config.client_id,
            client_secret=config.client_secret,
            org_url=config.org_url,
        )

        # Read accounts, filtered server-side
        response = client.get('accounts?$filter=revenue gt 1000000&$select=name,revenue')

        for account in response['value']:
            name = account['name']
            revenue = account['revenue']
            self.logger.info(f'{name} - {revenue}')
```

The full CRUD range is `get`, `post`, `patch` and `delete`, each taking an entity path:

```python
# Create an account
created = client.post('accounts', {'name': 'Northwind Traders'})

# Update it
client.patch(f'accounts({account_id})', {'telephone1': '+1 555 0100'})

# Delete it
client.delete(f'accounts({account_id})')
```

The entity path is the table's plural logical name, not the display name from the PowerApps UI, so the table shown as "Account" is addressed as `accounts`. The [Dataverse in Python](https://zato.io/blog/dataverse-in-python.html) guide covers a complete scenario, including where the `org_url` and Entra application details come from.

The `config.dataverse.api` values above live in an ordinary [configuration file](https://zato.io/docs/dev/examples/config.html) called `dataverse.ini` in your project's `config/user-conf` directory - the file name and stanza become the `self.config.dataverse.api` path:

```ini
[api]
tenant_id = 221de69a-602d-4a0b-a0a4-1ff2a3943e9f
client_id = 17aaa657-557c-4b18-95c3-71d742fbc6a3
client_secret = MjsrO1zc0.WEV5unJCS5vLa1
org_url = https://org123456.api.crm4.dynamics.com
```

## Business Central through a Microsoft 365 connection {#business-central-through-a-microsoft-365-connection}

Business Central's API is also reachable through the same Microsoft Graph credentials that a [Microsoft 365 connection](https://zato.io/tutorials/microsoft/365.html) holds, which matters when one connection should serve both document workflows and ERP reads. The `BusinessCentralAdapter` base class implements the pattern - a subclass names the connection, the URL and the endpoint, and placeholders are filled in from the request:

```python
# -*- coding: utf-8 -*-

from zato.server.service import BusinessCentralAdapter

class GetCustomerLedger(BusinessCentralAdapter):
    name = 'erp.bc.get-customer-ledger'

    conn_name = 'My MS365 Connection'
    base_url = 'https://api.businesscentral.dynamics.com/v2.0/{tenant_id}/Production/api/v2.0'
    endpoint = 'companies({company_id})/customerPayments'
```

Each `{placeholder}` resolves from the service's input payload or from configuration files, and the adapter returns the decoded response, raising on any non-200 reply. It is read-only by design - writes to Business Central go through the OData connection shown earlier.

## Testing {#testing}

Unit tests mock the OData responses with the [OData test support](https://zato.io/docs/api-testing/odata.html), including an in-process test server that simulates Dynamics 365 authentication and validation behavior.
