diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/ExtensionLogo.png b/Apps/W1/EDocumentConnectors/SignUp/app/ExtensionLogo.png new file mode 100644 index 0000000000..4d2c9a626c Binary files /dev/null and b/Apps/W1/EDocumentConnectors/SignUp/app/ExtensionLogo.png differ diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/app.json b/Apps/W1/EDocumentConnectors/SignUp/app/app.json new file mode 100644 index 0000000000..5d02eb4470 --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/app.json @@ -0,0 +1,47 @@ +{ + "id": "b56171bd-9a8e-47ad-a527-99f476d5af83", + "name": "E-Document Connector - SignUp", + "publisher": "Microsoft", + "brief": "E-Document Connector - SignUp", + "description": "E-Document Connector - SignUp", + "version": "26.0.0.0", + "privacyStatement": "https://go.microsoft.com/fwlink/?LinkId=724009", + "EULA": "https://go.microsoft.com/fwlink/?linkid=2009120", + "help": "https://go.microsoft.com/fwlink/?linkid=2204541", + "url": "https://go.microsoft.com/fwlink/?LinkId=724011", + "logo": "ExtensionLogo.png", + "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2206603", + "dependencies": [ + { + "id": "e1d97edc-c239-46b4-8d84-6368bdf67c8b", + "name": "E-Document Core", + "publisher": "Microsoft", + "version": "26.0.0.0" + }, + { + "id": "d852a468-263e-49e5-bfda-f09e33342b89", + "name": "E-Documents Connector with External Endpoints", + "publisher": "Microsoft", + "version": "26.0.0.0" + } + ], + "internalsVisibleTo": [], + "screenshots": [], + "platform": "26.0.0.0", + "idRanges": [ + { + "from": 6380, + "to": 6389 + } + ], + "resourceExposurePolicy": { + "allowDebugging": true, + "allowDownloadingSource": true, + "includeSourceInSymbolFile": true + }, + "application": "26.0.0.0", + "target": "OnPrem", + "features": [ + "TranslationFile" + ] +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/APIRequests.Codeunit.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/APIRequests.Codeunit.al new file mode 100644 index 0000000000..74059d2555 --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/APIRequests.Codeunit.al @@ -0,0 +1,110 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; + +using Microsoft.EServices.EDocument; +using System.Utilities; + + +codeunit 6380 APIRequests +{ + Access = Internal; + + var + APIRequestsImpl: Codeunit APIRequestsImpl; + + #region public methods + + /// + /// The method sends a file to the API. + /// https://[BASEURL]/api/Peppol + /// + /// TempBlob + /// EDocument table + /// Http Request Message + /// Http Response Message + /// True if successfully completed + procedure SendFilePostRequest(var TempBlob: Codeunit "Temp Blob"; EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + exit(this.APIRequestsImpl.SendFilePostRequest(TempBlob, EDocument, HttpRequestMessage, HttpResponseMessage)); + end; + + /// + /// The method checks the status of the sent document. + /// https://[BASE URL]/api/Peppol/status?peppolInstanceId= + /// + /// EDocument table + /// Http Request Message + /// Http Response Message + /// True if successfully completed + procedure GetSentDocumentStatus(EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + exit(this.APIRequestsImpl.GetSentDocumentStatus(EDocument, HttpRequestMessage, HttpResponseMessage)); + end; + + /// + /// The method modifies the document. + /// https://[BASE URL]/api/Peppol/outbox?peppolInstanceId= + /// + /// EDocument table + /// Http Request Message + /// Http Response Message + /// True if successfully completed + procedure PatchDocument(EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + exit(this.APIRequestsImpl.PatchDocument(EDocument, HttpRequestMessage, HttpResponseMessage)); + end; + + /// + /// The method gets the received document request. + /// https://[BASE URL]/api/Peppol/Inbox?peppolId= + /// + /// Http Request Message + /// Http Response Message + /// True if successfully completed + procedure GetReceivedDocumentsRequest(var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + exit(this.APIRequestsImpl.GetReceivedDocumentsRequest(HttpRequestMessage, HttpResponseMessage)); + end; + + /// + /// The method gets the target document request. + /// https://[BASE URL]/api/Peppol/inbox-document?peppolId= + /// + /// Document ID + /// Http Request Message + /// Http Response Message + /// True if successfully completed + procedure GetTargetDocumentRequest(DocumentId: Text; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + exit(this.APIRequestsImpl.GetTargetDocumentRequest(DocumentId, HttpRequestMessage, HttpResponseMessage)); + end; + + /// + /// The method modifies the received document. + /// // https://[BASE URL]/api/Peppol/inbox?peppolInstanceId= + /// + /// EDocument table + /// Http Request Message + /// Http Response Message + /// True if successfully completed + procedure PatchReceivedDocument(EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + exit(this.APIRequestsImpl.PatchReceivedDocument(EDocument, HttpRequestMessage, HttpResponseMessage)); + end; + + /// + /// The method gets the marketplace credentials. + /// + /// Http Request Message + /// Http Response Message + /// True if successfully completed + procedure GetMarketPlaceCredentials(var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + exit(this.APIRequestsImpl.GetMarketPlaceCredentials(HttpRequestMessage, HttpResponseMessage)); + end; + + #endregion +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/APIRequestsImpl.Codeunit.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/APIRequestsImpl.Codeunit.al new file mode 100644 index 0000000000..bb47a40cff --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/APIRequestsImpl.Codeunit.al @@ -0,0 +1,286 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; + +using Microsoft.EServices.EDocument; +using Microsoft.Foundation.Company; +using Microsoft.Sales.Customer; +using System.Security.Authentication; +using System.Text; +using System.Utilities; +using System.Xml; + +codeunit 6389 APIRequestsImpl +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + #region variables + + var + MissingSetupErr: Label 'Connection Setup is missing'; + MissingSetupMessageErr: Label 'You must set up service integration in the e-document service card.'; + MissingSetupNavigationActionErr: Label 'Show E-Document Services'; + UnSupportedDocumentTypeTxt: Label 'Document %1 is not supported.', Comment = '%1 = EDocument Type', Locked = true; + SenderReceiverPrefixTxt: Label 'iso6523-actorid-upis::', Locked = true; + ContentTypeTxt: Label 'Content-Type', Locked = true; + ApplicationJsonTxt: Label 'application/json', Locked = true; + AuthorizationTxt: Label 'Authorization', Locked = true; + AcceptTxt: Label 'Accept', Locked = true; + AllTxt: Label '*/*', Locked = true; + ApplicationResponseTxt: Label 'ApplicationResponse', Locked = true; + InvoiceTxt: Label 'Invoice', Locked = true; + PaymentReminderTxt: Label 'PaymentReminder', Locked = true; + DocumentTypeTxt: Label 'documentType', Locked = true; + ReceiverTxt: Label 'receiver', Locked = true; + SenderTxt: Label 'sender', Locked = true; + SenderCountryCodeTxt: Label 'senderCountryCode', Locked = true; + DocumentIdTxt: Label 'documentId', Locked = true; + DocumentIdValueTxt: Label 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1', Locked = true; + DocumentIdSchemeTxt: Label 'documentIdScheme', Locked = true; + BusdoxDocIdQNSTxt: Label 'busdox-docid-qns', Locked = true; + ProcessIdTxt: Label 'processId', Locked = true; + ProcessIdValueTxt: Label 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0', Locked = true; + ProcessIdSchemeTxt: Label 'processIdScheme', Locked = true; + ProcessIdSchemeValueTxt: Label 'cenbii-procid-ubl', Locked = true; + SendModeTxt: Label 'sendMode', Locked = true; + DocumentTxt: Label 'document', Locked = true; + + #endregion + + #region public methods + + // https:///api/Peppol + procedure SendFilePostRequest(var TempBlob: Codeunit "Temp Blob"; EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + var + ConnectionSetup: Record ConnectionSetup; + HttpContent: HttpContent; + Payload: Text; + begin + Payload := this.XmlToTxt(TempBlob); + if Payload = '' then + exit; + + this.InitRequest(HttpRequestMessage, HttpResponseMessage); + ConnectionSetup.SetLoadFields("Company Id", "Environment Type", ServiceURL); + this.GetSetup(ConnectionSetup); + + HttpRequestMessage := this.PrepareRequestMsg("Http Request Type"::POST, ConnectionSetup.ServiceURL + '/api/Peppol'); + this.PrepareContent(HttpContent, Payload, EDocument, ConnectionSetup); + HttpRequestMessage.Content(HttpContent); + exit(this.SendRequest(HttpRequestMessage, HttpResponseMessage)); + end; + + // https:///api/Peppol/status?peppolInstanceId= + procedure GetSentDocumentStatus(EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + var + ConnectionSetup: Record ConnectionSetup; + begin + this.InitRequest(HttpRequestMessage, HttpResponseMessage); + ConnectionSetup.SetLoadFields(ServiceURL); + this.GetSetup(ConnectionSetup); + + HttpRequestMessage := this.PrepareRequestMsg("Http Request Type"::GET, ConnectionSetup.ServiceURL + '/api/Peppol/status?peppolInstanceId=' + EDocument."Document Id"); + exit(this.SendRequest(HttpRequestMessage, HttpResponseMessage)); + end; + + // https:///api/Peppol/outbox?peppolInstanceId= + procedure PatchDocument(EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + var + ConnectionSetup: Record ConnectionSetup; + begin + this.InitRequest(HttpRequestMessage, HttpResponseMessage); + ConnectionSetup.SetLoadFields(ServiceURL); + this.GetSetup(ConnectionSetup); + + HttpRequestMessage := this.PrepareRequestMsg("Http Request Type"::PATCH, ConnectionSetup.ServiceURL + '/api/Peppol/outbox?peppolInstanceId=' + EDocument."Document Id"); + exit(this.SendRequest(HttpRequestMessage, HttpResponseMessage)); + end; + + // https:///api/Peppol/Inbox?peppolId= + procedure GetReceivedDocumentsRequest(var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + var + ConnectionSetup: Record ConnectionSetup; + begin + this.InitRequest(HttpRequestMessage, HttpResponseMessage); + ConnectionSetup.SetLoadFields(ServiceURL, "Company Id"); + this.GetSetup(ConnectionSetup); + + HttpRequestMessage := this.PrepareRequestMsg("Http Request Type"::GET, ConnectionSetup.ServiceURL + '/api/Peppol/Inbox?peppolId=' + this.SenderReceiverPrefixTxt + ConnectionSetup."Company Id"); + exit(this.SendRequest(HttpRequestMessage, HttpResponseMessage)); + end; + + // https:///api/Peppol/inbox-document?peppolId= + procedure GetTargetDocumentRequest(DocumentId: Text; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + var + ConnectionSetup: Record ConnectionSetup; + begin + this.InitRequest(HttpRequestMessage, HttpResponseMessage); + ConnectionSetup.SetLoadFields(ServiceURL, "Company Id"); + this.GetSetup(ConnectionSetup); + + HttpRequestMessage := this.PrepareRequestMsg("Http Request Type"::GET, ConnectionSetup.ServiceURL + '/api/Peppol/inbox-document?peppolId=' + this.SenderReceiverPrefixTxt + ConnectionSetup."Company Id" + '&peppolInstanceId=' + DocumentId); + exit(this.SendRequest(HttpRequestMessage, HttpResponseMessage)); + end; + + // https:///api/Peppol/inbox?peppolInstanceId= + procedure PatchReceivedDocument(EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + var + ConnectionSetup: Record ConnectionSetup; + begin + this.InitRequest(HttpRequestMessage, HttpResponseMessage); + ConnectionSetup.SetLoadFields(ServiceURL); + this.GetSetup(ConnectionSetup); + + HttpRequestMessage := this.PrepareRequestMsg("Http Request Type"::PATCH, ConnectionSetup.ServiceURL + '/api/Peppol/inbox?peppolInstanceId=' + EDocument."Document Id"); + exit(this.SendRequest(HttpRequestMessage, HttpResponseMessage)); + end; + + procedure GetMarketPlaceCredentials(var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + var + Authentication: Codeunit Authentication; + begin + this.InitRequest(HttpRequestMessage, HttpResponseMessage); + + HttpRequestMessage := this.PrepareRequestMsg("Http Request Type"::POST, Authentication.GetRootUrl() + '/api/Registration/init?EntraTenantId=' + Authentication.GetBCInstanceIdentifier()); + exit(this.SendRequest(HttpRequestMessage, HttpResponseMessage, true)); + end; + + #endregion + + #region local methods + + local procedure InitRequest(var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage) + begin + Clear(HttpRequestMessage); + Clear(HttpResponseMessage); + end; + + local procedure GetSetup(var ConnectionSetup: Record ConnectionSetup) + var + MissingSetupErrorInfo: ErrorInfo; + begin + if not IsNullGuid(ConnectionSetup.SystemId) then + exit; + + if not ConnectionSetup.Get() then begin + MissingSetupErrorInfo.Title := this.MissingSetupErr; + MissingSetupErrorInfo.Message := this.MissingSetupMessageErr; + MissingSetupErrorInfo.PageNo := Page::"E-Document Services"; + MissingSetupErrorInfo.AddNavigationAction(this.MissingSetupNavigationActionErr); + Error(MissingSetupErrorInfo); + end; + end; + + local procedure PrepareContent(var HttpContent: HttpContent; Payload: Text; EDocument: Record "E-Document"; ConnectionSetup: Record ConnectionSetup) + var + ContentText: Text; + HttpHeaders: HttpHeaders; + begin + Clear(HttpContent); + ContentText := this.PrepareContentForSend(this.GetDocumentType(EDocument), ConnectionSetup."Company Id", this.GetCustomerID(EDocument), this.GetSenderCountryCode(), Payload, ConnectionSetup."Environment Type"); + HttpContent.WriteFrom(ContentText); + HttpContent.GetHeaders(HttpHeaders); + if HttpHeaders.Contains(this.ContentTypeTxt) then + HttpHeaders.Remove(this.ContentTypeTxt); + HttpHeaders.Add(this.ContentTypeTxt, this.ApplicationJsonTxt); + end; + + local procedure SendRequest(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + this.SendRequest(HttpRequestMessage, HttpResponseMessage, false); + end; + + local procedure SendRequest(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage; RootRequest: Boolean): Boolean + var + Authentication: Codeunit Authentication; + HttpClient: HttpClient; + HttpHeaders: HttpHeaders; + begin + HttpRequestMessage.GetHeaders(HttpHeaders); + if RootRequest then + HttpHeaders.Add(this.AuthorizationTxt, Authentication.GetRootBearerAuthToken()) + else + HttpHeaders.Add(this.AuthorizationTxt, Authentication.GetBearerAuthToken()); + exit(HttpClient.Send(HttpRequestMessage, HttpResponseMessage)); + end; + + local procedure PrepareRequestMsg(HttpRequestType: Enum "Http Request Type"; Uri: Text) RequestMessage: HttpRequestMessage + var + HttpHeaders: HttpHeaders; + begin + RequestMessage.Method(Format(HttpRequestType)); + RequestMessage.SetRequestUri(Uri); + RequestMessage.GetHeaders(HttpHeaders); + HttpHeaders.Add(this.AcceptTxt, this.AllTxt); + end; + + local procedure XmlToTxt(var TempBlob: Codeunit "Temp Blob"): Text + var + XMLDOMManagement: Codeunit "XML DOM Management"; + Content: Text; + begin + XMLDOMManagement.TryGetXMLAsText(TempBlob.CreateInStream(TextEncoding::UTF8), Content); + exit(Content); + end; + + local procedure GetDocumentType(EDocument: Record "E-Document"): Text + begin + if EDocument.Direction = EDocument.Direction::Incoming then + exit(this.ApplicationResponseTxt); + + case EDocument."Document Type" of + "E-Document Type"::"Sales Invoice", "E-Document Type"::"Sales Credit Memo", "E-Document Type"::"Service Invoice", "E-Document Type"::"Service Credit Memo": + exit(this.InvoiceTxt); + "E-Document Type"::"Issued Finance Charge Memo", "E-Document Type"::"Issued Reminder": + exit(this.PaymentReminderTxt); + else + Error(this.UnSupportedDocumentTypeTxt, EDocument."Document Type"); + end; + end; + + local procedure GetCustomerID(EDocument: Record "E-Document"): Text[50] + var + Customer: Record Customer; + begin + Customer.SetLoadFields("Service Participant Id"); + Customer.Get(EDocument."Bill-to/Pay-to No."); + Customer.TestField("Service Participant Id"); + exit(Customer."Service Participant Id"); + end; + + local procedure GetSenderCountryCode(): Text + var + CompanyInformation: Record "Company Information"; + begin + CompanyInformation.SetLoadFields("Country/Region Code"); + CompanyInformation.Get(); + CompanyInformation.TestField("Country/Region Code"); + exit(CompanyInformation."Country/Region Code"); + end; + + local procedure PrepareContentForSend(DocumentType: Text; SendingCompanyID: Text; RecieverCompanyID: Text; SenderCountryCode: Text; Payload: Text; SendMode: Enum EnvironmentType): Text + var + Base64Convert: Codeunit "Base64 Convert"; + JsonObject: JsonObject; + ContentText: Text; + begin + JsonObject.Add(this.DocumentTypeTxt, DocumentType); + JsonObject.Add(this.ReceiverTxt, this.SenderReceiverPrefixTxt + RecieverCompanyID); + JsonObject.Add(this.SenderTxt, this.SenderReceiverPrefixTxt + SendingCompanyID); + JsonObject.Add(this.SenderCountryCodeTxt, SenderCountryCode); + JsonObject.Add(this.DocumentIdTxt, this.DocumentIdValueTxt); + JsonObject.Add(this.DocumentIdSchemeTxt, this.BusdoxDocIdQNSTxt); + JsonObject.Add(this.ProcessIdTxt, this.ProcessIdValueTxt); + JsonObject.Add(this.ProcessIdSchemeTxt, this.ProcessIdSchemeValueTxt); + JsonObject.Add(this.SendModeTxt, Format(SendMode)); + JsonObject.Add(this.DocumentTxt, Base64Convert.ToBase64(Payload)); + JsonObject.WriteTo(ContentText); + exit(ContentText); + end; + + #endregion +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/Authentication.Codeunit.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/Authentication.Codeunit.al new file mode 100644 index 0000000000..4cab43fe24 --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/Authentication.Codeunit.al @@ -0,0 +1,98 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; + +codeunit 6381 Authentication +{ + Access = Internal; + + var + AuthenticationImpl: Codeunit AuthenticationImpl; + + /// + /// The method initializes the connection setup. + /// + procedure InitConnectionSetup() + begin + this.AuthenticationImpl.InitConnectionSetup(); + end; + + /// + /// The method returns the onboarding URL. + /// + /// Onboarding URL + procedure GetRootOnboardingUrl(): Text + begin + exit(this.AuthenticationImpl.GetRootOnboardingUrl()); + end; + + /// + /// The method creates the client credentials. + /// + [NonDebuggable] + procedure CreateClientCredentials() + begin + this.AuthenticationImpl.CreateClientCredentials(); + end; + + /// + /// The method returns the bearer authentication text. + /// + /// Bearer authentication token + procedure GetBearerAuthToken(): SecretText; + begin + exit(this.AuthenticationImpl.GetBearerAuthToken()); + end; + + /// + /// The method returns the root bearer authentication token. + /// + /// Root bearer authentication token + procedure GetRootBearerAuthToken(): SecretText; + begin + exit(this.AuthenticationImpl.GetRootBearerAuthToken()); + end; + + /// + /// The mehod saves the token to the storage. + /// + /// Token Key + /// Token + [NonDebuggable] + procedure StorageSet(var TokenKey: Guid; Value: Text) + begin + this.AuthenticationImpl.StorageSet(TokenKey, Value); + end; + + /// + /// The mehod saves the token to the storage. + /// + /// Token Key + /// Token + [NonDebuggable] + procedure StorageSet(var TokenKey: Guid; Value: SecretText) + begin + this.AuthenticationImpl.StorageSet(TokenKey, Value); + end; + + /// + /// The method returns BC instance identifier. + /// + /// Identifier + procedure GetBCInstanceIdentifier() Identifier: Text + begin + exit(this.AuthenticationImpl.GetBCInstanceIdentifier()); + end; + + /// + /// The method returns the root URL. + /// + /// + [NonDebuggable] + procedure GetRootUrl() ReturnValue: Text + begin + exit(this.AuthenticationImpl.GetRootUrl()); + end; +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/AuthenticationImpl.Codeunit.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/AuthenticationImpl.Codeunit.al new file mode 100644 index 0000000000..eb67cae275 --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/AuthenticationImpl.Codeunit.al @@ -0,0 +1,344 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; + +using System.Azure.KeyVault; +using System.Environment; +using System.Security.Authentication; +using System.Azure.Identity; + +codeunit 6390 AuthenticationImpl +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + #region variables + var + ConnectionSetup: Record ConnectionSetup; + HelpersImpl: Codeunit HelpersImpl; + BearerTxt: Label 'Bearer %1', Comment = '%1 = text value', Locked = true; + AuthURLTxt: Label 'https://login.microsoftonline.com/%1/oauth2/token', Comment = '%1 Entra Tenant Id', Locked = true; + ProdTenantIdTxt: Label '0d725623-dc26-484f-a090-b09d2003d092', Locked = true; + ProdServiceAPITxt: Label 'https://edoc.exflow.io', Locked = true; + ErrorTokenLbl: Label 'Unable to fetch a root token.'; + ErrorUnableToCreateClientCredentialsLbl: Label 'Unable to create client credentials.'; + ClientIdTxt: Label 'clientId', Locked = true; + ClientSecretTxt: Label 'clientSecret', Locked = true; + SignupRootUrlTxt: Label 'signup-root-url', Locked = true; + RootIdTxt: Label '-root-id', Locked = true; + SignUpRootSecretTxt: Label 'signup-root-secret', Locked = true; + SignUpRootTenantTxt: Label 'signup-root-tenant', Locked = true; + SignUpAccessTokenKeyTxt: Label '{E45BB975-E67B-4A87-AC24-D409A5EF8301}', Locked = true; + + #endregion + + #region public methods + + procedure InitConnectionSetup() + begin + if this.ConnectionSetup.Get() then + exit; + + this.ConnectionSetup."Authentication URL" := this.AuthURLTxt; + this.ConnectionSetup.ServiceURL := this.ProdServiceAPITxt; + this.StorageSet(this.ConnectionSetup."Client Tenant", this.ProdTenantIdTxt); + this.ConnectionSetup.Insert(); + end; + + procedure GetRootOnboardingUrl(): Text + begin + exit(this.GetRootUrl() + '/supm/landingpage?EntraTenantId=' + this.GetBCInstanceIdentifier()); + end; + + [NonDebuggable] + procedure CreateClientCredentials() + var + HttpRequestMessage: HttpRequestMessage; + HttpResponseMessage: HttpResponseMessage; + ClientId, Response : Text; + ClientSecret: SecretText; + begin + if not this.GetClientCredentials(HttpRequestMessage, HttpResponseMessage) then + Error(this.ErrorUnableToCreateClientCredentialsLbl); + + if not HttpResponseMessage.Content.ReadAs(Response) then + exit; + + ClientId := this.HelpersImpl.GetJsonValueFromText(Response, this.ClientIdTxt); + ClientSecret := this.HelpersImpl.GetJsonValueFromText(Response, this.ClientSecretTxt); + + if (ClientId <> '') and (not ClientSecret.IsEmpty()) then + this.SaveClientCredentials(ClientId, ClientSecret); + end; + + procedure GetBearerAuthToken(): SecretText; + begin + exit(SecretStrSubstNo(this.BearerTxt, this.GetAuthToken())); + end; + + procedure GetRootBearerAuthToken(): SecretText; + begin + exit(SecretStrSubstNo(this.BearerTxt, this.GetRootAuthToken())); + end; + + [NonDebuggable] + procedure StorageSet(var TokenKey: Guid; Value: Text): Boolean + var + ModuleDataScope: DataScope; + begin + ModuleDataScope := ModuleDataScope::Module; + this.ValidateValueKey(TokenKey); + + if Value = '' then begin + if IsolatedStorage.Contains(TokenKey, ModuleDataScope) then + exit(IsolatedStorage.Delete(TokenKey, ModuleDataScope)) + end else + exit(IsolatedStorage.Set(TokenKey, Value, ModuleDataScope)); + end; + + procedure StorageSet(var TokenKey: Guid; Value: SecretText): Boolean + begin + exit(this.StorageSet(TokenKey, Value, DataScope::Module)); + end; + + procedure GetBCInstanceIdentifier() Identifier: Text + var + AADTenantID, AADDomainName : Text; + begin + Identifier := '10000000-d8ef-4dfb-b761-ffb073057794'; // Hardcoded fake during testing only + + if this.GetAADTenantInformation(AADTenantID, AADDomainName) then + Identifier := AADTenantID; + end; + + [NonDebuggable] + procedure GetRootUrl() ReturnValue: Text + begin + if this.FetchSecretFromKeyVault(this.SignupRootUrlTxt, ReturnValue) then + exit; + + if not this.ConnectionSetup.GetSetup() then + exit; + + this.ConnectionSetup.TestField("Root Market URL"); + ReturnValue := this.StorageGetText(this.ConnectionSetup."Root Market URL", DataScope::Module); + end; + + #endregion + + #region local methods + + local procedure GetAuthToken() AccessToken: SecretText; + var + HttpError: Text; + begin + AccessToken := this.StorageGet(this.SignUpAccessTokenKeyTxt, DataScope::Company); + + if this.HelpersImpl.IsTokenValid(AccessToken) then + exit; + + if not this.RefreshAccessToken(HttpError) then + Error(HttpError); + + exit(this.StorageGet(this.SignUpAccessTokenKeyTxt, DataScope::Company)); + end; + + local procedure GetRootAuthToken() ReturnValue: SecretText; + begin + if not this.GetRootAccessToken(ReturnValue) then + Error(this.ErrorTokenLbl); + end; + + local procedure SaveClientCredentials(ClientId: Text; ClientSecret: SecretText) + begin + Clear(this.ConnectionSetup); + + this.ConnectionSetup.GetSetup(); + this.StorageSet(this.ConnectionSetup."Client ID", ClientId); + this.StorageSet(this.ConnectionSetup."Client Secret", ClientSecret); + this.ConnectionSetup.Modify(); + + Clear(this.ConnectionSetup); + end; + + [NonDebuggable] + local procedure GetClientCredentials(var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + var + APIRequests: Codeunit APIRequests; + begin + APIRequests.GetMarketPlaceCredentials(HttpRequestMessage, HttpResponseMessage); + + if not HttpResponseMessage.IsSuccessStatusCode() then + exit; + + exit(this.HelpersImpl.ParseJsonString(HttpResponseMessage.Content) <> ''); + end; + + [NonDebuggable] + local procedure RefreshAccessToken(var HttpError: Text): Boolean; + var + SecretToken: SecretText; + begin + if not this.GetClientAccessToken(SecretToken) then begin + HttpError := GetLastErrorText(); + exit; + end; + + exit(this.SaveSignUpAccessToken(DataScope::Company, SecretToken)); + end; + + [NonDebuggable] + local procedure GetRootAccessToken(var AccessToken: SecretText): Boolean + begin + exit(this.GetAccessToken(AccessToken, this.GetRootId(), this.GetRootSecret(), this.GetRootTenant())); + end; + + [NonDebuggable] + local procedure GetClientAccessToken(var AccessToken: SecretText): Boolean + var + ModuleDataScope: DataScope; + begin + ModuleDataScope := ModuleDataScope::Module; + this.ConnectionSetup.GetSetup(); + + exit(this.GetAccessToken(AccessToken, this.StorageGetText(this.ConnectionSetup."Client ID", ModuleDataScope), + this.StorageGet(this.ConnectionSetup."Client Secret", ModuleDataScope), + this.StorageGetText(this.ConnectionSetup."Client Tenant", ModuleDataScope))); + end; + + [NonDebuggable] + local procedure GetAccessToken(var AccessToken: SecretText; ClientId: Text; ClientSecret: SecretText; ClientTenant: Text) Success: Boolean + var + OAuth2: Codeunit OAuth2; + begin + Success := OAuth2.AcquireTokenWithClientCredentials(ClientId, ClientSecret, StrSubstNo(this.ConnectionSetup."Authentication URL", ClientTenant), '', ClientId, AccessToken); + exit(Success and not AccessToken.IsEmpty()); + end; + + local procedure StorageSet(var TokenKey: Guid; Value: SecretText; TokenDataScope: DataScope): Boolean + begin + this.ValidateValueKey(TokenKey); + + if Value.IsEmpty() then begin + if IsolatedStorage.Contains(TokenKey, TokenDataScope) then + exit(IsolatedStorage.Delete(TokenKey, TokenDataScope)) + end else + exit(IsolatedStorage.Set(TokenKey, Value, TokenDataScope)); + end; + + local procedure StorageGet(TokenKey: Text; TokenDataScope: DataScope) TokenValueAsSecret: SecretText + begin + if not this.StorageContains(TokenKey, TokenDataScope) then + exit(TokenValueAsSecret); + + IsolatedStorage.Get(TokenKey, TokenDataScope, TokenValueAsSecret); + end; + + [NonDebuggable] + local procedure StorageGetText(TokenKey: Text; TokenDataScope: DataScope) TokenValue: Text + begin + if not this.StorageContains(TokenKey, TokenDataScope) then + exit(TokenValue); + + IsolatedStorage.Get(TokenKey, TokenDataScope, TokenValue); + end; + + local procedure SaveSignUpAccessToken(TokenDataScope: DataScope; AccessToken: SecretText): Boolean + var + SignUpAccessTokenKey: Guid; + begin + SignUpAccessTokenKey := this.GetSignUpAccessTokenKey(); + exit(this.StorageSet(SignUpAccessTokenKey, AccessToken, TokenDataScope)); + end; + + local procedure StorageContains(TokenKey: Text; TokenDataScope: DataScope): Boolean + begin + exit(IsolatedStorage.Contains(TokenKey, TokenDataScope)); + end; + + local procedure ValidateValueKey(var ValueKey: Guid) + begin + if IsNullGuid(ValueKey) then + ValueKey := CreateGuid(); + end; + + local procedure GetSignUpAccessTokenKey() SignUpAccessTokenKey: Guid + begin + Evaluate(SignUpAccessTokenKey, this.SignUpAccessTokenKeyTxt); + end; + + [NonDebuggable] + local procedure GetRootId() ReturnValue: Text + begin + if this.FetchSecretFromKeyVault(this.RootIdTxt, ReturnValue) then + exit; + + if not this.ConnectionSetup.GetSetup() then + exit; + + this.ConnectionSetup.TestField("Root App ID"); + ReturnValue := this.StorageGetText(this.ConnectionSetup."Root App ID", DataScope::Module); + end; + + [NonDebuggable] + local procedure GetRootSecret() ReturnValue: Text + begin + if this.FetchSecretFromKeyVault(this.SignUpRootSecretTxt, ReturnValue) then + exit; + + if not this.ConnectionSetup.GetSetup() then + exit; + + this.ConnectionSetup.TestField("Root Secret"); + ReturnValue := this.StorageGetText(this.ConnectionSetup."Root Secret", DataScope::Module); + end; + + [NonDebuggable] + local procedure GetRootTenant() ReturnValue: Text + begin + if this.FetchSecretFromKeyVault(this.SignUpRootTenantTxt, ReturnValue) then + exit; + + if not this.ConnectionSetup.GetSetup() then + exit; + + this.ConnectionSetup.TestField("Root Tenant"); + ReturnValue := this.StorageGetText(this.ConnectionSetup."Root Tenant", DataScope::Module); + end; + + [NonDebuggable] + local procedure FetchSecretFromKeyVault(KeyName: Text; var KeyValue: Text): Boolean + var + AzureKeyVault: Codeunit "Azure Key Vault"; + EnvironmentInformation: Codeunit "Environment Information"; + begin + if EnvironmentInformation.IsSaaSInfrastructure() then + exit(AzureKeyVault.GetAzureKeyVaultSecret(KeyName, KeyValue)); + end; + + local procedure GetAADTenantInformation(var AADTenantID: Text; var AADDomainName: Text): Boolean + begin + exit(this.GetAADTenantID(AADTenantID) and this.GetAADDomainName(AADDomainName)); + end; + + [TryFunction] + local procedure GetAADTenantID(var AADTenantID: Text) + var + AzureADTenant: Codeunit "Azure AD Tenant"; + begin + AADTenantID := AzureADTenant.GetAadTenantId(); + end; + + [TryFunction] + local procedure GetAADDomainName(var AADDomainName: Text) + var + AzureADTenant: Codeunit "Azure AD Tenant"; + begin + AADDomainName := AzureADTenant.GetAadTenantId(); + end; + + #endregion +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/Connection.Codeunit.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/Connection.Codeunit.al new file mode 100644 index 0000000000..69d6451946 --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/Connection.Codeunit.al @@ -0,0 +1,76 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; + +using Microsoft.EServices.EDocument; +using System.Utilities; + +codeunit 6382 Connection +{ + Access = Internal; + + var + ConnectionImpl: Codeunit ConnectionImpl; + + /// + /// The methods sends a file to the API. + /// + /// Content + /// E-Document record + /// Http Request Message + /// Http Response Message + /// True - if completed successfully + procedure SendFilePostRequest(var TempBlob: Codeunit "Temp Blob"; var EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + exit(this.ConnectionImpl.SendFilePostRequest(TempBlob, EDocument, HttpRequestMessage, HttpResponseMessage)); + end; + + /// + /// The method checks the status of the document. + /// + /// E-Document record + /// HttpRequestMessage + /// HttpResponseMessage + /// True - if completed successfully + procedure CheckDocumentStatus(var EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + exit(this.ConnectionImpl.CheckDocumentStatus(EDocument, HttpRequestMessage, HttpResponseMessage)); + end; + + /// + /// The method gets received documents. + /// + /// HttpRequestMessage + /// HttpResponseMessage + /// True - if completed successfully + procedure GetReceivedDocuments(var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + exit(this.ConnectionImpl.GetReceivedDocuments(HttpRequestMessage, HttpResponseMessage)); + end; + + /// + /// The method gets the target document. + /// + /// DocumentId + /// HttpRequestMessage + /// HttpResponseMessage + /// True - if completed successfully + procedure GetTargetDocumentRequest(DocumentId: Text; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + exit(this.ConnectionImpl.GetTargetDocumentRequest(DocumentId, HttpRequestMessage, HttpResponseMessage)); + end; + + /// + /// The method removes the document from received. + /// + /// E-Document record + /// HttpRequestMessage + /// HttpResponseMessage + /// True - if completed successfully + procedure RemoveDocumentFromReceived(EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + exit(this.ConnectionImpl.RemoveDocumentFromReceived(EDocument, HttpRequestMessage, HttpResponseMessage)); + end; +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/ConnectionImpl.Codeunit.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/ConnectionImpl.Codeunit.al new file mode 100644 index 0000000000..520cd055c6 --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/ConnectionImpl.Codeunit.al @@ -0,0 +1,116 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; + +using Microsoft.EServices.EDocument; +using Microsoft.Purchases.Document; +using Microsoft.Purchases.Posting; +using System.Utilities; + +codeunit 6391 ConnectionImpl +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + Permissions = tabledata "E-Document" = m; + + #region variables + + var + APIRequests: Codeunit APIRequests; + HelpersImpl: Codeunit HelpersImpl; + UnsuccessfulResponseErr: Label 'There was an error sending the request. Response code: %1 and error message: %2', Comment = '%1 - http response status code, e.g. 400, %2- error message'; + EnvironmentBlocksErr: Label 'The request to send documents has been blocked. To resolve the problem, enable outgoing HTTP requests for the E-Document apps on the Extension Management page.'; + + #endregion + + #region public methods + + procedure SendFilePostRequest(var TempBlob: Codeunit "Temp Blob"; var EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + this.APIRequests.SendFilePostRequest(TempBlob, EDocument, HttpRequestMessage, HttpResponseMessage); + exit(this.CheckIfSuccessfulRequest(EDocument, HttpResponseMessage)); + end; + + procedure CheckDocumentStatus(var EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + this.APIRequests.GetSentDocumentStatus(EDocument, HttpRequestMessage, HttpResponseMessage); + exit(this.CheckIfSuccessfulRequest(EDocument, HttpResponseMessage)); + end; + + procedure GetReceivedDocuments(var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + this.APIRequests.GetReceivedDocumentsRequest(HttpRequestMessage, HttpResponseMessage); + + if not HttpResponseMessage.IsSuccessStatusCode() then + exit; + + exit(this.HelpersImpl.ParseJsonString(HttpResponseMessage.Content) <> ''); + end; + + procedure GetTargetDocumentRequest(DocumentId: Text; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + this.APIRequests.GetTargetDocumentRequest(DocumentId, HttpRequestMessage, HttpResponseMessage); + exit(HttpResponseMessage.IsSuccessStatusCode()); + end; + + procedure RemoveDocumentFromReceived(EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + this.APIRequests.PatchReceivedDocument(EDocument, HttpRequestMessage, HttpResponseMessage); + exit(HttpResponseMessage.IsSuccessStatusCode()); + end; + + #endregion + + #region local methods + + local procedure CheckIfSuccessfulRequest(EDocument: Record "E-Document"; HttpResponseMessage: HttpResponseMessage): Boolean + var + EDocumentErrorHelper: Codeunit "E-Document Error Helper"; + begin + if HttpResponseMessage.IsSuccessStatusCode() then + exit(true); + + if HttpResponseMessage.IsBlockedByEnvironment() then + EDocumentErrorHelper.LogSimpleErrorMessage(EDocument, this.EnvironmentBlocksErr) + else + EDocumentErrorHelper.LogSimpleErrorMessage(EDocument, StrSubstNo(this.UnsuccessfulResponseErr, HttpResponseMessage.HttpStatusCode, HttpResponseMessage.ReasonPhrase)); + end; + + #endregion + + #region event subscribers + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Purch.-Post", OnAfterCheckAndUpdate, '', false, false)] + local procedure CheckOnPosting(var PurchaseHeader: Record "Purchase Header"; CommitIsSuppressed: Boolean; PreviewMode: Boolean) + var + EDocument: Record "E-Document"; + EDocumentService: Record "E-Document Service"; + EDocumentServiceStatus: Record "E-Document Service Status"; + begin + if PurchaseHeader.IsTemporary() then + exit; + + EDocument.SetLoadFields("Entry No"); + EDocument.SetRange("Document Record ID", PurchaseHeader.RecordId); + if not EDocument.FindFirst() then + exit; + + EDocumentService.SetLoadFields(Code); + EDocumentService.SetRange("Service Integration", EDocumentService."Service Integration"::"ExFlow E-Invoicing"); + if not EDocumentService.FindFirst() then + exit; + + EDocumentServiceStatus.SetLoadFields(Status); + EDocumentServiceStatus.SetRange("E-Document Entry No", EDocument."Entry No"); + EDocumentServiceStatus.SetRange("E-Document Service Code", EDocumentService.Code); + if EDocumentServiceStatus.FindSet() then + repeat + EDocumentServiceStatus.TestField(EDocumentServiceStatus.Status, EDocumentServiceStatus.Status::Approved); + until EDocumentServiceStatus.Next() = 0; + end; + + #endregion +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/HelpersImpl.Codeunit.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/HelpersImpl.Codeunit.al new file mode 100644 index 0000000000..d6bb899b42 --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/HelpersImpl.Codeunit.al @@ -0,0 +1,92 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; + +using System.Reflection; +using Microsoft.Utilities; +using System.Integration; + +codeunit 6385 HelpersImpl +{ + Access = Internal; + + var + ClaimTypeTxt: Label 'exp', Locked = true; + + #region public methods + + [NonDebuggable] + procedure ParseJsonString(HttpContent: HttpContent): Text + var + JsonObject: JsonObject; + Content: Text; + begin + if not HttpContent.ReadAs(Content) then + exit; + + if JsonObject.ReadFrom(Content) then + exit(Content); + end; + + [NonDebuggable] + procedure GetJsonValueFromText(JsonText: Text; Path: Text): Text + var + JsonObject: JsonObject; + JsonToken: JsonToken; + begin + if JsonObject.ReadFrom(JsonText) then + if JsonObject.SelectToken(Path, JsonToken) then + exit(this.GetJsonValue(JsonToken.AsValue())); + end; + + procedure IsTokenValid(InToken: SecretText): Boolean + begin + exit(this.GetTokenDateTimeValue(InToken, this.ClaimTypeTxt) > CurrentDateTime()); + end; + + #endregion + + #region local methods + + local procedure GetTokenDateTimeValue(InToken: SecretText; ClaimType: Text): DateTime + var + TypeHelper: Codeunit "Type Helper"; + Timestamp: Decimal; + begin + if Evaluate(Timestamp, this.GetValueFromToken(InToken, ClaimType)) then + exit(TypeHelper.EvaluateUnixTimestamp(Timestamp)); + end; + + [NonDebuggable] + local procedure GetValueFromToken(InToken: SecretText; ClaimType: Text): Text + var + TempNameValueBuffer: Record "Name/Value Buffer" temporary; + SOAPWebServiceRequestMgt: Codeunit "SOAP Web Service Request Mgt."; + begin + if InToken.IsEmpty() then + exit; + + TempNameValueBuffer.DeleteAll(); + SOAPWebServiceRequestMgt.GetTokenDetailsAsNameBuffer(InToken, TempNameValueBuffer); + TempNameValueBuffer.Reset(); + TempNameValueBuffer.SetRange(Name, ClaimType); + if TempNameValueBuffer.FindFirst() then + exit(TempNameValueBuffer.Value); + end; + + [NonDebuggable] + local procedure GetJsonValue(JsonValue: JsonValue): Text + begin + if JsonValue.IsNull() then + exit; + + if JsonValue.IsUndefined() then + exit; + + exit(JsonValue.AsText()); + end; + + #endregion +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/Integration/IntegrationEnumExt.EnumExt.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/Integration/IntegrationEnumExt.EnumExt.al new file mode 100644 index 0000000000..db4953f886 --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/Integration/IntegrationEnumExt.EnumExt.al @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; + +using Microsoft.EServices.EDocument; + +enumextension 6380 IntegrationEnumExt extends "E-Document Integration" +{ + value(6380; "ExFlow E-Invoicing") + { + Caption = 'ExFlow E-Invoicing'; + Implementation = "E-Document Integration" = IntegrationImpl; + } +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/Integration/IntegrationImpl.Codeunit.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/Integration/IntegrationImpl.Codeunit.al new file mode 100644 index 0000000000..019ad06a32 --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/Integration/IntegrationImpl.Codeunit.al @@ -0,0 +1,61 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; + +using System.Utilities; +using Microsoft.EServices.EDocument; + +codeunit 6386 IntegrationImpl implements "E-Document Integration" +{ + Access = Internal; + + var + Processing: Codeunit Processing; + BatchSendNotSupportedErr: Label 'Batch sending is not supported in this version'; + CancelNotSupportedErr: Label 'Cancel is not supported in this version'; + + procedure Send(var EDocument: Record "E-Document"; var TempBlob: Codeunit "Temp Blob"; var IsAsync: Boolean; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage) + var + begin + this.Processing.SendEDocument(EDocument, TempBlob, IsAsync, HttpRequestMessage, HttpResponseMessage); + end; + + procedure SendBatch(var EDocuments: Record "E-Document"; var TempBlob: Codeunit "Temp Blob"; var IsAsync: Boolean; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage) + begin + IsAsync := false; + Error(this.BatchSendNotSupportedErr); + end; + + procedure GetResponse(var EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + exit(this.Processing.GetDocumentResponse(EDocument, HttpRequestMessage, HttpResponseMessage)); + end; + + procedure GetApproval(var EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + exit(this.Processing.GetDocumentApproval(EDocument, HttpRequestMessage, HttpResponseMessage)); + end; + + procedure Cancel(var EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + Error(this.CancelNotSupportedErr); + end; + + procedure ReceiveDocument(var TempBlob: Codeunit "Temp Blob"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage) + begin + this.Processing.ReceiveDocument(TempBlob, HttpRequestMessage, HttpResponseMessage); + end; + + procedure GetDocumentCountInBatch(var TempBlob: Codeunit "Temp Blob"): Integer + begin + exit(this.Processing.GetDocumentCountInBatch(TempBlob)); + end; + + procedure GetIntegrationSetup(var SetupPage: Integer; var SetupTable: Integer) + begin + SetupPage := Page::ConnectionSetupCard; + SetupTable := Database::ConnectionSetup; + end; +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/Jobs/GetReadyStatusJob.Codeunit.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/Jobs/GetReadyStatusJob.Codeunit.al new file mode 100644 index 0000000000..64b10485a4 --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/Jobs/GetReadyStatusJob.Codeunit.al @@ -0,0 +1,90 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; + +using System.Telemetry; +using System.Threading; +using Microsoft.EServices.EDocument; + +codeunit 6384 GetReadyStatusJob +{ + TableNo = "Job Queue Entry"; + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + var + EDocTelemetryGetResponseScopeStartLbl: Label 'E-Document Get Response: Start Scope', Locked = true; + EDocTelemetryGetResponseScopeEndLbl: Label 'E-Document Get Response: End Scope', Locked = true; + + trigger OnRun() + var + JobHelperImpl: Codeunit JobHelperImpl; + BlankRecordId: RecordId; + begin + if not this.IsEDocumentStatusSent() then + exit; + + this.ProcessSentDocuments(); + + if this.IsEDocumentStatusSent() then + JobHelperImpl.ScheduleEDocumentJob(Codeunit::GetReadyStatusJob, BlankRecordId, 300000); + end; + + local procedure ProcessSentDocuments() + var + EDocumentServiceStatus: Record "E-Document Service Status"; + EDocumentService: Record "E-Document Service"; + EDocument: Record "E-Document"; + JobHelperImpl: Codeunit JobHelperImpl; + begin + EDocumentServiceStatus.SetLoadFields("E-Document Service Code", "E-Document Entry No"); + EDocumentServiceStatus.SetRange(Status, EDocumentServiceStatus.Status::Sent); + if EDocumentServiceStatus.FindSet() then + repeat + JobHelperImpl.FetchEDocumentAndService(EDocument, EDocumentService, EDocumentServiceStatus); + this.HandleResponse(EDocument, EDocumentService); + until EDocumentServiceStatus.Next() = 0; + end; + + local procedure HandleResponse(var EDocument: Record "E-Document"; EDocumentService: Record "E-Document Service") + var + Processing: Codeunit Processing; + JobHelperImpl: Codeunit JobHelperImpl; + BlankRecordId: RecordId; + HttpResponseMessage: HttpResponseMessage; + HttpRequestMessage: HttpRequestMessage; + begin + if this.GetResponse(EDocument, HttpRequestMessage, HttpResponseMessage) then begin + Processing.InsertLogWithIntegration(EDocument, EDocumentService, Enum::"E-Document Service Status"::Approved, 0, HttpRequestMessage, HttpResponseMessage); + JobHelperImpl.ScheduleEDocumentJob(Codeunit::PatchSentJob, BlankRecordId, 300000); + end; + end; + + local procedure GetResponse(var EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage) ReturnStatus: Boolean + var + Processing: Codeunit Processing; + Telemetry: Codeunit Telemetry; + TelemetryDimensions: Dictionary of [Text, Text]; + begin + // Commit before create document with error handling + Commit(); + + Telemetry.LogMessage('', this.EDocTelemetryGetResponseScopeStartLbl, Verbosity::Normal, DataClassification::OrganizationIdentifiableInformation, TelemetryScope::All, TelemetryDimensions); + + if Processing.GetDocumentSentResponse(EDocument, HttpRequestMessage, HttpResponseMessage) then + ReturnStatus := true; + + Telemetry.LogMessage('', this.EDocTelemetryGetResponseScopeEndLbl, Verbosity::Normal, DataClassification::OrganizationIdentifiableInformation, TelemetryScope::All); + end; + + local procedure IsEDocumentStatusSent(): Boolean + var + EdocumentServiceStatus: Record "E-Document Service Status"; + begin + EdocumentServiceStatus.SetRange(Status, EdocumentServiceStatus.Status::Sent); + exit(not EdocumentServiceStatus.IsEmpty()); + end; +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/Jobs/JobHelperImpl.Codeunit.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/Jobs/JobHelperImpl.Codeunit.al new file mode 100644 index 0000000000..fac63284e2 --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/Jobs/JobHelperImpl.Codeunit.al @@ -0,0 +1,83 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; + +using System.Telemetry; +using System.Threading; +using Microsoft.EServices.EDocument; + +codeunit 6392 JobHelperImpl +{ + TableNo = "Job Queue Entry"; + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + #region variables + + var + Telemetry: Codeunit Telemetry; + JobQueueCategoryTok: Label 'EDocument', Locked = true, Comment = 'Max Length 10'; + EDocumentJobTelemetryLbl: Label 'E-Document Background Job Scheduled', Locked = true; + JobQueueIdTxt: Label 'Job Queue Id', Locked = true; + CodeunitIsTxt: Label 'Codeunit Id', Locked = true; + RecordIdTxt: Label 'Record Id', Locked = true; + UserSessionIdTxt: Label 'User Session ID', Locked = true; + EarliestStartDateTimeTxt: Label 'Earliest Start Date/Time', Locked = true; + + #endregion + + #region public methods + + procedure ScheduleEDocumentJob(CodeunitId: Integer; JobRecordId: RecordId; EarliestStartDateTime: Integer): Guid + var + JobQueueEntry: Record "Job Queue Entry"; + TelemetryDimensions: Dictionary of [Text, Text]; + begin + if this.IsJobQueueScheduled(CodeunitId) then + exit; + + JobQueueEntry.Init(); + JobQueueEntry."Object Type to Run" := JobQueueEntry."Object Type to Run"::Codeunit; + JobQueueEntry."Object ID to Run" := CodeunitId; + JobQueueEntry."Record ID to Process" := JobRecordId; + JobQueueEntry."User Session ID" := SessionId(); + JobQueueEntry."Job Queue Category Code" := this.JobQueueCategoryTok; + JobQueueEntry."No. of Attempts to Run" := 0; + JobQueueEntry."Earliest Start Date/Time" := CurrentDateTime + EarliestStartDateTime; + + TelemetryDimensions.Add(this.JobQueueIdTxt, JobQueueEntry.ID); + TelemetryDimensions.Add(this.CodeunitIsTxt, Format(CodeunitId)); + TelemetryDimensions.Add(this.RecordIdTxt, Format(JobRecordId)); + TelemetryDimensions.Add(this.UserSessionIdTxt, Format(JobQueueEntry."User Session ID")); + TelemetryDimensions.Add(this.EarliestStartDateTimeTxt, Format(JobQueueEntry."Earliest Start Date/Time")); + this.Telemetry.LogMessage('', this.EDocumentJobTelemetryLbl, Verbosity::Normal, DataClassification::OrganizationIdentifiableInformation, TelemetryScope::All, TelemetryDimensions); + Codeunit.Run(Codeunit::"Job Queue - Enqueue", JobQueueEntry); + exit(JobQueueEntry.ID); + end; + + procedure FetchEDocumentAndService(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; EDocumentServiceStatus: Record "E-Document Service Status") + begin + EDocumentService.SetLoadFields("Service Integration", "Document Format"); + EDocumentService.Get(EDocumentServiceStatus."E-Document Service Code"); + EDocument.Get(EDocumentServiceStatus."E-Document Entry No"); + end; + + #endregion + + #region local methods + + local procedure IsJobQueueScheduled(CodeunitId: Integer): Boolean + var + JobQueueEntry: Record "Job Queue Entry"; + begin + JobQueueEntry.SetRange("Object Type to Run", JobQueueEntry."Object Type to Run"::Codeunit); + JobQueueEntry.SetRange("Object ID to Run", CodeunitId); + JobQueueEntry.SetFilter(Status, '%1|%2', JobQueueEntry.Status::Ready, JobQueueEntry.Status::"In Process"); + exit(not JobQueueEntry.IsEmpty()); + end; + + #endregion +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/Jobs/PatchSentJob.Codeunit.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/Jobs/PatchSentJob.Codeunit.al new file mode 100644 index 0000000000..69e07127ba --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/Jobs/PatchSentJob.Codeunit.al @@ -0,0 +1,79 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; + +using System.Threading; +using Microsoft.EServices.EDocument; +using System.Security.Authentication; + +codeunit 6387 PatchSentJob +{ + TableNo = "Job Queue Entry"; + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + trigger OnRun() + var + JobHelperImpl: Codeunit JobHelperImpl; + BlankRecordId: RecordId; + begin + if not this.IsEDocumentApproved() then + exit; + + this.ProcessApprovedDocuments(); + + if this.IsEDocumentApproved() then + JobHelperImpl.ScheduleEDocumentJob(Codeunit::PatchSentJob, BlankRecordId, 300000); + end; + + local procedure ProcessApprovedDocuments() + var + EDocumentServiceStatus: Record "E-Document Service Status"; + EDocumentService: Record "E-Document Service"; + EDocumentIntegrationLog: Record "E-Document Integration Log"; + EDocument: Record "E-Document"; + APIRequests: Codeunit APIRequests; + Processing: Codeunit Processing; + JobHelperImpl: Codeunit JobHelperImpl; + HttpResponseMessage: HttpResponseMessage; + HttpRequestMessage: HttpRequestMessage; + begin + EDocumentServiceStatus.SetLoadFields("E-Document Service Code", "E-Document Entry No"); + EDocumentServiceStatus.SetRange(Status, EDocumentServiceStatus.Status::Approved); + if EDocumentServiceStatus.FindSet() then + repeat + JobHelperImpl.FetchEDocumentAndService(EDocument, EDocumentService, EDocumentServiceStatus); + + EDocumentIntegrationLog.Reset(); + EDocumentIntegrationLog.SetRange("E-Doc. Entry No", EDocument."Entry No"); + EDocumentIntegrationLog.SetRange("Response Status", 204); + EDocumentIntegrationLog.SetRange(Method, Format("Http Request Type"::PATCH)); + if EDocumentIntegrationLog.IsEmpty() then + if APIRequests.PatchDocument(EDocument, HttpRequestMessage, HttpResponseMessage) then + Processing.InsertIntegrationLog(EDocument, EDocumentService, HttpRequestMessage, HttpResponseMessage); + until EDocumentServiceStatus.Next() = 0; + end; + + local procedure IsEDocumentApproved(): Boolean + var + EdocumentServiceStatus: Record "E-Document Service Status"; + EDocumentIntegrationLog: Record "E-Document Integration Log"; + HasRecords: Boolean; + begin + EdocumentServiceStatus.SetLoadFields("E-Document Entry No"); + EdocumentServiceStatus.SetRange(Status, EdocumentServiceStatus.Status::Approved); + if EdocumentServiceStatus.FindSet() then + repeat + EDocumentIntegrationLog.Reset(); + EDocumentIntegrationLog.SetRange("E-Doc. Entry No", EdocumentServiceStatus."E-Document Entry No"); + EDocumentIntegrationLog.SetRange("Response Status", 204); + EDocumentIntegrationLog.SetRange(Method, Format("Http Request Type"::PATCH)); + HasRecords := EDocumentIntegrationLog.IsEmpty(); + until (EdocumentServiceStatus.Next() = 0) or (HasRecords); + + exit(HasRecords); + end; +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/Permissions/Edit.PermissionSet.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/Permissions/Edit.PermissionSet.al new file mode 100644 index 0000000000..a2d386e6b3 --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/Permissions/Edit.PermissionSet.al @@ -0,0 +1,15 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; + +permissionset 6382 Edit +{ + Access = Internal; + Assignable = false; + Caption = 'SignUp E-Doc. Connector - Edit', MaxLength = 30; + IncludedPermissionSets = Read; + + Permissions = tabledata ConnectionSetup = imd; +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/Permissions/Objects.PermissionSet.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/Permissions/Objects.PermissionSet.al new file mode 100644 index 0000000000..9ade56ad84 --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/Permissions/Objects.PermissionSet.al @@ -0,0 +1,29 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; + +permissionset 6380 Objects +{ + Access = Internal; + Assignable = false; + Caption = 'SignUp E-Doc. Connector - Obj.', MaxLength = 30; + + Permissions = table ConnectionSetup = X, + page ConnectionSetupCard = X, + codeunit IntegrationImpl = X, + codeunit PatchSentJob = X, + codeunit JobHelperImpl = X, + codeunit GetReadyStatusJob = X, + codeunit APIRequests = X, + codeunit APIRequestsImpl = X, + codeunit Authentication = X, + codeunit AuthenticationImpl = X, + codeunit Connection = X, + codeunit ConnectionImpl = X, + codeunit HelpersImpl = X, + codeunit Processing = X, + codeunit ProcessingImpl = X; + +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/Permissions/Read.PermissionSet.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/Permissions/Read.PermissionSet.al new file mode 100644 index 0000000000..2129fcf1a1 --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/Permissions/Read.PermissionSet.al @@ -0,0 +1,15 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; + +permissionset 6381 Read +{ + Access = Internal; + Assignable = false; + Caption = 'SignUp E-Doc. Connector - Read', MaxLength = 30; + IncludedPermissionSets = Objects; + + Permissions = tabledata ConnectionSetup = r; +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/Permissions/SignUpEDocConnectorEdit.PermissionSetExt.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/Permissions/SignUpEDocConnectorEdit.PermissionSetExt.al new file mode 100644 index 0000000000..94cbf00cde --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/Permissions/SignUpEDocConnectorEdit.PermissionSetExt.al @@ -0,0 +1,11 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; +using Microsoft.EServices.EDocumentConnector; + +permissionsetextension 6380 "SignUp EDoc. Connector - Edit" extends "EDocConnector - Edit" +{ + IncludedPermissionSets = Edit; +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/Permissions/SignUpEDocConnectorRead.PermissionSetExt.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/Permissions/SignUpEDocConnectorRead.PermissionSetExt.al new file mode 100644 index 0000000000..9c27703bce --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/Permissions/SignUpEDocConnectorRead.PermissionSetExt.al @@ -0,0 +1,11 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; +using Microsoft.EServices.EDocumentConnector; + +permissionsetextension 6381 "SignUp EDoc. Connector - Read" extends "EDocConnector - Read" +{ + IncludedPermissionSets = Read; +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/Processing.Codeunit.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/Processing.Codeunit.al new file mode 100644 index 0000000000..a19c17812c --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/Processing.Codeunit.al @@ -0,0 +1,120 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; + +using Microsoft.EServices.EDocument; +using System.Utilities; + +codeunit 6388 Processing +{ + Access = Internal; + + #region variables + var + ProcessingImpl: Codeunit ProcessingImpl; + + #endregion + + #region public methods + + /// + /// The method sends the E-Document to the API. + /// + /// E-Document record + /// TempBlob + /// IsAsync + /// HttpRequestMessage + /// HttpResponseMessage + procedure SendEDocument(var EDocument: Record "E-Document"; var TempBlob: Codeunit "Temp Blob"; var IsAsync: Boolean; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage) + begin + this.ProcessingImpl.SendEDocument(EDocument, TempBlob, IsAsync, HttpRequestMessage, HttpResponseMessage); + end; + + /// + /// The method gets the E-Document response. + /// + /// E-Document record + /// HttpRequestMessage + /// HttpResponseMessage + /// True - if completed successfully + procedure GetDocumentResponse(var EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + exit(this.ProcessingImpl.GetDocumentResponse(EDocument, HttpRequestMessage, HttpResponseMessage)); + end; + + /// + /// The method gets the E-Document sent response. + /// + /// E-Document record + /// HttpRequestMessage + /// HttpResponseMessage + /// True - if completed successfully + procedure GetDocumentSentResponse(var EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + exit(this.ProcessingImpl.GetDocumentSentResponse(EDocument, HttpRequestMessage, HttpResponseMessage)); + end; + + /// + /// The method gets the E-Document approval. + /// + /// E-Document record + /// HttpRequestMessage + /// HttpResponseMessage + /// True - if completed successfully + procedure GetDocumentApproval(EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + begin + exit(this.ProcessingImpl.GetDocumentApproval(EDocument, HttpRequestMessage, HttpResponseMessage)); + end; + + /// + /// The method receives the document. + /// + /// TempBlob + /// HttpRequestMessage + /// HttpResponseMessage + procedure ReceiveDocument(var TempBlob: Codeunit "Temp Blob"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage) + begin + this.ProcessingImpl.ReceiveDocument(TempBlob, HttpRequestMessage, HttpResponseMessage); + end; + + /// + /// The method gets the document count in batch. + /// + /// TempBlob + /// True - if completed successfully + procedure GetDocumentCountInBatch(var TempBlob: Codeunit "Temp Blob"): Integer + begin + exit(this.ProcessingImpl.GetDocumentCountInBatch(TempBlob)); + end; + + /// + /// The method inserts the integration log. + /// + /// E-Document record + /// E-Document Service record + /// HttpRequestMessage + /// HttpResponseMessage + procedure InsertIntegrationLog(EDocument: Record "E-Document"; EDocumentService: Record "E-Document Service"; HttpRequestMessage: HttpRequestMessage; HttpResponseMessage: HttpResponseMessage) + begin + this.ProcessingImpl.InsertIntegrationLog(EDocument, EDocumentService, HttpRequestMessage, HttpResponseMessage); + end; + + /// + /// The method inserts the log with integration. + /// + /// E-Document record + /// E-Document Service record + /// E-Document Service Status + /// E-Document Data Storage Entry No. + /// HttpRequestMessage + /// HttpResponseMessage + procedure InsertLogWithIntegration(var EDocument: Record "E-Document"; EDocumentService: Record "E-Document Service"; + EDocumentServiceStatus: Enum "E-Document Service Status"; EDocDataStorageEntryNo: Integer; HttpRequestMessage: HttpRequestMessage; HttpResponseMessage: HttpResponseMessage) + begin + this.ProcessingImpl.InsertLogWithIntegration(EDocument, EDocumentService, EDocumentServiceStatus, EDocDataStorageEntryNo, HttpRequestMessage, HttpResponseMessage); + end; + + #endregion +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/ProcessingImpl.Codeunit.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/ProcessingImpl.Codeunit.al new file mode 100644 index 0000000000..0412890010 --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/ProcessingImpl.Codeunit.al @@ -0,0 +1,580 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; + +using Microsoft.EServices.EDocument; +using System.Telemetry; +using System.Text; +using System.Utilities; + +codeunit 6383 ProcessingImpl +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + Permissions = tabledata "E-Document" = rim, + tabledata "E-Document Service Status" = rm, + tabledata "E-Document Service" = r, + tabledata "E-Document Integration Log" = ri, + tabledata "E-Document Log" = ri; + + #region variables + var + Connection: Codeunit Connection; + HelpersImpl: Codeunit HelpersImpl; + EDocumentHelper: Codeunit "E-Document Helper"; + EDocumentLogHelper: Codeunit "E-Document Log Helper"; + EDocumentErrorHelper: Codeunit "E-Document Error Helper"; + GetApprovalCheckStatusErr: Label 'You cannot ask for approval with the E-Document in this current status %1. You can request for approval when E-document status is Sent or Pending Response.', Comment = '%1 - Status'; + CouldNotRetrieveDocumentErr: Label 'Could not retrieve document with id: %1 from the service', Comment = '%1 - Document ID'; + DocumentIdNotFoundErr: Label 'Document ID not found in response'; + ExternalServiceTok: Label 'E-Document - SignUp', Locked = true; + InboxTxt: Label 'inbox', Locked = true; + InstanceIdTxt: Label 'instanceId', Locked = true; + PeppolInstanceIdTxt: Label 'peppolInstanceId', Locked = true; + StatusTxt: Label 'status', Locked = true; + SentTxt: Label 'sent', Locked = true; + ReadyTxt: Label 'ready', Locked = true; + FailedTxt: Label 'failed', Locked = true; + DescriptionTxt: Label 'description', Locked = true; + ReasonTxt: Label 'Reason: ', Locked = true; + NewTxt: Label 'new', Locked = true; + DocumentTxt: Label 'document', Locked = true; + StandardBusinessDocumentHeaderTxt: Label '', Locked = true; + InvoiceTxt: Label '', Locked = true; + + + #endregion + + #region public methods + + procedure SendEDocument(var EDocument: Record "E-Document"; var TempBlob: Codeunit "Temp Blob"; var IsAsync: Boolean; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage) + var + EDocumentServiceStatus: Record "E-Document Service Status"; + EDocumentService: Record "E-Document Service"; + FeatureTelemetry: Codeunit "Feature Telemetry"; + begin + IsAsync := true; + + this.EDocumentHelper.GetEdocumentService(EDocument, EDocumentService); + EDocumentServiceStatus.Get(EDocument."Entry No", EDocumentService.Code); + + case EDocumentServiceStatus.Status of + EDocumentServiceStatus.Status::Exported: + this.SendEDocument(EDocument, TempBlob, HttpRequestMessage, HttpResponseMessage); + EDocumentServiceStatus.Status::"Sending Error": + if EDocument."Document Id" = '' then + this.SendEDocument(EDocument, TempBlob, HttpRequestMessage, HttpResponseMessage); + end; + + FeatureTelemetry.LogUptake('', this.ExternalServiceTok, Enum::"Feature Uptake Status"::Used); + end; + + procedure GetDocumentResponse(var EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + var + ErrorDescription: Text; + begin + if not this.Connection.CheckDocumentStatus(EDocument, HttpRequestMessage, HttpResponseMessage) then + exit; + exit(not this.DocumentHasErrorOrStillInProcessing(EDocument, HttpResponseMessage, ErrorDescription)); + end; + + procedure GetDocumentSentResponse(var EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + var + EDocumentService: Record "E-Document Service"; + APIRequests: Codeunit APIRequests; + Status, StatusDescription : Text; + begin + this.EDocumentHelper.GetEdocumentService(EDocument, EdocumentService); + APIRequests.GetSentDocumentStatus(EDocument, HttpRequestMessage, HttpResponseMessage); + if not this.ParseGetADocumentApprovalResponse(HttpResponseMessage.Content, Status, StatusDescription) then + exit; + + case Status of + this.ReadyTxt: + exit(true); + this.FailedTxt: + begin + if StatusDescription <> '' then + this.EDocumentErrorHelper.LogSimpleErrorMessage(EDocument, this.ReasonTxt + StatusDescription); + this.InsertLogWithIntegration(EDocument, EDocumentService, Enum::"E-Document Service Status"::Rejected, 0, HttpRequestMessage, HttpResponseMessage); + exit; + end; + end; + end; + + procedure GetDocumentApproval(EDocument: Record "E-Document"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage): Boolean + var + EDocumentService: Record "E-Document Service"; + EDocumentServiceStatus: Record "E-Document Service Status"; + APIRequests: Codeunit APIRequests; + JobHelperImpl: Codeunit JobHelperImpl; + BlankRecordId: RecordId; + Status, StatusDescription : Text; + begin + this.EDocumentHelper.GetEdocumentService(EDocument, EdocumentService); + EDocumentServiceStatus.SetLoadFields(Status); + EDocumentServiceStatus.Get(EDocument."Entry No", EdocumentService.Code); + if not (EDocumentServiceStatus.Status in [EDocumentServiceStatus.Status::Sent, EDocumentServiceStatus.Status::"Pending Response"]) then + Error(this.GetApprovalCheckStatusErr, EDocumentServiceStatus.Status); + + APIRequests.GetSentDocumentStatus(EDocument, HttpRequestMessage, HttpResponseMessage); + if not this.ParseGetADocumentApprovalResponse(HttpResponseMessage.Content, Status, StatusDescription) then + exit; + + case Status of + this.ReadyTxt: + begin + if EDocumentServiceStatus.Status = EDocumentServiceStatus.Status::Approved then + JobHelperImpl.ScheduleEDocumentJob(Codeunit::PatchSentJob, BlankRecordId, 300000) + else + JobHelperImpl.ScheduleEDocumentJob(Codeunit::GetReadyStatusJob, BlankRecordId, 300000); + exit(true); + end; + this.FailedTxt: + begin + if StatusDescription <> '' then + this.EDocumentErrorHelper.LogSimpleErrorMessage(EDocument, this.ReasonTxt + StatusDescription); + this.InsertLogWithIntegration(EDocument, EDocumentService, Enum::"E-Document Service Status"::Rejected, 0, HttpRequestMessage, HttpResponseMessage); + exit; + end; + end; + end; + + procedure ReceiveDocument(var TempBlob: Codeunit "Temp Blob"; var HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage) + var + ContentData: Text; + begin + if not this.Connection.GetReceivedDocuments(HttpRequestMessage, HttpResponseMessage) then + exit; + + if not HttpResponseMessage.Content.ReadAs(ContentData) then + exit; + + TempBlob.CreateOutStream(TextEncoding::UTF8).WriteText(ContentData); + end; + + procedure GetDocumentCountInBatch(var TempBlob: Codeunit "Temp Blob"): Integer + var + ResponseTxt: Text; + begin + TempBlob.CreateInStream().ReadText(ResponseTxt); + exit(this.GetNumberOfReceivedDocuments(ResponseTxt)); + end; + + procedure InsertIntegrationLog(EDocument: Record "E-Document"; EDocumentService: Record "E-Document Service"; HttpRequestMessage: HttpRequestMessage; HttpResponseMessage: HttpResponseMessage) + var + EDocumentIntegrationLog: Record "E-Document Integration Log"; + EDocIntegrationLogRecordRef: RecordRef; + RequestTxt: Text; + begin + if EDocumentService."Service Integration" = EDocumentService."Service Integration"::"No Integration" then + exit; + + EDocumentIntegrationLog.Validate("E-Doc. Entry No", EDocument."Entry No"); + EDocumentIntegrationLog.Validate("Service Code", EDocumentService.Code); + EDocumentIntegrationLog.Validate("Response Status", HttpResponseMessage.HttpStatusCode()); + EDocumentIntegrationLog.Validate("Request URL", HttpRequestMessage.GetRequestUri()); + EDocumentIntegrationLog.Validate(Method, HttpRequestMessage.Method()); + EDocumentIntegrationLog.Insert(); + + EDocIntegrationLogRecordRef.GetTable(EDocumentIntegrationLog); + + if HttpRequestMessage.Content.ReadAs(RequestTxt) then begin + this.InsertIntegrationBlob(EDocIntegrationLogRecordRef, RequestTxt, EDocumentIntegrationLog.FieldNo(EDocumentIntegrationLog."Request Blob")); + EDocIntegrationLogRecordRef.Modify(); + end; + + if HttpResponseMessage.Content.ReadAs(RequestTxt) then begin + this.InsertIntegrationBlob(EDocIntegrationLogRecordRef, RequestTxt, EDocumentIntegrationLog.FieldNo(EDocumentIntegrationLog."Response Blob")); + EDocIntegrationLogRecordRef.Modify(); + end; + end; + + procedure InsertLogWithIntegration(var EDocument: Record "E-Document"; EDocumentService: Record "E-Document Service"; + EDocumentServiceStatus: Enum "E-Document Service Status"; EDocDataStorageEntryNo: Integer; HttpRequestMessage: HttpRequestMessage; HttpResponseMessage: HttpResponseMessage) + begin + this.InsertLog(EDocument, EDocumentService, EDocDataStorageEntryNo, EDocumentServiceStatus); + + if (HttpRequestMessage.GetRequestUri() <> '') and (HttpResponseMessage.Headers.Keys().Count > 0) then + this.InsertIntegrationLog(EDocument, EDocumentService, HttpRequestMessage, HttpResponseMessage); + end; + + #endregion + + #region local methods + + local procedure ParseGetADocumentApprovalResponse(HttpContentResponse: HttpContent; var Status: Text; var StatusDescription: Text): Boolean + var + JsonManagement: Codeunit "JSON Management"; + Result: Text; + begin + Result := this.HelpersImpl.ParseJsonString(HttpContentResponse); + if Result = '' then + exit; + + if not JsonManagement.InitializeFromString(Result) then + exit; + + Status := this.GetStatus(JsonManagement); + + if Status in [this.ReadyTxt, this.SentTxt] then + exit(true); + + if Status = this.FailedTxt then begin + JsonManagement.GetArrayPropertyValueAsStringByName(this.DescriptionTxt, StatusDescription); + exit(true); + end; + + exit; + end; + + local procedure InsertLog(var EDocument: Record "E-Document"; EDocumentService: Record "E-Document Service"; EDocDataStorageEntryNo: Integer; EDocumentServiceStatus: Enum "E-Document Service Status"): Integer + var + EDocumentLog: Record "E-Document Log"; + begin + if EDocumentService.Code <> '' then + this.UpdateServiceStatus(EDocument, EDocumentService, EDocumentServiceStatus); + + EDocumentLog.Validate("Document Type", EDocument."Document Type"); + EDocumentLog.Validate("Document No.", EDocument."Document No."); + EDocumentLog.Validate("E-Doc. Entry No", EDocument."Entry No"); + EDocumentLog.Validate(Status, EDocumentServiceStatus); + EDocumentLog.Validate("Service Integration", EDocumentService."Service Integration"); + EDocumentLog.Validate("Service Code", EDocumentService.Code); + EDocumentLog.Validate("Document Format", EDocumentService."Document Format"); + EDocumentLog.Validate("E-Doc. Data Storage Entry No.", EDocDataStorageEntryNo); + EDocumentLog.Insert(); + + exit(EDocumentLog."Entry No."); + end; + + local procedure UpdateServiceStatus(var EDocument: Record "E-Document"; EDocumentService: Record "E-Document Service"; EDocumentStatus: Enum "E-Document Service Status") + var + EDocumentServiceStatus: Record "E-Document Service Status"; + Exists: Boolean; + begin + EDocument.Get(EDocument."Entry No"); + Exists := EDocumentServiceStatus.Get(EDocument."Entry No", EDocumentService.Code); + EDocumentServiceStatus.Validate(Status, EDocumentStatus); + if Exists then + EDocumentServiceStatus.Modify() + else begin + EDocumentServiceStatus.Validate("E-Document Entry No", EDocument."Entry No"); + EDocumentServiceStatus.Validate("E-Document Service Code", EDocumentService.Code); + EDocumentServiceStatus.Validate(Status, EDocumentStatus); + EDocumentServiceStatus.Insert(); + end; + + this.UpdateEDocumentStatus(EDocument); + end; + + local procedure SendEDocument(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob"; HttpRequestMessage: HttpRequestMessage; HttpResponseMessage: HttpResponseMessage); + begin + this.Connection.SendFilePostRequest(TempBlob, EDocument, HttpRequestMessage, HttpResponseMessage); + this.SetEDocumentFileID(EDocument."Entry No", this.ParseSendFileResponse(HttpResponseMessage.Content)); + end; + + local procedure ParseReceivedDocument(InputTxt: Text; Index: Integer; var DocumentId: Text): Boolean + var + JsonManagement: Codeunit "JSON Management"; + IncrementalTable, Value : Text; + begin + if not JsonManagement.InitializeFromString(InputTxt) then + exit; + + JsonManagement.GetArrayPropertyValueAsStringByName(this.InboxTxt, Value); + JsonManagement.InitializeCollection(Value); + + if Index = 0 then + Index := 1; + + if Index > JsonManagement.GetCollectionCount() then + exit; + + JsonManagement.GetObjectFromCollectionByIndex(IncrementalTable, Index - 1); + + Clear(JsonManagement); + JsonManagement.InitializeObject(IncrementalTable); + JsonManagement.GetArrayPropertyValueAsStringByName(this.InstanceIdTxt, DocumentId); + + exit(true); + end; + + local procedure GetNumberOfReceivedDocuments(InputTxt: Text): Integer + var + JsonManagement: Codeunit "JSON Management"; + Value: Text; + begin + InputTxt := this.LeaveJustNewLine(InputTxt); + + if not JsonManagement.InitializeFromString(InputTxt) then + exit(0); + + JsonManagement.GetArrayPropertyValueAsStringByName(this.InboxTxt, Value); + JsonManagement.InitializeCollection(Value); + + exit(JsonManagement.GetCollectionCount()); + end; + + local procedure ParseSendFileResponse(HttpContentResponse: HttpContent): Text + var + JsonManagement: Codeunit "JSON Management"; + Result, Value : Text; + begin + Result := this.HelpersImpl.ParseJsonString(HttpContentResponse); + if Result = '' then + exit; + + if not JsonManagement.InitializeFromString(Result) then + exit; + + JsonManagement.GetStringPropertyValueByName(this.PeppolInstanceIdTxt, Value); + exit(Value); + end; + + local procedure SetEDocumentFileID(EDocEntryNo: Integer; FileId: Text) + var + EDocument: Record "E-Document"; + begin + if FileId = '' then + exit; + + if not EDocument.Get(EDocEntryNo) then + exit; + + EDocument."Document Id" := CopyStr(FileId, 1, MaxStrLen(EDocument."Document Id")); + EDocument.Modify(); + end; + + local procedure DocumentHasErrorOrStillInProcessing(EDocument: Record "E-Document"; HttpResponseMessage: HttpResponseMessage; var ErrorDescription: Text): Boolean + var + EDocumentService: Record "E-Document Service"; + EDocumentServiceStatus: Record "E-Document Service Status"; + JsonManagement: Codeunit "JSON Management"; + JobHelperImpl: Codeunit JobHelperImpl; + BlankRecordId: RecordId; + Result, Status : Text; + begin + Result := this.HelpersImpl.ParseJsonString(HttpResponseMessage.Content); + if Result = '' then + exit(true); + + if not JsonManagement.InitializeFromString(Result) then + exit(true); + + Status := this.GetStatus(JsonManagement); + + if Status in [this.SentTxt] then begin + JobHelperImpl.ScheduleEDocumentJob(Codeunit::GetReadyStatusJob, BlankRecordId, 120000); + exit; + end; + + if Status in [this.ReadyTxt] then begin + this.EDocumentHelper.GetEdocumentService(EDocument, EDocumentService); + EDocumentServiceStatus.SetLoadFields(Status); + EDocumentServiceStatus.Get(EDocument."Entry No", EdocumentService.Code); + if EDocumentServiceStatus.Status = EDocumentServiceStatus.Status::Approved then + JobHelperImpl.ScheduleEDocumentJob(Codeunit::PatchSentJob, BlankRecordId, 180000) + else + JobHelperImpl.ScheduleEDocumentJob(Codeunit::GetReadyStatusJob, BlankRecordId, 120000); + exit; + end; + + if Status = this.FailedTxt then begin + JsonManagement.GetArrayPropertyValueAsStringByName(this.DescriptionTxt, ErrorDescription); + exit; + end; + + JsonManagement.GetArrayPropertyValueAsStringByName(this.DescriptionTxt, ErrorDescription); + exit(true); + end; + + local procedure GetStatus(var JsonManagement: Codeunit "Json Management") Status: Text + begin + JsonManagement.GetArrayPropertyValueAsStringByName(this.StatusTxt, Status); + Status := Status.ToLower(); + end; + + local procedure UpdateEDocumentStatus(var EDocument: Record "E-Document") + var + IsHandled: Boolean; + begin + if IsHandled then + exit; + + if this.EDocumentHasErrors(EDocument) then + exit; + + this.SetDocumentStatus(EDocument); + end; + + local procedure EDocumentHasErrors(var EDocument: Record "E-Document"): Boolean + var + EDocumentServiceStatus: Record "E-Document Service Status"; + begin + EDocumentServiceStatus.SetRange("E-Document Entry No", EDocument."Entry No"); + EDocumentServiceStatus.SetFilter(Status, '%1|%2|%3|%4|%5', + EDocumentServiceStatus.Status::"Sending Error", + EDocumentServiceStatus.Status::"Export Error", + EDocumentServiceStatus.Status::"Cancel Error", + EDocumentServiceStatus.Status::"Imported Document Processing Error", + EDocumentServiceStatus.Status::Rejected); + + if EDocumentServiceStatus.IsEmpty() then + exit; + + EDocument.Validate(Status, EDocument.Status::Error); + EDocument.Modify(); + exit(true); + end; + + local procedure InsertIntegrationBlob(var EDocIntegrationLogRecordRef: RecordRef; Data: Text; FieldNo: Integer) + var + TempBlob: Codeunit "Temp Blob"; + begin + TempBlob.CreateOutStream().WriteText(Data); + TempBlob.ToRecordRef(EDocIntegrationLogRecordRef, FieldNo); + end; + + local procedure SetDocumentStatus(var EDocument: Record "E-Document") + var + EDocumentServiceStatus: Record "E-Document Service Status"; + EDocServiceCount: Integer; + begin + EDocumentServiceStatus.SetRange("E-Document Entry No", EDocument."Entry No"); + EDocServiceCount := EDocumentServiceStatus.Count; + + EDocumentServiceStatus.SetFilter(Status, '%1|%2|%3|%4|%5', + EDocumentServiceStatus.Status::Exported, + EDocumentServiceStatus.Status::"Imported Document Created", + EDocumentServiceStatus.Status::"Journal Line Created", + EDocumentServiceStatus.Status::Approved, + EDocumentServiceStatus.Status::Canceled); + if EDocumentServiceStatus.Count() = EDocServiceCount then + EDocument.Status := EDocument.Status::Processed + else + EDocument.Status := EDocument.Status::"In Progress"; + + EDocument.Modify(); + end; + + local procedure LeaveJustNewLine(InputText: Text): Text + var + InputJson, OutputDocumentJsonObject, OutputJsonObject : JsonObject; + InputJsonArray, OutputDocumentJsonArray : JsonArray; + InputJsonToken, DocumentJsonToken : JsonToken; + OutputText: text; + DocumentList: List of [Text]; + i: Integer; + begin + OutputText := InputText; + InputJson.ReadFrom(InputText); + if InputJson.Contains(this.InboxTxt) then begin + InputJson.Get(this.InboxTxt, InputJsonToken); + InputJsonArray := InputJsonToken.AsArray(); + foreach InputJsonToken in InputJsonArray do + if InputJsonToken.AsObject().Get(this.StatusTxt, DocumentJsonToken) then + if DocumentJsonToken.AsValue().AsText().ToLower() = this.NewTxt then begin + InputJsonToken.AsObject().Get(this.InstanceIdTxt, DocumentJsonToken); + DocumentList.Add(DocumentJsonToken.AsValue().AsText()); + end; + + for i := 1 to DocumentList.Count do begin + Clear(OutputDocumentJsonObject); + OutputDocumentJsonObject.Add(this.InstanceIdTxt, DocumentList.Get(i)); + OutputDocumentJsonArray.Add(OutputDocumentJsonObject); + end; + + OutputJsonObject.Add(this.InboxTxt, OutputDocumentJsonArray); + OutputJsonObject.WriteTo(OutputText) + end; + + exit(OutputText); + end; + + local procedure ParseContentData(var InputText: Text): Boolean + var + JsonManagement: Codeunit "JSON Management"; + Base64Convert: Codeunit "Base64 Convert"; + Value: Text; + ParsePosition: Integer; + begin + if not JsonManagement.InitializeFromString(InputText) then + exit; + + JsonManagement.GetArrayPropertyValueAsStringByName(this.DocumentTxt, Value); + InputText := Base64Convert.FromBase64(Value); + ParsePosition := StrPos(InputText, this.StandardBusinessDocumentHeaderTxt); + if ParsePosition > 0 then begin + InputText := CopyStr(InputText, parsePosition, StrLen(InputText)); + ParsePosition := StrPos(InputText, this.InvoiceTxt); + InputText := CopyStr(InputText, parsePosition, StrLen(InputText)); + ParsePosition := StrPos(InputText, this.StandardBusinessDocumentTxt); + InputText := CopyStr(InputText, 1, parsePosition - 1); + end; + + exit(true); + end; + + #endregion + + #region event subscribers + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"E-Doc. Integration Management", OnGetEDocumentApprovalReturnsFalse, '', false, false)] + local procedure OnGetEDocumentApprovalReturnsFalse(EDocuments: Record "E-Document"; EDocumentService: Record "E-Document Service"; HttpRequest: HttpRequestMessage; HttpResponse: HttpResponseMessage; var IsHandled: Boolean) + var + HttpContent: HttpContent; + Status, StatusDescription : Text; + begin + HttpContent := HttpResponse.Content; + if not this.ParseGetADocumentApprovalResponse(HttpContent, Status, StatusDescription) then + IsHandled := true; + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"E-Doc. Import", OnAfterInsertImportedEdocument, '', false, false)] + local procedure OnAfterInsertEdocument(var EDocument: Record "E-Document"; EDocumentService: Record "E-Document Service"; var TempBlob: Codeunit "Temp Blob"; EDocCount: Integer; HttpRequest: HttpRequestMessage; HttpResponse: HttpResponseMessage) + var + HttpRequestMessage: HttpRequestMessage; + HttpResponseMessage: HttpResponseMessage; + ContentData, DocumentId : Text; + begin + if EDocumentService."Service Integration" <> EDocumentService."Service Integration"::"ExFlow E-Invoicing" then + exit; + + if not HttpResponse.Content.ReadAs(ContentData) then + exit; + + ContentData := this.LeaveJustNewLine(ContentData); + + if not this.ParseReceivedDocument(ContentData, EDocument."Index In Batch", DocumentId) then begin + this.EDocumentErrorHelper.LogSimpleErrorMessage(EDocument, this.DocumentIdNotFoundErr); + exit; + end; + + this.Connection.GetTargetDocumentRequest(DocumentId, HttpRequestMessage, HttpResponseMessage); + this.EDocumentLogHelper.InsertIntegrationLog(EDocument, EDocumentService, HttpRequestMessage, HttpResponseMessage); + + if not HttpResponseMessage.Content.ReadAs(ContentData) then + exit; + + if not this.ParseContentData(ContentData) then + ContentData := ''; + + if ContentData = '' then + this.EDocumentErrorHelper.LogSimpleErrorMessage(EDocument, StrSubstNo(this.CouldNotRetrieveDocumentErr, DocumentId)); + + Clear(TempBlob); + TempBlob.CreateOutStream(TextEncoding::UTF8).WriteText(ContentData); + EDocument."Document Id" := CopyStr(DocumentId, 1, MaxStrLen(EDocument."Document Id")); + this.EDocumentLogHelper.InsertLog(EDocument, EDocumentService, TempBlob, "E-Document Service Status"::Imported); + this.Connection.RemoveDocumentFromReceived(EDocument, HttpRequestMessage, HttpResponseMessage); + this.EDocumentLogHelper.InsertIntegrationLog(EDocument, EDocumentService, HttpRequestMessage, HttpResponseMessage); + end; + + #endregion + +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/Setup/ConnectionSetup.Table.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/Setup/ConnectionSetup.Table.al new file mode 100644 index 0000000000..7c07b7eec2 --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/Setup/ConnectionSetup.Table.al @@ -0,0 +1,92 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; + +table 6381 ConnectionSetup +{ + Access = Internal; + DataPerCompany = false; + DataClassification = CustomerContent; + + fields + { + field(1; PK; Code[10]) + { + Caption = 'PK', Locked = true; + ToolTip = 'PK', Locked = true; + } + field(4; "Authentication URL"; Text[250]) + { + Caption = 'Authentication URL'; + ToolTip = 'Specifies the URL to connect Microsoft Entra.'; + } + field(9; "Company Id"; Text[100]) + { + Caption = 'Company ID'; + ToolTip = 'Specifies the company ID.'; + } + field(10; "Client ID"; Guid) + { + Caption = 'Client ID'; + ToolTip = 'Specifies the client ID.'; + } + field(11; "Client Secret"; Guid) + { + Caption = 'Client Secret'; + ToolTip = 'Specifies the client secret.'; + } + field(12; "Environment Type"; Enum EnvironmentType) + { + Caption = 'Environment Type'; + ToolTip = 'Specifies the environment type.'; + } + field(13; ServiceURL; Text[250]) + { + Caption = 'Service URL'; + ToolTip = 'Specifies the service URL.'; + } + field(20; "Root App ID"; Guid) + { + Caption = 'Root App ID'; + ToolTip = 'Specifies the root app ID.'; + } + field(21; "Root Secret"; Guid) + { + Caption = 'Root App Secret'; + ToolTip = 'Specifies the root application secret.'; + } + field(22; "Root Tenant"; Guid) + { + Caption = 'Root App Tenant'; + ToolTip = 'Specifies the root application tenant.'; + } + field(23; "Root Market URL"; Guid) + { + Caption = 'Root Market URL'; + ToolTip = 'Specifies the root market URL.'; + } + field(24; "Client Tenant"; Guid) + { + Caption = 'Client App Tenant'; + ToolTip = 'Specifies the client application tenant.'; + } + } + + keys + { + key(Key1; PK) + { + Clustered = true; + } + } + + procedure GetSetup(): Boolean + begin + if not IsNullGuid(Rec.SystemId) then + exit(true); + + exit(Rec.Get()); + end; +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/Setup/ConnectionSetupCard.Page.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/Setup/ConnectionSetupCard.Page.al new file mode 100644 index 0000000000..2d804b4c11 --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/Setup/ConnectionSetupCard.Page.al @@ -0,0 +1,213 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; + +using System.Telemetry; +using System.Environment; + +page 6380 ConnectionSetupCard +{ + AdditionalSearchTerms = 'SignUp,electronic document,e-invoice,e-document,external,connection,connector'; + PageType = Card; + SourceTable = ConnectionSetup; + ApplicationArea = Basic, Suite; + UsageCategory = None; + Caption = 'E-Document External Connection Setup'; + Extensible = false; + + + layout + { + area(Content) + { + group(General) + { + field(ClientID; this.ClientID) + { + Caption = 'Client ID'; + ToolTip = 'Specifies the client ID token.'; + ApplicationArea = Basic, Suite; + ExtendedDatatype = Masked; + Visible = not this.IsSaaSInfrastructure; + ShowMandatory = true; + + trigger OnValidate() + begin + this.Authentication.StorageSet(Rec."Client ID", this.ClientID); + end; + } + field(ClientSecret; this.ClientSecret) + { + Caption = 'Client Secret'; + ToolTip = 'Specifies the client secret token.'; + ApplicationArea = Basic, Suite; + ExtendedDatatype = Masked; + Visible = not this.IsSaaSInfrastructure; + ShowMandatory = true; + + trigger OnValidate() + begin + this.SaveSecret(Rec."Client Secret", this.ClientSecret) + end; + } + field(ClientTenant; this.ClientTenant) + { + Caption = 'Client Tenant ID'; + ToolTip = 'Specifies the client tenant id token.'; + ApplicationArea = Basic, Suite; + ExtendedDatatype = Masked; + Visible = not this.IsSaaSInfrastructure; + + trigger OnValidate() + begin + this.Authentication.StorageSet(Rec."Client Tenant", this.ClientTenant); + end; + } + field(RootID; this.RootID) + { + Caption = 'Root App ID'; + ToolTip = 'Specifies the root app id token.'; + ApplicationArea = Basic, Suite; + ExtendedDatatype = Masked; + Visible = not this.IsSaaSInfrastructure; + + trigger OnValidate() + begin + this.Authentication.StorageSet(Rec."Root App ID", this.RootID); + end; + } + field(RootSecret; this.RootSecret) + { + Caption = 'Root Secret'; + ToolTip = 'Specifies the root secret token.'; + ApplicationArea = Basic, Suite; + ExtendedDatatype = Masked; + Visible = not this.IsSaaSInfrastructure; + + trigger OnValidate() + begin + this.SaveSecret(Rec."Root Secret", this.RootSecret) + end; + } + field(RootTenant; this.RootTenant) + { + Caption = 'Root Tenant ID'; + ToolTip = 'Specifies the root tenant id token.'; + ApplicationArea = Basic, Suite; + ExtendedDatatype = Masked; + Visible = not this.IsSaaSInfrastructure; + + trigger OnValidate() + begin + this.Authentication.StorageSet(Rec."Root Tenant", this.RootTenant); + end; + } + field(RootUrl; this.RootUrl) + { + Caption = 'Root Url'; + ToolTip = 'Specifies the root url token.'; + ApplicationArea = Basic, Suite; + ExtendedDatatype = Masked; + Visible = not this.IsSaaSInfrastructure; + + trigger OnValidate() + begin + this.Authentication.StorageSet(Rec."Root Market URL", this.RootUrl); + end; + } + field("Authentication URL"; Rec."Authentication URL") + { + ApplicationArea = Basic, Suite; + } + field(ServiceURL; Rec.ServiceURL) + { + ApplicationArea = Basic, Suite; + } + field("Company Id"; Rec."Company Id") + { + ApplicationArea = Basic, Suite; + ShowMandatory = true; + } + field("Environment Type"; Rec."Environment Type") + { + ApplicationArea = Basic, Suite; + ShowMandatory = true; + } + } + } + } + + actions + { + area(processing) + { + action(InitOnboarding01) + { + ApplicationArea = Basic, Suite; + Caption = 'Open Onboarding'; + Image = Setup; + Promoted = true; + PromotedCategory = Process; + PromotedOnly = true; + Visible = this.IsSaaSInfrastructure; + ToolTip = 'Create client credentials and open the onboarding process in a web browser.'; + + trigger OnAction() + begin + this.Authentication.CreateClientCredentials(); + CurrPage.Update(); + this.SetPageVariables(); + Hyperlink(this.Authentication.GetRootOnboardingUrl()); + this.FeatureTelemetry.LogUptake('', this.ExternalServiceTok, Enum::"Feature Uptake Status"::"Set up"); + end; + } + } + } + + trigger OnOpenPage() + var + EnvironmentInformation: Codeunit "Environment Information"; + begin + this.IsSaaSInfrastructure := EnvironmentInformation.IsSaaSInfrastructure(); + this.Authentication.InitConnectionSetup(); + if Rec.Get() then + ; + this.SetPageVariables(); + this.FeatureTelemetry.LogUptake('', this.ExternalServiceTok, Enum::"Feature Uptake Status"::Discovered); + end; + + local procedure SetPageVariables() + begin + if not IsNullGuid(Rec."Client ID") then + this.ClientID := this.MaskTxt; + if not IsNullGuid(Rec."Client Secret") then + this.ClientSecret := this.MaskTxt; + if not IsNullGuid(Rec."Client Tenant") then + this.ClientTenant := this.MaskTxt; + if not IsNullGuid(Rec."Root App ID") then + this.RootID := this.MaskTxt; + if not IsNullGuid(Rec."Root Secret") then + this.RootSecret := this.MaskTxt; + if not IsNullGuid(Rec."Root Tenant") then + this.RootTenant := this.MaskTxt; + if not IsNullGuid(Rec."Root Market URL") then + this.RootUrl := this.MaskTxt; + end; + + [NonDebuggable] + local procedure SaveSecret(var TokenKey: Guid; Value: SecretText) + begin + this.Authentication.StorageSet(TokenKey, Value); + end; + + var + Authentication: Codeunit Authentication; + FeatureTelemetry: Codeunit "Feature Telemetry"; + [NonDebuggable] + ClientID, ClientSecret, ClientTenant, ClientUrl, RootID, RootSecret, RootTenant, RootUrl : Text; + IsSaaSInfrastructure: Boolean; + ExternalServiceTok: Label 'E-Document - SignUp', Locked = true; + MaskTxt: Label '*', Locked = true; +} \ No newline at end of file diff --git a/Apps/W1/EDocumentConnectors/SignUp/app/src/Setup/EnvironmentType.Enum.al b/Apps/W1/EDocumentConnectors/SignUp/app/src/Setup/EnvironmentType.Enum.al new file mode 100644 index 0000000000..a9ff9166b2 --- /dev/null +++ b/Apps/W1/EDocumentConnectors/SignUp/app/src/Setup/EnvironmentType.Enum.al @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------------ +// 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.EDocumentConnector.SignUp; + +enum 6380 EnvironmentType +{ + Access = Internal; + Caption = 'Environment Type', Locked = true; + + value(0; Production) + { + Caption = 'Production', Locked = true; + } + value(1; Test) + { + Caption = 'Test', Locked = true; + } +} \ No newline at end of file