Codeunit 104040 Upgrade Profiles V2, source in 29

Source29

src/Layers/W1/BaseApp/OtherCapabilities/Upgrade/UpgradeProfilesV2.Codeunit.al412 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.Upgrade;

using System.Environment;
using System.Environment.Configuration;
using System.IO;
using System.Reflection;
using System.Upgrade;

codeunit 104040 "Upgrade Profiles V2"
{
    // This codeunit runs upgrade from 14.x to 15.0 to make sure we upgrade profiles from the System scope to the Tenant scope.

    Subtype = Upgrade;

    // Upgrade triggers

    trigger OnUpgradePerCompany()
    var
        TenantProfile: Record "Tenant Profile";
        AppProfile: Record "Profile";
        UpgradeTag: Codeunit "Upgrade Tag";
        UpgradeTagDef: Codeunit "Upgrade Tag Definitions";
        HybridDeployment: Codeunit "Hybrid Deployment";
    begin
        if not HybridDeployment.VerifyCanStartUpgrade(CompanyName()) then
            exit;

        if UpgradeTag.HasUpgradeTag(UpgradeTagDef.GetUpdateProfileReferencesForCompanyTag(), CompanyName) then
            exit;

        Session.LogMessage('0000A31', StrSubstNo('Per-company upgrade of profile references started. System Profiles: %1. Tenant Profiles: %2.', AppProfile.Count(), TenantProfile.Count()), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategory);

        UpdateConfigSetup();

        Session.LogMessage('0000A32', StrSubstNo('Per-company upgrade of profile references finished. System Profiles: %1. Tenant Profiles: %2.', AppProfile.Count(), TenantProfile.Count()), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategory);

        UpgradeTag.SetUpgradeTag(UpgradeTagDef.GetUpdateProfileReferencesForCompanyTag());
    end;

    trigger OnUpgradePerDatabase()
    begin
        UpgradeProfileReferences();
    end;

    local procedure UpgradeProfileReferences()
    var
        TenantProfile: Record "Tenant Profile";
        AppProfile: Record "Profile";
        UpgradeTag: Codeunit "Upgrade Tag";
        UpgradeTagDef: Codeunit "Upgrade Tag Definitions";
    begin
        if UpgradeTag.HasUpgradeTag(UpgradeTagDef.GetUpdateProfileReferencesForDatabaseTag()) then
            exit;

        Session.LogMessage('0000A33', StrSubstNo('Per-database upgrade of profile references started. System Profiles: %1. Tenant Profiles: %2.', AppProfile.Count(), TenantProfile.Count()), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategory);

        UpdateApplicationAreaSetup();
        UpdateUserPersonalizations();

        Session.LogMessage('0000A34', StrSubstNo('Per-database upgrade of profile references finished. System Profiles: %1. Tenant Profiles: %2.', AppProfile.Count(), TenantProfile.Count()), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategory);

        UpgradeTag.SetUpgradeTag(UpgradeTagDef.GetUpdateProfileReferencesForDatabaseTag());
    end;

    // Table-specific upgrade functions

    local procedure UpdateConfigSetup()
    var
        TenantProfile: Record "Tenant Profile";
        ConfigSetup: Record "Config. Setup";
        SuccessfulModifications: Integer;
        FailedModifications: Integer;
    begin
        ConfigSetup.SetRange("Your Profile Scope", ConfigSetup."Your Profile Scope"::System);
        ConfigSetup.SetFilter("Your Profile Code", '<>%1', '');

        if ConfigSetup.FindSet(true) then
            repeat
                if FindTenantProfileFromAppProfile(ConfigSetup."Your Profile Code", TenantProfile) then begin
                    ConfigSetup."Your Profile App ID" := TenantProfile."App ID";
                    ConfigSetup."Your Profile Code" := TenantProfile."Profile ID";
                    ConfigSetup."Your Profile Scope" := ConfigSetup."Your Profile Scope"::Tenant;
                    if ConfigSetup.Modify() then
                        SuccessfulModifications += 1
                    else begin
                        SendFailedToUpdateProfileReferenceTag(ConfigSetup.TableName, ConfigSetup."Your Profile Code", TenantProfile);
                        FailedModifications += 1;
                    end;
                end else
                    FailedModifications += 1; // Telemetry already raised as part of FindTenantProfileFromAppProfile
            until ConfigSetup.Next() = 0;

        SendProfileReferenceUpdatedTag(SuccessfulModifications, FailedModifications, ConfigSetup.TableName);
    end;

    local procedure UpdateUserPersonalizations()
    var
        UserPersonalization: Record "User Personalization";
        TenantProfile: Record "Tenant Profile";
        SuccessfulModifications: Integer;
        FailedModifications: Integer;
    begin
        UserPersonalization.SetRange(Scope, UserPersonalization.Scope::System);
        UserPersonalization.SetFilter("Profile ID", '<>%1', '');

        if UserPersonalization.FindSet(true) then
            repeat
                if FindTenantProfileFromAppProfile(UserPersonalization."Profile ID", TenantProfile) then begin
                    UserPersonalization."Profile ID" := TenantProfile."Profile ID";
                    UserPersonalization."App ID" := TenantProfile."App ID";
                    UserPersonalization.Scope := UserPersonalization.Scope::Tenant;
                    if UserPersonalization.Modify() then
                        SuccessfulModifications += 1
                    else begin
                        SendFailedToUpdateProfileReferenceTag(UserPersonalization.TableName, UserPersonalization."Profile ID", TenantProfile);
                        FailedModifications += 1;
                    end;
                end else
                    FailedModifications += 1; // Telemetry already raised as part of FindTenantProfileFromAppProfile
            until UserPersonalization.Next() = 0;

        SendProfileReferenceUpdatedTag(SuccessfulModifications, FailedModifications, UserPersonalization.TableName);
    end;

    local procedure UpdateApplicationAreaSetup()
    var
        ApplicationAreaSetup: Record "Application Area Setup";
        TenantProfile: Record "Tenant Profile";
        SuccessfulModifications: Integer;
        FailedModifications: Integer;
    begin
        ApplicationAreaSetup.SetFilter("Profile ID", '<>%1', '');
        // No scope for the Application Area Setup, the logic for it is implemented in ShouldUpdateProfileIdForApplicationAreaSetup

        if ApplicationAreaSetup.FindSet(true) then
            repeat
                if ShouldUpdateProfileIdForApplicationAreaSetup(ApplicationAreaSetup."Profile ID") then
                    if FindTenantProfileFromAppProfile(ApplicationAreaSetup."Profile ID", TenantProfile) then begin
                        ApplicationAreaSetup."Profile ID" := TenantProfile."Profile ID";
                        if ApplicationAreaSetup.Modify() then
                            SuccessfulModifications += 1
                        else begin
                            SendFailedToUpdateProfileReferenceTag(ApplicationAreaSetup.TableName, ApplicationAreaSetup."Profile ID", TenantProfile);
                            FailedModifications += 1;
                        end;
                    end else
                        FailedModifications += 1; // Telemetry already raised as part of FindTenantProfileFromAppProfile
            until ApplicationAreaSetup.Next() = 0;

        SendProfileReferenceUpdatedTag(SuccessfulModifications, FailedModifications, ApplicationAreaSetup.TableName);
    end;

    // Profile handling functions

    local procedure FindTenantProfileFromAppProfile(SystemProfileId: Code[30]; var TenantProfile: Record "Tenant Profile"): Boolean
    begin
        Clear(TenantProfile);
        if MatchSystemProfileToBaseAppTenantProfile(SystemProfileId, TenantProfile) then
            // The system profile that we referenced is one of the ones we provided as part of demotool. 
            // Those are now AL profiles, and we can match them.
            exit(true);

        if MatchSystemProfileWithAlreadyCreatedTenantProfile(SystemProfileId, TenantProfile) then
            // There is a user-created profile that matches the App Profile.
            // It might be because the user has created it, or because some per-company upgrade ran before this and created it in the user space.
            exit(true);

        if CreateTenantProfileFromAppProfileId(SystemProfileId, TenantProfile) then
            // This means the profile has been created by some partner extension, and we haven't migrated it as part of some other profile upgrade.
            // So we just migrate it to the Tenant scope.
            exit(true);

        // We could neither match a profile nor create one. Raise a telemetry error, as we might have a broken reference, but do not fail upgrade.
        Session.LogMessage('0000A35', StrSubstNo('Could not handle a system profile reference. ProfileID: %1', SystemProfileId), Verbosity::Error, DataClassification::CustomerContent, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategory);

        exit(false);
    end;

    local procedure MatchSystemProfileToBaseAppTenantProfile(AppProfileId: Code[30]; var TenantProfile: Record "Tenant Profile"): Boolean
    var
        BaseAppGuid: Guid;
        NewProfileId: Code[30];
    begin
        // Accountant Portal is provided by an extension that is not installed at the moment. This will update the reference, and in case the 
        //  extension is installed later, the reference will be pointing to an existing profile. If the extension is never installed back, 
        //  this reference will be broken, but this is by design
        if AppProfileId = UpperCase(AccountantPortalTxt) then begin
            TenantProfile."App ID" := 'd612d720-63b9-4b26-b062-a0c09c4ed433';
            TenantProfile."Profile ID" := AccountantPortalLockedTxt;
            exit(true);
        end;

        case AppProfileId of
            UpperCase(OrderProcessorTxt):
                NewProfileId := OrderProcessorLockedTxt;
            UpperCase(BusinessManagerIDTxt):
                NewProfileId := BusinessManagerIDLockedTxt;
            UpperCase(SecurityAdministratorTxt):
                NewProfileId := SecurityAdministratorLockedTxt;
            UpperCase(AccountantTxt):
                NewProfileId := AccountantLockedTxt;
            UpperCase(SalesRlshpMgrIDTxt):
                NewProfileId := SalesRlshpMgrIDLockedTxt;
            UpperCase(O365SalesTxt):
                NewProfileId := O365SalesLockedTxt;
            UpperCase(ProjectManagerTxt):
                NewProfileId := ProjectManagerLockedTxt;
            UpperCase(TeamMemberTxt):
                NewProfileId := TeamMemberLockedTxt;
            UpperCase(InvoicingTxt):
                NewProfileId := InvoicingLockedTxt;
            UpperCase(AccountingManagerTxt):
                NewProfileId := AccountingManagerLockedTxt;
            UpperCase(AccountingServicesTxt):
                NewProfileId := AccountingServicesLockedTxt;
            UpperCase(APCoordinatorTxt):
                NewProfileId := APCoordinatorLockedTxt;
            UpperCase(ARAdministratorTxt):
                NewProfileId := ARAdministratorLockedTxt;
            UpperCase(BookkeeperTxt):
                NewProfileId := BookkeeperLockedTxt;
            UpperCase(DispatcherTxt):
                NewProfileId := DispatcherLockedTxt;
            UpperCase(ITManagerTxt):
                NewProfileId := ITManagerLockedTxt;
            UpperCase(MachineOperatorTxt):
                NewProfileId := MachineOperatorLockedTxt;
            UpperCase(OutboundTechnicianTxt):
                NewProfileId := OutboundTechnicianLockedTxt;
            UpperCase(PresidentTxt):
                NewProfileId := PresidentLockedTxt;
            UpperCase(PresidentSmallBusTxt):
                NewProfileId := PresidentSmallBusLockedTxt;
            UpperCase(ProductionPlannerTxt):
                NewProfileId := ProductionPlannerLockedTxt;
            UpperCase(PurchasingAgentTxt):
                NewProfileId := PurchasingAgentLockedTxt;
            UpperCase(RapidstartServicesTxt):
                NewProfileId := RapidstartServicesLockedTxt;
            UpperCase(ResourceManagerTxt):
                NewProfileId := ResourceManagerTxt;
            UpperCase(SalesManagerTxt):
                NewProfileId := SalesManagerLockedTxt;
            UpperCase(ShippingAndReceivingTxt):
                NewProfileId := ShippingAndReceivingLockedTxt;
            UpperCase(ShippingAndReceivingWMSTxt):
                NewProfileId := ShippingAndReceivingWMSLockedTxt;
            UpperCase(ShopSupervisorTxt):
                NewProfileId := ShopSupervisorLockedTxt;
            UpperCase(ShopSupervisorFoundTxt):
                NewProfileId := ShopSupervisorFoundLockedTxt;
            UpperCase(WHSWorkerWMSTxt):
                NewProfileId := WHSWorkerWMSLockedTxt;
        end;

        BaseAppGuid := '437DBF0E-84FF-417A-965D-ED2BB9650972';

        if NewProfileId = '' then begin
            Session.LogMessage('0000A36', 'Could not match a profile id.', Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategory);
            exit(false);
        end;

        if not TenantProfile.Get(BaseAppGuid, NewProfileId) then begin
            Session.LogMessage('0000A37', StrSubstNo('One hardcoded tenant profile is not present: %1.', NewProfileId), Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategory);
            exit(false);
        end;

        exit(true);
    end;

    local procedure MatchSystemProfileWithAlreadyCreatedTenantProfile(AppProfileId: Code[30]; var TenantProfile: Record "Tenant Profile") GetSuccessful: Boolean
    var
        EmptyGuid: Guid;
    begin
        GetSuccessful := TenantProfile.Get(EmptyGuid, AppProfileId);
        if GetSuccessful then
            Session.LogMessage('0000A38', 'Found a tenant profile with the same Id of an app profile.', Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategory);

        exit(GetSuccessful);
    end;

    local procedure CreateTenantProfileFromAppProfileId(AppProfileId: Code[30]; var TenantProfile: Record "Tenant Profile") InsertSuccessful: Boolean
    var
        SystemProfile: Record "Profile";
        EmptyGuid: Guid;
    begin
        if not SystemProfile.Get(AppProfileId) then
            exit(false);

        TenantProfile."App ID" := EmptyGuid;
        TenantProfile."Profile ID" := SystemProfile."Profile ID";
        TenantProfile.Description := SystemProfile.Description;
        TenantProfile."Role Center ID" := SystemProfile."Role Center ID";
        TenantProfile."Default Role Center" := SystemProfile."Default Role Center";
        TenantProfile."Disable Personalization" := SystemProfile."Disable Personalization";
        InsertSuccessful := TenantProfile.Insert();

        Session.LogMessage('0000A39', StrSubstNo('Attempted to insert a new tenant profile copied from a system profile. Result: %1.', Format(InsertSuccessful)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategory);

        exit(InsertSuccessful);
    end;

    // Miscellaneous

    local procedure SendProfileReferenceUpdatedTag(SuccessfulModifications: Integer; FailedModifications: Integer; TableName: Text)
    begin
        Session.LogMessage('0000A3A', StrSubstNo('Attempted to modify %1 references in table "%2". Successful: %3; Failed: %4.', SuccessfulModifications + FailedModifications, TableName, SuccessfulModifications, FailedModifications), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategory);
    end;

    local procedure SendFailedToUpdateProfileReferenceTag(SourceTableName: Text; PreviousProfileId: Code[30]; TenantProfile: Record "Tenant Profile")
    begin
        Session.LogMessage('0000A3O', StrSubstNo('Failed to modify Profile reference in table %1. Previous profile ID: %2. New profile key: %3, %4.',
                SourceTableName, PreviousProfileId, TenantProfile."Profile ID", TenantProfile."App ID"), Verbosity::Error, DataClassification::CustomerContent, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategory);
    end;

    local procedure ShouldUpdateProfileIdForApplicationAreaSetup(ProfileId: Code[30]): Boolean
    var
        AllProfile: Record "All Profile";
        SystemProfile: Record "Profile";
    begin
        if SystemProfile.Get(ProfileId) then
            exit(true); // Partner-created profile: let's update references and move it to the tenant scope.


        // We are interested in updating only the references to something that is no longer there, which is our old BaseApp profiles.
        // If there is another profile with the same ID (IsEmpty returns false) there it means either one of:
        //  1 - The Application Area Setup was referencing a Tenant Profile: do not update it.
        //  2 - The Application Area Setup was referencing both a Tenant Profile and a System Profile (the same Application Area Setup
        //      would apply for both). In this case we do not update, because we do not want to break tenant profiles. This might mean 
        //      that the Application Area Setup is no longer applied to the System Profile.

        // Update only if AllProfile is empty
        AllProfile.SetRange("Profile ID", ProfileId);
        exit(AllProfile.IsEmpty());
    end;

    var
#pragma warning disable AA0074
        TelemetryCategory: Label 'AL Upg Profiles', Locked = true;
#pragma warning restore AA0074

    // Profile IDs that are LOCKED (must match profiles in the Tenant scope added by AL objects)
    var
        AccountantLockedTxt: Label 'Accountant', Locked = true;
        OrderProcessorLockedTxt: Label 'Order Processor', Locked = true;
        SecurityAdministratorLockedTxt: Label 'Security Administrator', Locked = true;
        BusinessManagerIDLockedTxt: Label 'Business Manager', Locked = true;
        SalesRlshpMgrIDLockedTxt: Label 'Sales and Relationship Manager', Locked = true;
        O365SalesLockedTxt: Label 'O365 Sales', Locked = true;
        ProjectManagerLockedTxt: Label 'PROJECT MANAGER', Locked = true;
        TeamMemberLockedTxt: Label 'TEAM MEMBER', Locked = true;
        InvoicingLockedTxt: Label 'Invoicing', Locked = true;
        AccountantPortalLockedTxt: Label 'Accountant Portal', Locked = true;
        AccountingManagerLockedTxt: Label 'Accounting Manager', Locked = true;
        AccountingServicesLockedTxt: Label 'Accounting Services', Locked = true;
        APCoordinatorLockedTxt: Label 'AP Coordinator', Locked = true;
        ARAdministratorLockedTxt: Label 'AR Administrator', Locked = true;
        BookkeeperLockedTxt: Label 'Bookkeeper', Locked = true;
        DispatcherLockedTxt: Label 'Dispatcher', Locked = true;
        ITManagerLockedTxt: Label 'IT Manager', Locked = true;
        MachineOperatorLockedTxt: Label 'Machine Operator', Locked = true;
        OutboundTechnicianLockedTxt: Label 'Outbound Technician', Locked = true;
        PresidentLockedTxt: Label 'President', Locked = true;
        PresidentSmallBusLockedTxt: Label 'President - Small Business', Locked = true;
        ProductionPlannerLockedTxt: Label 'Production Planner', Locked = true;
        PurchasingAgentLockedTxt: Label 'Purchasing Agent', Locked = true;
        RapidstartServicesLockedTxt: Label 'RapidStart Services', Locked = true;
        SalesManagerLockedTxt: Label 'Sales Manager', Locked = true;
        ShippingAndReceivingLockedTxt: Label 'Shipping and Receiving', Locked = true;
        ShippingAndReceivingWMSLockedTxt: Label 'Shipping and Receiving - WMS', Locked = true;
        ShopSupervisorLockedTxt: Label 'Shop Supervisor', Locked = true;
        ShopSupervisorFoundLockedTxt: Label 'Shop Supervisor - Foundation', Locked = true;
        WHSWorkerWMSLockedTxt: Label 'Warehouse Worker - WMS', Locked = true;

    // Same IDs, but not locked (translations must match the app profiles inserted until 14.x)
    var
        AccountantTxt: Label 'Accountant';
        OrderProcessorTxt: Label 'Order Processor';
        SecurityAdministratorTxt: Label 'Security Administrator';
        BusinessManagerIDTxt: Label 'Business Manager';
        SalesRlshpMgrIDTxt: Label 'Sales and Relationship Manager';
        O365SalesTxt: Label 'O365 Sales';
        ProjectManagerTxt: Label 'PROJECT MANAGER';
        TeamMemberTxt: Label 'TEAM MEMBER';
        InvoicingTxt: Label 'Invoicing';
        AccountantPortalTxt: Label 'Accountant Portal';
        AccountingManagerTxt: Label 'Accounting Manager';
        AccountingServicesTxt: Label 'Accounting Services';
        APCoordinatorTxt: Label 'AP Coordinator';
        ARAdministratorTxt: Label 'AR Administrator';
        BookkeeperTxt: Label 'Bookkeeper';
        DispatcherTxt: Label 'Dispatcher';
        ITManagerTxt: Label 'IT Manager';
        MachineOperatorTxt: Label 'Machine Operator';
        OutboundTechnicianTxt: Label 'Outbound Technician';
        PresidentTxt: Label 'President';
        PresidentSmallBusTxt: Label 'President - Small Business';
        ProductionPlannerTxt: Label 'Production Planner';
        PurchasingAgentTxt: Label 'Purchasing Agent';
        RapidstartServicesTxt: Label 'RapidStart Services';
        ResourceManagerTxt: Label 'Resource Manager';
        SalesManagerTxt: Label 'Sales Manager';
        ShippingAndReceivingTxt: Label 'Shipping and Receiving';
        ShippingAndReceivingWMSTxt: Label 'Shipping and Receiving - WMS';
        ShopSupervisorTxt: Label 'Shop Supervisor';
        ShopSupervisorFoundTxt: Label 'Shop Supervisor - Foundation';
        WHSWorkerWMSTxt: Label 'Warehouse Worker - WMS';
}