<?xml version="1.0" encoding="utf-8"?>
<AlvaoApplication xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" ModelVersion="1">
  <Applications>
    <Application id="2154">
      <Name>Jira Connector</Name>
      <UniqueId>657a0214-58d2-4318-a8cb-59908c2d7b38</UniqueId>
      <Version>4</Version>
      <AdvancedSettings>
        <Setting>
          <Name>JiraIntegrationToken0</Name>
          <Value />
        </Setting>
      </AdvancedSettings>
      <Scripts>
        <Script id="4108">
          <Name>Settings</Name>
          <Code>using System.Collections.Generic;

public class Settings
{
    // API URL and credential of THIS vendor integration.
    public static string ConnectorType = "Jira";
 
    // Text configuration
    // Subject of error (in ticket log) when fails operation "Create ticket"
    public static string CreateTicketErrorSubject = "Failed to create ticket in external system";
    // Subject of error (in ticket log) when fails operation "Update ticket"
    public static string UpdateTicketErrorSubject = "Failed to update ticket in external system";
    // Subject of error (in ticket log) when fails operation "Create act"
    public static string CreateActErrorSubject = "Failed to create comment in external system";

    public const string IssueCreatedMessage = "Issue automatically created in Jira";
 
    // Custom Columns configuration
    public const string AppSettingsPersonColumn = "ExtAppSettings";
    public const string ExternalTicketIdTicketColumn = "ExtAppTicketId";
    public const string StatusMapName = "StatusMap";
    public const string AttributeMapName = "AttributeMap";
    public const string SyncCommunicationName = "SyncCommunication";
    public const string TicketReporterName = "TicketReporter";

    public const string JiraProjectKeySettingsName = "JiraProjectKey";
    public const string JiraIssueTypeNameSettingsName = "JiraIssueTypeName";
    public const string JiraExternalIdColumnNameSettingsName = "JiraExternalIdColumnName";
    public const string JiraDescriptionTemplateSettingsName = "JiraDescriptionTemplate";

    public const string OriginalMessageSeparator = @"&lt;hr id=""origmsg""&gt;";
}
</Code>
          <IsLibCode>true</IsLibCode>
          <Codesign>oVFxl/3ThsIsVYdkrFsP0fMHhqmhUpjXjszp+mDc5RsUqmlxq77Ky1NIJfLDLrh34GvQdulu6h7sPG4Z9RBpj3xVcRW9Xx/m5ssUu7dakJ/d1VfIl0FecSlTw8dkqkV+Y/FPYXaoNqDSf1eOaPCy5RMdACztaGLZzfcCsnxTpXKj16mUP1rRHZPilmcTJKozOpEGaUibI3jfC2mk7XEPyX0ugrrUdhHP8qrG32WJXwfTv+EJLh+wCumArWGvG8LuL0UVZWC2++42oppRqNkh6LnVny2RwKmjvxqE1ePdIsUyhMqlWpUiV8yOmhGa6EEAQ1mImvM2mpaML7jX4kG3oQ==</Codesign>
        </Script>
        <Script id="4109">
          <Name>Helpers</Name>
          <Code>using Alvao.API.Common.Model;
using Alvao.API.Common.Model.Database;
using Alvao.API.SD;
using Alvao.Context;
using System;
using System.Collections.Generic;
using System.Data;
using Microsoft.Data.SqlClient;
using Dapper;
using System.Linq;
using Newtonsoft.Json.Linq;
 
public class Helpers
{
    public static string GetExternalTicketId(int ticketId, string externalTicketIdTicketColumn)
    {
        return Alvao.API.Common.CustomColumn.GetValue("tHdTicketCust", externalTicketIdTicketColumn, ticketId, new System.Globalization.CultureInfo(1033), string.Empty);
    }
 
    public static void SaveExternalTicketId(int ticketId, object externalTicketId, string externalTicketIdTicketColumn)
    {
        Alvao.API.Common.CustomColumn.UpdateValues(
            new Alvao.API.Common.Model.CustomColumnsModel() 
            {
                CustomTableName = "tHdTicketCust", 
                EntityId = ticketId, 
                ColumnValues = new List&lt;ColumnValue&gt;() 
                { 
                    new ColumnValue() 
                    { 
                        ColumnName = externalTicketIdTicketColumn, 
                        Value = externalTicketId 
                    } 
                } 
            }
        );
    }
    
