Codeunit 1545 Workflow Webhook Notification, source in 29

Source29

src/Layers/W1/BaseApp/System/Workflow/WorkflowWebhookNotification.Codeunit.al224 lines, Copyright (c) Microsoft Corporation. MIT

namespace System.Automation;

using System;
using System.Environment;
using System.Utilities;

codeunit 1545 "Workflow Webhook Notification"
{
    // // Intended to be called from a Task (background session)

    EventSubscriberInstance = Manual;
    Permissions = TableData "Workflow Webhook Notification" = rimd;

    trigger OnRun()
    begin
    end;

    var
        RetryCounter: Integer;
        WaitTime: Integer;
        IsInitialized: Boolean;
        NotInitializedErr: Label 'The code unit is not initialized.';
        ArgumentNullErr: Label '%1 cannot be null.', Comment = '%1=Argument';
        ArgumentEmptyErr: Label '%1 cannot be empty.', Comment = '%1=Argument';
        DataIDTxt: Label 'DataID';
        WorkflowStepInstanceIDTxt: Label 'WorkflowStepInstanceID';
        NotificationUrlTxt: Label 'NotificationUrl';
        WorkflowWebhookCategoryLbl: Label 'AL Workflow Webhook', Locked = true;
        WorkflowWebhookCorrelationGuidTxt: Label 'Correlation GUID for workflow webhook notification created: %1', Locked = true;

    procedure Initialize(RetryCount: Integer; WaitTimeInMs: Integer)
    begin
        RetryCounter := RetryCount;
        WaitTime := WaitTimeInMs;
        IsInitialized := true;
    end;

    [Scope('OnPrem')]
    procedure SendNotification(DataID: Guid; WorkflowStepInstanceID: Guid; NotificationUrl: Text; RequestedByUserEmail: Text)
    var
        WorkflowWebhookNotificationTable: Record "Workflow Webhook Notification";
    begin
        if IsInitialized = false then
            Error(NotInitializedErr);

        if IsNullGuid(DataID) then
            Error(ArgumentNullErr, DataIDTxt);

        if IsNullGuid(WorkflowStepInstanceID) then
            Error(ArgumentNullErr, WorkflowStepInstanceIDTxt);

        if NotificationUrl = '' then
            Error(ArgumentEmptyErr, NotificationUrlTxt);

        GetNotificationRecord(WorkflowWebhookNotificationTable, WorkflowStepInstanceID);
        Notify(DataID, WorkflowStepInstanceID, NotificationUrl, RequestedByUserEmail, WorkflowWebhookNotificationTable);
    end;

    procedure StartNotification(WorkflowStepInstanceID: Guid)
    var
        WorkflowWebhookNotificationTable: Record "Workflow Webhook Notification";
    begin
        GetNotificationRecord(WorkflowWebhookNotificationTable, WorkflowStepInstanceID);
        WorkflowWebhookNotificationTable.Status := WorkflowWebhookNotificationTable.Status::Pending;
        WorkflowWebhookNotificationTable.Modify(true);
    end;

    local procedure Notify(DataID: Guid; WorkflowStepInstanceID: Guid; NotificationUrl: Text; RequestedByUserEmail: Text; var WorkflowWebhookNotification: Record "Workflow Webhook Notification")
    var
        DotNetExceptionHandler: Codeunit "DotNet Exception Handler";
        Exception: DotNet Exception;
        HttpWebResponse: DotNet HttpWebResponse;
        WebException: DotNet WebException;
        Retry: Boolean;
        ErrorMessage: Text;
        ErrorDetails: Text;
    begin
        RetryCounter := RetryCounter - 1;
        if OnPostNotificationRequest(DataID, WorkflowStepInstanceID, NotificationUrl, RequestedByUserEmail) then begin
            WorkflowWebhookNotification.Status := WorkflowWebhookNotification.Status::Sent;
            WorkflowWebhookNotification.SetErrorMessage('');
            WorkflowWebhookNotification.SetErrorDetails('');
            WorkflowWebhookNotification.Modify(true);
            Commit();
        end else begin
            Exception := GetLastErrorObject();

            ErrorMessage := Exception.Message;
            ErrorDetails := Exception.StackTrace;

            if RetryCounter > 0 then begin
                Retry := true;
                DotNetExceptionHandler.Collect();
                if DotNetExceptionHandler.CastToType(WebException, GetDotNetType(WebException)) then begin
                    HttpWebResponse := WebException.Response;
                    if not IsNull(HttpWebResponse) then
                        if not IsNull(HttpWebResponse.StatusCode) then begin
                            ErrorDetails := StrSubstNo('%1 - StatusCode: %2 - StatusDescription: %3', ErrorDetails,
                                Format(HttpWebResponse.StatusCode), HttpWebResponse.StatusDescription);
                            Retry := ShouldRetry(HttpWebResponse.StatusCode, HttpWebResponse.StatusDescription);
                        end;
                end;
            end;

            if Retry then begin
                Sleep(WaitTime);
                Notify(DataID, WorkflowStepInstanceID, NotificationUrl, RequestedByUserEmail, WorkflowWebhookNotification);
            end else begin
                WorkflowWebhookNotification.SetErrorMessage(ErrorMessage);
                WorkflowWebhookNotification.SetErrorDetails(ErrorDetails);
                WorkflowWebhookNotification.Status := WorkflowWebhookNotification.Status::Failed;
                WorkflowWebhookNotification.Modify(true);
                Commit();
            end;
        end;
    end;

#pragma warning disable AL0547 // Accepted violation: turning off global variable access is a breaking change for existing subscribers of this published event.
    [TryFunction]
    [IntegrationEvent(true, true)]
    [Scope('OnPrem')]
    procedure OnPostNotificationRequest(DataID: Guid; WorkflowStepInstanceID: Guid; NotificationUrl: Text; RequestedByUserEmail: Text)
    begin
    end;
#pragma warning restore AL0547

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Workflow Webhook Notification", 'OnPostNotificationRequest', '', false, false)]
    [TryFunction]
    local procedure PostNotificationRequest(var Sender: Codeunit "Workflow Webhook Notification"; DataID: Guid; WorkflowStepInstanceID: Guid; NotificationUrl: Text; RequestedByUserEmail: Text)
    begin
        PostHttpRequest(DataID, WorkflowStepInstanceID, NotificationUrl, RequestedByUserEmail);
    end;

    [TryFunction]
    local procedure PostHttpRequest(DataID: Guid; WorkflowStepInstanceID: Guid; NotificationUrl: Text; RequestedByUserEmail: Text)
    var
        HttpWebRequest: DotNet HttpWebRequest;
        HttpWebResponse: DotNet HttpWebResponse;
        RequestStr: DotNet Stream;
        StreamWriter: DotNet StreamWriter;
        Encoding: DotNet Encoding;
        WebhookPayload: Text;
        CorrelationGuid: Text;
    begin
        HttpWebRequest := HttpWebRequest.Create(NotificationUrl);
        CorrelationGuid := LowerCase(System.Format(CreateGuid(), 0, 4));
        HttpWebRequest.Method := 'POST';
        HttpWebRequest.ContentType('application/json');
        HttpWebRequest.Headers.Add('clientRequestId', CorrelationGuid);
        HttpWebRequest.Headers.Add('x-ms-correlation-id', CorrelationGuid);

        Session.LogMessage('0000KX6', StrSubstNo(WorkflowWebhookCorrelationGuidTxt, CorrelationGuid), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', WorkflowWebhookCategoryLbl);

        RequestStr := HttpWebRequest.GetRequestStream();
        StreamWriter := StreamWriter.StreamWriter(RequestStr, Encoding.ASCII);
        WebhookPayload := PrepareWebhookPayload(DataID, WorkflowStepInstanceID, RequestedByUserEmail);
        StreamWriter.Write(WebhookPayload);
        StreamWriter.Flush();
        StreamWriter.Close();
        StreamWriter.Dispose();

        HttpWebResponse := HttpWebRequest.GetResponse();
        HttpWebResponse.Close(); // close connection
        HttpWebResponse.Dispose(); // cleanup of IDisposable
    end;

    local procedure PrepareWebhookPayload(DataID: Guid; WorkflowStepInstanceID: Guid; RequestedByUserEmail: Text) WebhookPayload: Text
    var
        Company: Record Company;
        EnvironmentInformation: Codeunit "Environment Information";
        JsonPayload: JsonObject;
    begin
        JsonPayload.Add('Row Id', LowerCase(Format(DataID, 0, 4)));
        JsonPayload.Add('Workflow Step Id', LowerCase(Format(WorkflowStepInstanceID, 0, 4)));
        JsonPayload.Add('Requested By User Email', RequestedByUserEmail);

        if Company.ReadPermission then
            if Company.Get(CompanyName()) then
                JsonPayload.Add('Company Id', LowerCase(Format(Company.Id, 0, 4)));

        if EnvironmentInformation.IsSaaS() then
            JsonPayload.Add('Environment Name', EnvironmentInformation.GetEnvironmentName());

        JsonPayload.WriteTo(WebhookPayload);
    end;

    local procedure FindNotificationRecord(var WorkflowWebhookNotificationTable: Record "Workflow Webhook Notification"; WorkflowStepInstanceID: Guid): Boolean
    begin
        // Fetch current notification record
        WorkflowWebhookNotificationTable.SetRange("Workflow Step Instance ID", WorkflowStepInstanceID);
        exit(WorkflowWebhookNotificationTable.FindFirst());
    end;

    local procedure GetNotificationRecord(var WorkflowWebhookNotificationTable: Record "Workflow Webhook Notification"; WorkflowStepInstanceID: Guid)
    begin
        if not FindNotificationRecord(WorkflowWebhookNotificationTable, WorkflowStepInstanceID) then begin
            // Create Notification Record
            WorkflowWebhookNotificationTable.Init();
            WorkflowWebhookNotificationTable."Workflow Step Instance ID" := WorkflowStepInstanceID;
            WorkflowWebhookNotificationTable.Status := WorkflowWebhookNotificationTable.Status::Pending;
            WorkflowWebhookNotificationTable.Insert(true);
            FindNotificationRecord(WorkflowWebhookNotificationTable, WorkflowStepInstanceID);
        end;
    end;

    procedure GetCurrentRetryCounter(): Integer
    begin
        exit(RetryCounter);
    end;

    procedure ShouldRetry(StatusCode: Integer; StatusDescription: Text): Boolean
    begin
        if StatusCode = 404 then
            if (StatusDescription = 'WorkflowNotFound') or (StatusDescription = 'WorkflowTriggerVersionNotFound') then
                exit(false);

        if (StatusCode = 400) and (StatusDescription = 'WorkflowTriggerIsNotEnabled') then
            exit(false);

        exit(true);
    end;
}