Codeunit 7201 CDS Integration Impl., source in 29

Source29

src/Layers/W1/BaseApp/Integration/Dataverse/CDSIntegrationImpl.Codeunit.al5384 lines, Copyright (c) Microsoft Corporation. MIT

// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
namespace Microsoft.Integration.Dataverse;

using Microsoft.CRM.Outlook;
using Microsoft.CRM.Team;
using Microsoft.Finance.Currency;
using Microsoft.Finance.GeneralLedger.Setup;
using Microsoft.Integration.D365Sales;
using Microsoft.Integration.SyncEngine;
using Microsoft.Utilities;
using System;
using System.Azure.Identity;
using System.Azure.KeyVault;
using System.Environment;
using System.Environment.Configuration;
using System.Globalization;
using System.Media;
using System.Reflection;
using System.Security.AccessControl;
using System.Security.Authentication;
using System.Threading;
using System.Utilities;

codeunit 7201 "CDS Integration Impl."
{
    SingleInstance = true;

    var
        CDSIntegrationMgt: Codeunit "CDS Integration Mgt.";
        EnvironmentInfo: Codeunit "Environment Information";
        CachedCompanyIdFieldNo: Dictionary of [Integer, Integer];
        CachedOwnerIdFieldNo: Dictionary of [Integer, Integer];
        CachedOwnerTypeFieldNo: Dictionary of [Integer, Integer];
        CachedOwningTeamCheckWithoutBusinessUnit: Dictionary of [Guid, Boolean];
        CachedOwningTeamCheckWithBusinessUnit: Dictionary of [Guid, Boolean];
        CachedOwningUserCheckWithoutBusinessUnit: Dictionary of [Guid, Boolean];
        CachedOwningUserCheckWithBusinessUnit: Dictionary of [Guid, Boolean];
        CachedCompanyId: Guid;
        CachedDefaultOwningTeamId: Guid;
        CachedOwningBusinessUnitId: Guid;
        AreCompanyValuesCached: Boolean;
        CategoryTok: Label 'AL Dataverse Integration', Locked = true;
        TimeoutTxt: Label 'timeout', Locked = true;
        RetryAfterTimeoutErr: Label 'The operation timed out. Try again.\\%1', Comment = '%1 - exception message ';
        IntegrationNotConfiguredTxt: Label 'Integration is not configured.', Locked = true;
        IntegrationDisabledTxt: Label 'Integration is disabled.', Locked = true;
        BusinessEventsDisabledTxt: Label 'Business Events are disabled.', Locked = true;
        NoPermissionsTxt: Label 'No permissions.', Locked = true;
        RebuildCouplingTableQst: Label 'You should run this action only if you have migrated your Business Central installation from version 2019 Wave 1 (version 14) to the Cloud.\To rebuild the coupling table you must upload a per-tenant extension to this Business Central environment.\Do you want to navigate to the location where the extension file is uploaded?';
        RebuildCouplingTableExtensionLinkTxt: Label 'https://go.microsoft.com/fwlink/?linkid=2245902', Locked = true;
        UpdateSynchronizationSetupTxt: Label 'Update synchronization setup.', Locked = true;
        SynchronizationSetupUpdatedTxt: Label 'Synchronization setup has been updated.', Locked = true;
        UpdateBusinessEventsSetupTxt: Label 'Update business events setup.', Locked = true;
        BusinessEventsSetupUpdatedTxt: Label 'Business events setup has been updated.', Locked = true;
        ConnectionFailureTxt: Label 'Connection failure.', Locked = true;
        OwnerSetTxt: Label 'Owner has been set.', Locked = true;
        UnsupportedOwnerTypeTxt: Label 'Unsupported owner type.', Locked = true;
        EntityHasNoCompanyIdFieldTxt: Label 'Entity has no CompanyId field.', Locked = true;
        CompanyIdSetTxt: Label 'CompanyId has been set.', Locked = true;
        CompanyIdCheckedTxt: Label 'CompanyId check is succeed.', Locked = true;
        InitializeCompanyCacheTxt: Label 'Initialize company cache.', Locked = true;
        SetCachedOwningTeamCheckTxt: Label 'Set cache for team check. Team: %1, skip business unit check: %2.', Locked = true;
        SetCachedOwningUserCheckTxt: Label 'Set cache for user check. User: %1, skip business unit check: %2.', Locked = true;
        ClearCacheTxt: Label 'Clear cache.', Locked = true;
        CannotFindCompanyIdFieldErr: Label 'There is no CompanyId field in table %1 %2.', Comment = '%1 = table ID, %2 = table name';
        CompanyIdDiffersFromExpectedTxt: Label 'CompanyId differs from the expected one.', Locked = true;
        SetDefaultOwningTeamTxt: Label 'Set default owning team.', Locked = true;
        TeamNotFoundTxt: Label 'Team has not been found.', Locked = true;
        UserNotFoundTxt: Label 'User has not been found.', Locked = true;
        UpdateUserNameTxt: Label 'Update integration user name.', Locked = true;
        UpdateUserEmailTxt: Label 'Update integration user email.', Locked = true;
        UserNameNotUpdatedTxt: Label 'Integration user name has not been updated.', Locked = true;
        UserEmailNotUpdatedTxt: Label 'Integration user email has not been updated.', Locked = true;
        CannotUpdateUserNameAndEmailTxt: Label 'Cannot update integration user name and email.', Locked = true;
        CoupledUsersNotFoundTxt: Label 'Coupled users have not been found.', Locked = true;
        BusinessUnitNotFoundTxt: Label 'Business unit has not been found.', Locked = true;
        BusinessUnitMismatchTxt: Label 'Business unit in BC does not match business unit in Dataverse.', Locked = true;
        CompanyNotFoundTxt: Label 'Company has not been found.', Locked = true;
        OrganizationNotFoundTxt: Label 'Organization has not been found.', Locked = true;
        CurrencyNotFoundTxt: Label 'Organization has not been found.', Locked = true;
        GLSetupNotFoundTxt: Label 'GL setup has not been found.', Locked = true;
        IntegrationRoleNotFoundTxt: Label 'Integration role has not been found.', Locked = true;
        IntegrationRoleNotAssignedToTeamTxt: Label 'Integration role is not assigned to team.', Locked = true;
        RoleNotFoundForBusinessUnitTxt: Label 'Integration role is not found for business unit.', Locked = true;
        TeamBusinessUnitDiffersFromSelectedTxt: Label 'Team business unit differs from the selected one.', Locked = true;
        CannotAssignRoleToUserTxt: Label 'Cannot assign role to user.', Locked = true;
        CannotAssignRoleToTeamTxt: Label 'Cannot assign role to team.', Locked = true;
        CannotAddUserToTeamTxt: Label 'Cannot add user to team.', Locked = true;
        ConnectionRequiredFieldsTxt: Label 'A URL, user name and password are required.', Locked = true;
        Office365ConnectionRequiredFieldsTxt: Label 'A URL is required.', Locked = true;
        ConnectionRequiredFieldsMismatchTxt: Label 'The URL, user name, password, and authentication type must be the same on the Dataverse Connection Setup and Microsoft Dynamics 365 Connection Setup pages.', Locked = true;
        IgnoredAdminCredentialsTxt: Label 'Ignored administrator credentials.', Locked = true;
        InvalidAdminCredentialsTxt: Label 'Invalid administrator credentials.', Locked = true;
        InvalidUserCredentialsTxt: Label 'Invalid user credentials.', Locked = true;
        ConfigureSolutionTxt: Label 'Import and configure integration solution.', Locked = true;
        ImportSolutionTxt: Label 'Import integration solution.', Locked = true;
        SolutionConfiguredTxt: Label 'Integration solution has been imported and configured.', Locked = true;
        SolutionNotInstalledTxt: Label 'Integration solution is not installed.', Locked = true;
        SolutionInstalledTxt: Label 'Integration solution is installed.', Locked = true;
        ConnectionNotRegisteredTxt: Label 'Connection is not registered.', Locked = true;
        IntegrationRequirementsMetTxt: Label 'Integration requirements are met.', Locked = true;
        DefaultOwningTeamSetTxt: Label 'Default owning team has been set.', Locked = true;
        CannotSetDefaultOwningTeamTxt: Label 'Cannot set default owning team.', Locked = true;
        BusinessUnitFixedTxt: Label 'Business unit has been fixed.', Locked = true;
        ConnectionTestSucceedTxt: Label 'Connection test succeed.', Locked = true;
        ConnectionTestFailedTxt: Label 'Connection test failed.', Locked = true;
        IntegrationRequirementsNotMetTxt: Label 'Integration requirements are not met.', Locked = true;
        SolutionRequirementsNotMetTxt: Label 'Integration solution requirements are not met.', Locked = true;
        IntegrationUserRequirementsNotMetTxt: Label 'Integration user requirements are not met.', Locked = true;
        OwningTeamRequirementsNotMetTxt: Label 'Owning team requirements are not met.', Locked = true;
        InvalidSolutionVersionTxt: Label 'Solution version is invalid.', Locked = true;
        OwnerDiffersFromExpectedTxt: Label 'Owner differs from the expected one.', Locked = true;
        SynchronizeCompanyTxt: Label 'Synchronize company entity.', Locked = true;
        CompanySynchronizedTxt: Label 'Company has been synchronized.', Locked = true;
        PreviousIntegrationUserRolesAddedTxt: Label 'Previous integration user security roles added to current integration user.', Locked = true;
        RoleAssignedToTeamTxt: Label 'Role is assigned to team.', Locked = true;
        RoleAssignedToUserTxt: Label 'Role is assigned to user.', Locked = true;
        UserAddedToTeamTxt: Label 'User is added to team.', Locked = true;
        CannotCreateBusinessUnitTxt: Label 'Cannot create business unit.', Locked = true;
        BusinessUnitCreatedTxt: Label 'Business unit has been created.', Locked = true;
        CannotCreateTeamTxt: Label 'Cannot create team.', Locked = true;
        TeamCreatedTxt: Label 'Team has been created.', Locked = true;
        CannotCreateCompanyTxt: Label 'Cannot create company.', Locked = true;
        CompanyCreatedTxt: Label 'Company has been created.', Locked = true;
        AddCoupledUsersToTeamTxt: Label 'Add coupled users to the default owning team.', Locked = true;
        AddingCoupledUsersToTeamMsg: Label 'Adding coupled users to the default owning team.\#1##############################', Comment = '#1 place holder for ProcessingUserMsg';
        ProcessingUserMsg: Label 'Processing user %1 of %2.', Comment = '%1 - user number, %2 - user count';
        SetUserAsIntegrationUserTxt: Label 'Set user as an integration user.', Locked = true;
        CannotSetUserAsIntegrationUserTxt: Label 'Cannot set user as an integration user.', Locked = true;
        UserAlreadySetAsIntegrationUserTxt: Label 'User has already been set as an integration user.', Locked = true;
        UserSetAsIntegrationUserTxt: Label 'User has been set as an integration user.', Locked = true;
        SetAccessModeToNonInteractiveTxt: Label 'Set the user''s access mode to Non-Interactive.', Locked = true;
        CannotSetAccessModeToNonInteractiveTxt: Label 'Cannot set the user''s access mode to Non-Interactive.', Locked = true;
        AccessModeAlreadySetToNonInteractiveTxt: Label 'The user''s access mode is already set to Non-Interactive.', Locked = true;
        AccessModeSetToNonInteractiveTxt: Label 'The access mode for the user specified for the integration is set to Non-Interactive.', Locked = true;
        AccessModeSetToNonInteractiveMsg: Label 'The access mode for the user specified for the integration is set to Non-Interactive.';
        FindOrCreateIntegrationUserTxt: Label 'Find or create integration user.', Locked = true;
        CreatedIntegrationUserTxt: Label 'Create integration user with app id %1 on %2.', Locked = true;
        FoundNoIntegrationUserTxt: Label 'Found no user with application id %1 on %2. Injecting a new application user.', Locked = true;
        FoundOneIntegrationUserTxt: Label 'Found one user with application id %1 on %2.', Locked = true;
        FoundMoreThanOneIntegrationUserTxt: Label 'Found more than one user with application id %1 on %2.', Locked = true;
        FoundMoreThanOneIntegrationUserErr: Label 'There are two or more users with application id %1 on %2.\\You must make sure that there is only one user with application id %1 on %2.', Comment = '%1 - this is a Guid; %2 - this is a URL';
        FailedToInsertApplicationUserErr: Label 'Inserting application user with application id %1 on %2 has failed.', Comment = '%1 - this is a Guid; %2 - this is a URL';
        FailedToInsertApplicationUserTxt: Label 'Inserting application user with application id %1 on %2 has failed.', Locked = true;
        IntegrationUserFullNameTxt: Label 'Business Central Integration', Locked = true;
        IntegrationUserFirstNameTxt: Label 'Business Central', Locked = true;
        IntegrationUserLastNameTxt: Label 'Integration', Locked = true;
        IntegrationUserPrimaryEmailStartTxt: Label 'john', Locked = true;
        IntegrationUserPrimaryEmailEndTxt: Label '@contoso.com', Locked = true;
        IntegrationUserPrimaryEmailTxt: Label 'john%1@contoso.com', Locked = true;
        CompanyAlreadyExistsTxt: Label 'Company already exists.', Locked = true;
        BusinessUnitAlreadyExistsTxt: Label 'Business unit already exists.', Locked = true;
        TeamAlreadyExistsTxt: Label 'Team already exists.', Locked = true;
        CheckBusinessUnitTxt: Label 'Check business unit.', Locked = true;
        CheckOwningTeamTxt: Label 'Check owning team.', Locked = true;
        CheckTeamRolesTxt: Label 'Check team roles.', Locked = true;
        CheckCompanyTxt: Label 'Check company.', Locked = true;
        CreateCompanyTxt: Label 'Create company.', Locked = true;
        BusinessUnitCoupledTxt: Label 'Busness unit is correctly coupled.', Locked = true;
        SolutionVersionReceivedTxt: Label 'Solution version has been received.', Locked = true;
        CannotGetSolutionVersionTxt: Label 'Cannot get solution version.', Locked = true;
        CurrencyMismatchTxt: Label 'Your local currency code does not match any ISO Currency Code in the Dataverse currency table.', Locked = true;
        UserHasNoRolesTxt: Label 'User has no roles.', Locked = true;
        SystemAdminRoleTxt: Label 'The user is assigned to the system administrator role.', Locked = true;
        NoSystemAdminRoleTxt: Label 'The admin user is not assigned to the System Administrator role.', Locked = true;
        NoSystemCustomizerRoleTxt: Label 'The admin user is not assigned to the System Customizer role.', Locked = true;
        NoIntegrationRoleTxt: Label 'The user is not assigned to the integration role.', Locked = true;
        UserNotLicensedTxt: Label 'The user''s access mode is not Non-Interactive, but the user is not a licensed user.', Locked = true;
        UserNotActiveTxt: Label 'The user account is disabled.', Locked = true;
        NotIntegrationUserTxt: Label 'User is not an integration user.', Locked = true;
        NotNonInteractiveAccessModeTxt: Label 'The user''s access mode is not Non-Interactive.', Locked = true;
        InvalidAccessModeTxt: Label 'The user''s access mode is not Read-Write or Non-Interactive.', Locked = true;
        ClearDisabledReasonTxt: Label 'Clear disabled reason.', Locked = true;
        CannotCreateCompanyErr: Label 'Cannot create company %1.', Comment = '%1 = company name';
        CannotCreateBusinessUnitErr: Label 'Cannot create business unit %1.', Comment = '%1 = business unit name';
        CannotCreateTeamErr: Label 'Cannot create team %1 for business unit %2.', Comment = '%1 = team name, %2 = business unit name';
        CannotSetDefaultOwningTeamErr: Label 'Cannot set default owning team.';
        CannotFindOwnerIdFieldErr: Label 'There is no OwnerId field in table %1 %2.', Comment = '%1 = table ID, %2 = table name';
        CannotFindOwnerTypeFieldErr: Label 'There is no OwnerIdType field in table %1 %2.', Comment = '%1 = table ID, %2 = table name';
        CannotFindOrganizationErr: Label 'Cannot find organization in Dataverse.';
        BaseCurrencyNotFoundErr: Label 'Cannot find base currency in Dataverse.';
        GLSetupNotFoundErr: Label 'Cannot find GL setup.';
        LCYCodeNotFoundErr: Label 'To enable the connection to Dataverse, you must specify the LCY Code in General Ledger Setup.';
        CompanyNotFoundErr: Label 'There is no company with external ID %1 in Dataverse.', Comment = '%1 = company external ID';
        TeamNotFoundErr: Label 'There is no team with ID %1 in Dataverse.', Comment = '%1 = team ID';
        UserNotFoundErr: Label 'There is no user with ID %1 in Dataverse.', Comment = '%1 = system user ID';
        RoleNotFoundErr: Label 'There is no role with ID %1 in Dataverse.', Comment = '%1 = role ID';
        BusinessUnitNotFoundErr: Label 'There is no business unit with ID %1 in Dataverse.', Comment = '%1 = business unit ID';
        BusinessUnitMismatchErr: Label 'Business unit in BC does not match the business unit in Dataverse.';
        OwnerIdTypeErr: Label 'Owner type must be either team or systemuser.';
        OwnerDiffersFromExpectedErr: Label 'Entity owner differs from the expected one.';
        TeamBusinessUnitDiffersFromSelectedErr: Label 'Team business unit differs from the selected one.';
        UserBusinessUnitDiffersFromSelectedErr: Label 'User business unit differs from the selected one.';
        UserDoesNotExistErr: Label 'There is no user with email address %1 in Dataverse. Enter a valid email address.', Comment = '%1 = User email address';
        IntegrationRoleNotFoundErr: Label 'There is no integration role %1 for business unit %2.', Comment = '%1 = role name, %2 = business unit name';
        SolutionFileNotFoundErr: Label 'A file that is required to complete the setup could not be found. Expand the session details at the bottom of this error message and open a support request.';
        IntegrationUserPasswordWrongErr: Label 'Enter valid integration user credentials.';
        AdminUserPasswordWrongErr: Label 'Enter valid administrator credentials.';
        GeneralFailureErr: Label 'The setup failed because of a generic error. You must sign in to the Dataverse organization with an account that has System Administrator role and make sure that another import or deletion of a PowerApps solution is not currently in progress.';
        InvalidUriErr: Label 'The value entered is not a valid URL.';
        MustUseHttpsErr: Label 'The application is set up to support secure connections (HTTPS) to the Dataverse environment only. You cannot use HTTP.';
        ReplaceServerAddressQst: Label 'The URL is not valid. Do you want to replace it with the URL suggested below?\\Entered URL: "%1".\Suggested URL: "%2".', Comment = '%1 and %2 are URLs';
        CDSConnectionURLWrongErr: Label 'The URL is incorrect. Enter the URL for the Dataverse environment.';
        TemporaryConnectionPrefixTok: Label 'TEMP-Dataverse-', Locked = true;
        TestServerAddressTok: Label '@@test@@', Locked = true;
        NewBusinessUnitNameTemplateTok: Label '<New> %1', Comment = '%1 = Business unit name', Locked = true;
        BusinessUnitNameTemplateTok: Label '%1 (%2)', Comment = '%1 = Company name, %2 = Company ID', Locked = true;
        BusinessUnitNameSuffixTok: Label ' (%1)', Comment = '%1 = Company ID', Locked = true;
        TeamNameTemplateTok: Label 'BCI - %1', Comment = '%1 = Business unit name', Locked = true;
        OAuthConnectionStringFormatTxt: Label 'Url=%1; AccessToken=%2; ProxyVersion=%3; %4', Locked = true;
        ClientSecretConnectionStringFormatTxt: Label '%1; Url=%2; ClientId=%3; ClientSecret=%4; ProxyVersion=%5', Locked = true;
        CertificateConnectionStringFormatTxt: Label '%1; Url=%2; ClientId=%3; Certificate=%4; ProxyVersion=%5', Locked = true;
        ClientSecretAuthTxt: Label 'AuthType=ClientSecret', Locked = true;
        CertificateAuthTxt: Label 'AuthType=Certificate', Locked = true;
        ClientSecretTok: Label '{CLIENTSECRET}', Locked = true;
        CertificateTok: Label '{CERTIFICATE}', Locked = true;
        ClientIdTok: Label '{CLIENTID}', Locked = true;
        ConnectionStringFormatTok: Label 'Url=%1; UserName=%2; Password=%3; ProxyVersion=%4; %5', Locked = true;
        ConnectionDisabledNotificationMsg: Label 'Connection to Dataverse is broken and that it has been disabled due to an error: %1', Comment = '%1 = Error text received from Dataverse';
        MultipleCompaniesNotificationMsg: Label 'There are multiple Business Central companies that are connected to this Dataverse environment. Check the checkbox ''Multi-Company Synchronization Enabled'' on each table mapping to optimize its filters for multi-company synchronization.';
        MultipleCompaniesNotificationNameTxt: Label 'Suggest to optimize the setup for connecting multiple companies to one Dataverse environment.';
        MultipleCompaniesNotificationDescriptionTxt: Label 'Suggests to open Integration Table Mappings and set up multi-company support for each table mapping.';
        OpenIntegrationTableMappingsMsg: Label 'Open Integration Table Mappings';
        LearnMoreTxt: Label 'Learn more';
        MultipleCompanySynchHelpLinkTxt: Label 'https://go.microsoft.com/fwlink/?linkid=2237688', Locked = true;
        CDSConnectionSetupTitleTxt: Label 'Set up a connection to Dataverse';
        CDSConnectionSetupShortTitleTxt: Label 'Connect to Dataverse';
        CDSConnectionSetupHelpTxt: Label 'https://go.microsoft.com/fwlink/?linkid=2115257', Locked = true;
        CDSConnectionSetupDescriptionTxt: Label 'Connect to Dataverse for better insights across business applications. Data will flow between the apps for better productivity.', Comment = 'Dataverse is the name of a Microsoft Service and should not be translated';
        BaseIntegrationSolutionNotInstalledErr: Label 'Base Dataverse integration solution %1 is not installed.', Comment = '%1 = Dataverse solution name';
        SolutionVersionErr: Label 'Version of the base Dataverse integration solution %1 is not the last one.', Comment = '%1 = solution version';
        SystemAdminErr: Label 'User %1 has the %2 role on server %3.\\You must choose a user that does not have the %2 role.', Comment = '%1 = user name, %2 = security role name, %3 = server address';
        UserRolesErr: Label 'User %1 does not have the required roles on server %3.\\You must choose a user that has the role %2.', Comment = '%1 = user name, %2 = security role name, %3 = server address';
        TeamRolesErr: Label 'Team %1 for business unit %2 does not have the required role %3.', Comment = '%1 = team name, %2 = business unit name, %3 = security role name';
        CannotAssignRoleToTeamErr: Label 'Cannot assign role %3 to team %1 for business unit %2.', Comment = '%1 = team name, %2 = business unit name, %3 = security role name';
        CannotAssignRoleToIntegrationUserErr: Label 'Cannot assign the integration role to the integration user.';
        UserNotLicensedErr: Label 'User %1 with access mode %3 is not licensed on server %2.', Comment = '%1 = user name, %2 = server address, %3 = access mode';
        UserNotActiveErr: Label 'User %1 is disabled on server %2.', Comment = '%1 = user name, %2 = server address';
        NotIntegrationUserErr: Label 'User %1 is not an integration user on server %2.', Comment = '%1 = user name, %2 = server address';
        NotNonInteractiveAccessModeErr: Label 'User %1 has invalid access mode %3 on server %2. Valid access mode is Non-Interactive.', Comment = '%1 = user name, %2 = server address, %3 = actual access mode';
        InvalidAccessModeErr: Label 'User %1 has invalid access mode %3 on server %2. Valid access modes are Read-Write or Non-Interactive.', Comment = '%1 = user name, %2 = server address, %3 = actual access mode';
        UserHasNoRolesErr: Label 'User %1 has no user roles assigned on server %2.', Comment = '%1 = user name, %2 = server address';
        NoSystemAdminRoleErr: Label 'Admin user %1 is not assigned to the System Administrator role on server %2.', Comment = '%1 = user name, %2 = server address';
        ConnectionRequiredFieldsErr: Label 'A URL, user name and password are required.';
        Office365ConnectionRequiredFieldsErr: Label 'A URL is required.';
        ConnectionRequiredFieldsMismatchErr: Label 'The values of the Server Address, User Name, User Password and Authentication Type fields must match the corresponding field values on the Microsoft Dynamics 365 Connection Setup page.';
        ConnectionStringPwdPlaceHolderMissingErr: Label 'The connection string must include the password placeholder {PASSWORD}.';
        UserNameMustIncludeDomainErr: Label 'The user name must include the domain when the authentication type is set to Active Directory.';
        UserNameMustBeEmailErr: Label 'The user name must be a valid email address when the authentication type is set to Office 365.';
        LCYMustMatchBaseCurrencyErr: Label 'Your local currency code %1 does not match any ISO Currency Code in the Dataverse currency table. To continue, make sure that the local currency code in General Ledger Setup complies with the ISO standard and create a currency in Dataverse currency table that uses it as ISO Currency Code.', Comment = '%1 - ISO currency code';
        BaseCurrencyMustExistInBCErr: Label 'The base currency %1 of the Dataverse environment must exist in Business Central and it must have an exchange rate.', Comment = '%1 - ISO currency code';
        UserSetupTxt: Label 'User Dataverse Setup';
        CannotResolveUserFromConnectionSetupErr: Label 'The user that is specified in the Dataverse Connection Setup does not exist.';
        MissingUsernameTok: Label '{USER}', Locked = true;
        MissingPasswordTok: Label '{PASSWORD}', Locked = true;
        SystemAdminRoleTemplateIdTxt: Label '627090ff-40a3-4053-8790-584edc5be201', Locked = true;
        SystemCustomizerRoleTemplateIdTxt: Label '119f245c-3cc8-4b62-b31c-d1a046ced15d', Locked = true;
        IntegrationRoleIdTxt: Label 'a2b18661-9ff5-e911-a812-000d3a0b9028', Locked = true;
        VirtualTablesIntegrationRoleIdTxt: Label 'd7dbad50-c0f6-4b30-81cf-d2158f1e7c06', Locked = true;
        ErrorNotificationIdTxt: Label '5e9ed8ec-dc7d-42b5-b7fc-da8c08cea60f', Locked = true;
        ConnectionDisabledNotificationIdTxt: Label 'db1b4430-99b7-48c4-94ba-0e4975353134', Locked = true;
        MultipleCompaniesNotificationIdTxt: Label 'e2416988-361b-4ad9-9409-8cfe161ce2f0', Locked = true;
        HideNotificationTxt: Label 'Don''t show again';
        ConnectionDefaultNameTok: Label 'Dataverse', Locked = true;
        BaseSolutionUniqueNameTxt: Label 'bcbi_CdsBaseIntegration', Locked = true;
        BaseSolutionDisplayNameTxt: Label 'Business Central Dataverse Base Integration', Locked = true;
        TemporaryConnectionName: Text;
        CDSConnectionClientIdAKVSecretNameLbl: Label 'globaldisco-clientid', Locked = true;
        CDSConnectionFirstPartyAppIdAKVSecretNameLbl: Label 'bctocdsappid', Locked = true;
        CDSConnectionClientSecretAKVSecretNameLbl: Label 'globaldisco-clientsecret', Locked = true;
        CDSConnectionFirstPartyAppCertificateNameAKVSecretNameLbl: Label 'bctocdsappcertificatename', Locked = true;
        MissingClientIdOrSecretTelemetryTxt: Label 'The client id or client secret have not been initialized. This indicates that the tenant is not embed ISV. Other tenants will use cert auth instead.', Locked = true;
        MissingFirstPartyappIdOrCertificateTelemetryTxt: Label 'The first-party app id or certificate have not been initialized.', Locked = true;
        MissingClientIdOrSecretErr: Label 'The client id or client secret have not been initialized.';
        MissingClientIdOrSecretOnPremErr: Label 'You must register an Microsoft Entra application that will be used to connect to the Dataverse environment and specify the application id, secret and redirect URL in the Dataverse Connection Setup page.', Comment = 'Dataverse and Microsoft Entra are names of a Microsoft service and a Microsoft Azure resource and should not be translated.';
        AuthTokenOrCodeNotReceivedErr: Label 'No access token or authorization error code received.', Locked = true;
        AccessTokenNotReceivedErr: Label 'Failed to acquire an access token for %1.', Comment = '%1 URL to the Dataverse environment.';
        GuiNotAllowedTxt: Label 'GUI not allowed, so acquiring the auth code through the interactive experience is not possible', Locked = true;
        FixPermissionsUrlTxt: Label 'https://go.microsoft.com/fwlink/?linkid=2206515', Locked = true;
        InsufficientPriviegesTxt: Label 'The Dataverse user has insufficient privileges to perform this task. Navigate to this link to read about how to add privileges to the user: %1', Comment = '%1 A URL';
        AttemptingAuthCodeTokenWithCertTxt: Label 'Attempting to acquire a CDS access token via authorization code flow with a SNI certificate', Locked = true;
        AttemptingClientCredentialsTokenWithCertTxt: Label 'Attempting to acquire a CDS access token via client credentials flow with a SNI certificate', Locked = true;
        AttemptingAuthCodeTokenFromCacheWithCertTxt: Label 'Attempting to acquire a CDS access token via authorization code flow from cache, with a SNI certificate', Locked = true;
        AttemptingClientCredentialsTokenFromCacheWithCertTxt: Label 'Attempting to acquire a CDS access token via client credentials flow from cache, with a SNI certificate', Locked = true;
        AttemptingAuthCodeTokenWithClientSecretTxt: Label 'Attempting to acquire a CDS access token via authorization code flow with a client secret', Locked = true;
        AttemptingClientCredentialsTokenWithClientSecretTxt: Label 'Attempting to acquire a CDS access token via client credentials flow with a client secret', Locked = true;
        AttemptingAuthCodeTokenFromCacheWithClientSecretTxt: Label 'Attempting to acquire a CDS access token via authorization code flow from cache, with a client secret', Locked = true;
        AttemptingClientCredentialsTokenFromCacheWithClientSecretTxt: Label 'Attempting to acquire a CDS access token via client credentials flow from cache, with a client secret', Locked = true;
        SuccessfulJITProvisioningTelemetryMsg: Label 'Service principal successfully provisioned for tenant.', Locked = true;
        ConnectionStringEmptyTxt: Label 'Connection string is is empty.', Locked = true;
        VTEnableAppTxt: Label 'Enable Entra app: %1', Locked = true;
        VTConfigureNonProdTxt: Label 'Configure virtual tables in non-production.', Locked = true;
        VTEmptyTenantIdTxt: Label 'Tetant Id is empty.', Locked = true;
        VTEmptyAadUserIdTxt: Label 'User Id is empty.', Locked = true;
        VTRegisterConnectionTxt: Label 'Registering connection.', Locked = true;
        VTConnectionRegisteredTxt: Label 'Connection is registered.', Locked = true;
        VTConfigureProdTxt: Label 'Configure virtual tables in production.', Locked = true;
        VTConfigFoundTxt: Label 'Virtual tables configuration is found.', Locked = true;
        VTConfigNotFoundTxt: Label 'Virtual tables configuration is not found.', Locked = true;
        VTConfigureTxt: Label 'Configure virtual tables.', Locked = true;
        VTCannotConfigureTxt: Label 'Cannot configure virtual tables.', Locked = true;
        VTAlreadyConfiguredTxt: Label 'Virtual tables are already configured.', Locked = true;
        VTAlreadyConfiguredQst: Label 'Virtual tables are already configured.\\Do you want to overwrite the connection parameters with the new values?';
        VTDiffTemplateTxt: Label '%1: %2 -> %3', Comment = '%1 - field caption, %2 - current value, %3 - new value';
        VTDiffFoundTxt: Label 'The %1 value stored in the virtual tables config differs from the new value.', Locked = true;
        VTConfiguredTxt: Label 'Virtual Tables have been configured.', Locked = true;
        VTConfigNameTxt: Label 'Business Central', Locked = true;
        VTSolutionPrefixTxt: Label 'dyn365bc', Locked = true;
        VTSolutionNameTxt: Label 'MicrosoftBusinessCentralERPVE', Locked = true;
        VTCompanyNotFoundTxt: Label 'Company has not been found.', Locked = true;
        VTDeleteCompanyTxt: Label 'Delete existing companies with the same company code.', Locked = true;
        VTInsertConfigTxt: Label 'Create the virtual tables config.', Locked = true;
        VTModifyConfigTxt: Label 'Update the virtual tables config.', Locked = true;
        VTResetAuthHeaderTxt: Label 'Reset the authorization header in the virtual tables config.', Locked = true;
        VTSetEnvironmentTxt: Label 'Set environment in the virtual tables config.', Locked = true;
        VTSetTargetHostTxt: Label 'Set target host in the virtual tables config.', Locked = true;
        VTSetCompanyIdTxt: Label 'Set company id in the virtual tables config.', Locked = true;
        VTSetTenantIdTxt: Label 'Set tenant id in the virtual tables config.', Locked = true;
        VTSetAadUserIdTxt: Label 'Set user id in the virtual tables config.', Locked = true;
        VTAppNotInstalledTxt: Label 'The Business Central Virtual Table app is not installed in Dataverse. You must install the app from Microsoft AppSource before you can enable business events.';
        VTAppSourceLinkTxt: Label 'https://appsource.microsoft.com/%1/product/dynamics-365/microsoftdynsmb.businesscentral_virtualentity', Locked = true;
        CRMEntityUrlTemplateTxt: Label '%1/main.aspx?pagetype=entityrecord&etn=%2&id=%3', Locked = true;
        CRMEntityWithAppUrlTemplateTxt: Label '%1/main.aspx?appname=%2&pagetype=entityrecord&etn=%3&id=%4', Locked = true;
        CRMEntityListUrlTemplateTxt: Label '%1/main.aspx?pagetype=entitylist&etn=%2&navbar=on&cmdbar=true', Locked = true;
        CRMEntityListWithAppUrlTemplateTxt: Label '%1/main.aspx?appname=%2&pagetype=entitylist&etn=%3&navbar=on&cmdbar=true', Locked = true;
        UsingCustomIntegrationUsernameEmailTxt: Label 'Dataverse setup is using custom integration username email, specified via event subscriber.', Locked = true;
        EnableCDSVirtualTablesJobQueueDescriptionTxt: Label 'Enabling %1 Dataverse virtual table', Comment = '%1 - virtual table name';
        EnableCDSVirtualTablesJobQueueCategoryTxt: Label 'VIRTUAL T';
        VirtualTableAppNameTxt: Label 'dyn365bc_BusinessCentralConfiguration', Locked = true;
        SyntheticRelationEntityNameTxt: Label 'dyn365bc_syntheticrelation', Locked = true;
        InstallVirtualTablesAppLbl: Label 'Install Virtual Tables App';
        RetrievingEntityMetadataTxt: Label 'Retrieving entity metadata for table %1.', Locked = true, Comment = '%1 - table external name';
        RetrievingEntityFieldsTxt: Label 'Retrieving entity fields for table %1.', Locked = true, Comment = '%1 - table external name';

    [Scope('OnPrem')]
    procedure GetBaseSolutionUniqueName(): Text
    begin
        exit(BaseSolutionUniqueNameTxt);
    end;

    [Scope('OnPrem')]
    procedure GetBaseSolutionDisplayName(): Text
    begin
        exit(BaseSolutionDisplayNameTxt);
    end;

    [Scope('OnPrem')]
    procedure IsConnectionActive(): Boolean
    begin
        exit(IsConnectionActive(GetConnectionDefaultName()));
    end;

    [Scope('OnPrem')]
    procedure IsConnectionActive(ConnectionName: Text): Boolean
    var
        CDSConnectionSetup: Record "CDS Connection Setup";
    begin
        if not CDSConnectionSetup.Get() then
            exit(false);

        exit(IsConnectionActive(CDSConnectionSetup, ConnectionName));
    end;

    local procedure IsConnectionActive(var CDSConnectionSetup: Record "CDS Connection Setup"; ConnectionName: Text): Boolean
    var
        ActiveConnectionName: Text;
    begin
        if not CDSConnectionSetup."Is Enabled" then
            exit(false);

        if not HasTableConnection(TABLECONNECTIONTYPE::CRM, ConnectionName) then
            exit(false);

        ActiveConnectionName := GetDefaultTableConnection(TABLECONNECTIONTYPE::CRM);
        if ConnectionName = ActiveConnectionName then
            exit(true);
        exit(false);
    end;

    [Scope('OnPrem')]
    procedure ActivateConnection(): Boolean
    begin
        exit(ActivateConnection(GetConnectionDefaultName()));
    end;

    [Scope('OnPrem')]
    procedure ActivateConnection(ConnectionName: Text): Boolean
    var
        CDSConnectionSetup: Record "CDS Connection Setup";
    begin
        if not CDSConnectionSetup.Get() then
            exit(false);

        if not CDSConnectionSetup."Is Enabled" then
            exit(false);

        exit(ActivateConnection(CDSConnectionSetup, ConnectionName));
    end;

    local procedure ActivateConnection(var CDSConnectionSetup: Record "CDS Connection Setup"; ConnectionName: Text): Boolean
    var
        ActiveConnectionName: Text;
    begin
        if IsConnectionActive(CDSConnectionSetup, ConnectionName) then
            exit(true);

        if not HasTableConnection(TABLECONNECTIONTYPE::CRM, ConnectionName) then
            exit(false);

        ActiveConnectionName := GetDefaultTableConnection(TABLECONNECTIONTYPE::CRM);
        if ConnectionName = ActiveConnectionName then
            exit(true);

        if not CDSConnectionSetup.IsTemporary() then
            CDSIntegrationMgt.OnBeforeActivateConnection();

        SetDefaultTableConnection(TABLECONNECTIONTYPE::CRM, ConnectionName, false);

        if not CDSConnectionSetup.IsTemporary() then
            CDSIntegrationMgt.OnAfterActivateConnection();

        exit(true);
    end;

    [Scope('OnPrem')]
    procedure HasConnection(): Boolean
    begin
        exit(HasConnection(GetConnectionDefaultName()));
    end;

    local procedure HasConnection(ConnectionName: Text): Boolean
    var
        CDSConnectionSetup: Record "CDS Connection Setup";
    begin
        if not CDSConnectionSetup.Get() then
            exit(false);

        exit(HasConnection(CDSConnectionSetup, ConnectionName));
    end;

    local procedure HasConnection(var CDSConnectionSetup: Record "CDS Connection Setup"; ConnectionName: Text): Boolean
    begin
        if not CDSConnectionSetup."Is Enabled" then
            exit(false);

        if HasTableConnection(TABLECONNECTIONTYPE::CRM, ConnectionName) then
            exit(true);

        exit(false);
    end;

    [Scope('OnPrem')]
    procedure RegisterConnection(): Boolean
    begin
        exit(RegisterConnection(true));
    end;

    [Scope('OnPrem')]
    procedure RegisterConnection(KeepExisting: Boolean): Boolean
    var
        ConnectionName: Text;
    begin
        ConnectionName := GetConnectionDefaultName();
        exit(RegisterConnection(ConnectionName, KeepExisting));
    end;

    [Scope('OnPrem')]
    procedure RegisterConnection(ConnectionName: Text): Boolean
    begin
        exit(RegisterConnection(ConnectionName, false));
    end;

    [Scope('OnPrem')]
    procedure RegisterConnection(ConnectionName: Text; KeepExisting: Boolean): Boolean
    var
        CDSConnectionSetup: Record "CDS Connection Setup";
    begin
        if not CDSConnectionSetup.Get() then
            exit(false);

        if not CDSConnectionSetup."Is Enabled" then
            exit(false);

        exit(RegisterConnection(CDSConnectionSetup, ConnectionName, KeepExisting));
    end;

    [Scope('OnPrem')]
    procedure RegisterConnection(var CDSConnectionSetup: Record "CDS Connection Setup"; KeepExisting: Boolean): Boolean
    var
        ConnectionName: Text;
    begin
        ConnectionName := GetConnectionDefaultName();
        exit(RegisterConnection(CDSConnectionSetup, ConnectionName, KeepExisting));
    end;

    [Scope('OnPrem')]
    procedure RegisterConnection(var CDSConnectionSetup: Record "CDS Connection Setup"; ConnectionName: Text): Boolean
    begin
        exit(RegisterConnection(CDSConnectionSetup, ConnectionName, false));
    end;

    [Scope('OnPrem')]
    procedure RegisterConnection(var CDSConnectionSetup: Record "CDS Connection Setup"; ConnectionName: Text; KeepExisting: Boolean): Boolean
    var
        ConnectionString: SecretText;
        IsTemporary: Boolean;
    begin
        IsTemporary := CDSConnectionSetup.IsTemporary();
        if not IsTemporary then
            if KeepExisting then
                if HasTableConnection(TABLECONNECTIONTYPE::CRM, ConnectionName) then
                    exit(true);

        ConnectionString := GetConnectionStringWithCredentials(CDSConnectionSetup);
        if ConnectionString.IsEmpty() then begin
            Session.LogMessage('0000GES', ConnectionStringEmptyTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit(false);
        end;

        if not UnregisterConnection(ConnectionName) then
            ClearLastError();

        if not IsTemporary then
            CDSIntegrationMgt.OnBeforeRegisterConnection();

        if not TryRegisterTableConnection(ConnectionName, ConnectionString) then
            exit(false);

        if not IsTemporary then
            CDSIntegrationMgt.OnAfterRegisterConnection();

        exit(true);
    end;

    [TryFunction]
    [NonDebuggable]
    local procedure TryRegisterTableConnection(ConnectionName: Text; ConnectionString: SecretText)
    begin
        RegisterTableConnection(TABLECONNECTIONTYPE::CRM, ConnectionName, ConnectionString.Unwrap());
    end;

    [Scope('OnPrem')]
    procedure UnregisterConnection(): Boolean
    begin
        exit(UnregisterConnection(GetConnectionDefaultName()));
    end;

    [TryFunction]
    [Scope('OnPrem')]
    procedure UnregisterConnection(ConnectionName: Text)
    var
        IsTemporary: Boolean;
    begin
        if not HasTableConnection(TableConnectionType::CRM, ConnectionName) then
            exit;

        IsTemporary := ConnectionName.StartsWith(TemporaryConnectionPrefixTok);
        if not IsTemporary then
            CDSIntegrationMgt.OnBeforeUnregisterConnection();
        UnregisterTableConnection(TABLECONNECTIONTYPE::CRM, ConnectionName);
        if not IsTemporary then
            CDSIntegrationMgt.OnAfterUnregisterConnection();

    end;

    [Scope('OnPrem')]
    procedure IsIntegrationEnabled(): Boolean
    var
        CDSConnectionSetup: Record "CDS Connection Setup";
        IsDetailedLoggingEnabled: Boolean;
    begin
        CDSIntegrationMgt.OnGetDetailedLoggingEnabled(IsDetailedLoggingEnabled);

        if not CDSConnectionSetup.ReadPermission() then
            exit(false);

        if not CDSConnectionSetup.Get() then begin
            if IsDetailedLoggingEnabled then
                Session.LogMessage('0000ARI', IntegrationNotConfiguredTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit(false);
        end;

        if not CDSConnectionSetup."Is Enabled" then begin
            if IsDetailedLoggingEnabled then
                Session.LogMessage('0000ARJ', IntegrationDisabledTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit(false);
        end;

        OnAfterIntegrationEnabled();
        exit(true);
    end;

    [Scope('OnPrem')]
    procedure IsBusinessEventsEnabled(): Boolean
    var
        CDSConnectionSetup: Record "CDS Connection Setup";
        IsDetailedLoggingEnabled: Boolean;
    begin
        CDSIntegrationMgt.OnGetDetailedLoggingEnabled(IsDetailedLoggingEnabled);

        if not CDSConnectionSetup.ReadPermission() then
            exit(false);

        if not CDSConnectionSetup.Get() then begin
            if IsDetailedLoggingEnabled then
                Session.LogMessage('0000GIE', IntegrationNotConfiguredTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit(false);
        end;

        if not CDSConnectionSetup."Business Events Enabled" then begin
            if IsDetailedLoggingEnabled then
                Session.LogMessage('0000GIF', BusinessEventsDisabledTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit(false);
        end;

        exit(true);
    end;

    [Scope('OnPrem')]
    procedure IsTeamOwnershipModelSelected(): Boolean
    var
        CDSConnectionSetup: Record "CDS Connection Setup";
    begin
        if IsIntegrationEnabled() then
            if CDSConnectionSetup.Get() then
                exit(CDSConnectionSetup."Ownership Model" = CDSConnectionSetup."Ownership Model"::Team);
        exit(false);
    end;

    [Scope('OnPrem')]
    procedure GetConnectionDefaultName(): Text
    begin
        exit(ConnectionDefaultNameTok);
    end;

    [Scope('OnPrem')]
    procedure IsSolutionInstalled(): Boolean
    begin
        exit(IsSolutionInstalled(GetBaseSolutionUniqueName()));
    end;

    [Scope('OnPrem')]
    procedure IsSolutionInstalled(var CDSConnectionSetup: Record "CDS Connection Setup"): Boolean
    begin
        exit(IsSolutionInstalled(CDSConnectionSetup, GetBaseSolutionUniqueName()));
    end;

    [Scope('OnPrem')]
    procedure IsSolutionInstalled(UniqueName: Text): Boolean
    var
        CDSSolution: Record "CDS Solution";
    begin
        CDSSolution.SetRange(UniqueName, UniqueName);
        if CDSSolution.FindFirst() then
            if CDSSolution.InstalledOn <> 0DT then begin
                Session.LogMessage('0000ARM', SolutionInstalledTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                exit(true);
            end;
        Session.LogMessage('0000ARN', SolutionNotInstalledTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        exit(false);
    end;

    [Scope('OnPrem')]
    procedure IsSolutionInstalled(var CDSConnectionSetup: Record "CDS Connection Setup"; UniqueName: Text): Boolean
    var
        Installed: Boolean;
    begin
        if TryCheckSolutionInstalled(CDSConnectionSetup, UniqueName, Installed) then
            if Installed then begin
                Session.LogMessage('0000ARO', SolutionInstalledTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                exit(true);
            end;
        Session.LogMessage('0000ARP', SolutionNotInstalledTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        exit(false);
    end;

    [TryFunction]
    local procedure TryCheckSolutionInstalled(var CDSConnectionSetup: Record "CDS Connection Setup"; UniqueName: Text; var Installed: Boolean)
    var
        CDSSolution: Record "CDS Solution";
        TempConnectionName: Text;
    begin
        TempConnectionName := GetTempConnectionName();
        RegisterConnection(CDSConnectionSetup, TempConnectionName);
        SetDefaultTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName, true);

        CDSSolution.SetRange(UniqueName, UniqueName);
        if CDSSolution.FindFirst() then
            Installed := CDSSolution.InstalledOn <> 0DT
        else
            Installed := false;

        UnregisterTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName);
    end;

    [Scope('OnPrem')]
    procedure GetSolutionVersion(var Version: Text): Boolean
    begin
        exit(GetSolutionVersion(GetBaseSolutionUniqueName(), Version));
    end;

    [Scope('OnPrem')]
    procedure GetSolutionVersion(var CDSConnectionSetup: Record "CDS Connection Setup"; var Version: Text): Boolean
    begin
        exit(GetSolutionVersion(CDSConnectionSetup, GetBaseSolutionUniqueName(), Version));
    end;

    [Scope('OnPrem')]
    procedure GetSolutionVersion(UniqueName: Text; var Version: Text): Boolean
    var
        CDSSolution: Record "CDS Solution";
    begin
        CDSSolution.SetRange(UniqueName, UniqueName);
        if CDSSolution.FindFirst() then
            if CDSSolution.InstalledOn <> 0DT then begin
                Version := CDSSolution.Version;
                Session.LogMessage('0000ARQ', SolutionVersionReceivedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                exit(true);
            end;
        Version := '';
        Session.LogMessage('0000ARL', CannotGetSolutionVersionTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        exit(false);
    end;

    [Scope('OnPrem')]
    procedure GetSolutionVersion(var CDSConnectionSetup: Record "CDS Connection Setup"; UniqueName: Text; var Version: Text): Boolean
    begin
        if TryGetSolutionVersion(CDSConnectionSetup, UniqueName, Version) then begin
            Session.LogMessage('0000ARR', SolutionVersionReceivedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            if Version <> '' then
                exit(true);
        end;
        Version := '';
        Session.LogMessage('0000ARS', CannotGetSolutionVersionTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        exit(false);
    end;

    [TryFunction]
    local procedure TryGetSolutionVersion(var CDSConnectionSetup: Record "CDS Connection Setup"; UniqueName: Text; var Version: Text)
    var
        CDSSolution: Record "CDS Solution";
        TempConnectionName: Text;
    begin
        TempConnectionName := GetTempConnectionName();
        RegisterConnection(CDSConnectionSetup, TempConnectionName);
        SetDefaultTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName, true);

        Version := '';
        CDSSolution.SetRange(UniqueName, UniqueName);
        if CDSSolution.FindFirst() then
            if CDSSolution.InstalledOn <> 0DT then
                Version := CDSSolution.Version;
        if Version <> '' then
            Session.LogMessage('0000AVR', SolutionNotInstalledTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok)
        else
            Session.LogMessage('0000ART', SolutionNotInstalledTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        UnregisterTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName);
    end;

    internal procedure GetVirtualTablesAppSourceLink(): Text
    var
        TempUserSettingsRecord: Record "User Settings";
        Language: Codeunit Language;
        UserSettings: Codeunit "User Settings";
        LanguageID: Integer;
        CultureName: Text;
    begin
        UserSettings.GetUserSettings(Database.UserSecurityId(), TempUserSettingsRecord);
        LanguageID := TempUserSettingsRecord."Language ID";
        if (LanguageID = 0) then
            LanguageID := 1033; // Default to EN-US
        CultureName := Language.GetCultureName(LanguageID).ToLower();
        exit(Text.StrSubstNo(VTAppSourceLinkTxt, CultureName));
    end;

    internal procedure OpenVirtualTablesApp(ErrorInfo: ErrorInfo)
    begin
        Hyperlink(GetVirtualTablesAppSourceLink());
    end;

    procedure SetupVirtualTables(var CDSConnectionSetup: Record "CDS Connection Setup"; var ConfigId: Guid)
    var
        CrmHelper: DotNet CrmHelper;
        EmptySecretText: SecretText;
    begin
        SetupVirtualTables(CDSConnectionSetup, CrmHelper, EmptySecretText, ConfigId);
    end;

    internal procedure SetupVirtualTables(var CDSConnectionSetup: Record "CDS Connection Setup"; var CrmHelper: DotNet CrmHelper; AccessToken: SecretText; var ConfigId: Guid)
    var
        TempAdminCDSConnectionSetup: Record "CDS Connection Setup" temporary;
        AADApplicationSetup: Codeunit "AAD Application Setup";
        TempConnectionStringWithPlaceholder: Text;
        TempConnectionString: SecretText;
        EmptySecretText: SecretText;
        VTAppNotInstalledErrorInfo: ErrorInfo;
    begin
        Session.LogMessage('0000GBF', VTConfigureTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        if CDSConnectionSetup."Authentication Type" <> CDSConnectionSetup."Authentication Type"::Office365 then begin
            Session.LogMessage('0000GBH', VTCannotConfigureTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            CDSConnectionSetup.TestField("Authentication Type", CDSConnectionSetup."Authentication Type"::Office365);
        end;

        if not CheckConnectionRequiredFields(CDSConnectionSetup, true) then begin
            Session.LogMessage('0000GBI', VTCannotConfigureTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(GetLastErrorText());
        end;

        if AccessToken.IsEmpty() then begin
            // sign in as admin in interactive auth code flow
            GetAccessToken(CDSConnectionSetup."Server Address", true, AccessToken);
            TempConnectionStringWithPlaceholder := StrSubstNo(OAuthConnectionStringFormatTxt, CDSConnectionSetup."Server Address", '%1', CDSConnectionSetup.GetProxyVersion(), GetAuthenticationTypeToken(CDSConnectionSetup));
            TempConnectionString := SecretStrSubstNo(TempConnectionStringWithPlaceholder, AccessToken);
            if not InitializeConnection(CrmHelper, TempConnectionString) then begin
                Session.LogMessage('0000GGP', ConnectionNotRegisteredTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                ProcessConnectionFailures();
            end;
            JITProvisionFirstPartyApp(CrmHelper);
            Sleep(5000);
        end;

        GetTempConnectionSetup(TempAdminCDSConnectionSetup, CDSConnectionSetup, AccessToken);

        if not IsVirtualTablesAppInstalled(TempAdminCDSConnectionSetup) then begin
            Session.LogMessage('0000GBJ', VTCannotConfigureTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            VTAppNotInstalledErrorInfo.Message := VTAppNotInstalledTxt;
            VTAppNotInstalledErrorInfo.AddAction(InstallVirtualTablesAppLbl, Codeunit::"CDS Integration Impl.", 'OpenVirtualTablesApp');
            Error(VTAppNotInstalledErrorInfo);
        end;

        FindOrCreateIntegrationUser(CDSConnectionSetup, '', EmptySecretText, AccessToken);
        AssignVirtualTablesIntegrationRole(CrmHelper, CDSConnectionSetup."User Name");

        EnableAADApp(AADApplicationSetup.GetD365BCForVEAppId());
        EnableAADApp(AADApplicationSetup.GetPowerPagesAnonymousAppId());
        EnableAADApp(AADApplicationSetup.GetPowerPagesAuthenticatedAppId());

        // for at an plugin in Dataverse can connect to BC with the VT app, configure the VT config        
        ConfigureVirtualTables(TempAdminCDSConnectionSetup, ConfigId);
    end;

    local procedure EnableAADApp(AppId: Guid)
    var
        AADApplication: Record "AAD Application";
        IdentityManagement: Codeunit "Identity Management";
        Validate: Boolean;
    begin
        Session.LogMessage('0000GEW', StrSubstNo(VTEnableAppTxt, AppId), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        AADApplication.Get(AppId);
        Validate := EnvironmentInfo.IsSaaSInfrastructure();
        if not Validate then
            Validate := IdentityManagement.IsUserNamePasswordAuthentication();
        if Validate then
            AADApplication.Validate(State, AADApplication.State::Enabled)
        else
            AADApplication.State := AADApplication.State::Enabled;
        AADApplication.Modify();
        Commit();
    end;

    [NonDebuggable]
    local procedure ConfigureVirtualTables(var TempAdminCDSConnectionSetup: Record "CDS Connection Setup"; var ConfigId: Guid)
    var
        Company: Record Company;
        UrlHelper: Codeunit "Url Helper";
        EnvironmentName: Text[100];
        TargetHost: Text[255];
        CompanyId: Guid;
        TenantId: Text[150];
        UserId: Text[80];
        IsPPE: Boolean;
    begin
        Session.LogMessage('0000GBK', VTConfigureTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        Company.Get(CompanyName());
        CompanyId := Company.Id;
        EnvironmentName := CopyStr(EnvironmentInfo.GetEnvironmentName(), 1, MaxStrLen(EnvironmentName));
        TargetHost := CopyStr(GetTargetHost(), 1, MaxStrLen(TargetHost));
        IsPPE := UrlHelper.IsPPE();
        if IsPPE then begin
            Session.LogMessage('0000GEV', VTConfigureNonProdTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            TenantId := CopyStr(GetAadTenantId(), 1, MaxStrLen(TenantId));
            if TenantId = '' then
                Session.LogMessage('0000GEX', VTEmptyTenantIdTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            UserId := GetAadUserId();
            if UserId = '' then
                Session.LogMessage('0000GEY', VTEmptyAadUserIdTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        end else
            Session.LogMessage('0000GEZ', VTConfigureProdTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        CheckVirtualTablesConfig(TempAdminCDSConnectionSetup, EnvironmentName, TargetHost, CompanyId, TenantId, UserId, IsPPE);
        UpdateVirtualTablesConfig(TempAdminCDSConnectionSetup, EnvironmentName, TargetHost, CompanyId, TenantId, UserId, IsPPE, ConfigId);

        Session.LogMessage('0000GBP', VTConfiguredTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
    end;

    [NonDebuggable]
    local procedure CheckVirtualTablesConfig(var TempAdminCDSConnectionSetup: Record "CDS Connection Setup"; EnvironmentName: Text[100]; TargetHost: Text[255]; CompanyId: Guid; TenantId: Text[150]; AadUserId: Text[80]; IsPPE: Boolean)
    var
        CRMBCVirtualTableConfig: Record "CRM BC Virtual Table Config.";
        TempConnectionName: Text;
        ConfigFound: Boolean;
        DiffFound: Boolean;
        DiffMessage: Text;
    begin
        Session.LogMessage('0000GF0', VTRegisterConnectionTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        TempConnectionName := GetTempConnectionName();
        RegisterConnection(TempAdminCDSConnectionSetup, TempConnectionName);
        SetDefaultTableConnection(TableConnectionType::CRM, TempConnectionName, true);
        Session.LogMessage('0000GF1', VTConnectionRegisteredTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        // Table connection is scoped, therefore all manipulations with CDS tables must be placed
        // in this procedure between SetDefaultTableConnection and UnregisterConnection

        CRMBCVirtualTableConfig.SetRange(msdyn_name, VTConfigNameTxt);
        ConfigFound := CRMBCVirtualTableConfig.FindFirst();

        if ConfigFound then begin
            Session.LogMessage('0000GF2', VTConfigFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            if (CRMBCVirtualTableConfig.msdyn_environment <> '') and (CRMBCVirtualTableConfig.msdyn_environment.ToLower() <> EnvironmentName.ToLower()) then begin
                Session.LogMessage('0000GE7', StrSubstNo(VTDiffFoundTxt, CRMBCVirtualTableConfig.FieldCaption(msdyn_environment)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                DiffMessage += '\\' + StrSubstNo(VTDiffTemplateTxt, CRMBCVirtualTableConfig.FieldCaption(msdyn_environment), CRMBCVirtualTableConfig.msdyn_environment, EnvironmentName);
                DiffFound := true;
            end;
            if (CRMBCVirtualTableConfig.msdyn_targethost <> '') and (CRMBCVirtualTableConfig.msdyn_targethost.ToLower().TrimEnd('/') <> TargetHost.ToLower()) then begin
                Session.LogMessage('0000GE8', StrSubstNo(VTDiffFoundTxt, CRMBCVirtualTableConfig.FieldCaption(msdyn_targethost)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                DiffMessage += '\\' + StrSubstNo(VTDiffTemplateTxt, CRMBCVirtualTableConfig.FieldCaption(msdyn_targethost), CRMBCVirtualTableConfig.msdyn_targethost, TargetHost);
                DiffFound := true;
            end;
            if (not IsNullGuid(CRMBCVirtualTableConfig.msdyn_DefaultCompanyId)) and (CRMBCVirtualTableConfig.msdyn_DefaultCompanyId <> CompanyId) then begin
                Session.LogMessage('0000GE9', StrSubstNo(VTDiffFoundTxt, CRMBCVirtualTableConfig.FieldCaption(msdyn_DefaultCompanyId)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                DiffMessage += '\\' + StrSubstNo(VTDiffTemplateTxt, CRMBCVirtualTableConfig.FieldCaption(msdyn_DefaultCompanyId), CRMBCVirtualTableConfig.msdyn_DefaultCompanyId, CompanyId);
                DiffFound := true;
            end;
            if IsPPE then begin
                if (CRMBCVirtualTableConfig.msdyn_tenantid <> '') and (CRMBCVirtualTableConfig.msdyn_tenantid.ToLower() <> TenantId.ToLower()) then begin
                    Session.LogMessage('0000GEA', StrSubstNo(VTDiffFoundTxt, CRMBCVirtualTableConfig.FieldCaption(msdyn_tenantid)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                    DiffMessage += '\\' + StrSubstNo(VTDiffTemplateTxt, CRMBCVirtualTableConfig.FieldCaption(msdyn_tenantid), CRMBCVirtualTableConfig.msdyn_tenantid, TenantId);
                    DiffFound := true;
                end;
                if (CRMBCVirtualTableConfig.msdyn_aadUserId <> '') and (CRMBCVirtualTableConfig.msdyn_aadUserId.ToLower() <> AadUserId.ToLower()) then begin
                    Session.LogMessage('0000GEB', StrSubstNo(VTDiffFoundTxt, CRMBCVirtualTableConfig.FieldCaption(msdyn_aadUserId)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                    DiffMessage += '\\' + StrSubstNo(VTDiffTemplateTxt, CRMBCVirtualTableConfig.FieldCaption(msdyn_aadUserId), CRMBCVirtualTableConfig.msdyn_aadUserId, AadUserId);
                    DiffFound := true;
                end;
            end;
            if DiffFound then begin
                Session.LogMessage('0000GBO', VTAlreadyConfiguredTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                if not Confirm(VTAlreadyConfiguredQst + DiffMessage, false) then
                    Error('');
            end;
        end;

        UnregisterTableConnection(TableConnectionType::CRM, TempConnectionName);
    end;

    [NonDebuggable]
    local procedure UpdateVirtualTablesConfig(var TempAdminCDSConnectionSetup: Record "CDS Connection Setup"; EnvironmentName: Text[100]; TargetHost: Text[255]; CompanyId: Guid; TenantId: Text[150]; AadUserId: Text[80]; IsPPE: Boolean; var ConfigId: Guid)
    var
        CRMBCVirtualTableConfig: Record "CRM BC Virtual Table Config.";
        CRMCompany: Record "CRM Company";
        Company: Record Company;
        TempConnectionName: Text;
        ConfigFound: Boolean;
        CompanyFound: Boolean;
        ModifyConfig: Boolean;
    begin
        Session.LogMessage('0000GMW', VTRegisterConnectionTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        TempConnectionName := GetTempConnectionName();
        RegisterConnection(TempAdminCDSConnectionSetup, TempConnectionName);
        SetDefaultTableConnection(TableConnectionType::CRM, TempConnectionName, true);
        Session.LogMessage('0000GMX', VTConnectionRegisteredTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        // Table connection is scoped, therefore all manipulations with CDS tables must be placed
        // in this procedure between SetDefaultTableConnection and UnregisterConnection

        CRMBCVirtualTableConfig.SetRange(msdyn_name, VTConfigNameTxt);
        ConfigFound := CRMBCVirtualTableConfig.FindFirst();
        CompanyFound := CRMCompany.Get(CompanyId);

        if not ConfigFound then begin
            Session.LogMessage('0000GF3', VTConfigNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            CRMBCVirtualTableConfig.msdyn_name := VTConfigNameTxt;
            CRMBCVirtualTableConfig.msdyn_apimanagedprefix := VTSolutionPrefixTxt;
            CRMBCVirtualTableConfig.msdyn_apimanagedsolutionname := VTSolutionNameTxt;
            Session.LogMessage('0000GEC', VTSetEnvironmentTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            CRMBCVirtualTableConfig.msdyn_environment := EnvironmentName; // when specified then a plugin in Dataverse creates the Company if missing
            Session.LogMessage('0000GED', VTSetTargetHostTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            CRMBCVirtualTableConfig.msdyn_targethost := TargetHost; // mandatory field
            if CompanyFound then begin
                Session.LogMessage('0000GEE', VTSetCompanyIdTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                CRMBCVirtualTableConfig.msdyn_DefaultCompanyId := CompanyId;
            end;
            if IsPPE then begin
                Session.LogMessage('0000GEF', VTSetTenantIdTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                CRMBCVirtualTableConfig.msdyn_tenantid := TenantId;
                Session.LogMessage('0000GEG', VTSetAadUserIdTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                CRMBCVirtualTableConfig.msdyn_aadUserId := AadUserId;
            end;
            Session.LogMessage('0000GBL', VTInsertConfigTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            CRMBCVirtualTableConfig.Insert();
        end else begin
            if CRMBCVirtualTableConfig.msdyn_environment <> EnvironmentName then begin
                Session.LogMessage('0000GBM', VTSetEnvironmentTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                CRMBCVirtualTableConfig.msdyn_environment := EnvironmentName; // when specified then a plugin in Dataverse creates the Company if missing
                ModifyConfig := true;
            end;
            if CRMBCVirtualTableConfig.msdyn_targethost <> TargetHost then begin
                Session.LogMessage('0000GEH', VTSetTargetHostTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                CRMBCVirtualTableConfig.msdyn_targethost := TargetHost; // mandatory field
                ModifyConfig := true;
            end;
            if CompanyFound and (CRMBCVirtualTableConfig.msdyn_DefaultCompanyId <> CompanyId) then begin
                Session.LogMessage('0000GEI', VTSetCompanyIdTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                CRMBCVirtualTableConfig.msdyn_DefaultCompanyId := CompanyId;
                ModifyConfig := true;
            end;
            if IsPPE then begin
                if CRMBCVirtualTableConfig.msdyn_tenantid <> TenantId then begin
                    Session.LogMessage('0000GEJ', VTSetTenantIdTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                    CRMBCVirtualTableConfig.msdyn_tenantid := TenantId;
                    ModifyConfig := true;
                end;
                if CRMBCVirtualTableConfig.msdyn_aadUserId <> AadUserId then begin
                    Session.LogMessage('0000GEK', VTSetAadUserIdTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                    CRMBCVirtualTableConfig.msdyn_aadUserId := AadUserId;
                    ModifyConfig := true;
                end;
            end;
            if ModifyConfig then begin
                Session.LogMessage('0000GEL', VTModifyConfigTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                CRMBCVirtualTableConfig.Modify();
            end;
        end;

        if not CompanyFound then begin
            if not ModifyConfig then begin
                // do fake modify to create cdm_company through a plugin
                Session.LogMessage('0000GH8', VTResetAuthHeaderTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                CRMBCVirtualTableConfig.msdyn_odataauthorizationheader := Format(CreateGuid());
                CRMBCVirtualTableConfig.Modify();
                CRMBCVirtualTableConfig.msdyn_odataauthorizationheader := '';
                CRMBCVirtualTableConfig.Modify();
            end;
            if not CRMCompany.Get(CompanyId) then begin
                Session.LogMessage('0000GBN', VTCompanyNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                CRMCompany.SetRange(cdm_CompanyCode, Company.Name);
                if not CRMCompany.IsEmpty() then begin
                    Session.LogMessage('0000GH9', VTDeleteCompanyTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                    CRMCompany.DeleteAll();
                end;
                CRMCompany.cdm_companyId := CompanyId;
                CRMCompany.cdm_CompanyCode := Company.Name;
                if Company."Display Name" = '' then
                    CRMCompany.cdm_Name := Company.Name
                else
                    CRMCompany.cdm_Name := CopyStr(Company."Display Name", 1, MaxStrLen(CRMCompany.cdm_Name));
                CRMCompany.Insert();
            end;
            if CRMBCVirtualTableConfig.msdyn_DefaultCompanyId <> CompanyId then begin
                Session.LogMessage('0000GEM', VTSetCompanyIdTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                CRMBCVirtualTableConfig.msdyn_DefaultCompanyId := CompanyId;
                Session.LogMessage('0000GEN', VTModifyConfigTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                CRMBCVirtualTableConfig.Modify();
            end;
        end;

        ConfigId := CRMBCVirtualTableConfig.msdyn_businesscentralvirtualentityId;

        UnregisterTableConnection(TableConnectionType::CRM, TempConnectionName);
    end;

    local procedure GetAadTenantId(): Text
    var
        AzureADTenant: Codeunit "Azure AD Tenant";
    begin
        exit(AzureADTenant.GetAadTenantId());
    end;

    local procedure GetAadUserId(): Text[80]
    var
        UserProperty: Record "User Property";
    begin
        if UserProperty.Get(UserSecurityId()) then
            exit(UserProperty."Authentication Object ID");
        exit('');
    end;

    local procedure GetTargetHost(): Text
    var
        BaseIndex: Integer;
        EndBaseUrlIndex: Integer;
        BaseUrl: Text;
        ApiUrl: Text;
        Path: Text;
        EndPathIndex: Integer;
    begin
        ApiUrl := GetUrl(ClientType::Api);
        if StrPos(LowerCase(ApiUrl), 'https://') <> 0 then
            BaseIndex := 9;
        if StrPos(LowerCase(ApiUrl), 'http://') <> 0 then
            BaseIndex := 8;

        BaseUrl := CopyStr(ApiUrl, BaseIndex);
        EndBaseUrlIndex := StrPos(BaseUrl, '/');

        if EndBaseUrlIndex = 0 then
            exit(ApiUrl);

        Path := CopyStr(BaseUrl, EndBaseUrlIndex + 1);

        BaseUrl := CopyStr(BaseUrl, 1, EndBaseUrlIndex - 1);
        if EnvironmentInfo.IsSaaSInfrastructure() then
            exit(CopyStr(ApiUrl, 1, BaseIndex - 1) + BaseUrl);

        EndPathIndex := StrPos(Path, '/');
        if EndPathIndex = 0 then
            exit(CopyStr(ApiUrl, 1, BaseIndex - 1) + BaseUrl);

        Path := CopyStr(Path, 1, EndPathIndex - 1);
        exit(CopyStr(ApiUrl, 1, BaseIndex - 1) + BaseUrl + '/' + Path);
    end;

    local procedure SetUserAsIntegrationUser(var CDSConnectionSetup: Record "CDS Connection Setup"; AdminUserName: Text; AdminPassword: SecretText; AccessToken: SecretText; AdminADDomain: Text)
    var
        CRMSystemuser: Record "CRM Systemuser";
        TempAdminCDSConnectionSetup: Record "CDS Connection Setup" temporary;
        TempConnectionName: Text;
    begin
        Session.LogMessage('0000ARW', SetUserAsIntegrationUserTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        GetTempAdminConnectionSetup(TempAdminCDSConnectionSetup, CDSConnectionSetup, AdminUserName, AdminPassword, AccessToken, AdminADDomain);
        TempConnectionName := GetTempConnectionName();
        RegisterConnection(TempAdminCDSConnectionSetup, TempConnectionName);
        SetDefaultTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName, true);

        // Table connection is scoped, therefore all manipulations with CDS tables must be placed
        // in this procedure between SetDefaultTableConnection and UnregisterConnection

        FilterUser(CDSConnectionSetup, CRMSystemuser);
        if not CRMSystemuser.FindFirst() then begin
            Session.LogMessage('0000ARX', UserNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(CannotResolveUserFromConnectionSetupErr);
        end;
        if (CRMSystemuser.InviteStatusCode <> CRMSystemuser.InviteStatusCode::InvitationAccepted) or
           (not CRMSystemuser.IsIntegrationUser)
        then begin
            Session.LogMessage('0000ARY', SetUserAsIntegrationUserTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            CRMSystemuser.InviteStatusCode := CRMSystemuser.InviteStatusCode::InvitationAccepted;
            CRMSystemuser.IsIntegrationUser := true;
            if not CRMSystemuser.Modify() then
                Session.LogMessage('0000ARZ', CannotSetUserAsIntegrationUserTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok)
            else
                Session.LogMessage('0000AS0', UserSetAsIntegrationUserTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        end else
            Session.LogMessage('0000AS1', UserAlreadySetAsIntegrationUserTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        UnregisterTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName);
    end;

    [NonDebuggable]
    local procedure SetAccessModeToNonInteractive(var CDSConnectionSetup: Record "CDS Connection Setup"; AdminUserName: Text; AdminPassword: SecretText; AccessToken: SecretText; AdminADDomain: Text): Boolean
    var
        CRMSystemuser: Record "CRM Systemuser";
        TempAdminCDSConnectionSetup: Record "CDS Connection Setup" temporary;
        TempConnectionName: Text;
        ChangedToNonInteractive: Boolean;
    begin
        // User non-interactive mode is not supported on CRM OnPrem, therefore if authentication type is AD or IFD, do nothing
        if CDSConnectionSetup."Authentication Type" in [CDSConnectionSetup."Authentication Type"::AD, CDSConnectionSetup."Authentication Type"::IFD] then
            exit(false);

        Session.LogMessage('0000B2I', SetAccessModeToNonInteractiveTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        GetTempAdminConnectionSetup(TempAdminCDSConnectionSetup, CDSConnectionSetup, AdminUserName, AdminPassword, AccessToken, AdminADDomain);
        TempConnectionName := GetTempConnectionName();
        RegisterConnection(TempAdminCDSConnectionSetup, TempConnectionName);
        SetDefaultTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName, true);

        // Table connection is scoped, therefore all manipulations with CDS tables must be placed
        // in this procedure between SetDefaultTableConnection and UnregisterConnection

        FilterUser(CDSConnectionSetup, CRMSystemuser);
        if not CRMSystemuser.FindFirst() then begin
            Session.LogMessage('0000B2J', UserNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(CannotResolveUserFromConnectionSetupErr);
        end;

        if CRMSystemuser.AccessMode <> CRMSystemuser.AccessMode::"Non-interactive" then begin
            Session.LogMessage('0000B2A', SetAccessModeToNonInteractiveTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            CRMSystemuser.AccessMode := CRMSystemuser.AccessMode::"Non-interactive";
            if not CRMSystemuser.Modify() then
                Session.LogMessage('0000B2B', CannotSetAccessModeToNonInteractiveTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok)
            else begin
                ChangedToNonInteractive := CRMSystemuser.AccessMode = CRMSystemuser.AccessMode::"Non-interactive";
                if ChangedToNonInteractive then
                    Session.LogMessage('0000B2C', AccessModeSetToNonInteractiveTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok)
                else
                    Session.LogMessage('0000B2H', CannotSetAccessModeToNonInteractiveTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok)
            end;
        end else
            Session.LogMessage('0000B2D', AccessModeAlreadySetToNonInteractiveTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        UnregisterTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName);

        exit(ChangedToNonInteractive);
    end;

    [NonDebuggable]
    local procedure FindOrCreateIntegrationUser(var CDSConnectionSetup: Record "CDS Connection Setup"; AdminUserName: Text; AdminPassword: SecretText; AccessToken: SecretText)
    var
        CRMSystemuser: Record "CRM Systemuser";
        RootBusinessUnit: Record "CRM Businessunit";
        TempAdminCDSConnectionSetup: Record "CDS Connection Setup" temporary;
        TempConnectionName: Text;
        CDSConnectionClientIdTxt: Text;
        CDSConnectionFirstPartyAppIdTxt: Text;
        CDSConnectionFirstPartyAppCertificateTxt: Text;
        NewConnectionString: Text;
        IntegrationUsernameEmailTxt: Text[100];
        OnGetIntegrationUserEmailHandled: Boolean;
        CDSConnectionClientId: Guid;
        EmptyGuid: Guid;
        ExistingApplicationUserCount: Integer;
        EmptySecretText: SecretText;
    begin
        if CDSConnectionSetup."Authentication Type" <> CDSConnectionSetup."Authentication Type"::Office365 then
            exit;
        Session.LogMessage('0000C4J', FindOrCreateIntegrationUserTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        GetTempAdminConnectionSetup(TempAdminCDSConnectionSetup, CDSConnectionSetup, AdminUserName, AdminPassword, AccessToken, '');
        TempConnectionName := GetTempConnectionName();
        RegisterConnection(TempAdminCDSConnectionSetup, TempConnectionName);
        SetDefaultTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName, true);

        CDSConnectionClientIdTxt := GetCDSConnectionClientId();
        CDSConnectionFirstPartyAppIdTxt := GetCDSConnectionFirstPartyAppId();
        CDSConnectionFirstPartyAppCertificateTxt := GetCDSConnectionFirstPartyAppCertificate();

        if CDSConnectionFirstPartyAppIdTxt <> '' then
            CDSConnectionClientId := CDSConnectionFirstPartyAppIdTxt
        else
            CDSConnectionClientId := CDSConnectionClientIdTxt;
        CRMSystemuser.SetRange(CRMSystemuser.ApplicationId, CDSConnectionClientId);
        ExistingApplicationUserCount := CRMSystemuser.Count();

        if ExistingApplicationUserCount > 1 then begin
            Session.LogMessage('0000C4K', StrSubstNo(FoundMoreThanOneIntegrationUserTxt, CDSConnectionClientId, CDSConnectionSetup."Server Address"), Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(FoundMoreThanOneIntegrationUserErr, CDSConnectionClientId, CDSConnectionSetup."Server Address");
        end;

        if ExistingApplicationUserCount = 1 then begin
            Session.LogMessage('0000C4L', StrSubstNo(FoundOneIntegrationUserTxt, CDSConnectionClientId, CDSConnectionSetup."Server Address"), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            CrmSystemUser.FindFirst();
        end else begin
            Session.LogMessage('0000C4M', StrSubstNo(FoundNoIntegrationUserTxt, CDSConnectionClientId, CDSConnectionSetup."Server Address"), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            OnGetIntegrationUserEmail(IntegrationUsernameEmailTxt, OnGetIntegrationUserEmailHandled);
            if not OnGetIntegrationUserEmailHandled then
                IntegrationUsernameEmailTxt := CopyStr(GetAvailableIntegrationUserEmail(), 1, MaxStrLen(CRMSystemuser.InternalEMailAddress))
            else
                Session.LogMessage('0000IOW', UsingCustomIntegrationUsernameEmailTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            if IntegrationUsernameEmailTxt = '' then begin
                Session.LogMessage('0000D1D', StrSubstNo(FailedToInsertApplicationUserTxt, CDSConnectionClientId, CDSConnectionSetup."Server Address"), Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                Error(FailedToInsertApplicationUserErr, CDSConnectionClientId, CDSConnectionSetup."Server Address");
            end;
            CRMSystemuser.ApplicationId := CDSConnectionClientId;
            CRMSystemuser.FirstName := IntegrationUserFirstNameTxt;
            CRMSystemuser.LastName := IntegrationUserLastNameTxt;
            CRMSystemuser.FullName := IntegrationUserFullNameTxt;
            RootBusinessunit.SetRange(ParentBusinessUnitId, EmptyGuid);
            RootBusinessunit.FindFirst();
            CRMSystemUser.BusinessUnitId := RootBusinessUnit.BusinessUnitId;
            CRMSystemuser.InternalEMailAddress := IntegrationUsernameEmailTxt;
            if not CRMSystemuser.Insert() then begin
                Session.LogMessage('0000C4N', StrSubstNo(FailedToInsertApplicationUserTxt, CDSConnectionClientId, CDSConnectionSetup."Server Address"), Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                Error(FailedToInsertApplicationUserErr, CDSConnectionClientId, CDSConnectionSetup."Server Address");
            end;
        end;
        if not UpdateIntegrationUserNameAndEmailIfNeeded(CRMSystemuser) then
            Session.LogMessage('0000ENR', CannotUpdateUserNameAndEmailTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        CDSConnectionSetup."User Name" := CRMSystemuser.InternalEMailAddress;
        CDSConnectionSetup.SetPassword(EmptySecretText);
        if (CDSConnectionFirstPartyAppIdTxt <> '') and (CDSConnectionFirstPartyAppCertificateTxt <> '') then
            NewConnectionString := StrSubstNo(CertificateConnectionStringFormatTxt, CertificateAuthTxt, CDSConnectionSetup."Server Address", ClientIdTok, CertificateTok, CDSConnectionSetup.GetProxyVersion())
        else
            NewConnectionString := StrSubstNo(ClientSecretConnectionStringFormatTxt, ClientSecretAuthTxt, CDSConnectionSetup."Server Address", ClientIdTok, ClientSecretTok, CDSConnectionSetup.GetProxyVersion());
        SetConnectionString(CDSConnectionSetup, NewConnectionString);

        UnregisterTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName);
    end;

    [NonDebuggable]
    local procedure FindOrCreateIntegrationUser(var CDSConnectionSetup: Record "CDS Connection Setup")
    var
        CRMSystemuser: Record "CRM Systemuser";
        RootBusinessUnit: Record "CRM Businessunit";
        CurrentCDSConnectionSetup: Record "CDS Connection Setup";
        CurrentCRMConnectionSetup: Record "CRM Connection Setup";
        TempConnectionName: Text;
        CDSConnectionClientIdTxt: Text;
        CDSConnectionFirstPartyAppIdTxt: Text;
        CDSConnectionFirstPartyAppCertificateTxt: Text;
        NewConnectionString: Text;
        IntegrationUsernameEmailTxt: Text[100];
        CDSConnectionClientId: Guid;
        EmptyGuid: Guid;
        ExistingApplicationUserCount: Integer;
        CDSConnectionSetupEnabled: Boolean;
        EmptySecretText: SecretText;
    begin
        if CDSConnectionSetup."Authentication Type" <> CDSConnectionSetup."Authentication Type"::Office365 then
            exit;
        Session.LogMessage('0000GJ8', FindOrCreateIntegrationUserTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        CDSConnectionClientIdTxt := GetCDSConnectionClientId();
        CDSConnectionFirstPartyAppIdTxt := GetCDSConnectionFirstPartyAppId();
        CDSConnectionFirstPartyAppCertificateTxt := GetCDSConnectionFirstPartyAppCertificate();

        if CDSConnectionFirstPartyAppIdTxt <> '' then
            CDSConnectionClientId := CDSConnectionFirstPartyAppIdTxt
        else
            CDSConnectionClientId := CDSConnectionClientIdTxt;

        TempConnectionName := GetTempConnectionName();
        // connect with the integration user, not with the admin. this relies on no user interaction
        if CurrentCDSConnectionSetup.Get() then
            if CurrentCDSConnectionSetup."Is Enabled" then
                CDSConnectionSetupEnabled := true;
        if CDSConnectionSetupEnabled then
            RegisterConnection(CurrentCDSConnectionSetup, TempConnectionName)
        else
            if CurrentCRMConnectionSetup.IsEnabled() then
                CurrentCRMConnectionSetup.RegisterConnectionWithName(TempConnectionName);
        SetDefaultTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName, true);

        CRMSystemuser.SetRange(CRMSystemuser.ApplicationId, CDSConnectionClientId);
        ExistingApplicationUserCount := CRMSystemuser.Count();

        if ExistingApplicationUserCount > 1 then begin
            Session.LogMessage('0000GJ9', StrSubstNo(FoundMoreThanOneIntegrationUserTxt, CDSConnectionClientId, CDSConnectionSetup."Server Address"), Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(FoundMoreThanOneIntegrationUserErr, CDSConnectionClientId, CDSConnectionSetup."Server Address");
        end;

        if ExistingApplicationUserCount = 1 then begin
            Session.LogMessage('0000GJA', StrSubstNo(FoundOneIntegrationUserTxt, CDSConnectionClientId, CDSConnectionSetup."Server Address"), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            CrmSystemUser.FindFirst();
        end else begin
            Session.LogMessage('0000GJB', StrSubstNo(FoundNoIntegrationUserTxt, CDSConnectionClientId, CDSConnectionSetup."Server Address"), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            IntegrationUsernameEmailTxt := CopyStr(GetAvailableIntegrationUserEmail(), 1, MaxStrLen(CRMSystemuser.InternalEMailAddress));
            if IntegrationUsernameEmailTxt = '' then begin
                Session.LogMessage('0000GJC', StrSubstNo(FailedToInsertApplicationUserTxt, CDSConnectionClientId, CDSConnectionSetup."Server Address"), Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                Error(FailedToInsertApplicationUserErr, CDSConnectionClientId, CDSConnectionSetup."Server Address");
            end;
            CRMSystemuser.ApplicationId := CDSConnectionClientId;
            CRMSystemuser.FirstName := IntegrationUserFirstNameTxt;
            CRMSystemuser.LastName := IntegrationUserLastNameTxt;
            CRMSystemuser.FullName := IntegrationUserFullNameTxt;
            RootBusinessunit.SetRange(ParentBusinessUnitId, EmptyGuid);
            RootBusinessunit.FindFirst();
            CRMSystemUser.BusinessUnitId := RootBusinessUnit.BusinessUnitId;
            CRMSystemuser.InternalEMailAddress := IntegrationUsernameEmailTxt;
            if not CRMSystemuser.Insert() then begin
                Session.LogMessage('0000GJD', StrSubstNo(FailedToInsertApplicationUserTxt, CDSConnectionClientId, CDSConnectionSetup."Server Address"), Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                Error(FailedToInsertApplicationUserErr, CDSConnectionClientId, CDSConnectionSetup."Server Address");
            end else
                Session.LogMessage('0000GJE', StrSubstNo(CreatedIntegrationUserTxt, CDSConnectionClientId, CDSConnectionSetup."Server Address"), Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        end;
        if not UpdateIntegrationUserNameAndEmailIfNeeded(CRMSystemuser) then
            Session.LogMessage('0000GJF', CannotUpdateUserNameAndEmailTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        CDSConnectionSetup."User Name" := CRMSystemuser.InternalEMailAddress;
        CDSConnectionSetup.SetPassword(EmptySecretText);
        if (CDSConnectionFirstPartyAppIdTxt <> '') and (CDSConnectionFirstPartyAppCertificateTxt <> '') then
            NewConnectionString := StrSubstNo(CertificateConnectionStringFormatTxt, CertificateAuthTxt, CDSConnectionSetup."Server Address", ClientIdTok, CertificateTok, CDSConnectionSetup.GetProxyVersion())
        else
            NewConnectionString := StrSubstNo(ClientSecretConnectionStringFormatTxt, ClientSecretAuthTxt, CDSConnectionSetup."Server Address", ClientIdTok, ClientSecretTok, CDSConnectionSetup.GetProxyVersion());
        SetConnectionString(CDSConnectionSetup, NewConnectionString);

        UnregisterTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName);
    end;

    [Scope('OnPrem')]
    procedure SetupCertificateAuthentication(var CDSConnectionSetup: Record "CDS Connection Setup")
    var
        CrmHelper: DotNet CRMHelper;
        [NonDebuggable]
        CDSConnectionFirstPartyAppIdTxt: Text;
        [NonDebuggable]
        CDSConnectionFirstPartyAppCertificateTxt: Text;
        AdminAccessToken: SecretText;
        TempConnectionStringWithPlaceholder: Text;
        TempConnectionString: SecretText;
        JITProvisioningTelemetryMessageTxt: Text;
        PreviousIntegrationUserName: Text[250];
        EmptySecretText: SecretText;
    begin
        CDSConnectionFirstPartyAppIdTxt := GetCDSConnectionFirstPartyAppId();
        CDSConnectionFirstPartyAppCertificateTxt := GetCDSConnectionFirstPartyAppCertificate();
        if (CDSConnectionFirstPartyAppIdTxt = '') or (CDSConnectionFirstPartyAppCertificateTxt = '') then
            exit;

        PreviousIntegrationUserName := CDSConnectionSetup."User Name";

        // sign in as admin in interactive auth code flow
        GetAccessToken(CDSConnectionSetup."Server Address", false, AdminAccessToken);

        // register connection with CrmHelper
        TempConnectionStringWithPlaceholder := StrSubstNo(OAuthConnectionStringFormatTxt, CDSConnectionSetup."Server Address", '%1', CDSConnectionSetup.GetProxyVersion(), GetAuthenticationTypeToken(CDSConnectionSetup));
        TempConnectionString := SecretStrSubstNo(TempConnectionStringWithPlaceholder, AdminAccessToken);
        if not InitializeConnection(CrmHelper, TempConnectionString) then begin
            Session.LogMessage('0000AU2', ConnectionNotRegisteredTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            ProcessConnectionFailures();
            exit;
        end;

        // JIT provision (backfill) the 1st party app service principal
        JITProvisioningTelemetryMessageTxt := CrmHelper.ProvisionServicePrincipal(CDSConnectionFirstPartyAppIdTxt, CDSConnectionFirstPartyAppCertificateTxt);
        if JITProvisioningTelemetryMessageTxt <> '' then
            if JITProvisioningTelemetryMessageTxt.Contains(SuccessfulJITProvisioningTelemetryMsg) then
                Session.LogMessage('0000ENK', JITProvisioningTelemetryMessageTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok)
            else
                Session.LogMessage('0000F0A', JITProvisioningTelemetryMessageTxt, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        // even though CrmHelper.ProvisionServicePrincipal contains an async Graph call, you can't expect Dataverse to be aware of the newly provisioned service principal right away.
        // It has been tested that it fails if you try to create the application user right after you provision the service principal
        Sleep(5000);

        if not JITProvisioningTelemetryMessageTxt.Contains(SuccessfulJITProvisioningTelemetryMsg) then begin
            // try one more time, it could have failed because of a timeout
            JITProvisioningTelemetryMessageTxt := CrmHelper.ProvisionServicePrincipal(CDSConnectionFirstPartyAppIdTxt, CDSConnectionFirstPartyAppCertificateTxt);
            if JITProvisioningTelemetryMessageTxt <> '' then
                if JITProvisioningTelemetryMessageTxt.Contains(SuccessfulJITProvisioningTelemetryMsg) then
                    Session.LogMessage('0000ENK', JITProvisioningTelemetryMessageTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok)
                else
                    Session.LogMessage('0000F0A', JITProvisioningTelemetryMessageTxt, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        end;

        // even though CrmHelper.ProvisionServicePrincipal contains an async Graph call, you can't expect Dataverse to be aware of the newly provisioned service principal right away.
        // It has been tested that it fails if you try to create the application user right after you provision the service principal
        Sleep(10000);

        // this will create integration user and update the proxy version and connection string
        FindOrCreateIntegrationUser(CDSConnectionSetup, '', EmptySecretText, AdminAccessToken);
        AssignPreviousIntegrationUserRoles(CrmHelper, PreviousIntegrationUserName, CDSConnectionSetup, AdminAccessToken);
    end;

    [Scope('OnPrem')]
    [NonDebuggable]
    procedure SetupCertificatetAuthenticationNoPrompt(var CDSConnectionSetup: Record "CDS Connection Setup")
    var
        CurrentCDSConnectionSetup: Record "CDS Connection Setup";
        CurrentCRMConnectionSetup: Record "CRM Connection Setup";
        CrmHelper: DotNet CRMHelper;
        [NonDebuggable]
        CDSConnectionFirstPartyAppIdTxt: Text;
        [NonDebuggable]
        CDSConnectionFirstPartyAppCertificateTxt: Text;
        PreviousIntegrationUserName: Text[250];
        JITProvisioningTelemetryMessageTxt: Text;
        TempConnectionString: SecretText;
    begin
        CDSConnectionFirstPartyAppIdTxt := GetCDSConnectionFirstPartyAppId();
        CDSConnectionFirstPartyAppCertificateTxt := GetCDSConnectionFirstPartyAppCertificate();
        if (CDSConnectionFirstPartyAppIdTxt = '') or (CDSConnectionFirstPartyAppCertificateTxt = '') then
            exit;

        PreviousIntegrationUserName := CDSConnectionSetup."User Name";

        // register connection with CrmHelper, with the previous integration user, not with an admin
        if CurrentCDSConnectionSetup.Get() then
            if CurrentCDSConnectionSetup."Is Enabled" then
                TempConnectionString := GetConnectionStringWithCredentials(CurrentCDSConnectionSetup);

        if TempConnectionString.IsEmpty() then
            if CurrentCRMConnectionSetup.Get() then
                if CurrentCRMConnectionSetup."Is Enabled" then
                    TempConnectionString := CurrentCRMConnectionSetup.GetSecretConnectionStringWithCredentials();

        if not InitializeConnection(CrmHelper, TempConnectionString) then begin
            Session.LogMessage('0000AU2', ConnectionNotRegisteredTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            ProcessConnectionFailures();
            exit;
        end;

        // JIT provision (backfill) the 1st party app service principal
        JITProvisioningTelemetryMessageTxt := CrmHelper.ProvisionServicePrincipal(CDSConnectionFirstPartyAppIdTxt, CDSConnectionFirstPartyAppCertificateTxt);
        if JITProvisioningTelemetryMessageTxt <> '' then
            if JITProvisioningTelemetryMessageTxt.Contains(SuccessfulJITProvisioningTelemetryMsg) then
                Session.LogMessage('0000GJG', JITProvisioningTelemetryMessageTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok)
            else
                Session.LogMessage('0000GJH', JITProvisioningTelemetryMessageTxt, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        // even though CrmHelper.ProvisionServicePrincipal contains an async Graph call, you can't expect Dataverse to be aware of the newly provisioned service principal right away.
        // It has been tested that it fails if you try to create the application user right after you provision the service principal
        Sleep(5000);

        if not JITProvisioningTelemetryMessageTxt.Contains(SuccessfulJITProvisioningTelemetryMsg) then begin
            // try one more time, it could have failed because of a timeout
            JITProvisioningTelemetryMessageTxt := CrmHelper.ProvisionServicePrincipal(CDSConnectionFirstPartyAppIdTxt, CDSConnectionFirstPartyAppCertificateTxt);
            if JITProvisioningTelemetryMessageTxt <> '' then
                if JITProvisioningTelemetryMessageTxt.Contains(SuccessfulJITProvisioningTelemetryMsg) then
                    Session.LogMessage('0000GJI', JITProvisioningTelemetryMessageTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok)
                else
                    Session.LogMessage('0000GJJ', JITProvisioningTelemetryMessageTxt, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        end;

        // even though CrmHelper.ProvisionServicePrincipal contains an async Graph call, you can't expect Dataverse to be aware of the newly provisioned service principal right away.
        // It has been tested that it fails if you try to create the application user right after you provision the service principal
        Sleep(10000);

        // this will create integration user and update the proxy version and connection string
        FindOrCreateIntegrationUser(CDSConnectionSetup);
        AssignPreviousIntegrationUserRoles(CrmHelper, PreviousIntegrationUserName, CDSConnectionSetup);
    end;

    [TryFunction]
    local procedure UpdateIntegrationUserNameAndEmailIfNeeded(var CRMSystemuser: Record "CRM Systemuser")
    var
        Email: Text[100];
        EmailChanged: Boolean;
        NameChanged: Boolean;
    begin
        // update email address as insert could set an autogenerated email instead of the specified
        if (not CRMSystemuser.InternalEMailAddress.StartsWith(IntegrationUserPrimaryEmailStartTxt)) or
           (not CRMSystemuser.InternalEMailAddress.EndsWith(IntegrationUserPrimaryEmailEndTxt))
        then begin
            Session.LogMessage('0000ENS', UpdateUserEmailTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Email := CopyStr(GetAvailableIntegrationUserEmail(), 1, MaxStrLen(CRMSystemuser.InternalEMailAddress));
            if Email <> '' then begin
                CRMSystemuser.InternalEMailAddress := Email;
                EmailChanged := true;
            end;
        end;
        // add last name to first name as insert could set an autogenerated last name instead of the specified
        if CRMSystemuser.LastName <> IntegrationUserLastNameTxt then begin
            Session.LogMessage('0000ENT', UpdateUserNameTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            CRMSystemuser.FirstName := IntegrationUserFullNameTxt;
            NameChanged := true;
        end;
        if (not EmailChanged) and (not NameChanged) then
            exit;

        CRMSystemuser.Modify();
        if EmailChanged and (
           (not CRMSystemuser.InternalEMailAddress.StartsWith(IntegrationUserPrimaryEmailStartTxt)) or
           (not CRMSystemuser.InternalEMailAddress.EndsWith(IntegrationUserPrimaryEmailEndTxt)))
        then
            Session.LogMessage('0000ENU', UserEmailNotUpdatedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        if NameChanged and (CRMSystemuser.FirstName <> IntegrationUserFullNameTxt) then
            Session.LogMessage('0000ENW', UserNameNotUpdatedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
    end;

    local procedure GetAvailableIntegrationUserEmail(): Text
    var
        IntegrationCRMSystemUser: Record "CRM Systemuser";
        IntegrationUsernameEmailTxt: Text;
        EmailSuffix: Integer;
    begin
        EmailSuffix := 0;
        IntegrationUsernameEmailTxt := StrSubstNo(IntegrationUserPrimaryEmailTxt, '');
        IntegrationCRMSystemuser.SetRange(InternalEMailAddress, IntegrationUsernameEmailTxt);
        while not IntegrationCRMSystemuser.IsEmpty() and (EmailSuffix < 100) do begin
            EmailSuffix += 1;
            IntegrationUsernameEmailTxt := StrSubstNo(IntegrationUserPrimaryEmailTxt, EmailSuffix);
            IntegrationCRMSystemuser.SetRange(InternalEMailAddress, IntegrationUsernameEmailTxt);
        end;

        if EmailSuffix >= 100 then
            exit('');

        exit(IntegrationUsernameEmailTxt);
    end;

    [NonDebuggable]
    local procedure SyncCompany(var CDSConnectionSetup: Record "CDS Connection Setup"; AdminUserName: Text; AdminPassword: SecretText; AccessToken: SecretText; AdminADDomain: Text)
    var
        TempAdminCDSConnectionSetup: Record "CDS Connection Setup" temporary;
        Company: Record Company;
        GeneralLedgerSetup: Record "General Ledger Setup";
        CDSCompany: Record "CDS Company";
        RootCRMBusinessunit: Record "CRM Businessunit";
        DefaultCRMBusinessunit: Record "CRM Businessunit";
        UpdatedCRMBusinessunit: Record "CRM Businessunit";
        CRMBusinessunit: Record "CRM Businessunit";
        DefaultCRMTeam: Record "CRM Team";
        InitialCRMTeam: Record "CRM Team";
        UpdatingCRMTeam: Record "CRM Team";
        UpdatedCRMTeam: Record "CRM Team";
        NewCRMTransactioncurrency: Record "CRM Transactioncurrency";
        CRMTransactioncurrency: Record "CRM Transactioncurrency";
        CRMOrganization: Record "CRM Organization";
        CRMRole: Record "CRM Role";
        Currency: Record Currency;
        CurrExchRate: Record "Currency Exchange Rate";
        CrmHelper: DotNet CrmHelper;
        TempConnectionName: Text;
        CompanyId: Text[36];
        CompanyName: Text[30];
        BusinessUnitName: Text[160];
        TeamName: Text[160];
        IntegrationRoleName: Text[100];
        EmptyGuid: Guid;
        UpdateOwningTeam: Boolean;
        OwningTeamUpdated: Boolean;
        DefaultBusinessUnitFound: Boolean;
        IsCurrencyCheckHandled: Boolean;
        CRMBaseCurrencyCode: Text[5];
        CRMBaseCurrencyFoundLocally: Boolean;
    begin
        Session.LogMessage('0000AS2', SynchronizeCompanyTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        ResetCache();

        Company.Get(CompanyName());
        CompanyId := GetCompanyExternalId(Company);
        CompanyName := CopyStr(Company.Name, 1, MaxStrLen(CompanyName));
        BusinessUnitName := GetDefaultBusinessUnitName(CompanyName, CompanyId);

        GetTempAdminConnectionSetup(TempAdminCDSConnectionSetup, CDSConnectionSetup, AdminUserName, AdminPassword, AccessToken, AdminADDomain);
        InitializeConnection(CrmHelper, TempAdminCDSConnectionSetup);
        TempConnectionName := GetTempConnectionName();
        RegisterConnection(TempAdminCDSConnectionSetup, TempConnectionName);
        SetDefaultTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName, true);

        // Table connection is scoped, therefore all manipulations with CDS tables must be placed
        // in this procedure between SetDefaultTableConnection and UnregisterConnection

        Session.LogMessage('0000AS3', CheckBusinessUnitTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        RootCRMBusinessunit.SetRange(ParentBusinessUnitId, EmptyGuid);
        RootCRMBusinessunit.FindFirst();
        DefaultCRMBusinessunit.SetRange(ParentBusinessUnitId, RootCRMBusinessunit.BusinessUnitId);
        DefaultCRMBusinessunit.SetRange(Name, BusinessUnitName);
        DefaultBusinessUnitFound := DefaultCRMBusinessunit.FindFirst();
        if not DefaultBusinessUnitFound then begin
            DefaultCRMBusinessunit.SetFilter(Name, '*' + CompanyId + '*');
            if DefaultCRMBusinessunit.FindFirst() then
                DefaultBusinessUnitFound := DefaultCRMBusinessunit.Name.EndsWith(StrSubstNo(BusinessUnitNameSuffixTok, CompanyId))
        end;
        if not DefaultBusinessUnitFound then begin
            DefaultCRMBusinessunit.Name := BusinessUnitName;
            DefaultCRMBusinessunit.TransactionCurrencyId := CRMTransactioncurrency.TransactionCurrencyId;
            DefaultCRMBusinessunit.ParentBusinessUnitId := RootCRMBusinessunit.BusinessUnitId;
            if not DefaultCRMBusinessunit.Insert() then begin
                Session.LogMessage('0000AS4', CannotCreateBusinessUnitTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                Error(EnrichWithDotNetException(CannotCreateBusinessUnitErr), BusinessUnitName);
            end;
            Session.LogMessage('0000AS5', BusinessUnitCreatedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        end else
            Session.LogMessage('0000AS6', BusinessUnitAlreadyExistsTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        Session.LogMessage('0000AS7', CheckOwningTeamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        TeamName := GetOwningTeamName(DefaultCRMBusinessunit.Name);
        FilterTeam(DefaultCRMBusinessunit.BusinessUnitId, TeamName, DefaultCRMTeam);
        if not DefaultCRMTeam.FindFirst() then begin
            DefaultCRMTeam.Name := TeamName;
            DefaultCRMTeam.BusinessUnitId := DefaultCRMBusinessunit.BusinessUnitId;
            DefaultCRMTeam.TeamType := DefaultCRMTeam.TeamType::Owner;
            if not DefaultCRMTeam.Insert() then begin
                Session.LogMessage('0000AS8', CannotCreateTeamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                Error(EnrichWithDotNetException(CannotCreateTeamErr), TeamName, BusinessUnitName);
            end;
            Session.LogMessage('0000AS9', TeamCreatedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        end else
            Session.LogMessage('0000ASA', TeamAlreadyExistsTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        Session.LogMessage('0000ASB', CheckTeamRolesTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        if not CRMRole.Get(GetIntegrationRoleId()) then begin
            Session.LogMessage('0000ASC', IntegrationRoleNotFoundTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(IntegrationRoleNotFoundErr, IntegrationRoleName, RootCRMBusinessunit.Name);
        end;
        IntegrationRoleName := CRMRole.Name;

        CRMRole.SetRange(ParentRoleId, GetIntegrationRoleId());
        CRMRole.SetRange(BusinessUnitId, DefaultCRMBusinessunit.BusinessUnitId);
        if not CRMRole.FindFirst() then begin
            Session.LogMessage('0000ASD', RoleNotFoundForBusinessUnitTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(IntegrationRoleNotFoundErr, IntegrationRoleName, DefaultCRMBusinessunit.Name);
        end;
        if not AssignTeamRole(CrmHelper, DefaultCRMTeam.TeamId, CRMRole.RoleId) then begin
            Session.LogMessage('0000ASE', CannotAssignRoleToTeamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(EnrichWithDotNetException(CannotAssignRoleToTeamErr), DefaultCRMTeam.Name, DefaultCRMBusinessunit.Name, IntegrationRoleName);
        end;
        Session.LogMessage('0000ASF', RoleAssignedToTeamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        Session.LogMessage('0000ASG', CheckCompanyTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        CDSCompany.SetRange(ExternalId, CompanyId);
        if not CDSCompany.FindFirst() then begin
            Session.LogMessage('0000ASH', CreateCompanyTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            if not CRMOrganization.FindFirst() then begin
                Session.LogMessage('0000ASI', OrganizationNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                Error(CannotFindOrganizationErr);
            end;
            if not CRMTransactioncurrency.Get(CRMOrganization.BaseCurrencyId) then begin
                Session.LogMessage('0000ASJ', CurrencyNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                Error(BaseCurrencyNotFoundErr);
            end;
            CRMBaseCurrencyCode := CRMTransactioncurrency.ISOCurrencyCode;
            if not GeneralLedgerSetup.Get() then begin
                Session.LogMessage('0000ASK', GLSetupNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                Error(GLSetupNotFoundErr);
            end;
            IsCurrencyCheckHandled := false;
            OnBeforeCheckGLSetupLCYCode(GeneralLedgerSetup, CRMTransactioncurrency, IsCurrencyCheckHandled);
            if not IsCurrencyCheckHandled then begin
                CRMTransactionCurrency.Reset();
                if (GeneralLedgerSetup."LCY Code" = '') then
                    Error(LCYCodeNotFoundErr);
                CRMTransactioncurrency.SetRange(ISOCurrencyCode, CopyStr(GeneralLedgerSetup."LCY Code", 1, MaxStrLen(TempAdminCDSConnectionSetup.BaseCurrencyCode)));
                if CRMTransactioncurrency.IsEmpty() then begin
                    if Currency.Get(Uppercase(CRMBaseCurrencyCode)) then
                        if CurrExchRate.ExchangeRateAdjmt(Today(), Currency.Code) <> 0 then
                            CRMBaseCurrencyFoundLocally := true;
                    if not CRMBaseCurrencyFoundLocally then begin
                        Session.LogMessage('0000K8O', CurrencyMismatchTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                        Error(LCYMustMatchBaseCurrencyErr, GeneralLedgerSetup."LCY Code");
                    end;
                    NewCRMTransactionCurrency.ISOCurrencyCode := CopyStr(GeneralLedgerSetup."LCY Code", 1, MaxStrLen(CRMTransactionCurrency.ISOCurrencyCode));
                    if GeneralLedgerSetup."Local Currency Description" <> '' then
                        NewCRMTransactioncurrency.CurrencyName := CopyStr(GeneralLedgerSetup."Local Currency Description", 1, MaxStrLen(CRMTransactionCurrency.CurrencyName))
                    else
                        NewCRMTransactioncurrency.CurrencyName := CopyStr(GeneralLedgerSetup."LCY Code", 1, MaxStrLen(CRMTransactionCurrency.CurrencyName));
                    if GeneralLedgerSetup."Local Currency Symbol" <> '' then
                        NewCRMTransactioncurrency.CurrencySymbol := CopyStr(GeneralLedgerSetup."Local Currency Symbol", 1, MaxStrLen(CRMTransactionCurrency.CurrencySymbol))
                    else
                        NewCRMTransactioncurrency.CurrencySymbol := CopyStr(GeneralLedgerSetup."LCY Code", 1, MaxStrLen(CRMTransactionCurrency.CurrencySymbol));
                    NewCRMTransactioncurrency.ExchangeRate := CurrExchRate.ExchangeRateAdjmt(Today(), Currency.Code);
                    Evaluate(NewCRMTransactioncurrency.CurrencyPrecision, CopyStr(Currency."Amount Decimal Places", StrPos(Currency."Amount Decimal Places", ':') + 1));
                    if not NewCRMTransactionCurrency.Insert() then begin
                        Session.LogMessage('0000K8P', CurrencyMismatchTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                        Error(LCYMustMatchBaseCurrencyErr, GeneralLedgerSetup."LCY Code");
                    end;
                end;
                if GeneralLedgerSetup."LCY Code" <> Uppercase(CRMBaseCurrencyCode) then begin
                    CRMTransactioncurrency.FindFirst();
                    if not Currency.Get(Uppercase(CRMBaseCurrencyCode)) then begin
                        Currency.Code := CopyStr(Uppercase(CRMBaseCurrencyCode), 1, MaxStrLen(Currency.Code));
                        Currency.Symbol := CRMTransactioncurrency.CurrencySymbol;
                        Currency."ISO Code" := CopyStr(CRMTransactioncurrency.ISOCurrencyCode, 1, MaxStrLen(Currency."ISO Code"));
                        Currency."Amount Decimal Places" := Format(CRMTransactioncurrency.CurrencyPrecision) + ':' + Format(CRMTransactioncurrency.CurrencyPrecision);
                        Currency.Description := CopyStr(CRMTransactioncurrency.CurrencyName, 1, MaxStrlen(Currency.Description));
                        if not Currency.Insert() then begin
                            Session.LogMessage('0000KJE', CurrencyMismatchTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                            Error(BaseCurrencyMustExistInBCErr, Uppercase(CRMBaseCurrencyCode));
                        end;
                    end;
                    CurrExchRate.SetRange("Currency Code", Currency.Code);
                    if CurrExchRate.IsEmpty() then begin
                        CurrExchRate."Currency Code" := Currency.Code;
                        CurrExchRate."Relational Exch. Rate Amount" := 1;
                        CurrExchRate."Relational Adjmt Exch Rate Amt" := 1;
                        CurrExchRate."Exchange Rate Amount" := 1 / CRMTransactioncurrency.ExchangeRate;
                        CurrExchRate."Relational Exch. Rate Amount" := CurrExchRate."Exchange Rate Amount";
                        CurrExchRate."Starting Date" := Today();
                        if not CurrExchRate.Insert() then begin
                            Session.LogMessage('0000KJF', CurrencyMismatchTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                            Error(BaseCurrencyMustExistInBCErr, Uppercase(CRMBaseCurrencyCode));
                        end;
                    end;
                end;
            end;
            CDSCompany.ExternalId := CompanyId;
            CDSCompany.Name := CompanyName;
            CDSCompany.DefaultOwningTeam := DefaultCRMTeam.TeamId;
            CDSCompany.OwnerIdType := CDSCompany.OwnerIdType::team;
            CDSCompany.OwnerId := DefaultCRMTeam.TeamId;
            if not CDSCompany.Insert() then begin
                Session.LogMessage('0000ASM', CannotCreateCompanyTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                Error(EnrichWithDotNetException(CannotCreateCompanyErr), CompanyName);
            end;
            Session.LogMessage('0000ASN', CompanyCreatedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        end else begin
            Session.LogMessage('0000ASO', CompanyAlreadyExistsTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            if IsNullGuid(CDSCompany.DefaultOwningTeam) then begin
                CDSCompany.DefaultOwningTeam := DefaultCRMTeam.TeamId;
                if not CDSCompany.Modify() then begin
                    Session.LogMessage('0000ASP', CannotSetDefaultOwningTeamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                    Error(EnrichWithDotNetException(CannotSetDefaultOwningTeamErr));
                end;
                Session.LogMessage('0000ASQ', DefaultOwningTeamSetTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            end;
        end;

        UpdateOwningTeam := true;
        if InitialCRMTeam.Get(CDSCompany.DefaultOwningTeam) then
            if CRMBusinessunit.Get(InitialCRMTeam.BusinessUnitId) then
                UpdateOwningTeam :=
                    (CDSConnectionSetup."Business Unit Id" <> CRMBusinessunit.BusinessUnitId) or
                    (CDSConnectionSetup."Business Unit Name" <> CRMBusinessunit.Name);

        if UpdateOwningTeam and not IsNullGuid(CDSConnectionSetup."Business Unit Id") then
            if UpdatedCRMBusinessunit.Get(CDSConnectionSetup."Business Unit Id") then begin
                // update default owning team field on CDS company
                Session.LogMessage('0000ASR', CheckOwningTeamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                TeamName := GetOwningTeamName(UpdatedCRMBusinessunit.Name);
                FilterTeam(UpdatedCRMBusinessunit.BusinessUnitId, TeamName, UpdatingCRMTeam);
                if not UpdatingCRMTeam.FindFirst() then begin
                    UpdatingCRMTeam.Name := TeamName;
                    UpdatingCRMTeam.BusinessUnitId := UpdatedCRMBusinessunit.BusinessUnitId;
                    UpdatingCRMTeam.TeamType := UpdatingCRMTeam.TeamType::Owner;
                    if not UpdatingCRMTeam.Insert() then begin
                        Session.LogMessage('0000ASS', CannotCreateTeamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                        Error(EnrichWithDotNetException(CannotCreateTeamErr), TeamName, UpdatedCRMBusinessunit.Name);
                    end;
                    Session.LogMessage('0000AST', TeamCreatedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                end else
                    Session.LogMessage('0000ASU', TeamAlreadyExistsTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

                CRMRole.SetRange(ParentRoleId, GetIntegrationRoleId());
                CRMRole.SetRange(BusinessUnitId, UpdatedCRMBusinessunit.BusinessUnitId);
                if not CRMRole.FindFirst() then begin
                    Session.LogMessage('0000ASV', RoleNotFoundForBusinessUnitTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                    Error(IntegrationRoleNotFoundErr, IntegrationRoleName, UpdatedCRMBusinessunit.Name);
                end;
                if not AssignTeamRole(CrmHelper, UpdatingCRMTeam.TeamId, CRMRole.RoleId) then begin
                    Session.LogMessage('0000ASW', CannotAssignRoleToTeamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                    Error(EnrichWithDotNetException(CannotAssignRoleToTeamErr), UpdatingCRMTeam.Name, UpdatedCRMBusinessunit.Name, IntegrationRoleName);
                end;
                Session.LogMessage('0000ASX', RoleAssignedToTeamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

                CDSCompany.DefaultOwningTeam := UpdatingCRMTeam.TeamId;
                if not CDSCompany.Modify() then begin
                    Session.LogMessage('0000ASY', CannotSetDefaultOwningTeamTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                    Error(EnrichWithDotNetException(CannotSetDefaultOwningTeamErr));
                end;
                OwningTeamUpdated := true;
                Session.LogMessage('0000ASZ', DefaultOwningTeamSetTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            end else
                Session.LogMessage('0000AT0', BusinessUnitNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        if not OwningTeamUpdated then begin
            UpdatedCRMTeam.Get(CDSCompany.DefaultOwningTeam);
            if not UpdatedCRMBusinessunit.Get(UpdatedCRMTeam.BusinessUnitId) then begin
                Session.LogMessage('0000AT1', BusinessUnitNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                Error(BusinessUnitNotFoundErr, UpdatedCRMTeam.BusinessUnitId);
            end;
            if (CDSConnectionSetup."Business Unit Id" <> UpdatedCRMBusinessunit.BusinessUnitId) or
                (CDSConnectionSetup."Business Unit Name" <> UpdatedCRMBusinessunit.Name) then begin
                // fix business unit related fields in the setup table
                Session.LogMessage('0000AT2', BusinessUnitFixedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                CDSConnectionSetup."Business Unit Id" := UpdatedCRMBusinessunit.BusinessUnitId;
                CDSConnectionSetup."Business Unit Name" := UpdatedCRMBusinessunit.Name;
                ModifyBusinessUnitCoupling(CDSConnectionSetup);
            end else
                Session.LogMessage('0000AT3', BusinessUnitCoupledTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        end;

        UnregisterTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName);

        Session.LogMessage('0000AT4', CompanySynchronizedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
    end;

    local procedure EnrichWithDotNetException(ErrorMessage: Text): Text
    var
        DotNetExceptionHandler: Codeunit "DotNet Exception Handler";
        DotNetExceptionMessage: Text;
        EnrichedErrorMessage: Text;
    begin
        DotNetExceptionHandler.Collect();
        DotNetExceptionMessage := DotNetExceptionHandler.GetMessage();
        if DotNetExceptionMessage = '' then
            exit(ErrorMessage);

        EnrichedErrorMessage := ErrorMessage + ' ' + DotNetExceptionMessage;

        if StrPos(DotNetExceptionMessage, 'PrivilegeId') = 0 then
            exit(EnrichedErrorMessage);

        EnrichedErrorMessage := EnrichedErrorMessage + ' ' + StrSubstNo(InsufficientPriviegesTxt, FixPermissionsUrlTxt);
        exit(EnrichedErrorMessage);
    end;

    [Scope('OnPrem')]
    procedure AddCoupledUsersToDefaultOwningTeam(var CDSConnectionSetup: Record "CDS Connection Setup"): Integer;
    var
        TempCRMSystemuser: Record "CRM Systemuser" temporary;
    begin
        GetCoupledUsers(TempCRMSystemuser);
        exit(AddUsersToDefaultOwningTeam(CDSConnectionSetup, TempCRMSystemuser));
    end;

    [Scope('OnPrem')]
    procedure AddCoupledUsersToDefaultOwningTeam(var CDSConnectionSetup: Record "CDS Connection Setup"; var TempCRMSystemuser: Record "CRM Systemuser" temporary): Integer;
    var
        CRMIntegrationRecord: Record "CRM Integration Record";
        TempSelectedCRMSystemuser: Record "CRM Systemuser" temporary;
    begin
        CRMIntegrationRecord.SetRange("Table ID", Database::"Salesperson/Purchaser");

        if TempCRMSystemuser.FindSet() then
            repeat
                CRMIntegrationRecord.SetRange("CRM ID", TempCRMSystemuser.SystemUserId);
                if not CRMIntegrationRecord.IsEmpty() then begin
                    TempSelectedCRMSystemuser.Init();
                    TempSelectedCRMSystemuser.TransferFields(TempCRMSystemuser);
                    TempSelectedCRMSystemuser.Insert();
                end;
            until TempCRMSystemuser.Next() = 0;
        exit(AddUsersToDefaultOwningTeam(CDSConnectionSetup, TempSelectedCRMSystemuser));
    end;

    [Scope('OnPrem')]
    [NonDebuggable]
    procedure AddUsersToDefaultOwningTeam(var CDSConnectionSetup: Record "CDS Connection Setup"; var TempCRMSystemuser: Record "CRM Systemuser" temporary): Integer;
    var
        TempCDSTeammembership: Record "CDS Teammembership" temporary;
        TempSelectedCRMSystemuser: Record "CRM Systemuser" temporary;
        CrmHelper: DotNet CrmHelper;
        AdminUser: Text;
        AdminPassword: SecretText;
        AccessToken: SecretText;
        AdminADDomain: Text;
    begin
        CheckConnectionRequiredFields(CDSConnectionSetup, false);
        GetDefaultOwningTeamMembership(CDSConnectionSetup, TempCDSTeammembership);
        if TempCRMSystemuser.FindSet() then
            repeat
                TempCDSTeammembership.SetRange(SystemUserId, TempCRMSystemuser.SystemUserId);
                if TempCDSTeammembership.IsEmpty() then begin
                    TempSelectedCRMSystemuser.Init();
                    TempSelectedCRMSystemuser.TransferFields(TempCRMSystemuser);
                    TempSelectedCRMSystemuser.Insert();
                end;
            until TempCRMSystemuser.Next() = 0;

        if TempSelectedCRMSystemuser.IsEmpty() then
            exit(0);

        SignInCDSAdminUser(CDSConnectionSetup, CrmHelper, AdminUser, AdminPassword, AccessToken, AdminADDomain, true);
        exit(AddUsersToDefaultOwningTeam(CDSConnectionSetup, CrmHelper, TempSelectedCRMSystemuser));
    end;

    [Scope('OnPrem')]
    procedure AddCoupledUsersToDefaultOwningTeam(var CDSConnectionSetup: Record "CDS Connection Setup"; var CrmHelper: DotNet CrmHelper): Integer
    var
        TempCRMSystemuser: Record "CRM Systemuser" temporary;
    begin
        GetCoupledUsers(TempCRMSystemuser);
        exit(AddUsersToDefaultOwningTeam(CDSConnectionSetup, CrmHelper, TempCRMSystemuser));
    end;

    [Scope('OnPrem')]
    procedure AddUsersToDefaultOwningTeam(var CDSConnectionSetup: Record "CDS Connection Setup"; var CrmHelper: DotNet CrmHelper; var TempCRMSystemuser: Record "CRM Systemuser" temporary): Integer
    var
        Window: Dialog;
        TeamId: Guid;
        Added: Integer;
        UserNumber: Integer;
        UserCount: Integer;
    begin
        Session.LogMessage('0000C9M', AddCoupledUsersToTeamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        UserCount := TempCRMSystemuser.Count();
        if UserCount = 0 then begin
            Session.LogMessage('0000C9O', CoupledUsersNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit(0);
        end;

        TeamId := GetOwningTeamId(CDSConnectionSetup);
        if IsNullGuid(TeamId) then begin
            Session.LogMessage('0000C9P', TeamNotFoundTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit(0);
        end;

        if GuiAllowed then begin
            Window.Open(AddingCoupledUsersToTeamMsg);
            Window.Update(1, '');
        end;

        if TempCRMSystemuser.FindSet() then
            repeat
                if GuiAllowed then begin
                    UserNumber += 1;
                    Window.Update(1, StrSubstNo(ProcessingUserMsg, UserNumber, UserCount));
                end;
                if AddUserToTeam(CrmHelper, TempCRMSystemuser.SystemUserId, TeamId) then
                    Added += 1;
            until TempCRMSystemuser.Next() = 0;

        if GuiAllowed then
            Window.Close();

        exit(Added);
    end;

    local procedure GetCoupledUsers(var TempCRMSystemuser: Record "CRM Systemuser" temporary)
    var
        CRMIntegrationRecord: Record "CRM Integration Record";
    begin
        CRMIntegrationRecord.SetRange("Table ID", Database::"Salesperson/Purchaser");
        if CRMIntegrationRecord.FindSet() then
            repeat
                TempCRMSystemuser.Init();
                TempCRMSystemuser.SystemUserId := CRMIntegrationRecord."CRM ID";
                if TempCRMSystemuser.Insert() then;
            until CRMIntegrationRecord.Next() = 0;
    end;

    procedure GetTempConnectionSetup(var TempCDSConnectionSetup: Record "CDS Connection Setup" temporary; var CDSConnectionSetup: Record "CDS Connection Setup"; AccessToken: SecretText);
    var
        EmptySecretText: SecretText;
    begin
        GetTempAdminConnectionSetup(TempCDSConnectionSetup, CDSConnectionSetup, '', EmptySecretText, AccessToken, '');
    end;

    local procedure GetTempAdminConnectionSetup(var TempAdminCDSConnectionSetup: Record "CDS Connection Setup" temporary; var CDSConnectionSetup: Record "CDS Connection Setup"; AdminUser: Text; AdminPassword: SecretText; AccessToken: SecretText; AdminADDomain: Text);
    begin
        TempAdminCDSConnectionSetup.Init();
        TempAdminCDSConnectionSetup."Proxy Version" := CDSConnectionSetup.GetProxyVersion();
        TempAdminCDSConnectionSetup."Server Address" := CDSConnectionSetup."Server Address";
        if CDSConnectionSetup."Authentication Type" = CDSConnectionSetup."Authentication Type"::Office365 then begin
            TempAdminCDSConnectionSetup.SetAccessToken(AccessToken);
            SetConnectionString(TempAdminCDSConnectionSetup, OAuthConnectionStringFormatTxt);
        end;

        if CDSConnectionSetup."Authentication Type" <> CDSConnectionSetup."Authentication Type"::Office365 then begin
            TempAdminCDSConnectionSetup."Authentication Type" := CDSConnectionSetup."Authentication Type";
            TempAdminCDSConnectionSetup.Domain := CopyStr(AdminADDomain, 1, MaxStrLen(TempAdminCDSConnectionSetup.Domain));
            TempAdminCDSConnectionSetup."User Name" := CopyStr(AdminUser, 1, MaxStrLen(TempAdminCDSConnectionSetup."User Name"));
            TempAdminCDSConnectionSetup.SetPassword(AdminPassword);
            SetConnectionString(TempAdminCDSConnectionSetup, ReplaceUserNameInConnectionString(CDSConnectionSetup, AdminUser));
        end;
    end;

    [Scope('OnPrem')]
    procedure TestActiveConnection(): Boolean
    begin
        if TryCheckEntitiesAvailability() then begin
            Session.LogMessage('0000AT5', ConnectionTestSucceedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit(true);
        end;
        Session.LogMessage('0000AT6', ConnectionTestFailedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        exit(false);
    end;

    [Scope('OnPrem')]
    procedure TestSystemUsersAvailability()
    begin
        CheckSystemUsersAvailability();
    end;


    [Scope('OnPrem')]
    procedure CheckIntegrationRequirements(var CDSConnectionSetup: Record "CDS Connection Setup"; Silent: Boolean): Boolean
    begin
        if Silent then begin
            if not TryCheckIntegrationRequirements(CDSConnectionSetup) then begin
                Session.LogMessage('0000AT7', IntegrationRequirementsNotMetTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                exit(false);
            end;

            Session.LogMessage('0000AU0', IntegrationRequirementsMetTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit(true);
        end;

        CheckIntegrationRequirements(CDSConnectionSetup);
        exit(true);
    end;

    [Scope('OnPrem')]
    procedure CheckIntegrationSolutionRequirements(var CDSConnectionSetup: Record "CDS Connection Setup"; Silent: Boolean): Boolean
    begin
        if Silent then begin
            if not TryCheckIntegrationSolutionRequirements(CDSConnectionSetup) then begin
                Session.LogMessage('0000AT8', SolutionRequirementsNotMetTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                exit(false);
            end;
            exit(true);
        end;
        CheckIntegrationSolutionRequirements(CDSConnectionSetup);
        exit(true);
    end;

    [Scope('OnPrem')]
    procedure CheckIntegrationUserRequirements(var CDSConnectionSetup: Record "CDS Connection Setup"; Silent: Boolean): Boolean
    begin
        if Silent then begin
            if not TryCheckIntegrationUserRequirements(CDSConnectionSetup) then begin
                Session.LogMessage('0000AT9', IntegrationUserRequirementsNotMetTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                exit(false);
            end;
            exit(true);
        end;
        CheckIntegrationUserRequirements(CDSConnectionSetup);
        exit(true);
    end;

    [Scope('OnPrem')]
    procedure CheckOwningTeamRequirements(var CDSConnectionSetup: Record "CDS Connection Setup"; Silent: Boolean): Boolean
    begin
        if Silent then begin
            if not TryCheckOwningTeamRequirements(CDSConnectionSetup) then begin
                Session.LogMessage('0000ATA', OwningTeamRequirementsNotMetTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                exit(false);
            end;
            exit(true);
        end;
        CheckOwningTeamRequirements(CDSConnectionSetup);
        exit(true);
    end;

    [TryFunction]
    local procedure TryCheckIntegrationRequirements(var CDSConnectionSetup: Record "CDS Connection Setup")
    begin
        CheckIntegrationRequirements(CDSConnectionSetup);
    end;

    local procedure CheckIntegrationRequirements(var CDSConnectionSetup: Record "CDS Connection Setup")
    begin
        CheckIntegrationSolutionRequirements(CDSConnectionSetup);
        CheckIntegrationUserRequirements(CDSConnectionSetup);
        CheckOwningTeamRequirements(CDSConnectionSetup);
        CheckEntitiesAvailability(CDSConnectionSetup);
    end;

    [TryFunction]
    local procedure TryCheckIntegrationSolutionRequirements(var CDSConnectionSetup: Record "CDS Connection Setup")
    begin
        CheckIntegrationSolutionRequirements(CDSConnectionSetup);
    end;

    local procedure CheckIntegrationSolutionRequirements(var CDSConnectionSetup: Record "CDS Connection Setup")
    var
        Version: Text;
    begin
        if not GetSolutionVersion(CDSConnectionSetup, Version) then begin
            Session.LogMessage('0000ATC', SolutionNotInstalledTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(BaseIntegrationSolutionNotInstalledErr, GetBaseSolutionDisplayName());
        end;

        if not IsSolutionVersionValid(Version) then begin
            Session.LogMessage('0000ATD', InvalidSolutionVersionTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(SolutionVersionErr);
        end;
    end;

    [TryFunction]
    local procedure TryCheckIntegrationUserRequirements(var CDSConnectionSetup: Record "CDS Connection Setup")
    begin
        CheckIntegrationUserRequirements(CDSConnectionSetup);
    end;

    local procedure CheckIntegrationUserRequirements(var CDSConnectionSetup: Record "CDS Connection Setup")
    var
        CRMRole: Record "CRM Role";
        TempCRMRole: Record "CRM Role" temporary;
        CRMSystemuserroles: Record "CRM Systemuserroles";
        CRMSystemuser: Record "CRM Systemuser";
        IntegrationRoleName: Text;
        SystemAdminRoleName: Text;
        TempConnectionName: Text;
        IntegrationRoleDeployed: Boolean;
        ChosenUserHasSystemAdminRole: Boolean;
        ChosenUserHasIntegrationRole: Boolean;
    begin
        TempConnectionName := GetTempConnectionName();
        RegisterConnection(CDSConnectionSetup, TempConnectionName);
        SetDefaultTableConnection(TableConnectionType::CRM, TempConnectionName, true);

        if CRMRole.FindSet() then
            repeat
                TempCRMRole.TransferFields(CRMRole);
                TempCRMRole.Insert();
                if TempCRMRole.RoleId = GetIntegrationRoleId() then begin
                    IntegrationRoleDeployed := true;
                    IntegrationRoleName := TempCRMRole.Name;
                end;
            until CRMRole.Next() = 0;

        FilterUser(CDSConnectionSetup, CRMSystemuser);
        if CRMSystemuser.FindFirst() then begin
            if CRMSystemuser.IsDisabled then begin
                Session.LogMessage('0000ATF', UserNotActiveTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                Error(UserNotActiveErr, CDSConnectionSetup."User Name", CDSConnectionSetup."Server Address");
            end;

            if not CDSConnectionSetup."Connection String".Contains(ClientSecretAuthTxt) and not CDSConnectionSetup."Connection String".Contains(CertificateAuthTxt) then begin
                if not CRMSystemuser.IsIntegrationUser then begin
                    Session.LogMessage('0000B2E', NotIntegrationUserTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                    Error(NotIntegrationUserErr, CDSConnectionSetup."User Name", CDSConnectionSetup."Server Address");
                end;

                if CRMSystemuser.AccessMode <> CRMSystemuser.AccessMode::"Non-interactive" then begin
                    Session.LogMessage('0000B2F', NotNonInteractiveAccessModeTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                    Error(NotNonInteractiveAccessModeErr, CDSConnectionSetup."User Name", CDSConnectionSetup."Server Address", CRMSystemuser.AccessMode);
                end;
            end;

            CRMSystemuserroles.SetRange(SystemUserId, CRMSystemuser.SystemUserId);
            if CRMSystemuserroles.FindSet() then
                repeat
                    if TempCRMRole.Get(CRMSystemuserroles.RoleId) then begin
                        if TempCRMRole.RoleTemplateId = GetSystemAdminRoleTemplateId() then begin
                            ChosenUserHasSystemAdminRole := true;
                            SystemAdminRoleName := TempCRMRole.Name
                        end;
                        if TempCRMRole.RoleId = GetIntegrationRoleId() then
                            ChosenUserHasIntegrationRole := true;
                    end;
                until CRMSystemuserroles.Next() = 0
            else
                if (CDSConnectionSetup."Server Address" <> '') and (CDSConnectionSetup."Server Address" <> TestServerAddressTok) then begin
                    Session.LogMessage('0000ATH', UserHasNoRolesTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                    Error(UserHasNoRolesErr, CDSConnectionSetup."User Name", CDSConnectionSetup."Server Address");
                end;

            if ChosenUserHasSystemAdminRole then begin
                Session.LogMessage('0000ATI', SystemAdminRoleTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                Error(SystemAdminErr, CDSConnectionSetup."User Name", SystemAdminRoleName, CDSConnectionSetup."Server Address");
            end;

            if IntegrationRoleDeployed and (not ChosenUserHasIntegrationRole) then begin
                Session.LogMessage('0000ATJ', NoIntegrationRoleTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                Error(UserRolesErr, CDSConnectionSetup."User Name", IntegrationRoleName, CDSConnectionSetup."Server Address");
            end;
        end;

        UnregisterTableConnection(TableConnectionType::CRM, TempConnectionName);
    end;


    [Scope('OnPrem')]
    [NonDebuggable]
    procedure CheckIntegrationUserPrerequisites(var CDSConnectionSetup: Record "CDS Connection Setup"; AdminUserName: text; AdminPassword: SecretText; AccessToken: SecretText; AdminADDomain: Text)
    var
        TempCDSConnectionSetup: Record "CDS Connection Setup" temporary;
        CRMRole: Record "CRM Role";
        CRMSystemuserroles: Record "CRM Systemuserroles";
        CRMSystemuser: Record "CRM Systemuser";
        TempConnectionName: Text;
    begin
        GetTempAdminConnectionSetup(TempCDSConnectionSetup, CDSConnectionSetup, AdminUserName, AdminPassword, AccessToken, AdminADDomain);
        TempConnectionName := GetTempConnectionName();
        RegisterConnection(TempCDSConnectionSetup, TempConnectionName);
        SetDefaultTableConnection(TableConnectionType::CRM, TempConnectionName, true);

        FilterUser(CDSConnectionSetup, CRMSystemuser);
        if not CRMSystemuser.FindFirst() then begin
            Session.LogMessage('0000BNG', UserNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(UserDoesNotExistErr, CDSConnectionSetup."User Name");
        end;

        if CRMSystemuser.IsDisabled then begin
            Session.LogMessage('0000BNH', UserNotActiveTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(UserNotActiveErr, CDSConnectionSetup."User Name", CDSConnectionSetup."Server Address");
        end;

        if not (CRMSystemuser.AccessMode in [CRMSystemuser.AccessMode::"Read-Write", CRMSystemuser.AccessMode::"Non-interactive"]) then begin
            Session.LogMessage('0000B2G', InvalidAccessModeTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(InvalidAccessModeErr, CDSConnectionSetup."User Name", CDSConnectionSetup."Server Address", CRMSystemuser.AccessMode);
        end;

        if (not CRMSystemuser.IsLicensed) and (CRMSystemuser.AccessMode <> CRMSystemuser.AccessMode::"Non-interactive") then begin
            Session.LogMessage('0000ATG', UserNotLicensedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(UserNotLicensedErr, CDSConnectionSetup."User Name", CDSConnectionSetup."Server Address", CRMSystemuser.AccessMode);
        end;

        CRMSystemuserroles.SetRange(SystemUserId, CRMSystemuser.SystemUserId);
        if CRMSystemuserroles.FindSet() then
            repeat
                if CRMRole.Get(CRMSystemuserroles.RoleId) then
                    if CRMRole.RoleTemplateId = GetSystemAdminRoleTemplateId() then begin
                        Session.LogMessage('0000BNJ', SystemAdminRoleTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                        Error(SystemAdminErr, CDSConnectionSetup."User Name", CRMRole.Name, CDSConnectionSetup."Server Address");
                    end;
            until CRMSystemuserroles.Next() = 0;

        UnregisterTableConnection(TableConnectionType::CRM, TempConnectionName);
    end;

    [Scope('OnPrem')]
    [NonDebuggable]
    procedure CheckAdminUserPrerequisites(var CDSConnectionSetup: Record "CDS Connection Setup"; AdminUserName: Text; AdminPassword: Text; AccessToken: Text; AdminADDomain: Text)
    var
        TempCDSConnectionSetup: Record "CDS Connection Setup" temporary;
        CRMRole: Record "CRM Role";
        CRMSystemuserroles: Record "CRM Systemuserroles";
        CRMSystemuser: Record "CRM Systemuser";
        TempConnectionName: Text;
        HasSystemAdminRole: Boolean;
        HasSystemCustomizerRole: Boolean;
    begin
        GetTempAdminConnectionSetup(TempCDSConnectionSetup, CDSConnectionSetup, AdminUserName, AdminPassword, AccessToken, AdminADDomain);
        CheckConnectionRequiredFields(TempCDSConnectionSetup, false);
        TempConnectionName := GetTempConnectionName();
        RegisterConnection(TempCDSConnectionSetup, TempConnectionName);
        SetDefaultTableConnection(TableConnectionType::CRM, TempConnectionName, true);

        FilterUser(TempCDSConnectionSetup, CRMSystemuser);
        if not CRMSystemuser.FindFirst() then begin
            Session.LogMessage('0000BNK', UserNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(UserDoesNotExistErr, TempCDSConnectionSetup."User Name");
        end;

        if CRMSystemuser.IsDisabled then begin
            Session.LogMessage('0000BNL', UserNotActiveTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(UserNotActiveErr, TempCDSConnectionSetup."User Name", TempCDSConnectionSetup."Server Address");
        end;

        if not CRMSystemuser.IsLicensed then begin
            Session.LogMessage('0000BNM', UserNotLicensedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(UserNotLicensedErr, TempCDSConnectionSetup."User Name", TempCDSConnectionSetup."Server Address", CRMSystemuser.AccessMode);
        end;

        CRMSystemuserroles.SetRange(SystemUserId, CRMSystemuser.SystemUserId);
        if not CRMSystemuserroles.FindSet() then
            if (CDSConnectionSetup."Server Address" <> '') and (CDSConnectionSetup."Server Address" <> TestServerAddressTok) then begin
                Session.LogMessage('0000BNN', UserHasNoRolesTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                Error(UserHasNoRolesErr, TempCDSConnectionSetup."User Name", TempCDSConnectionSetup."Server Address");
            end;

        repeat
            if CRMRole.Get(CRMSystemuserroles.RoleId) then begin
                if CRMRole.RoleTemplateId = GetSystemAdminRoleTemplateId() then
                    HasSystemAdminRole := true;
                if CRMRole.RoleTemplateId = GetSystemCustomizerRoleTemplateId() then
                    HasSystemCustomizerRole := true;
            end;
        until CRMSystemuserroles.Next() = 0;

        if not HasSystemAdminRole then begin
            Session.LogMessage('0000BNO', NoSystemAdminRoleTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(NoSystemAdminRoleErr, TempCDSConnectionSetup."User Name", TempCDSConnectionSetup."Server Address");
        end;

        if not HasSystemCustomizerRole then
            Session.LogMessage('0000BNP', NoSystemCustomizerRoleTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        UnregisterTableConnection(TableConnectionType::CRM, TempConnectionName);
    end;

    [TryFunction]
    local procedure TryCheckOwningTeamRequirements(var CDSConnectionSetup: Record "CDS Connection Setup")
    begin
        CheckOwningTeamRequirements(CDSConnectionSetup);
    end;

    local procedure CheckOwningTeamRequirements(var CDSConnectionSetup: Record "CDS Connection Setup")
    var
        CRMRole: Record "CRM Role";
        CDSCompany: Record "CDS Company";
        CDSTeamroles: Record "CDS Teamroles";
        CRMTeam: Record "CRM Team";
        CRMBusinessunit: Record "CRM Businessunit";
        IntegrationRoleName: Text;
        TempConnectionName: Text;
    begin
        TempConnectionName := GetTempConnectionName();
        RegisterConnection(CDSConnectionSetup, TempConnectionName);
        SetDefaultTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName, true);

        if not CRMRole.Get(GetIntegrationRoleId()) then begin
            Session.LogMessage('0000ATK', IntegrationRoleNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(RoleNotFoundErr, GetIntegrationRoleId());
        end;
        IntegrationRoleName := CRMRole.Name;

        CDSCompany.SetRange(ExternalId, GetCompanyExternalId());
        if not CDSCompany.FindFirst() then begin
            Session.LogMessage('0000ATL', CompanyNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(CompanyNotFoundErr, GetCompanyExternalId());
        end;

        if not CRMTeam.Get(CDSCompany.DefaultOwningTeam) then begin
            Session.LogMessage('0000ATM', TeamNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(TeamNotFoundErr, CDSCompany.DefaultOwningTeam);
        end;

        if not CRMBusinessunit.Get(CRMTeam.BusinessUnitId) then begin
            Session.LogMessage('0000ATN', BusinessUnitNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(BusinessUnitNotFoundErr, CRMTeam.BusinessUnitId);
        end;

        if (CRMBusinessunit.BusinessUnitId <> CDSConnectionSetup."Business Unit Id") or
           (CRMBusinessunit.Name <> CDSConnectionSetup."Business Unit Name") then begin
            Session.LogMessage('0000B24', BusinessUnitMismatchTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(BusinessUnitMismatchErr);
        end;

        CRMRole.Reset();
        CRMRole.SetRange(BusinessUnitId, CRMBusinessunit.BusinessUnitId);
        CRMRole.SetRange(ParentRoleId, GetIntegrationRoleId());
        if not CRMRole.FindFirst() then begin
            Session.LogMessage('0000ATO', RoleNotFoundForBusinessUnitTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(IntegrationRoleNotFoundErr, IntegrationRoleName, CRMBusinessunit.Name);
        end;

        CDSTeamroles.SetRange(TeamId, CRMTeam.TeamId);
        CDSTeamroles.SetRange(RoleId, CRMRole.RoleId);
        if CDSTeamroles.IsEmpty() then begin
            Session.LogMessage('0000ATP', IntegrationRoleNotAssignedToTeamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(TeamRolesErr, CRMTeam.Name, CRMBusinessunit.Name, IntegrationRoleName);
        end;

        UnregisterTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName);
    end;

    [TryFunction]
    [NonDebuggable]
    procedure IsVirtualTablesAppInstalled(var TempAdminCDSConnectionSetup: Record "CDS Connection Setup" temporary)
    begin
        CheckVirtualTablesAppInstalled(TempAdminCDSConnectionSetup);
    end;

    [NonDebuggable]
    local procedure CheckVirtualTablesAppInstalled(var TempAdminCDSConnectionSetup: Record "CDS Connection Setup" temporary)
    var
        CRMBCVirtualTableConfig: Record "CRM BC Virtual Table Config.";
        CRMCompany: Record "CRM Company";
        TempConnectionName: Text;
    begin
        TempConnectionName := GetTempConnectionName();
        RegisterConnection(TempAdminCDSConnectionSetup, TempConnectionName);
        SetDefaultTableConnection(TableConnectionType::CRM, TempConnectionName, true);

        if CRMBCVirtualTableConfig.IsEmpty() then;
        if CRMCompany.IsEmpty() then;

        UnregisterTableConnection(TableConnectionType::CRM, TempConnectionName);
    end;

    [Scope('OnPrem')]
    procedure CheckEntitiesAvailability(var CDSConnectionSetup: Record "CDS Connection Setup"; Silent: Boolean): Boolean
    begin
        if Silent then
            exit(TryCheckEntitiesAvailability(CDSConnectionSetup));
        CheckEntitiesAvailability(CDSConnectionSetup);
        exit(true);
    end;

    [TryFunction]
    local procedure TryCheckEntitiesAvailability(var CDSConnectionSetup: Record "CDS Connection Setup")
    begin
        CheckEntitiesAvailability(CDSConnectionSetup);
    end;

    local procedure CheckEntitiesAvailability(var CDSConnectionSetup: Record "CDS Connection Setup")
    var
        CRMSystemuser: Record "CRM Systemuser";
        CRMTeam: Record "CRM Team";
        CRMRole: Record "CRM Role";
        CRMSystemuserroles: Record "CRM Systemuserroles";
        CRMBusinessunit: Record "CRM Businessunit";
        CRMAccount: Record "CRM Account";
        CRMContact: Record "CRM Contact";
        CDSCompany: Record "CDS Company";
        Id: Guid;
        TempConnectionName: Text;
    begin
        TempConnectionName := GetTempConnectionName();
        RegisterConnection(CDSConnectionSetup, TempConnectionName);
        SetDefaultTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName, true);

        if CRMTeam.FindFirst() then
            Id := CRMTeam.TeamId;

        if CRMRole.FindFirst() then
            Id := CRMRole.RoleId;

        if CRMSystemuser.FindFirst() then
            Id := CRMSystemuser.SystemUserId;

        if CRMSystemuserroles.FindFirst() then
            Id := CRMSystemuserroles.SystemUserId;

        if CRMBusinessunit.FindFirst() then
            Id := CRMBusinessunit.BusinessUnitId;

        if CDSCompany.FindFirst() then
            Id := CDSCompany.CompanyId;

        if CRMAccount.FindFirst() then
            Id := CRMAccount.CompanyId;

        if CRMContact.FindFirst() then
            Id := CRMContact.CompanyId;

        UnregisterTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName);
    end;

    local procedure CheckSystemUsersAvailability()
    var
        CDSSystemuser: Record "CRM Systemuser";
        Id: Guid;
    begin
        if CDSSystemuser.FindFirst() then
            Id := CDSSystemuser.SystemUserId;
    end;

    [TryFunction]
    local procedure TryCheckEntitiesAvailability()
    var
        CRMSystemuser: Record "CRM Systemuser";
        CRMTeam: Record "CRM Team";
        CRMRole: Record "CRM Role";
        CRMSystemuserroles: Record "CRM Systemuserroles";
        CRMBusinessunit: Record "CRM Businessunit";
        CRMAccount: Record "CRM Account";
        CRMContact: Record "CRM Contact";
        CDSCompany: Record "CDS Company";
        Id: Guid;
    begin
        if CRMTeam.FindFirst() then
            Id := CRMTeam.TeamId;

        if CRMRole.FindFirst() then
            Id := CRMRole.RoleId;

        if CRMSystemuser.FindFirst() then
            Id := CRMSystemuser.SystemUserId;

        if CRMSystemuserroles.FindFirst() then
            Id := CRMSystemuserroles.SystemUserId;

        if CRMBusinessunit.FindFirst() then
            Id := CRMBusinessunit.BusinessUnitId;

        if CDSCompany.FindFirst() then
            Id := CDSCompany.CompanyId;

        if CRMAccount.FindFirst() then
            Id := CRMAccount.CompanyId;

        if CRMContact.FindFirst() then
            Id := CRMContact.CompanyId;
    end;

    [Scope('OnPrem')]
    procedure SelectSDKVersion(var CDSConnectionSetup: Record "CDS Connection Setup"): Boolean
    var
        TempStack: Record TempStack temporary;
    begin
        if PAGE.RunModal(PAGE::"SDK Version List", TempStack) = ACTION::LookupOK then begin
            CDSConnectionSetup.Validate("Proxy Version", TempStack.StackOrder);
            exit(true);
        end;
        exit(false);
    end;

    [Scope('OnPrem')]
    procedure SelectBusinessUnit(var CDSConnectionSetup: Record "CDS Connection Setup"): Boolean
    var
        TempCRMBusinessUnit: Record "CRM Businessunit" temporary;
        EmptyGuid: Guid;
        PrevBusinessUnitId: Guid;
        DefaultBusinessUnitName: Text[160];
        Changed: Boolean;
        AddArtificialDefaultBusinessUnit: Boolean;
    begin
        PrevBusinessUnitId := CDSConnectionSetup."Business Unit Id";
        DefaultBusinessUnitName := GetDefaultBusinessUnitName();

        if not GetFirstLevelBusinessUnits(CDSConnectionSetup, TempCRMBusinessUnit) then begin
            Changed := not IsNullGuid(PrevBusinessUnitId);
            if Changed then begin
                CDSConnectionSetup."Business Unit Id" := EmptyGuid;
                CDSConnectionSetup."Business Unit Name" := DefaultBusinessUnitName;
            end;
            exit(Changed);
        end;

        TempCRMBusinessUnit.SetRange(Name, DefaultBusinessUnitName);
        AddArtificialDefaultBusinessUnit := TempCRMBusinessUnit.IsEmpty();
        TempCRMBusinessUnit.SetRange(Name);
        if AddArtificialDefaultBusinessUnit then begin
            TempCRMBusinessUnit.Init();
            TempCRMBusinessUnit.BusinessUnitId := EmptyGuid;
            TempCRMBusinessUnit.Name := StrSubstNo(NewBusinessUnitNameTemplateTok, DefaultBusinessUnitName);
            TempCRMBusinessUnit.Insert();
        end;

        if PAGE.RunModal(PAGE::"CDS Business Units", TempCRMBusinessUnit) <> ACTION::LookupOK then
            exit(false);

        Changed := TempCRMBusinessUnit.BusinessUnitId <> PrevBusinessUnitId;
        if Changed then begin
            CDSConnectionSetup."Business Unit Id" := TempCRMBusinessUnit.BusinessUnitId;
            if IsNullGuid(CDSConnectionSetup."Business Unit Id") then
                CDSConnectionSetup."Business Unit Name" := DefaultBusinessUnitName
            else
                CDSConnectionSetup."Business Unit Name" := TempCRMBusinessUnit.Name;
        end;
        exit(Changed);
    end;

    [TryFunction]
    local procedure GetFirstLevelBusinessUnits(var CDSConnectionSetup: Record "CDS Connection Setup"; var TempCRMBusinessunit: Record "CRM Businessunit" temporary)
    var
        CRMBusinessunit: Record "CRM Businessunit";
        TempConnectionName: Text;
        RootBusinessUnitId: Guid;
        EmptyGuid: Guid;
    begin
        CheckConnectionRequiredFields(CDSConnectionSetup, false);

        TempConnectionName := GetTempConnectionName();
        RegisterConnection(CDSConnectionSetup, TempConnectionName);
        SetDefaultTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName, true);

        // Table connection is scoped, therefore all manipulations with CDS tables must be placed
        // in this procedure between SetDefaultTableConnection and UnregisterConnection

        CRMBusinessunit.SetRange(ParentBusinessUnitId, EmptyGuid);
        if CRMBusinessunit.FindFirst() then begin
            RootBusinessUnitId := CRMBusinessunit.BusinessUnitId;
            CRMBusinessunit.SetRange(ParentBusinessUnitId, RootBusinessUnitId);
            CRMBusinessunit.SetRange(IsDisabled, false);
            if CRMBusinessunit.FindSet() then
                repeat
                    TempCRMBusinessunit.Init();
                    TempCRMBusinessUnit.BusinessUnitId := CRMBusinessunit.BusinessUnitId;
                    TempCRMBusinessUnit.Name := CRMBusinessUnit.Name;
                    TempCRMBusinessunit.Insert();
                until CRMBusinessunit.Next() = 0;
            if TempCRMBusinessunit.FindFirst() then;
        end;

        UnregisterTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName);
    end;

    [Scope('OnPrem')]
    procedure CheckConnectionRequiredFields(var CDSConnectionSetup: Record "CDS Connection Setup"; Silent: Boolean): Boolean
    var
        Success: Boolean;
        TelemetryTxt: Text;
        ErrorTxt: Text;
    begin
        Success := true;
        if CDSConnectionSetup."Server Address" = '' then
            Success := false;
        if CDSConnectionSetup."Authentication Type" <> CDSConnectionSetup."Authentication Type"::Office365 then begin
            if CDSConnectionSetup."User Name" = '' then
                Success := false;
            if not CDSConnectionSetup.HasPassword() then
                Success := false;
        end;
        if not Success then begin
            if CDSConnectionSetup."Authentication Type" <> CDSConnectionSetup."Authentication Type"::Office365 then begin
                TelemetryTxt := ConnectionRequiredFieldsTxt;
                ErrorTxt := ConnectionRequiredFieldsErr;
            end else begin
                TelemetryTxt := Office365ConnectionRequiredFieldsTxt;
                ErrorTxt := Office365ConnectionRequiredFieldsErr;
            end;

            Session.LogMessage('0000ATQ', TelemetryTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            if not Silent then
                Error(ErrorTxt);
        end;
        exit(Success);
    end;


    [Scope('OnPrem')]
    procedure CheckConnectionRequiredFieldsMatch(var CDSConnectionSetup: Record "CDS Connection Setup"; Silent: Boolean): Boolean
    var
        CRMConnectionSetup: Record "CRM Connection Setup";
        Success: Boolean;
    begin
        Success := true;

        if not CRMConnectionSetup.Get() then
            exit(true);

        if not CRMConnectionSetup."Is Enabled" then
            exit(true);

        if CDSConnectionSetup."Server Address" <> CRMConnectionSetup."Server Address" then
            Success := false;
        if CDSConnectionSetup."User Name" <> CRMConnectionSetup."User Name" then
            Success := false;
        if CDSConnectionSetup."User Password Key" <> CRMConnectionSetup."User Password Key" then
            Success := false;
        if CDSConnectionSetup."Authentication Type" <> CRMConnectionSetup."Authentication Type" then
            Success := false;
        if not Success then begin
            Session.LogMessage('0000BCM', ConnectionRequiredFieldsMismatchTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            if not Silent then
                Error(ConnectionRequiredFieldsMismatchErr);
        end;
        exit(Success);
    end;

    local procedure FilterUser(var CDSConnectionSetup: Record "CDS Connection Setup"; var CRMSystemuser: Record "CRM Systemuser")
    begin
        case CDSConnectionSetup."Authentication Type" of
            CDSConnectionSetup."Authentication Type"::Office365, CDSConnectionSetup."Authentication Type"::OAuth:
                CRMSystemuser.SetFilter(InternalEMailAddress, StrSubstNo('@%1', CDSConnectionSetup."User Name"));
            CDSConnectionSetup."Authentication Type"::AD, CDSConnectionSetup."Authentication Type"::IFD:
                CRMSystemuser.SetFilter(DomainName, StrSubstNo('@%1', CDSConnectionSetup."User Name"));
        end;
    end;

    local procedure FilterTeam(BusinessUnitId: Guid; TeamName: Text; var CRMTeam: Record "CRM Team")
    begin
        CRMTeam.SetRange(Name, TeamName);
        CRMTeam.SetRange(BusinessUnitId, BusinessUnitId);
        CRMTeam.SetRange(IsDefault, false);
    end;

    local procedure GetUserName(var CDSConnectionSetup: Record "CDS Connection Setup") UserName: Text
    begin
        if CDSConnectionSetup."User Name" = '' then
            UserName := MissingUsernameTok
        else
            UserName := CopyStr(CDSConnectionSetup."User Name", StrPos(CDSConnectionSetup."User Name", '\') + 1);
    end;

    [Scope('OnPrem')]
    procedure RegisterAssistedSetup()
    var
        GuidedExperience: Codeunit "Guided Experience";
        Language: Codeunit Language;
        ModuleInfo: ModuleInfo;
        AssistedSetupGroup: Enum "Assisted Setup Group";
        VideoCategory: Enum "Video Category";
        GuidedExperienceType: Enum "Guided Experience Type";
        CurrentGlobalLanguage: Integer;
    begin
        if GuidedExperience.Exists(GuidedExperienceType::"Assisted Setup", ObjectType::Page, Page::"CDS Connection Setup Wizard") then
            exit;

        CurrentGlobalLanguage := GLOBALLANGUAGE;
        NavApp.GetCurrentModuleInfo(ModuleInfo);
        GuidedExperience.InsertAssistedSetup(CDSConnectionSetupTitleTxt, CDSConnectionSetupShortTitleTxt, CDSConnectionSetupDescriptionTxt, 10, ObjectType::Page,
            Page::"CDS Connection Setup Wizard", AssistedSetupGroup::Connect, '', VideoCategory::Connect, CDSConnectionSetupHelpTxt);
        GLOBALLANGUAGE(Language.GetDefaultApplicationLanguageId());
        GuidedExperience.AddTranslationForSetupObjectTitle(GuidedExperienceType::"Assisted Setup", ObjectType::Page,
            Page::"CDS Connection Setup Wizard", Language.GetDefaultApplicationLanguageId(), CDSConnectionSetupTitleTxt);
        GLOBALLANGUAGE(CurrentGlobalLanguage);
    end;

    [NonDebuggable]
    local procedure GetConnectionStringWithCredentials(var CDSConnectionSetup: Record "CDS Connection Setup"): SecretText
    var
        ConnectionStringWithPassword: SecretText;
        ConnectionStringWithPasswordPlaceholder: Text;
        ConnectionStringWithClientSecret: SecretText;
        ConnectionStringWithClientSecretPlaceholder: Text;
        ConnectionStringWithCertificate: Text;
        ConnectionStringWithAccessToken: SecretText;
        ConnectionStringWithAccessTokenPlaceholder: Text;
        PasswordPlaceHolderPos: Integer;
    begin
        if CDSConnectionSetup."Connection String" = '' then
            exit;

        // if the setup record is temporary and user name is empty, this is a temp setup record constructed for the admin log-on
        // in this case, the connection string contains the URL and access token, so just use the connection string
        if CDSConnectionSetup.IsTemporary() then
            if CDSConnectionSetup."User Name" = '' then begin
                ConnectionStringWithAccessTokenPlaceholder := StrSubstNo(OAuthConnectionStringFormatTxt, CDSConnectionSetup."Server Address", '%1', CDSConnectionSetup.GetProxyVersion(), GetAuthenticationTypeToken(CDSConnectionSetup));
                ConnectionStringWithAccessToken := SecretStrSubstNo(ConnectionStringWithAccessTokenPlaceholder, CDSConnectionSetup.GetSecretAccessToken());
                exit(ConnectionStringWithAccessToken);
            end;

        // if auth type is Office365 and connection string contains {ClientSecret} token
        // then we will connect via OAuth client credentials grant flow, and construct the connection string accordingly, with the actual client secret
        if CDSConnectionSetup."Authentication Type" = CDSConnectionSetup."Authentication Type"::Office365 then begin
            if CDSConnectionSetup."Connection String".Contains(ClientSecretTok) then begin
                ConnectionStringWithClientSecretPlaceholder := StrSubstNo(ClientSecretConnectionStringFormatTxt, ClientSecretAuthTxt, CDSConnectionSetup."Server Address", GetCDSConnectionClientId(), '%1', CDSConnectionSetup.GetProxyVersion());
                ConnectionStringWithClientSecret := SecretStrSubstNo(ConnectionStringWithClientSecretPlaceholder, GetCDSConnectionClientSecret());
                Session.LogMessage('0000GRU', GetCDSConnectionClientId(), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                exit(ConnectionStringWithClientSecret);
            end;

            if CDSConnectionSetup."Connection String".Contains(CertificateTok) then begin
                ConnectionStringWithCertificate := StrSubstNo(CertificateConnectionStringFormatTxt, CertificateAuthTxt, CDSConnectionSetup."Server Address", GetCDSConnectionFirstPartyAppId(), GetCDSConnectionFirstPartyAppcertificate(), CDSConnectionSetup.GetProxyVersion());
                exit(ConnectionStringWithCertificate);
            end;
        end;

        PasswordPlaceHolderPos := StrPos(CDSConnectionSetup."Connection String", MissingPasswordTok);
        ConnectionStringWithPasswordPlaceholder := CopyStr(CDSConnectionSetup."Connection String", 1, PasswordPlaceHolderPos - 1) + '%1' + CopyStr(CDSConnectionSetup."Connection String", PasswordPlaceHolderPos + StrLen(MissingPasswordTok));
        ConnectionStringWithPassword := SecretStrSubstNo(ConnectionStringWithPasswordPlaceholder, CDSConnectionSetup.GetSecretPassword());
        exit(ConnectionStringWithPassword);
    end;

    [Scope('OnPrem')]
    procedure UpdateConnectionString(var CDSConnectionSetup: Record "CDS Connection Setup")
    var
        ConnectionString: Text;
        ConnectionStringWithClientSecret: Text;
        ConnectionStringWithCertificate: Text;
    begin
        if CDSConnectionSetup."Authentication Type" = CDSConnectionSetup."Authentication Type"::Office365 then begin
            if CDSConnectionSetup."Connection String".Contains(ClientSecretAuthTxt) then begin
                ConnectionStringWithClientSecret := StrSubstNo(ClientSecretConnectionStringFormatTxt, ClientSecretAuthTxt, CDSConnectionSetup."Server Address", ClientIdTok, ClientSecretTok, CDSConnectionSetup.GetProxyVersion());
                SetConnectionString(CDSConnectionSetup, ConnectionStringWithClientSecret);
                exit;
            end;

            if CDSConnectionSetup."Connection String".Contains(CertificateAuthTxt) then begin
                ConnectionStringWithCertificate := StrSubstNo(CertificateConnectionStringFormatTxt, CertificateAuthTxt, CDSConnectionSetup."Server Address", ClientIdTok, CertificateTok, CDSConnectionSetup.GetProxyVersion());
                SetConnectionString(CDSConnectionSetup, ConnectionStringWithCertificate);
                exit;
            end;
        end;

        ConnectionString :=
          StrSubstNo(
            ConnectionStringFormatTok, CDSConnectionSetup."Server Address", GetUserName(CDSConnectionSetup), MissingPasswordTok, CDSConnectionSetup.GetProxyVersion(), GetAuthenticationTypeToken(CDSConnectionSetup));
        SetConnectionString(CDSConnectionSetup, ConnectionString);
    end;

    [Scope('OnPrem')]
    procedure GetConnectionString(var CDSConnectionSetup: Record "CDS Connection Setup") ConnectionString: Text
    begin
        ConnectionString := CDSConnectionSetup."Connection String";
    end;

    [Scope('OnPrem')]
    procedure SetConnectionString(var CDSConnectionSetup: Record "CDS Connection Setup"; ConnectionString: Text)
    begin
        if ConnectionString = '' then
            Clear(CDSConnectionSetup."Connection String")
        else begin
            if CDSConnectionSetup."Authentication Type" <> CDSConnectionSetup."Authentication Type"::Office365 then
                if StrPos(ConnectionString, MissingPasswordTok) = 0 then
                    Error(ConnectionStringPwdPlaceHolderMissingErr);
            CDSConnectionSetup."Connection String" := CopyStr(ConnectionString, 1, MaxStrLen(CDSConnectionSetup."Connection String"));
        end;
        if not CDSConnectionSetup.Modify() then;
    end;

    [Scope('OnPrem')]
    [TryFunction]
    procedure TryGetCDSCompany(var CDSCompany: Record "CDS Company")
    begin
        GetCDSCompany(CDSCompany);
    end;

    [Scope('OnPrem')]
    procedure GetCDSCompany(var CDSCompany: Record "CDS Company")
    begin
        CDSCompany.SetRange(ExternalId, GetCompanyExternalId());
        CDSCompany.FindFirst();
    end;

    [Scope('OnPrem')]
    [TryFunction]
    procedure TryGetCompanyId(var CompanyId: Guid)
    begin
        CompanyId := GetCachedCompanyId();
    end;

    [Scope('OnPrem')]
    procedure GetCoupledBusinessUnitId(): Guid
    var
        CDSConnectionSetup: Record "CDS Connection Setup";
        EmptyGuid: Guid;
    begin
        if CDSConnectionSetup.Get() then
            exit(CDSConnectionSetup."Business Unit Id");
        exit(EmptyGuid);
    end;

    [Scope('OnPrem')]
    procedure GetIntegrationUserId(var CDSConnectionSetup: Record "CDS Connection Setup") IntegrationUserId: Guid
    var
        CRMSystemuser: Record "CRM Systemuser";
        TempConnectionName: Text;
        HasPersistentConnection: Boolean;
    begin
        CheckConnectionRequiredFields(CDSConnectionSetup, false);

        HasPersistentConnection := IsConnectionActive(CDSConnectionSetup, GetConnectionDefaultName());
        if not HasPersistentConnection then begin
            TempConnectionName := GetTempConnectionName();
            RegisterConnection(CDSConnectionSetup, TempConnectionName);
            SetDefaultTableConnection(TableConnectionType::CRM, TempConnectionName, true);
        end;

        FilterUser(CDSConnectionSetup, CRMSystemuser);
        if CRMSystemuser.FindFirst() then
            IntegrationUserID := CRMSystemuser.SystemUserId;

        if not HasPersistentConnection then
            UnregisterTableConnection(TableConnectionType::CRM, TempConnectionName);

        if IsNullGuid(IntegrationUserID) then
            ShowError(UserSetupTxt, CannotResolveUserFromConnectionSetupErr);
    end;

    [Scope('OnPrem')]
    procedure GetOwningTeamId(var CDSConnectionSetup: Record "CDS Connection Setup"): Guid
    var
        CRMBusinessUnit: Record "CRM Businessunit";
        CRMTeam: Record "CRM team";
        BusinessUnitId: Guid;
        TeamId: Guid;
        TeamName: Text;
        TempConnectionName: Text;
        HasPersistentConnection: Boolean;
    begin
        CheckConnectionRequiredFields(CDSConnectionSetup, false);
        BusinessUnitId := CDSConnectionSetup."Business Unit Id";
        if IsNullGuid(BusinessUnitId) then begin
            Session.LogMessage('0000GDF', TeamNotFoundTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit(TeamId);
        end;

        HasPersistentConnection := IsConnectionActive(CDSConnectionSetup, GetConnectionDefaultName());
        if not HasPersistentConnection then begin
            TempConnectionName := GetTempConnectionName();
            RegisterConnection(CDSConnectionSetup, TempConnectionName);
            SetDefaultTableConnection(TableConnectionType::CRM, TempConnectionName, true);
        end;

        if CRMBusinessUnit.Get(BusinessUnitId) then begin
            TeamName := GetOwningTeamName(CRMBusinessUnit.Name);
            FilterTeam(BusinessUnitId, TeamName, CRMTeam);
            if CRMTeam.FindFirst() then
                TeamId := CRMTeam.TeamId
            else
                Session.LogMessage('0000GDG', TeamNotFoundTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        end else
            Session.LogMessage('0000GDH', TeamNotFoundTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        if not HasPersistentConnection then
            UnregisterTableConnection(TableConnectionType::CRM, TempConnectionName);

        exit(TeamId);
    end;

    [Scope('OnPrem')]
    procedure GetDefaultOwningTeamMembership(var TempCDSTeammembership: Record "CDS Teammembership" temporary)
    var
        CDSConnectionSetup: Record "CDS Connection Setup";
    begin
        if CDSConnectionSetup.Get() then
            GetDefaultOwningTeamMembership(CDSConnectionSetup, TempCDSTeammembership);
    end;

    [Scope('OnPrem')]
    procedure GetDefaultOwningTeamMembership(var CDSConnectionSetup: Record "CDS Connection Setup"; var TempCDSTeammembership: Record "CDS Teammembership" temporary)
    var
        CDSTeammembership: Record "CDS Teammembership";
        BusinessUnitId: Guid;
        TeamId: Guid;
        TempConnectionName: Text;
        HasPersistentConnection: Boolean;
    begin
        if not CheckConnectionRequiredFields(CDSConnectionSetup, true) then
            exit;

        BusinessUnitId := CDSConnectionSetup."Business Unit Id";
        if IsNullGuid(BusinessUnitId) then
            exit;

        TeamId := GetOwningTeamId(CDSConnectionSetup);
        if not IsNullGuid(TeamId) then begin
            HasPersistentConnection := IsConnectionActive(CDSConnectionSetup, GetConnectionDefaultName());
            if not HasPersistentConnection then begin
                TempConnectionName := GetTempConnectionName();
                RegisterConnection(CDSConnectionSetup, TempConnectionName);
                SetDefaultTableConnection(TableConnectionType::CRM, TempConnectionName, true);
            end;

            TempCDSTeammembership.Reset();
            TempCDSTeammembership.DeleteAll();

            CDSTeammembership.SetRange(TeamId, TeamId);
            if CDSTeammembership.FindSet() then
                repeat
                    TempCDSTeammembership.Init();
                    TempCDSTeammembership.TransferFields(CDSTeammembership);
                    TempCDSTeammembership.Insert();
                until CDSTeammembership.Next() = 0;

            if not HasPersistentConnection then
                UnregisterTableConnection(TableConnectionType::CRM, TempConnectionName);
        end;
    end;

    local procedure AssignUserRole(var CrmHelper: DotNet CrmHelper; UserId: Guid; RoleId: Guid): Boolean
    begin
        if CrmHelper.CheckRoleAssignedToUser(UserId, RoleId) then
            exit(true);

        if not TryAssignUserRole(CrmHelper, UserId, RoleId) then begin
            Session.LogMessage('0000ATR', CannotAssignRoleToUserTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit(false);
        end;

        if CrmHelper.CheckRoleAssignedToUser(UserId, RoleId) then
            exit(true);

        Session.LogMessage('0000ATS', CannotAssignRoleToUserTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        exit(false);
    end;

    [TryFunction]
    local procedure TryAssignUserRole(var CrmHelper: DotNet CrmHelper; UserId: Guid; RoleId: Guid)
    begin
        CrmHelper.AssociateUserWithRole(UserId, RoleId);
    end;

    [TryFunction]
    local procedure TryGetUserId(var CrmHelper: DotNet CrmHelper; UserName: Text; var UserId: Guid)
    begin
        UserId := CrmHelper.GetUserId(UserName);
    end;

    local procedure AssignPreviousIntegrationUserRoles(var CrmHelper: DotNet CrmHelper; var CDSConnectionSetup: Record "CDS Connection Setup"; AccessToken: SecretText)
    var
        CRMConnectionSetup: Record "CRM Connection Setup";
        Company: Record Company;
        TempAdminCDSConnectionSetup: Record "CDS Connection Setup" temporary;
        CRMSystemUserRoles: Record "CRM Systemuserroles";
        RootCRMBusinessunit: Record "CRM Businessunit";
        DefaultCRMBusinessunit: Record "CRM Businessunit";
        DefaultCRMTeam: Record "CRM Team";
        CRMRole: Record "CRM Role";
        PreviousIntegrationUserRole: Record "CRM Role";
        TeamName: Text[160];
        CompanyId: Text[36];
        CompanyName: Text[30];
        BusinessUnitName: Text[160];
        TempConnectionName: Text;
        EmptySecretText: SecretText;
        PreviousIntegrationUserId: Guid;
        CurrentIntegrationUserId: Guid;
        EmptyGuid: Guid;
    begin
        if not CRMConnectionSetup.Get() then
            exit;

        if CRMConnectionSetup."User Name" = '' then
            exit;

        if CRMConnectionSetup."User Name" = CDSConnectionSetup."User Name" then
            exit;

        if not TryGetUserId(CrmHelper, CRMConnectionSetup."User Name", PreviousIntegrationUserId) then begin
            Session.LogMessage('0000D4A', UserNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit;
        end;

        if not TryGetUserId(CrmHelper, CDSConnectionSetup."User Name", CurrentIntegrationUserId) then begin
            Session.LogMessage('0000D4B', UserNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit;
        end;

        ResetCache();

        Company.Get(CompanyName());
        CompanyId := GetCompanyExternalId(Company);
        CompanyName := CopyStr(Company.Name, 1, MaxStrLen(CompanyName));
        BusinessUnitName := GetDefaultBusinessUnitName(CompanyName, CompanyId);

        GetTempAdminConnectionSetup(TempAdminCDSConnectionSetup, CDSConnectionSetup, '', EmptySecretText, AccessToken, '');
        TempConnectionName := GetTempConnectionName();
        RegisterConnection(TempAdminCDSConnectionSetup, TempConnectionName);
        SetDefaultTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName, true);

        // Table connection is scoped, therefore all manipulations with CDS tables must be placed
        // in this procedure between SetDefaultTableConnection and UnregisterConnection

        RootCRMBusinessunit.SetRange(ParentBusinessUnitId, EmptyGuid);
        RootCRMBusinessunit.FindFirst();
        DefaultCRMBusinessunit.SetRange(ParentBusinessUnitId, RootCRMBusinessunit.BusinessUnitId);
        DefaultCRMBusinessunit.SetRange(Name, BusinessUnitName);
        if not DefaultCRMBusinessunit.FindFirst() then
            exit;
        TeamName := GetOwningTeamName(DefaultCRMBusinessunit.Name);
        FilterTeam(DefaultCRMBusinessunit.BusinessUnitId, TeamName, DefaultCRMTeam);
        if not DefaultCRMTeam.FindFirst() then
            exit;

        // assign the roles of the previous integration user to the current (newly set up) integration user and to the default owning team
        // if one of those roles is System Administrator, ignore it
        CRMSystemUserRoles.SetRange(SystemUserId, PreviousIntegrationUserId);
        if CRMSystemUserRoles.FindSet() then
            repeat
                if PreviousIntegrationUserRole.Get(CRMSystemUserRoles.RoleId) then
                    if PreviousIntegrationUserRole.RoleTemplateId <> GetSystemAdminRoleTemplateId() then begin
                        if not TryAssignUserRole(CrmHelper, CurrentIntegrationUserId, CRMSystemUserRoles.RoleId) then
                            Session.LogMessage('0000D4C', CannotAssignRoleToUserTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

                        CRMRole.SetRange(ParentRoleId, CRMSystemUserRoles.RoleId);
                        CRMRole.SetRange(BusinessUnitId, DefaultCRMBusinessunit.BusinessUnitId);
                        if not CRMRole.FindFirst() then
                            Session.LogMessage('0000D4D', RoleNotFoundForBusinessUnitTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok)
                        else
                            if not AssignTeamRole(CrmHelper, DefaultCRMTeam.TeamId, CRMRole.RoleId) then
                                Session.LogMessage('0000D4E', CannotAssignRoleToTeamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok)
                    end;
            until CRMSystemUserRoles.Next() = 0;

        UnregisterTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName);
        Session.LogMessage('0000D4F', PreviousIntegrationUserRolesAddedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
    end;

    local procedure AssignPreviousIntegrationUserRoles(var CrmHelper: DotNet CrmHelper; PreviousIntegrationUserName: Text[250]; var CDSConnectionSetup: Record "CDS Connection Setup"; AccessToken: SecretText)
    var
        TempAdminCDSConnectionSetup: Record "CDS Connection Setup" temporary;
        CRMSystemUserRoles: Record "CRM Systemuserroles";
        PreviousIntegrationUserRole: Record "CRM Role";
        TempConnectionName: Text;
        PreviousIntegrationUserId: Guid;
        CurrentIntegrationUserId: Guid;
        EmptySecretText: SecretText;
    begin
        if CDSConnectionSetup."User Name" = PreviousIntegrationUserName then
            exit;

        if not TryGetUserId(CrmHelper, PreviousIntegrationUserName, PreviousIntegrationUserId) then begin
            Session.LogMessage('0000D4A', UserNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit;
        end;

        if not TryGetUserId(CrmHelper, CDSConnectionSetup."User Name", CurrentIntegrationUserId) then begin
            Session.LogMessage('0000D4B', UserNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit;
        end;

        ResetCache();

        GetTempAdminConnectionSetup(TempAdminCDSConnectionSetup, CDSConnectionSetup, '', EmptySecretText, AccessToken, '');
        TempConnectionName := GetTempConnectionName();
        RegisterConnection(TempAdminCDSConnectionSetup, TempConnectionName);
        SetDefaultTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName, true);

        // Table connection is scoped, therefore all manipulations with CDS tables must be placed
        // in this procedure between SetDefaultTableConnection and UnregisterConnection

        // assign the roles of the previous integration user to the current (newly set up) integration user
        CRMSystemUserRoles.SetRange(SystemUserId, PreviousIntegrationUserId);
        if CRMSystemUserRoles.FindSet() then
            repeat
                if PreviousIntegrationUserRole.Get(CRMSystemUserRoles.RoleId) then
                    if not TryAssignUserRole(CrmHelper, CurrentIntegrationUserId, CRMSystemUserRoles.RoleId) then
                        Session.LogMessage('0000D4C', CannotAssignRoleToUserTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            until CRMSystemUserRoles.Next() = 0;

        UnregisterTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName);
        Session.LogMessage('0000D4F', PreviousIntegrationUserRolesAddedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
    end;

    [NonDebuggable]
    local procedure AssignPreviousIntegrationUserRoles(var CrmHelper: DotNet CrmHelper; PreviousIntegrationUserName: Text[250]; var CDSConnectionSetup: Record "CDS Connection Setup")
    var
        CurrentCDSConnectionSetup: Record "CDS Connection Setup";
        CRMSystemUserRoles: Record "CRM Systemuserroles";
        PreviousIntegrationUserRole: Record "CRM Role";
        TempConnectionName: Text;
        PreviousIntegrationUserId: Guid;
        CurrentIntegrationUserId: Guid;
    begin
        if CDSConnectionSetup."User Name" = PreviousIntegrationUserName then
            exit;

        if not TryGetUserId(CrmHelper, PreviousIntegrationUserName, PreviousIntegrationUserId) then begin
            Session.LogMessage('0000GJK', UserNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit;
        end;

        if not TryGetUserId(CrmHelper, CDSConnectionSetup."User Name", CurrentIntegrationUserId) then begin
            Session.LogMessage('0000GJL', UserNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit;
        end;

        ResetCache();

        CurrentCDSConnectionSetup.Get();
        // register the connection with the current integration user, not with an admin
        TempConnectionName := GetTempConnectionName();
        RegisterConnection(CurrentCDSConnectionSetup, TempConnectionName);
        SetDefaultTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName, true);

        // Table connection is scoped, therefore all manipulations with CDS tables must be placed
        // in this procedure between SetDefaultTableConnection and UnregisterConnection

        // assign the roles of the previous integration user to the current (newly set up) integration user
        CRMSystemUserRoles.SetRange(SystemUserId, PreviousIntegrationUserId);
        if CRMSystemUserRoles.FindSet() then
            repeat
                if PreviousIntegrationUserRole.Get(CRMSystemUserRoles.RoleId) then
                    if not TryAssignUserRole(CrmHelper, CurrentIntegrationUserId, CRMSystemUserRoles.RoleId) then
                        Session.LogMessage('0000GJM', CannotAssignRoleToUserTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            until CRMSystemUserRoles.Next() = 0;

        UnregisterTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName);
        Session.LogMessage('0000GJN', PreviousIntegrationUserRolesAddedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
    end;

    local procedure AssignVirtualTablesIntegrationRole(var CrmHelper: DotNet CrmHelper; UserName: Text)
    var
        RoleId: Guid;
    begin
        RoleId := GetVirtualTablesIntegrationRoleId();
        AssignIntegrationRole(CrmHelper, UserName, RoleId);
    end;

    [Scope('OnPrem')]
    procedure AssignIntegrationRole(var CrmHelper: DotNet CrmHelper; UserName: Text)
    var
        RoleId: Guid;
    begin
        RoleId := GetIntegrationRoleId();
        AssignIntegrationRole(CrmHelper, UserName, RoleId);
    end;

    local procedure AssignIntegrationRole(var CrmHelper: DotNet CrmHelper; UserName: Text; RoleId: Guid)
    var
        UserId: Guid;
    begin
        if not TryGetUserId(CrmHelper, UserName, UserId) then begin
            Session.LogMessage('0000ATT', UserNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(UserDoesNotExistErr, UserName);
        end;
        if IsNullGuid(UserId) then begin
            Session.LogMessage('0000ATU', UserNotFoundTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(UserDoesNotExistErr, UserName);
        end;
        if not AssignUserRole(CrmHelper, UserId, RoleId) then begin
            Session.LogMessage('0000ATV', CannotAssignRoleToUserTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(CannotAssignRoleToIntegrationUserErr);
        end;
        Session.LogMessage('0000ATW', RoleAssignedToUserTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
    end;

    procedure AssignTeamRole(var CrmHelper: DotNet CrmHelper; TeamId: Guid; RoleId: Guid): Boolean
    begin
        if IsNullGuid(TeamId) then begin
            Session.LogMessage('0000GDI', CannotAssignRoleToTeamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit(false);
        end;
        if IsNullGuid(RoleId) then begin
            Session.LogMessage('0000GDJ', CannotAssignRoleToTeamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit(false);
        end;
        if CheckRoleAssignedToTeam(CrmHelper, TeamId, RoleId) then
            exit(true);

        if not TryAssignTeamRole(CrmHelper, TeamId, RoleId) then begin
            Session.LogMessage('0000ATX', CannotAssignRoleToTeamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit(false);
        end;

        if CheckRoleAssignedToTeam(CrmHelper, TeamId, RoleId) then
            exit(true);

        Session.LogMessage('0000ATY', CannotAssignRoleToTeamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        exit(false);
    end;

    [TryFunction]
    local procedure TryAssignTeamRole(var CrmHelper: DotNet CrmHelper; TeamId: Guid; RoleId: Guid)
    begin
        CrmHelper.AssociateTeamWithRole(TeamId, RoleId);
    end;

    local procedure CheckRoleAssignedToTeam(var CrmHelper: DotNet CrmHelper; TeamId: Guid; RoleId: Guid): Boolean
    begin
        exit(CrmHelper.CheckRoleAssignedToTeam(TeamId, RoleId));
    end;

    local procedure AddUserToTeam(var CrmHelper: DotNet CrmHelper; UserId: Guid; TeamId: Guid): Boolean
    begin
        if CrmHelper.CheckUserAssociatedWithTeam(UserId, TeamId) then
            exit(false);

        if not TryAddUserToTeam(CrmHelper, UserId, TeamId) then begin
            Session.LogMessage('0000C97', CannotAddUserToTeamTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit(false);
        end;

        if CrmHelper.CheckUserAssociatedWithTeam(UserId, TeamId) then begin
            Session.LogMessage('0000C9Q', UserAddedToTeamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit(true);
        end;

        Session.LogMessage('0000C98', CannotAddUserToTeamTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        exit(false);
    end;

    [TryFunction]
    local procedure TryAddUserToTeam(var CrmHelper: DotNet CrmHelper; UserId: Guid; TeamId: Guid)
    begin
        CrmHelper.AssociateUserWithTeam(UserId, TeamId);
    end;

    [Scope('OnPrem')]
    [NonDebuggable]
    procedure ImportAndConfigureIntegrationSolution(var CDSConnectionSetup: Record "CDS Connection Setup"; RenewSolution: Boolean): Boolean
    begin
        exit(ImportAndConfigureIntegrationSolution(CDSConnectionSetup, RenewSolution, false));
    end;

    [NonDebuggable]
    internal procedure ImportAndConfigureIntegrationSolution(var CDSConnectionSetup: Record "CDS Connection Setup"; RenewSolution: Boolean; GetTokenFromCache: Boolean): Boolean
    var
        CrmHelper: DotNet CrmHelper;
        AdminAccessToken: SecretText;
        AdminUserName: Text;
        AdminPassword: SecretText;
        AdminADDomain: Text;
    begin
        Session.LogMessage('0000ATZ', ConfigureSolutionTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        CheckCredentials(CDSConnectionSetup);
        SignInCDSAdminUser(CDSConnectionSetup, CrmHelper, AdminUserName, AdminPassword, AdminAccessToken, AdminADDomain, GetTokenFromCache);
        JITProvisionFirstPartyApp(CrmHelper);
        Sleep(5000);
        ImportIntegrationSolution(CDSConnectionSetup, CrmHelper, AdminUserName, AdminPassword, AdminAccessToken, AdminADDomain, RenewSolution);
        ConfigureIntegrationSolution(CDSConnectionSetup, CrmHelper, AdminUserName, AdminPassword, AdminAccessToken, AdminADDomain, false);
        if not RenewSolution then
            if CheckIntegrationRequirements(CDSConnectionSetup, true) then begin
                Session.LogMessage('0000AU0', IntegrationRequirementsMetTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                exit(false);
            end;
        exit(true);
    end;

    [Scope('OnPrem')]
    [TryFunction]
    [NonDebuggable]
    procedure SignInCDSAdminUser(var CDSConnectionSetup: Record "CDS Connection Setup"; var CrmHelper: DotNet CrmHelper; var AdminUser: Text; var AdminPassword: SecretText; var AccessToken: SecretText; var AdminADDomain: Text; GetTokenFromCache: Boolean)
    var
        TempConnectionStringWithPlaceholder: Text;
        TempConnectionString: SecretText;
    begin
        if CDSConnectionSetup."Authentication Type" <> CDSConnectionSetup."Authentication Type"::Office365 then begin
            if not PromptForAdminCredentials(CDSConnectionSetup, AdminUser, AdminPassword, AdminADDomain) then begin
                Session.LogMessage('0000AU1', InvalidAdminCredentialsTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                Error(AdminUserPasswordWrongErr);
            end;

            case CDSConnectionSetup."Authentication Type" of
                CDSConnectionSetup."Authentication Type"::AD:
                    begin
                        TempConnectionStringWithPlaceholder := StrSubstNo(ConnectionStringFormatTok, CDSConnectionSetup."Server Address", AdminUser, '%1', CDSConnectionSetup.GetProxyVersion(), GetAuthenticationTypeToken(CDSConnectionSetup, AdminADDomain));
                        TempConnectionString := SecretStrSubstNo(TempConnectionStringWithPlaceholder, AdminPassword);
                    end;
                CDSConnectionSetup."Authentication Type"::OAuth:
                    TempConnectionString := ReplaceUserNamePasswordInConnectionString(CDSConnectionSetup, AdminUser, AdminPassword);
                else begin
                    TempConnectionStringWithPlaceholder := StrSubstNo(ConnectionStringFormatTok, CDSConnectionSetup."Server Address", AdminUser, '%1', CDSConnectionSetup.GetProxyVersion(), GetAuthenticationTypeToken(CDSConnectionSetup));
                    TempConnectionString := SecretStrSubstNo(TempConnectionStringWithPlaceholder, AdminPassword);
                end;
            end
        end else begin
            GetAccessToken(CDSConnectionSetup."Server Address", GetTokenFromCache, AccessToken);
            TempConnectionStringWithPlaceholder := StrSubstNo(OAuthConnectionStringFormatTxt, CDSConnectionSetup."Server Address", '%1', CDSConnectionSetup.GetProxyVersion(), GetAuthenticationTypeToken(CDSConnectionSetup));
            TempConnectionString := SecretStrSubstNo(TempConnectionStringWithPlaceholder, AccessToken);
        end;

        if not InitializeConnection(CrmHelper, TempConnectionString) then begin
            Session.LogMessage('0000AU2', ConnectionNotRegisteredTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            ProcessConnectionFailures();
        end;
    end;


    local procedure ReplaceUserNameInConnectionString(CDSConnectionSetup: Record "CDS Connection Setup"; NewUserName: Text): Text
    var
        UserNameTokenPos: Integer;
        NewConnectionString: Text;
        LeftPart: Text;
        RightPart: Text;
        UserNameTok: Text;
    begin
        UserNameTok := 'UserName=';

        UserNameTokenPos := StrPos(CDSConnectionSetup."Connection String", UserNameTok);
        LeftPart := CopyStr(NewConnectionString, 1, UserNameTokenPos - 1);
        RightPart := CopyStr(NewConnectionString, UserNameTokenPos);
        if RightPart.IndexOf(';') > 0 then
            NewConnectionString := LeftPart + UserNameTok + NewUserName + CopyStr(RightPart, RightPart.IndexOf(';'))
        else
            NewConnectionString := LeftPart + UserNameTok + NewUserName;

        exit(NewConnectionString);
    end;

    [Scope('OnPrem')]
    procedure ReplaceUserNamePasswordInConnectionString(CDSConnectionSetup: Record "CDS Connection Setup"; NewUserName: Text; NewPassword: SecretText): SecretText
    var
        PasswordPlaceHolderPos: Integer;
        NewConnectionStringWithPlaceholder: Text;
        NewConnectionString: SecretText;
    begin
        NewConnectionStringWithPlaceholder := ReplaceUserNameInConnectionString(CDSConnectionSetup, NewUserName);
        PasswordPlaceHolderPos := StrPos(NewConnectionStringWithPlaceholder, MissingPasswordTok);

        NewConnectionStringWithPlaceholder :=
            CopyStr(NewConnectionStringWithPlaceholder, 1, PasswordPlaceHolderPos - 1) + '%1' +
            CopyStr(NewConnectionStringWithPlaceholder, PasswordPlaceHolderPos + StrLen(MissingPasswordTok));
        NewConnectionString := SecretStrSubstNo(NewConnectionStringWithPlaceholder, NewPassword);

        exit(NewConnectionString);
    end;


    internal procedure GetDataverseCloudEndpoints(): Interface "Dataverse Cloud Endpoints"
    var
        CDSConnectionSetup: Record "CDS Connection Setup";
    begin
        if not CDSConnectionSetup.Get() then
            CDSConnectionSetup.Init();
        exit(CDSConnectionSetup.GetDataverseCloudEndpoints());
    end;

    [Scope('OnPrem')]
    procedure GetAccessToken(ResourceURL: Text; GetTokenFromCache: Boolean; var AccessToken: SecretText)
    var
        OAuth2: Codeunit OAuth2;
        PromptInteraction: Enum "Prompt Interaction";
        Endpoints: Interface "Dataverse Cloud Endpoints";
        Scopes: List of [Text];
        [NonDebuggable]
        ClientId: Text;
        ClientSecret: SecretText;
        [NonDebuggable]
        FirstPartyAppId: Text;
        [NonDebuggable]
        FirstPartyAppCertificate: SecretText;
        RedirectUrl: Text;
        AuthCodeError: Text;
    begin
        Endpoints := GetDataverseCloudEndpoints();
        Scopes.Add(ResourceURL + '/user_impersonation');
        ClientId := GetCDSConnectionClientId();
        ClientSecret := GetCDSConnectionClientSecret();
        FirstPartyAppId := GetCDSConnectionFirstPartyAppId();
        FirstPartyAppCertificate := GetCDSConnectionFirstPartyAppCertificate();

        if (FirstPartyAppId = '') or (FirstPartyAppCertificate.IsEmpty()) then
            if (ClientId = '') or (ClientSecret.IsEmpty()) then
                Error(GetMissingClientIdOrSecretErr());

        RedirectUrl := GetRedirectURL();
        if GetTokenFromCache then
            if (FirstPartyAppId <> '') and (not FirstPartyAppCertificate.IsEmpty()) then begin
                Session.LogMessage('0000EI9', AttemptingAuthCodeTokenFromCacheWithCertTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                OAuth2.AcquireAuthorizationCodeTokenFromCacheWithCertificate(FirstPartyAppId, FirstPartyAppCertificate, RedirectUrl, Endpoints.GetOAuthAuthorityUrl(), ResourceURL, AccessToken)
            end else begin
                Session.LogMessage('0000EIA', AttemptingAuthCodeTokenFromCacheWithClientSecretTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                OAuth2.AcquireAuthorizationCodeTokenFromCache(ClientId, ClientSecret, RedirectUrl, Endpoints.GetOAuthAuthorityUrl(), Scopes, AccessToken);
            end;
        if AccessToken.IsEmpty() then begin
            if not GuiAllowed then begin
                Session.LogMessage('0000DNV', GuiNotAllowedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                exit;
            end;

            if (FirstPartyAppId <> '') and (not FirstPartyAppCertificate.IsEmpty()) then begin
                Session.LogMessage('0000EIB', AttemptingAuthCodeTokenWithCertTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                OAuth2.AcquireTokenByAuthorizationCodeWithCertificate(
                    FirstPartyAppId,
                    FirstPartyAppCertificate,
                    Endpoints.GetOAuthAuthorityUrl(),
                    RedirectUrl,
                    ResourceURL,
                    PromptInteraction::Consent,
                    AccessToken,
                    AuthCodeError)
            end else begin
                Session.LogMessage('0000EIC', AttemptingAuthCodeTokenWithClientSecretTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                OAuth2.AcquireTokenByAuthorizationCode(
                    ClientId,
                    ClientSecret,
                    Endpoints.GetOAuthAuthorityUrl(),
                    RedirectUrl,
                    Scopes,
                    PromptInteraction::Consent,
                    AccessToken,
                    AuthCodeError)
            end;
        end;
        if AccessToken.IsEmpty() then begin
            if AuthCodeError <> '' then
                Session.LogMessage('0000C10', AuthCodeError, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok)
            else
                Session.LogMessage('0000C11', AuthTokenOrCodeNotReceivedErr, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(AccessTokenNotReceivedErr, ResourceURL);
        end;
    end;

    [Scope('OnPrem')]
    procedure GetBusinessEventAccessToken(ResourceURL: Text; GetTokenFromCache: Boolean; var AccessToken: SecretText)
    var
        OAuth2: Codeunit OAuth2;
        Endpoints: Interface "Dataverse Cloud Endpoints";
        Scopes: List of [Text];
        [NonDebuggable]
        ClientId: Text;
        ClientSecret: SecretText;
        [NonDebuggable]
        FirstPartyAppId: Text;
        [NonDebuggable]
        FirstPartyAppCertificate: SecretText;
        RedirectUrl: Text;
        AuthCodeError: Text;
        IdToken: Text;
    begin
        Endpoints := GetDataverseCloudEndpoints();
        Scopes.Add(ResourceURL + '/.default');
        ClientId := GetCDSConnectionClientId();
        ClientSecret := GetCDSConnectionClientSecret();
        FirstPartyAppId := GetCDSConnectionFirstPartyAppId();
        FirstPartyAppCertificate := GetCDSConnectionFirstPartyAppCertificate();

        if (FirstPartyAppId = '') or (FirstPartyAppCertificate.IsEmpty()) then
            if (ClientId = '') or (ClientSecret.IsEmpty()) then
                Error(GetMissingClientIdOrSecretErr());

        RedirectUrl := GetRedirectURL();
        if GetTokenFromCache then
            if (FirstPartyAppId <> '') and (not FirstPartyAppCertificate.IsEmpty()) then begin
                Session.LogMessage('0000GIG', AttemptingClientCredentialsTokenFromCacheWithCertTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                OAuth2.AcquireTokensFromCacheWithCertificate(FirstPartyAppId, FirstPartyAppCertificate, RedirectUrl, Endpoints.GetClientCredentialsTokenAuthorityUrl(), Scopes, AccessToken, IdToken);
            end else begin
                Session.LogMessage('0000GIH', AttemptingClientCredentialsTokenFromCacheWithClientSecretTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                OAuth2.AcquireTokensFromCache(ClientId, ClientSecret, RedirectUrl, Endpoints.GetClientCredentialsTokenAuthorityUrl(), Scopes, AccessToken, IdToken);
            end;

        if AccessToken.IsEmpty() then
            if (FirstPartyAppId <> '') and (not FirstPartyAppCertificate.IsEmpty()) then begin
                Session.LogMessage('0000GII', AttemptingClientCredentialsTokenWithCertTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                OAuth2.AcquireTokensWithCertificate(FirstPartyAppId, FirstPartyAppCertificate, RedirectUrl, Endpoints.GetClientCredentialsTokenAuthorityUrl(), Scopes, AccessToken, IdToken);
            end else begin
                Session.LogMessage('0000GIJ', AttemptingClientCredentialsTokenWithClientSecretTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                OAuth2.AcquireTokenWithClientCredentials(ClientId, ClientSecret, Endpoints.GetClientCredentialsTokenAuthorityUrl(), RedirectUrl, Scopes, AccessToken);
            end;

        if AccessToken.IsEmpty() then begin
            if AuthCodeError <> '' then
                Session.LogMessage('0000GIK', AuthCodeError, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok)
            else
                Session.LogMessage('0000GIL', AuthTokenOrCodeNotReceivedErr, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(AccessTokenNotReceivedErr, ResourceURL);
        end;
    end;

    [Scope('OnPrem')]
    procedure GetRedirectURL(): Text
    var
        CDSConnectionSetup: Record "CDS Connection Setup";
    begin
        if EnvironmentInfo.IsSaaSInfrastructure() then
            exit('');

        if CDSConnectionSetup.Get() then
            exit(CDSConnectionSetup."Redirect URL");

        exit('');
    end;


    [Scope('OnPrem')]
    procedure GetCDSConnectionClientId(): Text
    var
        CDSConnectionSetup: Record "CDS Connection Setup";
        AzureKeyVault: Codeunit "Azure Key Vault";
        [NonDebuggable]
        ClientId: Text;
    begin
        if EnvironmentInfo.IsSaaSInfrastructure() then
            if not AzureKeyVault.GetAzureKeyVaultSecret(CDSConnectionClientIdAKVSecretNameLbl, ClientId) then
                Session.LogMessage('0000C0Y', MissingClientIdOrSecretTelemetryTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok)
            else
                exit(ClientId);

        if CDSConnectionSetup.Get() then
            ClientId := CDSConnectionSetup."Client Id";

        if ClientId = '' then
            OnGetCDSConnectionClientId(ClientId);

        exit(ClientId);
    end;

    [Scope('OnPrem')]
    [NonDebuggable]
    procedure GetCDSConnectionFirstPartyAppId(): Text
    var
        AzureKeyVault: Codeunit "Azure Key Vault";
        ClientId: Text;
    begin
        if EnvironmentInfo.IsSaaSInfrastructure() then
            if not AzureKeyVault.GetAzureKeyVaultSecret(CDSConnectionFirstPartyAppIdAKVSecretNameLbl, ClientId) then
                Session.LogMessage('0000EBU', MissingFirstPartyappIdOrCertificateTelemetryTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok)
            else
                exit(ClientId);
        exit(ClientId);
    end;

    local procedure GetMissingClientIdOrSecretErr(): Text
    begin
        if EnvironmentInfo.IsSaaSInfrastructure() then
            exit(MissingClientIdOrSecretErr);

        exit(MissingClientIdOrSecretOnPremErr);
    end;

    [NonDebuggable]
    local procedure GetClientSecretFromEvent(): SecretText
    var
        ClientSecret: Text;
    begin
        OnGetCDSConnectionClientSecret(ClientSecret);
        exit(ClientSecret);
    end;

    [Scope('OnPrem')]
    [NonDebuggable]
    procedure GetCDSConnectionClientSecret(): SecretText
    var
        CDSConnectionSetup: Record "CDS Connection Setup";
        AzureKeyVault: Codeunit "Azure Key Vault";
        ClientSecret: SecretText;
    begin
        if EnvironmentInfo.IsSaaSInfrastructure() then
            if not AzureKeyVault.GetAzureKeyVaultSecret(CDSConnectionClientSecretAKVSecretNameLbl, ClientSecret) then
                Session.LogMessage('0000C0Z', MissingClientIdOrSecretTelemetryTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok)
            else
                exit(ClientSecret);

        if CDSConnectionSetup.Get() then
            ClientSecret := CDSConnectionSetup.GetSecretClientSecret();

        if ClientSecret.IsEmpty() then
            ClientSecret := GetClientSecretFromEvent();

        exit(ClientSecret);
    end;


    [Scope('OnPrem')]
    [NonDebuggable]
    procedure GetCDSConnectionFirstPartyAppCertificate(): Text;
    var
        AzureKeyVault: Codeunit "Azure Key Vault";
        Certificate: Text;
        CertificateName: Text;
    begin
        if EnvironmentInfo.IsSaaSInfrastructure() then
            if not AzureKeyVault.GetAzureKeyVaultSecret(CDSConnectionFirstPartyAppCertificateNameAKVSecretNameLbl, CertificateName) then begin
                Session.LogMessage('0000EBV', MissingFirstPartyappIdOrCertificateTelemetryTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                exit(Certificate);
            end;

        if not AzureKeyVault.GetAzureKeyVaultCertificate(CertificateName, Certificate) then
            Session.LogMessage('0000EC2', MissingFirstPartyappIdOrCertificateTelemetryTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        exit(Certificate);
    end;

    [Scope('OnPrem')]
    [NonDebuggable]
    procedure ImportIntegrationSolution(var CDSConnectionSetup: Record "CDS Connection Setup"; var CrmHelper: DotNet CrmHelper; AdminUsername: Text; AdminPassword: SecretText; AccessToken: SecretText; AdminADDomain: Text; RenewSolution: Boolean)
    var
        TempAdminCDSConnectionSetup: Record "CDS Connection Setup" temporary;
        NavTenantSettingsHelper: DotNet NavTenantSettingsHelper;
        Version: DotNet Version;
        SolutionVersion: Text;
        SolutionInstalled: Boolean;
        SolutionOutdated: Boolean;
        ImportSolution: Boolean;
    begin
        GetTempAdminConnectionSetup(TempAdminCDSConnectionSetup, CDSConnectionSetup, AdminUserName, AdminPassword, AccessToken, AdminADDomain);

        if GetSolutionVersion(TempAdminCDSConnectionSetup, GetBaseSolutionUniqueName(), SolutionVersion) then
            if Version.TryParse(SolutionVersion, Version) then begin
                SolutionInstalled := true;
                SolutionOutdated := Version.CompareTo(NavTenantSettingsHelper.GetPlatformVersion()) < 0;
            end;

        if RenewSolution then
            ImportSolution := (not SolutionInstalled) or SolutionOutdated
        else
            ImportSolution := not SolutionInstalled;

        if ImportSolution then begin
            Session.LogMessage('0000AU4', ImportSolutionTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            if not ImportDefaultCdsSolution(CrmHelper) then
                ProcessConnectionFailures();
        end;
    end;

    [TryFunction]
    local procedure ImportDefaultCdsSolution(var CRMHelper: DotNet CrmHelper)
    begin
        CrmHelper.ImportDefaultCdsSolution();
    end;

    [Scope('OnPrem')]
    [NonDebuggable]
    procedure ConfigureIntegrationSolution(var CDSConnectionSetup: Record "CDS Connection Setup"; var CrmHelper: DotNet CrmHelper; AdminUserName: Text; AdminPassword: SecretText; AccessToken: SecretText; AdminADDomain: Text; IsSilent: Boolean)
    begin
        FindOrCreateIntegrationUser(CDSConnectionSetup, AdminUserName, AdminPassword, AccessToken);
        AssignIntegrationRole(CrmHelper, CDSConnectionSetup."User Name");

        // if Authentication Type is Office365 we don't need to set the user as integration user or non-interactive user
        // in this case we have injected a non-licensed application user that is bound to the Azure AD application used to connect to CDS
        if CDSConnectionSetup."Authentication Type" <> CDSConnectionSetup."Authentication Type"::Office365 then begin
            SetUserAsIntegrationUser(CDSConnectionSetup, AdminUserName, AdminPassword, AccessToken, AdminADDomain);
            if SetAccessModeToNonInteractive(CDSConnectionSetup, AdminUserName, AdminPassword, AccessToken, AdminADDomain) then
                if not IsSilent then
                    Message(AccessModeSetToNonInteractiveMsg);
        end;

        SyncCompany(CDSConnectionSetup, AdminUserName, AdminPassword, AccessToken, AdminADDomain);
        AssignPreviousIntegrationUserRoles(CrmHelper, CDSConnectionSetup, AccessToken);

        Session.LogMessage('0000AU5', SolutionConfiguredTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
    end;

    [Scope('OnPrem')]
    procedure JITProvisionFirstPartyApp(var CrmHelper: DotNet CrmHelper)
    var
        [NonDebuggable]
        CDSConnectionFirstPartyAppIdTxt: Text;
        [NonDebuggable]
        CDSConnectionFirstPartyAppCertificateTxt: Text;
        JITProvisioningTelemetryMessageTxt: Text;
    begin
        CDSConnectionFirstPartyAppIdTxt := GetCDSConnectionFirstPartyAppId();
        CDSConnectionFirstPartyAppCertificateTxt := GetCDSConnectionFirstPartyAppCertificate();
        if (CDSConnectionFirstPartyAppIdTxt = '') or (CDSConnectionFirstPartyAppCertificateTxt = '') then
            exit;

        JITProvisioningTelemetryMessageTxt := CrmHelper.ProvisionServicePrincipal(CDSConnectionFirstPartyAppIdTxt, CDSConnectionFirstPartyAppCertificateTxt);
        if JITProvisioningTelemetryMessageTxt <> '' then
            if JITProvisioningTelemetryMessageTxt.Contains(SuccessfulJITProvisioningTelemetryMsg) then
                Session.LogMessage('0000ENK', JITProvisioningTelemetryMessageTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok)
            else
                Session.LogMessage('0000F0A', JITProvisioningTelemetryMessageTxt, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
    end;

    [Scope('OnPrem')]
    procedure HasCompanyIdField(TableId: Integer): Boolean
    var
        HasField: Boolean;
    begin
        CDSIntegrationMgt.OnHasCompanyIdField(TableId, HasField);
        exit(HasField);
    end;

    [Scope('OnPrem')]
    procedure CheckCompanyId(var RecRef: RecordRef): Boolean
    begin
        exit(TrySetAndCheckCompany(RecRef, true));
    end;

    [Scope('OnPrem')]
    procedure SetCompanyId(var RecRef: RecordRef): Boolean
    begin
        exit(TrySetAndCheckCompany(RecRef, false));
    end;

    [Scope('OnPrem')]
    procedure CheckCompanyIdNoTelemetry(var RecRef: RecordRef): Boolean
    var
        CompanyIdFldRef: FieldRef;
        ActualCompanyId: Guid;
        SavedCompanyId: Guid;
    begin
        if not FindCompanyIdField(RecRef, CompanyIdFldRef) then
            exit(false);

        ActualCompanyId := GetCachedCompanyId();
        SavedCompanyId := CompanyIdFldRef.Value();

        exit(ActualCompanyId = SavedCompanyId);
    end;

    [TryFunction]
    local procedure TrySetAndCheckCompany(var RecRef: RecordRef; CheckOnly: Boolean)
    var
        CompanyIdFldRef: FieldRef;
        ActualCompanyId: Guid;
        SavedCompanyId: Guid;
        IsCorrectCompany: Boolean;
    begin
        if not FindCompanyIdField(RecRef, CompanyIdFldRef) then begin
            Session.LogMessage('0000AVN', EntityHasNoCompanyIdFieldTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(CannotFindCompanyIdFieldErr, RecRef.Number(), RecRef.Name());
        end;

        ActualCompanyId := GetCachedCompanyId();
        SavedCompanyId := CompanyIdFldRef.Value();
        IsCorrectCompany := SavedCompanyId = ActualCompanyId;

        if CheckOnly then begin
            if not IsCorrectCompany then begin
                Session.LogMessage('0000AVO', CompanyIdDiffersFromExpectedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                Error(OwnerDiffersFromExpectedErr);
            end;
            Session.LogMessage('0000AVP', CompanyIdCheckedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit;
        end;

        if not IsCorrectCompany then
            CompanyIdFldRef.Value := ActualCompanyId;

        Session.LogMessage('0000AVT', CompanyIdSetTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
    end;

    [Scope('OnPrem')]
    procedure ResetCompanyId(var RecRef: RecordRef): Boolean
    var
        Changed: Boolean;
    begin
        if TryResetCompanyId(RecRef, Changed) then
            exit(Changed);
        exit(false);
    end;

    [TryFunction]
    local procedure TryResetCompanyId(var RecRef: RecordRef; var Changed: Boolean)
    var
        CRMSalesorder: Record "CRM Salesorder";
        CompanyIdFldRef: FieldRef;
        ActualCompanyId: Guid;
        SavedCompanyId: Guid;
        EmptyCompanyId: Guid;
    begin
        if RecRef.Number() = Database::"CRM Salesorder" then begin
            RecRef.SetTable(CRMSalesorder);
            if TryResetCompanyId(CRMSalesorder, Changed) then
                RecRef.GetTable(CRMSalesorder);
            exit;
        end;

        if not FindCompanyIdField(RecRef, CompanyIdFldRef) then begin
            Session.LogMessage('0000DDK', EntityHasNoCompanyIdFieldTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit;
        end;

        ActualCompanyId := GetCachedCompanyId();
        SavedCompanyId := CompanyIdFldRef.Value();
        if SavedCompanyId = ActualCompanyId then begin
            CompanyIdFldRef.Value := EmptyCompanyId;
            Changed := true;
        end;
    end;

    [TryFunction]
    local procedure TryResetCompanyId(var CRMSalesorder: Record "CRM Salesorder"; var Changed: Boolean)
    var
        ActualCompanyId: Guid;
    begin
        ActualCompanyId := GetCachedCompanyId();
        if CRMSalesorder.CompanyId <> ActualCompanyId then
            exit;

        case CRMSalesorder.StateCode of
            CRMSalesorder.StateCode::Submitted:
                begin
                    CRMSalesorder.StateCode := CRMSalesorder.StateCode::Active;
                    CRMSalesorder.Modify(true);
                    Clear(CRMSalesorder.CompanyId);
                    CRMSalesorder.Modify(true);
                    CRMSalesorder.StateCode := CRMSalesorder.StateCode::Submitted;
                    CRMSalesorder.Modify(true);
                    Changed := IsNullGuid(CRMSalesorder.CompanyId);
                end;
            CRMSalesorder.StateCode::Active:
                begin
                    Clear(CRMSalesorder.CompanyId);
                    CRMSalesorder.Modify(true);
                    Changed := IsNullGuid(CRMSalesorder.CompanyId);
                end;
        end;
    end;

    [Scope('OnPrem')]
    procedure CheckOwningTeam(var RecRef: RecordRef): Boolean
    begin
        exit(CheckOwningTeam(RecRef, false));
    end;

    [Scope('OnPrem')]
    procedure CheckOwningTeam(var RecRef: RecordRef; SkipBusinessUnitCheck: Boolean): Boolean
    var
        EmptyGuid: Guid;
    begin
        exit(CheckOwningTeam(RecRef, EmptyGuid, SkipBusinessUnitCheck));
    end;

    [Scope('OnPrem')]
    procedure CheckOwningTeam(var RecRef: RecordRef; TeamId: Guid; SkipBusinessUnitCheck: Boolean): Boolean
    var
        TempCDSCompany: Record "CDS Company" temporary;
    begin
        exit(SetAndCheckOwner(RecRef, TempCDSCompany.OwnerIdType::team, TeamId, true, SkipBusinessUnitCheck));
    end;

    [Scope('OnPrem')]
    procedure CheckOwningUser(var RecRef: RecordRef; UserId: Guid): Boolean
    begin
        exit(CheckOwningUser(RecRef, UserId, false));
    end;

    [Scope('OnPrem')]
    procedure CheckOwningUser(var RecRef: RecordRef; UserId: Guid; SkipBusinessUnitCheck: Boolean): Boolean
    var
        TempCDSCompany: Record "CDS Company" temporary;
    begin
        exit(SetAndCheckOwner(RecRef, TempCDSCompany.OwnerIdType::systemuser, UserId, true, SkipBusinessUnitCheck));
    end;

    [Scope('OnPrem')]
    procedure SetOwningTeam(var RecRef: RecordRef): Boolean
    var
        EmptyGuid: Guid;
    begin
        exit(SetOwningTeam(RecRef, EmptyGuid, false));
    end;

    [Scope('OnPrem')]
    procedure SetOwningTeam(var RecRef: RecordRef; TeamId: Guid; SkipBusinessUnitCheck: Boolean): Boolean
    var
        TempCDSCompany: Record "CDS Company" temporary;
    begin
        exit(SetAndCheckOwner(RecRef, TempCDSCompany.OwnerIdType::team, TeamId, false, SkipBusinessUnitCheck));
    end;

    [Scope('OnPrem')]
    procedure SetOwningUser(var RecRef: RecordRef; UserId: Guid; SkipBusinessUnitCheck: Boolean): Boolean
    var
        TempCDSCompany: Record "CDS Company" temporary;
    begin
        exit(SetAndCheckOwner(RecRef, TempCDSCompany.OwnerIdType::systemuser, UserId, false, SkipBusinessUnitCheck));
    end;

    local procedure SetAndCheckOwner(var RecRef: RecordRef; OwnerIdType: Option; OwnerId: Guid; CheckOnly: Boolean; SkipBusinessUnitCheck: Boolean): Boolean
    begin
        exit(TrySetAndCheckOwner(RecRef, OwnerIdType, OwnerId, CheckOnly, SkipBusinessUnitCheck));
    end;

    [TryFunction]
    local procedure TrySetAndCheckOwner(var RecRef: RecordRef; OwnerIdType: Option; OwnerId: Guid; CheckOnly: Boolean; SkipBusinessUnitCheck: Boolean)
    var
        TempCDSCompany: Record "CDS Company" temporary;
        OwnerIdTypeFldRef: FieldRef;
        OwnerIdFldRef: FieldRef;
        SavedOwnerIdType: Option;
        SavedOwnerId: Guid;
        IsCorrectOwner: Boolean;
    begin
        if IsNullGuid(OwnerId) and (OwnerIdType = TempCDSCompany.OwnerIdType::team) then begin
            Session.LogMessage('0000AU8', SetDefaultOwningTeamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            OwnerId := GetCachedDefaultOwningTeamId();
        end;

        if not FindOwnerIdField(RecRef, OwnerIdFldRef) then
            Error(CannotFindOwnerIdFieldErr, RecRef.Number(), RecRef.Name());

        if not FindOwnerTypeField(RecRef, OwnerIdTypeFldRef) then
            Error(CannotFindOwnerTypeFieldErr, RecRef.Number(), RecRef.Name());

        SavedOwnerIdType := OwnerIdTypeFldRef.Value();
        SavedOwnerId := OwnerIdFldRef.Value();
        IsCorrectOwner := (SavedOwnerIdType = OwnerIdType) and (SavedOwnerId = OwnerId);

        if CheckOnly then
            if not IsCorrectOwner then begin
                Session.LogMessage('0000AUA', OwnerDiffersFromExpectedTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                Error(OwnerDiffersFromExpectedErr);
            end;

        case OwnerIdType of
            TempCDSCompany.OwnerIdType::team:
                CheckOwningTeam(OwnerId, SkipBusinessUnitCheck);
            TempCDSCompany.OwnerIdType::systemuser:
                CheckOwningUser(OwnerId, SkipBusinessUnitCheck);
            else begin
                Session.LogMessage('0000AUC', UnsupportedOwnerTypeTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                Error(OwnerIdTypeErr);
            end;
        end;

        if CheckOnly then
            exit;

        if not IsCorrectOwner then begin
            OwnerIdTypeFldRef.Value := OwnerIdType;
            OwnerIdFldRef.Value := OwnerId;
        end;

        Session.LogMessage('0000AUG', OwnerSetTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
    end;

    local procedure CheckOwningTeam(TeamId: Guid; SkipBusinessUnitCheck: Boolean)
    var
        OwningCRMTeam: Record "CRM Team";
    begin
        if TeamId = GetCachedDefaultOwningTeamId() then
            exit;
        if GetCachedOwningTeamCheck(TeamId, SkipBusinessUnitCheck) then
            exit;
        if not OwningCRMTeam.Get(TeamId) then begin
            Session.LogMessage('0000AUH', TeamNotFoundTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(TeamNotFoundErr, TeamId);
        end;
        CheckTeamHasIntegrationRole(OwningCRMTeam);
        if not SkipBusinessUnitCheck then
            if OwningCRMTeam.BusinessUnitId <> GetCachedOwningBusinessUnitId() then begin
                Session.LogMessage('0000AUI', TeamBusinessUnitDiffersFromSelectedTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                Error(TeamBusinessUnitDiffersFromSelectedErr);
            end;
        SetCachedOwningTeamCheck(TeamId, SkipBusinessUnitCheck);
    end;

    local procedure CheckOwningUser(UserId: Guid; SkipBusinessUnitCheck: Boolean)
    var
        CRMSystemuser: Record "CRM Systemuser";
    begin
        if GetCachedOwningUserCheck(UserId, SkipBusinessUnitCheck) then
            exit;
        if not CRMSystemuser.Get(UserId) then
            Error(UserNotFoundErr, UserId);

        if not SkipBusinessUnitCheck then
            if CRMSystemuser.BusinessUnitId <> GetCachedOwningBusinessUnitId() then
                Error(UserBusinessUnitDiffersFromSelectedErr);

        SetCachedOwningUserCheck(UserId, SkipBusinessUnitCheck);
    end;

    local procedure CheckTeamHasIntegrationRole(var CRMTeam: Record "CRM Team")
    var
        CRMRole: Record "CRM Role";
        CDSTeamroles: Record "CDS Teamroles";
    begin
        CRMRole.SetRange(BusinessUnitId, CRMTeam.BusinessUnitId);
        CRMRole.SetRange(ParentRoleId, GetIntegrationRoleId());
        if not CRMRole.FindFirst() then begin
            Session.LogMessage('0000AUM', RoleNotFoundForBusinessUnitTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(IntegrationRoleNotFoundErr, GetIntegrationRoleName(), GetBusinessUnitName(CRMTeam.BusinessUnitId));
        end;
        CDSTeamroles.SetRange(TeamId, CRMTeam.TeamId);
        CDSTeamroles.SetRange(RoleId, CRMRole.RoleId);
        if CDSTeamroles.IsEmpty() then begin
            Session.LogMessage('0000AUN', IntegrationRoleNotAssignedToTeamTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(TeamRolesErr, CRMTeam.Name, GetBusinessUnitName(CRMTeam.BusinessUnitId), GetIntegrationRoleName());
        end;
    end;

    local procedure GetBusinessUnitName(BusinesUnitId: Guid) BusinessunitName: Text
    var
        CRMBusinessUnit: Record "CRM Businessunit";
    begin
        if CRMBusinessunit.Get(BusinesUnitId) then
            BusinessunitName := CRMBusinessunit.Name
        else
            BusinessunitName := Format(BusinesUnitId);
    end;

    local procedure GetIntegrationRoleName() IntegrationRoleName: Text
    var
        CRMRole: Record "CRM Role";
    begin
        if CRMRole.Get(GetIntegrationRoleId()) then
            IntegrationRoleName := CRMRole.Name
        else
            IntegrationRoleName := Format(GetIntegrationRoleId());
    end;

    internal procedure FindCompanyIdField(var RecRef: RecordRef; var CompanyIdFldRef: FieldRef): Boolean
    var
        Field: Record "Field";
        TableNo: Integer;
        FieldNo: Integer;
    begin
        TableNo := RecRef.Number();
        if CachedCompanyIdFieldNo.ContainsKey(TableNo) then
            FieldNo := CachedCompanyIdFieldNo.Get(TableNo)
        else begin
            Field.SetRange(TableNo, TableNo);
            Field.SetRange(Type, Field.Type::GUID);
            Field.SetRange(FieldName, 'CompanyId');
            if Field.FindFirst() then
                FieldNo := Field."No."
            else
                FieldNo := 0;
            CachedCompanyIdFieldNo.Add(TableNo, FieldNo);
        end;
        if FieldNo = 0 then
            exit(false);
        CompanyIdFldRef := RecRef.Field(FieldNo);
        exit(true);
    end;

    local procedure FindOwnerIdField(var RecRef: RecordRef; var OwnerIdFldRef: FieldRef): Boolean
    var
        Field: Record "Field";
        TableNo: Integer;
        FieldNo: Integer;
    begin
        TableNo := RecRef.Number();
        if CachedOwnerIdFieldNo.ContainsKey(TableNo) then
            FieldNo := CachedOwnerIdFieldNo.Get(TableNo)
        else begin
            Field.SetRange(TableNo, TableNo);
            Field.SetRange(Type, Field.Type::GUID);
            Field.SetRange(FieldName, 'OwnerId');
            if Field.FindFirst() then
                FieldNo := Field."No."
            else
                FieldNo := 0;
            CachedOwnerIdFieldNo.Add(TableNo, FieldNo);
        end;
        if FieldNo = 0 then
            exit(false);
        OwnerIdFldRef := RecRef.Field(FieldNo);
        exit(true);
    end;

    [Scope('OnPrem')]
    procedure FindOwnerTypeField(var RecRef: RecordRef; var OwnerIdTypeFldRef: FieldRef): Boolean
    var
        Field: Record "Field";
        TableNo: Integer;
        FieldNo: Integer;
    begin
        TableNo := RecRef.Number();

        if CachedOwnerTypeFieldNo.ContainsKey(TableNo) then
            FieldNo := CachedOwnerTypeFieldNo.Get(TableNo)
        else begin
            Field.SetRange(TableNo, TableNo);
            Field.SetRange(Type, Field.Type::Option);
            Field.SetRange(FieldName, 'OwnerIdType');
            Field.SetRange(OptionString, ' ,systemuser,team');
            if Field.FindFirst() then
                FieldNo := Field."No."
            else
                FieldNo := 0;
            CachedOwnerTypeFieldNo.Add(TableNo, FieldNo);
        end;
        if FieldNo = 0 then
            exit(false);
        OwnerIdTypeFldRef := RecRef.Field(FieldNo);
        exit(true);
    end;

    local procedure GetCachedOwningTeamCheck(TeamId: Guid; SkipBusinessUnitCheck: Boolean): Boolean
    begin
        if SkipBusinessUnitCheck then begin
            if CachedOwningTeamCheckWithoutBusinessUnit.ContainsKey(TeamId) then
                exit(true);
        end else
            if CachedOwningTeamCheckWithBusinessUnit.ContainsKey(TeamId) then
                exit(true);
        exit(false);
    end;

    local procedure SetCachedOwningTeamCheck(TeamId: Guid; SkipBusinessUnitCheck: Boolean)
    begin
        Session.LogMessage('0000B2M', StrSubstNo(SetCachedOwningTeamCheckTxt, TeamId, SkipBusinessUnitCheck), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        if SkipBusinessUnitCheck then begin
            if not CachedOwningTeamCheckWithoutBusinessUnit.ContainsKey(TeamId) then
                CachedOwningTeamCheckWithoutBusinessUnit.Add(TeamId, true);
        end else
            if not CachedOwningTeamCheckWithBusinessUnit.ContainsKey(TeamId) then
                CachedOwningTeamCheckWithBusinessUnit.Add(TeamId, true);
    end;

    local procedure GetCachedOwningUserCheck(UserId: Guid; SkipBusinessUnitCheck: Boolean): Boolean
    begin
        if SkipBusinessUnitCheck then begin
            if CachedOwningUserCheckWithoutBusinessUnit.ContainsKey(UserId) then
                exit(true);
        end else
            if CachedOwningUserCheckWithBusinessUnit.ContainsKey(UserId) then
                exit(true);
        exit(false);
    end;

    local procedure SetCachedOwningUserCheck(UserId: Guid; SkipBusinessUnitCheck: Boolean)
    begin
        Session.LogMessage('0000B2N', StrSubstNo(SetCachedOwningUserCheckTxt, UserId, SkipBusinessUnitCheck), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        if SkipBusinessUnitCheck then begin
            if not CachedOwningUserCheckWithoutBusinessUnit.ContainsKey(UserId) then
                CachedOwningUserCheckWithoutBusinessUnit.Add(UserId, true);
        end else
            if not CachedOwningUserCheckWithBusinessUnit.ContainsKey(UserId) then
                CachedOwningUserCheckWithBusinessUnit.Add(UserId, true);
    end;

    local procedure GetCachedCompanyId(): Guid
    begin
        InitializeCompanyCache();
        exit(CachedCompanyId);
    end;

    local procedure GetCachedDefaultOwningTeamId(): Guid
    begin
        InitializeCompanyCache();
        exit(CachedDefaultOwningTeamId);
    end;

    local procedure GetCachedOwningBusinessUnitId(): Guid
    begin
        InitializeCompanyCache();
        exit(CachedOwningBusinessUnitId);
    end;

    local procedure InitializeCompanyCache()
    var
        CDSCompany: Record "CDS Company";
        CRMTeam: Record "CRM Team";
    begin
        if AreCompanyValuesCached then
            exit;

        Session.LogMessage('0000B2O', InitializeCompanyCacheTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        CDSCompany.SetRange(ExternalId, GetCompanyExternalId());
        if not CDSCompany.FindFirst() then begin
            Session.LogMessage('0000B2P', CompanyNotFoundTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(CompanyNotFoundErr, GetCompanyExternalId());
        end;

        if not CRMTeam.Get(CDSCompany.DefaultOwningTeam) then begin
            Session.LogMessage('0000B2Q', TeamNotFoundTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(TeamNotFoundErr, CDSCompany.DefaultOwningTeam);
        end;

        CachedCompanyId := CDSCompany.CompanyId;
        CachedDefaultOwningTeamId := CDSCompany.DefaultOwningTeam;
        CachedOwningBusinessUnitId := CRMTeam.BusinessUnitId;
        AreCompanyValuesCached := true;
    end;

    [Scope('OnPrem')]
    procedure ResetCache()
    begin
        Session.LogMessage('0000B2R', ClearCacheTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        Clear(AreCompanyValuesCached);
        Clear(CachedCompanyId);
        Clear(CachedDefaultOwningTeamId);
        Clear(CachedOwningBusinessUnitId);
        Clear(CachedOwningTeamCheckWithoutBusinessUnit);
        Clear(CachedOwningTeamCheckWithBusinessUnit);
        Clear(CachedOwningUserCheckWithoutBusinessUnit);
        Clear(CachedOwningUserCheckWithBusinessUnit);
    end;

    [Scope('OnPrem')]
    procedure GetIntegrationSolutions(var SolutionUniqueNameList: List of [Text])
    begin
        CDSIntegrationMgt.OnGetIntegrationSolutions(SolutionUniqueNameList);
        SolutionUniqueNameList.Insert(1, GetBaseSolutionUniqueName())
    end;

    [Scope('OnPrem')]
    procedure GetIntegrationRequiredRoles(var RequiredRoleIdList: List of [Guid])
    begin
        CDSIntegrationMgt.OnGetIntegrationRequiredRoles(RequiredRoleIdList);
        RequiredRoleIdList.Insert(1, GetIntegrationRoleId());
    end;

    [Scope('OnPrem')]
    procedure CheckModifyConnectionURL(var ServerAddress: Text[250])
    var
        UriHelper: DotNet Uri;
        UriHelper2: DotNet Uri;
        UriKindHelper: DotNet UriKind;
        UriPartialHelper: DotNet UriPartial;
        ProposedUri: Text[250];
    begin
        if (ServerAddress = '') or (ServerAddress = TestServerAddressTok) then
            exit;

        ServerAddress := DelChr(ServerAddress, '<>');
        if ServerAddress.EndsWith('/') then
            ServerAddress := ServerAddress.TrimEnd('/');

        if not UriHelper.TryCreate(ServerAddress, UriKindHelper.Absolute, UriHelper2) then
            if not UriHelper.TryCreate('https://' + ServerAddress, UriKindHelper.Absolute, UriHelper2) then
                Error(InvalidUriErr);

        if UriHelper2.Scheme() <> 'https' then
            Error(MustUseHttpsErr);

        ProposedUri := UriHelper2.GetLeftPart(UriPartialHelper.Authority);

        // Test that a specific port number is given
        if ((UriHelper2.Port() = 443) or (UriHelper2.Port() = 80)) and (LowerCase(ServerAddress) <> LowerCase(ProposedUri)) then
            if Confirm(StrSubstNo(ReplaceServerAddressQst, ServerAddress, ProposedUri)) then
                ServerAddress := ProposedUri;
    end;

    [Scope('OnPrem')]
    [NonDebuggable]
    procedure InitializeConnection(var CrmHelper: DotNet CrmHelper): Boolean
    var
        CDSConnectionSetup: Record "CDS Connection Setup";
    begin
        if not CDSConnectionSetup.Get() then
            exit(false);
        exit(InitializeConnection(CrmHelper, CDSConnectionSetup));
    end;

    [Scope('OnPrem')]
    [NonDebuggable]
    procedure InitializeConnection(var CrmHelper: DotNet CrmHelper; var CDSConnectionSetup: Record "CDS Connection Setup"): Boolean
    begin
        exit(InitializeConnection(CrmHelper, GetConnectionStringWithCredentials(CDSConnectionSetup)));
    end;

    [TryFunction]
    [NonDebuggable]
    local procedure InitializeConnection(var CrmHelper: DotNet CrmHelper; ConnectionString: SecretText)
    begin
        CrmHelper := CrmHelper.CrmHelper(ConnectionString.Unwrap());
    end;

    local procedure ProcessConnectionFailures()
    var
        DotNetExceptionHandler: Codeunit "DotNet Exception Handler";
        FaultException: DotNet FaultException;
        FileNotFoundException: DotNet FileNotFoundException;
        ArgumentNullException: DotNet ArgumentNullException;
        CrmHelper: DotNet CrmHelper;
        ErrorMessage: Text;
    begin
        DotNetExceptionHandler.Collect();

        if DotNetExceptionHandler.TryCastToType(GetDotNetType(FaultException)) then begin
            Session.LogMessage('0000AUP', ConnectionFailureTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(GeneralFailureErr);
        end;
        if DotNetExceptionHandler.TryCastToType(GetDotNetType(FileNotFoundException)) then begin
            Session.LogMessage('0000AUQ', ConnectionFailureTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(SolutionFileNotFoundErr);
        end;
        if DotNetExceptionHandler.TryCastToType(CrmHelper.OrganizationServiceFaultExceptionType()) then begin
            Session.LogMessage('0000AUR', ConnectionFailureTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(GeneralFailureErr);
        end;
        if DotNetExceptionHandler.TryCastToType(CrmHelper.SystemNetWebException()) then begin
            Session.LogMessage('0000AUS', ConnectionFailureTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(CDSConnectionURLWrongErr);
        end;
        if DotNetExceptionHandler.CastToType(ArgumentNullException, GetDotNetType(ArgumentNullException)) then
            case ArgumentNullException.ParamName() of
                'cred':
                    begin
                        Session.LogMessage('0000AUT', ConnectionFailureTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                        Error(AdminUserPasswordWrongErr);
                    end;
                'Organization Name':
                    begin
                        Session.LogMessage('0000AUU', ConnectionFailureTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                        Error(CDSConnectionURLWrongErr);
                    end;
            end;

        ErrorMessage := DotNetExceptionHandler.GetMessage();
        if ErrorMessage <> '' then
            if ErrorMessage.ToLower().Contains(TimeoutTxt) then begin
                Session.LogMessage('0000EJ6', ConnectionFailureTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                Error(RetryAfterTimeoutErr, ErrorMessage);
            end;

        DotNetExceptionHandler.Rethrow();
    end;

    local procedure GetSystemAdminRoleTemplateId(): Guid
    begin
        exit(TextToGuid(SystemAdminRoleTemplateIdTxt));
    end;

    local procedure GetSystemCustomizerRoleTemplateId(): Guid
    begin
        exit(TextToGuid(SystemCustomizerRoleTemplateIdTxt));
    end;

    local procedure GetIntegrationRoleId(): Guid
    begin
        exit(TextToGuid(IntegrationRoleIdTxt));
    end;

    local procedure GetVirtualTablesIntegrationRoleId(): Guid
    begin
        exit(TextToGuid(VirtualTablesIntegrationRoleIdTxt));
    end;

    local procedure GetErrorNotificationId(): Guid
    begin
        exit(TextToGuid(ErrorNotificationIdTxt));
    end;

    local procedure GetConnectionDisabledNotificationId(): Guid
    begin
        exit(TextToGuid(ConnectionDisabledNotificationIdTxt));
    end;

    local procedure GetMultipleCompaniesNotificationId(): Guid
    begin
        exit(TextToGuid(MultipleCompaniesNotificationIdTxt));
    end;

    local procedure TextToGuid(TextVar: Text): Guid
    var
        GuidVar: Guid;
    begin
        if not Evaluate(GuidVar, TextVar) then;
        exit(GuidVar);
    end;

    internal procedure SendConnectionDisabledNotification(DisableReason: Text[250])
    var
        Notification: Notification;
    begin
        Notification.Id := GetConnectionDisabledNotificationId();
        Notification.Message := StrSubstNo(ConnectionDisabledNotificationMsg, DisableReason);
        Notification.Scope := NOTIFICATIONSCOPE::LocalScope;
        Notification.Send();
    end;

    procedure SendMultipleCompaniesNotification()
    var
        MyNotifications: Record "My Notifications";
        Notification: Notification;
    begin
        Notification.Id := GetMultipleCompaniesNotificationId();
        if not MyNotifications.IsEnabled(Notification.Id) then
            exit;

        Notification.Message := MultipleCompaniesNotificationMsg;
        Notification.AddAction(OpenIntegrationTableMappingsMsg, Codeunit::"CDS Integration Impl.", 'OpenIntegrationTableMappings');
        Notification.AddAction(LearnMoreTxt, Codeunit::"CDS Integration Impl.", 'MultipleCompanyNotificationHelpLink');
        Notification.AddAction(HideNotificationTxt, CODEUNIT::"CDS Integration Impl.", 'DisableMultipleCompanyNotification');
        Notification.Scope := NOTIFICATIONSCOPE::LocalScope;
        Notification.Send();
    end;

    procedure MultipleCompanyNotificationHelpLink(Notification: Notification)
    begin
        Hyperlink(MultipleCompanySynchHelpLinkTxt);
    end;


    procedure DisableMultipleCompanyNotification(Notification: Notification)
    var
        MyNotifications: Record "My Notifications";
    begin
        if not MyNotifications.Disable(GetMultipleCompaniesNotificationId()) then
            MyNotifications.InsertDefault(GetMultipleCompaniesNotificationId(), MultipleCompaniesNotificationNameTxt, MultipleCompaniesNotificationDescriptionTxt, false);
    end;

    procedure OpenIntegrationTableMappings(Notification: Notification)
    var
        IntegrationTableMapping: Record "Integration Table Mapping";
    begin
        IntegrationTableMapping.SetRange(Type, IntegrationTableMapping.Type::Dataverse);
        IntegrationTableMapping.SetRange("Delete After Synchronization", false);
        Page.Run(Page::"Integration Table Mapping List", IntegrationTableMapping);
    end;

    local procedure ShowError(ActivityDescription: Text[128]; ErrorMessage: Text)
    var
        MyNotifications: Record "My Notifications";
        SystemInitialization: Codeunit "System Initialization";
    begin
        if (not SystemInitialization.IsInProgress()) and (GetExecutionContext() = ExecutionContext::Normal) then
            Error(ErrorMessage);

        MyNotifications.InsertDefault(GetErrorNotificationId(), ActivityDescription, ErrorMessage, true);
    end;

    [Scope('OnPrem')]
    procedure InsertBusinessUnitCoupling(var CDSConnectionSetup: Record "CDS Connection Setup")
    var
        Company: Record Company;
        CDSCoupledBusinessUnit: Record "CDS Coupled Business Unit";
    begin
        if IsNullGuid(CDSConnectionSetup."Business Unit Id") then
            exit;

        if not Company.Get(CompanyName()) then
            exit;

        CDSCoupledBusinessUnit.Validate("Company Id", Company.SystemId);
        CDSCoupledBusinessUnit.Validate("Business Unit Id", CDSConnectionSetup."Business Unit Id");
        CDSCoupledBusinessUnit.Insert(true);
    end;

    [Scope('OnPrem')]
    procedure ModifyBusinessUnitCoupling(var CDSConnectionSetup: Record "CDS Connection Setup")
    var
        Company: Record Company;
        CDSCoupledBusinessUnit: Record "CDS Coupled Business Unit";
    begin
        if not Company.Get(CompanyName()) then
            exit;

        CDSCoupledBusinessUnit.SetRange("Company Id", Company.SystemId);
        CDSCoupledBusinessUnit.DeleteAll(true);

        if IsNullGuid(CDSConnectionSetup."Business Unit Id") then
            exit;

        CDSCoupledBusinessUnit.Init();
        CDSCoupledBusinessUnit.Validate("Company Id", Company.SystemId);
        CDSCoupledBusinessUnit.Validate("Business Unit Id", CDSConnectionSetup."Business Unit Id");
        CDSCoupledBusinessUnit.Insert(true);
    end;

    [Scope('OnPrem')]
    procedure DeleteBusinessUnitCoupling(var CDSConnectionSetup: Record "CDS Connection Setup")
    var
        Company: Record Company;
        CDSCoupledBusinessUnit: Record "CDS Coupled Business Unit";
    begin
        if not Company.Get(CompanyName()) then
            exit;

        CDSCoupledBusinessUnit.SetRange("Company Id", Company.SystemId);
        CDSCoupledBusinessUnit.SetRange("Business Unit Id", CDSConnectionSetup."Business Unit Id");
        CDSCoupledBusinessUnit.DeleteAll(true);
    end;

    [Scope('OnPrem')]
    procedure TestConnection(var CDSConnectionSetup: Record "CDS Connection Setup"): Boolean
    begin
        if TryTestConnection(CDSConnectionSetup) then
            exit(true);

        exit(TryCheckCredentials(CDSConnectionSetup));
    end;

    [TryFunction]
    local procedure TryTestConnection(var CDSConnectionSetup: Record "CDS Connection Setup")
    var
        CRMSystemuser: Record "CRM Systemuser";
        TempConnectionName: Text;
        Id: Guid;
    begin
        CheckConnectionRequiredFields(CDSConnectionSetup, false);

        TempConnectionName := GetTempConnectionName();
        RegisterConnection(CDSConnectionSetup, TempConnectionName);
        SetDefaultTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName, true);

        if CRMSystemuser.FindFirst() then
            Id := CRMSystemuser.SystemUserId;

        UnregisterTableConnection(TABLECONNECTIONTYPE::CRM, TempConnectionName);
    end;

    [Scope('OnPrem')]
    [TryFunction]
    procedure TryCheckCredentials(var CDSConnectionSetup: Record "CDS Connection Setup")
    begin
        CheckCredentials(CDSConnectionSetup);
    end;

    [Scope('OnPrem')]
    procedure CheckCredentials(var CDSConnectionSetup: Record "CDS Connection Setup")
    var
        CrmHelper: DotNet CrmHelper;
    begin
        CheckConnectionRequiredFields(CDSConnectionSetup, false);
        if (CDSConnectionSetup."Authentication Type" = CDSConnectionSetup."Authentication Type"::Office365) or (CDSConnectionSetup."Authentication Type" = CDSConnectionSetup."Authentication Type"::AD) then
            exit;

        if not InitializeConnection(CrmHelper, CDSConnectionSetup) then begin
            Session.LogMessage('0000BO2', ConnectionNotRegisteredTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            ProcessConnectionFailures();
        end;
        if not CheckCredentials(CrmHelper) then begin
            Session.LogMessage('0000BO3', InvalidUserCredentialsTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(IntegrationUserPasswordWrongErr);
        end;
    end;

    [TryFunction]
    local procedure CheckCredentials(var CrmHelper: DotNet CrmHelper)
    begin
        CrmHelper.CheckCredentials();
    end;

    [Scope('OnPrem')]
    [TryFunction]
    procedure GetCDSVersion(var CDSConnectionSetup: Record "CDS Connection Setup"; var CDSVersion: Text)
    var
        CrmHelper: DotNet CrmHelper;
    begin
        if InitializeConnection(CrmHelper, CDSConnectionSetup) then
            CDSVersion := CrmHelper.GetConnectedCrmVersion()
    end;

    [Scope('OnPrem')]
    procedure InitializeProxyVersionList(var TempStack: Record TempStack temporary)
    var
        CrmHelper: DotNet CrmHelper;
        IList: DotNet GenericList1;
        i: Integer;
        ProxyCount: Integer;
    begin
        IList := CrmHelper.GetProxyIdList();
        ProxyCount := IList.Count();
        for i := 0 to ProxyCount - 1 do begin
            TempStack.StackOrder := IList.Item(i);
            TempStack.Insert();
        end;
    end;

    [Scope('OnPrem')]
    procedure GetLastProxyVersionItem(): Integer
    var
        TempStack: Record TempStack temporary;
    begin
        InitializeProxyVersionList(TempStack);
        TempStack.FindLast();
        exit(TempStack.StackOrder);
    end;

    [Scope('OnPrem')]
    procedure ClearConnectionDisableReason(var CDSConnectionSetup: Record "CDS Connection Setup")
    var
        Notification: Notification;
    begin
        if CDSConnectionSetup."Disable Reason" = '' then
            exit;
        Session.LogMessage('0000AUV', ClearDisabledReasonTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        Notification.Id := GetConnectionDisabledNotificationId();
        Notification.Recall();
        Clear(CDSConnectionSetup."Disable Reason");
        CDSConnectionSetup.Modify();
    end;

    [Scope('OnPrem')]
    procedure UpdateDomainName(var CDSConnectionSetup: Record "CDS Connection Setup")
    var
        UserName: Text;
    begin
        UserName := CDSConnectionSetup."User Name";
        if UserName <> '' then
            if StrPos(UserName, '\') > 0 then
                CDSConnectionSetup.Validate(Domain, CopyStr(UserName, 1, StrPos(UserName, '\') - 1))
            else
                CDSConnectionSetup.Domain := '';
    end;

    [Scope('OnPrem')]
    procedure CheckUserName(var CDSConnectionSetup: Record "CDS Connection Setup")
    var
        UserName: Text;
    begin
        UserName := CDSConnectionSetup."User Name";
        if UserName <> '' then
            case CDSConnectionSetup."Authentication Type" of
                CDSConnectionSetup."Authentication Type"::AD:
                    if StrPos(UserName, '\') = 0 then
                        Error(UserNameMustIncludeDomainErr);
                CDSConnectionSetup."Authentication Type"::Office365:
                    if StrPos(UserName, '@') = 0 then
                        Error(UserNameMustBeEmailErr);
            end;
    end;

    local procedure GetDomainToken(var CDSConnectionSetup: Record "CDS Connection Setup"): Text
    begin
        if CDSConnectionSetup.Domain <> '' then
            exit(StrSubstNo('Domain=%1;', CDSConnectionSetup.Domain));
    end;

    [Scope('OnPrem')]
    procedure GetAuthenticationTypeToken(var CDSConnectionSetup: Record "CDS Connection Setup"): Text
    begin
        exit(GetAuthenticationTypeToken(CDSConnectionSetup, ''));
    end;

    [Scope('OnPrem')]
    procedure GetAuthenticationTypeToken(var CDSConnectionSetup: Record "CDS Connection Setup"; Domain: Text): Text
    begin
        case CDSConnectionSetup."Authentication Type" of
            CDSConnectionSetup."Authentication Type"::Office365:
                exit('AuthType=Office365;');
            CDSConnectionSetup."Authentication Type"::AD:
                if Domain = '' then
                    exit('AuthType=AD;' + GetDomainToken(CDSConnectionSetup))
                else
                    exit('AuthType=AD; Domain=' + Domain + ';');
            CDSConnectionSetup."Authentication Type"::IFD:
                exit('AuthType=IFD;' + GetDomainToken(CDSConnectionSetup) + 'HomeRealmUri= ;');
            CDSConnectionSetup."Authentication Type"::OAuth:
                exit('AuthType=OAuth;' + 'AppId= ;' + 'RedirectUri= ;' + 'TokenCacheStorePath= ;' + 'LoginPrompt=Auto;');
        end;
    end;

    [Scope('OnPrem')]
    procedure IsCDSVersionValid(CDSVersion: Text): Boolean
    var
        Version: DotNet Version;
    begin
        if CDSVersion <> '' then
            if Version.TryParse(CDSVersion, Version) then
                exit((Version.Major() > 6) and not ((Version.Major() = 7) and (Version.Minor() = 1)));
        exit(false);
    end;

    [Scope('OnPrem')]
    procedure IsSolutionVersionValid(SolutionVersion: Text): Boolean
    var
        Version: DotNet Version;
    begin
        if SolutionVersion <> '' then
            if Version.TryParse(SolutionVersion, Version) then
                exit(Version.Major() >= 1);
        exit(false);
    end;

    procedure MultipleCompaniesConnected(): Boolean
    var
        CDSCompanyCount: Integer;
    begin
        if not TryCDSCompanyCount(CDSCompanyCount) then
            exit(false);

        exit(CDSCompanyCount > 1);
    end;

    [TryFunction]
    local procedure TryCDSCompanyCount(var CompanyCount: Integer)
    var
        CDSCompany: Record "CDS Company";
        CRMIntegrationManagement: Codeunit "CRM Integration Management";
    begin
        if (CRMIntegrationManagement.IsCDSIntegrationEnabled() or CRMIntegrationManagement.IsCRMIntegrationEnabled()) then
            CompanyCount := CDSCompany.Count();
    end;

    internal procedure GetBusinessEventsSupported(): Boolean
    var
        Supported: Boolean;
        Handled: Boolean;
    begin
        OnBeforeGetBusinessEventsSupported(Supported, Handled);
        if Handled then
            exit(Supported);

        exit(EnvironmentInfo.IsSaaSInfrastructure());
    end;

    [NonDebuggable]
    procedure UpdateBusinessEventsSetupFromWizard(var SourceCDSConnectionSetup: Record "CDS Connection Setup")
    var
        CDSConnectionSetup: Record "CDS Connection Setup";
    begin
        Session.LogMessage('0000GBQ', UpdateBusinessEventsSetupTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
        if not CDSConnectionSetup.Get() then begin
            CDSConnectionSetup.Init();
            CDSConnectionSetup.Insert();
        end;
        CDSConnectionSetup.Validate("Server Address", SourceCDSConnectionSetup."Server Address");
        CDSConnectionSetup.Validate("Authentication Type", SourceCDSConnectionSetup."Authentication Type");
        if not EnvironmentInfo.IsSaaSInfrastructure() then begin
            CDSConnectionSetup.Validate("Client Id", SourceCDSConnectionSetup."Client Id");
            CDSConnectionSetup.SetClientSecret(SourceCDSConnectionSetup.GetSecretClientSecret());
            CDSConnectionSetup.Validate("Redirect URL", SourceCDSConnectionSetup."Redirect URL");
        end;
        CDSConnectionSetup.Validate("Business Events Enabled", SourceCDSConnectionSetup."Business Events Enabled");
        CDSConnectionSetup.Validate("Virtual Tables Config Id", SourceCDSConnectionSetup."Virtual Tables Config Id");
        CDSConnectionSetup.Modify();
        Session.LogMessage('0000GBR', BusinessEventsSetupUpdatedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
    end;

    [Scope('OnPrem')]
    procedure UpdateConnectionSetupFromWizard(var SourceCDSConnectionSetup: Record "CDS Connection Setup"; PasswordText: SecretText)
    var
        CDSConnectionSetup: Record "CDS Connection Setup";
    begin
        Session.LogMessage('0000AUZ', UpdateSynchronizationSetupTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        if not CDSConnectionSetup.Get() then begin
            CDSConnectionSetup.Init();
            CDSConnectionSetup.Insert();
        end;
        CDSConnectionSetup.Validate("Server Address", SourceCDSConnectionSetup."Server Address");
        CDSConnectionSetup.Validate("Authentication Type", SourceCDSConnectionSetup."Authentication Type");
        CDSConnectionSetup.Validate("User Name", SourceCDSConnectionSetup."User Name");
        CDSConnectionSetup.SetPassword(PasswordText);
        if not EnvironmentInfo.IsSaaSInfrastructure() then begin
            CDSConnectionSetup.Validate("Client Id", SourceCDSConnectionSetup."Client Id");
            CDSConnectionSetup.SetClientSecret(SourceCDSConnectionSetup.GetSecretClientSecret());
            CDSConnectionSetup.Validate("Redirect URL", SourceCDSConnectionSetup."Redirect URL");
        end;
        CDSConnectionSetup.Validate("Proxy Version", SourceCDSConnectionSetup.GetProxyVersion());
        CDSConnectionSetup.Validate("Business Unit Id", SourceCDSConnectionSetup."Business Unit Id");
        CDSConnectionSetup.Validate("Business Unit Name", SourceCDSConnectionSetup."Business Unit Name");
        CDSConnectionSetup.Validate("Is Enabled", SourceCDSConnectionSetup."Is Enabled");
        CDSConnectionSetup.Validate(CurrencyDecimalPrecision, SourceCDSConnectionSetup.CurrencyDecimalPrecision);
        CDSConnectionSetup.Validate(BaseCurrencyId, SourceCDSConnectionSetup.BaseCurrencyId);
        CDSConnectionSetup.Validate(BaseCurrencyPrecision, SourceCDSConnectionSetup.BaseCurrencyPrecision);
        CDSConnectionSetup.Validate(BaseCurrencySymbol, SourceCDSConnectionSetup.BaseCurrencySymbol);
        CDSConnectionSetup.Validate(BaseCurrencyCode, SourceCDSConnectionSetup.BaseCurrencyCode);
        if SourceCDSConnectionSetup."Ownership Model" in [CDSConnectionSetup."Ownership Model"::Person, CDSConnectionSetup."Ownership Model"::Team] then
            CDSConnectionSetup.Validate("Ownership Model", SourceCDSConnectionSetup."Ownership Model")
        else
            CDSConnectionSetup.Validate("Ownership Model", CDSConnectionSetup."Ownership Model"::Team);
        SetConnectionString(CDSConnectionSetup, SourceCDSConnectionSetup."Connection String");

        Session.LogMessage('0000AV0', SynchronizationSetupUpdatedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
    end;

    [NonDebuggable]
    local procedure PromptForAdminCredentials(var CDSConnectionSetup: Record "CDS Connection Setup"; var AdminUser: Text; var AdminPassword: SecretText; var AdminADDomain: Text): Boolean
    var
        TempOfficeAdminCredentials: Record "Office Admin. Credentials" temporary;
        BackslashPos: Integer;
    begin
        TempOfficeAdminCredentials.Endpoint := CDSConnectionSetup."Server Address";
        TempOfficeAdminCredentials.Insert();
        Commit();
        if Page.RunModal(Page::"Dynamics CRM Admin Credentials", TempOfficeAdminCredentials) <> Action::LookupOK then begin
            Session.LogMessage('0000AV1', IgnoredAdminCredentialsTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit(false);
        end;
        if (TempOfficeAdminCredentials.Email = '') or (TempOfficeAdminCredentials.Password = '') then begin
            Session.LogMessage('0000AV2', InvalidAdminCredentialsTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            exit(false);
        end;

        if CDSConnectionSetup."Authentication Type" = CDSConnectionSetup."Authentication Type"::AD then begin
            BackslashPos := StrPos(TempOfficeAdminCredentials.Email, '\');
            if (BackslashPos <= 1) or (BackslashPos = StrLen(TempOfficeAdminCredentials.Email)) then
                Error(UserNameMustIncludeDomainErr);
            AdminADDomain := CopyStr(TempOfficeAdminCredentials.Email, 1, BackslashPos - 1);
            AdminUser := CopyStr(TempOfficeAdminCredentials.Email, BackslashPos + 1);
            AdminPassword := TempOfficeAdminCredentials.Password;
            exit(true);
        end;
        AdminUser := TempOfficeAdminCredentials.Email;
        AdminPassword := TempOfficeAdminCredentials.Password;
        exit(true);
    end;

    [Scope('OnPrem')]
    procedure GetTempConnectionName(): Text
    begin
        if TemporaryConnectionName = '' then
            TemporaryConnectionName := StrSubstNo('%1%2', TemporaryConnectionPrefixTok, CreateGuid());
        exit(TemporaryConnectionName);
    end;

    [Scope('OnPrem')]
    procedure GetDefaultBusinessUnitName(): Text[160]
    var
        Company: Record Company;
    begin
        Company.Get(CompanyName());
        exit(GetDefaultBusinessUnitName(Company.Name, GetCompanyExternalId(Company)));
    end;

    local procedure GetDefaultBusinessUnitName(CompanyName: Text; CompanyId: Text) BusinessUnitName: Text[160]
    var
        IsHandled: Boolean;
    begin
        OnBeforeGetDefaultBusinessUnitName(BusinessUnitName, IsHandled);
        if IsHandled then
            exit;
        BusinessUnitName := CopyStr(StrSubstNo(BusinessUnitNameTemplateTok, CompanyName, CompanyId), 1, MaxStrLen(BusinessUnitName));
    end;

    local procedure GetOwningTeamName(BusinessUnitName: Text) TeamName: Text[160]
    begin
        TeamName := CopyStr(StrSubstNo(TeamNameTemplateTok, BusinessUnitName), 1, MaxStrLen(TeamName));
    end;

    [Scope('OnPrem')]
    procedure GetCompanyExternalId() ExternalId: Text[36]
    var
        Company: Record Company;
    begin
        Company.Get(CompanyName());
        ExternalId := GetCompanyExternalId(Company);
    end;

    local procedure GetCompanyExternalId(Company: Record Company) ExternalId: Text[36]
    begin
        ExternalId := CopyStr(Format(Company.SystemId).ToLower().Replace('{', '').Replace('}', ''), 1, MaxStrLen(ExternalId));
    end;

    [Scope('OnPrem')]
    procedure ShowIntegrationUser(CDSConnectionSetup: Record "CDS Connection Setup")
    var
        CRMIntegrationManagement: Codeunit "CRM Integration Management";
        IntegrationUserId: Guid;
    begin
        IntegrationUserId := GetIntegrationUserId(CDSConnectionSetup);
        Hyperlink(CRMIntegrationManagement.GetCRMEntityUrlFromCRMID(Database::"CRM Systemuser", IntegrationUserId));
    end;

    [Scope('OnPrem')]
    procedure ShowOwningTeam(CDSConnectionSetup: Record "CDS Connection Setup")
    var
        CRMIntegrationManagement: Codeunit "CRM Integration Management";
        OwningTeamId: Guid;
    begin
        OwningTeamId := GetOwningTeamId(CDSConnectionSetup);
        Hyperlink(CRMIntegrationManagement.GetCRMEntityUrlFromCRMID(Database::"CRM Team", OwningTeamId));
    end;

    internal procedure ShowSyntheticRelationsConfig()
    var
        CDSConnectionSetup: Record "CDS Connection Setup";
    begin
        if not CDSConnectionSetup.Get() then
            exit;
        if not IsNullGuid(CDSConnectionSetup."Virtual Tables Config Id") then
            Hyperlink(GetCRMEntityListUrl(CDSConnectionSetup, SyntheticRelationEntityNameTxt, VirtualTableAppNameTxt));
    end;

    internal procedure ShowVirtualTablesConfig(CDSConnectionSetup: Record "CDS Connection Setup")
    begin
        if not IsNullGuid(CDSConnectionSetup."Virtual Tables Config Id") then
            Hyperlink(GetCRMEntityUrlFromCRMID(CDSConnectionSetup."Server Address", Database::"CRM BC Virtual Table Config.", CDSConnectionSetup."Virtual Tables Config Id", VirtualTableAppNameTxt));
    end;

    internal procedure GetCRMEntityUrlFromCRMID(ServerAddress: Text; TableId: Integer; CRMId: Guid; AppName: Text): Text
    var
        CRMIntegrationManagement: Codeunit "CRM Integration Management";
        CRMEntityUrl: Text;
    begin
        if AppName <> '' then
            CRMEntityUrl := StrSubstNo(CRMEntityWithAppUrlTemplateTxt, ServerAddress, AppName, CRMIntegrationManagement.GetCRMEntityTypeName(TableId), CRMId)
        else
            CRMEntityUrl := StrSubstNo(CRMEntityUrlTemplateTxt, ServerAddress, CRMIntegrationManagement.GetCRMEntityTypeName(TableId), CRMId);
        exit(CRMEntityUrl);
    end;

    internal procedure GetCRMEntityListUrl(var CDSConnectionSetup: Record "CDS Connection Setup"; EntityTypeName: Text; AppName: Text): Text
    begin
        if AppName <> '' then
            exit(StrSubstNo(CRMEntityListWithAppUrlTemplateTxt, CDSConnectionSetup."Server Address", AppName, EntityTypeName))
        else
            exit(StrSubstNo(CRMEntityListUrlTemplateTxt, CDSConnectionSetup."Server Address", EntityTypeName));
    end;

    [EventSubscriber(ObjectType::Table, Database::"Service Connection", 'OnRegisterServiceConnection', '', false, false)]
    local procedure HandleOnRegisterServiceConnection(var ServiceConnection: Record "Service Connection")
    var
        CDSConnectionSetup: Record "CDS Connection Setup";
        [SecurityFiltering(SecurityFilter::Ignored)]
        CDSConnectionSetup2: Record "CDS Connection Setup";
        RecRef: RecordRef;
    begin
        if not CDSConnectionSetup.Get() then begin
            if not CDSConnectionSetup2.WritePermission() then begin
                Session.LogMessage('0000AV4', NoPermissionsTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                exit;
            end;
            CDSConnectionSetup.Init();
            CDSConnectionSetup.Insert();
        end;

        RecRef.GetTable(CDSConnectionSetup);
        ServiceConnection.Status := ServiceConnection.Status::Enabled;
        if not CDSConnectionSetup."Is Enabled" then
            ServiceConnection.Status := ServiceConnection.Status::Disabled
        else
            if TryCheckEntitiesAvailability(CDSConnectionSetup) then
                ServiceConnection.Status := ServiceConnection.Status::Connected
            else
                ServiceConnection.Status := ServiceConnection.Status::Error;
        ServiceConnection.InsertServiceConnectionExtended(
          ServiceConnection, RecRef.RecordId(),
          CDSConnectionSetup.TableCaption(), CDSConnectionSetup."Server Address",
          PAGE::"CDS Connection Setup", PAGE::"CDS Connection Setup Wizard");
    end;

    [Scope('OnPrem')]
    procedure ScheduleRebuildingOfCouplingTable()
    begin
        if not Confirm(RebuildCouplingTableQst) then
            exit
        else
            Hyperlink(RebuildCouplingTableExtensionLinkTxt);
    end;

    procedure GetOptionSetMetadata(EntityName: Text; FieldName: Text): Dictionary of [Integer, Text]
    var
        CrmHelper: DotNet CrmHelper;
        OptionMetadataDictionary: Dotnet GenericDictionary2;
        OptionMetadata: DotNet GenericKeyValuePair2;
        Result: Dictionary of [Integer, Text];
    begin
        if (EntityName <> '') and (FieldName <> '') then
            if InitializeConnection(CrmHelper) then begin
                OptionMetadataDictionary := CrmHelper.GetOptionSetMetadata(EntityName, FieldName);
                foreach OptionMetadata in OptionMetadataDictionary do
                    Result.Add(OptionMetadata.Key, OptionMetadata.Value);
            end;
        exit(Result);
    end;

    procedure InsertOptionSetMetadata(EntityName: Text; FieldName: Text; NewOptionLabel: Text): Integer
    var
        CRMOrganization: Record "CRM Organization";
        CrmHelper: DotNet CrmHelper;
        Result: Integer;
    begin
        if (EntityName <> '') and (FieldName <> '') and (NewOptionLabel <> '') then
            if InitializeConnection(CrmHelper) then begin
                CRMOrganization.FindFirst();
                if CRMOrganization.LanguageCode <> 0 then
                    Result := CRMHelper.InsertOptionSetMetadata(EntityName, FieldName, NewOptionLabel, CRMOrganization.LanguageCode)
                else
                    Result := CrmHelper.InsertOptionSetMetadata(EntityName, FieldName, NewOptionLabel);
            end;
        exit(Result);
    end;

    procedure InsertOptionSetMetadataWithOptionValue(EntityName: Text; FieldName: Text; NewOptionLabel: Text; NewOptionValue: Integer): Integer
    var
        CRMOrganization: Record "CRM Organization";
        CrmHelper: DotNet CrmHelper;
        Result: Integer;
    begin
        if (EntityName <> '') and (FieldName <> '') and (NewOptionLabel <> '') and (NewOptionValue <> 0) then
            if InitializeConnection(CrmHelper) then begin
                CRMOrganization.FindFirst();
                if CRMOrganization.LanguageCode <> 0 then
                    Result := CRMHelper.InsertOptionSetMetadataWithOptionValueAndLanguageCode(EntityName, FieldName, NewOptionLabel, NewOptionValue, CRMOrganization.LanguageCode)
                else
                    Result := CrmHelper.InsertOptionSetMetadataWithOptionValue(EntityName, FieldName, NewOptionLabel, NewOptionValue);
            end;
        exit(Result);
    end;

    procedure UpdateOptionSetMetadata(EntityName: Text; FieldName: Text; OptionValue: Integer; NewOptionLabel: Text)
    var
        CRMOrganization: Record "CRM Organization";
        CrmHelper: DotNet CrmHelper;
    begin
        if (EntityName <> '') and (FieldName <> '') and (OptionValue <> 0) and (NewOptionLabel <> '') then
            if InitializeConnection(CrmHelper) then begin
                CRMOrganization.FindFirst();
                if CRMOrganization.LanguageCode <> 0 then
                    CrmHelper.UpdateOptionSetMetadataWithLanguageCode(EntityName, FieldName, OptionValue, NewOptionLabel, CRMOrganization.LanguageCode)
                else
                    CrmHelper.UpdateOptionSetMetadata(EntityName, FieldName, OptionValue, NewOptionLabel);
            end;
    end;


    [NonDebuggable]
    internal procedure RegisterConfiguredConnection(var CrmHelper: DotNet CrmHelper): Text
    var
        CDSConnectionSetup: Record "CDS Connection Setup";
        TempAdminCDSConnectionSetup: Record "CDS Connection Setup" temporary;
        TempConnectionName, TempConnectionStringWithPlaceholder : Text;
        TempConnectionString, AccessToken : SecretText;
    begin
        if not CDSConnectionSetup.FindFirst() then
            exit;
        if not CDSConnectionSetupConfiguredForVirtualTables(CDSConnectionSetup) then
            exit;

        GetTempConnectionSetup(TempAdminCDSConnectionSetup, CDSConnectionSetup, AccessToken);
        TempConnectionName := GetTempConnectionName();

        GetAccessToken(CDSConnectionSetup."Server Address", true, AccessToken);
        TempConnectionStringWithPlaceholder := StrSubstNo(OAuthConnectionStringFormatTxt, CDSConnectionSetup."Server Address", '%1', CDSConnectionSetup.GetProxyVersion(), GetAuthenticationTypeToken(CDSConnectionSetup));
        TempConnectionString := SecretStrSubstNo(TempConnectionStringWithPlaceholder, AccessToken);
        if not InitializeConnection(CrmHelper, TempConnectionString) then begin
            Session.LogMessage('0000KN7', ConnectionNotRegisteredTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            ProcessConnectionFailures();
        end;

        GetTempConnectionSetup(TempAdminCDSConnectionSetup, CDSConnectionSetup, AccessToken);
        TempConnectionName := GetTempConnectionName();
        RegisterConnection(TempAdminCDSConnectionSetup, TempConnectionName);
        SetDefaultTableConnection(TableConnectionType::CRM, TempConnectionName, false);
        exit(TempConnectionName);
    end;

    local procedure CDSConnectionSetupConfiguredForVirtualTables(CDSConnectionSetup: Record "CDS Connection Setup"): Boolean
    begin
        if not CDSConnectionSetup."Business Events Enabled" then
            exit(false);

        if CDSConnectionSetup."Authentication Type" <> CDSConnectionSetup."Authentication Type"::Office365 then begin
            Session.LogMessage('0000LKE', VTCannotConfigureTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            CDSConnectionSetup.TestField("Authentication Type", CDSConnectionSetup."Authentication Type"::Office365);
        end;

        if not CheckConnectionRequiredFields(CDSConnectionSetup, true) then begin
            Session.LogMessage('0000LKF', VTCannotConfigureTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
            Error(GetLastErrorText());
        end;
        exit(true);
    end;

    internal procedure LoadAvailableVirtualTables(var CDSAvVirtualTableBuffer: Record "CDS Av. Virtual Table Buffer"; LoadOnlyVisible: Boolean)
    var
        CDSAvailableVirtualTable: Record "CDS Available Virtual Table";
        CDSConnectionSetup: Record "CDS Connection Setup";
        JobQueueEntry: Record "Job Queue Entry";
        TempAdminCDSConnectionSetup: Record "CDS Connection Setup" temporary;
        CrmHelper: DotNet CrmHelper;
        AccessToken: SecretText;
        TempConnectionStringWithPlaceholder: Text;
        TempConnectionString: SecretText;
        TempConnectionName: Text;
    begin
        if CDSConnectionSetup.FindFirst() then
            if CDSConnectionSetup."Business Events Enabled" then begin
                if CDSConnectionSetup."Authentication Type" <> CDSConnectionSetup."Authentication Type"::Office365 then begin
                    Session.LogMessage('0000KN5', VTCannotConfigureTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                    CDSConnectionSetup.TestField("Authentication Type", CDSConnectionSetup."Authentication Type"::Office365);
                end;

                if not CheckConnectionRequiredFields(CDSConnectionSetup, true) then begin
                    Session.LogMessage('0000KN6', VTCannotConfigureTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                    Error(GetLastErrorText());
                end;

                GetAccessToken(CDSConnectionSetup."Server Address", true, AccessToken);
                TempConnectionStringWithPlaceholder := StrSubstNo(OAuthConnectionStringFormatTxt, CDSConnectionSetup."Server Address", '%1', CDSConnectionSetup.GetProxyVersion(), GetAuthenticationTypeToken(CDSConnectionSetup));
                TempConnectionString := SecretStrSubstNo(TempConnectionStringWithPlaceholder, AccessToken);
                if not InitializeConnection(CrmHelper, TempConnectionString) then begin
                    Session.LogMessage('0000KN7', ConnectionNotRegisteredTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                    ProcessConnectionFailures();
                end;

                GetTempConnectionSetup(TempAdminCDSConnectionSetup, CDSConnectionSetup, AccessToken);
                TempConnectionName := GetTempConnectionName();
                RegisterConnection(TempAdminCDSConnectionSetup, TempConnectionName);
                SetDefaultTableConnection(TableConnectionType::CRM, TempConnectionName, true);

                if CDSAvailableVirtualTable.FindSet() then
                    repeat
                        if CDSAvailableVirtualTable.mserp_hasbeengenerated or (not LoadOnlyVisible) then begin
                            CDSAvVirtualTableBuffer.TransferFields(CDSAvailableVirtualTable);

                            JobQueueEntry.SetRange("Object ID to Run", Codeunit::"Enable CDS Virtual Tables");
                            JobQueueEntry.SetRange("Object Type to Run", JobQueueEntry."Object Type to Run"::Codeunit);
                            JobQueueEntry.SetRange("Job Queue Category Code", CopyStr(EnableCDSVirtualTablesJobQueueCategoryTxt, 1, MaxStrLen(JobQueueEntry."Job Queue Category Code")));
                            JobQueueEntry.SetRange(Description, StrSubstNo(EnableCDSVirtualTablesJobQueueDescriptionTxt, CDSAvailableVirtualTable.mserp_cdsentitylogicalname));
                            if JobQueueEntry.FindFirst() then
                                if JobQueueEntry.Status in [JobQueueEntry.Status::"In Process", JobQueueEntry.Status::Ready] then
                                    CDSAvVirtualTableBuffer."In Process" := true;

                            CDSAvVirtualTableBuffer.Insert();
                        end;
                    until CDSAvailableVirtualTable.Next() = 0;

                UnregisterTableConnection(TableConnectionType::CRM, TempConnectionName);
            end;
    end;

    [NonDebuggable]
    internal procedure EnableVirtualTables(FilterTxt: Text)
    var
        CDSAvailableVirtualTable: Record "CDS Available Virtual Table";
        CDSConnectionSetup: Record "CDS Connection Setup";
        TempAdminCDSConnectionSetup: Record "CDS Connection Setup" temporary;
        CrmHelper: DotNet CrmHelper;
        AccessToken: SecretText;
        TempConnectionStringWithPlaceholder: Text;
        TempConnectionString: SecretText;
        TempConnectionName: Text;
    begin
        if CDSConnectionSetup.FindFirst() then
            if FilterTxt <> '' then begin
                if not CheckConnectionRequiredFields(CDSConnectionSetup, true) then begin
                    Session.LogMessage('0000KN8', VTCannotConfigureTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                    Error(GetLastErrorText());
                end;

                GetAccessToken(CDSConnectionSetup."Server Address", true, AccessToken);
                TempConnectionStringWithPlaceholder := StrSubstNo(OAuthConnectionStringFormatTxt, CDSConnectionSetup."Server Address", '%1', CDSConnectionSetup.GetProxyVersion(), GetAuthenticationTypeToken(CDSConnectionSetup));
                TempConnectionString := SecretStrSubstNo(TempConnectionStringWithPlaceholder, AccessToken);
                if not InitializeConnection(CrmHelper, TempConnectionString) then begin
                    Session.LogMessage('0000KN9', ConnectionNotRegisteredTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);
                    ProcessConnectionFailures();
                end;

                GetTempConnectionSetup(TempAdminCDSConnectionSetup, CDSConnectionSetup, AccessToken);
                TempConnectionName := GetTempConnectionName();
                RegisterConnection(TempAdminCDSConnectionSetup, TempConnectionName);
                SetDefaultTableConnection(TableConnectionType::CRM, TempConnectionName, true);

                CDSAvailableVirtualTable.SetFilter(mserp_cdsentitylogicalname, FilterTxt);
                if CDSAvailableVirtualTable.FindFirst() then begin
                    CDSAvailableVirtualTable.mserp_hasbeengenerated := true;
                    CDSAvailableVirtualTable.Modify();
                end;

                UnregisterTableConnection(TableConnectionType::CRM, TempConnectionName);
            end;
    end;

    procedure ScheduleEnablingVirtualTables(FilterList: List of [Text])
    var
        JobQueueEntry: Record "Job Queue Entry";
        FilterTxt: Text;
        WaitTime: Integer;
        EarliestStartDateTime: DateTime;
    begin
        EarliestStartDateTime := CurrentDateTime() + 1000;
        foreach FilterTxt in FilterList do begin
            Clear(JobQueueEntry);
            JobQueueEntry."Object Type to Run" := JobQueueEntry."Object Type to Run"::Codeunit;
            JobQueueEntry."Object ID to Run" := Codeunit::"Enable CDS Virtual Tables";
            JobQueueEntry."Job Queue Category Code" := CopyStr(EnableCDSVirtualTablesJobQueueCategoryTxt, 1, MaxStrLen(JobQueueEntry."Job Queue Category Code"));
            EarliestStartDateTime += 90000 * WaitTime;
            JobQueueEntry."Earliest Start Date/Time" := EarliestStartDateTime;
            JobQueueEntry."Run in User Session" := false;
            JobQueueEntry.Description := StrSubstNo(EnableCDSVirtualTablesJobQueueDescriptionTxt, FilterTxt);
            JobQueueEntry."Maximum No. of Attempts to Run" := 3;
            JobQueueEntry."Rerun Delay (sec.)" := 30;
            JobQueueEntry.Insert(true);
            JobQueueEntry.SetXmlContent(FilterTxt);
            Commit();
            Codeunit.Run(Codeunit::"Job Queue - Enqueue", JobQueueEntry);
            WaitTime += 1;
        end;
    end;

    procedure OpenEnableVirtualTablesJobFromNotification(ScheduledJobNotification: Notification)
    var
        JobQueueEntry: Record "Job Queue Entry";
    begin
        JobQueueEntry.SetRange("Job Queue Category Code", EnableCDSVirtualTablesJobQueueCategoryTxt);
        Page.Run(Page::"Job Queue Entries", JobQueueEntry);
    end;

    procedure LearnMoreDisablingCRMConnection(ErrorInfo: ErrorInfo)
    begin
        Hyperlink('https://go.microsoft.com/fwlink/?linkid=2206514');
    end;

    internal procedure CleanCDSIntegration(CompanyName: Text)
    var
        CDSConnectionSetup: Record "CDS Connection Setup";
        CRMConnectionSetup: Record "CRM Connection Setup";
        CRMIntegrationRecord: Record "CRM Integration Record";
        IntegrationSyncJob: Record "Integration Synch. Job";
        IntegrationsSyncJobErrors: Record "Integration Synch. Job Errors";
        TableKey: Codeunit "Table Key";
        DisableIntegrationRecordCleanup: Boolean;
    begin
        if CompanyName() <> CompanyName then begin
            CDSConnectionSetup.ChangeCompany(CompanyName);
            CRMConnectionSetup.ChangeCompany(CompanyName);
            IntegrationSyncJob.ChangeCompany(CompanyName);
            IntegrationsSyncJobErrors.ChangeCompany(CompanyName);
            CRMIntegrationRecord.ChangeCompany(CompanyName);
        end;

        // Here we delete the setup records
        CDSConnectionSetup.DeleteAll();
        CRMConnectionSetup.DeleteAll();

        OnBeforeCleanCRMIntegrationRecords(DisableIntegrationRecordCleanup);
        if not DisableIntegrationRecordCleanup then
            if not CRMIntegrationRecord.Truncate() then begin
                // Deleting all couplings can timeout so disable the keys before deleting
                TableKey.DisableAll(Database::"CRM Integration Record");
                CRMIntegrationRecord.DeleteAll();
            end;
    end;

    internal procedure GetEntityMetadata(TableNo: Integer): Text
    var
        TableMetadata: Record "Table Metadata";
        CrmHelper: DotNet CrmHelper;
    begin
        if TableNo = 0 then
            exit('');

        if not TableMetadata.Get(TableNo) then
            exit('');

        if TableMetadata.ExternalName = '' then
            exit('');

        if not InitializeConnection(CrmHelper) then
            exit('');

        Session.LogMessage('0000QL8', StrSubstNo(RetrievingEntityMetadataTxt, TableMetadata.ExternalName), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        exit(CrmHelper.GetEntityMetadata(TableMetadata.ExternalName));
    end;

    internal procedure GetEntityFields(TableNo: Integer; var IntegrationField: Record "Integration Field")
    var
        Field: Record Field;
        TableMatadata: Record "Table Metadata";
        CrmHelper: DotNet CrmHelper;
        FieldsDictionary: Dotnet GenericDictionary2;
        Fields: DotNet GenericKeyValuePair2;
    begin
        if TableNo = 0 then
            exit;

        if not TableMatadata.Get(TableNo) then
            exit;

        if TableMatadata.ExternalName = '' then
            exit;

        if not InitializeConnection(CrmHelper) then
            exit;

        Session.LogMessage('0000QL9', StrSubstNo(RetrievingEntityFieldsTxt, TableMatadata.ExternalName), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CategoryTok);

        FieldsDictionary := CrmHelper.GetEntityFields(TableMatadata.ExternalName);
        foreach Fields in FieldsDictionary do begin
            Field.SetRange(TableNo, TableNo);
            Field.SetRange(ExternalName, Fields.Key);
            if not Field.IsEmpty() then
                continue;
            IntegrationField."Table No." := TableNo;
            IntegrationField."Field Name" := Fields.Key;
            IntegrationField."Field Caption" := Fields.Value;
            IntegrationField.IsRuntime := true;
            IntegrationField.Insert();
        end;
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeCleanCRMIntegrationRecords(var DisableIntegrationRecordCleanup: Boolean)
    begin
    end;

    [EventSubscriber(ObjectType::Table, Database::"Integration Synch. Job Errors", 'OnIsDataIntegrationEnabled', '', false, false)]
    local procedure IsDataIntegrationEnabled(var IsIntegrationEnabled: Boolean)
    begin
        if not IsIntegrationEnabled then
            IsIntegrationEnabled := CDSIntegrationMgt.IsIntegrationEnabled();
    end;

    [EventSubscriber(ObjectType::Page, Page::"My Notifications", 'OnInitializingNotificationWithDefaultState', '', false, false)]
    local procedure InsertDefaultStateOnInitializingNotificationWithDefaultState()
    var
        MyNotifications: Record "My Notifications";
    begin
        MyNotifications.InsertDefault(GetMultipleCompaniesNotificationId(), MultipleCompaniesNotificationNameTxt, MultipleCompaniesNotificationDescriptionTxt, true);
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeCheckGLSetupLCYCode(GLSetup: Record "General Ledger Setup"; CRMTransactioncurrency: Record "CRM Transactioncurrency"; var IsHandled: Boolean)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnGetCDSConnectionClientId(var ClientId: Text)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnGetCDSConnectionClientSecret(var ClientSecret: Text)
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnAfterIntegrationEnabled()
    begin
    end;

    [IntegrationEvent(false, false)]
    local procedure OnBeforeGetBusinessEventsSupported(var Supported: Boolean; var Handled: Boolean)
    begin
    end;

    [IntegrationEvent(true, false)]
    local procedure OnBeforeGetDefaultBusinessUnitName(var BusinessUnitName: Text[160]; var IsHandled: Boolean)
    begin
    end;

    [IntegrationEvent(true, false)]
    local procedure OnGetIntegrationUserEmail(var IntegrationUsernameEmailTxt: Text[100]; var IsHandled: Boolean)
    begin
    end;
}