    public static Service GetService(int serviceId, string extAppSettingsPersonColumn)
    {
        var defaultService = new Service() 
        { 
            ServiceId = serviceId, 
            ExtAppSettings = string.Empty 

        };
 
        using(var scope = AlvaoContext.GetConnectionScope())
        {
            string query = string.Format(@"
                select {0} as ExtAppSettings, iHdSectionId as ServiceId
                from tHdSection s
                inner join tHdSectionCust sc on sc.liHdSectionId = s.iHdSectionId
                where s.iHdSectionId = @serviceId", extAppSettingsPersonColumn);
 
            var service = scope.Connection.QueryFirstOrDefault&lt;Service&gt;(query, new { serviceId }, scope.Transaction);
            return service ?? defaultService;
        }
    }
 
    public static Service GetTicketService(int ticketId, string extAppSettingsPersonColumn)
    {
        var ticket = Alvao.API.SD.Ticket.GetById(ticketId);
        return GetService(ticket.liHdTicketHdSectionId, extAppSettingsPersonColumn);
    }
 
    public static tAct GetActById(int actId)
    {
        using(var scope = AlvaoContext.GetConnectionScope())
        {
            return scope.Connection.QueryFirstOrDefault&lt;tAct&gt;("select * from tAct where iActId = @actId", new {actId}, scope.Transaction);
        }
    }
 
    public static IEnumerable&lt;tDocument&gt; GetActDocuments(int actId)
    {
        using(var scope = AlvaoContext.GetConnectionScope())
        {
            return scope.Connection.Query&lt;tDocument&gt;("select * from tDocument where liDocumentActId = @actId", new {actId}, scope.Transaction);
        }
    }
 
    public static IVendorIntegration GetVendorIntegration()
    {
        var vendorInterface = typeof(IVendorIntegration);
        var assemblies = AppDomain.CurrentDomain.GetAssemblies();
        foreach(var assembly in assemblies)
        {
            foreach(var at in assembly.GetTypes())
            {
                if (vendorInterface.IsAssignableFrom(at))
                {
                    if (!at.IsInterface)
                        return (IVendorIntegration)Activator.CreateInstance(at);
                }
            }
        }
 
        throw new InvalidOperationException();
    }
 
    public static void LogError(int ticketId, string errorSubject, Exception ex)
    {
        Alvao.API.SD.Act.Create(
            ticketId, 
            errorSubject, 
            new HtmlTextModel(Alvao.Global.StringLib.HtmlEncode2(ex.ToString())),
            Alvao.API.Common.Person.GetSystem().iPersonId,
            null,
            Alvao.API.Common.Model.Database.tActKind.ActKind.Other,
            new Alvao.API.SD.Model.ActCreateSettings { IgnoreRights = true }
        );

        var logger = Alvao.API.Internal.TenantDiagnosticsLog.Get();
        ex.Data.Add("Ticket ID", ticketId);
        logger.Error(ex, $"Jira Connector encountered an error: {errorSubject}");
    }
}
 
public class Service
{
    public int ServiceId { get; set; }
    public string ExtAppSettings { get; set; }

    public bool IsExternalSystemConnector(string connectorType)
    {
        string serviceConnector = GetExtAppSettingsItem&lt;string&gt;("ConnectorType");
        return connectorType.Equals(serviceConnector, StringComparison.OrdinalIgnoreCase);
    }

    public string GetExtAppUrl()
    {
        return GetExtAppSettingsItem&lt;string&gt;("URL");
    }

    public bool GetExtAppSynchronizeAllCommunication()
    {
        return GetExtAppSettingsItem&lt;bool&gt;("SynchronizeAllCommunication");
    }

