Codeunit 1410 Doc. Exch. Service Mgt., source in 29
Source29
src/Layers/W1/BaseApp/eServices/EDocument/DocExchServiceMgt.Codeunit.al1748 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.EServices.EDocument;
using Microsoft.Purchases.Document;
using Microsoft.Sales.History;
using Microsoft.Utilities;
using System;
using System.Azure.KeyVault;
using System.Environment;
using System.Integration;
using System.IO;
using System.Security.Authentication;
using System.Telemetry;
using System.Utilities;
using System.Xml;
codeunit 1410 "Doc. Exch. Service Mgt."
{
Permissions = TableData "Sales Invoice Header" = rm,
TableData "Sales Cr.Memo Header" = rm;
trigger OnRun()
begin
end;
var
TempBlobResponse: Codeunit "Temp Blob";
TempBlobTrace: Codeunit "Temp Blob";
Trace: Codeunit Trace;
HttpWebRequestMgt: Codeunit "Http Web Request Mgt.";
DocExchLinks: Codeunit "Doc. Exch. Links";
XMLDOMMgt: Codeunit "XML DOM Management";
EnvironmentInfo: Codeunit "Environment Information";
GLBResponseInStream: InStream;
GLBHttpStatusCode: DotNet HttpStatusCode;
GLBResponseHeaders: DotNet NameValueCollection;
GLBLastUsedGUID: Text;
GLBTraceLogEnabled: Boolean;
NotConfiguredQst: Label 'The connection to the document exchange service is not configured. Do you want to open the %1 page to set it up?', Comment = '%1 - page caption';
NotConfiguredErr: Label 'You must configure the connection to the document exchange service on the %1 page.', Comment = '%1 - page caption';
ConnectionSuccessMsg: Label 'The connection test was successful. The settings are valid.';
DocSendSuccessMsg: Label 'The document was successfully sent to the document exchange service for processing.', Comment = '%1 is the actual document no.';
DocUploadSuccessMsg: Label 'The document was successfully uploaded to the document exchange service for processing.', Comment = '%1 is the actual document no.';
DocDispatchSuccessMsg: Label 'The document was successfully sent for dispatching.', Comment = '%1 is the actual document no.';
DocDispatchFailedMsg: Label 'The document was not successfully dispatched. ', Comment = '%1 is the actual document no.';
DocStatusOKMsg: Label 'The current status of the electronic document is %1.', Comment = '%1 is the returned value.';
NotSetUpTxt: Label 'The document exchange service is not set up.';
NotEnabledTxt: Label 'The document exchange service is not enabled.';
RenewTokenTxt: Label 'Renew token';
CheckConnectionTxt: Label 'Check connection.';
SendDocTxt: Label 'Send document.';
DispatchDocTxt: Label 'Dispatch document.';
GetDocStatusTxt: Label 'Check document status.';
GetDocsTxt: Label 'Get received documents.';
LoggingConstTxt: Label 'Document exchange service.';
GetDocErrorTxt: Label 'Check document dispatch errors.';
MarkBusinessProcessedTxt: Label 'Mark as Business Processed.';
DocIdImportedTxt: Label 'The document ID %1 is imported into incoming documents.', Comment = '%1 is the actual doc id.';
FileInvalidTxt: Label 'The document ID %1 is not a valid XML format. ', Comment = '%1 is the actual doc id';
UnSupportedTableTypeErr: Label 'The %1 table is not supported.', Comment = '%1 is the table.';
InvalidHeaderResponseMsg: Label 'The document exchange service did not return a document identifier.';
CannotResendErr: Label 'You cannot send this electronic document because it is already delivered or in progress.';
MalformedGuidErr: Label 'The document exchange service did not return a valid document identifier.';
DocExchServiceDocumentSuccessfullySentTxt: Label 'The user successfully sent a document via the exchange service.', Locked = true;
DocExchServiceDocumentSuccessfullyReceivedTxt: Label 'The user successfully received a document via the exchange service.', Locked = true;
TelemetryCategoryTok: Label 'AL Document Exchange Service', Locked = true;
MethodGetTxt: Label 'GET', Locked = true;
MethodPostTxt: Label 'POST', Locked = true;
MethodPutTxt: Label 'PUT', Locked = true;
TextXmlTxt: Label 'text/xml', Locked = true;
ApplicationJsonTxt: Label 'application/json', Locked = true;
ApplicationFormTxt: Label 'application/x-www-form-urlencoded', Locked = true;
EncodingUtf8Txt: Label 'utf-8', Locked = true;
AcceptEncodingHeaderNameTxt: Label 'Accept-Encoding', Locked = true;
AuthorizationHeaderNameTxt: Label 'Authorization', Locked = true;
AuthorizationHeaderValueTxt: Label 'Bearer %1', Locked = true;
AuthorizationCodeRequestUrlTxt: Label '%1?client_id=%2&redirect_uri=%3&response_type=code&scope=%4&state=%5', Locked = true;
AuthorizationCodeRequestBodyTxt: label 'grant_type=authorization_code&client_id=%1&client_secret=%2&code=%3&redirect_uri=%4', Locked = true;
RefreshTokenRequestBodyTxt: Label 'grant_type=refresh_token&client_id=%1&client_secret=%2&refresh_token=%3&scope=%4', Locked = true;
ClientIdAKVSecretNameProdTxt: Label 'DocExchClientIdProd', Locked = true;
ClientSecretAKVSecretNameProdTxt: Label 'DocExchClientSecretProd', Locked = true;
ClientIdAKVSecretNameSandboxTxt: Label 'DocExchClientIdTest', Locked = true;
ClientSecretAKVSecretNameSandboxTxt: Label 'DocExchClientSecretTest', Locked = true;
AuthCodeScopeTxt: Label 'openid offline', Locked = true;
AccessTokenParamNameTxt: Label 'access_token', Locked = true;
RefreshTokenParamNameTxt: Label 'refresh_token', Locked = true;
IdTokenParamNameTxt: Label 'id_token', Locked = true;
TokenTypeParamNameTxt: Label 'token_type', Locked = true;
ExpiresInParamNameTxt: Label 'expires_in', Locked = true;
ClientIdParamNameTxt: Label 'client_id', Locked = true;
AudienceParamNameTxt: Label 'aud', Locked = true;
IssuerParamNameTxt: Label 'iss', Locked = true;
TokenTypeBearerTxt: Label 'Bearer', Locked = true;
AppStorePathTxt: Label '#/apps/Tradeshift.AppStore/apps/', Locked = true;
DefaultSignUpUrlProdTxt: Label 'https://go.tradeshift.com/register', Locked = true;
DefaultSignInUrlProdTxt: Label 'https://go.tradeshift.com/login', Locked = true;
DefaultServiceUrlProdTxt: Label 'https://api.tradeshift.com/tradeshift/rest/external', Locked = true;
DefaultAuthUrlProdTxt: Label 'https://api.tradeshift.com/tradeshift/auth/login', Locked = true;
DefaultTokenUrlProdTxt: Label 'https://api.tradeshift.com/tradeshift/auth/token', Locked = true;
DefaultSignUpUrlSandboxTxt: Label 'https://sandbox.tradeshift.com/register', Locked = true;
DefaultSignInUrlSandboxTxt: Label 'https://sandbox.tradeshift.com/login', Locked = true;
DefaultServiceUrlSandboxTxt: Label 'https://api-sandbox.tradeshift.com/tradeshift/rest/external', Locked = true;
DefaultAuthUrlSandboxTxt: Label 'https://api-sandbox.tradeshift.com/tradeshift/auth/login', Locked = true;
DefaultTokenUrlSandboxTxt: Label 'https://api-sandbox.tradeshift.com/tradeshift/auth/token', Locked = true;
SandboxTxt: Label 'sandbox', Locked = true;
DefaultVersionTxt: Label '/v1.0', Locked = true;
TokenExpiredTxt: Label 'The token has expired.';
RenewTokenActionTxt: Label 'Renew Token';
RenewTokenNotificationTxt: Label 'The token for connecting to the document exchange service has expired.';
RenewTokenNotificationIdTxt: Label '3788e60d-366b-4b69-8827-591237ffe8b1', Locked = true;
ActivateAppActionTxt: Label 'Activate App';
ActivateAppNotificationTxt: Label 'To connect to the document exchange service, your administrator must activate the integration app.';
ActivateAppNotificationIdTxt: Label '7031414b-bc7a-4660-b175-fccf773ddd26', Locked = true;
MissingClientIdTxt: Label 'The client ID has not been initialized. Sandbox: %1.', Locked = true;
MissingClientSecretTxt: Label 'The client secret has not been initialized. Sandbox: %1.', Locked = true;
MissingClientIdOrSecretInSaasTxt: Label 'The client ID or client secret have not been initialized.';
MissingClientIdOrSecretOnPremTxt: Label 'You must register an app that will be used to connect to the document exchange service and specify the client ID, client secret and redirect URL in the Document Exchange Service Setup page.';
GuiNotAllowedTxt: Label 'The GUI is not allowed, so acquiring the authorization code through the interactive experience is not possible.';
AcquireAuthorizationCodeTxt: Label 'Attempting to acquire an authorization code.', Locked = true;
AcquireAccessTokenByAuthorizationCodeTxt: Label 'Attempting to acquire an access token by authorization code.', Locked = true;
AcquireAccessTokenByRefreshTokenTxt: Label 'Attempting to acquire an access token by refresh token.', Locked = true;
CannotGetResponseTxt: Label 'Cannot get a response. Status: %1. Message: %2', Comment = '%1 - response status code, %2 - error message';
CannotGetResponseWithDetailsTxt: Label 'Cannot get a response. Status: %1. Message: %2. Details: %3', Comment = '%1 - response status code, %2 - error message, %3 - error details';
CannotParseResponseTxt: Label 'Cannot parse a response.';
CannotParseIdTokenTxt: Label 'Cannot parse the ID token. %1', Comment = '%1 - error details';
IdTokenParsedTxt: Label 'Succeed to parse the ID token.', Locked = true;
CannotGetParamValueTxt: Label 'Cannot get the parameter value from the response. Parameter name: %1.', Comment = '%1 - parameter name';
NotMatchingParamValueTxt: Label 'The parameter value does not match. Parameter name: %1.', Comment = '%1 - parameter name';
SucceedRenewTokenTxt: Label 'The token was successfully renewed.';
SucceedRetrieveAccessTokenTxt: Label 'The access token was successfully retrieved. The token lifetime is %1.', Locked = true;
FailedAcquireAuthorizationCodeTxt: Label 'Failed to acquire an authorization code. %1', Comment = '%1 - error details';
SucceedAcquireAuthorizationCodeTxt: Label 'The authorization code was successfully acquired.', Locked = true;
FailedAcquireAccessTokenByAuthorizationCodeTxt: Label 'Failed to acquire an access token by authorization code. %1', Comment = '%1 - error details';
SucceedAcquireAccessTokenByAuthorizationCodeTxt: Label 'Acquiring an access token by the authorization code was successful.', Locked = true;
FailedAcquireAccessTokenByRefreshTokenTxt: Label 'Failed to acquire an access token by refresh token. %1', Comment = '%1 - error details';
SucceedAcquireAccessTokenByRefreshTokenTxt: Label 'Acquiring an access token by the refresh token was successful.', Locked = true;
FieldNotSpecifiedTxt: Label 'The field value is not specified. Field: %1', Comment = '%1 - field caption';
EmptyAccessTokenTxt: Label 'The access token is empty.';
EmptyRefreshTokenTxt: Label 'The refresh token is empty.';
EmptyIdTokenTxt: Label 'The ID token is empty.';
procedure IsSandbox(var DocExchServiceSetup: Record "Doc. Exch. Service Setup"): Boolean
begin
exit(DocExchServiceSetup."Service URL".Contains(SandboxTxt));
end;
procedure SetURLsToDefault(var DocExchServiceSetup: Record "Doc. Exch. Service Setup")
begin
SetURLsToDefault(DocExchServiceSetup, false);
end;
[Scope('OnPrem')]
procedure SetURLsToDefault(var DocExchServiceSetup: Record "Doc. Exch. Service Setup"; Sandbox: Boolean)
var
IsHandled: Boolean;
begin
IsHandled := false;
OnBeforeSetURLsToDefault(DocExchServiceSetup, Sandbox, IsHandled);
if IsHandled then
exit;
if not Sandbox then begin
DocExchServiceSetup."Sign-up URL" := DefaultSignUpUrlProdTxt;
DocExchServiceSetup."Sign-in URL" := DefaultSignInUrlProdTxt;
DocExchServiceSetup."Service URL" := DefaultServiceUrlProdTxt;
DocExchServiceSetup."Auth URL" := DefaultAuthUrlProdTxt;
DocExchServiceSetup."Token URL" := DefaultTokenUrlProdTxt;
end else begin
DocExchServiceSetup."Sign-up URL" := DefaultSignUpUrlSandboxTxt;
DocExchServiceSetup."Sign-in URL" := DefaultSignInUrlSandboxTxt;
DocExchServiceSetup."Service URL" := DefaultServiceUrlSandboxTxt;
DocExchServiceSetup."Auth URL" := DefaultAuthUrlSandboxTxt;
DocExchServiceSetup."Token URL" := DefaultTokenUrlSandboxTxt;
end;
DocExchServiceSetup.SetDefaultRedirectUrl();
DocExchServiceSetup."User Agent" := CopyStr(CompanyName() + DefaultVersionTxt, 1, MaxStrLen(DocExchServiceSetup."User Agent"));
end;
[Scope('OnPrem')]
procedure GetAppUrl(var DocExchServiceSetup: Record "Doc. Exch. Service Setup"): Text
var
ClientId: Text;
SignInUrl: Text;
AppUrl: Text;
Sandbox: Boolean;
begin
SignInUrl := DocExchServiceSetup."Sign-in URL".TrimEnd('/');
if SignInUrl = '' then
exit('');
Sandbox := IsSandbox(DocExchServiceSetup);
ClientId := GetClientId(Sandbox);
if ClientId = '' then
exit('');
AppUrl := SignInUrl.Substring(1, SignInUrl.LastIndexOf('/')) + AppStorePathTxt + ClientId;
exit(AppUrl);
end;
[Scope('OnPrem')]
procedure GetDefaultRedirectUrl(): Text
var
OAuth2: Codeunit "OAuth2";
RedirectUrl: Text;
begin
OAuth2.GetDefaultRedirectUrl(RedirectUrl);
exit(RedirectUrl);
end;
[Scope('OnPrem')]
procedure SetDefaultRedirectUrl(var DocExchServiceSetup: Record "Doc. Exch. Service Setup")
var
RedirectUrl: Text;
begin
RedirectUrl := GetDefaultRedirectUrl();
DocExchServiceSetup."Redirect URL" := CopyStr(RedirectUrl, 1, MaxStrLen(DocExchServiceSetup."Redirect URL"));
end;
local procedure SetPropertiesBasedOnAuthRequestUrlAndRunOAuth2ControlAddIn(AuthRequestUrl: Text; State: Text; var AuthCode: SecretText; var ErrorMessage: Text)
var
DocExchServiceAuth: Page "Doc. Exch. Service Auth.";
begin
if AuthRequestUrl = '' then begin
Clear(AuthCode);
exit;
end;
DocExchServiceAuth.SetOAuth2Properties(AuthRequestUrl, State);
Commit();
DocExchServiceAuth.RunModal();
AuthCode := DocExchServiceAuth.GetAuthCodeAsSecretText();
ErrorMessage := DocExchServiceAuth.GetAuthError();
end;
local procedure AcquireAccessTokenByAuthorizationCode(ClientId: Text; ClientSecret: SecretText; RedirectUrl: Text; AuthUrl: Text; TokenUrl: Text; var AccessToken: SecretText; var RefreshToken: SecretText; var IdToken: SecretText)
var
AuthRequestUrl: Text;
AuthorizationCode: SecretText;
State: Text;
RequestBody: SecretText;
ErrorMessage: Text;
begin
Session.LogMessage('0000EXU', AcquireAuthorizationCodeTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
State := GetGUID();
AuthRequestUrl := StrSubstNo(AuthorizationCodeRequestUrlTxt, AuthUrl, UrlEncode(ClientId), UrlEncode(RedirectUrl), UrlEncode(AuthCodeScopeTxt), UrlEncode(State));
SetPropertiesBasedOnAuthRequestUrlAndRunOAuth2ControlAddIn(AuthRequestUrl, State, AuthorizationCode, ErrorMessage);
if AuthorizationCode.IsEmpty() then begin
Session.LogMessage('0000EXW', StrSubstNo(FailedAcquireAuthorizationCodeTxt, ErrorMessage), Verbosity::Warning, DataClassification::CustomerContent, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(FailedAcquireAuthorizationCodeTxt, ErrorMessage);
end;
Session.LogMessage('0000EXX', SucceedAcquireAuthorizationCodeTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Session.LogMessage('0000EXY', AcquireAccessTokenByAuthorizationCodeTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
if RedirectUrl = '' then
RedirectUrl := GetDefaultRedirectUrl();
RequestBody := SecretStrSubstNo(AuthorizationCodeRequestBodyTxt, UrlEncode(ClientId), UrlEncode(ClientSecret), UrlEncode(AuthorizationCode), UrlEncode(RedirectUrl));
if not TryAcquireAccessToken(TokenUrl, ClientId, ClientSecret, RequestBody, AccessToken, RefreshToken, IdToken, ErrorMessage, true) then begin
if ErrorMessage = '' then
ErrorMessage := GetLastErrorText();
Session.LogMessage('0000EXZ', StrSubstNo(FailedAcquireAccessTokenByAuthorizationCodeTxt, ErrorMessage), Verbosity::Warning, DataClassification::CustomerContent, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(FailedAcquireAccessTokenByAuthorizationCodeTxt, ErrorMessage);
end;
if AccessToken.IsEmpty() then begin
Session.LogMessage('0000EY0', StrSubstNo(FailedAcquireAccessTokenByAuthorizationCodeTxt, ''), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(FailedAcquireAccessTokenByAuthorizationCodeTxt, '');
end;
if RefreshToken.IsEmpty() then
Session.LogMessage('0000EYQ', EmptyRefreshTokenTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
if IdToken.IsEmpty() then
Session.LogMessage('0000EZ6', EmptyIdTokenTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Session.LogMessage('0000EY1', SucceedAcquireAccessTokenByAuthorizationCodeTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok)
end;
local procedure AcquireAccessTokenByRefreshToken(ClientId: Text; ClientSecret: SecretText; TokenUrl: Text; var AccessToken: SecretText; var RefreshToken: SecretText)
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
OldRefreshToken: SecretText;
RequestBody: SecretText;
ErrorMessage: Text;
IdToken: SecretText;
begin
Session.LogMessage('0000EY2', AcquireAccessTokenByRefreshTokenTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
GetServiceSetUp(DocExchServiceSetup);
OldRefreshToken := DocExchServiceSetup.GetRefreshTokenAsSecretText();
if OldRefreshToken.IsEmpty() then begin
ErrorMessage := EmptyRefreshTokenTxt;
Session.LogMessage('0000EY3', StrSubstNo(FailedAcquireAccessTokenByRefreshTokenTxt, EmptyRefreshTokenTxt), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(FailedAcquireAccessTokenByRefreshTokenTxt, ErrorMessage);
end;
RequestBody := SecretStrSubstNo(RefreshTokenRequestBodyTxt, UrlEncode(ClientId), UrlEncode(ClientSecret), UrlEncode(OldRefreshToken), UrlEncode(ClientId));
if not TryAcquireAccessToken(TokenUrl, ClientId, ClientSecret, RequestBody, AccessToken, RefreshToken, IdToken, ErrorMessage, false) then begin
if ErrorMessage = '' then
ErrorMessage := GetLastErrorText();
Session.LogMessage('0000EY4', StrSubstNo(FailedAcquireAccessTokenByRefreshTokenTxt, ErrorMessage), Verbosity::Warning, DataClassification::CustomerContent, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(FailedAcquireAccessTokenByRefreshTokenTxt, ErrorMessage);
end;
if AccessToken.IsEmpty() then begin
Session.LogMessage('0000EY5', StrSubstNo(FailedAcquireAccessTokenByRefreshTokenTxt, ''), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(FailedAcquireAccessTokenByRefreshTokenTxt, '');
end;
if RefreshToken.IsEmpty() then begin
Session.LogMessage('0000EZ7', EmptyRefreshTokenTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(EmptyRefreshTokenTxt);
end;
Session.LogMessage('0000EY6', SucceedAcquireAccessTokenByRefreshTokenTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
end;
[NonDebuggable]
[TryFunction]
local procedure TryAcquireAccessToken(TokenUrl: Text; ClientId: Text; ClientSecret: SecretText; RequestBody: SecretText; var AccessToken: SecretText; var RefreshToken: SecretText; var IdToken: SecretText; var ErrorMessage: Text; ParseIdToken: Boolean)
var
TempBlob: Codeunit "Temp Blob";
OutStream: OutStream;
HttpStatusCode: DotNet HttpStatusCode;
ResponseHeaders: DotNet NameValueCollection;
ResponseBody: Text;
ErrorDetails: Text;
TokenType: Text;
ResponseClientId: Text;
ExpiresIn: Integer;
HttpStatusCodeNumber: Integer;
begin
TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8);
OutStream.WriteText(RequestBody.Unwrap());
HttpWebRequestMgt.Initialize(TokenUrl);
HttpWebRequestMgt.DisableUI();
HttpWebRequestMgt.SetMethod(MethodPostTxt);
HttpWebRequestMgt.SetContentType(ApplicationFormTxt);
HttpWebRequestMgt.SetContentLength(TempBlob.Length());
HttpWebRequestMgt.SetUserAgent(GetUserAgent());
HttpWebRequestMgt.SetReturnType(ApplicationJsonTxt);
HttpWebRequestMgt.AddHeader(AcceptEncodingHeaderNameTxt, EncodingUtf8Txt);
HttpWebRequestMgt.AddBasicAuthentication(ClientId, ClientSecret);
HttpWebRequestMgt.AddBodyBlob(TempBlob);
if not HttpWebRequestMgt.SendRequestAndReadTextResponse(ResponseBody, ErrorMessage, ErrorDetails, HttpStatusCode, ResponseHeaders) then begin
if not IsNull(HttpStatusCode) then
HttpStatusCodeNumber := HttpStatusCode;
Session.LogMessage('0000EY7', StrSubstNo(CannotGetResponseWithDetailsTxt, HttpStatusCodeNumber, ErrorMessage, ErrorDetails), Verbosity::Warning, DataClassification::CustomerContent, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(CannotGetResponseWithDetailsTxt, HttpStatusCodeNumber, ErrorMessage, ErrorDetails);
end;
ParseTokenResponse(ResponseBody, AccessToken, RefreshToken, IdToken, TokenType, ResponseClientId, ExpiresIn, ParseIdToken);
if TokenType <> TokenTypeBearerTxt then begin
Session.LogMessage('0000EY8', StrSubstNo(NotMatchingParamValueTxt, TokenTypeParamNameTxt), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(NotMatchingParamValueTxt, TokenTypeParamNameTxt);
end;
if ResponseClientId <> ClientId then begin
Session.LogMessage('0000EY9', StrSubstNo(NotMatchingParamValueTxt, ClientIdParamNameTxt), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(NotMatchingParamValueTxt, ClientIdParamNameTxt);
end;
Session.LogMessage('0000EYA', StrSubstNo(SucceedRetrieveAccessTokenTxt, ExpiresIn), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
end;
[TryFunction]
[NonDebuggable]
local procedure TryGetSecurityToken(Token: SecretText; var JwtSecurityToken: DotNet JwtSecurityToken)
var
JwtSecurityTokenHandler: DotNet JwtSecurityTokenHandler;
begin
JwtSecurityTokenHandler := JwtSecurityTokenHandler.JwtSecurityTokenHandler();
JwtSecurityToken := JwtSecurityTokenHandler.ReadToken(Token.Unwrap());
Session.LogMessage('0000EYB', IdTokenParsedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
end;
[NonDebuggable]
local procedure ParseTokenResponse(ResponseBody: Text; var AccessToken: SecretText; var RefreshToken: SecretText; var IdToken: SecretText; var TokenType: Text; var ClientId: Text; var ExpiresIn: Integer; ParseIdToken: Boolean)
var
JsonObject: JsonObject;
AccessTokenAsText, RefreshTokenAsText, IdTokenAsText : Text;
begin
if not JsonObject.ReadFrom(ResponseBody) then begin
Session.LogMessage('0000EYC', CannotParseResponseTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(CannotParseResponseTxt);
end;
if not GetJsonKeyValue(JsonObject, AccessTokenParamNameTxt, AccessTokenAsText) then begin
Session.LogMessage('0000EYD', StrSubstNo(CannotGetParamValueTxt, AccessTokenParamNameTxt), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(CannotGetParamValueTxt, AccessTokenParamNameTxt);
end;
AccessToken := AccessTokenAsText;
if not GetJsonKeyValue(JsonObject, RefreshTokenParamNameTxt, RefreshTokenAsText) then begin
Session.LogMessage('0000EYE', StrSubstNo(CannotGetParamValueTxt, RefreshTokenParamNameTxt), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(CannotGetParamValueTxt, RefreshTokenParamNameTxt);
end;
RefreshToken := RefreshTokenAsText;
if not GetJsonKeyValue(JsonObject, TokenTypeParamNameTxt, TokenType) then begin
Session.LogMessage('0000EYF', StrSubstNo(CannotGetParamValueTxt, TokenTypeParamNameTxt), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(CannotGetParamValueTxt, TokenTypeParamNameTxt);
end;
if ParseIdToken then
if not GetJsonKeyValue(JsonObject, IdTokenParamNameTxt, IdTokenAsText) then begin
Session.LogMessage('0000EYG', StrSubstNo(CannotGetParamValueTxt, IdTokenParamNameTxt), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(CannotGetParamValueTxt, IdTokenParamNameTxt);
end;
IdToken := IdTokenAsText;
if not GetJsonKeyValue(JsonObject, ExpiresInParamNameTxt, ExpiresIn) then
Session.LogMessage('0000EYH', StrSubstNo(CannotGetParamValueTxt, ExpiresInParamNameTxt), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
if not GetJsonKeyValue(JsonObject, ClientIdParamNameTxt, ClientId) then begin
Session.LogMessage('0000EYI', StrSubstNo(CannotGetParamValueTxt, ClientIdParamNameTxt), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(CannotGetParamValueTxt, ClientIdParamNameTxt);
end;
end;
[NonDebuggable]
local procedure GetJsonKeyValue(var JsonObject: JsonObject; KeyName: Text; var KeyValue: Text): Boolean
var
JsonToken: JsonToken;
begin
if not JsonObject.Get(KeyName, JsonToken) then
exit(false);
KeyValue := JsonToken.AsValue().AsText();
exit(true);
end;
[NonDebuggable]
local procedure GetJsonKeyValue(var JsonObject: JsonObject; KeyName: Text; var KeyValue: Integer): Boolean
var
JsonToken: JsonToken;
begin
if not JsonObject.Get(KeyName, JsonToken) then
exit(false);
KeyValue := JsonToken.AsValue().AsInteger();
exit(true);
end;
local procedure MarkTokenAsExpired()
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
begin
GetServiceSetUp(DocExchServiceSetup);
DocExchServiceSetup."Token Expired" := true;
DocExchServiceSetup.Modify();
end;
[Scope('OnPrem')]
procedure SendRenewTokenNotification()
var
RenewTokenNotification: Notification;
begin
if not GuiAllowed() then
exit;
RenewTokenNotification.Id := GetRenewTokenNotificationId();
RenewTokenNotification.Message := RenewTokenNotificationTxt;
RenewTokenNotification.Scope := NotificationScope::LocalScope;
RenewTokenNotification.AddAction(RenewTokenActionTxt, Codeunit::"Doc. Exch. Service Mgt.", 'RenewToken');
RenewTokenNotification.Send();
end;
procedure RecallActivateAppNotification()
var
ActivateAppNotification: Notification;
begin
ActivateAppNotification.Id := GetActivateAppNotificationId();
if not ActivateAppNotification.Recall() then;
end;
[Scope('OnPrem')]
procedure SendActivateAppNotification()
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
ActivateAppNotification: Notification;
ClientId: Text;
Sandbox: Boolean;
IsHandled: Boolean;
begin
IsHandled := false;
OnBeforeSendActivateAppNotification(IsHandled);
if IsHandled then
exit;
if not GuiAllowed() then
exit;
if not DocExchServiceSetup.Get() then
exit;
Sandbox := IsSandbox(DocExchServiceSetup);
ClientId := GetClientId(Sandbox);
if ClientId = '' then
exit;
ActivateAppNotification.Id := GetActivateAppNotificationId();
ActivateAppNotification.Recall();
ActivateAppNotification.Message := ActivateAppNotificationTxt;
ActivateAppNotification.Scope := NotificationScope::LocalScope;
if GetAppUrl(DocExchServiceSetup) <> '' then
ActivateAppNotification.AddAction(ActivateAppActionTxt, Codeunit::"Doc. Exch. Service Mgt.", 'ActivateApp');
ActivateAppNotification.Send();
end;
[Scope('OnPrem')]
procedure ActivateApp(var ActivateAppNotification: Notification)
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
begin
SendActivateAppNotification();
GetServiceSetUp(DocExchServiceSetup);
Hyperlink(GetAppUrl(DocExchServiceSetup));
end;
[Scope('OnPrem')]
procedure RenewToken(var RenewTokenNotification: Notification)
begin
RenewToken(true);
end;
[Scope('OnPrem')]
procedure RenewToken(TryRefreshToken: Boolean)
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
AccessToken: SecretText;
RefreshToken: SecretText;
Renewed: Boolean;
begin
if TryRefreshToken then
if TryAcquireAccessTokenByRefreshToken(AccessToken, RefreshToken) then begin
SaveTokens(AccessToken, RefreshToken);
Renewed := TryCheckConnection();
end;
if not Renewed then begin
AcquireAccessTokenByAuthorizationCode(true);
Renewed := TryCheckConnection();
end;
GetServiceSetUp(DocExchServiceSetup);
if Renewed then begin
LogActivitySucceeded(DocExchServiceSetup.RecordId(), RenewTokenTxt, SucceedRenewTokenTxt);
if GuiAllowed() then
Message(SucceedRenewTokenTxt);
exit;
end;
LogActivityFailedAndError(DocExchServiceSetup.RecordId(), RenewTokenTxt, GetLastErrorText());
end;
local procedure GetRenewTokenNotificationId(): Guid
begin
exit(TextToGuid(RenewTokenNotificationIdTxt));
end;
local procedure GetActivateAppNotificationId(): Guid
begin
exit(TextToGuid(ActivateAppNotificationIdTxt));
end;
local procedure TextToGuid(TextVar: Text): Guid
var
GuidVar: Guid;
begin
if not Evaluate(GuidVar, TextVar) then;
exit(GuidVar);
end;
internal procedure GetFeatureTelemetryName(): Text
var
DocumentExchangeTelemetryNameTxt: Label 'Document Exchange', Locked = true;
begin
exit(DocumentExchangeTelemetryNameTxt);
end;
[TryFunction]
local procedure TryCheckConnection()
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
begin
VerifyPrerequisites(true);
GetServiceSetUp(DocExchServiceSetup);
ExecuteWebServiceGetRequest(GetCheckConnectionURL());
end;
[Scope('OnPrem')]
procedure CheckConnection()
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
begin
VerifyPrerequisites(true);
GetServiceSetUp(DocExchServiceSetup);
if not ExecuteWebServiceGetRequest(GetCheckConnectionURL()) then
LogActivityFailedAndError(DocExchServiceSetup.RecordId(), CheckConnectionTxt, GetLastErrorText());
LogActivitySucceeded(DocExchServiceSetup.RecordId(), CheckConnectionTxt, ConnectionSuccessMsg);
if GuiAllowed() then
Message(ConnectionSuccessMsg);
if GLBTraceLogEnabled then
Trace.LogStreamToTempFile(GLBResponseInStream, 'checkstatus', TempBlobTrace);
end;
[Scope('OnPrem')]
procedure SendUBLDocument(DocVariant: Variant; var TempBlob: Codeunit "Temp Blob"): Text
var
DocRecRef: RecordRef;
FeatureTelemetry: Codeunit "Feature Telemetry";
begin
CheckServiceEnabled();
FeatureTelemetry.LogUptake('0000IM9', TelemetryCategoryTok, Enum::"Feature Uptake Status"::Used);
FeatureTelemetry.LogUsage('0000IMP', TelemetryCategoryTok, 'Document send');
DocRecRef.GetTable(DocVariant);
CheckDocumentStatus(DocRecRef);
if not ExecuteWebServicePostRequest(GetPostSalesURL(DocRecRef), TempBlob) then
LogActivityFailedAndError(DocRecRef.RecordId, SendDocTxt, '');
LogActivitySucceeded(DocRecRef.RecordId, SendDocTxt, DocSendSuccessMsg);
DocExchLinks.UpdateDocumentRecord(DocRecRef, GLBLastUsedGUID, '');
LogTelemetryDocumentSent();
if GuiAllowed then
Message(DocSendSuccessMsg);
exit(GLBLastUsedGUID);
end;
[Scope('OnPrem')]
procedure SendDocument(DocVariant: Variant; var TempBlob: Codeunit "Temp Blob"): Text
var
DocRecRef: RecordRef;
DocIdentifier: Text;
begin
CheckServiceEnabled();
DocIdentifier := GetGUID();
DocRecRef.GetTable(DocVariant);
CheckDocumentStatus(DocRecRef);
PutDocument(TempBlob, DocIdentifier, DocRecRef);
DispatchDocument(DocIdentifier, DocRecRef);
LogTelemetryDocumentSent();
if GuiAllowed() then
Message(DocSendSuccessMsg);
exit(DocIdentifier);
end;
[Scope('OnPrem')]
procedure HasPredefinedOAuth2Params(): Boolean
begin
if GetPredefinedClientId(false) = '' then
exit(false);
if GetPredefinedClientSecret(false).IsEmpty() then
exit(false);
if GetPredefinedClientId(true) = '' then
exit(false);
if GetPredefinedClientSecret(true).IsEmpty() then
exit(false);
exit(true);
end;
[Scope('OnPrem')]
procedure GetClientId(Sandbox: Boolean): Text
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
ClientId: Text;
begin
ClientId := GetPredefinedClientId(Sandbox);
if ClientId = '' then
if DocExchServiceSetup.Get() then
ClientId := DocExchServiceSetup."Client Id";
exit(ClientId);
end;
local procedure GetPredefinedClientId(Sandbox: Boolean): Text
var
AzureKeyVault: Codeunit "Azure Key Vault";
SecretName: Text;
ClientId: Text;
begin
if not Sandbox then
SecretName := ClientIdAKVSecretNameProdTxt
else
SecretName := ClientIdAKVSecretNameSandboxTxt;
if EnvironmentInfo.IsSaaSInfrastructure() then
if not AzureKeyVault.GetAzureKeyVaultSecret(SecretName, ClientId) then
Session.LogMessage('0000EYJ', StrSubstNo(MissingClientIdTxt, Sandbox), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok)
else
exit(ClientId);
if ClientId = '' then
OnGetClientId(ClientId);
exit(ClientId);
end;
[Scope('OnPrem')]
procedure GetClientSecretAsSecretText(Sandbox: Boolean): SecretText
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
ClientSecret: SecretText;
begin
ClientSecret := GetPredefinedClientSecret(Sandbox);
if ClientSecret.IsEmpty() then
if DocExchServiceSetup.Get() then
ClientSecret := DocExchServiceSetup.GetClientSecretAsSecretText();
exit(ClientSecret);
end;
local procedure GetPredefinedClientSecret(Sandbox: Boolean): SecretText
var
AzureKeyVault: Codeunit "Azure Key Vault";
ClientSecret: SecretText;
[NonDebuggable]
ClientSecretFromEvent: Text;
SecretName: Text;
begin
if not Sandbox then
SecretName := ClientSecretAKVSecretNameProdTxt
else
SecretName := ClientSecretAKVSecretNameSandboxTxt;
if EnvironmentInfo.IsSaaSInfrastructure() then
if not AzureKeyVault.GetAzureKeyVaultSecret(SecretName, ClientSecret) then
Session.LogMessage('0000EYK', StrSubstNo(MissingClientSecretTxt, Sandbox), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok)
else
exit(ClientSecret);
if ClientSecret.IsEmpty() then begin
OnGetClientSecret(ClientSecretFromEvent);
ClientSecret := ClientSecretFromEvent;
end;
exit(ClientSecret);
end;
[NonDebuggable]
local procedure VerifyIdToken(IdToken: SecretText)
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
JwtSecurityToken: DotNet JwtSecurityToken;
ErrorMessage: Text;
ClientId: Text;
AuthUrl: Text;
ExpectedIss: Text;
Aud: Text;
Iss: Text;
Sandbox: Boolean;
begin
if not TryGetSecurityToken(IdToken, JwtSecurityToken) then begin
ErrorMessage := GetLastErrorText();
Session.LogMessage('0000EYL', StrSubstNo(CannotParseIdTokenTxt, ErrorMessage), Verbosity::Warning, DataClassification::CustomerContent, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(CannotParseIdTokenTxt, ErrorMessage);
end;
GetServiceSetUp(DocExchServiceSetup);
AuthUrl := DocExchServiceSetup."Auth URL".ToLower();
Sandbox := IsSandbox(DocExchServiceSetup);
ClientId := GetClientId(Sandbox);
Aud := JwtSecurityToken.Payload().Aud().Item(0);
if Aud <> ClientId then begin
Session.LogMessage('0000EYM', StrSubstNo(NotMatchingParamValueTxt, AudienceParamNameTxt), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(NotMatchingParamValueTxt, AudienceParamNameTxt);
end;
Iss := JwtSecurityToken.Payload().Iss().ToLower();
ExpectedIss := AuthUrl.Substring(1, StrLen(Iss)).ToLower();
if Iss <> ExpectedIss then begin
Session.LogMessage('0000EYN', StrSubstNo(NotMatchingParamValueTxt, IssuerParamNameTxt), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(NotMatchingParamValueTxt, IssuerParamNameTxt);
end;
end;
local procedure ParseIdToken(IdToken: SecretText; var Json: Text; var Subject: Text; var IssuedAt: DateTime)
begin
if IdToken.IsEmpty() then begin
Session.LogMessage('0000EZ8', EmptyIdTokenTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
exit;
end;
if not TryParseIdToken(IdToken, Json, Subject, IssuedAt) then begin
Session.LogMessage('0000EZ9', StrSubstNo(CannotParseIdTokenTxt, GetLastErrorText()), Verbosity::Warning, DataClassification::CustomerContent, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
exit;
end;
end;
[TryFunction]
local procedure TryParseIdToken(IdToken: SecretText; var Json: Text; var Subject: Text; var IssuedAt: DateTime)
var
JwtSecurityToken: DotNet JwtSecurityToken;
DotNetDateTime: DotNet DateTime;
DotNetTimeSpan: DotNet TimeSpan;
DotNetDateTimeKind: DotNet DateTimeKind;
Iat: BigInteger;
begin
if not TryGetSecurityToken(IdToken, JwtSecurityToken) then begin
Session.LogMessage('0000EYO', StrSubstNo(CannotParseIdTokenTxt, GetLastErrorText()), Verbosity::Warning, DataClassification::CustomerContent, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
exit;
end;
Json := JwtSecurityToken.Payload().SerializeToJson();
Subject := JwtSecurityToken.Payload().Sub();
Iat := JwtSecurityToken.Payload().Iat;
if Iat > 0 then begin
DotNetDateTime := DotNetDateTime.DateTime(1970, 1, 1, 0, 0, 0, DotNetDateTimeKind.Utc);
DotNetTimeSpan := DotNetTimeSpan.FromSeconds(Iat);
IssuedAt := DotNetDateTime.Add(DotNetTimeSpan).ToLocalTime();
end;
end;
local procedure SaveTokens(AccessToken: SecretText; RefreshToken: SecretText)
var
IdToken: Text;
begin
SaveTokens(AccessToken, RefreshToken, IdToken);
end;
local procedure SaveTokens(AccessToken: SecretText; RefreshToken: SecretText; IdToken: SecretText)
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
Json: Text;
Subject: Text;
IssuedAt: DateTime;
begin
GetServiceSetUp(DocExchServiceSetup);
DocExchServiceSetup.SetAccessToken(AccessToken);
DocExchServiceSetup.SetRefreshToken(RefreshToken);
if not IdToken.IsEmpty() then begin
ParseIdToken(IdToken, Json, Subject, IssuedAt);
if Json <> '' then begin
DocExchServiceSetup."Id Token" := CopyStr(Json, 1, MaxStrLen(DocExchServiceSetup."Id Token"));
DocExchServiceSetup."Token Subject" := CopyStr(Subject, 1, MaxStrLen(DocExchServiceSetup."Token Subject"));
end;
end else
if not AccessToken.IsEmpty() then
IssuedAt := CurrentDateTime();
DocExchServiceSetup."Token Issued At" := IssuedAt;
DocExchServiceSetup."Token Expired" := false;
DocExchServiceSetup.Modify();
end;
[Scope('OnPrem')]
procedure AcquireAccessTokenByAuthorizationCode(EnabledOnly: Boolean)
var
AccessToken: SecretText;
RefreshToken: SecretText;
IdToken: SecretText;
begin
AcquireAccessTokenByAuthorizationCode(EnabledOnly, AccessToken, RefreshToken, IdToken);
VerifyIdToken(IdToken);
SaveTokens(AccessToken, RefreshToken, IdToken);
end;
local procedure AcquireAccessTokenByAuthorizationCode(EnabledOnly: Boolean; var AccessToken: SecretText; var RefreshToken: SecretText; var IdToken: SecretText)
var
ClientId: Text;
ClientSecret: SecretText;
RedirectUrl: Text;
AuthUrl: text;
TokenUrl: Text;
begin
if not GuiAllowed() then begin
Session.LogMessage('0000EYP', GuiNotAllowedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(GuiNotAllowedTxt);
end;
GetOAuth2Params(EnabledOnly, ClientId, ClientSecret, RedirectUrl, AuthUrl, TokenUrl);
AcquireAccessTokenByAuthorizationCode(ClientId, ClientSecret, RedirectUrl, AuthUrl, TokenUrl, AccessToken, RefreshToken, IdToken);
end;
[Scope('OnPrem')]
procedure AcquireAccessTokenByRefreshToken()
var
AccessToken: SecretText;
RefreshToken: SecretText;
begin
AcquireAccessTokenByRefreshToken(AccessToken, RefreshToken);
SaveTokens(AccessToken, RefreshToken);
end;
[TryFunction]
local procedure TryAcquireAccessTokenByRefreshToken(var AccessToken: SecretText; var RefreshToken: SecretText)
begin
AcquireAccessTokenByRefreshToken(AccessToken, RefreshToken);
end;
local procedure AcquireAccessTokenByRefreshToken(var AccessToken: SecretText; var RefreshToken: SecretText)
var
ClientId: Text;
ClientSecret: SecretText;
RedirectUrl: Text;
AuthUrl: Text;
TokenUrl: Text;
begin
GetOAuth2Params(true, ClientId, ClientSecret, RedirectUrl, AuthUrl, TokenUrl);
AcquireAccessTokenByRefreshToken(ClientId, ClientSecret, TokenUrl, AccessToken, RefreshToken);
end;
local procedure GetOAuth2Params(EnabledOnly: Boolean; var ClientId: Text; var ClientSecret: SecretText; var RedirectUrl: Text; var AuthUrl: Text; var TokenUrl: Text)
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
Sandbox: Boolean;
begin
GetServiceSetUp(DocExchServiceSetup);
if EnabledOnly then
CheckServiceEnabled(DocExchServiceSetup);
RedirectUrl := DocExchServiceSetup."Redirect URL";
if RedirectUrl = '' then begin
Session.LogMessage('0000EYS', StrSubstNo(FieldNotSpecifiedTxt, DocExchServiceSetup.FieldCaption("Redirect URL")), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(FieldNotSpecifiedTxt, DocExchServiceSetup.FieldCaption("Redirect URL"));
end;
AuthUrl := DocExchServiceSetup."Auth URL";
if AuthUrl = '' then begin
Session.LogMessage('0000EYT', StrSubstNo(FieldNotSpecifiedTxt, DocExchServiceSetup.FieldCaption("Auth URL")), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(FieldNotSpecifiedTxt, DocExchServiceSetup.FieldCaption("Auth URL"));
end;
TokenUrl := DocExchServiceSetup."Token URL";
if TokenUrl = '' then begin
Session.LogMessage('0000EYU', StrSubstNo(FieldNotSpecifiedTxt, DocExchServiceSetup.FieldCaption("Token URL")), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(FieldNotSpecifiedTxt, DocExchServiceSetup.FieldCaption("Token URL"));
end;
Sandbox := IsSandbox(DocExchServiceSetup);
ClientId := GetClientId(Sandbox);
if ClientId = '' then begin
Session.LogMessage('0000EYV', StrSubstNo(MissingClientIdTxt, Sandbox), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(GetMissingClientIdOrSecretErr());
end;
ClientSecret := GetClientSecretAsSecretText(Sandbox);
if ClientSecret.IsEmpty() then begin
Session.LogMessage('0000EYW', StrSubstNo(MissingClientSecretTxt, Sandbox), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(GetMissingClientIdOrSecretErr());
end;
end;
local procedure GetMissingClientIdOrSecretErr(): Text
begin
if EnvironmentInfo.IsSaaSInfrastructure() then
exit(MissingClientIdOrSecretInSaasTxt);
exit(MissingClientIdOrSecretOnPremTxt);
end;
local procedure PutDocument(var TempBlob: Codeunit "Temp Blob"; DocIdentifier: Text; DocRecRef: RecordRef)
var
Succeed: Boolean;
begin
if not ExecuteWebServicePutRequest(GetPUTDocURL(DocIdentifier), TempBlob) then
LogActivityFailedAndError(DocRecRef.RecordId, SendDocTxt, '');
if not IsNull(GLBHttpStatusCode) then
Succeed := GLBHttpStatusCode.Equals(GLBHttpStatusCode.NoContent);
if not Succeed then
LogActivityFailedAndError(DocRecRef.RecordId, SendDocTxt, '');
LogActivitySucceeded(DocRecRef.RecordId, SendDocTxt, DocUploadSuccessMsg);
if GLBTraceLogEnabled then
Trace.LogStreamToTempFile(GLBResponseInStream, 'put', TempBlobTrace);
end;
local procedure DispatchDocument(DocOrigIdentifier: Text; DocRecRef: RecordRef)
var
TempBlob: Codeunit "Temp Blob";
DocIdentifier: Text;
PlaceholderGuid: Guid;
Succeed: Boolean;
begin
if not ExecuteWebServicePostRequest(GetDispatchDocURL(DocOrigIdentifier), TempBlob) then
LogActivityFailedAndError(DocRecRef.RecordId, DispatchDocTxt, '');
if not IsNull(GLBHttpStatusCode) then
Succeed := GLBHttpStatusCode.Equals(GLBHttpStatusCode.Created);
if not Succeed then begin
DocExchLinks.UpdateDocumentRecord(DocRecRef, '', DocOrigIdentifier);
LogActivityFailedAndError(DocRecRef.RecordId, DispatchDocTxt, DocDispatchFailedMsg);
end;
if GLBTraceLogEnabled then
Trace.LogStreamToTempFile(GLBResponseInStream, 'dispatch', TempBlobTrace);
DocIdentifier := GLBResponseHeaders.Get(GetDocumentIDKey());
if not Evaluate(PlaceholderGuid, DocIdentifier) then
LogActivityFailedAndError(DocRecRef.RecordId, DispatchDocTxt, InvalidHeaderResponseMsg);
DocExchLinks.UpdateDocumentRecord(DocRecRef, DocIdentifier, DocOrigIdentifier);
LogActivitySucceeded(DocRecRef.RecordId, DispatchDocTxt, DocDispatchSuccessMsg);
end;
[Scope('OnPrem')]
procedure GetDocumentStatus(DocRecordID: RecordID; DocIdentifier: Text[50]; DocOrigIdentifier: Text[50]): Text
var
Errors: Text;
begin
CheckServiceEnabled();
// Check for dispatch errors first
if DocOrigIdentifier <> '' then
if GetDocDispatchErrors(DocRecordID, DocOrigIdentifier, Errors) then
if Errors <> '' then
exit('FAILED');
// Check metadata
if not GetDocumentMetadata(DocRecordID, DocIdentifier, Errors) then
exit('PENDING');
// If metadata exist it means doc has been dispatched
exit(Errors);
end;
local procedure GetDocDispatchErrors(DocRecordID: RecordID; DocIdentifier: Text; var Errors: Text): Boolean
var
XmlDoc: DotNet XmlDocument;
begin
CheckServiceEnabled();
if not ExecuteWebServiceGetRequest(GetDispatchErrorsURL(DocIdentifier)) then begin
LogActivityFailed(DocRecordID, GetDocErrorTxt, '');
exit(false);
end;
if not HttpWebRequestMgt.TryLoadXMLResponse(GLBResponseInStream, XmlDoc) then begin
LogActivityFailed(DocRecordID, GetDocErrorTxt, '');
exit(false);
end;
Errors := XMLDOMMgt.FindNodeTextWithNamespace(XmlDoc.DocumentElement, GetErrorXPath(),
GetPrefix(), GetApiNamespace());
LogActivitySucceeded(DocRecordID, GetDocErrorTxt, Errors);
if GLBTraceLogEnabled then
Trace.LogStreamToTempFile(GLBResponseInStream, 'dispatcherrors', TempBlobTrace);
exit(true);
end;
[Scope('OnPrem')]
procedure GetDocumentMetadata(DocRecordID: RecordID; DocIdentifier: Text[50]; var NewStatus: Text): Boolean
var
XmlDoc: DotNet XmlDocument;
begin
CheckServiceEnabled();
NewStatus := '';
if not ExecuteWebServiceGetRequest(GetDocStatusURL(DocIdentifier)) then begin
LogActivityFailed(DocRecordID, GetDocStatusTxt, '');
exit(false);
end;
if not HttpWebRequestMgt.TryLoadXMLResponse(GLBResponseInStream, XmlDoc) then begin
LogActivityFailed(DocRecordID, GetDocStatusTxt, '');
exit(false);
end;
if GLBTraceLogEnabled then
Trace.LogStreamToTempFile(GLBResponseInStream, 'checkstatus', TempBlobTrace);
NewStatus := XMLDOMMgt.FindNodeTextWithNamespace(XmlDoc.DocumentElement(), GetStatusXPath(), GetPrefix(), GetPublicNamespace());
LogActivitySucceeded(DocRecordID, GetDocStatusTxt, StrSubstNo(DocStatusOKMsg, NewStatus));
exit(true);
end;
[Scope('OnPrem')]
procedure ReceiveDocuments(ContextRecordID: RecordID)
var
XmlDoc: DotNet XmlDocument;
begin
CheckServiceEnabled();
if not ExecuteWebServiceGetRequest(GetRetrieveDocsURL()) then
LogActivityFailedAndError(ContextRecordID, GetDocsTxt, '');
if not HttpWebRequestMgt.TryLoadXMLResponse(GLBResponseInStream, XmlDoc) then
LogActivityFailedAndError(ContextRecordID, GetDocsTxt, '');
ProcessReceivedDocs(ContextRecordID, XmlDoc);
end;
local procedure ProcessReceivedDocs(ContextRecordID: RecordID; XmlDocs: DotNet XmlDocument)
var
IncomingDocument: Record "Incoming Document";
XMLRootNode: DotNet XmlNode;
Node: DotNet XmlNode;
DummyGuid: Guid;
DocIdentifier: Text;
Description: Text;
begin
XMLRootNode := XmlDocs.DocumentElement;
foreach Node in XMLRootNode.ChildNodes do begin
DocIdentifier := XMLDOMMgt.FindNodeTextWithNamespace(Node, GetDocumentIDXPath(),
GetPrefix(), GetPublicNamespace());
if not Evaluate(DummyGuid, DocIdentifier) then
LogActivityFailedAndError(ContextRecordID, GetDocsTxt, MalformedGuidErr);
if TryGetDocumentDescription(Node, Description) then;
if DelChr(Description, '<>', ' ') = '' then
Description := DocIdentifier;
GetOriginalDocument(ContextRecordID, DocIdentifier);
CreateIncomingDocEntry(IncomingDocument, ContextRecordID, DocIdentifier, Description);
if not MarkDocBusinessProcessed(DocIdentifier) then begin
IncomingDocument.Delete();
LogActivityFailed(ContextRecordID, MarkBusinessProcessedTxt, '');
end else
LogActivitySucceeded(ContextRecordID, MarkBusinessProcessedTxt, StrSubstNo(DocIdImportedTxt, DocIdentifier));
Commit();
IncomingDocument.Find();
LogTelemetryDocumentReceived();
OnAfterIncomingDocReceivedFromDocExch(IncomingDocument);
end;
end;
local procedure GetOriginalDocument(ContextRecordID: RecordID; DocIdentifier: Text)
begin
CheckServiceEnabled();
// If can't get the original, it means it was not a 2-step. Get the actual TS-UBL
if not ExecuteWebServiceGetRequest(GetRetrieveOriginalDocIDURL(DocIdentifier)) then
GetDocument(ContextRecordID, DocIdentifier);
end;
local procedure GetDocument(ContextRecordID: RecordID; DocIdentifier: Text)
begin
CheckServiceEnabled();
if not ExecuteWebServiceGetRequest(GetRetrieveDocIDURL(DocIdentifier)) then
LogActivityFailedAndError(ContextRecordID, GetDocsTxt, '');
end;
[TryFunction]
local procedure MarkDocBusinessProcessed(DocIdentifier: Text)
var
TempBlob: Codeunit "Temp Blob";
begin
CheckServiceEnabled();
ExecuteWebServicePutRequest(GetSetTagURL(DocIdentifier), TempBlob);
end;
local procedure CreateIncomingDocEntry(var IncomingDocument: Record "Incoming Document"; ContextRecordID: RecordID; DocIdentifier: Text; Description: Text)
var
IncomingDocumentAttachment: Record "Incoming Document Attachment";
XmlDoc: DotNet XmlDocument;
begin
// Assert response is XML
if not HttpWebRequestMgt.TryLoadXMLResponse(GLBResponseInStream, XmlDoc) then
LogActivityFailedAndError(ContextRecordID, GetDocsTxt, StrSubstNo(FileInvalidTxt, DocIdentifier));
IncomingDocument.CreateIncomingDocument(
CopyStr(Description, 1, MaxStrLen(IncomingDocument.Description)), GetExternalDocURL(DocIdentifier));
// set received XML as main attachment and extract additional ones as secondary attachments
IncomingDocument.AddAttachmentFromStream(IncomingDocumentAttachment, DocIdentifier, 'xml', GLBResponseInStream);
ProcessAttachments(IncomingDocument, XmlDoc);
end;
local procedure ProcessAttachments(var IncomingDocument: Record "Incoming Document"; XmlDoc: DotNet XmlDocument)
var
NodeList: DotNet XmlNodeList;
Node: DotNet XmlNode;
begin
XMLDOMMgt.FindNodesWithNamespace(XmlDoc.DocumentElement, GetEmbeddedDocXPath(), GetPrefix(), GetCBCNamespace(),
NodeList);
foreach Node in NodeList do
ExtractAdditionalAttachment(IncomingDocument, Node);
end;
local procedure ExtractAdditionalAttachment(var IncomingDocument: Record "Incoming Document"; Node: DotNet XmlNode)
var
FileMgt: Codeunit "File Management";
Convert: DotNet Convert;
TempFile: DotNet File;
FilePath: Text;
FileName: Text;
begin
FileName := XMLDOMMgt.GetAttributeValue(Node, 'filename');
FilePath := FileMgt.ServerTempFileName(FileMgt.GetExtension(FileName));
FileMgt.IsAllowedPath(FilePath, false);
TempFile.WriteAllBytes(FilePath, Convert.FromBase64String(Node.InnerText));
IncomingDocument.AddAttachmentFromServerFile(FileName, FilePath);
end;
local procedure Initialize(URL: Text; Method: Text[6]; var TempBlob: Codeunit "Temp Blob")
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
AccessToken: SecretText;
begin
CheckCredentials();
GetServiceSetUp(DocExchServiceSetup);
AccessToken := DocExchServiceSetup.GetAccessTokenAsSecretText();
if AccessToken.IsEmpty() then begin
Session.LogMessage('0000EYR', EmptyAccessTokenTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(EmptyAccessTokenTxt);
end;
Clear(HttpWebRequestMgt);
HttpWebRequestMgt.Initialize(URL);
HttpWebRequestMgt.SetMethod(Method);
HttpWebRequestMgt.AddHeader(AuthorizationHeaderNameTxt, SecretStrSubstNo(AuthorizationHeaderValueTxt, AccessToken));
SetDefaults(TempBlob);
end;
local procedure SetDefaults(var TempBlob: Codeunit "Temp Blob")
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
begin
HttpWebRequestMgt.SetContentType(TextXmlTxt);
HttpWebRequestMgt.SetReturnType(TextXmlTxt);
HttpWebRequestMgt.SetUserAgent(GetUserAgent());
HttpWebRequestMgt.AddHeader(AcceptEncodingHeaderNameTxt, EncodingUtf8Txt);
HttpWebRequestMgt.AddBodyBlob(TempBlob);
// Set tracing
GetServiceSetUp(DocExchServiceSetup);
GLBTraceLogEnabled := DocExchServiceSetup."Log Web Requests";
HttpWebRequestMgt.SetTraceLogEnabled(DocExchServiceSetup."Log Web Requests");
end;
procedure CheckCredentials()
var
DocExchServiceSetup: Page "Doc. Exch. Service Setup";
begin
if not VerifyPrerequisites(false) then
if GuiAllowed() and Confirm(StrSubstNo(NotConfiguredQst, DocExchServiceSetup.Caption()), true) then begin
Commit();
DocExchServiceSetup.RunModal();
if not VerifyPrerequisites(false) then
Error(NotConfiguredErr, DocExchServiceSetup.Caption());
end else
Error(NotConfiguredErr, DocExchServiceSetup.Caption());
end;
local procedure ExecuteWebServiceGetRequest(URL: Text): Boolean
var
TempBlob: Codeunit "Temp Blob";
begin
exit(ExecuteWebServiceRequest(URL, MethodGetTxt, TempBlob));
end;
local procedure ExecuteWebServicePostRequest(URL: Text; var TempBlob: Codeunit "Temp Blob"): Boolean
begin
exit(ExecuteWebServiceRequest(URL, MethodPostTxt, TempBlob));
end;
local procedure ExecuteWebServicePutRequest(URL: Text; var TempBlob: Codeunit "Temp Blob"): Boolean
begin
exit(ExecuteWebServiceRequest(URL, MethodPutTxt, TempBlob));
end;
local procedure ExecuteWebServiceRequest(URL: Text; Method: Text[6]; var TempBlob: Codeunit "Temp Blob"): Boolean
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
ErrorMessage: Text;
begin
Initialize(URL, Method, TempBlob);
if ExecuteWebServiceRequest() then
exit(true);
if IsNull(GLBHttpStatusCode) then
exit(false);
if not GLBHttpStatusCode.Equals(GLBHttpStatusCode.Unauthorized) then
exit(false);
AcquireAccessTokenByRefreshToken();
Initialize(URL, Method, TempBlob);
if ExecuteWebServiceRequest() then
exit(true);
if IsNull(GLBHttpStatusCode) then
exit(false);
if not GLBHttpStatusCode.Equals(GLBHttpStatusCode.Unauthorized) then
exit(false);
ErrorMessage := GetLastErrorText();
GetServiceSetUp(DocExchServiceSetup);
LogActivityFailed(DocExchServiceSetup.RecordId(), TokenExpiredTxt, ErrorMessage);
MarkTokenAsExpired();
SendRenewTokenNotification();
exit(false);
end;
[TryFunction]
local procedure ExecuteWebServiceRequest()
var
ErrorMessage: Text;
HttpStatusCodeNumber: Integer;
begin
Clear(GLBHttpStatusCode);
Clear(GLBResponseHeaders);
Clear(TempBlobResponse);
TempBlobResponse.CreateInStream(GLBResponseInStream);
if not GuiAllowed() then
HttpWebRequestMgt.DisableUI();
if not HttpWebRequestMgt.GetResponse(GLBResponseInStream, GLBHttpStatusCode, GLBResponseHeaders) then begin
if not HttpWebRequestMgt.ProcessFaultXMLResponse('', GetErrorXPath(), GetPrefix(), GetApiNamespace(), GLBHttpStatusCode, GLBResponseHeaders) then
; // catch
ErrorMessage := GetLastErrorText();
if not IsNull(GLBHttpStatusCode) then
HttpStatusCodeNumber := GLBHttpStatusCode;
Session.LogMessage('0000EYX', StrSubstNo(CannotGetResponseTxt, HttpStatusCodeNumber, ErrorMessage), Verbosity::Warning, DataClassification::CustomerContent, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(ErrorMessage); // rethrow
end;
end;
procedure CheckServiceEnabled()
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
begin
GetServiceSetUp(DocExchServiceSetup);
CheckServiceEnabled(DocExchServiceSetup);
end;
procedure GetServiceSetUp(var DocExchServiceSetup: Record "Doc. Exch. Service Setup")
begin
if not DocExchServiceSetup.Get() then begin
Session.LogMessage('0000EYY', NotSetUpTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(NotSetUpTxt);
end;
end;
local procedure CheckServiceEnabled(var DocExchServiceSetup: Record "Doc. Exch. Service Setup")
begin
if not DocExchServiceSetup.Enabled then begin
Session.LogMessage('0000EYZ', NotEnabledTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(NotEnabledTxt);
end;
end;
local procedure CheckDocumentStatus(DocRecRef: RecordRef)
var
SalesInvoiceHeader: Record "Sales Invoice Header";
SalesCrMemoHeader: Record "Sales Cr.Memo Header";
IsHandled: Boolean;
begin
IsHandled := false;
OnBeforeCheckDocumentStatus(DocRecRef, IsHandled);
if IsHandled then
exit;
case DocRecRef.Number of
DATABASE::"Sales Invoice Header":
begin
DocRecRef.SetTable(SalesInvoiceHeader);
if SalesInvoiceHeader."Document Exchange Status" in
[SalesInvoiceHeader."Document Exchange Status"::"Sent to Document Exchange Service",
SalesInvoiceHeader."Document Exchange Status"::"Delivered to Recipient",
SalesInvoiceHeader."Document Exchange Status"::"Pending Connection to Recipient"]
then
Error(CannotResendErr);
end;
DATABASE::"Sales Cr.Memo Header":
begin
DocRecRef.SetTable(SalesCrMemoHeader);
if SalesCrMemoHeader."Document Exchange Status" in
[SalesCrMemoHeader."Document Exchange Status"::"Sent to Document Exchange Service",
SalesCrMemoHeader."Document Exchange Status"::"Delivered to Recipient",
SalesCrMemoHeader."Document Exchange Status"::"Pending Connection to Recipient"]
then
Error(CannotResendErr);
end;
else
Error(UnSupportedTableTypeErr, DocRecRef.Number);
end;
end;
local procedure GetUserAgent(): Text
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
begin
GetServiceSetUp(DocExchServiceSetup);
if DocExchServiceSetup."User Agent" = '' then begin
Session.LogMessage('0000EZ0', StrSubstNo(FieldNotSpecifiedTxt, DocExchServiceSetup.FieldCaption("User Agent")), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(FieldNotSpecifiedTxt, DocExchServiceSetup.FieldCaption("User Agent"));
end;
exit(DocExchServiceSetup."User Agent");
end;
local procedure GetFullURL(PartialURL: Text): Text
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
begin
GetServiceSetUp(DocExchServiceSetup);
if DocExchServiceSetup."Service URL" = '' then begin
Session.LogMessage('0000EZ1', StrSubstNo(FieldNotSpecifiedTxt, DocExchServiceSetup.FieldCaption("Service URL")), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Error(FieldNotSpecifiedTxt, DocExchServiceSetup.FieldCaption("Service URL"));
end;
exit(DocExchServiceSetup."Service URL" + PartialURL);
end;
local procedure GetCheckConnectionURL(): Text
begin
exit(GetFullURL('/account/info'));
end;
local procedure GetPostSalesURL(DocRecRef: RecordRef) URL: Text
begin
OnBeforeGetPostSalesURL(DocRecRef, URL);
if URL <> '' then
exit(URL);
case DocRecRef.Number of
DATABASE::"Sales Invoice Header":
exit(GetPostSalesInvURL());
DATABASE::"Sales Cr.Memo Header":
exit(GetPostSalesCrMemoURL());
else
Error(UnSupportedTableTypeErr, DocRecRef.Number);
end;
end;
procedure GetPostSalesInvURL(): Text
begin
exit(
GetFullURL(StrSubstNo('/documents/dispatcher?documentId=%1&documentProfileId=tradeshift.invoice.ubl.1.0', GetGUID())));
end;
procedure GetPostSalesCrMemoURL(): Text
begin
exit(
GetFullURL(StrSubstNo('/documents/dispatcher?documentId=%1&documentProfileId=tradeshift.creditnote.ubl.1.0', GetGUID())));
end;
local procedure GetDocStatusURL(DocIdentifier: Text): Text
begin
exit(GetFullURL(StrSubstNo('/documents/%1/metadata', DocIdentifier)));
end;
local procedure GetPUTDocURL(FileName: Text): Text
begin
exit(GetFullURL(StrSubstNo('/documentfiles/%1/file?directory=outbox', FileName)));
end;
local procedure GetDispatchDocURL(FileName: Text): Text
begin
exit(GetFullURL(StrSubstNo('/documentfiles/%1/dispatcher?directory=outbox', FileName)));
end;
local procedure GetDispatchErrorsURL(DocIdentifier: Text): Text
begin
exit(GetFullURL(StrSubstNo('/documentfiles/%1/errors', DocIdentifier)));
end;
local procedure GetRetrieveDocsURL(): Text
begin
exit(GetFullURL(StrSubstNo('/documents?stag=inbox&withouttag=BusinessDelivered&limit=%1', GetChunckSize())));
end;
local procedure GetRetrieveDocIDURL(DocIdentifier: Text): Text
begin
exit(GetFullURL(StrSubstNo('/documents/%1', DocIdentifier)));
end;
local procedure GetRetrieveOriginalDocIDURL(DocIdentifier: Text): Text
begin
exit(GetFullURL(StrSubstNo('/documents/%1/original', DocIdentifier)));
end;
local procedure GetSetTagURL(DocIdentifier: Text): Text
begin
exit(GetFullURL(StrSubstNo('/documents/%1/tags/BusinessDelivered', DocIdentifier)));
end;
local procedure GetGUID(): Text
begin
GLBLastUsedGUID := DelChr(DelChr(Format(CreateGuid()), '=', '{'), '=', '}');
exit(GLBLastUsedGUID);
end;
local procedure UrlEncode(UrlComponent: Text): Text
var
HttpUtility: DotNet HttpUtility;
begin
exit(HttpUtility.UrlEncode(UrlComponent));
end;
[NonDebuggable]
local procedure UrlEncode(UrlComponent: SecretText): SecretText
var
HttpUtility: DotNet HttpUtility;
begin
exit(HttpUtility.UrlEncode(UrlComponent.Unwrap()));
end;
local procedure GetChunckSize(): Integer
begin
exit(100);
end;
local procedure GetApiNamespace(): Text
begin
exit('http://tradeshift.com/api/1.0');
end;
local procedure GetPublicNamespace(): Text
begin
exit('http://tradeshift.com/api/public/1.0');
end;
local procedure GetCBCNamespace(): Text
begin
exit('urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2');
end;
local procedure GetErrorXPath(): Text
begin
exit(StrSubstNo('//%1:Message', GetPrefix()));
end;
local procedure GetStatusXPath(): Text
begin
exit(StrSubstNo('//%1:DeliveryState', GetPrefix()));
end;
local procedure GetDocumentIDXPath(): Text
begin
exit(StrSubstNo('.//%1:DocumentId', GetPrefix()));
end;
local procedure GetDocumentTypeXPath(): Text
begin
exit(StrSubstNo('.//%1:DocumentType', GetPrefix()));
end;
local procedure GetDocumentIDForDescriptionXPath(): Text
begin
exit(StrSubstNo('.//%1:ID', GetPrefix()));
end;
local procedure GetEmbeddedDocXPath(): Text
begin
exit(StrSubstNo('//%1:EmbeddedDocumentBinaryObject', GetPrefix()));
end;
local procedure GetPrefix(): Text
begin
exit('newnamespace');
end;
local procedure GetDocumentIDKey(): Text
begin
exit('X-Tradeshift-DocumentId');
end;
[TryFunction]
local procedure TryGetDocumentDescription(Node: DotNet XmlNode;
var
Description: Text)
var
SrchNode: DotNet XmlNode;
begin
Description := '';
XMLDOMMgt.FindNodeWithNamespace(Node, GetDocumentTypeXPath(), GetPrefix(),
GetPublicNamespace(), SrchNode);
Description := MapDocumentType(XMLDOMMgt.GetAttributeValue(SrchNode, 'type'));
Description += ' ' + XMLDOMMgt.FindNodeTextWithNamespace(Node, GetDocumentIDForDescriptionXPath(),
GetPrefix(), GetPublicNamespace());
end;
local procedure MapDocumentType(DocType: Text): Text
var
PurchaseHeader: Record "Purchase Header";
begin
case DocType of
'invoice':
PurchaseHeader."Document Type" := PurchaseHeader."Document Type"::Invoice;
'creditnote':
PurchaseHeader."Document Type" := PurchaseHeader."Document Type"::"Credit Memo";
else
exit('');
end;
exit(Format(PurchaseHeader."Document Type"));
end;
procedure LogActivitySucceeded(RelatedRecordID: RecordID; ActivityDescription: Text; ActivityMessage: Text)
var
ActivityLog: Record "Activity Log";
begin
ActivityLog.LogActivity(RelatedRecordID, ActivityLog.Status::Success, LoggingConstTxt,
ActivityDescription, ActivityMessage);
end;
procedure LogActivityFailed(RelatedRecordID: RecordID; ActivityDescription: Text; ActivityMessage: Text)
var
ActivityMessageVar: Text;
begin
ActivityMessageVar := ActivityMessage;
LogActivityFailedCommon(RelatedRecordID, ActivityDescription, ActivityMessageVar);
end;
procedure LogActivityFailedAndError(RelatedRecordID: RecordID; ActivityDescription: Text; ActivityMessage: Text)
begin
LogActivityFailedCommon(RelatedRecordID, ActivityDescription, ActivityMessage);
if DelChr(ActivityMessage, '<>', ' ') <> '' then
Error(ActivityMessage);
end;
local procedure LogActivityFailedCommon(RelatedRecordID: RecordID; ActivityDescription: Text; var ActivityMessage: Text)
var
ActivityLog: Record "Activity Log";
begin
ActivityMessage := GetLastErrorText + ' ' + ActivityMessage;
ClearLastError();
ActivityLog.LogActivity(RelatedRecordID, ActivityLog.Status::Failed, LoggingConstTxt,
ActivityDescription, ActivityMessage);
if ActivityMessage = '' then
ActivityLog.SetDetailedInfoFromStream(GLBResponseInStream);
Commit();
end;
procedure EnableTraceLog(NewTraceLogEnabled: Boolean)
begin
GLBTraceLogEnabled := NewTraceLogEnabled;
end;
[EventSubscriber(ObjectType::Table, Database::"Service Connection", 'OnRegisterServiceConnection', '', false, false)]
procedure HandleVANRegisterServiceConnection(var ServiceConnection: Record "Service Connection")
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
RecRef: RecordRef;
begin
if not DocExchServiceSetup.Get() then begin
DocExchServiceSetup.Init();
DocExchServiceSetup.Insert();
end;
RecRef.GetTable(DocExchServiceSetup);
if DocExchServiceSetup.Enabled then
ServiceConnection.Status := ServiceConnection.Status::Enabled
else
ServiceConnection.Status := ServiceConnection.Status::Disabled;
ServiceConnection.InsertServiceConnection(
ServiceConnection, RecRef.RecordId(), DocExchServiceSetup.TableCaption(), DocExchServiceSetup."Service URL", PAGE::"Doc. Exch. Service Setup");
end;
[IntegrationEvent(true, false)]
[Scope('OnPrem')]
procedure OnAfterIncomingDocReceivedFromDocExch(var IncomingDocument: Record "Incoming Document")
begin
end;
[IntegrationEvent(false, false)]
local procedure OnGetClientId(var ClientId: Text)
begin
end;
[IntegrationEvent(false, false)]
local procedure OnGetClientSecret(var ClientSecret: Text)
begin
end;
procedure GetExternalDocURL(DocID: Text): Text
var
URLPart: Text;
begin
URLPart := 'www';
if StrPos(GetFullURL(''), SandboxTxt) > 0 then
URLPart := SandboxTxt;
exit(StrSubstNo('https://%1.tradeshift.com/app/Tradeshift.Migration#::conversation/view/%2::', URLPart, DocID));
end;
procedure VerifyPrerequisites(ShowFailure: Boolean): Boolean
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
PageDocExchServiceSetup: Page "Doc. Exch. Service Setup";
Success: Boolean;
IsHandled: Boolean;
begin
IsHandled := false;
OnBeforeVerifyPrerequisites(ShowFailure, Success, IsHandled);
if IsHandled then
exit(Success);
if DocExchServiceSetup.Get() then
if DocExchServiceSetup."Service URL" <> '' then
if DocExchServiceSetup."Auth URL" <> '' then
if DocExchServiceSetup."Token URL" <> '' then
Success := true;
if not Success then
if ShowFailure then
Error(NotConfiguredErr, PageDocExchServiceSetup.Caption());
exit(Success);
end;
local procedure LogTelemetryDocumentSent()
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
begin
GetServiceSetUp(DocExchServiceSetup);
Session.LogMessage('000089R', DocExchServiceDocumentSuccessfullySentTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Session.LogMessage('000089S', DocExchServiceSetup."Service URL", Verbosity::Normal, DataClassification::CustomerContent, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
end;
local procedure LogTelemetryDocumentReceived()
var
DocExchServiceSetup: Record "Doc. Exch. Service Setup";
begin
GetServiceSetUp(DocExchServiceSetup);
Session.LogMessage('000089T', DocExchServiceDocumentSuccessfullyReceivedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
Session.LogMessage('000089U', DocExchServiceSetup."Service URL", Verbosity::Normal, DataClassification::CustomerContent, TelemetryScope::ExtensionPublisher, 'Category', TelemetryCategoryTok);
end;
[IntegrationEvent(false, false)]
local procedure OnBeforeSendActivateAppNotification(var IsHandled: Boolean)
begin
end;
[IntegrationEvent(false, false)]
local procedure OnBeforeSetURLsToDefault(var DocExchServiceSetup: Record "Doc. Exch. Service Setup"; Sandbox: Boolean; var IsHandled: Boolean)
begin
end;
[IntegrationEvent(false, false)]
local procedure OnBeforeVerifyPrerequisites(ShowFailure: Boolean; var Success: Boolean; var IsHandled: Boolean)
begin
end;
[IntegrationEvent(false, false)]
local procedure OnBeforeCheckDocumentStatus(DocRecRef: RecordRef; var IsHandled: Boolean)
begin
end;
[IntegrationEvent(true, false)]
local procedure OnBeforeGetPostSalesURL(DocRecRef: RecordRef; var URL: Text)
begin
end;
}