Codeunit 9033 Invite External Accountant
- App
- Base Application
- Namespace
- Microsoft.AccountantPortal
- Versions
- 17-28
Procedures, 11Events, 1Obsolete, 1
Versions171819202122232425262728
Source29
Source in 29
src/Layers/W1/BaseApp/OtherCapabilities/AccountantPortal/InviteExternalAccountant.Codeunit.al403 lines, Copyright (c) Microsoft Corporation. MIT
namespace Microsoft.AccountantPortal;
using System;
using System.Azure.Identity;
using System.Environment.Configuration;
using System.Integration;
using System.Text;
using System.Utilities;
codeunit 9033 "Invite External Accountant"
{
trigger OnRun()
begin
end;
var
UrlHelper: Codeunit "Url Helper";
ErrorAcquiringTokenErr: Label 'Failed to acquire an access token. This is necessary to invite the external accountant. Contact your administrator.', Locked = true;
InviteReedemUrlTxt: Label 'inviteRedeemUrl', Locked = true;
InvitedUserIdTxt: Label 'invitedUser', Locked = true;
IdTxt: Label 'id', Locked = true;
ConsumedUnitsTxt: Label 'consumedUnits', Locked = true;
ErrorTxt: Label 'error', Locked = true;
MessageTxt: Label 'message', Locked = true;
ValueTxt: Label 'value', Locked = true;
SkuIdTxt: Label 'skuId', Locked = true;
PrepaidUnitsTxt: Label 'prepaidUnits', Locked = true;
EnabledUnitsTxt: Label 'enabled', Locked = true;
GlobalAdministratorRoleTemplateIdTxt: Label '62e90394-69f5-4237-9190-012177145e10', Locked = true;
UserAdministratorRoleTemplateIdTxt: Label 'fe930be7-5e62-47db-91af-98c3a49a38b1', Locked = true;
ExternalAccountantLicenseSkuIdTxt: Label '9a1e33ed-9697-43f3-b84c-1b0959dbb1d4', Locked = true;
InviteExternalAccountantTelemetryCategoryTxt: Label 'AL Dynamics 365 Invite External Accountant', Locked = true;
InviteExternalAccountantTelemetryStartTxt: Label 'Invite External Accountant process started.', Locked = true;
InviteExternalAccountantTelemetryEndTxt: Label 'Invite External Accountant process ended with the following result: %1: License is %2.', Locked = true;
InviteExternalAccountantTelemetryLicenseFailTxt: Label 'Invite External Accountant wizard failed to start due to there being no external accountant license available.', Locked = true;
InviteExternalAccountantTelemetryAADPermissionFailTxt: Label 'Invite External Accountant wizard failed to start due to the user not having the necessary Microsoft Entra permissions.', Locked = true;
InviteExternalAccountantTelemetryUserTablePermissionFailTxt: Label 'Invite External Accountant wizard failed to start due to the session not being admin or the user being Super in all companies.', Locked = true;
InviteExternalAccountantTelemetryCreateNewUserSuccessTxt: Label 'Invite External Accountant wizard successfully created a new user.', Locked = true;
InviteExternalAccountantTelemetryCreateNewUserFailedTxt: Label 'Invite External Accountant wizard was unable to create a new user.', Locked = true;
InviteExternalAccountantWizardFailedTxt: Label 'Invite External Accountant wizard has failed on step %1, ErrorMessage %2.', Locked = true;
InvokeWebRequestFailedTxt: Label 'Invoking web request has failed. Status %1, Message %2', Locked = true;
InvokeWebRequestFailedDetailedTxt: Label 'Invoking web request has failed. Status %1, Message %2, Response Details %3', Locked = true;
InsufficientDataReturnedFromInvitationsApiTxt: Label 'Insufficient information was returned when inviting the user. Please contact your administrator.';
WidsClaimNameTok: Label 'WIDS', Locked = true;
ExternalAccountantLicenseAvailabilityErr: Label 'Failed to determine if an External Accountant license is available. Please try again later.';
[Scope('OnPrem')]
procedure InvokeInvitationsRequest(DisplayName: Text; EmailAddress: Text; WebClientUrl: Text; var InvitedUserId: Guid; var InviteReedemUrl: Text; var ErrorMessage: Text): Boolean
var
JSONManagement: Codeunit "JSON Management";
JsonObject: DotNet JObject;
InviteUserIdJsonObject: DotNet JObject;
InviteUserIdObjectValue: Text;
ResponseContent: Text;
Body: Text;
FoundInviteRedeemUrlValue: Boolean;
FoundInvitedUserObjectValue: Boolean;
FoundInviteUserIdValue: Boolean;
begin
Body := '{';
Body := Body + '"invitedUserDisplayName" : "' + DisplayName + '",';
Body := Body + '"invitedUserEmailAddress" : "' + EmailAddress + '",';
Body := Body + '"inviteRedirectUrl" : "' + WebClientUrl + '",';
Body := Body + '"sendInvitationMessage" : "false"';
Body := Body + '}';
if InvokeRequestWithGraphAccessToken(GetGraphInvitationsUrl(), 'POST', Body, ResponseContent) then begin
JSONManagement.InitializeObject(ResponseContent);
JSONManagement.GetJSONObject(JsonObject);
FoundInviteRedeemUrlValue :=
JSONManagement.GetStringPropertyValueFromJObjectByName(JsonObject, InviteReedemUrlTxt, InviteReedemUrl);
FoundInvitedUserObjectValue :=
JSONManagement.GetStringPropertyValueFromJObjectByName(JsonObject, InvitedUserIdTxt, InviteUserIdObjectValue);
JSONManagement.InitializeObject(InviteUserIdObjectValue);
JSONManagement.GetJSONObject(InviteUserIdJsonObject);
FoundInviteUserIdValue := JSONManagement.GetGuidPropertyValueFromJObjectByName(InviteUserIdJsonObject, IdTxt, InvitedUserId);
if FoundInviteRedeemUrlValue and FoundInvitedUserObjectValue and FoundInviteUserIdValue then
exit(true);
ErrorMessage := InsufficientDataReturnedFromInvitationsApiTxt;
exit(false);
end;
ErrorMessage := GetMessageFromErrorJSON(ResponseContent);
exit(false);
end;
[Scope('OnPrem')]
procedure InvokeUserProfileUpdateRequest(var GuestGraphUser: DotNet UserInfo; CountryLetterCode: Text; var ErrorMessage: Text): Boolean
var
Body: Text;
ResponseContent: Text;
GraphUserUrl: Text;
begin
GraphUserUrl := GetGraphUserUrl() + '/' + GuestGraphUser.ObjectId;
Body := '{"usageLocation" : "' + CountryLetterCode + '"}';
// Set usage location on guest user to current tenant's country letter code.
if InvokeRequestWithGraphAccessToken(GraphUserUrl, 'PATCH', Body, ResponseContent) then
exit(true);
ErrorMessage := GetMessageFromErrorJSON(ResponseContent);
exit(false);
end;
[Scope('OnPrem')]
procedure InvokeUserAssignLicenseRequest(var GraphUser: DotNet UserInfo; TargetLicense: Text; var ErrorMessage: Text): Boolean
var
Body: Text;
ResponseContent: Text;
Url: Text;
begin
Url := GetGraphUserUrl() + '/' + GraphUser.ObjectId + '/assignLicense';
Body := '{';
Body := Body + '"addLicenses": [';
Body := Body + '{';
Body := Body + '"disabledPlans": [],';
Body := Body + '"skuId": "' + TargetLicense + '"';
Body := Body + '}],';
Body := Body + '"removeLicenses": []';
Body := Body + '}';
if InvokeRequestWithGraphAccessToken(Url, 'POST', Body, ResponseContent) then
exit(true);
ErrorMessage := GetMessageFromErrorJSON(ResponseContent);
exit(false);
end;
[Scope('OnPrem')]
procedure InvokeIsExternalAccountantLicenseAvailable(var ErrorMessage: Text; var TargetLicense: Text): Boolean
var
JSONManagement: Codeunit "JSON Management";
JsonObject: DotNet JObject;
JsonArray: DotNet JArray;
PrepaidUnitsJsonObject: DotNet JObject;
NumberSkus: Integer;
ResponseContent: Text;
SkuIdValue: Text;
ConsumedUnitsValue: Decimal;
PrepaidUnitsValue: Text;
EnabledUnitsValue: Decimal;
begin
if InvokeRequestWithGraphAccessToken(GetGraphSubscribedSkusUrl(), 'GET', '', ResponseContent) then begin
if not JSONManagement.TryParseJObjectFromString(JsonObject, ResponseContent) then
Error(ExternalAccountantLicenseAvailabilityErr);
if not JSONManagement.GetArrayPropertyValueFromJObjectByName(JsonObject, ValueTxt, JsonArray) then
Error(ExternalAccountantLicenseAvailabilityErr);
JSONManagement.InitializeCollectionFromJArray(JsonArray);
NumberSkus := JSONManagement.GetCollectionCount();
while NumberSkus > 0 do begin
NumberSkus := NumberSkus - 1;
if JSONManagement.GetJObjectFromCollectionByIndex(JsonObject, NumberSkus) then begin
if not JSONManagement.GetStringPropertyValueFromJObjectByName(JsonObject, SkuIdTxt, SkuIdValue) then
Error(ExternalAccountantLicenseAvailabilityErr);
if not JSONManagement.GetDecimalPropertyValueFromJObjectByName(JsonObject, ConsumedUnitsTxt, ConsumedUnitsValue) then
Error(ExternalAccountantLicenseAvailabilityErr);
// Check to see if there is an external accountant license available.
if (SkuIdValue = ExternalAccountantLicenseSkuIdTxt) then begin
TargetLicense := SkuIdValue;
if not JSONManagement.GetStringPropertyValueFromJObjectByName(JsonObject, PrepaidUnitsTxt, PrepaidUnitsValue) then
Error(ExternalAccountantLicenseAvailabilityErr);
if not JSONManagement.TryParseJObjectFromString(PrepaidUnitsJsonObject, PrepaidUnitsValue) then
Error(ExternalAccountantLicenseAvailabilityErr);
if not JSONManagement.GetDecimalPropertyValueFromJObjectByName(PrepaidUnitsJsonObject, EnabledUnitsTxt, EnabledUnitsValue) then
Error(ExternalAccountantLicenseAvailabilityErr);
if ConsumedUnitsValue < EnabledUnitsValue then
exit(true);
exit(false);
end;
end;
end;
end;
ErrorMessage := GetMessageFromErrorJSON(ResponseContent);
exit(false);
end;
[Scope('OnPrem')]
procedure InvokeIsUserAdministrator(): Boolean
var
ClaimValue: Text;
begin
ClaimValue := GetCurrentUserTokenClaim(WidsClaimNameTok);
if ClaimValue = '' then
exit(false);
if StrPos(UpperCase(ClaimValue), UpperCase(GlobalAdministratorRoleTemplateIdTxt)) > 0 then
exit(true);
if StrPos(UpperCase(ClaimValue), UpperCase(UserAdministratorRoleTemplateIdTxt)) > 0 then
exit(true);
exit(false);
end;
local procedure GetCurrentUserTokenClaim(ClaimName: Text): Text
var
UserAccountHelper: DotNet NavUserAccountHelper;
begin
exit(UserAccountHelper.GetCurrentUserTokenClaim(ClaimName));
end;
[Scope('OnPrem')]
procedure CreateNewUser(InvitedUserId: Guid)
var
AzureADUserManagement: Codeunit "Azure AD User Management";
AzureADGraph: codeunit "Azure AD Graph";
UserAssignedPlans: DotNet GenericList1;
GuestGraphUser: DotNet UserInfo;
NumberOfAssignedPlan: Integer;
"Count": Integer;
begin
repeat
Sleep(2000);
Count := Count + 1;
if AzureADGraph.TryGetUserByObjectId(InvitedUserId, GuestGraphUser) then begin
AzureADGraph.GetUserAssignedPlans(GuestGraphUser, UserAssignedPlans);
if (not IsNull(UserAssignedPlans)) then
NumberOfAssignedPlan := UserAssignedPlans.Count();
end;
until (NumberOfAssignedPlan > 1) or (Count = 10);
if NumberOfAssignedPlan > 1 then begin
OnInvitationCreateNewUser(true);
AzureADUserManagement.CreateNewUserFromGraphUser(GuestGraphUser);
end else
OnInvitationCreateNewUser(false);
end;
[Scope('OnPrem')]
procedure IsLicenseAlreadyAssigned(GuestGraphUser: DotNet UserInfo): Boolean
var
AzureADGraph: Codeunit "Azure AD Graph";
UserAssignedPlans: DotNet GenericList1;
begin
AzureADGraph.GetUserAssignedPlans(GuestGraphUser, UserAssignedPlans);
if IsNull(UserAssignedPlans) then
exit(false);
exit(UserAssignedPlans.Count() > 0);
end;
[Scope('OnPrem')]
procedure TryGetGuestGraphUser(InvitedUserId: Guid; var GuestGraphUser: DotNet UserInfo): Boolean
var
AzureADGraph: codeunit "Azure AD Graph";
FoundUser: Boolean;
"Count": Integer;
begin
repeat
Sleep(2000);
Count := Count + 1;
FoundUser := AzureADGraph.TryGetUserByObjectId(InvitedUserId, GuestGraphUser);
until FoundUser or (Count = 10);
exit(FoundUser);
end;
procedure UpdateAssistedSetup()
var
GuidedExperience: Codeunit "Guided Experience";
begin
GuidedExperience.CompleteAssistedSetup(ObjectType::Page, PAGE::"Invite External Accountant");
end;
local procedure InvokeRequestWithGraphAccessToken(Url: Text; Verb: Text; Body: Text; var ResponseContent: Text): Boolean
begin
exit(InvokeRequest(Url, Verb, Body, UrlHelper.GetGraphUrl(), ResponseContent));
end;
local procedure InvokeRequest(Url: Text; Verb: Text; Body: Text; AuthResourceUrl: Text; var ResponseContent: Text): Boolean
var
AzureADMgt: Codeunit "Azure AD Mgt.";
AzureADTenant: Codeunit "Azure AD Tenant";
HttpWebRequestMgt: Codeunit "Http Web Request Mgt.";
HttpStatusCode: DotNet HttpStatusCode;
ResponseHeaders: DotNet NameValueCollection;
ResponseErrorMessage: Text;
ResponseErrorDetails: Text;
AccessToken: Text;
begin
AccessToken := AzureADMgt.GetGuestAccessToken(AuthResourceUrl, AzureADTenant.GetAadTenantId());
if AccessToken = '' then
Error(ErrorAcquiringTokenErr);
HttpWebRequestMgt.Initialize(Url);
HttpWebRequestMgt.DisableUI();
HttpWebRequestMgt.SetReturnType('application/json');
HttpWebRequestMgt.SetContentType('application/json');
HttpWebRequestMgt.SetMethod(Verb);
HttpWebRequestMgt.AddHeader('Authorization', 'Bearer ' + AccessToken);
if Verb <> 'GET' then
HttpWebRequestMgt.AddBodyAsText(Body);
if HttpWebRequestMgt.SendRequestAndReadTextResponse(ResponseContent, ResponseErrorMessage, ResponseErrorDetails, HttpStatusCode, ResponseHeaders) then
exit(true)
else begin
Session.LogMessage('0000B3O', StrSubstNo(InvokeWebRequestFailedTxt, HttpStatusCode, ResponseErrorMessage), Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', InviteExternalAccountantTelemetryCategoryTxt);
Session.LogMessage('0000B3P', StrSubstNo(InvokeWebRequestFailedDetailedTxt, HttpStatusCode, ResponseErrorMessage, ResponseErrorDetails), Verbosity::Error, DataClassification::CustomerContent, TelemetryScope::ExtensionPublisher, 'Category', InviteExternalAccountantTelemetryCategoryTxt);
exit(false);
end;
end;
local procedure GetMessageFromErrorJSON(ResponseContent: Text): Text
var
JSONManagement: Codeunit "JSON Management";
JsonObject: DotNet JObject;
ErrorObjectValue: Text;
MessageValue: Text;
begin
JSONManagement.InitializeObject(ResponseContent);
JSONManagement.GetJSONObject(JsonObject);
if JSONManagement.GetStringPropertyValueFromJObjectByName(JsonObject, ErrorTxt, ErrorObjectValue) then begin
JSONManagement.InitializeObject(ErrorObjectValue);
JSONManagement.GetJSONObject(JsonObject);
JSONManagement.GetStringPropertyValueFromJObjectByName(JsonObject, MessageTxt, MessageValue);
end;
exit(MessageValue);
end;
local procedure GetGraphInvitationsUrl(): Text
begin
exit(UrlHelper.GetGraphUrl() + 'v1.0/invitations');
end;
local procedure GetGraphUserUrl(): Text
begin
exit(UrlHelper.GetGraphUrl() + 'v1.0/users');
end;
local procedure GetGraphSubscribedSkusUrl(): Text
begin
exit(UrlHelper.GetGraphUrl() + 'v1.0/subscribedSkus');
end;
procedure SendTelemetryForWizardFailure(stepFailed: Text; ErrorMessage: Text)
begin
Session.LogMessage('0000B97', StrSubstNo(InviteExternalAccountantWizardFailedTxt, stepFailed, ErrorMessage), Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', InviteExternalAccountantTelemetryCategoryTxt);
end;
[EventSubscriber(ObjectType::Page, Page::"Invite External Accountant", 'OnInvitationStart', '', false, false)]
local procedure SendTelemetryForInvitationStart()
begin
Session.LogMessage('0000178', InviteExternalAccountantTelemetryStartTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', InviteExternalAccountantTelemetryCategoryTxt);
end;
[EventSubscriber(ObjectType::Page, Page::"Invite External Accountant", 'OnInvitationNoExternalAccountantLicenseFail', '', false, false)]
local procedure SendTelemetryForInvitationNoExternalAccountantLicenseFail()
begin
Session.LogMessage('0000179', InviteExternalAccountantTelemetryLicenseFailTxt, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', InviteExternalAccountantTelemetryCategoryTxt);
end;
[EventSubscriber(ObjectType::Page, Page::"Invite External Accountant", 'OnInvitationNoAADPermissionsFail', '', false, false)]
local procedure SendTelemetryForInvitationNoAADPermissionsFail()
begin
Session.LogMessage('000017A', InviteExternalAccountantTelemetryAADPermissionFailTxt, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', InviteExternalAccountantTelemetryCategoryTxt);
end;
[EventSubscriber(ObjectType::Page, Page::"Invite External Accountant", 'OnInvitationNoUserTablePermissionsFail', '', false, false)]
local procedure SendTelemetryForInvitationNoUserTableWritePermissionsFail()
begin
Session.LogMessage('00001DK', InviteExternalAccountantTelemetryUserTablePermissionFailTxt, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', InviteExternalAccountantTelemetryCategoryTxt);
end;
[EventSubscriber(ObjectType::Page, Page::"Invite External Accountant", 'OnInvitationEnd', '', false, false)]
local procedure SendTelemetryForInvitationEnd(WasInvitationSuccessful: Boolean; Result: Text; TargetLicense: Text)
begin
if WasInvitationSuccessful then
Session.LogMessage('000017B', StrSubstNo(InviteExternalAccountantTelemetryEndTxt, Result, TargetLicense), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', InviteExternalAccountantTelemetryCategoryTxt)
else
Session.LogMessage('000017C', StrSubstNo(InviteExternalAccountantTelemetryEndTxt, Result, TargetLicense), Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', InviteExternalAccountantTelemetryCategoryTxt);
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Invite External Accountant", 'OnInvitationCreateNewUser', '', false, false)]
local procedure SendTelemetryForInvitationCreateNewUser(UserCreated: Boolean)
begin
if UserCreated then
Session.LogMessage('00001DL', InviteExternalAccountantTelemetryCreateNewUserSuccessTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', InviteExternalAccountantTelemetryCategoryTxt)
else
Session.LogMessage('00001DM', InviteExternalAccountantTelemetryCreateNewUserFailedTxt, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', InviteExternalAccountantTelemetryCategoryTxt);
end;
[IntegrationEvent(false, false)]
local procedure OnInvitationCreateNewUser(UserCreated: Boolean)
begin
// This event is called when the invitation process tries to create a user.
end;
}