    public T GetExtAppSettingsItem&lt;T&gt;(string itemName)
    {
        if (string.IsNullOrEmpty(ExtAppSettings))
            return default(T);

        var settings = JObject.Parse(ExtAppSettings);
        var item = settings[itemName];

        if (item != null)
        {
            return item.ToObject&lt;T&gt;();
        }

        return default(T);
    }
}
 
public interface IVendorIntegration
{
    string ExternalSystemApiUrl { get; set; }
    Service IntegratedService { get; set; }
    string CreateTicket(int ticketId, SqlConnection con, SqlTransaction trans);
    void UpdateTicket(int ticketId, string changedProperties, string externalTicketId, SqlConnection con, SqlTransaction trans);
    void CreateAct(int actId, string externalTicketId, SqlConnection con, SqlTransaction trans);
}</Code>
          <IsLibCode>true</IsLibCode>
          <Codesign>OrED8R4pBnn0oJpEGU6wKxlHIBipV+cUCNfd1i0upGlVOvTi+2RAfI7+1AAU6lxFemSbpHhsjKze4AdXCah/U2U+CJ/20jo3v1zbRAu7wZAWiCz3U+Z0Xzzuj3fL9tzxQnPjM1SAtIthN4Pk1ika/yWSz23asiECE4EXf+3LRY5atjCCIV8Pxi6clZOZIx3Zzf0cjHfcTeIOxJ59jThYwBigSI38nODvA76G89OfZhUw+OhQ/TKmV3mtBCAOYqoF/DRdkaMTJjXYTZrUAxcnDGmFsm2FpSWT8XVzTg2mAA+6l+wxapSBdVJxi6sprvcpS0g066IJPKrKGy5BM430IQ==</Codesign>
        </Script>
        <Script id="4110">
          <Name>AlvaoVendorIntegrationAutoActions</Name>
          <Code>using System;
using System.Data;
using Microsoft.Data.SqlClient;
using Alvao.Global;
using Alvao.API.Common;
using Alvao.API.SD.Model;
using Alvao.Apps.API;
using System.Net.Http;
using System.Net.Http.Headers;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using System.Text;
using Alvao.API.Common.Model.Database;
using Alvao.API.Utils;
using System.Linq;
using System.IO;
using System.Collections.Generic;
using Alvao.Context;
using Dapper;
using Alvao.API.SD;
using System.Globalization;
using System.Text.RegularExpressions;

public class AlvaoVendorIntegrationAutoActions : ITicketAutoAction, IActAutoAction
{
    public string Name { get; set; }
    private IVendorIntegration vendor;
 
    public AlvaoVendorIntegrationAutoActions()
    {
        Name = "Vendor Integration: " + Settings.ConnectorType;
        vendor = Helpers.GetVendorIntegration();
    }

    private bool LicenceCheck() =&gt; Alvao.API.Common.Activation.IsModuleActivated(Alvao.Global.ModuleInfo.ModuleId.DevOpsConnectors);
 
    public void OnTicketChanged(SqlConnection con, SqlTransaction trans, int ticketId, int personId, string properties)
    {
        if(!LicenceCheck())
            return;

        Service service = Helpers.GetTicketService(ticketId, Settings.AppSettingsPersonColumn);
        if (service.IsExternalSystemConnector(Settings.ConnectorType) &amp;&amp; personId != Alvao.API.Common.Person.GetSystem().iPersonId)
        {
            string externalTicketId = Helpers.GetExternalTicketId(ticketId, Settings.ExternalTicketIdTicketColumn);
            var ticket = Ticket.GetById(ticketId);
 
            vendor.ExternalSystemApiUrl = service.GetExtAppUrl();
            vendor.IntegratedService = service;
            var statusMap = service.GetExtAppSettingsItem&lt;Dictionary&lt;string, string&gt;&gt;(Settings.StatusMapName);

            Alvao.API.Common.Model.Database.TicketState ticketState;
            if (string.IsNullOrEmpty(externalTicketId) &amp;&amp; properties.Contains("tHdTicket.TicketStateId") &amp;&amp; statusMap.ContainsValue((ticketState = Alvao.API.SD.TicketState.GetById(ticket.TicketStateId))?._TicketState))
            {
                if(ticketState.TicketStateBehaviorId == (int)TicketStateBehavior.BehaviorId.Reopen)
                    return; // dont create ticket when ticket has been reopened

                try
                {
                    externalTicketId = vendor.CreateTicket(ticketId, con, trans);
                    Helpers.SaveExternalTicketId(ticketId, externalTicketId, Settings.ExternalTicketIdTicketColumn);
                }
                catch (Exception e)
                {
                    Helpers.LogError(ticketId, Settings.CreateTicketErrorSubject, e);
                }
            }
            else
            {
                try
                {
                    vendor.UpdateTicket(ticketId, properties, externalTicketId, con, trans);
                }
                catch (Exception e)
                {
                    Helpers.LogError(ticketId, Settings.UpdateTicketErrorSubject, e);
                }
            }
        }
    }
 
