diff --git a/src/ZendeskApi_v2/Requests/Tickets.cs b/src/ZendeskApi_v2/Requests/Tickets.cs index 4cfb3bb9..f118b49c 100644 --- a/src/ZendeskApi_v2/Requests/Tickets.cs +++ b/src/ZendeskApi_v2/Requests/Tickets.cs @@ -151,6 +151,8 @@ public interface ITickets : ICore GroupTicketResponse GetTicketsByExternalId(string externalId, int pageNumber = 0, int itemsPerPage = 0, TicketSideLoadOptionsEnum sideLoadOptions = TicketSideLoadOptionsEnum.None); + JobStatusResponse MergeTickets(long targetTicketId, IEnumerable sourceTicketIds, string targetComment = "", string sourceComment = "", bool targetCommentPublic = false, bool sourceCommentPublic = false); + #endif #if ASYNC @@ -264,6 +266,8 @@ public interface ITickets : ICore Task BulkImportTicketsAsync(IEnumerable tickets); + Task MergeTicketsAsync(long targetTicketId, IEnumerable sourceTicketIds, string targetComment = "", string sourceComment = "", bool targetCommentPublic = false, bool sourceCommentPublic = false); + #endif } @@ -470,6 +474,31 @@ public bool DeleteMultiple(IEnumerable ids) return GenericDelete($"{_tickets}/destroy_many.json?ids={ids.ToCsv()}"); } + /// + /// Merges the source tickets in the "ids" list into the target ticket with comments as defined. + /// + /// Id of ticket to be merged into. + /// List of ids of source tickets to be merged from. + /// Private comment to add to the target ticket (optional but recommended) + /// Private comment to add to the source ticket(s) (optional but recommended) + /// Whether comment in target ticket is public or private (default = private) + /// Whether comment in source ticket is public or private (default = private) + /// JobStatusResponse + public JobStatusResponse MergeTickets(long targetTicketId, IEnumerable sourceTicketIds, string targetComment = "", string sourceComment = "", bool targetCommentPublic = false, bool sourceCommentPublic = false) + { + return GenericPost( + $"{_tickets}/{targetTicketId}/merge.json", + new + { + ids = sourceTicketIds, + target_comment = targetComment, + source_comment = sourceComment, + target_comment_is_public = targetCommentPublic, + source_comment_is_public = sourceCommentPublic + }); + } + + public GroupUserResponse GetCollaborators(long id) { return GenericGet($"{_tickets}/{id}/collaborators.json"); @@ -980,6 +1009,31 @@ public async Task DeleteTicketFormAsync(long id) return await GenericDeleteAsync($"{_ticket_forms}/{id}.json"); } + /// + /// Merges the source tickets in the "ids" list into the target ticket with comments as defined. + /// + /// Id of ticket to be merged into. + /// List of ids of source tickets to be merged from. + /// Private comment to add to the target ticket (optional but recommended) + /// Private comment to add to the source ticket(s) (optional but recommended) + /// Whether comment in target ticket is public or private (default = private) + /// Whether comment in source ticket is public or private (default = private) + /// JobStatusResponse + public async Task MergeTicketsAsync(long targetTicketId, IEnumerable sourceTicketIds, string targetComment = "", string sourceComment = "", bool targetCommentPublic = false, bool sourceCommentPublic = false) + { + return await GenericPostAsync( + $"{_tickets}/{targetTicketId}/merge.json", + new + { + ids = sourceTicketIds, + target_comment = targetComment, + source_comment = sourceComment, + target_comment_is_public = targetCommentPublic, + source_comment_is_public = sourceCommentPublic, + }); + } + + #region TicketMetrics public Task GetAllTicketMetricsAsync() diff --git a/test/ZendeskApi_v2.Test/TicketTests.cs b/test/ZendeskApi_v2.Test/TicketTests.cs index 55772b46..9243dfa2 100644 --- a/test/ZendeskApi_v2.Test/TicketTests.cs +++ b/test/ZendeskApi_v2.Test/TicketTests.cs @@ -1119,6 +1119,166 @@ public void CanImportTicketAsync() api.Tickets.DeleteAsync(res.Id); } + [Test] + public void CanMergeTickets() + { + var sourceDescription = new List { "This is a source ticket 1", "This is a source ticket 2" }; + var targetDescription = "This is a the target ticket"; + + var sourceTicket1 = new Ticket + { + Subject = "Source Ticket 1", + Comment = new Comment { Body = sourceDescription[0], Public = true, } + }; + var sourceTicket2 = new Ticket + { + Subject = "Source Ticket 2", + Comment = new Comment { Body = sourceDescription[1], Public=true, } + }; + var targetTicket = new Ticket + { + Subject = "Target Ticket", + Comment = new Comment { Body = targetDescription, Public = true, } + }; + + var mergeIds = new List(); + + var tick = api.Tickets.CreateTicket(sourceTicket1); + mergeIds.Add(tick.Ticket.Id.Value); + tick = api.Tickets.CreateTicket(sourceTicket2); + mergeIds.Add(tick.Ticket.Id.Value); + tick = api.Tickets.CreateTicket(targetTicket); + var targetTicketId = tick.Ticket.Id.Value; + + var targetMergeComment = + $"Merged with ticket(s) {string.Join(", ", mergeIds.Select(m => $"#{m}").ToArray())}"; + var sourceMergeComment = $"Closing in favor of #{targetTicketId}"; + + var res = api.Tickets.MergeTickets( + targetTicketId, + mergeIds, + targetMergeComment, + sourceMergeComment, + true, + true); + + Assert.That(res.JobStatus.Status, Is.EqualTo("queued")); + + do + { + Thread.Sleep(5000); + var job = api.JobStatuses.GetJobStatus(res.JobStatus.Id); + Assert.That(job.JobStatus.Id, Is.EqualTo(res.JobStatus.Id)); + + if (job.JobStatus.Status == "completed") break; + } while (true); + + var counter = 0; + foreach (var id in mergeIds) + { + var oldTicket = api.Tickets.GetTicket(id); + Assert.That(oldTicket.Ticket.Id.Value, Is.EqualTo(id)); + Assert.That(oldTicket.Ticket.Status, Is.EqualTo("closed")); + + var oldComments = api.Tickets.GetTicketComments(id); + Assert.That(oldComments.Comments.Count, Is.EqualTo(2)); + Assert.That(oldComments.Comments[0].Body, Is.EqualTo(sourceDescription[counter])); + Assert.That(oldComments.Comments[1].Body, Is.EqualTo(sourceMergeComment)); + + api.Tickets.DeleteAsync(id); + counter++; + } + + var ticket = api.Tickets.GetTicket(targetTicketId); + Assert.That(ticket.Ticket.Id.Value, Is.EqualTo(targetTicketId)); + + var comments = api.Tickets.GetTicketComments(targetTicketId); + Assert.That(comments.Comments.Count, Is.EqualTo(2)); + Assert.That(comments.Comments[0].Body, Is.EqualTo(targetDescription)); + Assert.That(comments.Comments[1].Body, Is.EqualTo(targetMergeComment)); + + api.Tickets.DeleteAsync(targetTicketId); + } + + [Test] + public async Task CanMergeTicketsAsync() + { + var sourceDescription = new List { "This is a source ticket 1", "This is a source ticket 2" }; + var targetDescription = "This is a the target ticket"; + + var sourceTicket1 = new Ticket + { + Subject = "Source Ticket 1", + Comment = new Comment { Body = sourceDescription[0], Public = true, } + }; + var sourceTicket2 = new Ticket + { + Subject = "Source Ticket 2", + Comment = new Comment { Body = sourceDescription[1], Public = true, } + }; + var targetTicket = new Ticket + { + Subject = "Target Ticket", + Comment = new Comment { Body = targetDescription, Public = true, } + }; + + var mergeIds = new List(); + + var tick = await api.Tickets.CreateTicketAsync(sourceTicket1); + mergeIds.Add(tick.Ticket.Id.Value); + tick = await api.Tickets.CreateTicketAsync(sourceTicket2); + mergeIds.Add(tick.Ticket.Id.Value); + tick = await api.Tickets.CreateTicketAsync(targetTicket); + var targetTicketId = tick.Ticket.Id.Value; + + var targetMergeComment = + $"Merged with ticket(s) {string.Join(", ", mergeIds.Select(m => $"#{m}").ToArray())}"; + var sourceMergeComment = $"Closing in favor of #{targetTicketId}"; + + var res = await api.Tickets.MergeTicketsAsync( + targetTicketId, + mergeIds, + targetMergeComment, + sourceMergeComment); + + Assert.That(res.JobStatus.Status, Is.EqualTo("queued")); + + do + { + await Task.Delay(5000); + var job = await api.JobStatuses.GetJobStatusAsync(res.JobStatus.Id); + Assert.That(job.JobStatus.Id, Is.EqualTo(res.JobStatus.Id)); + + if (job.JobStatus.Status == "completed") break; + } while (true); + + var counter = 0; + foreach (var id in mergeIds) + { + var oldTicket = await api.Tickets.GetTicketAsync(id); + Assert.That(oldTicket.Ticket.Id.Value, Is.EqualTo(id)); + Assert.That(oldTicket.Ticket.Status, Is.EqualTo("closed")); + + var oldComments = await api.Tickets.GetTicketCommentsAsync(id); + Assert.That(oldComments.Comments.Count, Is.EqualTo(2)); + Assert.That(oldComments.Comments[0].Body, Is.EqualTo(sourceDescription[counter])); + Assert.That(oldComments.Comments[1].Body, Is.EqualTo(sourceMergeComment)); + + await api.Tickets.DeleteAsync(id); + counter++; + } + + var ticket = await api.Tickets.GetTicketAsync(targetTicketId); + Assert.That(ticket.Ticket.Id.Value, Is.EqualTo(targetTicketId)); + + var comments = await api.Tickets.GetTicketCommentsAsync(targetTicketId); + Assert.That(comments.Comments.Count, Is.EqualTo(2)); + Assert.That(comments.Comments[0].Body, Is.EqualTo(targetDescription)); + Assert.That(comments.Comments[1].Body, Is.EqualTo(targetMergeComment)); + + await api.Tickets.DeleteAsync(targetTicketId); + } + [Test] public void CanBulkImportTicket() {