I had a need to improve how I work with customers and how I record the tasks and time and bill the customers. So I decided to start using Jira + Business Central.

But the problem is that there is no integration available between these two and so I had to built this myself. I need to sync Projects and Tasks and Time entries from Jira to Business Central and when the time comes post the time entries against the Job Task to be able to bill the customer.

So how I architected and built the integration between these two software?

I decided to use webhooks in Jira and push the webhook data into a storage queue and then consume the queue in Business Central.

Why use the storage queue and not to post the webhook directly into BC? Couple of reasons:

  • I would need some middleware anyway since I can’t authenticate from Jira to BC directly
  • BC can be down, extension can be deployed etc so posting directly is not very bullet proof
  • using queues there’s no hard coupling between these two, I can swap out Jira if I want to
  • BC has API limits and might run into them
  • I had old project implementing the Storage Queues available and I really like queues Azure Storage Queues in Business Central

This is what the high level architecture looks like:

Getting data from the webhook: Overall Architecture

Doing full sync for all Jiras projects, issues and time entries.

Full Sync

Going into more detail I have a Azure Function written in C# which handles receiving the webhook from Jira and pushing it to the queue:

public class BCAndJiraProjectAndTask
{

    [Function("SyncBCAndJiraProjectAndTask")]
    public static async Task<JobAndTaskResponse> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
    FunctionContext executionContext)
    {
        var logger = executionContext.GetLogger("HttpExample");
        logger.LogInformation("C# HTTP trigger function processed a request.");

        var content = await new StreamReader(req.Body).ReadToEndAsync();

        JiraTask jiraTask = JiraTask.FromJson(content);
        StringBuilder message = new StringBuilder();
        message.Append(jiraTask.Issue.Fields.Project.Key);
        message.Append(';');
        message.Append(jiraTask.Issue.Fields.Project.Name);
        message.Append(";");
        message.Append(jiraTask.Issue.Key);
        message.Append(";");
        message.Append(jiraTask.Issue.Fields.Summary);
        message.Append(";");
        message.Append(jiraTask.Issue.Id);

        var response = req.CreateResponse(HttpStatusCode.OK);
        response.Headers.Add("Content-Type", "text/plain; charset=utf-8");

        response.WriteString("Added to the queue");            

        return new JobAndTaskResponse()
        {
            // Write a single message.
            Messages = new string[] { message.ToString() },
            HttpResponse = response
        };
    }
}

public class JobAndTaskResponse
{
    [QueueOutput("syncprojectandtask", Connection = "AzureWebJobsStorage")]
    public string[] Messages { get; set; }
    public HttpResponseData HttpResponse { get; set; }
}

In Business Central I have a codeunit that I can schedule to process the queue. Thats using the project referenced before to communicate with the Storage Queue.

    local procedure Process(Queue: Text): Boolean
    var
        AzureStorageQueueSdk: Codeunit AzureStorageQueuesSdk;
        MessageBody: Text;
        MessageId: Text;
        MessageText: Text;
        MessageTextList: list of [text];
        MessagePopreceipt: Text;
        Base64Convert: Codeunit "Base64 Convert";
    begin
        MessageBody := AzureStorageQueueSdk.GetNextMessageFromQueue(Queue);
        if MessageBody = '' then
            exit(false);
        MessageId := AzureStorageQueueSdk.GetMessageIdFromXmlText(MessageBody);
        MessageText := AzureStorageQueueSdk.GetMessageTextFromXmlText(MessageBody);
        MessageText := Base64Convert.FromBase64(MessageText);
        MessagePopreceipt := AzureStorageQueueSdk.GetMessagePopReceiptFromXmlText(MessageBody);

        if (MessageId <> '') AND (MessageText <> '') then begin
            MessageTextList := MessageText.Split(';');

            if Queue = 'syncprojectandtask' then
                if SyncJob(MessageTextList.Get(1), MessageTextList.Get(2)) and SyncJobTask(MessageTextList.Get(1), MessageTextList.Get(3), MessageTextList.Get(4), MessageTextList.Get(5)) then begin
                    //important here to check if delete is succesful and then commit. Otherwise we end in loop where messages are reappearing
                    if AzureStorageQueueSdk.DeleteMessageFromQueue(Queue, MessageId, MessagePopreceipt) then
                        Commit();
                    exit(true);
                end;

            if Queue = 'syncprojecttimeentries' then
                if SyncJobTimeEntry(MessageTextList.Get(1), MessageTextList.Get(2), MessageTextList.Get(3), MessageTextList.Get(4), MessageTextList.Get(5), MessageTextList.Get(6)) then begin
                    //important here to check if delete is succesful and then commit. Otherwise we end in loop where messages are reappearing
                    if AzureStorageQueueSdk.DeleteMessageFromQueue(Queue, MessageId, MessagePopreceipt) then
                        Commit();
                    exit(true);
                end;
        end;
    end;

And thats it. Running all this I have pretty robust integration between Jira and BC and I get all the projects, tasks and time entries synced to BC and can use them to invoice customers. Of course in real life something will happen and the webhook is missed and I need to have a full sync also available to force the sync for all projects, tasks and time entries from Jira but this architecture should most of the stuff in sync. Time Entries in BC