    public void OnActCreated(SqlConnection con, SqlTransaction trans, int actId, int personId)
    {
        if(!LicenceCheck())
            return;

        var act = Helpers.GetActById(actId);

        if(!act.liActHdTicketId.HasValue)
            return;
                    
        if(act.liActFromPersonId == Alvao.API.Common.Person.GetSystem().iPersonId)
            return;

        if (act.liActKindId == (int)tActKind.ActKind.Process || act.liActKindId == (int)tActKind.ActKind.Other || act.liActKindId == (int)tActKind.ActKind.Notification)
            return;

        int ticketId = act.liActHdTicketId.Value;
        Service service = Helpers.GetTicketService(ticketId, Settings.AppSettingsPersonColumn);

        if (!service.IsExternalSystemConnector(Settings.ConnectorType))
            return;

        if(!service.GetExtAppSettingsItem&lt;bool&gt;(Settings.SyncCommunicationName))
            return;

        string externalTicketId = Helpers.GetExternalTicketId(ticketId, Settings.ExternalTicketIdTicketColumn);
 
        if (string.IsNullOrEmpty(externalTicketId))
            return;

        var ticket = Ticket.GetById(ticketId);
        if(ticket != null &amp;&amp; ticket.liHdTicketStartingActId == actId)
            return;

        vendor.ExternalSystemApiUrl = service.GetExtAppUrl();
        vendor.IntegratedService = service;
        
        try
        {
            vendor.CreateAct(actId, externalTicketId, con, trans);
        }
        catch (Exception e)
        {
            Helpers.LogError(ticketId, Settings.CreateActErrorSubject, e);
        }
    }
 
    public void OnTicketCreated(SqlConnection con, SqlTransaction trans, int ticketId, int personId)
    {
        // only solver -&gt; automatically assigned -&gt; changed to status which is in statusMapping
        OnTicketChanged(con, trans, ticketId, personId, "tHdTicket.TicketStateId");
    }
 
    public void OnActChanged(SqlConnection con, SqlTransaction trans, int actId, int personId, string properties)
    {
        throw new NotImplementedException();
    }
 
    public void OnActRemoved(SqlConnection con, SqlTransaction trans, int actId, int personId)
    {
        throw new NotImplementedException();
    }
}

public class JiraResponse 
{
    public string Key { get; set; }
}

public class JiraUser {
    public string Name { get; set; }
}

public class JiraTransitionsResponse 
{
    public IEnumerable&lt;JiraTransition&gt; Transitions { get; set; }
}

public class JiraTransition 
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class JiraAttachment 
{
    public string Filename { get; set; }

}

public static class tDocumentExtension 
{
    public static string GetJiraAttachmentName(this tDocument doc)
    {
        return $"{doc.iDocumentId}_{doc.sDocument}";
    }
}

public class JiraIntegration : IVendorIntegration
{
    public string ExternalSystemApiUrl { get; set; }
    public Service IntegratedService { get; set; }
    private static readonly HttpClient client = new HttpClient();

    public void CreateAct(int actId, string externalTicketId, SqlConnection con, SqlTransaction trans)
    {
        var act = Helpers.GetActById(actId);
        if (act is null)
            return;
        var docs = Helpers.GetActDocuments(actId);

        var message = PrepareMessage(act.ActHtml, ref docs);
        UploadAttachments(docs, externalTicketId, act.liActHdTicketId.Value);

        var createComment = new {
            body = $@"{act.sActFrom} wrote: 
            
            {act.sAct}
            
            {message}"
        };

        var request = PrepareRequest(HttpMethod.Post, $"issue/{externalTicketId}/comment", createComment );
        var response = client.SendAsync(request).Result;
        var responseContent = response.Content.ReadAsStringAsync().Result;
        if (!response.IsSuccessStatusCode)
            throw new Exception($"Unsuccessful response from JIRA: {response.StatusCode} {responseContent}");
    }

    private static string PrepareMessage(string message, ref IEnumerable&lt;tDocument&gt; docs)
    {
        if(message.Contains(Settings.OriginalMessageSeparator))
        {
            var messageParts = message.Split(new string[] {Settings.OriginalMessageSeparator}, 2, StringSplitOptions.None);
            if(messageParts.Count() == 2)
            {
                message = messageParts[0]; // remove original message
            }
        }

        List&lt;int&gt; docIdsToRemove = new();
        foreach(var intextImg in docs.Where(d =&gt; d.EmbededImage))
        {
            var intextImgTag = $"[$cid:{intextImg.iDocumentId}$]";
            
            if(message.Contains(intextImgTag))
                message = message.Replace(intextImgTag, $"\"&gt;!{intextImg.GetJiraAttachmentName()}|thumbnail!&lt;img src=\""); // extract CID, images will be removed in StripHtml
            else // remove attachment, it is from original message 
                docIdsToRemove.Add(intextImg.iDocumentId);
        }

        docs = docs.Where(d =&gt; !docIdsToRemove.Contains(d.iDocumentId));

        message = Alvao.Global.StringLib.StripHtml(message);

        var atts = docs.Where(d =&gt; !d.EmbededImage);
        if(atts.Any())
        {
            message += @"

            Attachments: 
            ";

            foreach(var att in atts)
            {
                message += $"[^{att.GetJiraAttachmentName()}]" + Environment.NewLine;
            }
        }
        return message;
    }

    private HttpRequestMessage PrepareRequest(HttpMethod method, string url, object content = null){
        var httpRequest = new HttpRequestMessage(method, $"{ExternalSystemApiUrl}/rest/api/2/{url}");
        
        httpRequest = AddAuth(httpRequest);

        var headers = IntegratedService.GetExtAppSettingsItem&lt;Dictionary&lt;string, string&gt;&gt;("CustomHttpHeaders");
        if(headers != null &amp;&amp; headers.Any())
        {
            foreach(var header in headers)
            {
                httpRequest.Headers.Add(header.Key, header.Value);
            }
        }

        if(content != null){
            DefaultContractResolver contractResolver = new DefaultContractResolver
            {
                NamingStrategy = new CamelCaseNamingStrategy()
            };
            var settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ContractResolver = contractResolver, };

            var strContent = JsonConvert.SerializeObject(content, settings);

            httpRequest.Content = new StringContent(strContent, Encoding.UTF8, "application/json");
        }
        return httpRequest;
    }

    private HttpRequestMessage AddAuth(HttpRequestMessage httpRequest)
    {
        string token = GetToken();
        
        if(IsTokenForJiraCloud(token))
            httpRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(token)));
        else
            httpRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
        
        return httpRequest;
    }

    private string GetToken()
    {
        var tokenNumber = IntegratedService.GetExtAppSettingsItem&lt;string&gt;("AccessToken");

        using (var scope = AlvaoContext.GetConnectionScope())
        {
            var token = scope.Connection.ExecuteScalar&lt;string&gt;("SELECT TOP 1 sPropertyValue FROM tProperty WHERE sProperty = @propertyName",
                new { propertyName = $"JiraIntegrationToken{tokenNumber}" }, scope.Transaction);
            return token ?? string.Empty;
        }
        
    }

    private static bool IsTokenForJiraCloud(string token)
    {
        // jira cloud token = &lt;email:token&gt; sent as basic auth
        // jira server token = &lt;token&gt; sent as bearer auth

        string pattern = @"^[^@\s]+@[^@\s]+\.[^@\s]+:.+$";
        return Regex.IsMatch(token, pattern);
    } 

    private string CheckReporterExists(string username)
    {
        var request = PrepareRequest(HttpMethod.Get, $"user?username={username}");
        var response = client.SendAsync(request).Result;
        var responseContent = response.Content.ReadAsStringAsync().Result;
        if (response.IsSuccessStatusCode)
        {
            var user = JsonConvert.DeserializeObject&lt;JiraUser&gt;(responseContent);
            return user?.Name;
        }
        return null;
    }

    public string CreateTicket(int ticketId, SqlConnection con, SqlTransaction trans)
    {
        tHdTicket ticket = Alvao.API.SD.Ticket.GetById(ticketId);

        string projectKey = IntegratedService.GetExtAppSettingsItem&lt;string&gt;(Settings.JiraProjectKeySettingsName);
        string issueTypeName = IntegratedService.GetExtAppSettingsItem&lt;string&gt;(Settings.JiraIssueTypeNameSettingsName);
        string externalIdColumnName = IntegratedService.GetExtAppSettingsItem&lt;string&gt;(Settings.JiraExternalIdColumnNameSettingsName);
        string reporter = IntegratedService.GetExtAppSettingsItem&lt;string&gt;(Settings.TicketReporterName);

        Dictionary&lt;string, object&gt; attrMap = IntegratedService.GetExtAppSettingsItem&lt;Dictionary&lt;string, object&gt;&gt;(Settings.AttributeMapName);
        if(!string.IsNullOrEmpty(reporter) &amp;&amp; reporter.ToLower() != "default")
        {
            if(reporter.ToLower() == "requester")
            {
                var requester = Alvao.API.Common.Person.GetById(ticket.liHdTicketUserPersonId);
                var username = CheckReporterExists(requester?.sPersonLogin);
                if(!string.IsNullOrEmpty(username)){
                    attrMap.Add("reporter.name", username);
                }
            }
            else 
                attrMap.Add("reporter.name", reporter);
        }

        var createIssue = new 
        {
            fields = CreateRequestFromSetting(attrMap, ticketId, ticket.liHdTicketHdSectionId)
        };

        var request = PrepareRequest(HttpMethod.Post, "issue", createIssue );
        var response = client.SendAsync(request).Result;      

        var responseContent = response.Content.ReadAsStringAsync().Result;
        if (response.IsSuccessStatusCode)
        {
            var responseObject = JsonConvert.DeserializeObject&lt;JiraResponse&gt;(responseContent);
            string externalId = responseObject.Key;
            
            // we have to upload attachments after the ticket creation
            var docs = Helpers.GetActDocuments(ticket.liHdTicketStartingActId.Value);
            UploadAttachments(docs, externalId, ticketId);

            Act.Create(
                ticketId, 
                Settings.IssueCreatedMessage, 
                new Alvao.API.Common.Model.HtmlTextModel(Settings.IssueCreatedMessage), 
                Alvao.API.Common.Person.GetSystem().iPersonId, 
                null, 
                tActKind.ActKind.Process, 
                new Alvao.API.SD.Model.ActCreateSettings(){CallCustomApps = false}
                );
            return externalId;
        }
        else
        {
            throw new Exception($"Unsuccessful response from JIRA: {response.StatusCode} {responseContent}");
        }
    }

    public void UploadAttachments(IEnumerable&lt;tDocument&gt; documents, string issueKey, int ticketId)
    {
        if(documents == null || !documents.Any())
            return;
        try
        {
            using (var content = new MultipartFormDataContent())
            {
                foreach(var doc in documents)
                {
                    var fileContent = new ByteArrayContent(doc.oDocument);
                    fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/form-data");
                    content.Add(fileContent, "file", doc.GetJiraAttachmentName());
                }            
                var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"{ExternalSystemApiUrl}/rest/api/2/issue/{issueKey}/attachments");
                httpRequest.Content = content;

                httpRequest = AddAuth(httpRequest);
                    
                httpRequest.Headers.Add("X-Atlassian-Token", "no-check");

                var headers = IntegratedService.GetExtAppSettingsItem&lt;Dictionary&lt;string, string&gt;&gt;("CustomHttpHeaders");
                if(headers != null &amp;&amp; headers.Any())
                {
                    foreach(var header in headers)
                    {
                        httpRequest.Headers.Add(header.Key, header.Value);
                    }
                }

                var response = client.SendAsync(httpRequest).Result;
                var contentResult = response.Content.ReadAsStringAsync().Result;

                if (!response.IsSuccessStatusCode)
                    throw new Exception($"Unsuccessful response from JIRA: {response.StatusCode} {contentResult}");
            }
        }
        catch (Exception ex)
        {
            Helpers.LogError(ticketId, "Attachment upload failed", ex);
        }
    }

    public static Dictionary&lt;string, object&gt; CreateRequestFromSetting(Dictionary&lt;string, object&gt; input, int ticketId, int serviceId) 
    {
        if(input == null)
            throw new Exception("AttributeMap is not set");
        
        var culture = CultureInfo.GetCultureInfo(GetServiceLanguage(serviceId));

        return input.Select(kv =&gt; GetValue(kv.Key, Evaluate(kv.Value, ticketId, culture))).ToDictionary(kv =&gt; kv.Key, kv =&gt; kv.Value);
    }

    private static int GetServiceLanguage(int serviceId){
        using(var scope = AlvaoContext.GetConnectionScope()){
            return scope.Connection.ExecuteScalar&lt;int&gt;(
                "SELECT COALESCE((select DefaultLanguageId from tHdSection where iHdSectionId=@serviceId),(SELECT iPropertyValue FROM tProperty WHERE sProperty = 'DefaultLanguageId'), iDbLocaleId) FROM tDb",
                new { serviceId }, scope.Transaction );
        }
    }

    private static KeyValuePair&lt;string, object&gt; GetValue(string key, object value)
    {
        int separatorIdx = key.IndexOf(".");
        if(separatorIdx &gt; 0)
        {
            string part1 = key.Substring(0, separatorIdx);
            string part2 = key.Substring(separatorIdx + 1);
			var val = GetValue(part2, value);
            return new KeyValuePair&lt;string, object&gt;(part1, new Dictionary&lt;string, object&gt;() {{val.Key, val.Value}});
        }
        else return new KeyValuePair&lt;string, object&gt;(key, value);
    }

    private static object Evaluate(object value, int ticketId, CultureInfo culture)
    {
        if(value is string)
        {
            string strValue = (string)value;
            string result = string.Empty;
            int? descriptionActId = 0;

            if(strValue == TemplatesVariablesConsts.OriginalMessage.Wrap())
                descriptionActId = Ticket.GetById(ticketId)?.liHdTicketStartingActId;
            else if(strValue == TemplatesVariablesConsts.LatestImportantMessage.Wrap())
            {
                using var scope = AlvaoContext.GetConnectionScope(); 
                descriptionActId = scope.Connection.QueryFirstOrDefault&lt;int&gt;(@"select top 1 iActId
                                                                               from tAct where ActMarkId = 1
                                                                               and liActHdTicketId = @ticketId
                                                                              order by dAct desc", new { ticketId }, scope.Transaction);
            }
            else
                return Alvao.Global.StringLib.StripHtml(
                    Alvao.API.SD.MessageTemplate.EvaluateMessageForRequester(
                        strValue,
                        ticketId,
                        culture,
                        formatDateTimeToUniversalSortable: true));

            if(descriptionActId.HasValue &amp;&amp; descriptionActId &gt; 0)
            {
                var docs = Helpers.GetActDocuments(descriptionActId.Value);
                var act = Helpers.GetActById(descriptionActId.Value);    
                return PrepareMessage(act.ActHtml, ref docs);
            }
        }
        return value;
    }

    private static object[] GetActAttachments(int actId)
    {
        var documents = Helpers.GetActDocuments(actId);
        return DocumentsToAttachments(documents);
    }

    private static object[] DocumentsToAttachments(IEnumerable&lt;tDocument&gt; documents){
        if(documents == null){
            return null;
        }
        return documents.Where(d =&gt; !d.EmbededImage).Select(d =&gt; new { name = d.sDocument, data = System.Convert.ToBase64String(d.oDocument) }).ToArray();
    }

    public void UpdateTicket(int ticketId, string changedProperties, string externalTicketId, SqlConnection con, SqlTransaction trans)
    {
        if(string.IsNullOrEmpty(changedProperties))
            return;

        if(changedProperties.Contains("tHdTicket.TicketStateId"))
        {
            var ticket = Alvao.API.SD.Ticket.GetById(ticketId);
            var statusMap = IntegratedService.GetExtAppSettingsItem&lt;Dictionary&lt;string, string&gt;&gt;(Settings.StatusMapName);
            var status = Alvao.API.SD.TicketState.GetById(ticket.TicketStateId);
            string statusName = status?._TicketState;
            var state = statusMap.FirstOrDefault(m =&gt; m.Value == statusName);

            if(string.IsNullOrEmpty(state.Key)){
                return;
            }

            var transitionId = GetJiraTransitionByName(state.Key, externalTicketId);

            if(!transitionId.HasValue)
                return;

            int? messageId = null;
            if(status.TicketStateBehaviorId == (int)TicketStateBehavior.BehaviorId.Resolve 
            || status.TicketStateBehaviorId == (int)TicketStateBehavior.BehaviorId.Close 
            || status.TicketStateBehaviorId == (int)TicketStateBehavior.BehaviorId.Reopen)
            {
                messageId = GetChangeStatusMessage(ticketId);
            }

            var createTransition = new {
                transition = new {
                    id = transitionId
                },
            };

            var request = PrepareRequest(HttpMethod.Post, $"issue/{externalTicketId}/transitions", createTransition );
            var response = client.SendAsync(request).Result;
            var responseContent = response.Content.ReadAsStringAsync().Result;
            if (!response.IsSuccessStatusCode)
            {
                throw new Exception($"Unsuccessful response from JIRA: {response.StatusCode} {responseContent}");
            }  
            if(messageId.HasValue)
                CreateAct(messageId.Value, externalTicketId, con, trans);
        }    
    }

    public int? GetChangeStatusMessage(int ticketId){
        using(var scope = AlvaoContext.GetConnectionScope())
        {
            return scope.Connection.ExecuteScalar&lt;int?&gt;("select top 1 iActId from tAct where liActHdTicketId = @ticketId and ActOperationId in (3,4) order by dAct desc", new { ticketId }, scope.Transaction);
        }
    }

    public int? GetJiraTransitionByName(string name, string externalTicketId)
    {
        var request = PrepareRequest(HttpMethod.Get, $"issue/{externalTicketId}/transitions");
        var response = client.SendAsync(request).Result;
        var responseContent = response.Content.ReadAsStringAsync().Result;
        if (response.IsSuccessStatusCode)
        {
            var transitions = JsonConvert.DeserializeObject&lt;JiraTransitionsResponse&gt;(responseContent);
            var t = transitions.Transitions.Where(t =&gt; t.Name.ToLower() == name.ToLower()).FirstOrDefault();
            return t?.Id;
        }
        return null;
    }
}</Code>
          <IsLibCode>false</IsLibCode>
          <Codesign>SDBYll5LPyy74v5Rw0AUB7PNXzRXOgD2cVdpHMWFSXIgYMJqzAytek6HBInf4/XpENhsR6hABHo90Wr+aCRmjPxoCmJWnoxEKPotVHno/bxc5Tz8+d0ytKh7VapATI0wfCUQvJX+eokSC6GMyLEiEg7Es9xDa2+i19IB8oNlWQtN2gxSdAkEKcNU0Tt2i+xA1Q+zFavEOocrPAIDHb32w6oRrPxQNeTnXes+/73EYM8djFlan5iZ3DoDi7D1v6sSEN656GSgPyLQn4pUhs8XOgOOn4I6Qo4kkLdXbPlScG5gT3fdNJtTkKxFIXpleBWwIDpMHK0ZOp5LP6dhHp0GYQ==</Codesign>
        </Script>
        <Script id="4111">
          <Name>OpenExternalTicketEntityCommand</Name>
          <Code>using System;
using System.Data;
using Alvao.Global;
using Alvao.API.Common;
using Alvao.API.Common.Model.CustomApps;
using Alvao.Apps.API;
using Alvao.API.Common.Model;

public class OpenExternalTicketEntityCommand : IEntityCommand 
{
    public string Id {get; set;}
    public Entity Entity {get; set;}

    public OpenExternalTicketEntityCommand()
    {
        Id = "OpenExternalTicketEntityCommand";
        Entity = Entity.Request;
    }

    public EntityCommandShowResult Show(int entityId, int personId)
    {   
        bool moduleActivated = Alvao.API.Common.Activation.IsModuleActivated(Alvao.Global.ModuleInfo.ModuleId.DevOpsConnectors);
        
        int position = 2;
        string icon = "open_20_regular";
        string name = "Open related Jira ticket";  
        bool show = moduleActivated &amp;&amp; !string.IsNullOrEmpty(Database.ReadColumn(entityId, CustomTables.tHdTicket, Settings.ExternalTicketIdTicketColumn)); // Set if command should show for person
        return new EntityCommandShowResult (show, name, icon, position);
    }

    public CommandResult Run(int entityId, int personId)
    {
        Service service = Helpers.GetTicketService(entityId, Settings.AppSettingsPersonColumn);
        var extId = Database.ReadColumn(entityId, CustomTables.tHdTicket, Settings.ExternalTicketIdTicketColumn);
        MessageType messageType = MessageType.None ;
        string messageText = null; 
        string navigateToUrl = service.GetExtAppUrl() + "/browse/" + extId; 
        return new CommandResult(messageType, messageText, navigateToUrl);
    }
}</Code>
          <IsLibCode>false</IsLibCode>
          <Codesign>su5j/3KZTFJtCo2j5JhKKa2PfMqQo3B5QFr0p7W3Ue0mjsXoAiMkxGkC4F39FqEcduA+G47RLbQb04X6uEt50F/6DImAcEMmqHoZBoRC3zU0Nl1HxV1d3T8Ow09BhloA54ueOmp3di1dpDToqCr5EFrmqDc4McOD/Hx65U9Vj18KQUcrQAdmOBWzdFFvUKZmXSgcQR5q0gokR2UXVvXLx99mQnPmYvkyElmHcjlNfPYpD1mcSD6u0xntYAHE0Nrof6Dn8bGGEqWFjP3+V11NZ+BjCHQ2PZAc3AZzjSMCrC2jOhrjcyKuhSZO21Q4LCE8lQtNLAaOxdp7zj4zMVJ0KQ==</Codesign>
        </Script>
      </Scripts>
    </Application>
  </Applications>
</AlvaoApplication>