diff --git a/source/backend/Pims.Scheduler.Test/Pims.Scheduler.Test.csproj b/source/backend/Pims.Scheduler.Test/Pims.Scheduler.Test.csproj new file mode 100644 index 0000000000..2ec4df52a3 --- /dev/null +++ b/source/backend/Pims.Scheduler.Test/Pims.Scheduler.Test.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/source/backend/Pims.Scheduler.Test/Repositories/PimsDocumentQueueRepositoryTests.cs b/source/backend/Pims.Scheduler.Test/Repositories/PimsDocumentQueueRepositoryTests.cs new file mode 100644 index 0000000000..6083026ef6 --- /dev/null +++ b/source/backend/Pims.Scheduler.Test/Repositories/PimsDocumentQueueRepositoryTests.cs @@ -0,0 +1,105 @@ +using FluentAssertions; +using Moq; +using Pims.Api.Models.CodeTypes; +using Pims.Api.Models.Concepts.Document; +using Pims.Api.Models.Requests.Http; +using Pims.Dal.Entities.Models; +using Pims.Scheduler.Repositories; +using Xunit; + +namespace Pims.Scheduler.Test.Repositories +{ + public class PimsDocumentQueueRepositoryTest + { + [Fact] + public async Task PollQueuedDocument_ValidDocument_ReturnsExternalResponse() + { + // Arrange + var document = new DocumentQueueModel { Id = 1 }; + var expectedResponse = new ExternalResponse { Status = ExternalResponseStatus.Success }; + var repositoryMock = new Mock(); + repositoryMock.Setup(x => x.PollQueuedDocument(document)).ReturnsAsync(expectedResponse); + + // Act + var result = await repositoryMock.Object.PollQueuedDocument(document); + + // Assert + result.Should().NotBeNull(); + result.Status.Should().Be(ExternalResponseStatus.Success); + repositoryMock.Verify(x => x.PollQueuedDocument(document), Times.Once); + } + + [Fact] + public async Task UploadQueuedDocument_ValidDocument_ReturnsExternalResponse() + { + // Arrange + var document = new DocumentQueueModel { Id = 1 }; + var expectedResponse = new ExternalResponse { Status = ExternalResponseStatus.Success }; + var repositoryMock = new Mock(); + repositoryMock.Setup(x => x.UploadQueuedDocument(document)).ReturnsAsync(expectedResponse); + + // Act + var result = await repositoryMock.Object.UploadQueuedDocument(document); + + // Assert + result.Should().NotBeNull(); + result.Status.Should().Be(ExternalResponseStatus.Success); + repositoryMock.Verify(x => x.UploadQueuedDocument(document), Times.Once); + } + + [Fact] + public async Task UpdateQueuedDocument_ValidDocument_ReturnsExternalResponse() + { + // Arrange + var documentQueueId = 1; + var document = new DocumentQueueModel { Id = documentQueueId }; + var expectedResponse = new ExternalResponse { Status = ExternalResponseStatus.Success }; + var repositoryMock = new Mock(); + repositoryMock.Setup(x => x.UpdateQueuedDocument(documentQueueId, document)).ReturnsAsync(expectedResponse); + + // Act + var result = await repositoryMock.Object.UpdateQueuedDocument(documentQueueId, document); + + // Assert + result.Should().NotBeNull(); + result.Status.Should().Be(ExternalResponseStatus.Success); + repositoryMock.Verify(x => x.UpdateQueuedDocument(documentQueueId, document), Times.Once); + } + + [Fact] + public async Task SearchQueuedDocumentsAsync_ValidFilter_ReturnsExternalResponse() + { + // Arrange + var filter = new DocumentQueueFilter(); + var expectedResponse = new ExternalResponse> { Status = ExternalResponseStatus.Success }; + var repositoryMock = new Mock(); + repositoryMock.Setup(x => x.SearchQueuedDocumentsAsync(filter)).ReturnsAsync(expectedResponse); + + // Act + var result = await repositoryMock.Object.SearchQueuedDocumentsAsync(filter); + + // Assert + result.Should().NotBeNull(); + result.Status.Should().Be(ExternalResponseStatus.Success); + repositoryMock.Verify(x => x.SearchQueuedDocumentsAsync(filter), Times.Once); + } + + [Fact] + public async Task GetById_ValidDocumentQueueId_ReturnsExternalResponse() + { + // Arrange + var documentQueueId = 1; + var expectedResponse = new ExternalResponse { Status = ExternalResponseStatus.Success }; + var repositoryMock = new Mock(); + repositoryMock.Setup(x => x.GetById(documentQueueId)).ReturnsAsync(expectedResponse); + + // Act + var result = await repositoryMock.Object.GetById(documentQueueId); + + // Assert + result.Should().NotBeNull(); + result.Status.Should().Be(ExternalResponseStatus.Success); + repositoryMock.Verify(x => x.GetById(documentQueueId), Times.Once); + } + } +} diff --git a/source/backend/Pims.Scheduler.Test/Services/DocumentQueueServiceTests.cs b/source/backend/Pims.Scheduler.Test/Services/DocumentQueueServiceTests.cs new file mode 100644 index 0000000000..fe54a96092 --- /dev/null +++ b/source/backend/Pims.Scheduler.Test/Services/DocumentQueueServiceTests.cs @@ -0,0 +1,303 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Pims.Api.Models.Base; +using Pims.Api.Models.CodeTypes; +using Pims.Api.Models.Concepts.Document; +using Pims.Api.Models.Requests.Http; +using Pims.Dal.Entities.Models; +using Pims.Scheduler.Http.Configuration; +using Pims.Scheduler.Models; +using Pims.Scheduler.Repositories; +using Pims.Scheduler.Services; +using Xunit; + +namespace Pims.Scheduler.Test.Services +{ + public class DocumentQueueServiceTests + { + private readonly Mock> _loggerMock; + private readonly Mock _documentQueueRepositoryMock; + private readonly Mock> _uploadOptionsMock; + private readonly Mock> _queryOptionsMock; + private readonly Mock> _retryOptionsMock; + private readonly DocumentQueueService _service; + + public DocumentQueueServiceTests() + { + _loggerMock = new Mock>(); + _documentQueueRepositoryMock = new Mock(); + _uploadOptionsMock = new Mock>(); + _queryOptionsMock = new Mock>(); + _retryOptionsMock = new Mock>(); + _uploadOptionsMock.Setup(x => x.CurrentValue).Returns(new UploadQueuedDocumentsJobOptions() { BatchSize = 10, MaxFileSize = 100 }); + _queryOptionsMock.Setup(x => x.CurrentValue).Returns(new QueryProcessingDocumentsJobOptions() { BatchSize = 10, MaxProcessingMinutes = 100 }); + + _service = new DocumentQueueService( + _loggerMock.Object, + _uploadOptionsMock.Object, + _queryOptionsMock.Object, + _retryOptionsMock.Object, + _documentQueueRepositoryMock.Object + ); + } + + [Fact] + public async Task UploadQueuedDocuments_NoDocumentsToProcess_ReturnsSkipped() + { + // Arrange + var searchResponse = new ExternalResponse> { Status = ExternalResponseStatus.Success, Payload = new List() }; + _documentQueueRepositoryMock.Setup(x => x.SearchQueuedDocumentsAsync(It.IsAny())).ReturnsAsync(searchResponse); + + // Act + var result = await _service.UploadQueuedDocuments(); + + // Assert + result.Status.Should().Be(TaskResponseStatusTypes.SKIPPED); + result.Message.Should().Be("No documents to process, skipping execution."); + } + + [Fact] + public async Task UploadQueuedDocuments_ErrorStatus_ReturnsError() + { + // Arrange + var searchResponse = new ExternalResponse> { Status = ExternalResponseStatus.Error, Message = "Error", Payload = new List() { new DocumentQueueModel() } }; + _documentQueueRepositoryMock.Setup(x => x.SearchQueuedDocumentsAsync(It.IsAny())).ReturnsAsync(searchResponse); + + // Act + var result = await _service.UploadQueuedDocuments(); + + // Assert + result.Status.Should().Be(TaskResponseStatusTypes.ERROR); + result.Message.Should().Be("Received error status from pims document queue service, aborting."); + } + + [Fact] + public async Task UploadQueuedDocuments_SingleDocumentError_ReturnsError() + { + // Arrange + var document = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; + var searchResponse = new ExternalResponse> + { + Status = ExternalResponseStatus.Success, + Payload = new List { document }, + }; + _documentQueueRepositoryMock.Setup(x => x.SearchQueuedDocumentsAsync(It.IsAny())).ReturnsAsync(searchResponse); + _documentQueueRepositoryMock.Setup(x => x.UploadQueuedDocument(document)).ReturnsAsync(new ExternalResponse + { + Status = ExternalResponseStatus.Error, + Message = "Error uploading document.", + }); + + // Act + var result = await _service.UploadQueuedDocuments(); + + // Assert + result.Status.Should().Be(TaskResponseStatusTypes.ERROR); + result.DocumentQueueResponses.FirstOrDefault()?.Message.Should().Be("Received error response from UploadQueuedDocument for queued document 1 status Error message: Error uploading document."); + } + + [Fact] + public async Task UploadQueuedDocuments_SingleDocumentError_ReturnsError_UpdatesQueue() + { + // Arrange + var document = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; + var searchResponse = new ExternalResponse> + { + Status = ExternalResponseStatus.Success, + Payload = new List { document }, + }; + _documentQueueRepositoryMock.Setup(x => x.SearchQueuedDocumentsAsync(It.IsAny())).ReturnsAsync(searchResponse); + _documentQueueRepositoryMock.Setup(x => x.GetById(It.IsAny())).ReturnsAsync(new ExternalResponse + { + Status = ExternalResponseStatus.Success, + Payload = document, + }); + _documentQueueRepositoryMock.Setup(x => x.UploadQueuedDocument(document)).ReturnsAsync(new ExternalResponse + { + Status = ExternalResponseStatus.Error, + Message = "Error uploading document.", + }); + + // Act + var result = await _service.UploadQueuedDocuments(); + + // Assert + result.Status.Should().Be(TaskResponseStatusTypes.ERROR); + result.DocumentQueueResponses.FirstOrDefault()?.Message.Should().Be("Received error response from UploadQueuedDocument for queued document 1 status Error message: Error uploading document."); + } + + [Fact] + public async Task UploadQueuedDocuments_SingleDocumentSuccess_ReturnsSuccess() + { + // Arrange + var document = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; + var searchResponse = new ExternalResponse> + { + Status = ExternalResponseStatus.Success, + Payload = new List { document }, + }; + _documentQueueRepositoryMock.Setup(x => x.SearchQueuedDocumentsAsync(It.IsAny())).ReturnsAsync(searchResponse); + _documentQueueRepositoryMock.Setup(x => x.UploadQueuedDocument(document)).ReturnsAsync(new ExternalResponse + { + Status = ExternalResponseStatus.Success, + Payload = document, + }); + + // Act + var result = await _service.UploadQueuedDocuments(); + + // Assert + result.Status.Should().Be(TaskResponseStatusTypes.SUCCESS); + } + + [Fact] + public async Task UploadQueuedDocuments_TwoDocumentsMixedResults_ReturnsPartialSuccess() + { + // Arrange + var document1 = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; + var document2 = new DocumentQueueModel { Id = 2, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; + var searchResponse = new ExternalResponse> + { + Status = ExternalResponseStatus.Success, + Payload = new List { document1, document2 }, + }; + _documentQueueRepositoryMock.Setup(x => x.SearchQueuedDocumentsAsync(It.IsAny())).ReturnsAsync(searchResponse); + _documentQueueRepositoryMock.Setup(x => x.UploadQueuedDocument(document1)).ReturnsAsync(new ExternalResponse + { + Status = ExternalResponseStatus.Success, + Payload = document1, + }); + _documentQueueRepositoryMock.Setup(x => x.UploadQueuedDocument(document2)).ReturnsAsync(new ExternalResponse + { + Status = ExternalResponseStatus.Error, + Message = "Error uploading document 2.", + }); + + // Act + var result = await _service.UploadQueuedDocuments(); + + // Assert + result.Status.Should().Be(TaskResponseStatusTypes.PARTIAL); + result.DocumentQueueResponses.Should().HaveCount(2); + result.DocumentQueueResponses.ToArray()[1].Message.Should().Be("Received error response from UploadQueuedDocument for queued document 2 status Error message: Error uploading document 2."); + } + + [Fact] + public async Task RetryQueuedDocuments_ErrorStatus_ReturnsError() + { + // Arrange + var searchResponse = new ExternalResponse> { Status = ExternalResponseStatus.Error, Message = "Error", Payload = new List() { new DocumentQueueModel() } }; + _documentQueueRepositoryMock.Setup(x => x.SearchQueuedDocumentsAsync(It.IsAny())).ReturnsAsync(searchResponse); + + // Act + var result = await _service.RetryQueuedDocuments(); + + // Assert + result.Status.Should().Be(TaskResponseStatusTypes.ERROR); + result.Message.Should().Be("Received error status from pims document queue service, aborting."); + } + + [Fact] + public async Task QueryProcessingDocuments_NoDocumentsToProcess_ReturnsSkipped() + { + // Arrange + var searchResponse = new ExternalResponse> { Status = ExternalResponseStatus.Success, Payload = new List() }; + _documentQueueRepositoryMock.Setup(x => x.SearchQueuedDocumentsAsync(It.IsAny())).ReturnsAsync(searchResponse); + + // Act + var result = await _service.QueryProcessingDocuments(); + + // Assert + result.Status.Should().Be(TaskResponseStatusTypes.SKIPPED); + result.Message.Should().Be("No documents to process, skipping execution."); + } + + [Fact] + public async Task QueryProcessingDocuments_ErrorStatus_ReturnsError() + { + // Arrange + var searchResponse = new ExternalResponse> { Status = ExternalResponseStatus.Error, Message = "Error", Payload = new List() { new DocumentQueueModel() } }; + _documentQueueRepositoryMock.Setup(x => x.SearchQueuedDocumentsAsync(It.IsAny())).ReturnsAsync(searchResponse); + + // Act + var result = await _service.QueryProcessingDocuments(); + + // Assert + result.Status.Should().Be(TaskResponseStatusTypes.ERROR); + result.Message.Should().Be("Received error status from pims document queue service, aborting."); + } + + [Fact] + public async Task QueryProcessingDocuments_OneDocumentError_ReturnsError() + { + // Arrange + var document = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; + var searchResponse = new ExternalResponse> + { + Status = ExternalResponseStatus.Success, + Payload = new List { document }, + }; + _documentQueueRepositoryMock.Setup(x => x.SearchQueuedDocumentsAsync(It.IsAny())).ReturnsAsync(searchResponse); + _documentQueueRepositoryMock.Setup(x => x.PollQueuedDocument(document)).ReturnsAsync(new ExternalResponse + { + Status = ExternalResponseStatus.Error, + Message = "Error processing document.", + }); + + // Act + var result = await _service.QueryProcessingDocuments(); + + // Assert + result.Status.Should().Be(TaskResponseStatusTypes.ERROR); + result.DocumentQueueResponses.FirstOrDefault()?.Message.Should().Be("Received error response from PollQueuedDocument for queued document 1 status Error message: Error processing document."); + } + + [Fact] + public async Task QueryProcessingDocuments_OneDocumentSuccess_ReturnsSuccess() + { + // Arrange + var document = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; + var searchResponse = new ExternalResponse> + { + Status = ExternalResponseStatus.Success, + Payload = new List { document }, + }; + _documentQueueRepositoryMock.Setup(x => x.SearchQueuedDocumentsAsync(It.IsAny())).ReturnsAsync(searchResponse); + _documentQueueRepositoryMock.Setup(x => x.PollQueuedDocument(document)).ReturnsAsync(new ExternalResponse + { + Status = ExternalResponseStatus.Success, + Payload = new DocumentQueueModel { Id = document.Id, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.SUCCESS.ToString() } }, + }); + + // Act + var result = await _service.QueryProcessingDocuments(); + + // Assert + result.Status.Should().Be(TaskResponseStatusTypes.SUCCESS); + } + + [Fact] + public async Task QueryProcessingDocuments_OneDocumentExceededMaxProcessingTime_ReturnsError() + { + // Arrange + var document = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() }, DocumentProcessStartTimestamp = DateTime.UtcNow.AddDays(-2) }; + var searchResponse = new ExternalResponse> + { + Status = ExternalResponseStatus.Success, + Payload = new List { document }, + }; + _documentQueueRepositoryMock.Setup(x => x.SearchQueuedDocumentsAsync(It.IsAny())).ReturnsAsync(searchResponse); + + // Act + var result = await _service.QueryProcessingDocuments(); + + // Assert + result.Status.Should().Be(TaskResponseStatusTypes.ERROR); + result.DocumentQueueResponses.FirstOrDefault()?.Message.Should().Be("Document processing for document 1 has exceeded maximum processing time of 100"); + } + + + } +} diff --git a/source/backend/api/Areas/Documents/DocumentQueueController.cs b/source/backend/api/Areas/Documents/DocumentQueueController.cs index 9fead6a65e..3a3fd79efa 100644 --- a/source/backend/api/Areas/Documents/DocumentQueueController.cs +++ b/source/backend/api/Areas/Documents/DocumentQueueController.cs @@ -1,11 +1,18 @@ +using System; using System.Collections.Generic; +using System.Threading.Tasks; using MapsterMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Pims.Api.Models.Concepts.Document; using Pims.Api.Services; +using Pims.Core.Api.Exceptions; using Pims.Core.Api.Policies; +using Pims.Core.Extensions; using Pims.Core.Json; using Pims.Core.Security; +using Pims.Dal.Entities; using Pims.Dal.Entities.Models; using Swashbuckle.AspNetCore.Annotations; @@ -18,12 +25,13 @@ namespace Pims.Api.Controllers [ApiController] [ApiVersion("1.0")] [Route("v{version:apiVersion}/documents/queue")] - [Route("/documents")] + [Route("/documents/queue")] public class DocumentQueueController : ControllerBase { #region Variables private readonly IDocumentQueueService _documentQueueService; private readonly IMapper _mapper; + private readonly ILogger _logger; #endregion #region Constructors @@ -33,32 +41,155 @@ public class DocumentQueueController : ControllerBase /// /// /// - public DocumentQueueController(IDocumentQueueService documentQueueService, IMapper mapper) + /// + public DocumentQueueController(IDocumentQueueService documentQueueService, IMapper mapper, ILogger logger) { _documentQueueService = documentQueueService; _mapper = mapper; + _logger = logger; } #endregion #region Endpoints + /// + /// Update a Queued Document. + /// + /// + [HttpPut("{documentQueueId:long}")] + [HasPermission(Permissions.SystemAdmin)] + [Produces("application/json")] + [ProducesResponseType(typeof(List), 200)] + [SwaggerOperation(Tags = new[] { "document-types" })] + [TypeFilter(typeof(NullJsonResultFilter))] + public IActionResult Update(long documentQueueId, [FromBody] DocumentQueueModel documentQueue) + { + _logger.LogInformation( + "Request received by Controller: {Controller}, Action: {ControllerAction}, User: {User}, DateTime: {DateTime}", + nameof(DocumentQueueController), + nameof(Update), + User.GetUsername(), + DateTime.Now); + + documentQueue.ThrowIfNull(nameof(documentQueue)); + if (documentQueueId != documentQueue.Id) + { + throw new BadRequestException("Invalid document queue id."); + } + + var queuedDocuments = _documentQueueService.Update(_mapper.Map(documentQueue)); + var updatedDocumentQueue = _mapper.Map(queuedDocuments); + return new JsonResult(updatedDocumentQueue); + } + + /// + /// Poll a queud document to check on the upload status. + /// + /// + [HttpPost("{documentQueueId:long}/poll")] + [HasPermission(Permissions.SystemAdmin)] + [Produces("application/json")] + [ProducesResponseType(typeof(List), 200)] + [SwaggerOperation(Tags = new[] { "document-types" })] + [TypeFilter(typeof(NullJsonResultFilter))] + public async Task Poll(long documentQueueId, [FromBody] DocumentQueueModel documentQueue) + { + _logger.LogInformation( + "Request received by Controller: {Controller}, Action: {ControllerAction}, User: {User}, DateTime: {DateTime}", + nameof(DocumentQueueController), + nameof(Poll), + User.GetUsername(), + DateTime.Now); + + documentQueue.ThrowIfNull(nameof(documentQueue)); + if (documentQueueId != documentQueue.Id) + { + throw new BadRequestException("Invalid document queue id."); + } + + var queuedDocuments = await _documentQueueService.PollForDocument(_mapper.Map(documentQueue)); + var updatedDocumentQueue = _mapper.Map(queuedDocuments); + return new JsonResult(updatedDocumentQueue); + } + + /// + /// Upload a Queued Document. + /// + /// + [HttpPost("{documentQueueId:long}/upload")] + [HasPermission(Permissions.SystemAdmin)] + [Produces("application/json")] + [ProducesResponseType(typeof(List), 200)] + [SwaggerOperation(Tags = new[] { "document-types" })] + [TypeFilter(typeof(NullJsonResultFilter))] + public async Task Upload(long documentQueueId, [FromBody] DocumentQueueModel documentQueue) + { + _logger.LogInformation( + "Request received by Controller: {Controller}, Action: {ControllerAction}, User: {User}, DateTime: {DateTime}", + nameof(DocumentQueueController), + nameof(Upload), + User.GetUsername(), + DateTime.Now); + + documentQueue.ThrowIfNull(nameof(documentQueue)); + if (documentQueueId != documentQueue.Id) + { + throw new BadRequestException("Invalid document queue id."); + } + + var queuedDocuments = await _documentQueueService.Upload(_mapper.Map(documentQueue)); + var updatedDocumentQueue = _mapper.Map(queuedDocuments); + return new JsonResult(updatedDocumentQueue); + } + /// /// Search for Document Queue items via filter. /// /// - [HttpGet("search")] + [HttpPost("search")] [HasPermission(Permissions.SystemAdmin)] [Produces("application/json")] - [ProducesResponseType(typeof(List), 200)] + [ProducesResponseType(typeof(List), 200)] [SwaggerOperation(Tags = new[] { "document-types" })] [TypeFilter(typeof(NullJsonResultFilter))] - public IActionResult GetDocumentTypes([FromBody] DocumentQueueFilter filter) + public IActionResult SearchQueuedDocuments([FromBody] DocumentQueueFilter filter) { + _logger.LogInformation( + "Request received by Controller: {Controller}, Action: {ControllerAction}, User: {User}, DateTime: {DateTime}", + nameof(DocumentQueueController), + nameof(SearchQueuedDocuments), + User.GetUsername(), + DateTime.Now); + var queuedDocuments = _documentQueueService.SearchDocumentQueue(filter); - var documentQueueModels = _mapper.Map>(queuedDocuments); + var documentQueueModels = _mapper.Map>(queuedDocuments); return new JsonResult(documentQueueModels); } + /// + /// Get Document Queue item via id. + /// + /// + [HttpGet("{documentQueueId:long}")] + [HasPermission(Permissions.SystemAdmin)] + [Produces("application/json")] + [ProducesResponseType(typeof(List), 200)] + [SwaggerOperation(Tags = new[] { "document-types" })] + [TypeFilter(typeof(NullJsonResultFilter))] + public IActionResult GetQueuedDocument(long documentQueueId) + { + _logger.LogInformation( + "Request received by Controller: {Controller}, Action: {ControllerAction}, User: {User}, DateTime: {DateTime}", + nameof(DocumentQueueController), + nameof(GetQueuedDocument), + User.GetUsername(), + DateTime.Now); + + var queuedDocuments = _documentQueueService.GetById(documentQueueId); + var documentQueueModel = _mapper.Map(queuedDocuments); + return new JsonResult(documentQueueModel); + } + #endregion } } diff --git a/source/backend/api/Areas/Documents/DocumentRelationshipController.cs b/source/backend/api/Areas/Documents/DocumentRelationshipController.cs index 5202ab1026..e0ca25a724 100644 --- a/source/backend/api/Areas/Documents/DocumentRelationshipController.cs +++ b/source/backend/api/Areas/Documents/DocumentRelationshipController.cs @@ -4,15 +4,15 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Pims.Api.Constants; -using Pims.Core.Api.Exceptions; using Pims.Api.Models.CodeTypes; using Pims.Api.Models.Concepts.Document; using Pims.Api.Models.Requests.Document.Upload; -using Pims.Core.Api.Policies; using Pims.Api.Services; +using Pims.Core.Api.Exceptions; +using Pims.Core.Api.Policies; using Pims.Core.Json; -using Pims.Dal.Entities; using Pims.Core.Security; +using Pims.Dal.Entities; using Swashbuckle.AspNetCore.Annotations; namespace Pims.Api.Controllers @@ -142,19 +142,27 @@ public async Task UploadDocumentWithParent( string parentId, [FromForm] DocumentUploadRequest uploadRequest) { - var response = relationshipType switch + switch (relationshipType) { - DocumentRelationType.AcquisitionFiles => await _documentFileService.UploadAcquisitionDocumentAsync(long.Parse(parentId), uploadRequest), - DocumentRelationType.ResearchFiles => await _documentFileService.UploadResearchDocumentAsync(long.Parse(parentId), uploadRequest), - DocumentRelationType.Templates => await _formDocumentService.UploadFormDocumentTemplateAsync(parentId, uploadRequest), - DocumentRelationType.Projects => await _documentFileService.UploadProjectDocumentAsync(long.Parse(parentId), uploadRequest), - DocumentRelationType.Leases => await _documentFileService.UploadLeaseDocumentAsync(long.Parse(parentId), uploadRequest), - DocumentRelationType.ManagementFiles => await _documentFileService.UploadPropertyActivityDocumentAsync(long.Parse(parentId), uploadRequest), - DocumentRelationType.DispositionFiles => await _documentFileService.UploadDispositionDocumentAsync(long.Parse(parentId), uploadRequest), - _ => throw new BadRequestException("Relationship type not valid for upload."), - }; + case DocumentRelationType.AcquisitionFiles: + await _documentFileService.UploadAcquisitionDocument(long.Parse(parentId), uploadRequest); break; + case DocumentRelationType.ResearchFiles: + await _documentFileService.UploadResearchDocument(long.Parse(parentId), uploadRequest); break; + case DocumentRelationType.Projects: + await _documentFileService.UploadProjectDocument(long.Parse(parentId), uploadRequest); break; + case DocumentRelationType.Leases: + await _documentFileService.UploadLeaseDocument(long.Parse(parentId), uploadRequest); break; + case DocumentRelationType.ManagementFiles: + await _documentFileService.UploadPropertyActivityDocument(long.Parse(parentId), uploadRequest); break; + case DocumentRelationType.DispositionFiles: + await _documentFileService.UploadDispositionDocument(long.Parse(parentId), uploadRequest); break; + case DocumentRelationType.Templates: + await _formDocumentService.UploadFormDocumentTemplateAsync(parentId, uploadRequest); break; + default: + throw new BadRequestException("Relationship type not valid for upload."); + } - return new JsonResult(response); + return Ok(); } /// @@ -169,7 +177,7 @@ public async Task UploadDocumentWithParent( [ProducesResponseType(typeof(bool), 200)] [SwaggerOperation(Tags = new[] { "document" })] [TypeFilter(typeof(NullJsonResultFilter))] - public async Task DeleteDocumentRelationship(DocumentRelationType relationshipType, [FromBody] DocumentRelationshipModel model) + public async Task DeleteDocumentRelationship([FromRoute]DocumentRelationType relationshipType, [FromBody]DocumentRelationshipModel model) { switch (relationshipType) { diff --git a/source/backend/api/Helpers/Extensions/DocumentUploadRequestExtension.cs b/source/backend/api/Helpers/Extensions/DocumentUploadRequestExtension.cs new file mode 100644 index 0000000000..23a8cef1b7 --- /dev/null +++ b/source/backend/api/Helpers/Extensions/DocumentUploadRequestExtension.cs @@ -0,0 +1,21 @@ +using System; +using Pims.Api.Models.Requests.Document.Upload; +using Pims.Core.Api.Exceptions; + +namespace Pims.Api.Helpers.Extensions +{ + public static class DocumentUploadRequestExtension + { + public static void ThrowInvalidFileSize(this DocumentUploadRequest documentUploadRequest) + { + ArgumentNullException.ThrowIfNull(documentUploadRequest); + + if (documentUploadRequest.File is not null && documentUploadRequest.File.Length == 0) + { + throw new BadRequestException("The submitted file is empty"); + } + + return; + } + } +} diff --git a/source/backend/api/Helpers/Extensions/FormFileExtensions.cs b/source/backend/api/Helpers/Extensions/FormFileExtensions.cs new file mode 100644 index 0000000000..d7b7d0cbe4 --- /dev/null +++ b/source/backend/api/Helpers/Extensions/FormFileExtensions.cs @@ -0,0 +1,17 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Pims.Api.Helpers.Extensions +{ + public static class FormFileExtensions + { + public static async Task GetBytes(this IFormFile formFile) + { + using var memoryStream = new MemoryStream(); + await formFile.CopyToAsync(memoryStream); + + return memoryStream.ToArray(); + } + } +} diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index 4fd7e03de3..0d1ee769ad 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -2,7 +2,7 @@ 0ef6255f-9ea0-49ec-8c65-c172304b4926 - 5.7.0-96.7 + 5.7.0-96.11 5.7.0.96 true {16BC0468-78F6-4C91-87DA-7403C919E646} diff --git a/source/backend/api/Pims.sln b/source/backend/api/Pims.sln index 0a83937ba1..051693cbc0 100644 --- a/source/backend/api/Pims.sln +++ b/source/backend/api/Pims.sln @@ -44,15 +44,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{A0343C94-4 docs\VERSIONING.md = docs\VERSIONING.md EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "..\tests", "{F256F2A5-0DBF-4137-A7D6-21F08111BD4A}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F256F2A5-0DBF-4137-A7D6-21F08111BD4A}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "unit", "..\unit", "{3D70B211-74A8-484C-9B86-B0A2835C71E7}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "unit", "unit", "{3D70B211-74A8-484C-9B86-B0A2835C71E7}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pims.Api.Test", "..\tests\unit\api\Pims.Api.Test.csproj", "{1F4E301C-F03B-4A31-A6F2-6A77384A74DA}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pims.Dal.Test", "..\tests\unit\dal\Pims.Dal.Test.csproj", "{412BF533-2759-4FBE-B4C6-B89DB44FB6B5}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "core", "..\core", "{04780892-FC30-4B6B-A10C-5795C657E574}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "core", "core", "{04780892-FC30-4B6B-A10C-5795C657E574}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pims.Core.Test", "..\tests\core\Pims.Core.Test.csproj", "{5A83C636-741A-4795-8588-70F033E79B5A}" EndProject @@ -66,6 +66,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pims.Av", "..\clamav\Pims.A EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pims.Core.Api", "..\core.api\Pims.Core.Api.csproj", "{89A99CC5-ADFB-4FC2-9136-7B0029EEA2D8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pims.Scheduler.Test", "..\Pims.Scheduler.Test\Pims.Scheduler.Test.csproj", "{6B20887E-B784-4D78-939B-BDD8206DBE17}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pims.Scheduler", "..\scheduler\Pims.Scheduler.csproj", "{AC4336C5-5631-4D9D-B78F-6C2DF79A6F1F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -256,6 +260,30 @@ Global {89A99CC5-ADFB-4FC2-9136-7B0029EEA2D8}.Release|x64.Build.0 = Release|Any CPU {89A99CC5-ADFB-4FC2-9136-7B0029EEA2D8}.Release|x86.ActiveCfg = Release|Any CPU {89A99CC5-ADFB-4FC2-9136-7B0029EEA2D8}.Release|x86.Build.0 = Release|Any CPU + {6B20887E-B784-4D78-939B-BDD8206DBE17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B20887E-B784-4D78-939B-BDD8206DBE17}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B20887E-B784-4D78-939B-BDD8206DBE17}.Debug|x64.ActiveCfg = Debug|Any CPU + {6B20887E-B784-4D78-939B-BDD8206DBE17}.Debug|x64.Build.0 = Debug|Any CPU + {6B20887E-B784-4D78-939B-BDD8206DBE17}.Debug|x86.ActiveCfg = Debug|Any CPU + {6B20887E-B784-4D78-939B-BDD8206DBE17}.Debug|x86.Build.0 = Debug|Any CPU + {6B20887E-B784-4D78-939B-BDD8206DBE17}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B20887E-B784-4D78-939B-BDD8206DBE17}.Release|Any CPU.Build.0 = Release|Any CPU + {6B20887E-B784-4D78-939B-BDD8206DBE17}.Release|x64.ActiveCfg = Release|Any CPU + {6B20887E-B784-4D78-939B-BDD8206DBE17}.Release|x64.Build.0 = Release|Any CPU + {6B20887E-B784-4D78-939B-BDD8206DBE17}.Release|x86.ActiveCfg = Release|Any CPU + {6B20887E-B784-4D78-939B-BDD8206DBE17}.Release|x86.Build.0 = Release|Any CPU + {AC4336C5-5631-4D9D-B78F-6C2DF79A6F1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AC4336C5-5631-4D9D-B78F-6C2DF79A6F1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AC4336C5-5631-4D9D-B78F-6C2DF79A6F1F}.Debug|x64.ActiveCfg = Debug|Any CPU + {AC4336C5-5631-4D9D-B78F-6C2DF79A6F1F}.Debug|x64.Build.0 = Debug|Any CPU + {AC4336C5-5631-4D9D-B78F-6C2DF79A6F1F}.Debug|x86.ActiveCfg = Debug|Any CPU + {AC4336C5-5631-4D9D-B78F-6C2DF79A6F1F}.Debug|x86.Build.0 = Debug|Any CPU + {AC4336C5-5631-4D9D-B78F-6C2DF79A6F1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AC4336C5-5631-4D9D-B78F-6C2DF79A6F1F}.Release|Any CPU.Build.0 = Release|Any CPU + {AC4336C5-5631-4D9D-B78F-6C2DF79A6F1F}.Release|x64.ActiveCfg = Release|Any CPU + {AC4336C5-5631-4D9D-B78F-6C2DF79A6F1F}.Release|x64.Build.0 = Release|Any CPU + {AC4336C5-5631-4D9D-B78F-6C2DF79A6F1F}.Release|x86.ActiveCfg = Release|Any CPU + {AC4336C5-5631-4D9D-B78F-6C2DF79A6F1F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -276,6 +304,8 @@ Global {2C31E92C-9C95-45FF-9F95-928C2962F37D} = {3D70B211-74A8-484C-9B86-B0A2835C71E7} {16C06BDA-112F-4D04-82FF-0BBE45072372} = {5237F8A4-67F5-4751-B8B2-B93A06791480} {89A99CC5-ADFB-4FC2-9136-7B0029EEA2D8} = {5237F8A4-67F5-4751-B8B2-B93A06791480} + {6B20887E-B784-4D78-939B-BDD8206DBE17} = {3D70B211-74A8-484C-9B86-B0A2835C71E7} + {AC4336C5-5631-4D9D-B78F-6C2DF79A6F1F} = {5237F8A4-67F5-4751-B8B2-B93A06791480} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3433C5DD-DC49-4A96-A1AE-90C1A1EBA87C} diff --git a/source/backend/api/Repositories/Cdogs/CdogsAuthRepository.cs b/source/backend/api/Repositories/Cdogs/CdogsAuthRepository.cs index f86a032277..d2bf249d70 100644 --- a/source/backend/api/Repositories/Cdogs/CdogsAuthRepository.cs +++ b/source/backend/api/Repositories/Cdogs/CdogsAuthRepository.cs @@ -1,13 +1,15 @@ using System; using System.Collections.Generic; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -using Pims.Core.Api.Exceptions; +using Microsoft.Extensions.Options; using Pims.Api.Models.Cdogs; using Pims.Api.Models.CodeTypes; using Pims.Api.Models.Requests.Http; +using Pims.Core.Api.Exceptions; namespace Pims.Api.Repositories.Cdogs { @@ -25,11 +27,13 @@ public class CdogsAuthRepository : CdogsBaseRepository, IDocumentGenerationAuthR /// Injected Logger Provider. /// Injected Httpclient factory. /// The injected configuration provider. + /// The jsonOptions. public CdogsAuthRepository( ILogger logger, IHttpClientFactory httpClientFactory, - IConfiguration configuration) - : base(logger, httpClientFactory, configuration) + IConfiguration configuration, + IOptions jsonOptions) + : base(logger, httpClientFactory, configuration, jsonOptions) { _currentToken = null; _lastSucessfullRequest = DateTime.UnixEpoch; diff --git a/source/backend/api/Repositories/Cdogs/CdogsBaseRepository.cs b/source/backend/api/Repositories/Cdogs/CdogsBaseRepository.cs index d1d192b1d5..185199adc0 100644 --- a/source/backend/api/Repositories/Cdogs/CdogsBaseRepository.cs +++ b/source/backend/api/Repositories/Cdogs/CdogsBaseRepository.cs @@ -1,7 +1,9 @@ using System.Net.Http; using System.Net.Http.Headers; +using System.Text.Json; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Pims.Api.Models.Config; using Pims.Core.Api.Repositories.Rest; @@ -21,11 +23,13 @@ public abstract class CdogsBaseRepository : BaseRestRepository /// Injected Logger Provider. /// Injected Httpclient factory. /// The injected configuration provider. + /// The json options. protected CdogsBaseRepository( ILogger logger, IHttpClientFactory httpClientFactory, - IConfiguration configuration) - : base(logger, httpClientFactory) + IConfiguration configuration, + IOptions jsonOptions) + : base(logger, httpClientFactory, jsonOptions) { _config = new CdogsConfig(); configuration.Bind(CdogsConfigSectionKey, _config); diff --git a/source/backend/api/Repositories/Cdogs/CdogsRepository.cs b/source/backend/api/Repositories/Cdogs/CdogsRepository.cs index 435f0e4c8b..7d3a36d51e 100644 --- a/source/backend/api/Repositories/Cdogs/CdogsRepository.cs +++ b/source/backend/api/Repositories/Cdogs/CdogsRepository.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Pims.Api.Models.Cdogs; using Pims.Api.Models.CodeTypes; using Pims.Api.Models.Requests.Http; @@ -30,12 +31,14 @@ public class CdogsRepository : CdogsBaseRepository, IDocumentGenerationRepositor /// Injected Httpclient factory. /// Injected repository that handles authentication. /// The injected configuration provider. + /// The jsonOptions. public CdogsRepository( ILogger logger, IHttpClientFactory httpClientFactory, IDocumentGenerationAuthRepository authRepository, - IConfiguration configuration) - : base(logger, httpClientFactory, configuration) + IConfiguration configuration, + IOptions jsonOptions) + : base(logger, httpClientFactory, configuration, jsonOptions) { _authRepository = authRepository; } diff --git a/source/backend/api/Repositories/Mayan/MayanAuthRepository.cs b/source/backend/api/Repositories/Mayan/MayanAuthRepository.cs index 4f534fdc29..952ba6b4d4 100644 --- a/source/backend/api/Repositories/Mayan/MayanAuthRepository.cs +++ b/source/backend/api/Repositories/Mayan/MayanAuthRepository.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Pims.Api.Models.CodeTypes; using Pims.Api.Models.Mayan; using Pims.Api.Models.Requests.Http; @@ -26,11 +27,13 @@ public class MayanAuthRepository : MayanBaseRepository, IEdmsAuthRepository /// Injected Logger Provider. /// Injected Httpclient factory. /// The injected configuration provider. + /// The jsonOptions. public MayanAuthRepository( ILogger logger, IHttpClientFactory httpClientFactory, - IConfiguration configuration) - : base(logger, httpClientFactory, configuration) + IConfiguration configuration, + IOptions jsonOptions) + : base(logger, httpClientFactory, configuration, jsonOptions) { _currentToken = string.Empty; } diff --git a/source/backend/api/Repositories/Mayan/MayanBaseRepository.cs b/source/backend/api/Repositories/Mayan/MayanBaseRepository.cs index ab06e068fc..a57678618a 100644 --- a/source/backend/api/Repositories/Mayan/MayanBaseRepository.cs +++ b/source/backend/api/Repositories/Mayan/MayanBaseRepository.cs @@ -1,8 +1,10 @@ using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; +using System.Text.Json; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Pims.Api.Models.Config; using Pims.Core.Api.Repositories.Rest; @@ -22,11 +24,13 @@ public abstract class MayanBaseRepository : BaseRestRepository /// Injected Logger Provider. /// Injected Httpclient factory. /// The injected configuration provider. + /// The injected json options. protected MayanBaseRepository( ILogger logger, IHttpClientFactory httpClientFactory, - IConfiguration configuration) - : base(logger, httpClientFactory) + IConfiguration configuration, + IOptions jsonOptions) + : base(logger, httpClientFactory, jsonOptions) { _config = new MayanConfig(); configuration.Bind(MayanConfigSectionKey, _config); diff --git a/source/backend/api/Repositories/Mayan/MayanDocumentRepository.cs b/source/backend/api/Repositories/Mayan/MayanDocumentRepository.cs index 89fb6921a3..72a3315968 100644 --- a/source/backend/api/Repositories/Mayan/MayanDocumentRepository.cs +++ b/source/backend/api/Repositories/Mayan/MayanDocumentRepository.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Pims.Api.Models; using Pims.Api.Models.CodeTypes; using Pims.Api.Models.Mayan; @@ -35,12 +36,14 @@ public class MayanDocumentRepository : MayanBaseRepository, IEdmsDocumentReposit /// Injected Httpclient factory. /// Injected repository that handles authentication. /// The injected configuration provider. + /// The jsonOptions. public MayanDocumentRepository( ILogger logger, IHttpClientFactory httpClientFactory, IEdmsAuthRepository authRepository, - IConfiguration configuration) - : base(logger, httpClientFactory, configuration) + IConfiguration configuration, + IOptions jsonOptions) + : base(logger, httpClientFactory, configuration, jsonOptions) { _authRepository = authRepository; } diff --git a/source/backend/api/Repositories/Mayan/MayanMetadataRepository.cs b/source/backend/api/Repositories/Mayan/MayanMetadataRepository.cs index 24e77a0da3..9ecaa04300 100644 --- a/source/backend/api/Repositories/Mayan/MayanMetadataRepository.cs +++ b/source/backend/api/Repositories/Mayan/MayanMetadataRepository.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Pims.Api.Models.Mayan; using Pims.Api.Models.Mayan.Metadata; using Pims.Api.Models.Requests.Http; @@ -28,12 +29,14 @@ public class MayanMetadataRepository : MayanBaseRepository, IEdmsMetadataReposit /// Injected Httpclient factory. /// Injected repository that handles authentication. /// The injected configuration provider. + /// The json options. public MayanMetadataRepository( ILogger logger, IHttpClientFactory httpClientFactory, IEdmsAuthRepository authRepository, - IConfiguration configuration) - : base(logger, httpClientFactory, configuration) + IConfiguration configuration, + IOptions jsonOptions) + : base(logger, httpClientFactory, configuration, jsonOptions) { _authRepository = authRepository; } diff --git a/source/backend/api/Services/DocumentFileService.cs b/source/backend/api/Services/DocumentFileService.cs index 098e3e794c..55dd68affa 100644 --- a/source/backend/api/Services/DocumentFileService.cs +++ b/source/backend/api/Services/DocumentFileService.cs @@ -1,12 +1,14 @@ +using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Security.Claims; +using System.Text.Json; using System.Threading.Tasks; -using MapsterMapper; using Microsoft.Extensions.Logging; using Pims.Api.Constants; +using Pims.Api.Helpers.Extensions; using Pims.Api.Models.CodeTypes; -using Pims.Api.Models.Concepts.Document; using Pims.Api.Models.Requests.Document.Upload; using Pims.Api.Models.Requests.Http; using Pims.Core.Api.Exceptions; @@ -23,15 +25,15 @@ namespace Pims.Api.Services /// public class DocumentFileService : BaseService, IDocumentFileService { - private readonly IAcquisitionFileDocumentRepository acquisitionFileDocumentRepository; - private readonly IResearchFileDocumentRepository researchFileDocumentRepository; - private readonly IDocumentService documentService; + private readonly IAcquisitionFileDocumentRepository _acquisitionFileDocumentRepository; + private readonly IResearchFileDocumentRepository _researchFileDocumentRepository; + private readonly IDocumentService _documentService; private readonly IProjectRepository _projectRepository; private readonly IDocumentRepository _documentRepository; + private readonly IDocumentQueueRepository _documentQueueRepository; private readonly ILeaseRepository _leaseRepository; private readonly IPropertyActivityDocumentRepository _propertyActivityDocumentRepository; private readonly IDispositionFileDocumentRepository _dispositionFileDocumentRepository; - private readonly IMapper mapper; public DocumentFileService( ClaimsPrincipal user, @@ -39,23 +41,23 @@ public DocumentFileService( IAcquisitionFileDocumentRepository acquisitionFileDocumentRepository, IResearchFileDocumentRepository researchFileDocumentRepository, IDocumentService documentService, - IMapper mapper, IProjectRepository projectRepository, IDocumentRepository documentRepository, ILeaseRepository leaseRepository, IPropertyActivityDocumentRepository propertyActivityDocumentRepository, - IDispositionFileDocumentRepository dispositionFileDocumentRepository) + IDispositionFileDocumentRepository dispositionFileDocumentRepository, + IDocumentQueueRepository documentQueueRepository) : base(user, logger) { - this.acquisitionFileDocumentRepository = acquisitionFileDocumentRepository; - this.researchFileDocumentRepository = researchFileDocumentRepository; - this.documentService = documentService; - this.mapper = mapper; + _acquisitionFileDocumentRepository = acquisitionFileDocumentRepository; + _researchFileDocumentRepository = researchFileDocumentRepository; + _documentService = documentService; _projectRepository = projectRepository; _documentRepository = documentRepository; _leaseRepository = leaseRepository; _propertyActivityDocumentRepository = propertyActivityDocumentRepository; _dispositionFileDocumentRepository = dispositionFileDocumentRepository; + _documentQueueRepository = documentQueueRepository; } public IList GetFileDocuments(FileType fileType, long fileId) @@ -68,10 +70,10 @@ public IList GetFileDocuments(FileType fileType, long fileId) { case FileType.Research: User.ThrowIfNotAuthorized(Permissions.ResearchFileView); - return researchFileDocumentRepository.GetAllByResearchFile(fileId).Select(f => f as T).ToArray(); + return _researchFileDocumentRepository.GetAllByResearchFile(fileId).Select(f => f as T).ToArray(); case FileType.Acquisition: User.ThrowIfNotAuthorized(Permissions.AcquisitionFileView); - return acquisitionFileDocumentRepository.GetAllByAcquisitionFile(fileId).Select(f => f as T).ToArray(); + return _acquisitionFileDocumentRepository.GetAllByAcquisitionFile(fileId).Select(f => f as T).ToArray(); case FileType.Project: User.ThrowIfNotAuthorized(Permissions.ProjectView); return _projectRepository.GetAllProjectDocuments(fileId).Select(f => f as T).ToArray(); @@ -89,210 +91,136 @@ public IList GetFileDocuments(FileType fileType, long fileId) } } - public async Task UploadResearchDocumentAsync(long researchFileId, DocumentUploadRequest uploadRequest) + public async Task UploadAcquisitionDocument(long acquisitionFileId, DocumentUploadRequest uploadRequest) { - Logger.LogInformation("Uploading document for single research file"); - User.ThrowIfNotAllAuthorized(Permissions.DocumentAdd, Permissions.ResearchFileEdit); - - // Do not call Mayan if uploaded file is empty (zero-size) - ValidateZeroLengthFile(uploadRequest); + Logger.LogInformation("Uploading document for single acquisition file"); + User.ThrowIfNotAllAuthorized(Permissions.DocumentAdd, Permissions.AcquisitionFileEdit); + uploadRequest.ThrowInvalidFileSize(); - DocumentUploadResponse uploadResult = await documentService.UploadDocumentAsync(uploadRequest); + PimsDocument pimsDocument = CreatePimsDocument(uploadRequest); + _documentQueueRepository.SaveChanges(); - DocumentUploadRelationshipResponse relationshipResponse = new DocumentUploadRelationshipResponse() + PimsAcquisitionFileDocument newAcquisitionDocument = new() { - UploadResponse = uploadResult, + AcquisitionFileId = acquisitionFileId, + DocumentId = pimsDocument.DocumentId, }; + _acquisitionFileDocumentRepository.AddAcquisition(newAcquisitionDocument); - // Throw an error if Mayan returns a null document. This means it wasn't able to store it. - ValidateDocumentUploadResponse(uploadResult); + await GenerateQueuedDocumentItem(pimsDocument.DocumentId, uploadRequest); + _acquisitionFileDocumentRepository.CommitTransaction(); - if (uploadResult.Document is not null && uploadResult.Document.Id != 0) - { - // Create the pims document research file relationship - PimsResearchFileDocument newResearchFileDocument = new PimsResearchFileDocument() - { - ResearchFileId = researchFileId, - DocumentId = uploadResult.Document.Id, - }; - newResearchFileDocument = researchFileDocumentRepository.AddResearch(newResearchFileDocument); - researchFileDocumentRepository.CommitTransaction(); - - relationshipResponse.DocumentRelationship = mapper.Map(newResearchFileDocument); - } - - return relationshipResponse; + return; } - public async Task UploadAcquisitionDocumentAsync(long acquisitionFileId, DocumentUploadRequest uploadRequest) + public async Task UploadResearchDocument(long researchFileId, DocumentUploadRequest uploadRequest) { - Logger.LogInformation("Uploading document for single acquisition file"); - User.ThrowIfNotAllAuthorized(Permissions.DocumentAdd, Permissions.AcquisitionFileEdit); - - // Do not call Mayan if uploaded file is empty (zero-size) - ValidateZeroLengthFile(uploadRequest); + Logger.LogInformation("Uploading document for single research file"); + User.ThrowIfNotAllAuthorized(Permissions.DocumentAdd, Permissions.ResearchFileEdit); + uploadRequest.ThrowInvalidFileSize(); - DocumentUploadResponse uploadResult = await documentService.UploadDocumentAsync(uploadRequest); + PimsDocument pimsDocument = CreatePimsDocument(uploadRequest); + _documentQueueRepository.SaveChanges(); - DocumentUploadRelationshipResponse relationshipResponse = new DocumentUploadRelationshipResponse() + PimsResearchFileDocument newFileDocument = new() { - UploadResponse = uploadResult, + ResearchFileId = researchFileId, + DocumentId = pimsDocument.DocumentId, }; + _researchFileDocumentRepository.AddResearch(newFileDocument); - // Throw an error if Mayan returns a null document. This means it wasn't able to store it. - ValidateDocumentUploadResponse(uploadResult); + await GenerateQueuedDocumentItem(pimsDocument.DocumentId, uploadRequest); + _documentQueueRepository.CommitTransaction(); - if (uploadResult.Document is not null && uploadResult.Document.Id != 0) - { - // Create the pims document acquisition file relationship - PimsAcquisitionFileDocument newAcquisitionDocument = new PimsAcquisitionFileDocument() - { - AcquisitionFileId = acquisitionFileId, - DocumentId = uploadResult.Document.Id, - }; - newAcquisitionDocument = acquisitionFileDocumentRepository.AddAcquisition(newAcquisitionDocument); - acquisitionFileDocumentRepository.CommitTransaction(); - - relationshipResponse.DocumentRelationship = mapper.Map(newAcquisitionDocument); - } - - return relationshipResponse; + return; } - public async Task UploadProjectDocumentAsync(long projectId, DocumentUploadRequest uploadRequest) + public async Task UploadProjectDocument(long projectId, DocumentUploadRequest uploadRequest) { Logger.LogInformation("Uploading document for single Project"); User.ThrowIfNotAllAuthorized(Permissions.DocumentAdd, Permissions.ProjectEdit); + uploadRequest.ThrowInvalidFileSize(); - // Do not call Mayan if uploaded file is empty (zero-size) - ValidateZeroLengthFile(uploadRequest); + PimsDocument pimsDocument = CreatePimsDocument(uploadRequest); + _documentQueueRepository.SaveChanges(); - DocumentUploadResponse uploadResult = await documentService.UploadDocumentAsync(uploadRequest); - - DocumentUploadRelationshipResponse relationshipResponse = new() + PimsProjectDocument newFileDocument = new() { - UploadResponse = uploadResult, + ProjectId = projectId, + DocumentId = pimsDocument.DocumentId, }; + _projectRepository.AddProjectDocument(newFileDocument); - // Throw an error if Mayan returns a null document. This means it wasn't able to store it. - ValidateDocumentUploadResponse(uploadResult); - - if (uploadResult.Document is not null && uploadResult.Document.Id != 0) - { - PimsProjectDocument newProjectDocument = new() - { - ProjectId = projectId, - DocumentId = uploadResult.Document.Id, - }; - newProjectDocument = _projectRepository.AddProjectDocument(newProjectDocument); - _projectRepository.CommitTransaction(); - - relationshipResponse.DocumentRelationship = mapper.Map(newProjectDocument); - } + await GenerateQueuedDocumentItem(pimsDocument.DocumentId, uploadRequest); + _documentQueueRepository.CommitTransaction(); - return relationshipResponse; + return; } - public async Task UploadLeaseDocumentAsync(long leaseId, DocumentUploadRequest uploadRequest) + public async Task UploadLeaseDocument(long leaseId, DocumentUploadRequest uploadRequest) { Logger.LogInformation("Uploading document for single Lease"); User.ThrowIfNotAllAuthorized(Permissions.DocumentAdd, Permissions.LeaseEdit); + uploadRequest.ThrowInvalidFileSize(); - // Do not call Mayan if uploaded file is empty (zero-size) - ValidateZeroLengthFile(uploadRequest); - - DocumentUploadResponse uploadResult = await documentService.UploadDocumentAsync(uploadRequest); + PimsDocument pimsDocument = CreatePimsDocument(uploadRequest); + _documentQueueRepository.SaveChanges(); - DocumentUploadRelationshipResponse relationshipResponse = new() + PimsLeaseDocument newFileDocument = new() { - UploadResponse = uploadResult, + LeaseId = leaseId, + DocumentId = pimsDocument.DocumentId, }; + _leaseRepository.AddLeaseDocument(newFileDocument); - // Throw an error if Mayan returns a null document. This means it wasn't able to store it. - ValidateDocumentUploadResponse(uploadResult); - - if (uploadResult.Document is not null && uploadResult.Document.Id != 0) - { - PimsLeaseDocument newDocument = new() - { - LeaseId = leaseId, - DocumentId = uploadResult.Document.Id, - }; - newDocument = _leaseRepository.AddLeaseDocument(newDocument); - _leaseRepository.CommitTransaction(); - - relationshipResponse.DocumentRelationship = mapper.Map(newDocument); - } + await GenerateQueuedDocumentItem(pimsDocument.DocumentId, uploadRequest); + _documentQueueRepository.CommitTransaction(); - return relationshipResponse; + return; } - public async Task UploadPropertyActivityDocumentAsync(long propertyActivityId, DocumentUploadRequest uploadRequest) + public async Task UploadPropertyActivityDocument(long propertyActivityId, DocumentUploadRequest uploadRequest) { Logger.LogInformation("Uploading document for single Property Activity"); User.ThrowIfNotAllAuthorized(Permissions.DocumentAdd, Permissions.ManagementEdit); + uploadRequest.ThrowInvalidFileSize(); - // Do not call Mayan if uploaded file is empty (zero-size) - ValidateZeroLengthFile(uploadRequest); - - DocumentUploadResponse uploadResult = await documentService.UploadDocumentAsync(uploadRequest); + PimsDocument pimsDocument = CreatePimsDocument(uploadRequest); + _documentQueueRepository.SaveChanges(); - DocumentUploadRelationshipResponse relationshipResponse = new() + PimsPropertyActivityDocument newFileDocument = new() { - UploadResponse = uploadResult, + PimsPropertyActivityId = propertyActivityId, + DocumentId = pimsDocument.DocumentId, }; + _propertyActivityDocumentRepository.AddPropertyActivityDocument(newFileDocument); - // Throw an error if Mayan returns a null document. This means it wasn't able to store it. - ValidateDocumentUploadResponse(uploadResult); + await GenerateQueuedDocumentItem(pimsDocument.DocumentId, uploadRequest); + _documentQueueRepository.CommitTransaction(); - if (uploadResult.Document is not null && uploadResult.Document.Id != 0) - { - PimsPropertyActivityDocument newDocument = new() - { - PimsPropertyActivityId = propertyActivityId, - DocumentId = uploadResult.Document.Id, - }; - newDocument = _propertyActivityDocumentRepository.AddPropertyActivityDocument(newDocument); - _propertyActivityDocumentRepository.CommitTransaction(); - - relationshipResponse.DocumentRelationship = mapper.Map(newDocument); - } - - return relationshipResponse; + return; } - public async Task UploadDispositionDocumentAsync(long dispositionFileId, DocumentUploadRequest uploadRequest) + public async Task UploadDispositionDocument(long dispositionFileId, DocumentUploadRequest uploadRequest) { Logger.LogInformation("Uploading document for single disposition file"); User.ThrowIfNotAllAuthorized(Permissions.DocumentAdd, Permissions.DispositionEdit); + uploadRequest.ThrowInvalidFileSize(); - // Do not call Mayan if uploaded file is empty (zero-size) - ValidateZeroLengthFile(uploadRequest); - - DocumentUploadResponse uploadResult = await documentService.UploadDocumentAsync(uploadRequest); + PimsDocument pimsDocument = CreatePimsDocument(uploadRequest); + _documentQueueRepository.SaveChanges(); - DocumentUploadRelationshipResponse relationshipResponse = new() + PimsDispositionFileDocument newFileDocument = new() { - UploadResponse = uploadResult, + DispositionFileId = dispositionFileId, + DocumentId = pimsDocument.DocumentId, }; + _dispositionFileDocumentRepository.AddDispositionDocument(newFileDocument); - // Throw an error if Mayan returns a null document. This means it wasn't able to store it. - ValidateDocumentUploadResponse(uploadResult); - - if (uploadResult.Document is not null && uploadResult.Document.Id != 0) - { - PimsDispositionFileDocument newDocument = new() - { - DispositionFileId = dispositionFileId, - DocumentId = uploadResult.Document.Id, - }; - newDocument = _dispositionFileDocumentRepository.AddDispositionDocument(newDocument); - _dispositionFileDocumentRepository.CommitTransaction(); - - relationshipResponse.DocumentRelationship = mapper.Map(newDocument); - } + await GenerateQueuedDocumentItem(pimsDocument.DocumentId, uploadRequest); + _documentQueueRepository.CommitTransaction(); - return relationshipResponse; + return; } public async Task> DeleteResearchDocumentAsync(PimsResearchFileDocument researchFileDocument) @@ -300,17 +228,26 @@ public async Task> DeleteResearchDocumentAsync(PimsRese Logger.LogInformation("Deleting PIMS document for single research file"); User.ThrowIfNotAllAuthorized(Permissions.DocumentDelete, Permissions.ResearchFileEdit); - var relationshipCount = _documentRepository.DocumentRelationshipCount(researchFileDocument.DocumentId); - if (relationshipCount == 1) - { - return await documentService.DeleteDocumentAsync(researchFileDocument.Document); - } - else + var result = new ExternalResponse() { Status = ExternalResponseStatus.NotExecuted }; + PimsDocument currentDocument = _documentRepository.Find(researchFileDocument.Document.DocumentId); + + if (currentDocument.MayanId.HasValue && currentDocument.MayanId.Value > 0) { - researchFileDocumentRepository.DeleteResearch(researchFileDocument); - researchFileDocumentRepository.CommitTransaction(); - return new ExternalResponse() { Status = ExternalResponseStatus.NotExecuted }; + result = await DeleteMayanDocument((long)currentDocument.MayanId); + currentDocument = RemoveDocumentMayanID(currentDocument); + _documentRepository.CommitTransaction(); } + + using var transaction = _documentRepository.BeginTransaction(); + + DeleteQueuedDocumentItem(currentDocument.DocumentId); + _researchFileDocumentRepository.DeleteResearch(researchFileDocument); + DeleteDocument(currentDocument); + + _documentRepository.SaveChanges(); + transaction.Commit(); + + return result; } public async Task> DeleteProjectDocumentAsync(PimsProjectDocument projectDocument) @@ -318,17 +255,26 @@ public async Task> DeleteProjectDocumentAsync(PimsProje Logger.LogInformation("Deleting PIMS document for single Project"); User.ThrowIfNotAllAuthorized(Permissions.DocumentDelete, Permissions.ProjectEdit); - var relationshipCount = _documentRepository.DocumentRelationshipCount(projectDocument.DocumentId); - if (relationshipCount == 1) - { - return await documentService.DeleteDocumentAsync(projectDocument.Document); - } - else + var result = new ExternalResponse() { Status = ExternalResponseStatus.NotExecuted }; + PimsDocument currentDocument = _documentRepository.Find(projectDocument.Document.DocumentId); + + if (currentDocument.MayanId.HasValue && currentDocument.MayanId.Value > 0) { - _projectRepository.DeleteProjectDocument(projectDocument.ProjectDocumentId); - _projectRepository.CommitTransaction(); - return new ExternalResponse() { Status = ExternalResponseStatus.NotExecuted }; + result = await DeleteMayanDocument((long)currentDocument.MayanId); + currentDocument = RemoveDocumentMayanID(currentDocument); + _documentRepository.CommitTransaction(); } + + using var transaction = _documentRepository.BeginTransaction(); + + DeleteQueuedDocumentItem(currentDocument.DocumentId); + _projectRepository.DeleteProjectDocument(projectDocument.ProjectDocumentId); + DeleteDocument(currentDocument); + + _documentRepository.SaveChanges(); + transaction.Commit(); + + return result; } public async Task> DeleteAcquisitionDocumentAsync(PimsAcquisitionFileDocument acquisitionFileDocument) @@ -336,17 +282,28 @@ public async Task> DeleteAcquisitionDocumentAsync(PimsA Logger.LogInformation("Deleting PIMS document for single acquisition file"); User.ThrowIfNotAllAuthorized(Permissions.DocumentDelete, Permissions.AcquisitionFileEdit); - var relationshipCount = _documentRepository.DocumentRelationshipCount(acquisitionFileDocument.DocumentId); - if (relationshipCount == 1) - { - return await documentService.DeleteDocumentAsync(acquisitionFileDocument.Document); - } - else + var result = new ExternalResponse() { Status = ExternalResponseStatus.NotExecuted }; + PimsDocument currentDocument = _documentRepository.Find(acquisitionFileDocument.Document.DocumentId); + + if (currentDocument.MayanId.HasValue && currentDocument.MayanId.Value > 0) { - acquisitionFileDocumentRepository.DeleteAcquisition(acquisitionFileDocument); - acquisitionFileDocumentRepository.CommitTransaction(); - return new ExternalResponse() { Status = ExternalResponseStatus.NotExecuted }; + result = await DeleteMayanDocument((long)acquisitionFileDocument.Document.MayanId); + currentDocument = RemoveDocumentMayanID(currentDocument); + _documentRepository.CommitTransaction(); } + + using var transaction = _documentRepository.BeginTransaction(); + + DeleteQueuedDocumentItem(acquisitionFileDocument.DocumentId); + + _acquisitionFileDocumentRepository.DeleteAcquisition(acquisitionFileDocument); + + DeleteDocument(currentDocument); + + _documentRepository.SaveChanges(); + transaction.Commit(); + + return result; } public async Task> DeleteLeaseDocumentAsync(PimsLeaseDocument leaseDocument) @@ -354,17 +311,28 @@ public async Task> DeleteLeaseDocumentAsync(PimsLeaseDo Logger.LogInformation("Deleting PIMS document for single lease"); User.ThrowIfNotAllAuthorized(Permissions.DocumentDelete, Permissions.LeaseEdit); - var relationshipCount = _documentRepository.DocumentRelationshipCount(leaseDocument.DocumentId); - if (relationshipCount == 1) - { - return await documentService.DeleteDocumentAsync(leaseDocument.Document); - } - else + var result = new ExternalResponse() { Status = ExternalResponseStatus.NotExecuted }; + PimsDocument currentDocument = _documentRepository.Find(leaseDocument.Document.DocumentId); + + // 1 - Delete Mayan first. + if (currentDocument.MayanId.HasValue && currentDocument.MayanId.Value > 0) { - _leaseRepository.DeleteLeaseDocument(leaseDocument.LeaseDocumentId); - _leaseRepository.CommitTransaction(); - return new ExternalResponse() { Status = ExternalResponseStatus.NotExecuted }; + result = await DeleteMayanDocument((long)currentDocument.MayanId); + currentDocument = RemoveDocumentMayanID(currentDocument); + _documentRepository.CommitTransaction(); } + + using var transaction = _documentRepository.BeginTransaction(); + + DeleteQueuedDocumentItem(currentDocument.DocumentId); + _leaseRepository.DeleteLeaseDocument(leaseDocument.LeaseDocumentId); + + DeleteDocument(currentDocument); + + _documentRepository.SaveChanges(); + transaction.Commit(); + + return result; } public async Task> DeletePropertyActivityDocumentAsync(PimsPropertyActivityDocument propertyActivityDocument) @@ -372,17 +340,27 @@ public async Task> DeletePropertyActivityDocumentAsync( Logger.LogInformation("Deleting PIMS document for single Property Activity"); User.ThrowIfNotAllAuthorized(Permissions.DocumentDelete, Permissions.ManagementEdit); - var relationshipCount = _documentRepository.DocumentRelationshipCount(propertyActivityDocument.DocumentId); - if (relationshipCount == 1) - { - return await documentService.DeleteDocumentAsync(propertyActivityDocument.Document); - } - else + var result = new ExternalResponse() { Status = ExternalResponseStatus.NotExecuted }; + PimsDocument currentDocument = _documentRepository.Find(propertyActivityDocument.Document.DocumentId); + + if (currentDocument.MayanId.HasValue && currentDocument.MayanId.Value > 0) { - _propertyActivityDocumentRepository.DeletePropertyActivityDocument(propertyActivityDocument); - _propertyActivityDocumentRepository.CommitTransaction(); - return new ExternalResponse() { Status = ExternalResponseStatus.NotExecuted }; + result = await DeleteMayanDocument((long)currentDocument.MayanId); + currentDocument = RemoveDocumentMayanID(currentDocument); + _documentRepository.CommitTransaction(); } + + using var transaction = _documentRepository.BeginTransaction(); + + DeleteQueuedDocumentItem(currentDocument.DocumentId); + + _propertyActivityDocumentRepository.DeletePropertyActivityDocument(propertyActivityDocument); + DeleteDocument(currentDocument); + + _documentRepository.SaveChanges(); + transaction.Commit(); + + return result; } public async Task> DeleteDispositionDocumentAsync(PimsDispositionFileDocument dispositionFileDocument) @@ -390,32 +368,99 @@ public async Task> DeleteDispositionDocumentAsync(PimsD Logger.LogInformation("Deleting PIMS document for single disposition file"); User.ThrowIfNotAllAuthorized(Permissions.DocumentDelete, Permissions.DispositionEdit); - var relationshipCount = _documentRepository.DocumentRelationshipCount(dispositionFileDocument.DocumentId); - if (relationshipCount == 1) + var result = new ExternalResponse() { Status = ExternalResponseStatus.NotExecuted }; + PimsDocument currentDocument = _documentRepository.Find(dispositionFileDocument.DocumentId); + + if (currentDocument.MayanId.HasValue && currentDocument.MayanId.Value > 0) { - return await documentService.DeleteDocumentAsync(dispositionFileDocument.Document); + result = await DeleteMayanDocument((long)currentDocument.MayanId); + currentDocument = RemoveDocumentMayanID(currentDocument); + _documentRepository.CommitTransaction(); // leave trace when mayan document deleted. } - else + + using var transaction = _documentRepository.BeginTransaction(); + + DeleteQueuedDocumentItem(currentDocument.DocumentId); + + _dispositionFileDocumentRepository.DeleteDispositionDocument(dispositionFileDocument); + + DeleteDocument(currentDocument); + + _documentRepository.SaveChanges(); + transaction.Commit(); + + return result; + } + + private PimsDocument CreatePimsDocument(DocumentUploadRequest uploadRequest, string documentExternalId = null) + { + // Create the pims document + PimsDocument newPimsDocument = new() + { + FileName = uploadRequest.File.FileName, + DocumentTypeId = uploadRequest.DocumentTypeId, + DocumentStatusTypeCode = uploadRequest.DocumentStatusCode, + MayanId = null, + DocumentExternalId = documentExternalId, + }; + + _documentRepository.Add(newPimsDocument); + + return newPimsDocument; + } + + private async Task GenerateQueuedDocumentItem(long documentId, DocumentUploadRequest uploadRequest) + { + PimsDocumentQueue queueDocument = new() { - _dispositionFileDocumentRepository.DeleteDispositionDocument(dispositionFileDocument); - _dispositionFileDocumentRepository.CommitTransaction(); - return new ExternalResponse() { Status = ExternalResponseStatus.NotExecuted }; + DocumentId = documentId, + Document = await uploadRequest.File.GetBytes(), + DocumentMetadata = uploadRequest.DocumentMetadata != null ? JsonSerializer.Serialize(uploadRequest.DocumentMetadata) : null, + }; + _documentQueueRepository.Add(queueDocument); + } + + private async Task> DeleteMayanDocument(long mayanDocumentId) + { + var result = await _documentService.DeleteMayanStorageDocumentAsync(mayanDocumentId); + if (result.HttpStatusCode == HttpStatusCode.NotFound) + { + result.Status = ExternalResponseStatus.Success; } + + return result; } - private static void ValidateZeroLengthFile(DocumentUploadRequest uploadRequest) + private PimsDocument RemoveDocumentMayanID(PimsDocument doc) { - if (uploadRequest.File is not null && uploadRequest.File.Length == 0) + doc.MayanId = null; + return _documentRepository.Update(doc, false); + } + + private void DeleteQueuedDocumentItem(long documentId) + { + var documentQueuedItem = _documentQueueRepository.GetByDocumentId(documentId); + if (documentQueuedItem.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.PENDING.ToString() + || documentQueuedItem.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.PROCESSING.ToString()) + { + throw new BadRequestException("Doucment in process can not be deleted"); + } + + bool deleted = _documentQueueRepository.Delete(documentQueuedItem); + if(!deleted) { - throw new BadRequestException("The submitted file is empty"); + Logger.LogWarning("Failed to delete Queued Document {documentId}", documentId); + throw new InvalidOperationException("Could not delete document queue item"); } } - private static void ValidateDocumentUploadResponse(DocumentUploadResponse uploadResult) + private void DeleteDocument(PimsDocument document) { - if (uploadResult.Document is null) + bool deleted = _documentRepository.DeleteDocument(document); + if (!deleted) { - throw new BadRequestException("Unexpected exception uploading file", new System.Exception(uploadResult.DocumentExternalResponse.Message)); + Logger.LogWarning("Failed to delete Document {documentId}", document.DocumentId); + throw new InvalidOperationException("Could not delete document"); } } } diff --git a/source/backend/api/Services/DocumentQueueService.cs b/source/backend/api/Services/DocumentQueueService.cs index 4ffb2c4363..031418e489 100644 --- a/source/backend/api/Services/DocumentQueueService.cs +++ b/source/backend/api/Services/DocumentQueueService.cs @@ -1,7 +1,19 @@ +using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Security.Claims; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Pims.Api.Models.CodeTypes; +using Pims.Api.Models.Concepts.Document; +using Pims.Api.Models.Mayan.Document; +using Pims.Api.Models.Requests.Document.Upload; +using Pims.Api.Models.Requests.Http; +using Pims.Core.Api.Exceptions; using Pims.Core.Api.Services; using Pims.Core.Extensions; using Pims.Core.Http.Configuration; @@ -17,26 +29,325 @@ namespace Pims.Api.Services /// public class DocumentQueueService : BaseService, IDocumentQueueService { - private readonly IDocumentQueueRepository documentQueueRepository; - private readonly IOptionsMonitor keycloakOptions; + private readonly IDocumentQueueRepository _documentQueueRepository; + private readonly IDocumentRepository _documentRepository; + private readonly IDocumentTypeRepository _documentTypeRepository; + private readonly IDocumentService _documentService; + private readonly IOptionsMonitor _keycloakOptions; public DocumentQueueService( ClaimsPrincipal user, ILogger logger, IDocumentQueueRepository documentQueueRepository, + IDocumentRepository documentRepository, + IDocumentTypeRepository documentTypeRepository, + IDocumentService documentService, IOptionsMonitor options) : base(user, logger) { - this.documentQueueRepository = documentQueueRepository; - this.keycloakOptions = options; + this._documentQueueRepository = documentQueueRepository; + this._documentRepository = documentRepository; + this._documentTypeRepository = documentTypeRepository; + this._documentService = documentService; + this._keycloakOptions = options; } + /// + /// Get document in the document queue based on the specified id. + /// + /// The id of the document in the queue. + /// that match the id criteria. + /// Thrown when the user is not authorized to perform this operation. + /// If the requested Id does not exist. + public PimsDocumentQueue GetById(long documentQueueId) + { + this.Logger.LogInformation("Retrieving queued PIMS document using id {documentQueueId}", documentQueueId); + this.User.ThrowIfNotAuthorizedOrServiceAccount(Permissions.SystemAdmin, this._keycloakOptions); + + var documentQueue = _documentQueueRepository.TryGetById(documentQueueId); + if (documentQueue == null) + { + throw new KeyNotFoundException($"Unable to find queued document by id: ${documentQueueId}"); + } + + return documentQueue; + } + + /// + /// Searches for documents in the document queue based on the specified filter. + /// + /// The filter criteria to apply when searching the document queue. + /// An enumerable collection of that match the filter criteria. + /// Thrown when the user is not authorized to perform this operation. public IEnumerable SearchDocumentQueue(DocumentQueueFilter filter) { - this.Logger.LogInformation("Retrieving queued PIMS documents using filter {filter}", filter); - this.User.ThrowIfNotAuthorizedOrServiceAccount(Permissions.SystemAdmin, this.keycloakOptions); + this.Logger.LogInformation("Retrieving queued PIMS documents using filter {filter}", filter.Serialize()); + this.User.ThrowIfNotAuthorizedOrServiceAccount(Permissions.SystemAdmin, this._keycloakOptions); + + var queuedDocuments = _documentQueueRepository.GetAllByFilter(filter); + + if (filter.MaxFileSize != null) + { + List documentsBelowMaxFileSize = new List(); + long totalFileSize = 0; + var documentIdSizeDict = _documentQueueRepository.GetFileLengthsById(queuedDocuments.Select(qd => qd.DocumentQueueId)); + queuedDocuments.ForEach(currentDocument => + { + long currentFileSize = documentIdSizeDict.GetValueOrDefault(currentDocument.DocumentQueueId, 0); + if (currentFileSize + totalFileSize <= filter.MaxFileSize) + { + totalFileSize += currentFileSize; + documentsBelowMaxFileSize.Add(currentDocument); + } + }); + if(documentsBelowMaxFileSize.Count == 0 && queuedDocuments.Any()) + { + documentsBelowMaxFileSize.Add(queuedDocuments.FirstOrDefault()); + } + this.Logger.LogDebug("returning {length} documents below file size", documentsBelowMaxFileSize.Count); + return documentsBelowMaxFileSize; + } + return queuedDocuments; + } + + /// + /// Updates the specified document queue. + /// + /// The document queue object to update. + /// The updated document queue object. + /// Thrown when the user is not authorized to perform this operation. + public PimsDocumentQueue Update(PimsDocumentQueue documentQueue) + { + this.Logger.LogInformation("Updating queued document {documentQueueId}", documentQueue.DocumentQueueId); + this.Logger.LogDebug("Incoming queued document {document}", documentQueue.Serialize()); + + this.User.ThrowIfNotAuthorizedOrServiceAccount(Permissions.SystemAdmin, this._keycloakOptions); + + _documentQueueRepository.Update(documentQueue); + _documentQueueRepository.CommitTransaction(); + return documentQueue; + } + + /// + /// Polls for the status of a document in mayan, and updates the queue based on the result. + /// + /// The document queue object containing the document details. + /// A task that represents the asynchronous operation. The task result contains the updated document queue object, or null if the polling failed. + /// Thrown when the user is not authorized to perform this operation. + /// Thrown when the document queue does not have a valid document ID or related document. + public async Task PollForDocument(PimsDocumentQueue documentQueue) + { + this.Logger.LogInformation("Polling queued document {documentQueueId}", documentQueue.DocumentQueueId); + this.Logger.LogDebug("Polling queued document {document}", documentQueue.Serialize()); + + this.User.ThrowIfNotAuthorizedOrServiceAccount(Permissions.SystemAdmin, this._keycloakOptions); + if (documentQueue.DocumentId == null) + { + this.Logger.LogError("polled queued document does not have a document Id {documentQueueId}", documentQueue.DocumentQueueId); + throw new InvalidDataException("DocumentId is required to poll for a document."); + } + + var databaseDocumentQueue = _documentQueueRepository.TryGetById(documentQueue.DocumentQueueId); + if (databaseDocumentQueue == null) + { + this.Logger.LogError("Unable to find document queue with {id}", documentQueue.DocumentQueueId); + throw new KeyNotFoundException($"Unable to find document queue with matching id: {documentQueue.DocumentQueueId}"); + } + + var relatedDocument = _documentRepository.TryGet(documentQueue.DocumentId.Value); + + if (relatedDocument?.MayanId == null || relatedDocument?.MayanId < 0) + { + this.Logger.LogError("Queued Document {documentQueueId} has no mayan id and is invalid.", documentQueue.DocumentQueueId); + databaseDocumentQueue.MayanError = "Document does not have a valid MayanId."; + UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); + return databaseDocumentQueue; + } + + ExternalResponse documentDetailsResponse = await _documentService.GetStorageDocumentDetail(relatedDocument.MayanId.Value); + + if (documentDetailsResponse.Status != ExternalResponseStatus.Success || documentDetailsResponse?.Payload == null) + { + this.Logger.LogError("Polling for queued document {documentQueueId} failed with status {documentDetailsResponseStatus}", documentQueue.DocumentQueueId, documentDetailsResponse.Status); + databaseDocumentQueue.MayanError = "Document Polling failed."; + UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); + return databaseDocumentQueue; + } + + if (documentDetailsResponse.Payload.FileLatest?.Id == null) + { + this.Logger.LogInformation("Polling for queued document {documentQueueId} complete, file still processing", documentQueue.DocumentQueueId); + } + else + { + this.Logger.LogInformation("Polling for queued document {documentQueueId} complete, file uploaded successfully", documentQueue.DocumentQueueId); + UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.SUCCESS); + } - return documentQueueRepository.GetAllByFilter(filter); + return databaseDocumentQueue; + } + + /// + /// Uploads the specified document queue. + /// + /// The document queue object containing the document to upload. + /// A task that represents the asynchronous operation. The task result contains the updated document queue object, or null if the upload failed. + /// Thrown when the user is not authorized to perform this operation. + /// Thrown when the document queue does not have a valid document ID or related document. + public async Task Upload(PimsDocumentQueue documentQueue) + { + this.Logger.LogInformation("Uploading queued document {documentQueueId}", documentQueue.DocumentQueueId); + this.Logger.LogDebug("Uploading queued document {document}", documentQueue.Serialize()); + + this.User.ThrowIfNotAuthorizedOrServiceAccount(Permissions.SystemAdmin, this._keycloakOptions); + + var databaseDocumentQueue = _documentQueueRepository.TryGetById(documentQueue.DocumentQueueId); + if (databaseDocumentQueue == null) + { + this.Logger.LogError("Unable to find document queue with {id}", documentQueue.DocumentQueueId); + throw new KeyNotFoundException($"Unable to find document queue with matching id: {documentQueue.DocumentQueueId}"); + } + databaseDocumentQueue.DocProcessStartDt = DateTime.UtcNow; + + // if the document queued for upload is already in an error state, update the retries. + if (databaseDocumentQueue.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.PIMS_ERROR.ToString() || databaseDocumentQueue.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.MAYAN_ERROR.ToString()) + { + this.Logger.LogDebug("Document Queue {documentQueueId}, previously errored, retrying", documentQueue.DocumentQueueId); + databaseDocumentQueue.DocProcessRetries += 1; + databaseDocumentQueue.DocProcessEndDt = null; + } + + bool isValid = ValidateQueuedDocument(databaseDocumentQueue, documentQueue); + if (!isValid) + { + this.Logger.LogDebug("Document Queue {documentQueueId}, invalid, aborting upload.", documentQueue.DocumentQueueId); + databaseDocumentQueue.MayanError = "Document is invalid."; + UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); + return databaseDocumentQueue; + } + + PimsDocument relatedDocument = null; + relatedDocument = _documentRepository.TryGetDocumentRelationships(databaseDocumentQueue.DocumentId.Value); + if (relatedDocument?.DocumentTypeId == null) + { + databaseDocumentQueue.MayanError = "Document does not have a valid DocumentType."; + this.Logger.LogError("Queued document {documentQueueId} does not have a related PIMS_DOCUMENT {documentId} with valid DocumentType, aborting.", databaseDocumentQueue.DocumentQueueId, relatedDocument?.DocumentId); + UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); + return databaseDocumentQueue; + } + else if (relatedDocument?.MayanId != null && relatedDocument?.MayanId > 0) + { + this.Logger.LogInformation("Queued document {documentQueueId} already has a mayan id {mayanid}, no further processing required.", databaseDocumentQueue.DocumentQueueId, relatedDocument.MayanId); + UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.SUCCESS); + return databaseDocumentQueue; // The document poll job should pick this up and fix the document queue status. + } + + try + { + PimsDocumentTyp documentTyp = _documentTypeRepository.GetById(relatedDocument.DocumentTypeId); // throws KeyNotFoundException if not found. + + IFormFile file = null; + using MemoryStream memStream = new(databaseDocumentQueue.Document); + file = new FormFile(memStream, 0, databaseDocumentQueue.Document.Length, relatedDocument.FileName, relatedDocument.FileName); + + DocumentUploadRequest request = new DocumentUploadRequest() + { + File = file, + DocumentStatusCode = relatedDocument.DocumentStatusTypeCode, + DocumentTypeId = relatedDocument.DocumentTypeId, + DocumentTypeMayanId = documentTyp.MayanId, + DocumentId = relatedDocument.DocumentId, + DocumentMetadata = databaseDocumentQueue.DocumentMetadata != null ? JsonSerializer.Deserialize>(databaseDocumentQueue.DocumentMetadata) : null, + }; + this.Logger.LogDebug("Document Queue {documentQueueId}, beginning upload.", documentQueue.DocumentQueueId); + DocumentUploadResponse response = await _documentService.UploadDocumentAsync(request, true); + UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PROCESSING); // Set the status to processing, as the document is now being uploaded. Must be set after the mayan id is set, so that the poll logic functions correctly. + + if (response.DocumentExternalResponse.Status != ExternalResponseStatus.Success || response?.DocumentExternalResponse?.Payload == null) + { + this.Logger.LogError( + "Queued document upload failed {databaseDocumentQueueDocumentQueueId} {databaseDocumentQueueDocumentQueueStatusTypeCode}, {documentExternalResponseStatus}", + databaseDocumentQueue.DocumentQueueId, + databaseDocumentQueue.DocumentQueueStatusTypeCode, + response.DocumentExternalResponse.Status); + + databaseDocumentQueue.MayanError = $"Failed to upload document, mayan error: {response.DocumentExternalResponse.Message}"; + UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.MAYAN_ERROR); + return databaseDocumentQueue; + } + response.MetadataExternalResponse.Where(r => r.Status != ExternalResponseStatus.Success).ForEach(r => this.Logger.LogError("url: ${url} status: ${status} message ${message}", r.Payload.Url, r.Status, r.Message)); // Log any metadata errors, but don't fail the upload. + + // Mayan may have already returned a file id from the original upload. If not, this job will remain in the processing state (to be periodically checked for completion in another job). + if (response.DocumentExternalResponse?.Payload?.FileLatest?.Id != null) + { + UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.SUCCESS); + } + } + catch (Exception ex) when (ex is BadRequestException || ex is KeyNotFoundException || ex is InvalidDataException || ex is JsonException) + { + this.Logger.LogError($"Error: {ex.Message}"); + databaseDocumentQueue.MayanError = ex.Message; + UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); + } + return databaseDocumentQueue; + } + + /// + /// Updates the status of the specified document queue. + /// + /// The document queue object to update. + /// The new status type to set for the document queue. + /// + /// This method updates the document queue's status and commits the transaction. + /// If the status is a final state, it also updates the processing end date. + /// + private void UpdateDocumentQueueStatus(PimsDocumentQueue documentQueue, DocumentQueueStatusTypes statusType) + { + documentQueue.DocumentQueueStatusTypeCode = statusType.ToString(); + bool removeDocument = false; + + // Any final states should update the processing end date. + if (statusType != DocumentQueueStatusTypes.PROCESSING && statusType != DocumentQueueStatusTypes.PENDING) + { + documentQueue.DocProcessEndDt = DateTime.UtcNow; + if (statusType == DocumentQueueStatusTypes.SUCCESS) + { + documentQueue.Document = null; + removeDocument = true; + } + } + _documentQueueRepository.Update(documentQueue, removeDocument); + _documentQueueRepository.CommitTransaction(); + } + + /// + /// Validates the queued document against the database document queue. + /// + /// The document queue object from the database. + /// The document queue object to validate against the database. + /// True if the queued document is valid; otherwise, false. + /// + /// This method checks if the status type, process retries, and document content are valid. + /// It also ensures that at least one file document ID is associated with the document. + /// + private bool ValidateQueuedDocument(PimsDocumentQueue databaseDocumentQueue, PimsDocumentQueue externalDocument) + { + if (databaseDocumentQueue.DocumentQueueStatusTypeCode != externalDocument.DocumentQueueStatusTypeCode) + { + this.Logger.LogError("Requested document queue status: {documentQueueStatusTypeCode} does not match current database status: {documentQueueStatusTypeCode}", externalDocument.DocumentQueueStatusTypeCode, databaseDocumentQueue.DocumentQueueStatusTypeCode); + return false; + } + else if (databaseDocumentQueue.DocProcessRetries != externalDocument.DocProcessRetries) + { + this.Logger.LogError("Requested document retries: {documentQueueStatusTypeCode} does not match current database retries: {documentQueueStatusTypeCode}", externalDocument.DocumentQueueStatusTypeCode, databaseDocumentQueue.DocumentQueueStatusTypeCode); + return false; + } + else if (databaseDocumentQueue.Document == null || databaseDocumentQueue.DocumentId == null) + { + this.Logger.LogError("Queued document file content is empty, unable to upload."); + return false; + } + return true; } } } diff --git a/source/backend/api/Services/DocumentService.cs b/source/backend/api/Services/DocumentService.cs index 333fdb1c2b..2407d796ed 100644 --- a/source/backend/api/Services/DocumentService.cs +++ b/source/backend/api/Services/DocumentService.cs @@ -83,7 +83,8 @@ public DocumentService( IDocumentTypeRepository documentTypeRepository, IAvService avService, IMapper mapper, - IOptionsMonitor options) + IOptionsMonitor options, + IDocumentQueueRepository queueRepository) : base(user, logger) { this.documentRepository = documentRepository; @@ -136,9 +137,9 @@ public IList GetPimsDocumentTypes(DocumentRelationType relation return documentTypeRepository.GetByCategory(categoryType); } - public async Task UploadDocumentAsync(DocumentUploadRequest uploadRequest) + public async Task UploadDocumentSync(DocumentUploadRequest uploadRequest) { - this.Logger.LogInformation("Uploading document"); + this.Logger.LogInformation("Uploading document and waiting for mayan upload."); this.User.ThrowIfNotAuthorized(Permissions.DocumentAdd); ExternalResponse externalResponse = await UploadDocumentAsync(uploadRequest.DocumentTypeMayanId, uploadRequest.File); @@ -173,6 +174,7 @@ public async Task UploadDocumentAsync(DocumentUploadRequ { _ = PrecacheDocumentPreviews(externalDocument.Id, externalDocument.FileLatest.Id); } + // Create metadata of document if (uploadRequest.DocumentMetadata != null) { @@ -209,6 +211,63 @@ public async Task UploadDocumentAsync(DocumentUploadRequ return response; } + public async Task UploadDocumentAsync(DocumentUploadRequest uploadRequest, bool skipExtensionCheck = false) + { + this.Logger.LogInformation("Uploading document, do not wait for mayan processing. documentId: {documentId}", uploadRequest.DocumentId); + this.User.ThrowIfNotAuthorized(Permissions.DocumentAdd); + + ExternalResponse externalResponse = await UploadDocumentAsync(uploadRequest.DocumentTypeMayanId, uploadRequest.File, skipExtensionCheck); + DocumentUploadResponse response = new DocumentUploadResponse() + { + DocumentExternalResponse = externalResponse, + MetadataExternalResponse = new List>(), + }; + + PimsDocument databaseDocument = documentRepository.TryGet(uploadRequest.DocumentId); + response.Document = databaseDocument != null ? mapper.Map(databaseDocument) : null; + + if (response?.DocumentExternalResponse?.Payload?.Id != null && response?.DocumentExternalResponse?.Payload?.Id > 0 && databaseDocument != null) + { + // Create metadata of document + if (uploadRequest.DocumentMetadata != null) + { + List creates = new List(); + foreach (var metadata in uploadRequest.DocumentMetadata) + { + if (!string.IsNullOrEmpty(metadata.Value)) + { + creates.Add(metadata); + } + } + + response.MetadataExternalResponse = await CreateMetadata(response.DocumentExternalResponse.Payload.Id, creates); + } + + databaseDocument.MayanId = response.DocumentExternalResponse.Payload.Id; + documentRepository.Update(databaseDocument); + documentRepository.CommitTransaction(); + } + else + { + this.Logger.LogError("Failed to update associated PIMS document with uploaded Mayan Id. documentId: {documentId}", uploadRequest.DocumentId); + this.Logger.LogDebug("Mayan response: {response}", response.Serialize()); + } + + return response; + } + + public PimsDocument AddDocument(PimsDocument newPimsDocument) + { + this.Logger.LogInformation("Adding document uploaded asynchronously."); + this.User.ThrowIfNotAuthorized(Permissions.DocumentAdd); + newPimsDocument.ThrowIfNull(nameof(newPimsDocument)); + + documentRepository.Add(newPimsDocument); + documentRepository.CommitTransaction(); + + return newPimsDocument; + } + public async Task UpdateDocumentAsync(DocumentUpdateRequest updateRequest) { this.Logger.LogInformation("Updating document {documentId}", updateRequest.DocumentId); @@ -314,8 +373,8 @@ public async Task UpdateDocumentAsync(DocumentUpdateRequ public async Task> DeleteDocumentAsync(PimsDocument document) { - this.Logger.LogInformation("Deleting document {documentId}", document.Internal_Id); - this.User.ThrowIfNotAuthorized(Permissions.DocumentDelete); + Logger.LogInformation("Deleting document {documentId}", document.Internal_Id); + User.ThrowIfNotAuthorized(Permissions.DocumentDelete); var result = new ExternalResponse() { Status = ExternalResponseStatus.NotExecuted }; if (document.MayanId.HasValue) @@ -337,6 +396,26 @@ public async Task> DeleteDocumentAsync(PimsDocument doc { throw GetMayanResponseError(result.Message); } + + return result; + } + + public async Task> DeleteMayanStorageDocumentAsync(long mayanDocumentId) + { + Logger.LogInformation("Deleting Mayan document {documentId}", mayanDocumentId); + User.ThrowIfNotAuthorized(Permissions.DocumentDelete); + + ExternalResponse result = await documentStorageRepository.TryDeleteDocument(mayanDocumentId); + if(result.Status == ExternalResponseStatus.Error && result.HttpStatusCode == HttpStatusCode.NotFound) + { + return result; + } + + if (result.Status == ExternalResponseStatus.Error || result.Status == ExternalResponseStatus.NotExecuted) + { + throw GetMayanResponseError(result.Message); + } + return result; } @@ -591,13 +670,13 @@ private async Task PrecacheDocumentPreviews(long documentId, long documentFileId } } - private async Task> UploadDocumentAsync(long documentType, IFormFile fileRaw) + private async Task> UploadDocumentAsync(long documentType, IFormFile fileRaw, bool skipExtensionCheck = false) { this.Logger.LogInformation("Uploading storage document {documentType}", documentType); this.User.ThrowIfNotAuthorized(Permissions.DocumentAdd); await this.avService.ScanAsync(fileRaw); - if (IsValidDocumentExtension(fileRaw.FileName)) + if (skipExtensionCheck || IsValidDocumentExtension(fileRaw.FileName)) { ExternalResponse result = await documentStorageRepository.TryUploadDocumentAsync(documentType, fileRaw); return result; diff --git a/source/backend/api/Services/FormDocumentService.cs b/source/backend/api/Services/FormDocumentService.cs index 00cab505bd..b95ece2431 100644 --- a/source/backend/api/Services/FormDocumentService.cs +++ b/source/backend/api/Services/FormDocumentService.cs @@ -80,14 +80,14 @@ public async Task UploadFormDocumentTemplate } } - DocumentUploadResponse uploadResult = await _documentService.UploadDocumentAsync(uploadRequest); + DocumentUploadResponse uploadResult = await _documentService.UploadDocumentSync(uploadRequest); DocumentUploadRelationshipResponse relationshipResponse = new DocumentUploadRelationshipResponse() { UploadResponse = uploadResult, }; - if (uploadResult.DocumentExternalResponse.Status == ExternalResponseStatus.Success && uploadResult.Document != null && uploadResult.Document.Id != 0) + if (uploadResult.DocumentExternalResponse.Status == ExternalResponseStatus.Success) { currentFormType.DocumentId = uploadResult.Document.Id; var updatedFormType = _formTypeRepository.SetFormTypeDocument(currentFormType); @@ -138,7 +138,7 @@ public PimsAcquisitionFileForm AddAcquisitionForm(PimsFormType formType, long ac public IEnumerable GetAcquisitionForms(long acquisitionFileId) { - _logger.LogInformation("Getting acquisition forms by acquisition file id ...", acquisitionFileId); + _logger.LogInformation("Getting acquisition forms by acquisition file id {acquisitionFileId}", acquisitionFileId); this.User.ThrowIfNotAuthorized(Permissions.FormView, Permissions.AcquisitionFileView); var fileForms = _acquisitionFileFormRepository.GetAllByAcquisitionFileId(acquisitionFileId); @@ -147,7 +147,7 @@ public IEnumerable GetAcquisitionForms(long acquisition public PimsAcquisitionFileForm GetAcquisitionForm(long fileFormId) { - _logger.LogInformation("Getting acquisition form by form file id ...", fileFormId); + _logger.LogInformation("Getting acquisition form by form file id {fileFormId}", fileFormId); this.User.ThrowIfNotAuthorized(Permissions.FormView, Permissions.AcquisitionFileView); var fileForm = _acquisitionFileFormRepository.GetByAcquisitionFileFormId(fileFormId); @@ -156,7 +156,7 @@ public PimsAcquisitionFileForm GetAcquisitionForm(long fileFormId) public bool DeleteAcquisitionFileForm(long fileFormId) { - _logger.LogInformation("Deleting acquisition file form id ...", fileFormId); + _logger.LogInformation("Deleting acquisition file form id {fileFormId}", fileFormId); this.User.ThrowIfNotAuthorized(Permissions.FormDelete, Permissions.AcquisitionFileEdit); var fileFormToDelete = _acquisitionFileFormRepository.TryDelete(fileFormId); diff --git a/source/backend/api/Services/IDocumentFileService.cs b/source/backend/api/Services/IDocumentFileService.cs index 987f34bcc2..aa56e119fb 100644 --- a/source/backend/api/Services/IDocumentFileService.cs +++ b/source/backend/api/Services/IDocumentFileService.cs @@ -15,17 +15,17 @@ public interface IDocumentFileService public IList GetFileDocuments(FileType fileType, long fileId) where T : PimsFileDocument; - Task UploadResearchDocumentAsync(long researchFileId, DocumentUploadRequest uploadRequest); + Task UploadAcquisitionDocument(long acquisitionFileId, DocumentUploadRequest uploadRequest); - Task UploadAcquisitionDocumentAsync(long acquisitionFileId, DocumentUploadRequest uploadRequest); + Task UploadResearchDocument(long researchFileId, DocumentUploadRequest uploadRequest); - Task UploadLeaseDocumentAsync(long leaseId, DocumentUploadRequest uploadRequest); + Task UploadProjectDocument(long projectId, DocumentUploadRequest uploadRequest); - Task UploadProjectDocumentAsync(long projectId, DocumentUploadRequest uploadRequest); + Task UploadLeaseDocument(long leaseId, DocumentUploadRequest uploadRequest); - Task UploadPropertyActivityDocumentAsync(long propertyActivityId, DocumentUploadRequest uploadRequest); + Task UploadPropertyActivityDocument(long propertyActivityId, DocumentUploadRequest uploadRequest); - Task UploadDispositionDocumentAsync(long dispositionFileId, DocumentUploadRequest uploadRequest); + Task UploadDispositionDocument(long dispositionFileId, DocumentUploadRequest uploadRequest); Task> DeleteResearchDocumentAsync(PimsResearchFileDocument researchFileDocument); diff --git a/source/backend/api/Services/IDocumentQueueService.cs b/source/backend/api/Services/IDocumentQueueService.cs index b1f3c09206..2a797d7df8 100644 --- a/source/backend/api/Services/IDocumentQueueService.cs +++ b/source/backend/api/Services/IDocumentQueueService.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading.Tasks; using Pims.Dal.Entities; using Pims.Dal.Entities.Models; @@ -9,6 +10,14 @@ namespace Pims.Api.Services /// public interface IDocumentQueueService { + public PimsDocumentQueue GetById(long documentQueueId); + public IEnumerable SearchDocumentQueue(DocumentQueueFilter filter); + + public PimsDocumentQueue Update(PimsDocumentQueue documentQueue); + + public Task PollForDocument(PimsDocumentQueue documentQueue); + + public Task Upload(PimsDocumentQueue documentQueue); } } diff --git a/source/backend/api/Services/IDocumentService.cs b/source/backend/api/Services/IDocumentService.cs index ebcd1ded2e..c37d3b6552 100644 --- a/source/backend/api/Services/IDocumentService.cs +++ b/source/backend/api/Services/IDocumentService.cs @@ -38,10 +38,14 @@ public interface IDocumentService IList GetPimsDocumentTypes(DocumentRelationType relationshipType); - Task UploadDocumentAsync(DocumentUploadRequest uploadRequest); + Task UploadDocumentAsync(DocumentUploadRequest uploadRequest, bool skipExtensionCheck = false); + + Task UploadDocumentSync(DocumentUploadRequest uploadRequest); Task UpdateDocumentAsync(DocumentUpdateRequest updateRequest); + Task> DeleteMayanStorageDocumentAsync(long mayanDocumentId); + Task> DeleteDocumentAsync(PimsDocument document); Task> GetStorageDocumentDetail(long mayanDocumentId); @@ -49,5 +53,7 @@ public interface IDocumentService Task>> GetDocumentFilePageListAsync(long documentId, long documentFileId); Task DownloadFilePageImageAsync(long mayanDocumentId, long mayanFileId, long mayanFilePageId); + + PimsDocument AddDocument(PimsDocument newPimsDocument); } } diff --git a/source/backend/api/Services/ResearchFileService.cs b/source/backend/api/Services/ResearchFileService.cs index f28025f757..81b66e5e67 100644 --- a/source/backend/api/Services/ResearchFileService.cs +++ b/source/backend/api/Services/ResearchFileService.cs @@ -5,12 +5,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Pims.Core.Extensions; +using Pims.Core.Security; using Pims.Dal.Entities; using Pims.Dal.Entities.Models; using Pims.Dal.Exceptions; -using Pims.Dal.Helpers.Extensions; using Pims.Dal.Repositories; -using Pims.Core.Security; namespace Pims.Api.Services { @@ -120,6 +119,7 @@ public PimsResearchFile UpdateProperties(PimsResearchFile researchFile, IEnumera { incomingResearchProperty.Internal_Id = matchingProperty.Internal_Id; } + // If the property is not new, check if the name has been updated. if (incomingResearchProperty.Internal_Id != 0) { @@ -173,7 +173,7 @@ public Paged GetPage(ResearchFilter filter) { _logger.LogInformation("Searching for research files..."); - _logger.LogDebug("Research file search with filter", filter); + _logger.LogDebug("Research file search with filter {filter}", filter.Serialize()); _user.ThrowIfNotAuthorized(Permissions.ResearchFileView); return _researchFileRepository.GetPage(filter); diff --git a/source/backend/api/Startup.cs b/source/backend/api/Startup.cs index ba3ab2fb7f..d0e9c5b8c2 100644 --- a/source/backend/api/Startup.cs +++ b/source/backend/api/Startup.cs @@ -31,7 +31,6 @@ using Microsoft.OpenApi.Models; using Pims.Api.Handlers; using Pims.Api.Helpers; -using Pims.Core.Api.Exceptions; using Pims.Api.Helpers.Healthchecks; using Pims.Api.Helpers.HealthChecks; using Pims.Api.Helpers.Mapping; @@ -42,21 +41,23 @@ using Pims.Api.Services; using Pims.Api.Services.Interfaces; using Pims.Av; +using Pims.Core.Api.Exceptions; using Pims.Core.Api.Helpers; +using Pims.Core.Api.Middleware; using Pims.Core.Converters; using Pims.Core.Http; using Pims.Core.Json; using Pims.Dal; using Pims.Dal.Keycloak; +using Pims.Dal.Repositories; using Pims.Geocoder; using Pims.Ltsa; using Prometheus; -using Pims.Core.Api.Middleware; namespace Pims.Api { /// - /// Startup class, provides a way to startup the .netcore RESTful API and configure it. + /// Startup class, provides a way to startup the .netcore REST API and configure it. /// [ExcludeFromCodeCoverage] public class Startup @@ -482,6 +483,7 @@ private static void AddPimsApiRepositories(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddSingleton(); } @@ -524,6 +526,7 @@ private static void AddPimsApiServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } /// diff --git a/source/backend/api/appsettings.Development.json b/source/backend/api/appsettings.Development.json index 987052db9c..7df068180a 100644 --- a/source/backend/api/appsettings.Development.json +++ b/source/backend/api/appsettings.Development.json @@ -31,7 +31,7 @@ } }, "ConnectionStrings": { - "PIMS": "Server=localhost,5433;uid=admin;Database=pims;Password=Password12" + "PIMS": "Server=localhost,5433;User ID=admin;Database=pims;TrustServerCertificate=True;Encrypt=false;" }, "Pims": { "Environment": { diff --git a/source/backend/apimodels/CodeTypes/DataSourceTypes.cs b/source/backend/apimodels/CodeTypes/DataSourceTypes.cs new file mode 100644 index 0000000000..d608790cae --- /dev/null +++ b/source/backend/apimodels/CodeTypes/DataSourceTypes.cs @@ -0,0 +1,66 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Pims.Api.Models.CodeTypes +{ + [JsonConverter(typeof(JsonStringEnumMemberConverter))] + public enum DataSourceTypes + { + [EnumMember(Value = "BIP")] + BIP, + + [EnumMember(Value = "GAZ")] + GAZ, + + [EnumMember(Value = "GWP")] + GWP, + + [EnumMember(Value = "LIS")] + LIS, + + [EnumMember(Value = "LIS_OPSS")] + LIS_OPSS, + + [EnumMember(Value = "LIS_OPSS_PAIMS")] + LIS_OPSS_PAIMS, + + [EnumMember(Value = "LIS_OPSS_PAIMS_PMBC")] + LIS_OPSS_PAIMS_PMBC, + + [EnumMember(Value = "LIS_PAIMS")] + LIS_PAIMS, + + [EnumMember(Value = "LIS_PAIMS_PMBC")] + LIS_PAIMS_PMBC, + + [EnumMember(Value = "LIS_PMBC")] + LIS_PMBC, + + [EnumMember(Value = "OPSS")] + OPSS, + + [EnumMember(Value = "OPSS_PAIMS")] + OPSS_PAIMS, + + [EnumMember(Value = "PAIMS")] + PAIMS, + + [EnumMember(Value = "PAIMS_PMBC")] + PAIMS_PMBC, + + [EnumMember(Value = "PAT")] + PAT, + + [EnumMember(Value = "PIMS")] + PIMS, + + [EnumMember(Value = "PMBC")] + PMBC, + + [EnumMember(Value = "SHAREPOINT")] + SHAREPOINT, + + [EnumMember(Value = "TAP")] + TAP, + } +} diff --git a/source/backend/apimodels/CodeTypes/DocumentQueueStatusTypes.cs b/source/backend/apimodels/CodeTypes/DocumentQueueStatusTypes.cs index 3b38b2899d..f13b022817 100644 --- a/source/backend/apimodels/CodeTypes/DocumentQueueStatusTypes.cs +++ b/source/backend/apimodels/CodeTypes/DocumentQueueStatusTypes.cs @@ -6,7 +6,6 @@ namespace Pims.Api.Models.CodeTypes [JsonConverter(typeof(JsonStringEnumMemberConverter))] public enum DocumentQueueStatusTypes { - [EnumMember(Value = "MAYAN_ERROR")] MAYAN_ERROR, diff --git a/source/backend/apimodels/CodeTypes/DocumentStatusTypes.cs b/source/backend/apimodels/CodeTypes/DocumentStatusTypes.cs new file mode 100644 index 0000000000..95f6372c7e --- /dev/null +++ b/source/backend/apimodels/CodeTypes/DocumentStatusTypes.cs @@ -0,0 +1,40 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Pims.Api.Models.CodeTypes +{ + [JsonConverter(typeof(JsonStringEnumMemberConverter))] + public enum DocumentStatusTypes + { + + [EnumMember(Value = "AMENDD")] + AMENDD, + + [EnumMember(Value = "APPROVD")] + APPROVD, + + [EnumMember(Value = "CNCLD")] + CNCLD, + + [EnumMember(Value = "DRAFT")] + DRAFT, + + [EnumMember(Value = "FINAL")] + FINAL, + + [EnumMember(Value = "NONE")] + NONE, + + [EnumMember(Value = "RGSTRD")] + RGSTRD, + + [EnumMember(Value = "SENT")] + SENT, + + [EnumMember(Value = "SIGND")] + SIGND, + + [EnumMember(Value = "UNREGD")] + UNREGD, + } +} diff --git a/source/backend/apimodels/Models/Base/CodeTypeMap.cs b/source/backend/apimodels/Models/Base/CodeTypeMap.cs index 1559535948..ed7e2b5720 100644 --- a/source/backend/apimodels/Models/Base/CodeTypeMap.cs +++ b/source/backend/apimodels/Models/Base/CodeTypeMap.cs @@ -13,7 +13,6 @@ public void Register(TypeAdapterConfig config) .Map("IsDisabled", "IsDisabled") .Map("DisplayOrder", "DisplayOrder"); - config.ForType(typeof(Entity.ITypeEntity), typeof(CodeTypeModel)) .Map("Id", "Id") .Map("Description", "Description") diff --git a/source/backend/apimodels/Models/Concepts/Document/DocumentMap.cs b/source/backend/apimodels/Models/Concepts/Document/DocumentMap.cs index 98c7252660..5fe0e656fe 100644 --- a/source/backend/apimodels/Models/Concepts/Document/DocumentMap.cs +++ b/source/backend/apimodels/Models/Concepts/Document/DocumentMap.cs @@ -1,3 +1,4 @@ +using System.Linq; using Mapster; using Pims.Api.Models.Base; using Entity = Pims.Dal.Entities; @@ -14,6 +15,7 @@ public void Register(TypeAdapterConfig config) .Map(dest => dest.DocumentType, src => src.DocumentType) .Map(dest => dest.StatusTypeCode, src => src.DocumentStatusTypeCodeNavigation) .Map(dest => dest.FileName, src => src.FileName) + .Map(dest => dest.DocumentQueueStatusTypeCode, src => src.PimsDocumentQueues.Count > 0 ? src.PimsDocumentQueues.FirstOrDefault().DocumentQueueStatusTypeCodeNavigation : null) .Inherits(); config.NewConfig() diff --git a/source/backend/apimodels/Models/Concepts/Document/DocumentModel.cs b/source/backend/apimodels/Models/Concepts/Document/DocumentModel.cs index d46734b021..6fea721ad2 100644 --- a/source/backend/apimodels/Models/Concepts/Document/DocumentModel.cs +++ b/source/backend/apimodels/Models/Concepts/Document/DocumentModel.cs @@ -18,7 +18,7 @@ public class DocumentModel : BaseAuditModel /// /// get/set - The document id on the external storage. /// - public int MayanDocumentId { get; set; } + public int? MayanDocumentId { get; set; } /// /// get/set - Document Type. @@ -34,6 +34,11 @@ public class DocumentModel : BaseAuditModel /// get/set - Document/File Name. /// public string FileName { get; set; } + + /// + /// get/set - The document queue status type. + /// + public CodeTypeModel DocumentQueueStatusTypeCode { get; set; } #endregion } } diff --git a/source/backend/apimodels/Models/Concepts/Document/DocumentTypeModel.cs b/source/backend/apimodels/Models/Concepts/Document/DocumentTypeModel.cs index 5f558d1d51..46aa23053d 100644 --- a/source/backend/apimodels/Models/Concepts/Document/DocumentTypeModel.cs +++ b/source/backend/apimodels/Models/Concepts/Document/DocumentTypeModel.cs @@ -33,7 +33,7 @@ public class DocumentTypeModel : BaseAuditModel /// /// get/set - The document type id in mayan. /// - public long MayanId { get; set; } + public long? MayanId { get; set; } /// /// get/set - The document type is disabled and is maintained for reference only. diff --git a/source/backend/apimodels/Models/Concepts/DocumentQueue/DocumentQueueMap.cs b/source/backend/apimodels/Models/Concepts/DocumentQueue/DocumentQueueMap.cs index 1a98ba0d07..22f5018cae 100644 --- a/source/backend/apimodels/Models/Concepts/DocumentQueue/DocumentQueueMap.cs +++ b/source/backend/apimodels/Models/Concepts/DocumentQueue/DocumentQueueMap.cs @@ -1,5 +1,6 @@ using Mapster; using Pims.Api.Models.Base; +using Pims.Dal.Entities; using Entity = Pims.Dal.Entities; namespace Pims.Api.Models.Concepts.Document @@ -12,12 +13,14 @@ public void Register(TypeAdapterConfig config) .Map(dest => dest.Id, src => src.DocumentQueueId) .Map(dest => dest.DocumentExternalId, src => src.DocumentExternalId) .Map(dest => dest.DocumentId, src => src.DocumentId) - .Map(dest => dest.DocumentQueueStatusType, src => src.DocumentQueueStatusTypeCodeNavigation) + .Map(dest => dest.DocumentQueueStatusType, src => src.DocumentQueueStatusTypeCodeNavigation == null ? new PimsDocumentQueueStatusType() { Id = src.DocumentQueueStatusTypeCode } : src.DocumentQueueStatusTypeCodeNavigation) .Map(dest => dest.DataSourceTypeCode, src => src.DataSourceTypeCodeNavigation) .Map(dest => dest.DocumentProcessStartTimestamp, src => src.DocProcessStartDt) .Map(dest => dest.DocumentProcessEndTimestamp, src => src.DocProcessEndDt) .Map(dest => dest.DocumentProcessRetries, src => src.DocProcessRetries) - .Map(dest => dest.Document, src => src.Document) + .Map(dest => dest.PimsDocument, src => src.DocumentNavigation) + .Map(dest => dest.MayanError, src => src.MayanError) + .Map(dest => dest.DocumentQueueStatusTypeCode, src => src.DocumentQueueStatusTypeCodeNavigation) .Inherits(); config.NewConfig() @@ -29,7 +32,9 @@ public void Register(TypeAdapterConfig config) .Map(dest => dest.DocProcessStartDt, src => src.DocumentProcessStartTimestamp) .Map(dest => dest.DocProcessEndDt, src => src.DocumentProcessEndTimestamp) .Map(dest => dest.DocProcessRetries, src => src.DocumentProcessRetries) + .Map(dest => dest.DocumentNavigation, src => src.PimsDocument) .Map(dest => dest.Document, src => src.Document) + .Map(dest => dest.MayanError, src => src.MayanError) .Inherits(); } } diff --git a/source/backend/apimodels/Models/Concepts/DocumentQueue/DocumentQueueModel.cs b/source/backend/apimodels/Models/Concepts/DocumentQueue/DocumentQueueModel.cs index 73d553f74d..aa7e0c27f6 100644 --- a/source/backend/apimodels/Models/Concepts/DocumentQueue/DocumentQueueModel.cs +++ b/source/backend/apimodels/Models/Concepts/DocumentQueue/DocumentQueueModel.cs @@ -57,11 +57,19 @@ public class DocumentQueueModel : BaseAuditModel public string MayanError { get; set; } /// - /// get/set - The actual document, represented as a byte[]. + /// get/set - The related pims document. /// - public byte[] Document { get; set; } + public DocumentModel PimsDocument { get; set; } + /// + /// get/set - The actual document content, as a byte array. + /// + public byte[] Document { get; set; } - #endregion - } + /// + /// get/set - The queue status type. + /// + public CodeTypeModel DocumentQueueStatusTypeCode { get; set; } + #endregion +} } diff --git a/source/backend/apimodels/Models/Requests/Document/Upload/DocumentUploadRequest.cs b/source/backend/apimodels/Models/Requests/Document/Upload/DocumentUploadRequest.cs index 83102292e3..7d08d437e5 100644 --- a/source/backend/apimodels/Models/Requests/Document/Upload/DocumentUploadRequest.cs +++ b/source/backend/apimodels/Models/Requests/Document/Upload/DocumentUploadRequest.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using Microsoft.AspNetCore.Http; using Pims.Api.Models.Concepts.Document; @@ -21,6 +22,11 @@ public class DocumentUploadRequest /// public long DocumentTypeId { get; set; } + /// + /// get/set - The id of the document to be uploaded (in PIMS). + /// + public long DocumentId { get; set; } + /// /// get/set - Initial status code of the document. /// diff --git a/source/backend/core.api/Pims.Core.Api.csproj b/source/backend/core.api/Pims.Core.Api.csproj index 70c947b9d1..51d218ea46 100644 --- a/source/backend/core.api/Pims.Core.Api.csproj +++ b/source/backend/core.api/Pims.Core.Api.csproj @@ -20,6 +20,7 @@ + diff --git a/source/backend/core.api/Repositories/RestCommon/BaseRestRepository.cs b/source/backend/core.api/Repositories/RestCommon/BaseRestRepository.cs index 214d359f9b..d0aaa708d2 100644 --- a/source/backend/core.api/Repositories/RestCommon/BaseRestRepository.cs +++ b/source/backend/core.api/Repositories/RestCommon/BaseRestRepository.cs @@ -10,6 +10,7 @@ using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Pims.Api.Models; using Pims.Api.Models.CodeTypes; using Pims.Api.Models.Requests.Http; @@ -23,6 +24,7 @@ public abstract class BaseRestRepository : IRestRespository { protected readonly IHttpClientFactory _httpClientFactory; protected readonly ILogger _logger; + protected readonly IOptions _jsonOptions; /// /// Initializes a new instance of the class. @@ -31,10 +33,12 @@ public abstract class BaseRestRepository : IRestRespository /// Injected Httpclient factory. protected BaseRestRepository( ILogger logger, - IHttpClientFactory httpClientFactory) + IHttpClientFactory httpClientFactory, + IOptions jsonOptions) { _logger = logger; _httpClientFactory = httpClientFactory; + _jsonOptions = jsonOptions; } public abstract void AddAuthentication(HttpClient client, string authenticationToken = null); @@ -305,7 +309,7 @@ private async Task> ProcessResponse(HttpResponseMessage r }; _logger.LogTrace("Response: {response}", response); - string payload = await response.Content.ReadAsStringAsync().ConfigureAwait(true); + var payload = await response.Content.ReadAsStreamAsync().ConfigureAwait(true); result.HttpStatusCode = response.StatusCode; switch (response.StatusCode) @@ -321,7 +325,7 @@ private async Task> ProcessResponse(HttpResponseMessage r result.Payload = (T)Convert.ChangeType(payload, typeof(T), CultureInfo.InvariantCulture); break; default: - T requestTokenResult = JsonSerializer.Deserialize(payload); + T requestTokenResult = JsonSerializer.Deserialize(payload, _jsonOptions.Value); result.Payload = requestTokenResult; break; } @@ -342,7 +346,7 @@ private async Task> ProcessResponse(HttpResponseMessage r case HttpStatusCode.BadRequest: case HttpStatusCode.MethodNotAllowed: result.Status = ExternalResponseStatus.Error; - result.Message = payload; + result.Message = await response.Content.ReadAsStringAsync(); break; default: result.Status = ExternalResponseStatus.Error; diff --git a/source/backend/dal/IRepository.cs b/source/backend/dal/IRepository.cs index 247773ffde..4188616194 100644 --- a/source/backend/dal/IRepository.cs +++ b/source/backend/dal/IRepository.cs @@ -1,9 +1,15 @@ +using Microsoft.EntityFrameworkCore.Storage; + namespace Pims.Dal { public interface IRepository { #region Methods + IDbContextTransaction BeginTransaction(); + + void SaveChanges(); + void CommitTransaction(); #endregion } diff --git a/source/backend/dal/Pims.Dal.csproj b/source/backend/dal/Pims.Dal.csproj index d1f653dc4f..453a8d2c64 100644 --- a/source/backend/dal/Pims.Dal.csproj +++ b/source/backend/dal/Pims.Dal.csproj @@ -36,7 +36,7 @@ - + diff --git a/source/backend/dal/Repositories/AcquisitionFileDocumentRepository.cs b/source/backend/dal/Repositories/AcquisitionFileDocumentRepository.cs index 56b967aa60..f0636616d9 100644 --- a/source/backend/dal/Repositories/AcquisitionFileDocumentRepository.cs +++ b/source/backend/dal/Repositories/AcquisitionFileDocumentRepository.cs @@ -41,6 +41,9 @@ public IList GetAllByAcquisitionFile(long fileId) .ThenInclude(d => d.DocumentStatusTypeCodeNavigation) .Include(ad => ad.Document) .ThenInclude(d => d.DocumentType) + .Include(ad => ad.Document) + .ThenInclude(q => q.PimsDocumentQueues) + .ThenInclude(s => s.DocumentQueueStatusTypeCodeNavigation) .Where(ad => ad.AcquisitionFileId == fileId) .AsNoTracking() .ToList(); diff --git a/source/backend/dal/Repositories/AcquisitionFileRepository.cs b/source/backend/dal/Repositories/AcquisitionFileRepository.cs index fbac91b873..b558efca19 100644 --- a/source/backend/dal/Repositories/AcquisitionFileRepository.cs +++ b/source/backend/dal/Repositories/AcquisitionFileRepository.cs @@ -926,7 +926,7 @@ private IQueryable GetCommonAcquisitionFileQueryDeep(Acquis .ThenInclude(fp => fp.AlternateProject) .Where(predicate); - query = (filter.Sort?.Any() == true) ? query.OrderByProperty(true, filter.Sort) : query.OrderBy(acq => acq.AcquisitionFileId); + query = (filter.Sort?.Length > 0) ? query.OrderByProperty(true, filter.Sort) : query.OrderBy(acq => acq.AcquisitionFileId); return query; } diff --git a/source/backend/dal/Repositories/BaseRepository.cs b/source/backend/dal/Repositories/BaseRepository.cs index 1261a87011..f8486b76a6 100644 --- a/source/backend/dal/Repositories/BaseRepository.cs +++ b/source/backend/dal/Repositories/BaseRepository.cs @@ -1,4 +1,5 @@ using System.Security.Claims; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Logging; namespace Pims.Dal.Repositories @@ -44,6 +45,23 @@ protected BaseRepository(PimsContext dbContext, ClaimsPrincipal user, ILogger + /// Begin a DB transaction. + /// + /// + public IDbContextTransaction BeginTransaction() + { + return this.Context.Database.BeginTransaction(); + } + + /// + /// Save changes for a DB action. + /// + public void SaveChanges() + { + Context.SaveChanges(); + } + /// /// Commit all saved changes as a single transaction. /// diff --git a/source/backend/dal/Repositories/BaseRepository{T_Entity}.cs b/source/backend/dal/Repositories/BaseRepository{T_Entity}.cs index 2781d0d459..6173347ee4 100644 --- a/source/backend/dal/Repositories/BaseRepository{T_Entity}.cs +++ b/source/backend/dal/Repositories/BaseRepository{T_Entity}.cs @@ -41,11 +41,6 @@ public T_Entity Find(params object[] keyValues) { return this.Context.Find(keyValues); } - - public void SaveChanges() - { - this.Context.SaveChanges(); - } #endregion } } diff --git a/source/backend/dal/Repositories/DispositionFileDocumentRepository.cs b/source/backend/dal/Repositories/DispositionFileDocumentRepository.cs index 7855458d55..82d5a77032 100644 --- a/source/backend/dal/Repositories/DispositionFileDocumentRepository.cs +++ b/source/backend/dal/Repositories/DispositionFileDocumentRepository.cs @@ -42,6 +42,9 @@ public IList GetAllByDispositionFile(long fileId) .ThenInclude(d => d.DocumentStatusTypeCodeNavigation) .Include(fd => fd.Document) .ThenInclude(d => d.DocumentType) + .Include(x => x.Document) + .ThenInclude(q => q.PimsDocumentQueues) + .ThenInclude(s => s.DocumentQueueStatusTypeCodeNavigation) .Where(fd => fd.DispositionFileId == fileId) .AsNoTracking() .ToList(); diff --git a/source/backend/dal/Repositories/DocumentQueueRepository.cs b/source/backend/dal/Repositories/DocumentQueueRepository.cs index fa2472427a..e6664d3604 100644 --- a/source/backend/dal/Repositories/DocumentQueueRepository.cs +++ b/source/backend/dal/Repositories/DocumentQueueRepository.cs @@ -1,7 +1,10 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Pims.Api.Models.CodeTypes; using Pims.Core.Extensions; using Pims.Dal.Entities; using Pims.Dal.Entities.Models; @@ -32,16 +35,69 @@ public DocumentQueueRepository( #region Methods + /// + /// Attempts to find a queued document via the documentQueueId. Returns null if not found. + /// + /// + /// + public PimsDocumentQueue TryGetById(long documentQueueId) + { + + return Context.PimsDocumentQueues + .AsNoTracking() + .FirstOrDefault(dq => dq.DocumentQueueId == documentQueueId); + } + + /// + /// Add Document to Queue. + /// + /// + /// + public PimsDocumentQueue Add(PimsDocumentQueue queuedDocument) + { + queuedDocument.ThrowIfNull(nameof(queuedDocument)); + + // Default values for new queue items. + queuedDocument.DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.PENDING.ToString(); + queuedDocument.DataSourceTypeCode = DataSourceTypes.PIMS.ToString(); + queuedDocument.MayanError = null; + + // Add + Context.PimsDocumentQueues.Add(queuedDocument); + + return queuedDocument; + } + + /// + /// Find Queue Item for a Document. + /// + /// + /// + public PimsDocumentQueue GetByDocumentId(long documentId) + { + return Context.PimsDocumentQueues.Where(x => x.DocumentId == documentId).FirstOrDefault(); + } + /// /// Updates the queued document in the database. /// /// /// - public PimsDocumentQueue Update(PimsDocumentQueue queuedDocument) + public PimsDocumentQueue Update(PimsDocumentQueue queuedDocument, bool removeDocument = false) { queuedDocument.ThrowIfNull(nameof(queuedDocument)); + var existingQueuedDocument = TryGetById(queuedDocument.DocumentQueueId); + if (!removeDocument) + { + queuedDocument.Document = existingQueuedDocument.Document; + } + + queuedDocument.MayanError = queuedDocument.MayanError?.Truncate(4000); + queuedDocument.DataSourceTypeCode = existingQueuedDocument.DataSourceTypeCode; // Do not allow the data source to be updated. + Context.Entry(existingQueuedDocument).CurrentValues.SetValues(queuedDocument); queuedDocument = Context.Update(queuedDocument).Entity; + return queuedDocument; } @@ -53,11 +109,20 @@ public PimsDocumentQueue Update(PimsDocumentQueue queuedDocument) public bool Delete(PimsDocumentQueue queuedDocument) { queuedDocument.ThrowIfNull(nameof(queuedDocument)); - Context.Remove(queuedDocument); + return true; } + public Dictionary GetFileLengthsById(IEnumerable documentQueueIds) + { + return Context.PimsDocumentQueues + .AsNoTracking() + .Where(dq => documentQueueIds.Any(dqId => dqId == dq.DocumentQueueId)) + .Select(dq => new { Key = dq.Internal_Id, Value = dq.Document.Length }) + .ToDictionary(pair => pair.Key, pair => pair.Value); + } + /// /// Return a list of documents, filtered by the specified arguments. /// @@ -65,25 +130,62 @@ public bool Delete(PimsDocumentQueue queuedDocument) /// public IEnumerable GetAllByFilter(DocumentQueueFilter filter) { - var query = Context.PimsDocumentQueues.Where(q => true); + var query = Context.PimsDocumentQueues + .Include(dq => dq.DocumentNavigation) + .ThenInclude(d => d.DocumentType) + .Include(dq => dq.DocumentQueueStatusTypeCodeNavigation) + .Include(dq => dq.DataSourceTypeCodeNavigation) + .Where(q => true).AsNoTracking(); if (filter.DataSourceTypeCode != null) { - query.Where(d => d.DataSourceTypeCode == filter.DataSourceTypeCode); + query = query.Where(d => d.DataSourceTypeCode == filter.DataSourceTypeCode); } - if (filter.DocumentQueueStatusTypeCode != null) + if (filter.DocumentQueueStatusTypeCodes != null && filter.DocumentQueueStatusTypeCodes.Length > 0) { - query.Where(d => d.DocumentQueueStatusTypeCode == filter.DocumentQueueStatusTypeCode); + query = query.Where(d => filter.DocumentQueueStatusTypeCodes.Any(filterStatus => d.DocumentQueueStatusTypeCode == filterStatus)); } if (filter.DocProcessStartDate != null) { - query.Where(d => d.DocProcessStartDt >= filter.DocProcessStartDate); + query = query.Where(d => d.DocProcessStartDt >= filter.DocProcessStartDate); } if (filter.DocProcessEndDate != null) { - query.Where(d => d.DocProcessEndDt <= filter.DocProcessEndDate); + query = query.Where(d => d.DocProcessEndDt <= filter.DocProcessEndDate); } - return query.ToList(); + if (filter.MaxDocProcessRetries != null) + { + query = query.Where(d => d.DocProcessRetries == null || d.DocProcessRetries < filter.MaxDocProcessRetries); + } + + // Return the PimsDocumentQueue search results without the file contents - to avoid memory issues. + return query.Take(filter.Quantity).Select(dq => new PimsDocumentQueue() + { + DocumentQueueId = dq.DocumentQueueId, + DocumentId = dq.DocumentId, + DocumentQueueStatusTypeCode = dq.DocumentQueueStatusTypeCode, + DocumentQueueStatusTypeCodeNavigation = dq.DocumentQueueStatusTypeCodeNavigation, + DataSourceTypeCode = dq.DataSourceTypeCode, + DataSourceTypeCodeNavigation = dq.DataSourceTypeCodeNavigation, + DocumentExternalId = dq.DocumentExternalId, + DocProcessStartDt = dq.DocProcessStartDt, + DocProcessEndDt = dq.DocProcessEndDt, + DocProcessRetries = dq.DocProcessRetries, + MayanError = dq.MayanError, + AppCreateTimestamp = dq.AppCreateTimestamp, + AppCreateUserDirectory = dq.AppCreateUserDirectory, + AppCreateUserGuid = dq.AppCreateUserGuid, + AppCreateUserid = dq.AppCreateUserid, + AppLastUpdateTimestamp = dq.AppLastUpdateTimestamp, + AppLastUpdateUserDirectory = dq.AppLastUpdateUserDirectory, + AppLastUpdateUserGuid = dq.AppLastUpdateUserGuid, + AppLastUpdateUserid = dq.AppLastUpdateUserid, + DbCreateTimestamp = dq.DbCreateTimestamp, + DbCreateUserid = dq.DbCreateUserid, + DbLastUpdateTimestamp = dq.DbLastUpdateTimestamp, + DbLastUpdateUserid = dq.DbLastUpdateUserid, + ConcurrencyControlNumber = dq.ConcurrencyControlNumber, + }).ToList(); } public int DocumentQueueCount(PimsDocumentQueueStatusType pimsDocumentQueueStatusType) @@ -92,6 +194,7 @@ public int DocumentQueueCount(PimsDocumentQueueStatusType pimsDocumentQueueStatu { Context.PimsDocumentQueues.Count(); } + return Context.PimsDocumentQueues.Count(d => d.DocumentQueueStatusTypeCode == pimsDocumentQueueStatusType.DocumentQueueStatusTypeCode); } diff --git a/source/backend/dal/Repositories/DocumentRepository.cs b/source/backend/dal/Repositories/DocumentRepository.cs index c9a9716baa..2cdf09989b 100644 --- a/source/backend/dal/Repositories/DocumentRepository.cs +++ b/source/backend/dal/Repositories/DocumentRepository.cs @@ -4,9 +4,8 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Pims.Core.Extensions; -using Pims.Dal.Entities; -using Pims.Dal.Helpers.Extensions; using Pims.Core.Security; +using Pims.Dal.Entities; namespace Pims.Dal.Repositories { @@ -44,6 +43,23 @@ public PimsDocument TryGet(long documentId) return this.Context.PimsDocuments.AsNoTracking().FirstOrDefault(x => x.DocumentId == documentId); } + public PimsDocument TryGetDocumentRelationships(long documentId) + { + var documentRelationships = Context.PimsDocuments.AsNoTracking() + .Include(d => d.PimsResearchFileDocuments) + .Include(d => d.PimsAcquisitionFileDocuments) + .Include(d => d.PimsProjectDocuments) + .Include(d => d.PimsFormTypes) + .Include(d => d.PimsLeaseDocuments) + .Include(d => d.PimsPropertyActivityDocuments) + .Include(d => d.PimsDispositionFileDocuments) + .Where(d => d.DocumentId == documentId) + .AsNoTracking() + .FirstOrDefault(); + + return documentRelationships; + } + /// /// Adds the passed document to the database. /// @@ -72,9 +88,10 @@ public PimsDocument Add(PimsDocument document) public PimsDocument Update(PimsDocument document, bool commitTransaction = true) { document.ThrowIfNull(nameof(document)); + User.ThrowIfNotAuthorized(Permissions.DocumentEdit); - this.User.ThrowIfNotAuthorized(Permissions.DocumentEdit); document = Context.Update(document).Entity; + return document; } @@ -83,7 +100,7 @@ public PimsDocument Update(PimsDocument document, bool commitTransaction = true) /// /// /// - public bool Delete(PimsDocument document) + public bool Delete(PimsDocument document, bool commitTransaction = true) { document.ThrowIfNull(nameof(document)); @@ -137,9 +154,27 @@ public bool Delete(PimsDocument document) Context.Entry(pimsFormTypeDocument).Property(x => x.DocumentId).IsModified = true; } - Context.CommitTransaction(); // TODO: required to enforce delete order. Can be removed when cascade deletes are implemented. + if (commitTransaction) + { + Context.CommitTransaction(); // TODO: required to enforce delete order. Can be removed when cascade deletes are implemented. + } Context.PimsDocuments.Remove(new PimsDocument() { Internal_Id = document.Internal_Id }); + + return true; + } + + /// + /// Deletes the passed document from the database. + /// + /// + /// + public bool DeleteDocument(PimsDocument document) + { + document.ThrowIfNull(nameof(document)); + + Context.PimsDocuments.Remove(document); + return true; } diff --git a/source/backend/dal/Repositories/Interfaces/IDocumentQueueRepository.cs b/source/backend/dal/Repositories/Interfaces/IDocumentQueueRepository.cs index 45c14d19ac..8ff5bf4e65 100644 --- a/source/backend/dal/Repositories/Interfaces/IDocumentQueueRepository.cs +++ b/source/backend/dal/Repositories/Interfaces/IDocumentQueueRepository.cs @@ -9,9 +9,18 @@ namespace Pims.Dal.Repositories /// public interface IDocumentQueueRepository : IRepository { + + PimsDocumentQueue TryGetById(long documentQueueId); + + PimsDocumentQueue Add(PimsDocumentQueue queuedDocument); + + PimsDocumentQueue GetByDocumentId(long documentId); + IEnumerable GetAllByFilter(DocumentQueueFilter filter); - PimsDocumentQueue Update(PimsDocumentQueue queuedDocument); + Dictionary GetFileLengthsById(IEnumerable documentQueueIds); + + PimsDocumentQueue Update(PimsDocumentQueue queuedDocument, bool removeDocument = false); bool Delete(PimsDocumentQueue queuedDocument); diff --git a/source/backend/dal/Repositories/Interfaces/IDocumentRepository.cs b/source/backend/dal/Repositories/Interfaces/IDocumentRepository.cs index 3be4e2b302..dd53b48872 100644 --- a/source/backend/dal/Repositories/Interfaces/IDocumentRepository.cs +++ b/source/backend/dal/Repositories/Interfaces/IDocumentRepository.cs @@ -13,8 +13,12 @@ public interface IDocumentRepository : IRepository PimsDocument Update(PimsDocument document, bool commitTransaction = true); - bool Delete(PimsDocument document); + bool Delete(PimsDocument document, bool commitTransaction = true); + + bool DeleteDocument(PimsDocument document); int DocumentRelationshipCount(long documentId); + + PimsDocument TryGetDocumentRelationships(long documentId); } } diff --git a/source/backend/dal/Repositories/LeaseRepository.cs b/source/backend/dal/Repositories/LeaseRepository.cs index 0748d9e223..f241527fac 100644 --- a/source/backend/dal/Repositories/LeaseRepository.cs +++ b/source/backend/dal/Repositories/LeaseRepository.cs @@ -782,6 +782,9 @@ public IList GetAllLeaseDocuments(long leaseId) .ThenInclude(d => d.DocumentStatusTypeCodeNavigation) .Include(ad => ad.Document) .ThenInclude(d => d.DocumentType) + .Include(x => x.Document) + .ThenInclude(q => q.PimsDocumentQueues) + .ThenInclude(s => s.DocumentQueueStatusTypeCodeNavigation) .Where(x => x.LeaseId == leaseId) .AsNoTracking() .ToList(); diff --git a/source/backend/dal/Repositories/ProjectRepository.cs b/source/backend/dal/Repositories/ProjectRepository.cs index 5fb6c42f84..223ceee44f 100644 --- a/source/backend/dal/Repositories/ProjectRepository.cs +++ b/source/backend/dal/Repositories/ProjectRepository.cs @@ -184,6 +184,9 @@ public IList GetAllProjectDocuments(long projectId) .ThenInclude(d => d.DocumentStatusTypeCodeNavigation) .Include(ad => ad.Document) .ThenInclude(d => d.DocumentType) + .Include(x => x.Document) + .ThenInclude(q => q.PimsDocumentQueues) + .ThenInclude(s => s.DocumentQueueStatusTypeCodeNavigation) .Where(x => x.ProjectId == projectId) .AsNoTracking() .ToList(); diff --git a/source/backend/dal/Repositories/PropertyActivityFileDocumentRepository.cs b/source/backend/dal/Repositories/PropertyActivityFileDocumentRepository.cs index b07156a10d..9d4a08263a 100644 --- a/source/backend/dal/Repositories/PropertyActivityFileDocumentRepository.cs +++ b/source/backend/dal/Repositories/PropertyActivityFileDocumentRepository.cs @@ -41,6 +41,9 @@ public IList GetAllByPropertyActivity(long propert .ThenInclude(d => d.DocumentStatusTypeCodeNavigation) .Include(ad => ad.Document) .ThenInclude(d => d.DocumentType) + .Include(x => x.Document) + .ThenInclude(q => q.PimsDocumentQueues) + .ThenInclude(s => s.DocumentQueueStatusTypeCodeNavigation) .Where(ad => ad.PimsPropertyActivityId == propertyActivityId) .AsNoTracking() .ToList(); diff --git a/source/backend/dal/Repositories/ResearchFileDocumentRepository.cs b/source/backend/dal/Repositories/ResearchFileDocumentRepository.cs index 6759964926..4aa9fcd2f5 100644 --- a/source/backend/dal/Repositories/ResearchFileDocumentRepository.cs +++ b/source/backend/dal/Repositories/ResearchFileDocumentRepository.cs @@ -41,6 +41,9 @@ public IList GetAllByResearchFile(long fileId) .ThenInclude(d => d.DocumentStatusTypeCodeNavigation) .Include(ad => ad.Document) .ThenInclude(d => d.DocumentType) + .Include(ad => ad.Document) + .ThenInclude(q => q.PimsDocumentQueues) + .ThenInclude(s => s.DocumentQueueStatusTypeCodeNavigation) .Where(ad => ad.ResearchFileId == fileId) .AsNoTracking() .ToList(); diff --git a/source/backend/dal/Repositories/UserRepository.cs b/source/backend/dal/Repositories/UserRepository.cs index 4e4902ffd1..3150d21730 100644 --- a/source/backend/dal/Repositories/UserRepository.cs +++ b/source/backend/dal/Repositories/UserRepository.cs @@ -7,12 +7,12 @@ using Microsoft.Extensions.Options; using Pims.Core.Extensions; using Pims.Core.Http.Configuration; +using Pims.Core.Security; using Pims.Dal.Entities; using Pims.Dal.Entities.Comparers; using Pims.Dal.Entities.Models; using Pims.Dal.Exceptions; using Pims.Dal.Helpers.Extensions; -using Pims.Core.Security; namespace Pims.Dal.Repositories { diff --git a/source/backend/entities/Models/DocumentQueueFilter.cs b/source/backend/entities/Models/DocumentQueueFilter.cs index c9d43c5e61..d6d8259823 100644 --- a/source/backend/entities/Models/DocumentQueueFilter.cs +++ b/source/backend/entities/Models/DocumentQueueFilter.cs @@ -14,7 +14,7 @@ public class DocumentQueueFilter : PageFilter /// /// get/set - The status of the document in the queue, such as 'Pending'. /// - public string DocumentQueueStatusTypeCode { get; set; } + public string[] DocumentQueueStatusTypeCodes { get; set; } /// /// get/set - The date/time that processing of the document started. @@ -26,6 +26,16 @@ public class DocumentQueueFilter : PageFilter /// public DateTime? DocProcessEndDate { get; set; } + /// + /// get/set - The maximum number of times that the system has attempted to upload the document after the initial failure. + /// + public int? MaxDocProcessRetries { get; set; } + + /// + /// get/set - The maximum file size to return from the filter. + /// + public int? MaxFileSize { get; set; } + #endregion #region Constructors diff --git a/source/backend/entities/Partials/DocumentQueueStatusType.cs b/source/backend/entities/Partials/DocumentQueueStatusType.cs new file mode 100644 index 0000000000..9fd14e1cf7 --- /dev/null +++ b/source/backend/entities/Partials/DocumentQueueStatusType.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Pims.Dal.Entities +{ + /// + /// PimsDocumentQueueStatusType class, provides an entity for the datamodel to manage document queue status types. + /// + public partial class PimsDocumentQueueStatusType : ITypeEntity + { + #region Properties + + /// + /// get/set - Primary key to identify disposition type. + /// + [NotMapped] + public string Id { get => DocumentQueueStatusTypeCode; set => DocumentQueueStatusTypeCode = value; } + #endregion + + #region Constructors + + public PimsDocumentQueueStatusType() { } + + /// + /// Create a new instance of a PimsDocumentQueueStatusType class. + /// + /// + public PimsDocumentQueueStatusType(string id) + { + Id = id; + } + #endregion + } +} diff --git a/source/backend/entities/ef/PimsDocument.cs b/source/backend/entities/ef/PimsDocument.cs index bf16d7a285..1ebb797281 100644 --- a/source/backend/entities/ef/PimsDocument.cs +++ b/source/backend/entities/ef/PimsDocument.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; diff --git a/source/backend/entities/ef/PimsDocumentQueue.cs b/source/backend/entities/ef/PimsDocumentQueue.cs index a4dc9c20a1..8efe09b4a7 100644 --- a/source/backend/entities/ef/PimsDocumentQueue.cs +++ b/source/backend/entities/ef/PimsDocumentQueue.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -29,7 +29,7 @@ public partial class PimsDocumentQueue public long? DocumentId { get; set; } /// - /// Code value that represents the current status of the document as it is processed by PIMS/MAYAN + /// Code value that represents the current status of the document as it is processed by PIMS/MAYAN. /// [Required] [Column("DOCUMENT_QUEUE_STATUS_TYPE_CODE")] @@ -65,7 +65,7 @@ public partial class PimsDocumentQueue public DateTime? DocProcessStartDt { get; set; } /// - /// When the document?s processing finishes, this will be populated + /// When the document?s processing finishes, this will be populated. /// [Column("DOC_PROCESS_END_DT", TypeName = "datetime")] public DateTime? DocProcessEndDt { get; set; } @@ -90,7 +90,7 @@ public partial class PimsDocumentQueue public byte[] Document { get; set; } /// - /// Application code is responsible for retrieving the row and then incrementing the value of the CONCURRENCY_CONTROL_NUMBER column by one prior to issuing an update. If this is done then the update will succeed, provided that the row was not updated by any o + /// Application code is responsible for retrieving the row and then incrementing the value of the CONCURRENCY_CONTROL_NUMBER column by one prior to issuing an update. If this is done then the update will succeed, provided that the row was not updated by any o. /// [Column("CONCURRENCY_CONTROL_NUMBER")] public long ConcurrencyControlNumber { get; set; } diff --git a/source/backend/geocoder/Pims.Geocoder.csproj b/source/backend/geocoder/Pims.Geocoder.csproj index cceef3324b..df67a1e506 100644 --- a/source/backend/geocoder/Pims.Geocoder.csproj +++ b/source/backend/geocoder/Pims.Geocoder.csproj @@ -12,7 +12,7 @@ - + diff --git a/source/backend/keycloak/Pims.Keycloak.csproj b/source/backend/keycloak/Pims.Keycloak.csproj index 1a4db4b783..90f26d1ebb 100644 --- a/source/backend/keycloak/Pims.Keycloak.csproj +++ b/source/backend/keycloak/Pims.Keycloak.csproj @@ -12,7 +12,7 @@ - + diff --git a/source/backend/ltsa/Pims.Ltsa.csproj b/source/backend/ltsa/Pims.Ltsa.csproj index ecd2ca6fef..e471010bca 100644 --- a/source/backend/ltsa/Pims.Ltsa.csproj +++ b/source/backend/ltsa/Pims.Ltsa.csproj @@ -15,7 +15,7 @@ - + diff --git a/source/backend/scheduler/Configuration/QueryProcessingDocumentsJobOptions.cs b/source/backend/scheduler/Configuration/QueryProcessingDocumentsJobOptions.cs new file mode 100644 index 0000000000..3e30f8034d --- /dev/null +++ b/source/backend/scheduler/Configuration/QueryProcessingDocumentsJobOptions.cs @@ -0,0 +1,21 @@ +namespace Pims.Scheduler.Http.Configuration +{ + /// + /// QueryProcessingDocumentsJobOptions class, provides a way to store job configuration. + /// + public class QueryProcessingDocumentsJobOptions + { + #region Properties + + /// + /// get/set - the number of queued documents to pull in a single operation - affects the number of documents that will be uploaded in a single job run. + /// + public int? BatchSize { get; set; } + + /// + /// get/set - the maximum number of minutes a document can be processing for before the upload is considered to be a failure. + /// + public int MaxProcessingMinutes { get; set; } + #endregion + } +} diff --git a/source/backend/scheduler/Configuration/RetryQueuedDocumentsJobOptions.cs b/source/backend/scheduler/Configuration/RetryQueuedDocumentsJobOptions.cs new file mode 100644 index 0000000000..36686d8437 --- /dev/null +++ b/source/backend/scheduler/Configuration/RetryQueuedDocumentsJobOptions.cs @@ -0,0 +1,21 @@ +namespace Pims.Scheduler.Http.Configuration +{ + /// + /// RetryQueuedDocumentsJobOptions class, provides a way to store job configuration. + /// + public class RetryQueuedDocumentsJobOptions + { + #region Properties + + /// + /// get/set - the number of queued documents to pull in a single operation - affects the number of documents that will be uploaded in a single job run. + /// + public int? BatchSize { get; set; } + + /// + /// get/set - the file size, in bytes, that will be processed in a single job run. + /// + public int? MaxFileSize { get; set; } + #endregion + } +} diff --git a/source/backend/scheduler/Configuration/UploadQueuedDocumentsJobOptions.cs b/source/backend/scheduler/Configuration/UploadQueuedDocumentsJobOptions.cs new file mode 100644 index 0000000000..0c7dddc252 --- /dev/null +++ b/source/backend/scheduler/Configuration/UploadQueuedDocumentsJobOptions.cs @@ -0,0 +1,21 @@ +namespace Pims.Scheduler.Http.Configuration +{ + /// + /// UploadQueuedDocumentsJobOptions class, provides a way to store job configuration. + /// + public class UploadQueuedDocumentsJobOptions + { + #region Properties + + /// + /// get/set - the number of queued documents to pull in a single operation - affects the number of documents that will be uploaded in a single job run. + /// + public int? BatchSize { get; set; } + + /// + /// get/set - the file size, in bytes, that will be processed in a single job run. + /// + public int? MaxFileSize { get; set; } + #endregion + } +} diff --git a/source/backend/scheduler/Models/DocumentQueueResponseModel.cs b/source/backend/scheduler/Models/DocumentQueueResponseModel.cs new file mode 100644 index 0000000000..c7fc1b699f --- /dev/null +++ b/source/backend/scheduler/Models/DocumentQueueResponseModel.cs @@ -0,0 +1,11 @@ +using Pims.Api.Models.CodeTypes; + +namespace Pims.Scheduler.Models +{ + public class DocumentQueueResponseModel + { + public DocumentQueueStatusTypes DocumentQueueStatus { get; set; } + + public string Message { get; set; } + } +} diff --git a/source/backend/scheduler/Models/ScheduledTaskResponseModel.cs b/source/backend/scheduler/Models/ScheduledTaskResponseModel.cs new file mode 100644 index 0000000000..5b2e6c7631 --- /dev/null +++ b/source/backend/scheduler/Models/ScheduledTaskResponseModel.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Pims.Scheduler.Models +{ + public class ScheduledTaskResponseModel + { + public TaskResponseStatusTypes Status { get; set; } + + public string Message { get; set; } + + public IEnumerable DocumentQueueResponses { get; set; } + } +} diff --git a/source/backend/scheduler/Models/SearchQueuedDocumentsResponseModel.cs b/source/backend/scheduler/Models/SearchQueuedDocumentsResponseModel.cs new file mode 100644 index 0000000000..f37ee8dfe4 --- /dev/null +++ b/source/backend/scheduler/Models/SearchQueuedDocumentsResponseModel.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Pims.Api.Models.Concepts.Document; +using Pims.Api.Models.Requests.Http; + +namespace Pims.Scheduler.Models +{ + public class SearchQueuedDocumentsResponseModel + { + public ExternalResponse> SearchResults { get; set; } + + public ScheduledTaskResponseModel ScheduledTaskResponseModel { get; set; } + } +} diff --git a/source/backend/scheduler/Models/TaskResponseStatusTypes.cs b/source/backend/scheduler/Models/TaskResponseStatusTypes.cs new file mode 100644 index 0000000000..7f118cdba9 --- /dev/null +++ b/source/backend/scheduler/Models/TaskResponseStatusTypes.cs @@ -0,0 +1,10 @@ +namespace Pims.Scheduler.Models +{ + public enum TaskResponseStatusTypes + { + ERROR, + SUCCESS, + PARTIAL, + SKIPPED, + } +} diff --git a/source/backend/scheduler/Pims.Scheduler.csproj b/source/backend/scheduler/Pims.Scheduler.csproj index e4f4993886..f8553b3712 100644 --- a/source/backend/scheduler/Pims.Scheduler.csproj +++ b/source/backend/scheduler/Pims.Scheduler.csproj @@ -2,7 +2,7 @@ true - 16BC0468-78F6-4C91-87DA-7403C919E646 + {AC4336C5-5631-4D9D-B78F-6C2DF79A6F1F} net8.0 diff --git a/source/backend/scheduler/Repositories/Interfaces/IPimsDocumentQueueRepository.cs b/source/backend/scheduler/Repositories/Interfaces/IPimsDocumentQueueRepository.cs new file mode 100644 index 0000000000..7142760d1b --- /dev/null +++ b/source/backend/scheduler/Repositories/Interfaces/IPimsDocumentQueueRepository.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Pims.Api.Models.Concepts.Document; +using Pims.Api.Models.Requests.Http; +using Pims.Dal.Entities.Models; + +namespace Pims.Scheduler.Repositories +{ + /// + /// IPimsDocumentQueueRepository interface, defines the functionality for a repository that interacts with the pims document queue api. + /// + public interface IPimsDocumentQueueRepository + { + Task> GetById(long documentQueueId); + + Task> UploadQueuedDocument(DocumentQueueModel document); + + Task> PollQueuedDocument(DocumentQueueModel document); + + Task> UpdateQueuedDocument(long documentQueueId, DocumentQueueModel document); + + Task>> SearchQueuedDocumentsAsync(DocumentQueueFilter filter); + } +} diff --git a/source/backend/scheduler/Repositories/Interfaces/IPimsDocumentRepository.cs b/source/backend/scheduler/Repositories/Interfaces/IPimsDocumentRepository.cs deleted file mode 100644 index 7505e3576d..0000000000 --- a/source/backend/scheduler/Repositories/Interfaces/IPimsDocumentRepository.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Pims.Api.Models.Concepts.Document; -using Pims.Api.Models.Requests.Http; -using Pims.Dal.Entities.Models; - -namespace Pims.Scheduler.Repositories.Pims -{ - /// - /// IPimsDocumentQueueRepository interface, defines the functionality for a pims repository. - /// - public interface IPimsDocumentQueueRepository - { - Task>> SearchQueuedDocumentsAsync(DocumentQueueFilter filter); - } -} diff --git a/source/backend/scheduler/Repositories/PimsBaseRepository.cs b/source/backend/scheduler/Repositories/PimsBaseRepository.cs index 6cdccd99cc..5f822e7eb3 100644 --- a/source/backend/scheduler/Repositories/PimsBaseRepository.cs +++ b/source/backend/scheduler/Repositories/PimsBaseRepository.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; +using System.Text.Json; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Pims.Core.Api.Repositories.Rest; namespace Pims.Scheduler.Repositories @@ -14,10 +16,12 @@ public abstract class PimsBaseRepository : BaseRestRepository /// /// Injected Logger Provider. /// Injected Httpclient factory. + /// Injected app-wide json options. protected PimsBaseRepository( ILogger logger, - IHttpClientFactory httpClientFactory) - : base(logger, httpClientFactory) + IHttpClientFactory httpClientFactory, + IOptions jsonOptions) + : base(logger, httpClientFactory, jsonOptions) { } diff --git a/source/backend/scheduler/Repositories/PimsDocumentQueueRepository.cs b/source/backend/scheduler/Repositories/PimsDocumentQueueRepository.cs new file mode 100644 index 0000000000..f78ef001f5 --- /dev/null +++ b/source/backend/scheduler/Repositories/PimsDocumentQueueRepository.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Pims.Api.Models.Concepts.Document; +using Pims.Api.Models.Requests.Http; +using Pims.Core.Extensions; +using Pims.Core.Http; +using Pims.Dal.Entities.Models; +using Pims.Scheduler.Http.Configuration; + +namespace Pims.Scheduler.Repositories +{ + /// + /// PimsDocumentQueueRepository provides document access from the PIMS document queue api. + /// + public class PimsDocumentQueueRepository : PimsBaseRepository, IPimsDocumentQueueRepository + { + private static readonly JsonSerializerOptions SerializerOptions = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; + + private readonly IOpenIdConnectRequestClient _authRepository; + private readonly IOptionsMonitor _configuration; + + /// + /// Initializes a new instance of the class. + /// + /// Injected Logger Provider. + /// Injected Httpclient factory. + /// Injected repository that handles authentication. + /// The injected configuration provider. + /// The injected json options. + public PimsDocumentQueueRepository( + ILogger logger, + IHttpClientFactory httpClientFactory, + IOpenIdConnectRequestClient authRepository, + IOptionsMonitor configuration, + IOptions jsonOptions) + : base(logger, httpClientFactory, jsonOptions) + { + _authRepository = authRepository; + _configuration = configuration; + } + + /// + /// Polls the upload status in mayan of a queued document using the provided document model. + /// + /// The document to poll. + /// A task that represents the asynchronous operation. The result is an external response containing the document queue model and status information. + public async Task> PollQueuedDocument(DocumentQueueModel document) + { + _logger.LogDebug("polling queued document with id {documentId}", document.Id); + + string authenticationToken = await _authRepository.RequestAccessToken(); + + Uri endpoint = new($"{_configuration.CurrentValue.Uri}/documents/queue/{document.Id}/poll"); + + string serializedFilter = JsonSerializer.Serialize(document, SerializerOptions); + using var content = new StringContent(serializedFilter, Encoding.UTF8, "application/json"); + + var response = await PostAsync(endpoint, content, authenticationToken); + _logger.LogDebug("queued document poll for document with id {documentId} complete with status: {response}", document.Id, response.Serialize()); + + return response; + } + + /// + /// Uploads a queued document to the specified endpoint. + /// + /// The document queue model containing the document details. + /// A task that represents the asynchronous operation and returns an external response containing the status of the upload. + public async Task> UploadQueuedDocument(DocumentQueueModel document) + { + _logger.LogDebug("uploading queued document with id {documentId}", document.Id); + + string authenticationToken = await _authRepository.RequestAccessToken(); + + Uri endpoint = new($"{_configuration.CurrentValue.Uri}/documents/queue/{document.Id}/upload"); + + string serializedFilter = JsonSerializer.Serialize(document, SerializerOptions); + using var content = new StringContent(serializedFilter, Encoding.UTF8, "application/json"); + + var response = await PostAsync(endpoint, content, authenticationToken); + _logger.LogDebug("queued document upload for document with id {documentId} complete with status: {response}", document.Id, response.Serialize()); + + return response; + } + + /// + /// Updates an existing queued document. + /// + /// The ID of the document to update. + /// The updated document details. + /// The result of the update operation. + public async Task> UpdateQueuedDocument(long documentQueueId, DocumentQueueModel document) + { + _logger.LogDebug("updating queued document with id {documentId}", documentQueueId); + + string authenticationToken = await _authRepository.RequestAccessToken(); + + Uri endpoint = new($"{_configuration.CurrentValue.Uri}/documents/queue/{documentQueueId}"); + + string serializedFilter = JsonSerializer.Serialize(document, SerializerOptions); + using var content = new StringContent(serializedFilter, Encoding.UTF8, "application/json"); + + var response = await PutAsync(endpoint, content, authenticationToken); + _logger.LogDebug("queued document update for document with id {documentId} complete with {response}", documentQueueId, response.Serialize()); + + return response; + } + + /// + /// Updates an existing queued document. + /// + /// The ID of the document to update. + /// The result of the update operation. + public async Task> GetById(long documentQueueId) + { + _logger.LogDebug("getting queued document with id {documentId}", documentQueueId); + + string authenticationToken = await _authRepository.RequestAccessToken(); + + Uri endpoint = new($"{_configuration.CurrentValue.Uri}/documents/queue/{documentQueueId}"); + + var response = await GetAsync(endpoint, authenticationToken); + _logger.LogDebug("queued document retrieval for document with id {documentId} complete with {response}", documentQueueId, response.Serialize()); + + return response; + } + + /// + /// Searches for queued documents based on the provided filter. + /// + /// The filter to apply to the search. + /// A task that represents the asynchronous operation, returning a list of document queue models. + public async Task>> SearchQueuedDocumentsAsync(DocumentQueueFilter filter) + { + _logger.LogDebug("Getting filtered list of queued documents by {filter}", filter); + + string authenticationToken = await _authRepository.RequestAccessToken(); + + Uri endpoint = new($"{_configuration.CurrentValue.Uri}/documents/queue/search"); + + string serializedFilter = JsonSerializer.Serialize(filter, SerializerOptions); + using var content = new StringContent(serializedFilter, Encoding.UTF8, "application/json"); + + var response = await PostAsync>(endpoint, content, authenticationToken); + _logger.LogDebug("Retrieved list of queued documents based on {filter}, {response} ", filter.Serialize(), response.Serialize()); + + return response; + } + } +} diff --git a/source/backend/scheduler/Repositories/PimsRepository.cs b/source/backend/scheduler/Repositories/PimsRepository.cs deleted file mode 100644 index f6b2b061c6..0000000000 --- a/source/backend/scheduler/Repositories/PimsRepository.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Pims.Api.Models.Concepts.Document; -using Pims.Api.Models.Requests.Http; -using Pims.Core.Http; -using Pims.Dal.Entities.Models; -using Pims.Scheduler.Http.Configuration; - -namespace Pims.Scheduler.Repositories.Pims -{ - /// - /// PimsDocumentQueueRepository provides document access from the PIMS document queue api. - /// - public class PimsDocumentQueueRepository : PimsBaseRepository, IPimsDocumentQueueRepository - { - private readonly IOpenIdConnectRequestClient _authRepository; - private readonly IOptionsMonitor _configuration; - - /// - /// Initializes a new instance of the class. - /// - /// Injected Logger Provider. - /// Injected Httpclient factory. - /// Injected repository that handles authentication. - /// The injected configuration provider. - public PimsDocumentQueueRepository( - ILogger logger, - IHttpClientFactory httpClientFactory, - IOpenIdConnectRequestClient authRepository, - IOptionsMonitor configuration) - : base(logger, httpClientFactory) - { - _authRepository = authRepository; - _configuration = configuration; - } - - public async Task>> SearchQueuedDocumentsAsync(DocumentQueueFilter filter) - { - _logger.LogDebug("Getting filtered list of queued documents by {filter}", filter); - - string authenticationToken = await _authRepository.RequestAccessToken(); - - Uri endpoint = new($"{_configuration.CurrentValue.Uri}/documents/queue/search"); - - var response = await GetAsync>(endpoint, authenticationToken); - _logger.LogDebug($"Retrieved list of queued documents based on {filter} ", filter); - - return response; - } - } -} diff --git a/source/backend/scheduler/Scheduler/JobRescheduler.cs b/source/backend/scheduler/Scheduler/JobRescheduler.cs index 791942711c..03e480f4ec 100644 --- a/source/backend/scheduler/Scheduler/JobRescheduler.cs +++ b/source/backend/scheduler/Scheduler/JobRescheduler.cs @@ -42,16 +42,10 @@ public void LoadSchedules(JobScheduleOptions options) throw new ConfigurationException($"Unable to find TimeZoneInfo : {timezoneId}"); } - var cron = scheduling.Cron; - if (cron == null) - { - throw new ConfigurationException($"Cron is required"); - } - _recurringJobManager.AddOrUpdate( recurringJob.Id, recurringJob.Job, - scheduling.Cron, + scheduling.Cron ?? recurringJob.Cron, new RecurringJobOptions() { TimeZone = timezone }); } } diff --git a/source/backend/scheduler/Services/DocumentQueueService.cs b/source/backend/scheduler/Services/DocumentQueueService.cs index 6e2c2a01c3..5f1bf9e14b 100644 --- a/source/backend/scheduler/Services/DocumentQueueService.cs +++ b/source/backend/scheduler/Services/DocumentQueueService.cs @@ -1,8 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Pims.Api.Models.CodeTypes; +using Pims.Api.Models.Concepts.Document; +using Pims.Api.Models.Requests.Http; using Pims.Core.Api.Services; -using Pims.Scheduler.Repositories.Pims; +using Pims.Core.Extensions; +using Pims.Dal.Entities.Models; +using Pims.Scheduler.Http.Configuration; +using Pims.Scheduler.Models; +using Pims.Scheduler.Repositories; namespace Pims.Scheduler.Services { @@ -10,20 +20,165 @@ public class DocumentQueueService : BaseService, IDocumentQueueService { private readonly ILogger _logger; private readonly IPimsDocumentQueueRepository _pimsDocumentQueueRepository; + private readonly IOptionsMonitor _uploadQueuedDocumentsJobOptions; + private readonly IOptionsMonitor _queryProcessingDocumentsJobOptions; + private readonly IOptionsMonitor _retryProcessingDocumentsJobOptions; public DocumentQueueService( ILogger logger, + IOptionsMonitor uploadQueuedDocumentsJobOptions, + IOptionsMonitor queryProcessingDocumentsJobOptions, + IOptionsMonitor retryProcessingDocumentsJobOptions, IPimsDocumentQueueRepository pimsDocumentQueueRepository) : base(null, logger) { _logger = logger; _pimsDocumentQueueRepository = pimsDocumentQueueRepository; + _uploadQueuedDocumentsJobOptions = uploadQueuedDocumentsJobOptions; + _queryProcessingDocumentsJobOptions = queryProcessingDocumentsJobOptions; + _retryProcessingDocumentsJobOptions = retryProcessingDocumentsJobOptions; } - public async Task UploadQueuedDocuments() + public async Task UploadQueuedDocuments() { - var queuedDocuments = await _pimsDocumentQueueRepository.SearchQueuedDocumentsAsync(new Dal.Entities.Models.DocumentQueueFilter() { Quantity = 50, DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.PENDING.ToString() }); - _logger.LogInformation("retrieved {queuedDocuments} documents", queuedDocuments?.Payload?.Count); + var filter = new DocumentQueueFilter() { Quantity = _uploadQueuedDocumentsJobOptions?.CurrentValue?.BatchSize ?? 50, DocumentQueueStatusTypeCodes = new string[] { DocumentQueueStatusTypes.PENDING.ToString() }, MaxFileSize = _uploadQueuedDocumentsJobOptions?.CurrentValue?.MaxFileSize }; + var searchResponse = await SearchQueuedDocuments(filter); + if (searchResponse?.ScheduledTaskResponseModel != null) + { + return searchResponse?.ScheduledTaskResponseModel; + } + + IEnumerable> responses = searchResponse?.SearchResults?.Payload?.Select(qd => + { + _logger.LogInformation("Uploading Queued document {documentQueueId}", qd.Id); + _logger.LogDebug("document contents {document}", qd.Serialize()); + + return _pimsDocumentQueueRepository.UploadQueuedDocument(qd).ContinueWith(response => HandleDocumentQueueResponse("UploadQueuedDocument", qd, response)); + }); + var results = await Task.WhenAll(responses); + return new ScheduledTaskResponseModel() { Status = GetMergedStatus(results), DocumentQueueResponses = results }; + } + + public async Task RetryQueuedDocuments() + { + var filter = new DocumentQueueFilter() + { + Quantity = _retryProcessingDocumentsJobOptions?.CurrentValue?.BatchSize ?? 50, + DocumentQueueStatusTypeCodes = new string[] { DocumentQueueStatusTypes.PIMS_ERROR.ToString(), DocumentQueueStatusTypes.MAYAN_ERROR.ToString() }, + MaxFileSize = _retryProcessingDocumentsJobOptions?.CurrentValue?.MaxFileSize, + MaxDocProcessRetries = 3, + }; + var searchResponse = await SearchQueuedDocuments(filter); + if (searchResponse?.ScheduledTaskResponseModel != null) + { + return searchResponse?.ScheduledTaskResponseModel; + } + IEnumerable> responses = searchResponse?.SearchResults?.Payload?.Select(qd => + { + _logger.LogInformation("uploading Queued document {documentQueueId}", qd.Id); + _logger.LogDebug("document contents {document}", qd.Serialize()); + return _pimsDocumentQueueRepository.UploadQueuedDocument(qd).ContinueWith(response => HandleDocumentQueueResponse("UploadQueuedDocument", qd, response)); + }); + var results = await Task.WhenAll(responses); + return new ScheduledTaskResponseModel() { Status = GetMergedStatus(results), DocumentQueueResponses = results }; + } + + public async Task QueryProcessingDocuments() + { + var filter = new Dal.Entities.Models.DocumentQueueFilter() + { + Quantity = _queryProcessingDocumentsJobOptions?.CurrentValue?.BatchSize ?? 50, + DocumentQueueStatusTypeCodes = new string[] { DocumentQueueStatusTypes.PROCESSING.ToString() }, + }; + var searchResponse = await SearchQueuedDocuments(filter); + if (searchResponse?.ScheduledTaskResponseModel != null) + { + return searchResponse?.ScheduledTaskResponseModel; + } + + IEnumerable> responses = searchResponse?.SearchResults?.Payload.Select(qd => + { + _logger.LogInformation("Querying for queued document {documentQueueId}", qd.Id); + _logger.LogDebug("document contents {document}", qd.Serialize()); + if (qd.DocumentProcessStartTimestamp.HasValue && DateTime.UtcNow.Subtract(qd.DocumentProcessStartTimestamp.Value).TotalMinutes > _queryProcessingDocumentsJobOptions?.CurrentValue?.MaxProcessingMinutes) + { + _logger.LogError("Document processing for document {documentQueueId} has exceeded maximum processing time of {maxProcessingMinutes}", qd.Id, _queryProcessingDocumentsJobOptions?.CurrentValue?.MaxProcessingMinutes); + qd.DocumentQueueStatusType.Id = DocumentQueueStatusTypes.MAYAN_ERROR.ToString(); + qd.MayanError = "Document processing has exceeded maximum processing time."; + qd.DocumentProcessEndTimestamp = DateTime.UtcNow; + _ = _pimsDocumentQueueRepository.UpdateQueuedDocument(qd.Id, qd).ContinueWith(response => + { + _logger.LogInformation("Received response from PIMS document update for queued document {documentQueueId} status {Status} message: {Message}", qd.Id, response?.Result?.Status, response?.Result?.Message); + }); + return Task.FromResult(new DocumentQueueResponseModel() { DocumentQueueStatus = DocumentQueueStatusTypes.PIMS_ERROR, Message = $"Document processing for document {qd.Id} has exceeded maximum processing time of {_queryProcessingDocumentsJobOptions?.CurrentValue?.MaxProcessingMinutes}" }); + } + else + { + return _pimsDocumentQueueRepository.PollQueuedDocument(qd).ContinueWith(response => HandleDocumentQueueResponse("PollQueuedDocument", qd, response)); + } + }); + var results = await Task.WhenAll(responses); + return new ScheduledTaskResponseModel() { Status = GetMergedStatus(results), DocumentQueueResponses = results }; + } + + private static TaskResponseStatusTypes GetMergedStatus(IEnumerable responses) + { + if (responses.All(r => r.DocumentQueueStatus == DocumentQueueStatusTypes.SUCCESS)) + { + return TaskResponseStatusTypes.SUCCESS; + } + else if (responses.All(r => r.DocumentQueueStatus == DocumentQueueStatusTypes.MAYAN_ERROR || r.DocumentQueueStatus == DocumentQueueStatusTypes.PIMS_ERROR)) + { + return TaskResponseStatusTypes.ERROR; + } + return TaskResponseStatusTypes.PARTIAL; + } + + private async Task SearchQueuedDocuments(DocumentQueueFilter filter) + { + ScheduledTaskResponseModel scheduledTaskResponseModel = null; + var queuedDocuments = await _pimsDocumentQueueRepository.SearchQueuedDocumentsAsync(filter); + + if (queuedDocuments?.Status != ExternalResponseStatus.Success) + { + _logger.LogError("Received error status from pims document queue search service, aborting. {status} {message}", queuedDocuments?.Status, queuedDocuments?.Message); + scheduledTaskResponseModel = new ScheduledTaskResponseModel() { Status = TaskResponseStatusTypes.ERROR, Message = "Received error status from pims document queue service, aborting." }; + } + if (queuedDocuments?.Payload?.Count == 0) + { + _logger.LogInformation("No documents to process, skipping execution."); + scheduledTaskResponseModel = new ScheduledTaskResponseModel() { Status = TaskResponseStatusTypes.SKIPPED, Message = "No documents to process, skipping execution." }; + } + return new SearchQueuedDocumentsResponseModel() { ScheduledTaskResponseModel = scheduledTaskResponseModel, SearchResults = queuedDocuments }; + } + + private DocumentQueueResponseModel HandleDocumentQueueResponse(string httpMethodName, DocumentQueueModel qd, Task> response) + { + var responseObject = response?.Result; + if (responseObject?.Status == ExternalResponseStatus.Success && (responseObject?.Payload?.DocumentQueueStatusType?.Id == DocumentQueueStatusTypes.PROCESSING.ToString() || responseObject?.Payload?.DocumentQueueStatusType?.Id == DocumentQueueStatusTypes.SUCCESS.ToString())) + { + _logger.LogInformation("Received response from {httpMethodName} for queued document {documentQueueId} status {Status} message: {Message}", httpMethodName, qd?.Id, response?.Result?.Status, response?.Result?.Message); + return new DocumentQueueResponseModel() { DocumentQueueStatus = DocumentQueueStatusTypes.SUCCESS }; + } + else if (responseObject?.Payload?.DocumentQueueStatusType?.Id != DocumentQueueStatusTypes.PIMS_ERROR.ToString() && responseObject?.Payload?.DocumentQueueStatusType?.Id != DocumentQueueStatusTypes.MAYAN_ERROR.ToString()) + { + // If the poll failed, but the document is not in an error state, update the document to an error state. + _pimsDocumentQueueRepository.GetById(qd.Id).ContinueWith(currentDocumentResponse => + { + if (currentDocumentResponse?.Result == null || currentDocumentResponse?.Result.Status != ExternalResponseStatus.Success || currentDocumentResponse?.Result?.Payload == null) + { + _logger.LogError("Document {documentQueueId} not found in database. {response}", qd.Id, currentDocumentResponse.Serialize()); + throw new InvalidOperationException($"Document {qd.Id} not found in database."); + } + var currentDocumentQueue = currentDocumentResponse.Result.Payload; + _logger.LogError("Received error response from {httpMethodName} for queued document {documentQueueId} status {Status} message: {Message}", httpMethodName, currentDocumentQueue?.Id, response?.Result?.Status, response?.Result?.Message); + currentDocumentQueue.DocumentQueueStatusType.Id = DocumentQueueStatusTypes.PIMS_ERROR.ToString(); + currentDocumentQueue.MayanError = $"Document {httpMethodName} failed: {response?.Result?.Message}".Truncate(4000); + _ = _pimsDocumentQueueRepository.UpdateQueuedDocument(currentDocumentQueue.Id, currentDocumentQueue); + }); + return new DocumentQueueResponseModel() { DocumentQueueStatus = DocumentQueueStatusTypes.PIMS_ERROR, Message = $"Received error response from {httpMethodName} for queued document {qd?.Id} status {response?.Result?.Status} message: {response?.Result?.Message}" }; + } + return new DocumentQueueResponseModel() { DocumentQueueStatus = DocumentQueueStatusTypes.PIMS_ERROR }; } } } diff --git a/source/backend/scheduler/Services/Interfaces/IDocumentQueueService.cs b/source/backend/scheduler/Services/Interfaces/IDocumentQueueService.cs index 8820b3913b..ee6ec824e8 100644 --- a/source/backend/scheduler/Services/Interfaces/IDocumentQueueService.cs +++ b/source/backend/scheduler/Services/Interfaces/IDocumentQueueService.cs @@ -1,9 +1,18 @@ using System.Threading.Tasks; +using Hangfire; +using Pims.Scheduler.Models; namespace Pims.Scheduler.Services { public interface IDocumentQueueService { - public Task UploadQueuedDocuments(); + [DisableConcurrentExecution(timeoutInSeconds: 10 * 30)] + public Task UploadQueuedDocuments(); + + [DisableConcurrentExecution(timeoutInSeconds: 10 * 30)] + public Task RetryQueuedDocuments(); + + [DisableConcurrentExecution(timeoutInSeconds: 10 * 30)] + public Task QueryProcessingDocuments(); } } diff --git a/source/backend/scheduler/Startup.cs b/source/backend/scheduler/Startup.cs index 86b192e7c7..74899aab00 100644 --- a/source/backend/scheduler/Startup.cs +++ b/source/backend/scheduler/Startup.cs @@ -39,7 +39,7 @@ using Pims.Keycloak.Configuration; using Pims.Scheduler.Http.Configuration; using Pims.Scheduler.Policies; -using Pims.Scheduler.Repositories.Pims; +using Pims.Scheduler.Repositories; using Pims.Scheduler.Rescheduler; using Pims.Scheduler.Services; using Prometheus; @@ -110,6 +110,9 @@ public void ConfigureServices(IServiceCollection services) services.Configure(this.Configuration.GetSection("OpenIdConnect")); services.Configure(this.Configuration.GetSection("Keycloak")); services.Configure(this.Configuration.GetSection("Pims:Environment")); + services.Configure(this.Configuration.GetSection("UploadQueuedDocumentsOptions")); + services.Configure(this.Configuration.GetSection("RetryQueuedDocumentsOptions")); + services.Configure(this.Configuration.GetSection("QueryProcessingDocumentsOptions")); services.AddOptions(); services.AddApiVersioning(options => { @@ -319,16 +322,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) ScheduleHangfireJobs(app.ApplicationServices); } - private void ScheduleHangfireJobs(IServiceProvider services) - { - // provide default definition of all jobs. - RecurringJob.AddOrUpdate(nameof(DocumentQueueService.UploadQueuedDocuments), x => x.UploadQueuedDocuments(), Cron.Hourly); - - // override scheduled jobs with configuration. - JobScheduleOptions jobOptions = this.Configuration.GetSection("JobOptions").Get(); - services.GetService().LoadSchedules(jobOptions); - } - /// /// Configures the app to to use content security policies. /// @@ -338,13 +331,25 @@ private static void ConfigureSecureHeaders(IApplicationBuilder app) app.Use( async (context, next) => { - context.Response.Headers.Add("Strict-Transport-Security", "max-age=86400; includeSubDomains"); - context.Response.Headers.Add("X-Content-Type-Options", "nosniff"); - context.Response.Headers.Add("X-XSS-Protection", "1"); - context.Response.Headers.Add("X-Frame-Options", " DENY"); + context.Response.Headers.Append("Strict-Transport-Security", "max-age=86400; includeSubDomains"); + context.Response.Headers.Append("X-Content-Type-Options", "nosniff"); + context.Response.Headers.Append("X-XSS-Protection", "1"); + context.Response.Headers.Append("X-Frame-Options", " DENY"); await next().ConfigureAwait(true); }); } + + private void ScheduleHangfireJobs(IServiceProvider services) + { + // provide default definition of all jobs. + RecurringJob.AddOrUpdate(nameof(DocumentQueueService.UploadQueuedDocuments), x => x.UploadQueuedDocuments(), Cron.Minutely); + RecurringJob.AddOrUpdate(nameof(DocumentQueueService.RetryQueuedDocuments), x => x.RetryQueuedDocuments(), "0 0 * * *"); + RecurringJob.AddOrUpdate(nameof(DocumentQueueService.QueryProcessingDocuments), x => x.QueryProcessingDocuments(), Cron.Minutely); + + // override scheduled jobs with configuration. + JobScheduleOptions jobOptions = this.Configuration.GetSection("JobOptions").Get(); + services.GetService().LoadSchedules(jobOptions); + } #endregion } } diff --git a/source/backend/scheduler/appsettings.json b/source/backend/scheduler/appsettings.json index 8bf86b8d30..df93244f95 100644 --- a/source/backend/scheduler/appsettings.json +++ b/source/backend/scheduler/appsettings.json @@ -47,7 +47,7 @@ }, "Server": { "HeartbeatInterval": "00:00:30", - "Queues": [ "default" ], + "Queues": ["default"], "SchedulePollingInterval": "00:00:15", "ServerCheckInterval": "00:05:00", "ServerName": null, @@ -61,10 +61,32 @@ { "JobId": "UploadQueuedDocuments", "IsEnabled": true, - "Cron": "*/5 * * * *" + "cron": "*/30 * * * * *" + }, + { + "JobId": "QueryProcessingDocuments", + "IsEnabled": true, + "cron": "*/15 * * * * *" + }, + { + "JobId": "RetryProcessingDocuments", + "IsEnabled": true, + "cron": "20 */15 * * * *" } ] }, + "UploadQueuedDocumentsOptions": { + "BatchSize": 15, + "MaxFileSize": 100000000 + }, + "RetryQueuedDocumentsOptions": { + "BatchSize": 15, + "MaxFileSize": 50000000 + }, + "QueryProcessingDocumentsOptions": { + "BatchSize": 15, + "MaxProcessingMinutes": 60 + }, "AllowedHosts": "*", "ContentSecurityPolicy": { "Base": "'none'", diff --git a/source/backend/scheduler/tests/.editorconfig b/source/backend/scheduler/tests/.editorconfig deleted file mode 100644 index 3c1051119d..0000000000 --- a/source/backend/scheduler/tests/.editorconfig +++ /dev/null @@ -1,72 +0,0 @@ -# Editor configuration, see https://editorconfig.org -root = true - -[*] -end_of_line = lf -charset = utf-8 -indent_style = space -insert_final_newline = true -trim_trailing_whitespace = true - -[*.ts] -indent_size = 2 - -[*.md] -max_line_length = off -trim_trailing_whitespace = false - -[*.env] -insert_final_newline = false - -[{Makefile,**.mk}] -# Use tabs for indentation (Makefiles require tabs) -indent_style = tab - -[*.cs] -indent_size = 4 - - -# Test files -# SA1515 Single-line comment should be preceded by blank line -dotnet_diagnostic.SA1515.severity = none -# SA1513: Closing brace should be followed by blank line -dotnet_diagnostic.SA1513.severity = none -# SA1633 The file header is missing or not located at the top of the file -dotnet_diagnostic.SA1633.severity = none -# SA1200 Using directive should appear within a namespace declaration -dotnet_diagnostic.SA1200.severity = none -# SA1124 Do not use regions -dotnet_diagnostic.SA1124.severity = none -# SA1201 A constructor should not follow a property -dotnet_diagnostic.SA1201.severity = none -# SA1309 Field 'X' should not begin with an underscore -dotnet_diagnostic.SA1309.severity = none -# SA1117 The parameters should all be placed on the same line or each parameter should be placed on its own line. -dotnet_diagnostic.SA1117.severity = none - -# -- Set to 'error' before running formatter -# dotnet format --severity error --exclude entities/ef/** --exclude entities/PimsBaseContext.cs -# SA1208: Using directive for X should appear before directive for Y -dotnet_diagnostic.SA1208.severity = warning -# SA1121 Use built-in type alias -dotnet_diagnostic.SA1121.severity = warning -# SA1413 Use trailing comma in multi-line initializers. -dotnet_diagnostic.SA1413.severity = warning -# SA1122 Use string.Empty for empty strings -dotnet_diagnostic.SA1122.severity = warning -# SA1518 Code should not contain blank lines at the end of the file. -dotnet_diagnostic.SA1518.severity = warning -# SA1101 Prefix local calls with this -dotnet_diagnostic.SA1101.severity = warning -# SA1507 Code should not contain multiple blank lines in a row -dotnet_diagnostic.SA1507.severity = warning -# SA1127 Generic type constraints should be on their own line -dotnet_diagnostic.SA1127.severity = warning -# SA1002 Semicolons should be followed by a space. -dotnet_diagnostic.SA1002.severity = warning -# SA1009 Closing parenthesis should not be preceded by a space. -dotnet_diagnostic.SA1009.severity = warning -# SA1508 A closing brace should not be preceded by a blank line -dotnet_diagnostic.SA1508.severity = warning -# SA1005 Single line comment should begin with a space. -dotnet_diagnostic.SA1005.severity = warning \ No newline at end of file diff --git a/source/backend/scheduler/tests/.gitignore b/source/backend/scheduler/tests/.gitignore deleted file mode 100644 index 5923334c0e..0000000000 --- a/source/backend/scheduler/tests/.gitignore +++ /dev/null @@ -1,49 +0,0 @@ -# Environment variables -.env -# Build -.obj - -*.swp -*.*~ -project.lock.json -.DS_Store -*.pyc -nupkg/ - -# IDE - VSCode -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json - -# Rider -.idea - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -build/ -bld/ -[Bb]in/ -[Oo]bj/ -[Oo]ut/ -msbuild.log -msbuild.err -msbuild.wrn - -# Visual Studio 2015 -.vs/ - -# SonarQube -.sonarqube/ diff --git a/source/backend/scheduler/tests/Directory.Build.props b/source/backend/scheduler/tests/Directory.Build.props deleted file mode 100644 index 4357d5e289..0000000000 --- a/source/backend/scheduler/tests/Directory.Build.props +++ /dev/null @@ -1,9 +0,0 @@ - - - net8.0 - 9.0 - - - - - diff --git a/source/backend/tests/core/Entities/DocumentQueueHelper.cs b/source/backend/tests/core/Entities/DocumentQueueHelper.cs new file mode 100644 index 0000000000..f80ee9f85b --- /dev/null +++ b/source/backend/tests/core/Entities/DocumentQueueHelper.cs @@ -0,0 +1,37 @@ +using System; +using Entity = Pims.Dal.Entities; + +namespace Pims.Core.Test +{ + /// + /// EntityHelper static class, provides helper methods to create test entities. + /// + public static partial class EntityHelper + { + /// + /// Create a new instance of a DocumentQueue. + /// + /// the document queue id. + /// the status of the queued document. + /// the source of the queued document. + /// the filled-out test entity. + public static Entity.PimsDocumentQueue CreateDocumentQueue(long id = 1, string status = "Pending", string dataSourceTypeCd = "PIMS") + { + return new Entity.PimsDocumentQueue() + { + DocumentQueueId = id, + DocumentQueueStatusTypeCode = status, + AppCreateTimestamp = DateTime.Now, + AppCreateUserid = "admin", + AppLastUpdateTimestamp = DateTime.Now, + AppLastUpdateUserid = "admin", + DocumentId = id, + DocumentNavigation = CreateDocument(id: id), + Document = new byte[] { 1, 2, 3 }, + DataSourceTypeCode = "PIMS", + DataSourceTypeCodeNavigation = new Entity.PimsDataSourceType() { Id = dataSourceTypeCd ?? $"PIMS-{id}", DbCreateUserid = "test", DbLastUpdateUserid = "test", Description = "desc" }, + DocumentQueueStatusTypeCodeNavigation = new Entity.PimsDocumentQueueStatusType() { Id = status ?? $"PENDING-{id}", DbCreateUserid = "test", DbLastUpdateUserid = "test", Description = "desc" }, + }; + } + } +} diff --git a/source/backend/tests/unit/api/Services/DocumentFileServiceTest.cs b/source/backend/tests/unit/api/Services/DocumentFileServiceTest.cs index da9d935eb4..245f44f232 100644 --- a/source/backend/tests/unit/api/Services/DocumentFileServiceTest.cs +++ b/source/backend/tests/unit/api/Services/DocumentFileServiceTest.cs @@ -12,12 +12,14 @@ using Pims.Dal.Repositories; using Pims.Core.Security; using Xunit; - using Pims.Api.Models.Concepts.Document; using Pims.Api.Models.Requests.Document.Upload; using Pims.Api.Models.Requests.Http; using Pims.Api.Models.CodeTypes; using Pims.Core.Exceptions; +using System.ComponentModel; +using Microsoft.EntityFrameworkCore.Storage; +using System.Data.Common; namespace Pims.Api.Test.Services { @@ -194,7 +196,7 @@ public void GetFileDocuments_Project_NotAuthorized() } [Fact] - public void UploadDocumentAsync_Research_ShouldThrowException_NotAuthorized() + public void UploadDocument_Research_ShouldThrowException_NotAuthorized() { // Arrange var service = this.CreateDocumentFileServiceWithPermissions(); @@ -203,15 +205,15 @@ public void UploadDocumentAsync_Research_ShouldThrowException_NotAuthorized() DocumentUploadRequest uploadRequest = new() { DocumentTypeId = 1, File = this._helper.GetFormFile("Lorem Ipsum") }; // Assert - Func sut = async () => await service.UploadResearchDocumentAsync(1, uploadRequest); + Func sut = async () => await service.UploadResearchDocument(1, uploadRequest); // Assert sut.Should().ThrowAsync(); - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Never); + documentService.Verify(x => x.UploadDocumentAsync(It.IsAny(), false), Times.Never); } [Fact] - public void UploadDocumentAsync_Acquisition_ShouldThrowException_NotAuthorized() + public void UploadDocument_Acquisition_ShouldThrowException_NotAuthorized() { // Arrange var service = this.CreateDocumentFileServiceWithPermissions(); @@ -220,15 +222,15 @@ public void UploadDocumentAsync_Acquisition_ShouldThrowException_NotAuthorized() DocumentUploadRequest uploadRequest = new() { DocumentTypeId = 1, File = this._helper.GetFormFile("Lorem Ipsum") }; // Assert - Func sut = async () => await service.UploadResearchDocumentAsync(1, uploadRequest); + Func sut = async () => await service.UploadAcquisitionDocument(1, uploadRequest); // Assert sut.Should().ThrowAsync(); - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Never); + documentService.Verify(x => x.UploadDocumentAsync(It.IsAny(), false), Times.Never); } [Fact] - public void UploadDocumentAsync_Project_ShouldThrowException_NotAuthorized() + public void UploadDocument_Project_ShouldThrowException_NotAuthorized() { // Arrange var service = this.CreateDocumentFileServiceWithPermissions(); @@ -237,15 +239,15 @@ public void UploadDocumentAsync_Project_ShouldThrowException_NotAuthorized() DocumentUploadRequest uploadRequest = new() { DocumentTypeId = 1, File = this._helper.GetFormFile("Lorem Ipsum") }; // Assert - Func sut = async () => await service.UploadProjectDocumentAsync(1, uploadRequest); + Func sut = async () => await service.UploadProjectDocument(1, uploadRequest); // Assert sut.Should().ThrowAsync(); - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Never); + documentService.Verify(x => x.UploadDocumentAsync(It.IsAny(), false), Times.Never); } [Fact] - public void UploadDocumentAsync_Lease_ShouldThrowException_NotAuthorized() + public void UploadDocument_Lease_ShouldThrowException_NotAuthorized() { // Arrange var service = this.CreateDocumentFileServiceWithPermissions(); @@ -254,15 +256,15 @@ public void UploadDocumentAsync_Lease_ShouldThrowException_NotAuthorized() DocumentUploadRequest uploadRequest = new() { DocumentTypeId = 1, File = this._helper.GetFormFile("Lorem Ipsum") }; // Assert - Func sut = async () => await service.UploadLeaseDocumentAsync(1, uploadRequest); + Func sut = async () => await service.UploadLeaseDocument(1, uploadRequest); // Assert sut.Should().ThrowAsync(); - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Never); + documentService.Verify(x => x.UploadDocumentAsync(It.IsAny(), false), Times.Never); } [Fact] - public void UploadDocumentAsync_PropertyActivity_ShouldThrowException_NotAuthorized() + public void UploadDocument_PropertyActivity_ShouldThrowException_NotAuthorized() { // Arrange var service = this.CreateDocumentFileServiceWithPermissions(); @@ -271,15 +273,15 @@ public void UploadDocumentAsync_PropertyActivity_ShouldThrowException_NotAuthori DocumentUploadRequest uploadRequest = new() { DocumentTypeId = 1, File = this._helper.GetFormFile("Lorem Ipsum") }; // Assert - Func sut = async () => await service.UploadPropertyActivityDocumentAsync(1, uploadRequest); + Func sut = async () => await service.UploadPropertyActivityDocument(1, uploadRequest); // Assert sut.Should().ThrowAsync(); - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Never); + documentService.Verify(x => x.UploadDocumentAsync(It.IsAny(), false), Times.Never); } [Fact] - public void UploadDocumentAsync_Disposition_ShouldThrowException_NotAuthorized() + public void UploadDocument_Disposition_ShouldThrowException_NotAuthorized() { // Arrange var service = this.CreateDocumentFileServiceWithPermissions(); @@ -288,29 +290,44 @@ public void UploadDocumentAsync_Disposition_ShouldThrowException_NotAuthorized() DocumentUploadRequest uploadRequest = new() { DocumentTypeId = 1, File = this._helper.GetFormFile("Lorem Ipsum") }; // Assert - Func action = async () => await service.UploadDispositionDocumentAsync(1, uploadRequest); + Func action = async () => await service.UploadDispositionDocument(1, uploadRequest); // Assert action.Should().ThrowExactlyAsync(); - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Never); + documentService.Verify(x => x.UploadDocumentAsync(It.IsAny(), false), Times.Never); } [Fact] - public async void UploadDocumentAsync_Project_Success() + public async void UploadDocument_Project_Success() { // Arrange var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.ProjectEdit); - var documentService = this._helper.GetService>(); + var documentRepository = this._helper.GetService>(); + var documentQueueRepository = this._helper.GetService>(); var projectRepository = this._helper.GetService>(); - documentService.Setup(x => x.UploadDocumentAsync(It.IsAny())) - .ReturnsAsync(new DocumentUploadResponse() - { - Document = new DocumentModel() - { - Id = 1, - }, - }); + documentQueueRepository.Setup(x => x.BeginTransaction()).Returns(new Mock().Object); + documentRepository.Setup(x => x.Add(It.IsAny())).Returns(new PimsDocument() + { + DocumentId = 100, + DocumentTypeId = 4, + MayanId = null, + FileName = "NewFile.docx", + }); + + projectRepository.Setup(x => x.AddProjectDocument(It.IsAny())).Returns(new PimsProjectDocument() + { + ProjectDocumentId = 101, + ProjectId = 1, + DocumentId = 100, + }); + + documentQueueRepository.Setup(x => x.Add(It.IsAny())).Returns(new PimsDocumentQueue() + { + DocumentId = 100, + Document = new byte[1] { 1 }, + DocumentMetadata = null, + }); // Act DocumentUploadRequest uploadRequest = new() @@ -321,21 +338,22 @@ public async void UploadDocumentAsync_Project_Success() DocumentStatusCode = "DocumentStatus", }; - await service.UploadProjectDocumentAsync(1, uploadRequest); + await service.UploadProjectDocument(1, uploadRequest); // Assert - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Once); + documentRepository.Verify(x => x.Add(It.IsAny()), Times.Once); projectRepository.Verify(x => x.AddProjectDocument(It.IsAny()), Times.Once); + documentQueueRepository.Verify(x => x.Add(It.IsAny()), Times.Once); } [Fact] - public async void UploadDocumentAsync_Project_Fail_EmptyFile() + public async void UploadDocument_Project_Fail_EmptyFile() { // Arrange var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.ProjectEdit); var documentService = this._helper.GetService>(); var projectRepository = this._helper.GetService>(); - documentService.Setup(x => x.UploadDocumentAsync(It.IsAny())); + documentService.Setup(x => x.UploadDocumentAsync(It.IsAny(), false)); // Act DocumentUploadRequest uploadRequest = new() @@ -346,67 +364,47 @@ public async void UploadDocumentAsync_Project_Fail_EmptyFile() DocumentStatusCode = "DocumentStatus", }; - Func act = async () => await service.UploadProjectDocumentAsync(1, uploadRequest); + Func act = async () => await service.UploadProjectDocument(1, uploadRequest); // Assert var ex = await act.Should().ThrowAsync(); ex.Which.Message.Should().Be("The submitted file is empty"); - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Never); + documentService.Verify(x => x.UploadDocumentAsync(It.IsAny(), false), Times.Never); projectRepository.Verify(x => x.AddProjectDocument(It.IsAny()), Times.Never); } [Fact] - public async void UploadDocumentAsync_Project_Fail_GenericError() + public async void UploadDocument_Acquisition_Success() { // Arrange - var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.ProjectEdit); - var documentService = this._helper.GetService>(); - var projectRepository = this._helper.GetService>(); - - documentService.Setup(x => x.UploadDocumentAsync(It.IsAny())) - .ReturnsAsync(new DocumentUploadResponse() - { - Document = null, - DocumentExternalResponse = new() { Message = "Mayan test error", Status = ExternalResponseStatus.Error } - }); + var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.AcquisitionFileEdit); + var documentRepository = this._helper.GetService>(); + var documentQueueRepository = this._helper.GetService>(); + var acquisitionFileDocumentRepository = this._helper.GetService>(); - // Act - DocumentUploadRequest uploadRequest = new() + documentQueueRepository.Setup(x => x.BeginTransaction()).Returns(new Mock().Object); + documentRepository.Setup(x => x.Add(It.IsAny())).Returns(new PimsDocument() { - DocumentTypeMayanId = 3, + DocumentId = 100, DocumentTypeId = 4, - File = this._helper.GetFormFile("Lorem Ipsum"), - DocumentStatusCode = "DocumentStatus", - }; - - Func act = async () => await service.UploadProjectDocumentAsync(1, uploadRequest); - - // Assert - var ex = await act.Should().ThrowAsync(); - ex.Which.Message.Should().Be("Unexpected exception uploading file"); - ex.Which.InnerException.Message.Should().Be("Mayan test error"); - - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Once); - projectRepository.Verify(x => x.AddProjectDocument(It.IsAny()), Times.Never); - } + MayanId = null, + FileName = "NewFile.docx", + }); - [Fact] - public async void UploadDocumentAsync_Acquisition_Success() - { - // Arrange - var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.AcquisitionFileEdit); - var documentService = this._helper.GetService>(); - var acquisitionFileDocumentRepository = this._helper.GetService>(); + acquisitionFileDocumentRepository.Setup(x => x.AddAcquisition(It.IsAny())).Returns(new PimsAcquisitionFileDocument() + { + AcquisitionFileDocumentId = 101, + AcquisitionFileId = 1, + DocumentId = 100, + }); - documentService.Setup(x => x.UploadDocumentAsync(It.IsAny())) - .ReturnsAsync(new DocumentUploadResponse() - { - Document = new DocumentModel() - { - Id = 1, - }, - }); + documentQueueRepository.Setup(x => x.Add(It.IsAny())).Returns(new PimsDocumentQueue() + { + DocumentId = 100, + Document = new byte[1] { 1 }, + DocumentMetadata = null, + }); // Act DocumentUploadRequest uploadRequest = new() @@ -417,21 +415,22 @@ public async void UploadDocumentAsync_Acquisition_Success() DocumentStatusCode = "DocumentStatus", }; - await service.UploadAcquisitionDocumentAsync(1, uploadRequest); + await service.UploadAcquisitionDocument(1, uploadRequest); // Assert - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Once); + documentRepository.Verify(x => x.Add(It.IsAny()), Times.Once); acquisitionFileDocumentRepository.Verify(x => x.AddAcquisition(It.IsAny()), Times.Once); + documentQueueRepository.Verify(x => x.Add(It.IsAny()), Times.Once); } [Fact] - public async void UploadDocumentAsync_Acquisition_Fail_EmptyFile() + public async void UploadDocument_Acquisition_Fail_EmptyFile() { // Arrange var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.AcquisitionFileEdit); var documentService = this._helper.GetService>(); var acquisitionFileDocumentRepository = this._helper.GetService>(); - documentService.Setup(x => x.UploadDocumentAsync(It.IsAny())); + documentService.Setup(x => x.UploadDocumentAsync(It.IsAny(), false)); // Act DocumentUploadRequest uploadRequest = new() @@ -442,67 +441,40 @@ public async void UploadDocumentAsync_Acquisition_Fail_EmptyFile() DocumentStatusCode = "DocumentStatus", }; - Func act = async () => await service.UploadAcquisitionDocumentAsync(1, uploadRequest); + Func act = async () => await service.UploadAcquisitionDocument(1, uploadRequest); // Assert var ex = await act.Should().ThrowAsync(); ex.Which.Message.Should().Be("The submitted file is empty"); - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Never); + documentService.Verify(x => x.UploadDocumentAsync(It.IsAny(), false), Times.Never); acquisitionFileDocumentRepository.Verify(x => x.AddAcquisition(It.IsAny()), Times.Never); } [Fact] - public async void UploadDocumentAsync_Acquisition_Fail_GenericError() + public async void UploadDocument_Research_Success() { // Arrange - var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.AcquisitionFileEdit); - var documentService = this._helper.GetService>(); - var acquisitionFileDocumentRepository = this._helper.GetService>(); - - documentService.Setup(x => x.UploadDocumentAsync(It.IsAny())) - .ReturnsAsync(new DocumentUploadResponse() - { - Document = null, - DocumentExternalResponse = new() { Message = "Mayan test error", Status = ExternalResponseStatus.Error } - }); + var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.ResearchFileEdit); + var documentRepository = this._helper.GetService>(); + var documentQueueRepository = this._helper.GetService>(); + var researchFileDocumentRepository = this._helper.GetService>(); - // Act - DocumentUploadRequest uploadRequest = new() + documentQueueRepository.Setup(x => x.BeginTransaction()).Returns(new Mock().Object); + documentRepository.Setup(x => x.Add(It.IsAny())).Returns(new PimsDocument() { - DocumentTypeMayanId = 3, + DocumentId = 100, DocumentTypeId = 4, - File = this._helper.GetFormFile("Lorem Ipsum"), - DocumentStatusCode = "DocumentStatus", - }; - - Func act = async () => await service.UploadAcquisitionDocumentAsync(1, uploadRequest); - - // Assert - var ex = await act.Should().ThrowAsync(); - ex.Which.Message.Should().Be("Unexpected exception uploading file"); - ex.Which.InnerException.Message.Should().Be("Mayan test error"); - - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Once); - acquisitionFileDocumentRepository.Verify(x => x.AddAcquisition(It.IsAny()), Times.Never); - } - - [Fact] - public async void UploadDocumentAsync_Research_Success() - { - // Arrange - var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.ResearchFileEdit); - var documentService = this._helper.GetService>(); - var researchFileDocumentRepository = this._helper.GetService>(); + MayanId = null, + FileName = "NewFile.docx", + }); - documentService.Setup(x => x.UploadDocumentAsync(It.IsAny())) - .ReturnsAsync(new DocumentUploadResponse() - { - Document = new DocumentModel() - { - Id = 1, - }, - }); + researchFileDocumentRepository.Setup(x => x.AddResearch(It.IsAny())).Returns(new PimsResearchFileDocument() + { + ResearchFileDocumentId = 101, + ResearchFileId = 1, + DocumentId = 100, + }); // Act DocumentUploadRequest uploadRequest = new() @@ -513,21 +485,22 @@ public async void UploadDocumentAsync_Research_Success() DocumentStatusCode = "DocumentStatus", }; - await service.UploadResearchDocumentAsync(1, uploadRequest); + await service.UploadResearchDocument(1, uploadRequest); // Assert - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Once); + documentRepository.Verify(x => x.Add(It.IsAny()), Times.Once); researchFileDocumentRepository.Verify(x => x.AddResearch(It.IsAny()), Times.Once); + documentQueueRepository.Verify(x => x.Add(It.IsAny()), Times.Once); } [Fact] - public async void UploadDocumentAsync_Research_Fail_EmptyFile() + public async void UploadDocument_Research_Fail_EmptyFile() { // Arrange var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.ResearchFileEdit); var documentService = this._helper.GetService>(); var researchFileDocumentRepository = this._helper.GetService>(); - documentService.Setup(x => x.UploadDocumentAsync(It.IsAny())); + documentService.Setup(x => x.UploadDocumentAsync(It.IsAny(), false)); // Act DocumentUploadRequest uploadRequest = new() @@ -538,67 +511,40 @@ public async void UploadDocumentAsync_Research_Fail_EmptyFile() DocumentStatusCode = "DocumentStatus", }; - Func act = async () => await service.UploadResearchDocumentAsync(1, uploadRequest); + Func act = async () => await service.UploadResearchDocument(1, uploadRequest); // Assert var ex = await act.Should().ThrowAsync(); ex.Which.Message.Should().Be("The submitted file is empty"); - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Never); + documentService.Verify(x => x.UploadDocumentAsync(It.IsAny(), false), Times.Never); researchFileDocumentRepository.Verify(x => x.AddResearch(It.IsAny()), Times.Never); } [Fact] - public async void UploadDocumentAsync_Research_Fail_GenericError() + public async void UploadDocument_Lease_Success() { // Arrange - var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.ResearchFileEdit); - var documentService = this._helper.GetService>(); - var researchFileDocumentRepository = this._helper.GetService>(); - - documentService.Setup(x => x.UploadDocumentAsync(It.IsAny())) - .ReturnsAsync(new DocumentUploadResponse() - { - Document = null, - DocumentExternalResponse = new() { Message = "Mayan test error", Status = ExternalResponseStatus.Error } - }); + var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.LeaseEdit); + var documentRepository = this._helper.GetService>(); + var documentQueueRepository = this._helper.GetService>(); + var leaseRepository = this._helper.GetService>(); - // Act - DocumentUploadRequest uploadRequest = new() + documentQueueRepository.Setup(x => x.BeginTransaction()).Returns(new Mock().Object); + documentRepository.Setup(x => x.Add(It.IsAny())).Returns(new PimsDocument() { - DocumentTypeMayanId = 3, + DocumentId = 100, DocumentTypeId = 4, - File = this._helper.GetFormFile("Lorem Ipsum"), - DocumentStatusCode = "DocumentStatus", - }; - - Func act = async () => await service.UploadResearchDocumentAsync(1, uploadRequest); - - // Assert - var ex = await act.Should().ThrowAsync(); - ex.Which.Message.Should().Be("Unexpected exception uploading file"); - ex.Which.InnerException.Message.Should().Be("Mayan test error"); - - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Once); - researchFileDocumentRepository.Verify(x => x.AddResearch(It.IsAny()), Times.Never); - } - - [Fact] - public async void UploadDocumentAsync_Lease_Success() - { - // Arrange - var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.LeaseEdit); - var documentService = this._helper.GetService>(); - var leaseRepository = this._helper.GetService>(); + MayanId = null, + FileName = "NewFile.docx", + }); - documentService.Setup(x => x.UploadDocumentAsync(It.IsAny())) - .ReturnsAsync(new DocumentUploadResponse() - { - Document = new DocumentModel() - { - Id = 1, - }, - }); + leaseRepository.Setup(x => x.AddLeaseDocument(It.IsAny())).Returns(new PimsLeaseDocument() + { + LeaseDocumentId = 101, + LeaseId = 1, + DocumentId = 100, + }); // Act DocumentUploadRequest uploadRequest = new() @@ -609,21 +555,22 @@ public async void UploadDocumentAsync_Lease_Success() DocumentStatusCode = "DocumentStatus", }; - await service.UploadLeaseDocumentAsync(1, uploadRequest); + await service.UploadLeaseDocument(1, uploadRequest); // Assert - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Once); + documentRepository.Verify(x => x.Add(It.IsAny()), Times.Once); leaseRepository.Verify(x => x.AddLeaseDocument(It.IsAny()), Times.Once); + documentQueueRepository.Verify(x => x.Add(It.IsAny()), Times.Once); } [Fact] - public async void UploadDocumentAsync_Lease_Fail_EmptyFile() + public async void UploadDocument_Lease_Fail_EmptyFile() { // Arrange var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.LeaseEdit); var documentService = this._helper.GetService>(); var leaseRepository = this._helper.GetService>(); - documentService.Setup(x => x.UploadDocumentAsync(It.IsAny())); + documentService.Setup(x => x.UploadDocumentAsync(It.IsAny(), false)); // Act DocumentUploadRequest uploadRequest = new() @@ -634,67 +581,40 @@ public async void UploadDocumentAsync_Lease_Fail_EmptyFile() DocumentStatusCode = "DocumentStatus", }; - Func act = async () => await service.UploadLeaseDocumentAsync(1, uploadRequest); + Func act = async () => await service.UploadLeaseDocument(1, uploadRequest); // Assert var ex = await act.Should().ThrowAsync(); ex.Which.Message.Should().Be("The submitted file is empty"); - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Never); + documentService.Verify(x => x.UploadDocumentAsync(It.IsAny(), false), Times.Never); leaseRepository.Verify(x => x.AddLeaseDocument(It.IsAny()), Times.Never); } [Fact] - public async void UploadDocumentAsync_Lease_Fail_GenericError() + public async void UploadDocument_PropertyActivity_Success() { // Arrange - var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.LeaseEdit); - var documentService = this._helper.GetService>(); - var leaseRepository = this._helper.GetService>(); - - documentService.Setup(x => x.UploadDocumentAsync(It.IsAny())) - .ReturnsAsync(new DocumentUploadResponse() - { - Document = null, - DocumentExternalResponse = new() { Message = "Mayan test error", Status = ExternalResponseStatus.Error } - }); + var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.ManagementEdit); + var documentRepository = this._helper.GetService>(); + var documentQueueRepository = this._helper.GetService>(); + var propertyActivityDocumentRepository = this._helper.GetService>(); - // Act - DocumentUploadRequest uploadRequest = new() + documentQueueRepository.Setup(x => x.BeginTransaction()).Returns(new Mock().Object); + documentRepository.Setup(x => x.Add(It.IsAny())).Returns(new PimsDocument() { - DocumentTypeMayanId = 3, + DocumentId = 100, DocumentTypeId = 4, - File = this._helper.GetFormFile("Lorem Ipsum"), - DocumentStatusCode = "DocumentStatus", - }; - - Func act = async () => await service.UploadLeaseDocumentAsync(1, uploadRequest); - - // Assert - var ex = await act.Should().ThrowAsync(); - ex.Which.Message.Should().Be("Unexpected exception uploading file"); - ex.Which.InnerException.Message.Should().Be("Mayan test error"); - - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Once); - leaseRepository.Verify(x => x.AddLeaseDocument(It.IsAny()), Times.Never); - } - - [Fact] - public async void UploadDocumentAsync_PropertyActivity_Success() - { - // Arrange - var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.ManagementEdit); - var documentService = this._helper.GetService>(); - var propertyActivityDocumentRepository = this._helper.GetService>(); + MayanId = null, + FileName = "NewFile.docx", + }); - documentService.Setup(x => x.UploadDocumentAsync(It.IsAny())) - .ReturnsAsync(new DocumentUploadResponse() - { - Document = new DocumentModel() - { - Id = 1, - }, - }); + propertyActivityDocumentRepository.Setup(x => x.AddPropertyActivityDocument(It.IsAny())).Returns(new PimsPropertyActivityDocument() + { + PropertyActivityDocumentId = 101, + PimsPropertyActivityId = 1, + DocumentId = 100, + }); // Act DocumentUploadRequest uploadRequest = new() @@ -705,21 +625,22 @@ public async void UploadDocumentAsync_PropertyActivity_Success() DocumentStatusCode = "DocumentStatus", }; - await service.UploadPropertyActivityDocumentAsync(1, uploadRequest); + await service.UploadPropertyActivityDocument(1, uploadRequest); // Assert - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Once); + documentRepository.Verify(x => x.Add(It.IsAny()), Times.Once); propertyActivityDocumentRepository.Verify(x => x.AddPropertyActivityDocument(It.IsAny()), Times.Once); + documentQueueRepository.Verify(x => x.Add(It.IsAny()), Times.Once); } [Fact] - public async void UploadDocumentAsync_PropertyActivity_Fail_EmptyFile() + public async void UploadDocument_PropertyActivity_Fail_EmptyFile() { // Arrange var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.ManagementEdit); var documentService = this._helper.GetService>(); var propertyActivityDocumentRepository = this._helper.GetService>(); - documentService.Setup(x => x.UploadDocumentAsync(It.IsAny())); + documentService.Setup(x => x.UploadDocumentAsync(It.IsAny(), false)); // Act DocumentUploadRequest uploadRequest = new() @@ -730,67 +651,40 @@ public async void UploadDocumentAsync_PropertyActivity_Fail_EmptyFile() DocumentStatusCode = "DocumentStatus", }; - Func act = async () => await service.UploadPropertyActivityDocumentAsync(1, uploadRequest); + Func act = async () => await service.UploadPropertyActivityDocument(1, uploadRequest); // Assert var ex = await act.Should().ThrowAsync(); ex.Which.Message.Should().Be("The submitted file is empty"); - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Never); + documentService.Verify(x => x.UploadDocumentAsync(It.IsAny(), false), Times.Never); propertyActivityDocumentRepository.Verify(x => x.AddPropertyActivityDocument(It.IsAny()), Times.Never); } [Fact] - public async void UploadDocumentAsync_PropertyActivity_Fail_GenericError() + public async void UploadDocument_Disposition_Success() { // Arrange - var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.ManagementEdit); - var documentService = this._helper.GetService>(); - var propertyActivityDocumentRepository = this._helper.GetService>(); - - documentService.Setup(x => x.UploadDocumentAsync(It.IsAny())) - .ReturnsAsync(new DocumentUploadResponse() - { - Document = null, - DocumentExternalResponse = new() { Message = "Mayan test error", Status = ExternalResponseStatus.Error } - }); + var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.DispositionEdit); + var documentRepository = this._helper.GetService>(); + var documentQueueRepository = this._helper.GetService>(); + var dispositionFileDocumentRepository = this._helper.GetService>(); - // Act - DocumentUploadRequest uploadRequest = new() + documentQueueRepository.Setup(x => x.BeginTransaction()).Returns(new Mock().Object); + documentRepository.Setup(x => x.Add(It.IsAny())).Returns(new PimsDocument() { - DocumentTypeMayanId = 3, + DocumentId = 100, DocumentTypeId = 4, - File = this._helper.GetFormFile("Lorem Ipsum"), - DocumentStatusCode = "DocumentStatus", - }; - - Func act = async () => await service.UploadPropertyActivityDocumentAsync(1, uploadRequest); - - // Assert - var ex = await act.Should().ThrowAsync(); - ex.Which.Message.Should().Be("Unexpected exception uploading file"); - ex.Which.InnerException.Message.Should().Be("Mayan test error"); - - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Once); - propertyActivityDocumentRepository.Verify(x => x.AddPropertyActivityDocument(It.IsAny()), Times.Never); - } - - [Fact] - public async void UploadDocumentAsync_Disposition_Success() - { - // Arrange - var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.DispositionEdit); - var documentService = this._helper.GetService>(); - var dispositionFileDocumentRepository = this._helper.GetService>(); + MayanId = null, + FileName = "NewFile.docx", + }); - documentService.Setup(x => x.UploadDocumentAsync(It.IsAny())) - .ReturnsAsync(new DocumentUploadResponse() - { - Document = new DocumentModel() - { - Id = 1, - }, - }); + dispositionFileDocumentRepository.Setup(x => x.AddDispositionDocument(It.IsAny())).Returns(new PimsDispositionFileDocument() + { + DispositionFileDocumentId = 101, + DispositionFileId = 1, + DocumentId = 100, + }); dispositionFileDocumentRepository.Setup(x => x.AddDispositionDocument(It.IsAny())) .Returns(new PimsDispositionFileDocument() { DispositionFileId = 100, DocumentId = 1 }); @@ -804,14 +698,12 @@ public async void UploadDocumentAsync_Disposition_Success() DocumentStatusCode = "DocumentStatus", }; - var result = await service.UploadDispositionDocumentAsync(100, uploadRequest); + await service.UploadDispositionDocument(100, uploadRequest); // Assert - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Once); + documentRepository.Verify(x => x.Add(It.IsAny()), Times.Once); dispositionFileDocumentRepository.Verify(x => x.AddDispositionDocument(It.IsAny()), Times.Once); - result.UploadResponse.Document.Id.Should().Be(1); - result.DocumentRelationship.ParentId.Should().Be("100"); - result.DocumentRelationship.RelationshipType.Should().Be(DocumentRelationType.DispositionFiles); + documentQueueRepository.Verify(x => x.Add(It.IsAny()), Times.Once); } [Fact] @@ -821,7 +713,7 @@ public async void UploadDocumentAsync_Disposition_Fail_EmptyFile() var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.DispositionEdit); var documentService = this._helper.GetService>(); var dispositionFileDocumentRepository = this._helper.GetService>(); - documentService.Setup(x => x.UploadDocumentAsync(It.IsAny())); + documentService.Setup(x => x.UploadDocumentAsync(It.IsAny(), false)); // Act DocumentUploadRequest uploadRequest = new() @@ -832,48 +724,13 @@ public async void UploadDocumentAsync_Disposition_Fail_EmptyFile() DocumentStatusCode = "DocumentStatus", }; - Func act = async () => await service.UploadDispositionDocumentAsync(100, uploadRequest); + Func act = async () => await service.UploadDispositionDocument(100, uploadRequest); // Assert var ex = await act.Should().ThrowAsync(); ex.Which.Message.Should().Be("The submitted file is empty"); - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Never); - dispositionFileDocumentRepository.Verify(x => x.AddDispositionDocument(It.IsAny()), Times.Never); - } - - [Fact] - public async void UploadDocumentAsync_Disposition_Fail_GenericError() - { - // Arrange - var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentAdd, Permissions.DispositionEdit); - var documentService = this._helper.GetService>(); - var dispositionFileDocumentRepository = this._helper.GetService>(); - - documentService.Setup(x => x.UploadDocumentAsync(It.IsAny())) - .ReturnsAsync(new DocumentUploadResponse() - { - Document = null, - DocumentExternalResponse = new() { Message = "Mayan test error", Status = ExternalResponseStatus.Error } - }); - - // Act - DocumentUploadRequest uploadRequest = new() - { - DocumentTypeMayanId = 3, - DocumentTypeId = 4, - File = this._helper.GetFormFile("Lorem Ipsum"), - DocumentStatusCode = "DocumentStatus", - }; - - Func act = async () => await service.UploadDispositionDocumentAsync(100, uploadRequest); - - // Assert - var ex = await act.Should().ThrowAsync(); - ex.Which.Message.Should().Be("Unexpected exception uploading file"); - ex.Which.InnerException.Message.Should().Be("Mayan test error"); - - documentService.Verify(x => x.UploadDocumentAsync(It.IsAny()), Times.Once); + documentService.Verify(x => x.UploadDocumentAsync(It.IsAny(), false), Times.Never); dispositionFileDocumentRepository.Verify(x => x.AddDispositionDocument(It.IsAny()), Times.Never); } @@ -905,25 +762,43 @@ public async void DeleteDocumentResearch_Success_Status_Success() var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentDelete, Permissions.ResearchFileEdit); var documentService = this._helper.GetService>(); var documentRepository = this._helper.GetService>(); + var documentQueueRepository = this._helper.GetService>(); + var researchDocumentRepository = this._helper.GetService>(); - documentRepository.Setup(x => x.DocumentRelationshipCount(It.IsAny())).Returns(1); - documentService.Setup(x => x.DeleteDocumentAsync(It.IsAny())) - .ReturnsAsync(new ExternalResponse() - { - Status = ExternalResponseStatus.Success, - }); + documentRepository.Setup(x => x.Find(It.IsAny())).Returns(new PimsDocument() { DocumentId = 2, MayanId = 200 }); + documentRepository.Setup(x => x.BeginTransaction()).Returns(new Mock().Object); + documentService.Setup(x => x.DeleteMayanStorageDocumentAsync(It.IsAny())).ReturnsAsync(new ExternalResponse() + { + Status = ExternalResponseStatus.Success, + HttpStatusCode = System.Net.HttpStatusCode.OK, + }); + documentRepository.Setup(x => x.Update(It.IsAny(), false)).Returns(new PimsDocument() { DocumentId = 2, MayanId = null }); + documentQueueRepository.Setup(x => x.GetByDocumentId(It.IsAny())).Returns(new PimsDocumentQueue() + { + DocumentId = 2, + DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.SUCCESS.ToString(), + }); + documentQueueRepository.Setup(x => x.Delete(It.IsAny())).Returns(true); + researchDocumentRepository.Setup(x => x.DeleteResearch(It.IsAny())).Returns(true); + documentRepository.Setup(x => x.DeleteDocument(It.IsAny())).Returns(true); PimsResearchFileDocument doc = new() { Internal_Id = 1, DocumentId = 2, + Document = new PimsDocument() + { + DocumentId = 2, + MayanId = 200, + } }; // Act - await service.DeleteResearchDocumentAsync(doc); + var result = await service.DeleteResearchDocumentAsync(doc); // Assert - documentService.Verify(x => x.DeleteDocumentAsync(It.IsAny()), Times.Once); + Assert.NotNull(result); + Assert.Equal(result.Status, ExternalResponseStatus.Success); } [Fact] @@ -933,81 +808,89 @@ public async void DeleteDocumentResearch_Success_Status_NotFound() var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentDelete, Permissions.ResearchFileEdit); var documentService = this._helper.GetService>(); var documentRepository = this._helper.GetService>(); + var documentQueueRepository = this._helper.GetService>(); + var researchDocumentRepository = this._helper.GetService>(); - documentRepository.Setup(x => x.DocumentRelationshipCount(It.IsAny())).Returns(1); - documentService.Setup(x => x.DeleteDocumentAsync(It.IsAny())) - .ReturnsAsync(new ExternalResponse() - { - HttpStatusCode = System.Net.HttpStatusCode.NotFound, - }); - - PimsResearchFileDocument doc = new() + documentRepository.Setup(x => x.Find(It.IsAny())).Returns(new PimsDocument() { DocumentId = 2, MayanId = 200 }); + documentRepository.Setup(x => x.BeginTransaction()).Returns(new Mock().Object); + documentService.Setup(x => x.DeleteMayanStorageDocumentAsync(It.IsAny())).ReturnsAsync(new ExternalResponse() + { + Status = ExternalResponseStatus.Error, + HttpStatusCode = System.Net.HttpStatusCode.NotFound, + }); + documentRepository.Setup(x => x.Update(It.IsAny(), false)).Returns(new PimsDocument() { DocumentId = 2, MayanId = null }); + documentQueueRepository.Setup(x => x.GetByDocumentId(It.IsAny())).Returns(new PimsDocumentQueue() { - Internal_Id = 1, DocumentId = 2, - }; - - // Act - await service.DeleteResearchDocumentAsync(doc); - - // Assert - documentService.Verify(x => x.DeleteDocumentAsync(It.IsAny()), Times.Once); - } - - [Fact] - public async void DeleteDocumentResearch_Success_NoResults_Status_NotFound() - { - // Arrange - var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentDelete, Permissions.ResearchFileEdit); - var documentService = this._helper.GetService>(); - var researchDocumentRepository = this._helper.GetService>(); - - researchDocumentRepository.Setup(x => x.GetAllByDocument(It.IsAny())).Returns(new List()); - documentService.Setup(x => x.DeleteDocumentAsync(It.IsAny())) - .ReturnsAsync(new ExternalResponse() - { - HttpStatusCode = System.Net.HttpStatusCode.NotFound, - }); + DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.SUCCESS.ToString(), + }); + documentQueueRepository.Setup(x => x.Delete(It.IsAny())).Returns(true); + researchDocumentRepository.Setup(x => x.DeleteResearch(It.IsAny())).Returns(true); + documentRepository.Setup(x => x.DeleteDocument(It.IsAny())).Returns(true); PimsResearchFileDocument doc = new() { Internal_Id = 1, DocumentId = 2, + Document = new PimsDocument() + { + DocumentId = 2, + MayanId = 200, + } }; // Act - await service.DeleteResearchDocumentAsync(doc); + var result = await service.DeleteResearchDocumentAsync(doc); // Assert - researchDocumentRepository.Verify(x => x.DeleteResearch(It.IsAny()), Times.Once); + Assert.NotNull(result); + Assert.Equal(result.Status, ExternalResponseStatus.Success); } [Fact] - public async void DeleteDocumentPropertyActivity_Success_NoResults_Status_NotFound() + public async void DeleteDocumentPropertyActivity_Success_Status_NotFound() { // Arrange var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentDelete, Permissions.ManagementEdit); var documentService = this._helper.GetService>(); + var documentRepository = this._helper.GetService>(); + var documentQueueRepository = this._helper.GetService>(); var propertyActivityDocumentRepository = this._helper.GetService>(); - propertyActivityDocumentRepository.Setup(x => x.GetAllByPropertyActivity(It.IsAny())).Returns(new List()); - documentService.Setup(x => x.DeleteDocumentAsync(It.IsAny())) - .ReturnsAsync(new ExternalResponse() - { - HttpStatusCode = System.Net.HttpStatusCode.NotFound, - }); + documentRepository.Setup(x => x.Find(It.IsAny())).Returns(new PimsDocument() { DocumentId = 2, MayanId = 200 }); + documentRepository.Setup(x => x.BeginTransaction()).Returns(new Mock().Object); + documentService.Setup(x => x.DeleteMayanStorageDocumentAsync(It.IsAny())).ReturnsAsync(new ExternalResponse() + { + Status = ExternalResponseStatus.Error, + HttpStatusCode = System.Net.HttpStatusCode.NotFound, + }); + documentRepository.Setup(x => x.Update(It.IsAny(), false)).Returns(new PimsDocument() { DocumentId = 2, MayanId = null }); + documentQueueRepository.Setup(x => x.GetByDocumentId(It.IsAny())).Returns(new PimsDocumentQueue() + { + DocumentId = 2, + DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.SUCCESS.ToString(), + }); + documentQueueRepository.Setup(x => x.Delete(It.IsAny())).Returns(true); + propertyActivityDocumentRepository.Setup(x => x.DeletePropertyActivityDocument(It.IsAny())).Returns(true); + documentRepository.Setup(x => x.DeleteDocument(It.IsAny())).Returns(true); PimsPropertyActivityDocument doc = new() { Internal_Id = 1, DocumentId = 2, + Document = new PimsDocument() + { + DocumentId = 2, + MayanId = 200, + } }; // Act - await service.DeletePropertyActivityDocumentAsync(doc); + var result = await service.DeletePropertyActivityDocumentAsync(doc); // Assert - propertyActivityDocumentRepository.Verify(x => x.DeletePropertyActivityDocument(It.IsAny()), Times.Once); + Assert.NotNull(result); + Assert.Equal(result.Status, ExternalResponseStatus.Success); } [Fact] @@ -1017,25 +900,43 @@ public async void Delete_PropertyActivity_Success_Status_Success() var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentDelete, Permissions.ManagementEdit); var documentService = this._helper.GetService>(); var documentRepository = this._helper.GetService>(); + var documentQueueRepository = this._helper.GetService>(); + var propertyActivityDocumentRepository = this._helper.GetService>(); - documentRepository.Setup(x => x.DocumentRelationshipCount(It.IsAny())).Returns(1); - documentService.Setup(x => x.DeleteDocumentAsync(It.IsAny())) - .ReturnsAsync(new ExternalResponse() - { - Status = ExternalResponseStatus.Success, - }); + documentRepository.Setup(x => x.Find(It.IsAny())).Returns(new PimsDocument() { DocumentId = 2, MayanId = 200 }); + documentRepository.Setup(x => x.BeginTransaction()).Returns(new Mock().Object); + documentService.Setup(x => x.DeleteMayanStorageDocumentAsync(It.IsAny())).ReturnsAsync(new ExternalResponse() + { + Status = ExternalResponseStatus.Success, + HttpStatusCode = System.Net.HttpStatusCode.OK, + }); + documentRepository.Setup(x => x.Update(It.IsAny(), false)).Returns(new PimsDocument() { DocumentId = 2, MayanId = null }); + documentQueueRepository.Setup(x => x.GetByDocumentId(It.IsAny())).Returns(new PimsDocumentQueue() + { + DocumentId = 2, + DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.SUCCESS.ToString(), + }); + documentQueueRepository.Setup(x => x.Delete(It.IsAny())).Returns(true); + propertyActivityDocumentRepository.Setup(x => x.DeletePropertyActivityDocument(It.IsAny())).Returns(true); + documentRepository.Setup(x => x.DeleteDocument(It.IsAny())).Returns(true); PimsPropertyActivityDocument doc = new() { Internal_Id = 1, DocumentId = 2, + Document = new PimsDocument() + { + DocumentId = 2, + MayanId = 200, + } }; // Act - await service.DeletePropertyActivityDocumentAsync(doc); + var result = await service.DeletePropertyActivityDocumentAsync(doc); // Assert - documentService.Verify(x => x.DeleteDocumentAsync(It.IsAny()), Times.Once); + Assert.NotNull(result); + Assert.Equal(result.Status, ExternalResponseStatus.Success); } [Fact] @@ -1087,25 +988,43 @@ public async void DeleteDocumentAcquisition_Success_Status_Success() var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentDelete, Permissions.AcquisitionFileEdit); var documentService = this._helper.GetService>(); var documentRepository = this._helper.GetService>(); + var documentQueueRepository = this._helper.GetService>(); + var acquisitionDocumentRepository = this._helper.GetService>(); - documentRepository.Setup(x => x.DocumentRelationshipCount(It.IsAny())).Returns(1); - documentService.Setup(x => x.DeleteDocumentAsync(It.IsAny())) - .ReturnsAsync(new ExternalResponse() - { - Status = ExternalResponseStatus.Success, - }); + documentRepository.Setup(x => x.Find(It.IsAny())).Returns(new PimsDocument() { DocumentId = 2, MayanId = 200 }); + documentRepository.Setup(x => x.BeginTransaction()).Returns(new Mock().Object); + documentService.Setup(x => x.DeleteMayanStorageDocumentAsync(It.IsAny())).ReturnsAsync(new ExternalResponse() + { + Status = ExternalResponseStatus.Success, + HttpStatusCode = System.Net.HttpStatusCode.OK, + }); + documentRepository.Setup(x => x.Update(It.IsAny(), false)).Returns(new PimsDocument() { DocumentId = 2, MayanId = null }); + documentQueueRepository.Setup(x => x.GetByDocumentId(It.IsAny())).Returns(new PimsDocumentQueue() + { + DocumentId = 2, + DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.SUCCESS.ToString(), + }); + documentQueueRepository.Setup(x => x.Delete(It.IsAny())).Returns(true); + acquisitionDocumentRepository.Setup(x => x.DeleteAcquisition(It.IsAny())).Returns(true); + documentRepository.Setup(x => x.DeleteDocument(It.IsAny())).Returns(true); PimsAcquisitionFileDocument doc = new() { Internal_Id = 1, DocumentId = 2, + Document = new PimsDocument() + { + DocumentId = 2, + MayanId = 200, + } }; // Act - await service.DeleteAcquisitionDocumentAsync(doc); + var result = await service.DeleteAcquisitionDocumentAsync(doc); // Assert - documentService.Verify(x => x.DeleteDocumentAsync(It.IsAny()), Times.Once); + Assert.NotNull(result); + Assert.Equal(result.Status, ExternalResponseStatus.Success); } [Fact] @@ -1115,25 +1034,43 @@ public async void DeleteDocumentAcquisition_Success_Status_NotFound() var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentDelete, Permissions.AcquisitionFileEdit); var documentService = this._helper.GetService>(); var documentRepository = this._helper.GetService>(); + var documentQueueRepository = this._helper.GetService>(); + var acquisitionDocumentRepository = this._helper.GetService>(); - documentRepository.Setup(x => x.DocumentRelationshipCount(It.IsAny())).Returns(1); - documentService.Setup(x => x.DeleteDocumentAsync(It.IsAny())) - .ReturnsAsync(new ExternalResponse() - { - HttpStatusCode = System.Net.HttpStatusCode.NotFound, - }); + documentRepository.Setup(x => x.Find(It.IsAny())).Returns(new PimsDocument() { DocumentId = 2, MayanId = 200 }); + documentRepository.Setup(x => x.BeginTransaction()).Returns(new Mock().Object); + documentService.Setup(x => x.DeleteMayanStorageDocumentAsync(It.IsAny())).ReturnsAsync(new ExternalResponse() + { + Status = ExternalResponseStatus.Error, + HttpStatusCode = System.Net.HttpStatusCode.NotFound, + }); + documentRepository.Setup(x => x.Update(It.IsAny(), false)).Returns(new PimsDocument() { DocumentId = 2, MayanId = null }); + documentQueueRepository.Setup(x => x.GetByDocumentId(It.IsAny())).Returns(new PimsDocumentQueue() + { + DocumentId = 2, + DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.SUCCESS.ToString(), + }); + documentQueueRepository.Setup(x => x.Delete(It.IsAny())).Returns(true); + acquisitionDocumentRepository.Setup(x => x.DeleteAcquisition(It.IsAny())).Returns(true); + documentRepository.Setup(x => x.DeleteDocument(It.IsAny())).Returns(true); PimsAcquisitionFileDocument doc = new() { Internal_Id = 1, DocumentId = 2, + Document = new PimsDocument() + { + DocumentId = 2, + MayanId = 200, + } }; // Act - await service.DeleteAcquisitionDocumentAsync(doc); + var result = await service.DeleteAcquisitionDocumentAsync(doc); // Assert - documentService.Verify(x => x.DeleteDocumentAsync(It.IsAny()), Times.Once); + Assert.NotNull(result); + Assert.Equal(result.Status, ExternalResponseStatus.Success); } [Fact] @@ -1162,25 +1099,43 @@ public async void Delete_ProjectDocument_Success_Status_Success() var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentDelete, Permissions.ProjectEdit); var documentService = this._helper.GetService>(); var documentRepository = this._helper.GetService>(); + var documentQueueRepository = this._helper.GetService>(); + var projectRepository = this._helper.GetService>(); - documentRepository.Setup(x => x.DocumentRelationshipCount(It.IsAny())).Returns(1); - documentService.Setup(x => x.DeleteDocumentAsync(It.IsAny())) - .ReturnsAsync(new ExternalResponse() - { - Status = ExternalResponseStatus.Success, - }); + documentRepository.Setup(x => x.Find(It.IsAny())).Returns(new PimsDocument() { DocumentId = 2, MayanId = 200 }); + documentRepository.Setup(x => x.BeginTransaction()).Returns(new Mock().Object); + documentService.Setup(x => x.DeleteMayanStorageDocumentAsync(It.IsAny())).ReturnsAsync(new ExternalResponse() + { + Status = ExternalResponseStatus.Success, + HttpStatusCode = System.Net.HttpStatusCode.OK, + }); + documentRepository.Setup(x => x.Update(It.IsAny(), false)).Returns(new PimsDocument() { DocumentId = 2, MayanId = null }); + documentQueueRepository.Setup(x => x.GetByDocumentId(It.IsAny())).Returns(new PimsDocumentQueue() + { + DocumentId = 2, + DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.SUCCESS.ToString(), + }); + documentQueueRepository.Setup(x => x.Delete(It.IsAny())).Returns(true); + projectRepository.Setup(x => x.DeleteProjectDocument(It.IsAny())); + documentRepository.Setup(x => x.DeleteDocument(It.IsAny())).Returns(true); PimsProjectDocument doc = new() { Internal_Id = 1, DocumentId = 2, + Document = new PimsDocument() + { + DocumentId = 2, + MayanId = 200, + } }; // Act - await service.DeleteProjectDocumentAsync(doc); + var result = await service.DeleteProjectDocumentAsync(doc); // Assert - documentService.Verify(x => x.DeleteDocumentAsync(It.IsAny()), Times.Once); + Assert.NotNull(result); + Assert.Equal(result.Status, ExternalResponseStatus.Success); } [Fact] @@ -1190,53 +1145,43 @@ public async void Delete_ProjectDocument_Success_Status_NotFound() var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentDelete, Permissions.ProjectEdit); var documentService = this._helper.GetService>(); var documentRepository = this._helper.GetService>(); + var documentQueueRepository = this._helper.GetService>(); + var projectRepository = this._helper.GetService>(); - documentRepository.Setup(x => x.DocumentRelationshipCount(It.IsAny())).Returns(1); - documentService.Setup(x => x.DeleteDocumentAsync(It.IsAny())) - .ReturnsAsync(new ExternalResponse() - { - HttpStatusCode = System.Net.HttpStatusCode.NotFound, - }); - - PimsProjectDocument doc = new() + documentRepository.Setup(x => x.Find(It.IsAny())).Returns(new PimsDocument() { DocumentId = 2, MayanId = 200 }); + documentRepository.Setup(x => x.BeginTransaction()).Returns(new Mock().Object); + documentService.Setup(x => x.DeleteMayanStorageDocumentAsync(It.IsAny())).ReturnsAsync(new ExternalResponse() + { + Status = ExternalResponseStatus.Error, + HttpStatusCode = System.Net.HttpStatusCode.NotFound, + }); + documentRepository.Setup(x => x.Update(It.IsAny(), false)).Returns(new PimsDocument() { DocumentId = 2, MayanId = null }); + documentQueueRepository.Setup(x => x.GetByDocumentId(It.IsAny())).Returns(new PimsDocumentQueue() { - Internal_Id = 1, DocumentId = 2, - }; - - // Act - await service.DeleteProjectDocumentAsync(doc); - - // Assert - documentService.Verify(x => x.DeleteDocumentAsync(It.IsAny()), Times.Once); - } - - [Fact] - public async void Delete_ProjectDocument_Success_NoResults_Status_NotFound() - { - // Arrange - var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentDelete, Permissions.ProjectEdit); - var documentService = this._helper.GetService>(); - var projectRepository = this._helper.GetService>(); - - projectRepository.Setup(x => x.GetAllByDocument(It.IsAny())).Returns(new List()); - documentService.Setup(x => x.DeleteDocumentAsync(It.IsAny())) - .ReturnsAsync(new ExternalResponse() - { - HttpStatusCode = System.Net.HttpStatusCode.NotFound, - }); + DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.SUCCESS.ToString(), + }); + documentQueueRepository.Setup(x => x.Delete(It.IsAny())).Returns(true); + projectRepository.Setup(x => x.DeleteProjectDocument(It.IsAny())); + documentRepository.Setup(x => x.DeleteDocument(It.IsAny())).Returns(true); PimsProjectDocument doc = new() { Internal_Id = 1, DocumentId = 2, + Document = new PimsDocument() + { + DocumentId = 2, + MayanId = 200, + } }; // Act - await service.DeleteProjectDocumentAsync(doc); + var result = await service.DeleteProjectDocumentAsync(doc); // Assert - projectRepository.Verify(x => x.DeleteProjectDocument(It.Is(x => x == 1)), Times.Once); + Assert.NotNull(result); + Assert.Equal(result.Status, ExternalResponseStatus.Success); } [Fact] @@ -1265,53 +1210,44 @@ public async void Delete_LeaseDocument_Success_Status_Success() var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentDelete, Permissions.LeaseEdit); var documentService = this._helper.GetService>(); var documentRepository = this._helper.GetService>(); + var documentQueueRepository = this._helper.GetService>(); - documentRepository.Setup(x => x.DocumentRelationshipCount(It.IsAny())).Returns(1); - documentService.Setup(x => x.DeleteDocumentAsync(It.IsAny())) - .ReturnsAsync(new ExternalResponse() - { - Status = ExternalResponseStatus.Success, - }); + var leaseRepository = this._helper.GetService>(); - PimsLeaseDocument doc = new() + documentRepository.Setup(x => x.Find(It.IsAny())).Returns(new PimsDocument() { DocumentId = 2, MayanId = 200 }); + documentRepository.Setup(x => x.BeginTransaction()).Returns(new Mock().Object); + documentService.Setup(x => x.DeleteMayanStorageDocumentAsync(It.IsAny())).ReturnsAsync(new ExternalResponse() + { + Status = ExternalResponseStatus.Success, + HttpStatusCode = System.Net.HttpStatusCode.OK, + }); + documentRepository.Setup(x => x.Update(It.IsAny(), false)).Returns(new PimsDocument() { DocumentId = 2, MayanId = null }); + documentQueueRepository.Setup(x => x.GetByDocumentId(It.IsAny())).Returns(new PimsDocumentQueue() { - Internal_Id = 1, DocumentId = 2, - }; - - // Act - await service.DeleteLeaseDocumentAsync(doc); - - // Assert - documentService.Verify(x => x.DeleteDocumentAsync(It.IsAny()), Times.Once); - } - - [Fact] - public async void Delete_LeaseDocument_Success_Status_NotFound() - { - // Arrange - var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentDelete, Permissions.LeaseEdit); - var documentService = this._helper.GetService>(); - var documentRepository = this._helper.GetService>(); - - documentRepository.Setup(x => x.DocumentRelationshipCount(It.IsAny())).Returns(1); - documentService.Setup(x => x.DeleteDocumentAsync(It.IsAny())) - .ReturnsAsync(new ExternalResponse() - { - HttpStatusCode = System.Net.HttpStatusCode.NotFound, - }); + DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.SUCCESS.ToString(), + }); + documentQueueRepository.Setup(x => x.Delete(It.IsAny())).Returns(true); + leaseRepository.Setup(x => x.DeleteLeaseDocument(It.IsAny())); + documentRepository.Setup(x => x.DeleteDocument(It.IsAny())).Returns(true); PimsLeaseDocument doc = new() { Internal_Id = 1, DocumentId = 2, + Document = new PimsDocument() + { + DocumentId = 2, + MayanId = 200, + } }; // Act - await service.DeleteLeaseDocumentAsync(doc); + var result = await service.DeleteLeaseDocumentAsync(doc); // Assert - documentService.Verify(x => x.DeleteDocumentAsync(It.IsAny()), Times.Once); + Assert.NotNull(result); + Assert.Equal(result.Status, ExternalResponseStatus.Success); } [Fact] @@ -1320,26 +1256,45 @@ public async void Delete_LeaseDocument_Success_NoResults_Status_NotFound() // Arrange var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentDelete, Permissions.LeaseEdit); var documentService = this._helper.GetService>(); + var documentRepository = this._helper.GetService>(); + var documentQueueRepository = this._helper.GetService>(); + var leaseRepository = this._helper.GetService>(); - leaseRepository.Setup(x => x.GetAllLeaseDocuments(It.IsAny())).Returns(new List()); - documentService.Setup(x => x.DeleteDocumentAsync(It.IsAny())) - .ReturnsAsync(new ExternalResponse() - { - HttpStatusCode = System.Net.HttpStatusCode.NotFound, - }); + documentRepository.Setup(x => x.Find(It.IsAny())).Returns(new PimsDocument() { DocumentId = 2, MayanId = 200 }); + documentRepository.Setup(x => x.BeginTransaction()).Returns(new Mock().Object); + documentService.Setup(x => x.DeleteMayanStorageDocumentAsync(It.IsAny())).ReturnsAsync(new ExternalResponse() + { + Status = ExternalResponseStatus.Error, + HttpStatusCode = System.Net.HttpStatusCode.NotFound, + }); + documentRepository.Setup(x => x.Update(It.IsAny(), false)).Returns(new PimsDocument() { DocumentId = 2, MayanId = null }); + documentQueueRepository.Setup(x => x.GetByDocumentId(It.IsAny())).Returns(new PimsDocumentQueue() + { + DocumentId = 2, + DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.SUCCESS.ToString(), + }); + documentQueueRepository.Setup(x => x.Delete(It.IsAny())).Returns(true); + leaseRepository.Setup(x => x.DeleteLeaseDocument(It.IsAny())); + documentRepository.Setup(x => x.DeleteDocument(It.IsAny())).Returns(true); PimsLeaseDocument doc = new() { Internal_Id = 1, DocumentId = 2, + Document = new PimsDocument() + { + DocumentId = 2, + MayanId = 200, + } }; // Act - await service.DeleteLeaseDocumentAsync(doc); + var result = await service.DeleteLeaseDocumentAsync(doc); // Assert - leaseRepository.Verify(x => x.DeleteLeaseDocument(It.Is(x => x == 1)), Times.Once); + Assert.NotNull(result); + Assert.Equal(result.Status, ExternalResponseStatus.Success); } [Fact] @@ -1368,25 +1323,43 @@ public async void Delete_DispositionDocument_Success_Status_Success() var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentDelete, Permissions.DispositionEdit); var documentService = this._helper.GetService>(); var documentRepository = this._helper.GetService>(); + var documentQueueRepository = this._helper.GetService>(); + var dispositionDocumentRepository = this._helper.GetService>(); - documentRepository.Setup(x => x.DocumentRelationshipCount(It.IsAny())).Returns(1); - documentService.Setup(x => x.DeleteDocumentAsync(It.IsAny())) - .ReturnsAsync(new ExternalResponse() - { - Status = ExternalResponseStatus.Success, - }); + documentRepository.Setup(x => x.Find(It.IsAny())).Returns(new PimsDocument() { DocumentId = 2, MayanId = 200 }); + documentRepository.Setup(x => x.BeginTransaction()).Returns(new Mock().Object); + documentService.Setup(x => x.DeleteMayanStorageDocumentAsync(It.IsAny())).ReturnsAsync(new ExternalResponse() + { + Status = ExternalResponseStatus.Success, + HttpStatusCode = System.Net.HttpStatusCode.OK, + }); + documentRepository.Setup(x => x.Update(It.IsAny(), false)).Returns(new PimsDocument() { DocumentId = 2, MayanId = null }); + documentQueueRepository.Setup(x => x.GetByDocumentId(It.IsAny())).Returns(new PimsDocumentQueue() + { + DocumentId = 2, + DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.SUCCESS.ToString(), + }); + documentQueueRepository.Setup(x => x.Delete(It.IsAny())).Returns(true); + dispositionDocumentRepository.Setup(x => x.DeleteDispositionDocument(It.IsAny())).Returns(true); + documentRepository.Setup(x => x.DeleteDocument(It.IsAny())).Returns(true); PimsDispositionFileDocument doc = new() { Internal_Id = 1, DocumentId = 2, + Document = new PimsDocument() + { + DocumentId = 2, + MayanId = 200, + } }; // Act - await service.DeleteDispositionDocumentAsync(doc); + var result = await service.DeleteDispositionDocumentAsync(doc); // Assert - documentService.Verify(x => x.DeleteDocumentAsync(It.IsAny()), Times.Once); + Assert.NotNull(result); + Assert.Equal(result.Status, ExternalResponseStatus.Success); } [Fact] @@ -1396,54 +1369,43 @@ public async void Delete_DispositionDocument_Success_Status_NotFound() var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentDelete, Permissions.DispositionEdit); var documentService = this._helper.GetService>(); var documentRepository = this._helper.GetService>(); + var documentQueueRepository = this._helper.GetService>(); + var dispositionDocumentRepository = this._helper.GetService>(); - documentRepository.Setup(x => x.DocumentRelationshipCount(It.IsAny())).Returns(1); - documentService.Setup(x => x.DeleteDocumentAsync(It.IsAny())) - .ReturnsAsync(new ExternalResponse() - { - HttpStatusCode = System.Net.HttpStatusCode.NotFound, - }); - - PimsDispositionFileDocument doc = new() + documentRepository.Setup(x => x.Find(It.IsAny())).Returns(new PimsDocument() { DocumentId = 2, MayanId = 200 }); + documentRepository.Setup(x => x.BeginTransaction()).Returns(new Mock().Object); + documentService.Setup(x => x.DeleteMayanStorageDocumentAsync(It.IsAny())).ReturnsAsync(new ExternalResponse() + { + Status = ExternalResponseStatus.Error, + HttpStatusCode = System.Net.HttpStatusCode.NotFound, + }); + documentRepository.Setup(x => x.Update(It.IsAny(), false)).Returns(new PimsDocument() { DocumentId = 2, MayanId = null }); + documentQueueRepository.Setup(x => x.GetByDocumentId(It.IsAny())).Returns(new PimsDocumentQueue() { - Internal_Id = 1, DocumentId = 2, - }; - - // Act - await service.DeleteDispositionDocumentAsync(doc); - - // Assert - documentService.Verify(x => x.DeleteDocumentAsync(It.IsAny()), Times.Once); - } - - [Fact] - public async void Delete_DispositionDocument_Success_NoResults_Status_NotFound() - { - // Arrange - var service = this.CreateDocumentFileServiceWithPermissions(Permissions.DocumentDelete, Permissions.DispositionEdit); - var documentService = this._helper.GetService>(); - var documentRepository = this._helper.GetService>(); - var dispositionRepository = this._helper.GetService>(); - - documentRepository.Setup(x => x.DocumentRelationshipCount(It.IsAny())).Returns(100); - documentService.Setup(x => x.DeleteDocumentAsync(It.IsAny())) - .ReturnsAsync(new ExternalResponse() - { - HttpStatusCode = System.Net.HttpStatusCode.NotFound, - }); + DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.SUCCESS.ToString(), + }); + documentQueueRepository.Setup(x => x.Delete(It.IsAny())).Returns(true); + dispositionDocumentRepository.Setup(x => x.DeleteDispositionDocument(It.IsAny())).Returns(true); + documentRepository.Setup(x => x.DeleteDocument(It.IsAny())).Returns(true); PimsDispositionFileDocument doc = new() { Internal_Id = 1, DocumentId = 2, + Document = new PimsDocument() + { + DocumentId = 2, + MayanId = 200, + } }; // Act - await service.DeleteDispositionDocumentAsync(doc); + var result = await service.DeleteDispositionDocumentAsync(doc); // Assert - dispositionRepository.Verify(x => x.DeleteDispositionDocument(doc), Times.Once); + Assert.NotNull(result); + Assert.Equal(result.Status, ExternalResponseStatus.Success); } } } diff --git a/source/backend/tests/unit/api/Services/DocumentQueueServiceTest.cs b/source/backend/tests/unit/api/Services/DocumentQueueServiceTest.cs new file mode 100644 index 0000000000..eb7e69dc25 --- /dev/null +++ b/source/backend/tests/unit/api/Services/DocumentQueueServiceTest.cs @@ -0,0 +1,761 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Text.Json; +using System.Threading.Tasks; +using Azure; +using FluentAssertions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Pims.Api.Models.CodeTypes; +using Pims.Api.Models.Concepts.Document; +using Pims.Api.Models.Mayan.Document; +using Pims.Api.Models.Requests.Document.Upload; +using Pims.Api.Models.Requests.Http; +using Pims.Api.Services; +using Pims.Core.Api.Exceptions; +using Pims.Core.Exceptions; +using Pims.Core.Http.Configuration; +using Pims.Core.Security; +using Pims.Core.Test; +using Pims.Dal.Entities; +using Pims.Dal.Entities.Models; +using Pims.Dal.Repositories; +using Xunit; + +namespace Pims.Api.Test.Services +{ + [Trait("category", "unit")] + [Trait("category", "api")] + [Trait("group", "documentQueues")] + [ExcludeFromCodeCoverage] + public class DocumentQueueServiceTest + { + private TestHelper _helper; + + public DocumentQueueServiceTest() + { + this._helper = new TestHelper(); + } + + private DocumentQueueService CreateDocumentQueueServiceWithPermissions(params Permissions[] permissions) + { + var user = PrincipalHelper.CreateForPermission(permissions); + this._helper.CreatePimsContext(user, true); + var builder = new ConfigurationBuilder(); + return this._helper.Create(builder.Build()); + } + + [Fact] + public void SearchDocumentQueue_Success() + { + // Arrange + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + var filter = new DocumentQueueFilter(); + var documentQueues = new List { new PimsDocumentQueue() }; + var documentQueueRepositoryMock = this._helper.GetService>(); + + documentQueueRepositoryMock.Setup(m => m.GetAllByFilter(filter)).Returns(documentQueues); + + // Act + var result = service.SearchDocumentQueue(filter); + + // Assert + result.Should().BeEquivalentTo(documentQueues); + documentQueueRepositoryMock.Verify(m => m.GetAllByFilter(filter), Times.Once); + } + + [Fact] + public void SearchDocumentQueue_Success_MaxFileSize() + { + // Arrange + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + var filter = new DocumentQueueFilter(); + filter.MaxFileSize = 4; + var documentQueues = new List { new PimsDocumentQueue() { DocumentQueueId = 1, Document = new byte[] {1, 2, 3 , 4 } }, new PimsDocumentQueue() { DocumentQueueId = 2, Document = new byte[] { 5, 6, 7, 8 } } }; + + var documentQueueRepositoryMock = this._helper.GetService>(); + + documentQueueRepositoryMock.Setup(m => m.GetFileLengthsById(It.IsAny>())).Returns(new Dictionary() { { 1, 4 }, { 2, 4 } }); + documentQueueRepositoryMock.Setup(m => m.GetAllByFilter(filter)).Returns(documentQueues); + + // Act + var result = service.SearchDocumentQueue(filter); + + // Assert + result.Should().HaveCount(1); + result.First().Should().BeEquivalentTo(documentQueues.FirstOrDefault()); + documentQueueRepositoryMock.Verify(m => m.GetAllByFilter(filter), Times.Once); + } + + [Fact] + public void SearchDocumentQueue_Success_MaxFileSize_MinOne() + { + // Arrange + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + var filter = new DocumentQueueFilter(); + filter.MaxFileSize = 0; + var documentQueues = new List { new PimsDocumentQueue() { DocumentQueueId = 1, Document = new byte[] { 1, 2, 3, 4 } }, new PimsDocumentQueue() { DocumentQueueId = 2, Document = new byte[] { 5, 6, 7, 8 } } }; + var documentQueueRepositoryMock = this._helper.GetService>(); + + documentQueueRepositoryMock.Setup(m => m.GetFileLengthsById(It.IsAny>())).Returns(new Dictionary() { { 1, 4 }, { 2, 4 } }); + documentQueueRepositoryMock.Setup(m => m.GetAllByFilter(filter)).Returns(documentQueues); + + // Act + var result = service.SearchDocumentQueue(filter); + + // Assert + result.Should().HaveCount(1); + result.First().Should().BeEquivalentTo(documentQueues.FirstOrDefault()); + documentQueueRepositoryMock.Verify(m => m.GetAllByFilter(filter), Times.Once); + } + + [Fact] + public void SearchDocumentQueue_InvalidPermissions_ThrowsNotAuthorizedException() + { + // Arrange + var filter = new DocumentQueueFilter(); + var service = CreateDocumentQueueServiceWithPermissions(); + + // Act + Action act = () => service.SearchDocumentQueue(filter); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void Update_Success() + { + // Arrange + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + var documentQueue = new PimsDocumentQueue { DocumentQueueId = 1 }; + var documentQueueRepositoryMock = this._helper.GetService>(); + + documentQueueRepositoryMock.Setup(m => m.Update(documentQueue, false)); + documentQueueRepositoryMock.Setup(m => m.CommitTransaction()); + + // Act + var result = service.Update(documentQueue); + + // Assert + result.Should().Be(documentQueue); + documentQueueRepositoryMock.Verify(m => m.Update(documentQueue, false), Times.Once); + documentQueueRepositoryMock.Verify(m => m.CommitTransaction(), Times.Once); + } + + [Fact] + public void Update_InvalidPermissions_ThrowsNotAuthorizedException() + { + // Arrange + var service = CreateDocumentQueueServiceWithPermissions(); + var documentQueue = new PimsDocumentQueue { DocumentQueueId = 1 }; + + // Act + Action act = () => service.Update(documentQueue); + + // Assert + act.Should().Throw(); + } + + [Fact] + public async Task PollForDocumentDocumentIdNull_ThrowsInvalidDataException() + { + // Arrange + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + var documentQueue = new PimsDocumentQueue { DocumentQueueId = 1, DocumentId = null }; + var documentRepositoryMock = this._helper.GetService>(); + var documentQueueRepositoryMock = this._helper.GetService>(); + var documentServiceMock = this._helper.GetService>(); + + // Act + Func act = async () => await service.PollForDocument(documentQueue); + + // Assert + await act.Should().ThrowAsync(); + documentRepositoryMock.Verify(m => m.TryGet(It.IsAny()), Times.Never); + documentQueueRepositoryMock.Verify(m => m.TryGetById(It.IsAny()), Times.Never); + documentServiceMock.Verify(m => m.GetStorageDocumentDetail(It.IsAny()), Times.Never); + } + + [Fact] + public async Task PollForDocument_NoDatabaseDocumentQueue() + { + // Arrange + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + var documentQueue = new PimsDocumentQueue { DocumentQueueId = 1, DocumentId = 1 }; + var documentRepositoryMock = this._helper.GetService>(); + + // Act + Func act = async () => await service.PollForDocument(documentQueue); + + // Assert + await act.Should().ThrowAsync(); + documentRepositoryMock.Verify(m => m.TryGet(It.IsAny()), Times.Never); + } + + [Fact] + public async Task PollForDocument_RelatedDocumentMayanIdNull_UpdatesStatusToPIMSError() + { + // Arrange + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + var documentQueue = new PimsDocumentQueue { DocumentQueueId = 1, DocumentId = 1 }; + var relatedDocument = new PimsDocument { MayanId = null }; + var databaseDocumentQueue = new PimsDocumentQueue { DocumentQueueId = 1 }; + var documentRepositoryMock = this._helper.GetService>(); + var documentQueueRepositoryMock = this._helper.GetService>(); + var documentServiceMock = this._helper.GetService>(); + + documentRepositoryMock.Setup(m => m.TryGet(documentQueue.DocumentId.Value)).Returns(relatedDocument); + documentQueueRepositoryMock.Setup(m => m.TryGetById(documentQueue.DocumentQueueId)).Returns(databaseDocumentQueue); + + // Act + var result = await service.PollForDocument(documentQueue); + + // Assert + result.Should().Be(databaseDocumentQueue); + documentQueueRepositoryMock.Verify(m => m.Update(databaseDocumentQueue, false), Times.Once); + documentQueueRepositoryMock.Verify(m => m.CommitTransaction(), Times.Once); + } + + [Fact] + public async Task PollForDocument_GetStorageDocumentDetailFails_UpdatesStatusToPIMSError() + { + // Arrange + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + var documentQueue = new PimsDocumentQueue { DocumentQueueId = 1, DocumentId = 1 }; + var relatedDocument = new PimsDocument { MayanId = 1 }; + var databaseDocumentQueue = new PimsDocumentQueue { DocumentQueueId = 1 }; + var documentDetailsResponse = new ExternalResponse { Status = ExternalResponseStatus.Error }; + var documentRepositoryMock = this._helper.GetService>(); + var documentQueueRepositoryMock = this._helper.GetService>(); + var documentServiceMock = this._helper.GetService>(); + + documentRepositoryMock.Setup(m => m.TryGet(documentQueue.DocumentId.Value)).Returns(relatedDocument); + documentQueueRepositoryMock.Setup(m => m.TryGetById(documentQueue.DocumentQueueId)).Returns(databaseDocumentQueue); + documentServiceMock.Setup(m => m.GetStorageDocumentDetail(relatedDocument.MayanId.Value)).ReturnsAsync(documentDetailsResponse); + + // Act + var result = await service.PollForDocument(documentQueue); + + // Assert + result.Should().Be(databaseDocumentQueue); + documentQueueRepositoryMock.Verify(m => m.Update(databaseDocumentQueue, false), Times.Once); + documentQueueRepositoryMock.Verify(m => m.CommitTransaction(), Times.Once); + } + + [Fact] + public async Task PollForDocument_FileLatestIdNull_LogsFileStillProcessing() + { + // Arrange + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + var documentQueue = new PimsDocumentQueue { DocumentQueueId = 1, DocumentId = 1 }; + var relatedDocument = new PimsDocument { MayanId = 1 }; + var databaseDocumentQueue = new PimsDocumentQueue { DocumentQueueId = 1 }; + var documentDetailModel = new DocumentDetailModel { FileLatest = null }; + var documentDetailsResponse = new ExternalResponse { Status = ExternalResponseStatus.Success, Payload = documentDetailModel }; + var documentRepositoryMock = this._helper.GetService>(); + var documentQueueRepositoryMock = this._helper.GetService>(); + var documentServiceMock = this._helper.GetService>(); + + documentRepositoryMock.Setup(m => m.TryGet(documentQueue.DocumentId.Value)).Returns(relatedDocument); + documentQueueRepositoryMock.Setup(m => m.TryGetById(documentQueue.DocumentQueueId)).Returns(databaseDocumentQueue); + documentServiceMock.Setup(m => m.GetStorageDocumentDetail(relatedDocument.MayanId.Value)).ReturnsAsync(documentDetailsResponse); + + // Act + var result = await service.PollForDocument(documentQueue); + + // Assert + result.Should().Be(databaseDocumentQueue); + documentQueueRepositoryMock.Verify(m => m.Update(databaseDocumentQueue, false), Times.Never); + documentQueueRepositoryMock.Verify(m => m.CommitTransaction(), Times.Never); + } + + [Fact] + public async Task PollForDocument_FileLatestIdNotNull_UpdatesStatusToSuccess() + { + // Arrange + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + var documentQueue = new PimsDocumentQueue { DocumentQueueId = 1, DocumentId = 1 }; + var relatedDocument = new PimsDocument { MayanId = 1 }; + var databaseDocumentQueue = new PimsDocumentQueue { DocumentQueueId = 1 }; + var documentDetailModel = new DocumentDetailModel { FileLatest = new FileLatestModel { Id = 1 } }; + var documentDetailsResponse = new ExternalResponse { Status = ExternalResponseStatus.Success, Payload = documentDetailModel }; + var documentRepositoryMock = this._helper.GetService>(); + var documentQueueRepositoryMock = this._helper.GetService>(); + var documentServiceMock = this._helper.GetService>(); + + documentRepositoryMock.Setup(m => m.TryGet(documentQueue.DocumentId.Value)).Returns(relatedDocument); + documentQueueRepositoryMock.Setup(m => m.TryGetById(documentQueue.DocumentQueueId)).Returns(databaseDocumentQueue); + documentServiceMock.Setup(m => m.GetStorageDocumentDetail(relatedDocument.MayanId.Value)).ReturnsAsync(documentDetailsResponse); + + // Act + var result = await service.PollForDocument(documentQueue); + + // Assert + result.Should().Be(databaseDocumentQueue); + result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.SUCCESS.ToString()); + documentQueueRepositoryMock.Verify(m => m.Update(databaseDocumentQueue, true), Times.Once); + documentQueueRepositoryMock.Verify(m => m.CommitTransaction(), Times.Once); + } + + [Fact] + public async Task Upload_Success() + { + // Arrange + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + var documentQueue = new PimsDocumentQueue + { + DocumentQueueId = 1, + DocumentId = 1, + DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.PENDING.ToString(), + Document = new byte[] { 1, 2, 3 }, + DocProcessRetries = 0, + }; + + var relatedDocument = new PimsDocument + { + DocumentId = 1, + DocumentTypeId = 1, + FileName = "test.pdf", + DocumentStatusTypeCode = "STATUS", + MayanId = null + }; + + var documentType = new PimsDocumentTyp + { + DocumentTypeId = 1, + MayanId = 1 + }; + + var documentUploadResponse = new DocumentUploadResponse + { + DocumentExternalResponse = new ExternalResponse + { + Status = ExternalResponseStatus.Success, + Payload = new DocumentDetailModel + { + FileLatest = new FileLatestModel + { + Id = 1 + } + } + }, + MetadataExternalResponse = new List>() + }; + + var documentRepositoryMock = this._helper.GetService>(); + var documentQueueRepositoryMock = this._helper.GetService>(); + var documentServiceMock = this._helper.GetService>(); + var documentTypeRepositoryMock = this._helper.GetService>(); + + documentQueueRepositoryMock.Setup(x => x.TryGetById(It.IsAny())).Returns(documentQueue); + documentRepositoryMock.Setup(x => x.TryGetDocumentRelationships(It.IsAny())).Returns(relatedDocument); + documentTypeRepositoryMock.Setup(x => x.GetById(It.IsAny())).Returns(documentType); + documentServiceMock.Setup(x => x.UploadDocumentAsync(It.IsAny(), true)).ReturnsAsync(documentUploadResponse); + + // Act + var result = await service.Upload(documentQueue); + + // Assert + result.Should().NotBeNull(); + result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.SUCCESS.ToString()); + documentQueueRepositoryMock.Verify(x => x.Update(It.IsAny(), It.IsAny()), Times.AtLeastOnce); + documentQueueRepositoryMock.Verify(x => x.CommitTransaction(), Times.AtLeastOnce); + documentServiceMock.Verify(x => x.UploadDocumentAsync(It.IsAny(), true), Times.Once); + } + + [Fact] + public async Task Upload_Retry_Success() + { + // Arrange + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + var documentQueue = new PimsDocumentQueue + { + DocumentQueueId = 1, + DocumentId = 1, + DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.PIMS_ERROR.ToString(), + Document = new byte[] { 1, 2, 3 }, + DocProcessRetries = 0, + }; + + var relatedDocument = new PimsDocument + { + DocumentId = 1, + DocumentTypeId = 1, + FileName = "test.pdf", + DocumentStatusTypeCode = "STATUS", + MayanId = null + }; + + var documentType = new PimsDocumentTyp + { + DocumentTypeId = 1, + MayanId = 1 + }; + + var documentUploadResponse = new DocumentUploadResponse + { + DocumentExternalResponse = new ExternalResponse + { + Status = ExternalResponseStatus.Success, + Payload = new DocumentDetailModel + { + FileLatest = new FileLatestModel + { + Id = 1 + } + } + }, + MetadataExternalResponse = new List>() + }; + + var documentRepositoryMock = this._helper.GetService>(); + var documentQueueRepositoryMock = this._helper.GetService>(); + var documentServiceMock = this._helper.GetService>(); + var documentTypeRepositoryMock = this._helper.GetService>(); + + documentQueueRepositoryMock.Setup(x => x.TryGetById(It.IsAny())).Returns(documentQueue); + documentRepositoryMock.Setup(x => x.TryGetDocumentRelationships(It.IsAny())).Returns(relatedDocument); + documentTypeRepositoryMock.Setup(x => x.GetById(It.IsAny())).Returns(documentType); + documentServiceMock.Setup(x => x.UploadDocumentAsync(It.IsAny(), true)).ReturnsAsync(documentUploadResponse); + + // Act + var result = await service.Upload(documentQueue); + + // Assert + result.Should().NotBeNull(); + result.DocProcessRetries.Should().Be(1); + result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.SUCCESS.ToString()); + documentQueueRepositoryMock.Verify(x => x.Update(It.IsAny(), It.IsAny()), Times.AtLeastOnce); + documentQueueRepositoryMock.Verify(x => x.CommitTransaction(), Times.AtLeastOnce); + documentServiceMock.Verify(x => x.UploadDocumentAsync(It.IsAny(), true), Times.Once); + } + + [Fact] + public async Task Upload_ValidateQueuedDocumentFails_UpdatesStatusToPIMSError() + { + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + // Arrange + var documentQueue = new PimsDocumentQueue + { + DocumentQueueId = 1, + DocumentId = 1, + DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.PENDING.ToString(), + Document = null, + DocProcessRetries = 0, + }; + + var documentRepositoryMock = this._helper.GetService>(); + var documentQueueRepositoryMock = this._helper.GetService>(); + var documentServiceMock = this._helper.GetService>(); + + documentQueueRepositoryMock.Setup(x => x.TryGetById(It.IsAny())).Returns(documentQueue); + + // Act + var result = await service.Upload(documentQueue); + + // Assert + result.Should().NotBeNull(); + result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.PIMS_ERROR.ToString()); + documentQueueRepositoryMock.Verify(x => x.Update(It.IsAny(), It.IsAny()), Times.AtLeastOnce); + documentQueueRepositoryMock.Verify(x => x.CommitTransaction(), Times.AtLeastOnce); + documentServiceMock.Verify(x => x.UploadDocumentAsync(It.IsAny(), true), Times.Never); + } + + [Fact] + public async Task UploadDocumentQueueNotFound_ThrowsKeyNotFoundException() + { + // Arrange + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + var documentQueue = new PimsDocumentQueue { DocumentQueueId = 1 }; + var documentQueueRepositoryMock = this._helper.GetService>(); + + documentQueueRepositoryMock.Setup(x => x.TryGetById(It.IsAny())).Returns((PimsDocumentQueue)null); + + // Act + Func act = async () => await service.Upload(documentQueue); + + // Assert + await act.Should().ThrowAsync(); + documentQueueRepositoryMock.Verify(x => x.TryGetById(It.IsAny()), Times.Once); + } + + [Fact] + public async Task Upload_ValidationFails_NoDocument() + { + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + // Arrange + var documentQueue = new PimsDocumentQueue { DocumentQueueId = 1, DocumentId = 1 }; + + var documentQueueRepositoryMock = this._helper.GetService>(); + + documentQueueRepositoryMock.Setup(x => x.TryGetById(It.IsAny())).Returns(documentQueue); + + // Act + var result = await service.Upload(documentQueue); + + // Assert + result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.PIMS_ERROR.ToString()); + documentQueueRepositoryMock.Verify(x => x.TryGetById(It.IsAny()), Times.Once); + } + + [Fact] + public async Task Upload_ValidationFails_Status() + { + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + // Arrange + var documentQueue = new PimsDocumentQueue { DocumentQueueId = 1, DocumentId = 1, Document = new byte[] { 1, 2, 3 }, DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.SUCCESS.ToString() }; + + var documentQueueRepositoryMock = this._helper.GetService>(); + + documentQueueRepositoryMock.Setup(x => x.TryGetById(It.IsAny())).Returns(new PimsDocumentQueue { DocumentQueueId = 1, DocumentId = 1, Document = new byte[] { 1, 2, 3 }, DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.PIMS_ERROR.ToString() }); + + // Act + var result = await service.Upload(documentQueue); + + // Assert + result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.PIMS_ERROR.ToString()); + documentQueueRepositoryMock.Verify(x => x.TryGetById(It.IsAny()), Times.Once); + } + + [Fact] + public async Task Upload_ValidationFails_Retries() + { + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + // Arrange + var documentQueue = new PimsDocumentQueue { DocumentQueueId = 1, DocumentId = 1, Document = new byte[] { 1, 2, 3 }, DocProcessRetries = 10 }; + + var documentQueueRepositoryMock = this._helper.GetService>(); + + documentQueueRepositoryMock.Setup(x => x.TryGetById(It.IsAny())).Returns(new PimsDocumentQueue { DocumentQueueId = 1, DocumentId = 1, Document = new byte[] { 1, 2, 3 }, DocProcessRetries = 0 }); + + // Act + var result = await service.Upload(documentQueue); + + // Assert + result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.PIMS_ERROR.ToString()); + documentQueueRepositoryMock.Verify(x => x.TryGetById(It.IsAny()), Times.Once); + } + + [Fact] + public async Task Upload_RelatedDocument_MayanId() + { + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + // Arrange + var documentQueue = new PimsDocumentQueue { DocumentQueueId = 1, DocumentId = 1, Document = new byte[] { 1, 2, 3 } }; + + var documentRepositoryMock = this._helper.GetService>(); + var documentQueueRepositoryMock = this._helper.GetService>(); + var documentServiceMock = this._helper.GetService>(); + + documentQueueRepositoryMock.Setup(x => x.TryGetById(It.IsAny())).Returns(documentQueue); + documentRepositoryMock.Setup(x => x.TryGetDocumentRelationships(It.IsAny())).Returns(new PimsDocument() { DocumentTypeId = 1, MayanId = 1 }); + + // Act + var result = await service.Upload(documentQueue); + + // Assert + result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.SUCCESS.ToString()); + documentQueueRepositoryMock.Verify(x => x.TryGetById(It.IsAny()), Times.Once); + documentRepositoryMock.Verify(x => x.TryGetDocumentRelationships(It.IsAny()), Times.Once); + } + + [Fact] + public async Task UploadDocumentTypeNotFound_ThrowsKeyNotFoundException() + { + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + // Arrange + var documentQueue = new PimsDocumentQueue { DocumentQueueId = 1, DocumentId = 1, Document = new byte[] { 1, 2, 3 } }; + var relatedDocument = new PimsDocument { DocumentId = 1, DocumentTypeId = 1 }; + + var documentRepositoryMock = this._helper.GetService>(); + var documentQueueRepositoryMock = this._helper.GetService>(); + var documentServiceMock = this._helper.GetService>(); + var documentTypeRepositoryMock = this._helper.GetService>(); + + documentQueueRepositoryMock.Setup(x => x.TryGetById(It.IsAny())).Returns(documentQueue); + documentRepositoryMock.Setup(x => x.TryGetDocumentRelationships(It.IsAny())).Returns(relatedDocument); + documentTypeRepositoryMock.Setup(x => x.GetById(It.IsAny())).Throws(); + + // Act + var response = await service.Upload(documentQueue); + + // Assert + response.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.PIMS_ERROR.ToString()); + documentQueueRepositoryMock.Verify(x => x.TryGetById(It.IsAny()), Times.Once); + documentRepositoryMock.Verify(x => x.TryGetDocumentRelationships(It.IsAny()), Times.Once); + documentTypeRepositoryMock.Verify(x => x.GetById(It.IsAny()), Times.Once); + } + + [Fact] + public async Task UploadDocument_ThrowsJsonException() + { + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + // Arrange + var documentQueue = new PimsDocumentQueue { DocumentQueueId = 1, DocumentId = 1, Document = new byte[] { 1, 2, 3 } }; + var relatedDocument = new PimsDocument { DocumentId = 1, DocumentTypeId = 1 }; + + var documentRepositoryMock = this._helper.GetService>(); + var documentQueueRepositoryMock = this._helper.GetService>(); + var documentServiceMock = this._helper.GetService>(); + var documentTypeRepositoryMock = this._helper.GetService>(); + + documentQueueRepositoryMock.Setup(x => x.TryGetById(It.IsAny())).Returns(documentQueue); + documentRepositoryMock.Setup(x => x.TryGetDocumentRelationships(It.IsAny())).Returns(relatedDocument); + documentTypeRepositoryMock.Setup(x => x.GetById(It.IsAny())).Throws(); + documentServiceMock.Setup(x => x.UploadDocumentAsync(It.IsAny(), true)).ThrowsAsync(new JsonException("test error")); + + // Act + var response = await service.Upload(documentQueue); + + // Assert + response.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.PIMS_ERROR.ToString()); + documentQueueRepositoryMock.Verify(x => x.TryGetById(It.IsAny()), Times.Once); + documentRepositoryMock.Verify(x => x.TryGetDocumentRelationships(It.IsAny()), Times.Once); + documentTypeRepositoryMock.Verify(x => x.GetById(It.IsAny()), Times.Once); + } + + [Fact] + public async Task Upload_DocumentTypeIdNull() + { + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + // Arrange + var documentQueue = new PimsDocumentQueue + { + DocumentQueueId = 1, + DocumentId = 1, + DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.PENDING.ToString(), + Document = new byte[] { 1, 2, 3 }, + DocProcessRetries = 0, + }; + + var relatedDocument = new PimsDocument + { + DocumentId = 1, + DocumentTypeId = 1, + FileName = "test.pdf", + DocumentStatusTypeCode = "STATUS", + MayanId = null + }; + + var documentRepositoryMock = this._helper.GetService>(); + var documentQueueRepositoryMock = this._helper.GetService>(); + + documentQueueRepositoryMock.Setup(x => x.TryGetById(It.IsAny())).Returns(documentQueue); + documentRepositoryMock.Setup(x => x.TryGetDocumentRelationships(It.IsAny())).Returns((PimsDocument)null); + + // Act + var result = await service.Upload(documentQueue); + + // Assert + result.Should().NotBeNull(); + result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.PIMS_ERROR.ToString()); + documentQueueRepositoryMock.Verify(x => x.Update(It.IsAny(), It.IsAny()), Times.AtLeastOnce); + documentQueueRepositoryMock.Verify(x => x.CommitTransaction(), Times.AtLeastOnce); + } + + [Fact] + public async Task Upload_UploadDocumentFails_UpdatesStatusToMayanError() + { + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + // Arrange + var documentQueue = new PimsDocumentQueue + { + DocumentQueueId = 1, + DocumentId = 1, + DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.PENDING.ToString(), + Document = new byte[] { 1, 2, 3 }, + DocProcessRetries = 0, + }; + + var relatedDocument = new PimsDocument + { + DocumentId = 1, + DocumentTypeId = 1, + FileName = "test.pdf", + DocumentStatusTypeCode = "STATUS", + MayanId = null + }; + + var documentType = new PimsDocumentTyp + { + DocumentTypeId = 1, + MayanId = 1 + }; + + var documentUploadResponse = new DocumentUploadResponse + { + DocumentExternalResponse = new ExternalResponse + { + Status = ExternalResponseStatus.Error + }, + MetadataExternalResponse = new List>() + }; + + var documentRepositoryMock = this._helper.GetService>(); + var documentQueueRepositoryMock = this._helper.GetService>(); + var documentServiceMock = this._helper.GetService>(); + var documentTypeRepositoryMock = this._helper.GetService>(); + + documentQueueRepositoryMock.Setup(x => x.TryGetById(It.IsAny())).Returns(documentQueue); + documentRepositoryMock.Setup(x => x.TryGetDocumentRelationships(It.IsAny())).Returns(relatedDocument); + documentTypeRepositoryMock.Setup(x => x.GetById(It.IsAny())).Returns(documentType); + documentServiceMock.Setup(x => x.UploadDocumentAsync(It.IsAny(), true)).ReturnsAsync(documentUploadResponse); + + // Act + var result = await service.Upload(documentQueue); + + // Assert + result.Should().NotBeNull(); + result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.MAYAN_ERROR.ToString()); + documentQueueRepositoryMock.Verify(x => x.Update(It.IsAny(), It.IsAny()), Times.AtLeastOnce); + documentQueueRepositoryMock.Verify(x => x.CommitTransaction(), Times.AtLeastOnce); + documentServiceMock.Verify(x => x.UploadDocumentAsync(It.IsAny(), true), Times.Once); + } + + [Fact] + public void GetById_ValidDocumentQueueId_ReturnsDocumentQueue() + { + // Arrange + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + + var documentQueueId = 1; + var expectedDocumentQueue = new PimsDocumentQueue { DocumentQueueId = documentQueueId }; + + var documentQueueRepositoryMock = this._helper.GetService>(); + documentQueueRepositoryMock.Setup(x => x.TryGetById(documentQueueId)).Returns(expectedDocumentQueue); + + // Act + var result = service.GetById(documentQueueId); + + // Assert + result.Should().NotBeNull(); + result.DocumentQueueId.Should().Be(documentQueueId); + documentQueueRepositoryMock.Verify(x => x.TryGetById(documentQueueId), Times.Once); + } + + [Fact] + public void GetById_InvalidDocumentQueueId_ReturnsNull() + { + // Arrange + var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); + + var documentQueueId = 1; + + var documentQueueRepositoryMock = this._helper.GetService>(); + documentQueueRepositoryMock.Setup(x => x.TryGetById(documentQueueId)).Returns((PimsDocumentQueue)null); + + // Act + Action act = () => service.GetById(documentQueueId); + + // Assert + act.Should().Throw(); + } + } +} diff --git a/source/backend/tests/unit/api/Services/DocumentServiceTest.cs b/source/backend/tests/unit/api/Services/DocumentServiceTest.cs index b43525fab6..ce278d5de5 100644 --- a/source/backend/tests/unit/api/Services/DocumentServiceTest.cs +++ b/source/backend/tests/unit/api/Services/DocumentServiceTest.cs @@ -198,7 +198,6 @@ public async void UploadDocumentAsync_UploadRequest_Sucess() // Assert avService.Verify(x => x.ScanAsync(It.IsAny()), Times.Once); documentStorageRepository.Verify(x => x.TryUploadDocumentAsync(It.IsAny(), It.IsAny()), Times.Once); - documentStorageRepository.Verify(x => x.TryCreateDocumentMetadataAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } [Fact] diff --git a/source/backend/tests/unit/api/Services/FormDocumentServiceTest.cs b/source/backend/tests/unit/api/Services/FormDocumentServiceTest.cs index 7d1abef4b9..034b7b0e7c 100644 --- a/source/backend/tests/unit/api/Services/FormDocumentServiceTest.cs +++ b/source/backend/tests/unit/api/Services/FormDocumentServiceTest.cs @@ -124,13 +124,13 @@ public void UploadFormDocumentTemplateAsync_Success() repository.Setup(x => x.GetByFormTypeCode(testTypeCode)).Returns(new PimsFormType()); var documentService = this._helper.GetService>(); - documentService.Setup(x => x.UploadDocumentAsync(testUploadRequest)); + documentService.Setup(x => x.UploadDocumentAsync(testUploadRequest, true)); // Act var result = service.UploadFormDocumentTemplateAsync(testTypeCode, testUploadRequest); // Assert - documentService.Verify(x => x.UploadDocumentAsync(testUploadRequest), Times.Once); + documentService.Verify(x => x.UploadDocumentSync(testUploadRequest), Times.Once); } [Fact] @@ -142,14 +142,14 @@ public void UploadFormDocumentTemplateAsync_NoPermission() DocumentUploadRequest testUploadRequest = new DocumentUploadRequest(); var documentService = this._helper.GetService>(); - documentService.Setup(x => x.UploadDocumentAsync(testUploadRequest)); + documentService.Setup(x => x.UploadDocumentAsync(testUploadRequest, true)); // Act Func act = () => service.UploadFormDocumentTemplateAsync(testTypeCode, testUploadRequest); // Assert act.Should().ThrowAsync(); - documentService.Verify(x => x.UploadDocumentAsync(testUploadRequest), Times.Never); + documentService.Verify(x => x.UploadDocumentAsync(testUploadRequest, true), Times.Never); } [Fact] @@ -166,7 +166,7 @@ public void UploadFormDocumentTemplateAsync_DeleteIfPrevious() formTypeRepositoryMock.Setup(x => x.GetByFormTypeCode(testTypeCode)).Returns(new PimsFormType() { DocumentId = testDocumentId, Document = testExistingDocument }); var documentServiceMock = this._helper.GetService>(); - documentServiceMock.Setup(x => x.UploadDocumentAsync(testUploadRequest)); + documentServiceMock.Setup(x => x.UploadDocumentAsync(testUploadRequest, true)); var documentRepositoryMock = this._helper.GetService>(); documentRepositoryMock.Setup(x => x.DocumentRelationshipCount(testDocumentId)).Returns(1); @@ -178,7 +178,7 @@ public void UploadFormDocumentTemplateAsync_DeleteIfPrevious() // Assert documentRepositoryMock.Verify(x => x.DocumentRelationshipCount(testDocumentId), Times.Once); documentServiceMock.Verify(x => x.DeleteDocumentAsync(testExistingDocument), Times.Once); - documentServiceMock.Verify(x => x.UploadDocumentAsync(testUploadRequest), Times.Once); + documentServiceMock.Verify(x => x.UploadDocumentSync(testUploadRequest), Times.Once); } [Fact] @@ -195,7 +195,7 @@ public void UploadFormDocumentTemplateAsync_DeleteIfPrevious_Error() formTypeRepositoryMock.Setup(x => x.GetByFormTypeCode(testTypeCode)).Returns(new PimsFormType() { DocumentId = testDocumentId, Document = testExistingDocument }); var documentServiceMock = this._helper.GetService>(); - documentServiceMock.Setup(x => x.UploadDocumentAsync(testUploadRequest)); + documentServiceMock.Setup(x => x.UploadDocumentAsync(testUploadRequest, true)); var documentRepositoryMock = this._helper.GetService>(); documentRepositoryMock.Setup(x => x.DocumentRelationshipCount(testDocumentId)).Returns(1); @@ -208,7 +208,7 @@ public void UploadFormDocumentTemplateAsync_DeleteIfPrevious_Error() act.Should().ThrowAsync(); documentRepositoryMock.Verify(x => x.DocumentRelationshipCount(testDocumentId), Times.Once); documentServiceMock.Verify(x => x.DeleteDocumentAsync(testExistingDocument), Times.Once); - documentServiceMock.Verify(x => x.UploadDocumentAsync(testUploadRequest), Times.Never); + documentServiceMock.Verify(x => x.UploadDocumentAsync(testUploadRequest, true), Times.Never); } #endregion @@ -226,7 +226,7 @@ public void DeleteFormDocumentTemplateAsync_Success() DocumentUploadRequest testUploadRequest = new DocumentUploadRequest(); var documentServiceMock = this._helper.GetService>(); - documentServiceMock.Setup(x => x.UploadDocumentAsync(testUploadRequest)); + documentServiceMock.Setup(x => x.UploadDocumentAsync(testUploadRequest, true)); var documentRepositoryMock = this._helper.GetService>(); documentRepositoryMock.Setup(x => x.DocumentRelationshipCount(testDocumentId)).Returns(1); diff --git a/source/backend/tests/unit/dal/Repositories/DocumentQueueRepositoryTest.cs b/source/backend/tests/unit/dal/Repositories/DocumentQueueRepositoryTest.cs new file mode 100644 index 0000000000..05076834e0 --- /dev/null +++ b/source/backend/tests/unit/dal/Repositories/DocumentQueueRepositoryTest.cs @@ -0,0 +1,253 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using FluentAssertions; +using Pims.Core.Test; +using Pims.Dal.Entities; +using Pims.Dal.Exceptions; +using Pims.Dal.Repositories; +using Pims.Core.Security; +using Xunit; +using Pims.Core.Exceptions; +using Pims.Dal.Entities.Models; +using System.Security; +using Pims.Api.Models.CodeTypes; + +namespace Pims.Dal.Test.Repositories +{ + [Trait("category", "unit")] + [Trait("category", "dal")] + [Trait("group", "documentQueues")] + [ExcludeFromCodeCoverage] + public class DocumentQueueRepositoryTest + { + #region Constructors + public DocumentQueueRepositoryTest() { } + #endregion + + #region Tests + + #region TryGetById + [Fact] + public void TryGetById_Success() + { + // Arrange + var helper = new TestHelper(); + var user = PrincipalHelper.CreateForPermission(Permissions.DocumentView); + + var documentQueue = EntityHelper.CreateDocumentQueue(); + + var context = helper.CreatePimsContext(user, true).AddAndSaveChanges(documentQueue); + var repository = helper.CreateRepository(user); + + // Act + var result = repository.TryGetById(1); + + // Assert + result.Should().NotBeNull(); + result.Should().BeAssignableTo(); + } + #endregion + + #region Update + [Fact] + public void Update_Success() + { + // Arrange + var helper = new TestHelper(); + var user = PrincipalHelper.CreateForPermission(Permissions.DocumentEdit); + + var documentQueue = EntityHelper.CreateDocumentQueue(); + + var context = helper.CreatePimsContext(user, true).AddAndSaveChanges(documentQueue); + var repository = helper.CreateRepository(user); + + // Act + documentQueue.DocumentQueueStatusTypeCode = "Updated"; + var result = repository.Update(documentQueue); + context.CommitTransaction(); + + // Assert + context.PimsDocumentQueues.Should().HaveCount(1); + context.PimsDocumentQueues.FirstOrDefault().Document.Should().NotBeNull(); + context.PimsDocumentQueues.FirstOrDefault().DocumentQueueStatusTypeCode.Should().Be("Updated"); + } + + [Fact] + public void Update_Success_RemoveDocument() + { + // Arrange + var helper = new TestHelper(); + var user = PrincipalHelper.CreateForPermission(Permissions.DocumentEdit); + + var documentQueue = EntityHelper.CreateDocumentQueue(); + + var context = helper.CreatePimsContext(user, true).AddAndSaveChanges(documentQueue); + var repository = helper.CreateRepository(user); + + // Act + documentQueue.DocumentQueueStatusTypeCode = "Updated"; + documentQueue.Document = null; + var result = repository.Update(documentQueue, removeDocument: true); + context.CommitTransaction(); + + // Assert + context.PimsDocumentQueues.Should().HaveCount(1); + context.PimsDocumentQueues.FirstOrDefault().Document.Should().BeNull(); + context.PimsDocumentQueues.FirstOrDefault().DocumentQueueStatusTypeCode.Should().Be("Updated"); + } + + [Fact] + public void Update_Null() + { + // Arrange + var helper = new TestHelper(); + var user = PrincipalHelper.CreateForPermission(Permissions.DocumentView); + + var repository = helper.CreateRepository(user); + + // Act + Action act = () => repository.Update(null); + + // Assert + act.Should().Throw(); + } + #endregion + + #region Delete + [Fact] + public void Delete_Success() + { + // Arrange + var helper = new TestHelper(); + var user = PrincipalHelper.CreateForPermission(Permissions.DocumentView); + + var documentQueue = EntityHelper.CreateDocumentQueue(); + + var context = helper.CreatePimsContext(user, true).AddAndSaveChanges(documentQueue); + var repository = helper.CreateRepository(user); + + // Act + context.ChangeTracker.Clear(); + var result = repository.Delete(documentQueue); + context.CommitTransaction(); + + // Assert + context.PimsDocumentQueues.Should().BeEmpty(); + } + + [Fact] + public void Delete_Null() + { + // Arrange + var helper = new TestHelper(); + var user = PrincipalHelper.CreateForPermission(Permissions.DocumentView); + + var repository = helper.CreateRepository(user); + + // Act + Action act = () => repository.Delete(null); + + // Assert + act.Should().Throw(); + } + #endregion + + #region GetAllByFilter + [Theory] + [InlineData(2, null, 2, null, null, null, null, new int[] { 1, 2 })] + [InlineData(1, null, 1, null, null, null, null, new int[] { 1 })] + [InlineData(1, DocumentQueueStatusTypes.PENDING, 1, null, null, null, null, new int[] { 1 })] + [InlineData(1, DocumentQueueStatusTypes.SUCCESS, 1, null, null, null, null, new int[] { 2 })] + [InlineData(1, DocumentQueueStatusTypes.PROCESSING, 0, null, null, null, null, null)] + [InlineData(0, null, 0, null, null, null, null, null)] + [InlineData(1, null, 1, "2023-01-01", null, null, null, new int[] { 2 })] + [InlineData(1, null, 1, null, "2023-12-31", null, null, new int[] { 1 })] + [InlineData(1, null, 1, null, null, 3, null, new int[] { 1 })] + [InlineData(1, null, 1, null, null, null, "PAIMS", new int[] { 1 })] + public void GetAllByFilter_Success(int quantity, string status, int expectedCount, string startDate, string endDate, int? maxRetries, string dataSourceTypeCode, int[] expectedDocumentQueueIds) + { + // Arrange + var helper = new TestHelper(); + var user = PrincipalHelper.CreateForPermission(Permissions.DocumentView); + + var documentQueue1 = EntityHelper.CreateDocumentQueue(1, DocumentQueueStatusTypes.PENDING.ToString(), "PAIMS"); + documentQueue1.DocProcessStartDt = DateTime.Parse("2022-01-01"); + documentQueue1.DocProcessEndDt = DateTime.Parse("2023-12-31"); + + var documentQueue2 = EntityHelper.CreateDocumentQueue(2, DocumentQueueStatusTypes.SUCCESS.ToString()); + documentQueue2.DocProcessStartDt = DateTime.Parse("2023-01-01"); + documentQueue2.DocProcessEndDt = DateTime.Parse("2024-12-31"); + documentQueue2.DocProcessRetries = 4; + + var context = helper.CreatePimsContext(user, true).AddAndSaveChanges(documentQueue1, documentQueue2); + var repository = helper.CreateRepository(user); + + var filter = new DocumentQueueFilter + { + Quantity = quantity, + DocumentQueueStatusTypeCodes = status != null ? new string[] { status } : null, + DocProcessStartDate = startDate != null ? DateTime.Parse(startDate) : (DateTime?)null, + DocProcessEndDate = endDate != null ? DateTime.Parse(endDate) : (DateTime?)null, + MaxDocProcessRetries = maxRetries, + DataSourceTypeCode = dataSourceTypeCode + }; + + // Act + var result = repository.GetAllByFilter(filter); + + // Assert + result.Should().HaveCount(expectedCount); + if (expectedCount > 0) + { + result.Should().OnlyContain(dq => expectedDocumentQueueIds.Any(edq => edq == dq.DocumentQueueId)); + } + } + + [Fact] + public void GetAllByFilter_NoResults() + { + // Arrange + var helper = new TestHelper(); + var user = PrincipalHelper.CreateForPermission(Permissions.DocumentView); + + var context = helper.CreatePimsContext(user, true); + var repository = helper.CreateRepository(user); + + var filter = new DocumentQueueFilter { Quantity = 1 }; + + // Act + var result = repository.GetAllByFilter(filter); + + // Assert + result.Should().BeEmpty(); + } + #endregion + + #region DocumentQueueCount + [Fact] + public void DocumentQueueCount_Success() + { + // Arrange + var helper = new TestHelper(); + var user = PrincipalHelper.CreateForPermission(Permissions.DocumentView); + + var documentQueue = EntityHelper.CreateDocumentQueue(); + + var context = helper.CreatePimsContext(user, true).AddAndSaveChanges(documentQueue); + var repository = helper.CreateRepository(user); + + var statusType = new PimsDocumentQueueStatusType { DocumentQueueStatusTypeCode = documentQueue.DocumentQueueStatusTypeCode }; + + // Act + var result = repository.DocumentQueueCount(statusType); + + // Assert + result.Should().Be(1); + } + #endregion + + #endregion + } +} diff --git a/source/frontend/package.json b/source/frontend/package.json index 5d1e23ae9d..de2d3551fc 100644 --- a/source/frontend/package.json +++ b/source/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "5.7.0-96.7", + "version": "5.7.0-96.11", "private": true, "dependencies": { "@bcgov/bc-sans": "1.0.1", diff --git a/source/frontend/src/components/common/buttons/RefreshButton.tsx b/source/frontend/src/components/common/buttons/RefreshButton.tsx new file mode 100644 index 0000000000..8dbecf937b --- /dev/null +++ b/source/frontend/src/components/common/buttons/RefreshButton.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { TbRefresh } from 'react-icons/tb'; + +import TooltipWrapper from '../TooltipWrapper'; +import { Button, ButtonProps } from './Button'; + +interface IRefreshButtonProps extends ButtonProps { + /** set the text of the tooltip that appears on hover of the plus button */ + toolText: string; + /** set the id of the tool tip use for on hover of the plus buttons */ + toolId: string; + /** set the refresh button id */ + refreshButtonId?: string | null; + /** set the refresh button id */ + refreshButtonTestId?: string | null; + /** set the refresh button id */ + refreshButtonTitle?: string | null; +} + +/** + * Button displaying a refresh/recycle icon, used to reset form data. + * @param param0 + */ +const RefreshButton: React.FC> = ({ + toolId, + toolText, + refreshButtonId, + refreshButtonTestId, + refreshButtonTitle, + ...props +}) => { + const refreshButtonIdValue = refreshButtonId ?? 'refresh-button'; + const refreshButtonTestIdValue = refreshButtonTestId ?? 'refresh-button'; + const refreshButtonTitleValue = refreshButtonTitle ?? 'refresh-button'; + + return ( + + + + ); +}; + +export default RefreshButton; diff --git a/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx b/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx index 80614f022e..b7b267d0b2 100644 --- a/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx +++ b/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx @@ -3,6 +3,7 @@ import { dequal } from 'dequal'; import { LatLngBounds, LatLngLiteral } from 'leaflet'; import React, { useCallback, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; +import { AnyEventObject } from 'xstate'; import { PropertyFilterFormModel } from '@/components/maps/leaflet/Control/AdvancedFilter/models'; import { ILayerItem } from '@/components/maps/leaflet/Control/LayersControl/types'; @@ -13,7 +14,7 @@ import { IPropertyFilter, } from '@/features/properties/filter/IPropertyFilter'; import { exists } from '@/utils'; -import { pidParser } from '@/utils/propertyUtils'; +import { pidParser, pinParser } from '@/utils/propertyUtils'; import { mapMachine } from './machineDefinition/mapMachine'; import { MachineContext, SideBarType } from './machineDefinition/types'; @@ -120,35 +121,48 @@ export const MapStateMachineProvider: React.FC> actions: { navigateToProperty: context => { const selectedFeatureData = context.mapLocationFeatureDataset; - if (selectedFeatureData?.pimsFeature?.properties?.PROPERTY_ID) { + if (exists(selectedFeatureData?.pimsFeature?.properties?.PROPERTY_ID)) { const pimsFeature = selectedFeatureData.pimsFeature; history.push(`/mapview/sidebar/property/${pimsFeature.properties.PROPERTY_ID}`); - } else if (selectedFeatureData?.parcelFeature?.properties?.PID) { - const parcelFeature = selectedFeatureData.parcelFeature; + } else if (exists(selectedFeatureData?.parcelFeature?.properties?.PID)) { + const parcelFeature = selectedFeatureData?.parcelFeature; const parsedPid = pidParser(parcelFeature.properties.PID); - history.push(`/mapview/sidebar/non-inventory-property/${parsedPid}`); + history.push(`/mapview/sidebar/non-inventory-property/pid/${parsedPid}`); + } else if (exists(selectedFeatureData?.parcelFeature?.properties?.PIN)) { + const parcelFeature = selectedFeatureData?.parcelFeature; + const parsedPin = pinParser(parcelFeature.properties.PIN); + history.push(`/mapview/sidebar/non-inventory-property/pin/${parsedPin}`); } }, }, services: { - loadLocationData: (context: MachineContext, event: any) => { - const result = locationLoader.loadLocationDetails( - event.type === 'MAP_CLICK' ? event.latlng : event.featureSelected.latlng, - ); - - if (event.type === 'MAP_MARKER_CLICK') { + loadLocationData: async ( + context: MachineContext, + event: + | (AnyEventObject & { type: 'MAP_CLICK'; latlng: LatLngLiteral }) + | (AnyEventObject & { type: 'MAP_MARKER_CLICK'; featureSelected: FeatureSelected }), + ) => { + let result: LocationFeatureDataset | undefined = undefined; + + if (event.type === 'MAP_CLICK') { + result = await locationLoader.loadLocationDetails({ latLng: event.latlng }); + } else if (event.type === 'MAP_MARKER_CLICK') { + result = await locationLoader.loadLocationDetails({ + latLng: event.featureSelected.latlng, + pimsPropertyId: event.featureSelected?.pimsLocationFeature?.PROPERTY_ID ?? null, + }); // In the case of the map marker being clicked, we must use the search result properties, as the minimal layer does not have the necessary feature data. // However, use the coordinates of the clicked marker. - result.then(data => { - data.pimsFeature = { - properties: { ...data.pimsFeature.properties }, + if (exists(result.pimsFeature)) { + result.pimsFeature = { + properties: { ...result.pimsFeature?.properties }, type: 'Feature', geometry: { type: 'Point', coordinates: [event.featureSelected.latlng.lng, event.featureSelected.latlng.lat], }, }; - }); + } } return result; diff --git a/source/frontend/src/components/common/mapFSM/useLocationFeatureLoader.tsx b/source/frontend/src/components/common/mapFSM/useLocationFeatureLoader.tsx index 5982ff160a..57fdadfc32 100644 --- a/source/frontend/src/components/common/mapFSM/useLocationFeatureLoader.tsx +++ b/source/frontend/src/components/common/mapFSM/useLocationFeatureLoader.tsx @@ -22,6 +22,7 @@ import { WHSE_Municipalities_Feature_Properties } from '@/models/layers/municipa import { PMBC_FullyAttributed_Feature_Properties } from '@/models/layers/parcelMapBC'; import { ISS_ProvincialPublicHighway } from '@/models/layers/pimsHighwayLayer'; import { PIMS_Property_Location_View } from '@/models/layers/pimsPropertyLocationView'; +import { exists, isValidId } from '@/utils'; export interface LocationFeatureDataset { selectingComponentId: string | null; @@ -72,7 +73,13 @@ const useLocationFeatureLoader = () => { const crownLandLayerServiceFindOneInventory = crownLandLayerService.findOneCrownLandInventory; const loadLocationDetails = useCallback( - async (latLng: LatLngLiteral): Promise => { + async ({ + latLng, + pimsPropertyId, + }: { + latLng: LatLngLiteral; + pimsPropertyId?: number | null; + }): Promise => { const result: LocationFeatureDataset = { selectingComponentId: null, location: latLng, @@ -100,6 +107,7 @@ const useLocationFeatureLoader = () => { const crownLandTenuresTask = crownLandLayerServiceFindOneTenure(latLng); const crownLandInventoryTask = crownLandLayerServiceFindOneInventory(latLng); const crownLandInclusionsTask = crownLandLayerServiceFindOneInclusion(latLng); + const municipalityFeatureTask = adminLegalBoundaryLayerServiceFindOneMunicipality(latLng); const [ parcelFeature, @@ -111,6 +119,7 @@ const useLocationFeatureLoader = () => { crownLandTenuresFeature, crownLandInventoryFeature, crownLandInclusionsFeature, + municipalityFeature, ] = await Promise.all([ fullyAttributedTask, regionTask, @@ -121,6 +130,7 @@ const useLocationFeatureLoader = () => { crownLandTenuresTask, crownLandInventoryTask, crownLandInclusionsTask, + municipalityFeatureTask, ]); let pimsLocationProperties: @@ -128,24 +138,25 @@ const useLocationFeatureLoader = () => { | undefined = undefined; // Load PimsProperties - if (latLng) { - const latLngFeature = await findOneByBoundary(latLng, 'GEOMETRY', 4326); - if (latLngFeature !== undefined) { - pimsLocationProperties = { features: [latLngFeature], type: 'FeatureCollection' }; + // - first attempt to find it by our internal PIMS id + // - then try to find it on our boundary layer. + // - if not found by boundary attempt to match it by PID / PIN coming from parcel-map + const isInPims = isValidId(Number(pimsPropertyId)); + if (isInPims) { + pimsLocationProperties = await loadProperties({ PROPERTY_ID: Number(pimsPropertyId) }); + } else { + const boundaryFeature = await findOneByBoundary(latLng, 'GEOMETRY', 4326); + if (exists(boundaryFeature)) { + pimsLocationProperties = { features: [boundaryFeature], type: 'FeatureCollection' }; + } else if (exists(parcelFeature)) { + pimsLocationProperties = await loadProperties({ + PID: parcelFeature.properties?.PID || '', + PIN: parcelFeature.properties?.PIN?.toString() || '', + }); } - } else if (parcelFeature !== undefined) { - pimsLocationProperties = await loadProperties({ - PID: parcelFeature.properties?.PID || '', - PIN: parcelFeature.properties?.PIN?.toString() || '', - }); } - const municipalityFeature = await adminLegalBoundaryLayerServiceFindOneMunicipality(latLng); - - if ( - pimsLocationProperties?.features !== undefined && - pimsLocationProperties.features.length > 0 - ) { + if (exists(pimsLocationProperties?.features) && pimsLocationProperties.features.length > 0) { result.pimsFeature = pimsLocationProperties.features[0] ?? null; } diff --git a/source/frontend/src/components/maps/leaflet/LayerPopup/LayerPopupView.test.tsx b/source/frontend/src/components/maps/leaflet/LayerPopup/LayerPopupView.test.tsx index d5de3554c9..e6d2ba1560 100644 --- a/source/frontend/src/components/maps/leaflet/LayerPopup/LayerPopupView.test.tsx +++ b/source/frontend/src/components/maps/leaflet/LayerPopup/LayerPopupView.test.tsx @@ -1,19 +1,14 @@ import { createMemoryHistory } from 'history'; -import { useMap } from 'react-leaflet'; -import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; import Claims from '@/constants/claims'; import { mockLookups } from '@/mocks/lookups.mock'; -import { mapMachineBaseMock } from '@/mocks/mapFSM.mock'; import { emptyPmbcParcel } from '@/models/layers/parcelMapBC'; import { EmptyPropertyLocation } from '@/models/layers/pimsPropertyLocationView'; import { lookupCodesSlice } from '@/store/slices/lookupCodes'; -import { pidParser } from '@/utils/propertyUtils'; +import { pidParser, pinParser } from '@/utils/propertyUtils'; import { act, render, RenderOptions, userEvent } from '@/utils/test-utils'; import { ILayerPopupViewProps, LayerPopupView } from './LayerPopupView'; -import { emptyPimsBoundaryFeatureCollection } from '@/components/common/mapFSM/models'; -import { useKeycloak } from '@react-keycloak/web'; vi.mock('react-leaflet'); @@ -54,6 +49,7 @@ describe('LayerPopupView component', () => { }); expect(asFragment()).toMatchSnapshot(); }); + describe('fly out behaviour', () => { it('fly out is hidden by default', async () => { const { queryByText } = setup({ @@ -122,7 +118,7 @@ describe('LayerPopupView component', () => { expect(history.location.pathname).toBe(`/mapview/sidebar/property/${propertyId}`); }); - it('handles view property action for non-inventory properties', async () => { + it('handles view property action for non-inventory properties - with PID', async () => { const pid = '123456789'; const parsedPid = pidParser(pid); const { getByTestId, getByText } = setup({ @@ -162,7 +158,51 @@ describe('LayerPopupView component', () => { const link = getByText('View Property Info'); await act(async () => userEvent.click(link)); expect(history.location.pathname).toBe( - `/mapview/sidebar/non-inventory-property/${parsedPid}`, + `/mapview/sidebar/non-inventory-property/pid/${parsedPid}`, + ); + }); + + it('handles view property action for non-inventory properties - with PIN', async () => { + const pin = '123456789'; + const parsedPin = pinParser(pin); + const { getByTestId, getByText } = setup({ + layerPopup: { + layers: [ + { + data: { PIN: pin }, + title: '', + config: {}, + }, + ], + latlng: undefined, + }, + featureDataset: { + parcelFeature: { + type: 'Feature', + properties: { ...emptyPmbcParcel, PIN: parsedPin }, + geometry: { type: 'Point', coordinates: [] }, + }, + location: { lat: 0, lng: 0 }, + fileLocation: null, + pimsFeature: null, + regionFeature: null, + districtFeature: null, + municipalityFeature: null, + highwayFeature: null, + selectingComponentId: null, + crownLandLeasesFeature: null, + crownLandLicensesFeature: null, + crownLandTenuresFeature: null, + crownLandInventoryFeature: null, + crownLandInclusionsFeature: null, + }, + }); + const ellipsis = getByTestId('fly-out-ellipsis'); + await act(async () => userEvent.click(ellipsis)); + const link = getByText('View Property Info'); + await act(async () => userEvent.click(link)); + expect(history.location.pathname).toBe( + `/mapview/sidebar/non-inventory-property/pin/${parsedPin}`, ); }); @@ -440,5 +480,39 @@ describe('LayerPopupView component', () => { await act(async () => userEvent.click(link)); expect(history.location.pathname).toBe('/mapview/sidebar/consolidation/new'); }); + + it('handles create lease and licence file action', async () => { + const { getByTestId, getByText } = setup({ + layerPopup: { + latlng: undefined, + layers: [], + }, + featureDataset: null, + + claims: [Claims.LEASE_ADD], + }); + const ellipsis = getByTestId('fly-out-ellipsis'); + await act(async () => userEvent.click(ellipsis)); + const link = getByText('Lease/Licence File'); + await act(async () => userEvent.click(link)); + expect(history.location.pathname).toBe('/mapview/sidebar/lease/new'); + }); + + it('handles create disposition file action', async () => { + const { getByTestId, getByText } = setup({ + layerPopup: { + latlng: undefined, + layers: [], + }, + featureDataset: null, + + claims: [Claims.DISPOSITION_ADD], + }); + const ellipsis = getByTestId('fly-out-ellipsis'); + await act(async () => userEvent.click(ellipsis)); + const link = getByText('Disposition File'); + await act(async () => userEvent.click(link)); + expect(history.location.pathname).toBe('/mapview/sidebar/disposition/new'); + }); }); }); diff --git a/source/frontend/src/components/maps/leaflet/LayerPopup/LayerPopupView.tsx b/source/frontend/src/components/maps/leaflet/LayerPopup/LayerPopupView.tsx index 5bc6e0f0ee..5dc9ff8101 100644 --- a/source/frontend/src/components/maps/leaflet/LayerPopup/LayerPopupView.tsx +++ b/source/frontend/src/components/maps/leaflet/LayerPopup/LayerPopupView.tsx @@ -10,7 +10,7 @@ import { LocationFeatureDataset } from '@/components/common/mapFSM/useLocationFe import SimplePagination from '@/components/common/SimplePagination'; import TooltipWrapper from '@/components/common/TooltipWrapper'; import { Scrollable } from '@/features/projects/styles'; -import { exists, isValidId, pidParser } from '@/utils'; +import { exists, isValidId, pidParser, pinParser } from '@/utils'; import { LayerPopupContent } from './components/LayerPopupContent'; import { LayerPopupFlyout } from './components/LayerPopupFlyout'; @@ -45,14 +45,19 @@ export const LayerPopupView: React.FC | undefined; + queueStatusTypeCode: ApiGen_Base_CodeType | null; fileName: string | undefined; isFileAvailable: boolean | undefined; appCreateTimestamp?: UtcIsoDateTime; @@ -39,7 +40,8 @@ export class DocumentRow { const row: DocumentRow = new DocumentRow(); row.id = relationship.document?.id; row.documentType = relationship.document?.documentType ?? undefined; - row.mayanDocumentId = relationship.document?.mayanDocumentId ?? undefined; + row.mayanDocumentId = relationship.document?.mayanDocumentId; + row.queueStatusTypeCode = relationship.document?.documentQueueStatusTypeCode; row.statusTypeCode = relationship.document?.statusTypeCode ?? undefined; row.fileName = relationship.document?.fileName ?? undefined; row.appCreateTimestamp = relationship.document?.appCreateTimestamp; @@ -53,10 +55,12 @@ export class DocumentRow { public static fromApiDocument(document: ApiGen_Concepts_Document): DocumentRow { const row: DocumentRow = new DocumentRow(); + row.id = document?.id; row.documentType = document?.documentType ?? undefined; row.mayanDocumentId = document?.mayanDocumentId ?? undefined; row.statusTypeCode = document?.statusTypeCode ?? undefined; + row.queueStatusTypeCode = document?.documentQueueStatusTypeCode; row.fileName = document?.fileName ?? undefined; row.appCreateTimestamp = document?.appCreateTimestamp; row.appCreateUserid = document?.appCreateUserid ?? undefined; @@ -90,6 +94,7 @@ export class DocumentRow { appCreateUserid: null, appLastUpdateUserGuid: null, appCreateUserGuid: null, + documentQueueStatusTypeCode: null, }, rowVersion: 0, appCreateTimestamp: EpochIsoDateTime, @@ -109,7 +114,6 @@ export class BatchUploadFormModel { export class BatchUploadResponseModel { public readonly fileName: string; public readonly isSuccess: boolean; - public readonly response: ApiGen_Requests_DocumentUploadRelationshipResponse | null; public readonly errorMessage: string; constructor( @@ -120,18 +124,12 @@ export class BatchUploadResponseModel { if (exists(apiResponse)) { if (isApiError(apiResponse)) { this.isSuccess = false; - this.response = null; this.errorMessage = (apiResponse as IApiError).details; } else { - this.isSuccess = - apiResponse.uploadResponse?.documentExternalResponse?.status === 'Success' ? true : false; - this.response = apiResponse; - this.errorMessage = - apiResponse.uploadResponse?.documentExternalResponse.message ?? 'Mayan error'; + this.isSuccess = true; } } else { this.isSuccess = false; - this.response = null; this.errorMessage = 'Network error, please try again or contact your system administrator'; } } diff --git a/source/frontend/src/features/documents/documentDetail/DocumentDetailForm.test.tsx b/source/frontend/src/features/documents/documentDetail/DocumentDetailForm.test.tsx index 6dfe6eff72..2495b5722c 100644 --- a/source/frontend/src/features/documents/documentDetail/DocumentDetailForm.test.tsx +++ b/source/frontend/src/features/documents/documentDetail/DocumentDetailForm.test.tsx @@ -141,6 +141,7 @@ const mockDocument: ComposedDocument = { appCreateUserid: null, appLastUpdateUserGuid: null, appCreateUserGuid: null, + documentQueueStatusTypeCode: null, }, parentId: null, relationshipType: ApiGen_CodeTypes_DocumentRelationType.AcquisitionFiles, diff --git a/source/frontend/src/features/documents/documentDetail/DocumentDetailView.test.tsx b/source/frontend/src/features/documents/documentDetail/DocumentDetailView.test.tsx index 2079c89dc0..476fb98eee 100644 --- a/source/frontend/src/features/documents/documentDetail/DocumentDetailView.test.tsx +++ b/source/frontend/src/features/documents/documentDetail/DocumentDetailView.test.tsx @@ -90,6 +90,7 @@ const mockDocument: ComposedDocument = { document: { id: 1, mayanDocumentId: 15, + documentQueueStatusTypeCode: null, documentType: mockDocumentTypesResponse()[0], statusTypeCode: { id: 'AMEND', diff --git a/source/frontend/src/features/documents/hooks/useDocumentRelationshipProvider.ts b/source/frontend/src/features/documents/hooks/useDocumentRelationshipProvider.ts index 8531bf9ba3..5400f96d9d 100644 --- a/source/frontend/src/features/documents/hooks/useDocumentRelationshipProvider.ts +++ b/source/frontend/src/features/documents/hooks/useDocumentRelationshipProvider.ts @@ -7,7 +7,6 @@ import { useApiRequestWrapper } from '@/hooks/util/useApiRequestWrapper'; import { IApiError } from '@/interfaces/IApiError'; import { ApiGen_CodeTypes_DocumentRelationType } from '@/models/api/generated/ApiGen_CodeTypes_DocumentRelationType'; import { ApiGen_Concepts_DocumentRelationship } from '@/models/api/generated/ApiGen_Concepts_DocumentRelationship'; -import { ApiGen_Requests_DocumentUploadRelationshipResponse } from '@/models/api/generated/ApiGen_Requests_DocumentUploadRelationshipResponse'; import { ApiGen_Requests_DocumentUploadRequest } from '@/models/api/generated/ApiGen_Requests_DocumentUploadRequest'; /** @@ -85,7 +84,7 @@ export const useDocumentRelationshipProvider = () => { relationshipType: ApiGen_CodeTypes_DocumentRelationType, parentId: string, uploadRequest: ApiGen_Requests_DocumentUploadRequest, - ) => Promise> + ) => Promise >({ requestFunction: useCallback( async ( diff --git a/source/frontend/src/features/documents/list/DocumentListContainer.tsx b/source/frontend/src/features/documents/list/DocumentListContainer.tsx index b0d187f0a2..4083ca6671 100644 --- a/source/frontend/src/features/documents/list/DocumentListContainer.tsx +++ b/source/frontend/src/features/documents/list/DocumentListContainer.tsx @@ -74,6 +74,10 @@ const DocumentListContainer: React.FunctionComponent { + retrieveDocuments(); + }; + return ( diff --git a/source/frontend/src/features/documents/list/DocumentListView.test.tsx b/source/frontend/src/features/documents/list/DocumentListView.test.tsx index a1501ba47a..8243ef250b 100644 --- a/source/frontend/src/features/documents/list/DocumentListView.test.tsx +++ b/source/frontend/src/features/documents/list/DocumentListView.test.tsx @@ -30,6 +30,7 @@ const storeState = { const onDelete = vi.fn().mockResolvedValue(true); const onSuccess = vi.fn(); +const onRefresh = vi.fn(); const mockDocumentRowResponse = () => mockDocumentsResponse().map(x => (x?.document ? DocumentRow.fromApi(x) : new DocumentRow())); @@ -52,6 +53,7 @@ describe('Document List View', () => { } onDelete={renderOptions?.onDelete || onDelete} onSuccess={renderOptions?.onSuccess || onSuccess} + onRefresh={renderOptions?.onRefresh || onRefresh} />, { ...renderOptions, @@ -115,7 +117,7 @@ describe('Document List View', () => { expect(getByTestId('document-filename')).toBeInTheDocument(); }); - it('should have the Documents add button in the component', async () => { + it('should have the Documents "Add button" in the component', async () => { const { getByText } = setup({ hideFilters: false, isLoading: false, @@ -127,6 +129,18 @@ describe('Document List View', () => { expect(getByText('Add Document')).toBeInTheDocument(); }); + it('should have the Documents "Refresh button" in the component', async () => { + const { getByTestId } = setup({ + hideFilters: false, + isLoading: false, + parentId: '1', + relationshipType: ApiGen_CodeTypes_DocumentRelationType.ResearchFiles, + documentResults: mockDocumentRowResponse(), + claims: [Claims.DOCUMENT_ADD, Claims.DOCUMENT_DELETE, Claims.DOCUMENT_VIEW], + }); + expect(getByTestId('refresh-button')).toBeInTheDocument(); + }); + it('should not display the download icon on the listview', async () => { mockAxios.onGet().reply(200, []); const documentRows = mockDocumentRowResponse(); diff --git a/source/frontend/src/features/documents/list/DocumentListView.tsx b/source/frontend/src/features/documents/list/DocumentListView.tsx index 9b085b3112..83cfd760ee 100644 --- a/source/frontend/src/features/documents/list/DocumentListView.tsx +++ b/source/frontend/src/features/documents/list/DocumentListView.tsx @@ -1,13 +1,17 @@ import orderBy from 'lodash/orderBy'; import React, { useContext, useEffect, useMemo, useState } from 'react'; +import { Col, Row } from 'react-bootstrap'; import { FaPlus } from 'react-icons/fa'; +import styled from 'styled-components'; +import RefreshButton from '@/components/common/buttons/RefreshButton'; import GenericModal from '@/components/common/GenericModal'; import { Section } from '@/components/common/Section/Section'; -import { SectionListHeader } from '@/components/common/SectionListHeader'; +import { InlineFlexDiv, StyledSectionAddButton } from '@/components/common/styles'; import { TableSort } from '@/components/Table/TableSort'; import Claims from '@/constants/claims'; import { DocumentTypeName } from '@/constants/documentType'; +import { useKeycloakWrapper } from '@/hooks/useKeycloakWrapper'; import { defaultDocumentFilter, IDocumentFilter } from '@/interfaces/IDocumentResults'; import { ApiGen_CodeTypes_DocumentRelationType } from '@/models/api/generated/ApiGen_CodeTypes_DocumentRelationType'; import { ApiGen_Concepts_Document } from '@/models/api/generated/ApiGen_Concepts_Document'; @@ -30,10 +34,11 @@ export interface IDocumentListViewProps { hideFilters?: boolean; defaultFilters?: IDocumentFilter; addButtonText?: string; - onDelete: (relationship: ApiGen_Concepts_DocumentRelationship) => Promise; - onSuccess: () => void; disableAdd?: boolean; title?: string; + onDelete: (relationship: ApiGen_Concepts_DocumentRelationship) => Promise; + onSuccess: () => void; + onRefresh: () => void; } /** * Page that displays document information as a list. @@ -41,6 +46,8 @@ export interface IDocumentListViewProps { export const DocumentListView: React.FunctionComponent = ( props: IDocumentListViewProps, ) => { + const { hasClaim } = useKeycloakWrapper(); + const { documentResults, isLoading, defaultFilters, hideFilters, title } = props; const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); @@ -172,20 +179,41 @@ export const DocumentListView: React.FunctionComponent = props.onSuccess(); }; - const getHeader = () => { + const getHeader = (): React.ReactNode => { if (props.disableAdd === true) { return title ?? 'Documents'; } + return ( - } - addButtonText={props.addButtonText || 'Add Document'} - onAdd={() => setIsUploadVisible(true)} - /> + <> + + + {title ?? 'Documents'} + + + + {hasClaim([Claims.DOCUMENT_ADD]) && ( + setIsUploadVisible && setIsUploadVisible(true)} + data-testid={props['data-testId']} + > + +  {'Add Document'} + + )} + props.onRefresh && props.onRefresh()} + type="button" + toolText="Refresh" + toolId="btn-refresh-tooltip" + > + + + + ); }; + return ( <>
@@ -250,3 +278,17 @@ export const DocumentListView: React.FunctionComponent = }; export default DocumentListView; + +const StyledRow = styled(Row)` + justify-content: space-between; + align-items: end; + min-height: 4.5rem; + .btn { + margin: 0; + } +`; + +const ListHeaderActionsDiv = styled(InlineFlexDiv)` + justify-content: space-between; + gap: 0.5rem; +`; diff --git a/source/frontend/src/features/documents/list/DocumentResults/DocumentResults.test.tsx b/source/frontend/src/features/documents/list/DocumentResults/DocumentResults.test.tsx index 8716b7a77b..bbbff686d4 100644 --- a/source/frontend/src/features/documents/list/DocumentResults/DocumentResults.test.tsx +++ b/source/frontend/src/features/documents/list/DocumentResults/DocumentResults.test.tsx @@ -7,6 +7,7 @@ import { mockDocumentResponse, mockDocumentsResponse } from '@/mocks/documents.m import { cleanup, mockKeycloak, render, RenderOptions, userEvent } from '@/utils/test-utils'; import { DocumentResults, IDocumentResultProps } from './DocumentResults'; +import { get } from 'lodash'; const setSort = vi.fn(); @@ -40,6 +41,21 @@ const setup = (renderOptions: RenderOptions & Partial = { ...utils, tableRows, sortButtons, + // Finding elements + getDocumentFileNameLink: (documentId: number) => + utils.container.querySelector( + `button#document-view-filename-link-${documentId}`, + ) as HTMLElement, + getDocumentFileNameText: (documentId: number) => + utils.container.querySelector( + `span#document-view-filename-text-${documentId}`, + ) as HTMLElement, + getDocumentProcessingIcon: (rowNumber: number) => + utils.container.querySelector(`svg#document-processing-${rowNumber}`) as SVGElement, + getDocumentProcessingErrorIcon: (rowNumber: number) => + utils.container.querySelector(`svg#document-error-${rowNumber}`) as SVGElement, + getDocumenDeleteIcon: (rowNumber: number) => + utils.container.querySelector(`svg#document-delete-${rowNumber}`) as SVGElement, }; }; @@ -81,27 +97,54 @@ describe('Document Results Table', () => { }); it('displays document filename as link', async () => { - const { queryByTestId, getAllByTestId } = setup({ + const { getDocumentFileNameLink, getDocumentFileNameText } = setup({ results: mockDocumentsResponse().map(x => DocumentRow.fromApi(x)), claims: [Claims.DOCUMENT_VIEW], }); - const filenameLink = await getAllByTestId('document-view-filename-link'); - expect(filenameLink[0]).toBeVisible(); + const filenameLink = await getDocumentFileNameLink(20); + expect(filenameLink).toBeVisible(); - expect(queryByTestId('document-view-filename-text')).toBeNull(); + expect(getDocumentFileNameText(20)).toBeNull(); }); it('displays document filename as plain text', async () => { mockKeycloak({ claims: [] }); - const { queryByTestId, getAllByTestId } = setup({ + const { getDocumentFileNameLink, getDocumentFileNameText } = setup({ results: mockDocumentsResponse().map(x => DocumentRow.fromApi(x)), }); - const filenameText = await getAllByTestId('document-view-filename-text'); - expect(filenameText[0]).toBeVisible(); + const filenameText = await getDocumentFileNameText(20); + expect(filenameText).toBeVisible(); - expect(queryByTestId('document-view-filename-link')).toBeNull(); + expect(getDocumentFileNameLink(20)).toBeNull(); + }); + + it('displays document processing icon', async () => { + const { getDocumentProcessingIcon } = setup({ + results: mockDocumentsResponse().map(x => DocumentRow.fromApi(x)), + claims: [Claims.DOCUMENT_VIEW, Claims.DOCUMENT_DELETE], + }); + + expect(getDocumentProcessingIcon(2)).toBeVisible(); + }); + + it('displays document PIMS ERROR processing icon', async () => { + const { getDocumentProcessingErrorIcon } = setup({ + results: mockDocumentsResponse().map(x => DocumentRow.fromApi(x)), + claims: [Claims.DOCUMENT_VIEW, Claims.DOCUMENT_DELETE], + }); + + expect(getDocumentProcessingErrorIcon(3)).toBeVisible(); + }); + + it('displays document MAYAN ERROR processing icon', async () => { + const { getDocumentProcessingErrorIcon } = setup({ + results: mockDocumentsResponse().map(x => DocumentRow.fromApi(x)), + claims: [Claims.DOCUMENT_VIEW, Claims.DOCUMENT_DELETE], + }); + + expect(getDocumentProcessingErrorIcon(4)).toBeVisible(); }); it('displays document delete button', async () => { @@ -123,12 +166,12 @@ describe('Document Results Table', () => { }); it('previews a document when text clicked', async () => { - const { queryByTestId, getAllByTestId } = setup({ + const { getDocumentFileNameLink } = setup({ results: mockDocumentsResponse().map(x => DocumentRow.fromApi(x)), claims: [Claims.DOCUMENT_VIEW], }); - const filenameLink = getAllByTestId('document-view-filename-link')[0]; + const filenameLink = await getDocumentFileNameLink(20); userEvent.click(filenameLink); expect(onPreview).toHaveBeenCalled(); @@ -145,7 +188,7 @@ describe('Document Results Table', () => { expect(onViewDetails).toHaveBeenCalled(); }); - it('deletes a document when delete icon cliecked', async () => { + it('deletes a document when delete icon clicked', async () => { const { getAllByTestId } = setup({ results: mockDocumentsResponse().map(x => DocumentRow.fromApi(x)), claims: [Claims.DOCUMENT_VIEW, Claims.DOCUMENT_DELETE], diff --git a/source/frontend/src/features/documents/list/DocumentResults/DocumentResultsColumns.tsx b/source/frontend/src/features/documents/list/DocumentResults/DocumentResultsColumns.tsx index 3cc7aac73e..30ac41dd46 100644 --- a/source/frontend/src/features/documents/list/DocumentResults/DocumentResultsColumns.tsx +++ b/source/frontend/src/features/documents/list/DocumentResults/DocumentResultsColumns.tsx @@ -1,15 +1,17 @@ import { Col, Row } from 'react-bootstrap'; -import { FaEye, FaTrash, FaUserAlt } from 'react-icons/fa'; +import { FaClock, FaEye, FaTimesCircle, FaTrash, FaUserAlt } from 'react-icons/fa'; import { CellProps } from 'react-table'; import styled from 'styled-components'; import { StyledRemoveLinkButton } from '@/components/common/buttons'; import { Button } from '@/components/common/buttons/Button'; +import { InlineFlexDiv } from '@/components/common/styles'; import TooltipIcon from '@/components/common/TooltipIcon'; import { ColumnWithProps, renderGenTypeCode } from '@/components/Table'; import { Claims } from '@/constants/index'; import { DocumentRow } from '@/features/documents/ComposedDocument'; import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; +import { ApiGen_CodeTypes_DocumentQueueStatusTypes } from '@/models/api/generated/ApiGen_CodeTypes_DocumentQueueStatusTypes'; import { ApiGen_Concepts_DocumentRelationship } from '@/models/api/generated/ApiGen_Concepts_DocumentRelationship'; import { ApiGen_Concepts_DocumentType } from '@/models/api/generated/ApiGen_Concepts_DocumentType'; import { prettyFormatUTCDate, stringToFragment } from '@/utils'; @@ -31,11 +33,15 @@ export const getDocumentColumns = ({ accessor: 'documentType', align: 'left', sortable: true, + width: 30, + maxWidth: 30, Cell: renderDocumentType, }, { Header: 'File name', accessor: 'fileName', + width: 40, + maxWidth: 40, sortable: true, Cell: renderFileName(onPreview), }, @@ -43,17 +49,22 @@ export const getDocumentColumns = ({ Header: 'Uploaded', accessor: 'appCreateTimestamp', sortable: true, + width: 20, + maxWidth: 20, Cell: renderUploaded, }, { Header: 'Status', accessor: 'statusTypeCode', sortable: true, + width: 20, + maxWidth: 20, Cell: renderGenTypeCode, }, { Header: 'Actions', - width: '90', + width: 10, + maxWidth: 10, Cell: renderActions(onViewDetails, onDelete), }, ]; @@ -68,11 +79,18 @@ function renderDocumentType({ const renderFileName = (onViewDetails: (values: ApiGen_Concepts_DocumentRelationship) => void) => { return function (cell: CellProps) { const { hasClaim } = useKeycloakWrapper(); + const documentProcessed = + (cell.row.original.mayanDocumentId && + cell.row.original.queueStatusTypeCode?.id === + ApiGen_CodeTypes_DocumentQueueStatusTypes.SUCCESS) || + (cell.row.original.mayanDocumentId && cell.row.original.queueStatusTypeCode === null); + return ( - {hasClaim(Claims.DOCUMENT_VIEW) === true ? ( + {hasClaim(Claims.DOCUMENT_VIEW) === true && documentProcessed ? ( ) : ( - {cell.value} + + {cell.value} + )} ); @@ -112,46 +135,97 @@ const renderActions = ( ) => { return function ({ row: { original, index } }: CellProps) { const { hasClaim } = useKeycloakWrapper(); - return ( - - {hasClaim(Claims.DOCUMENT_VIEW) && ( - - - - )} - {hasClaim(Claims.DOCUMENT_DELETE) && ( - + + const documentInError = + original.queueStatusTypeCode?.id === ApiGen_CodeTypes_DocumentQueueStatusTypes.PIMS_ERROR || + original.queueStatusTypeCode?.id === ApiGen_CodeTypes_DocumentQueueStatusTypes.MAYAN_ERROR; + + const documentProcessing = + original.queueStatusTypeCode?.id === ApiGen_CodeTypes_DocumentQueueStatusTypes.PENDING || + original.queueStatusTypeCode?.id === ApiGen_CodeTypes_DocumentQueueStatusTypes.PROCESSING; + + const canViewDocument = + (original.mayanDocumentId && original.queueStatusTypeCode === null) || + (original.queueStatusTypeCode?.id === ApiGen_CodeTypes_DocumentQueueStatusTypes.SUCCESS && + original.mayanDocumentId); + + const canDeleteDocument = documentInError || !documentProcessing; + + if (documentProcessing) { + return ( + + + + ); + } + + if (documentInError) { + return ( + + + + {hasClaim(Claims.DOCUMENT_DELETE) && ( } + icon={} onClick={() => original?.id && onDelete(DocumentRow.toApi(original))} > - + )} + + ); + } + + return ( + + {hasClaim(Claims.DOCUMENT_VIEW) && canViewDocument && ( + + )} + + {hasClaim(Claims.DOCUMENT_DELETE) && canDeleteDocument && ( + } + onClick={() => original?.id && onDelete(DocumentRow.toApi(original))} + > )} - + ); }; }; -const StyledIconsRow = styled(Row)` - [id^='document-view'] { - color: ${props => props.theme.css.activeActionColor}; +const DocumentsActionsDiv = styled(InlineFlexDiv)` + justify-content: center; + gap: 1rem; + align-items: center; + flex-grow: 1; + align-content: space-between; + + [id^='document-processing'] { + color: ${props => props.theme.bcTokens.themeGold100}; + } + [id^='document-error'] { + color: ${props => props.theme.bcTokens.typographyColorDanger}; } - [id^='document-delete'] { + [id^='document-view'] { color: ${props => props.theme.css.activeActionColor}; - :hover { - color: ${({ theme }) => theme.bcTokens.surfaceColorPrimaryDangerButtonDefault}; - } } + .btn.btn-primary { background-color: transparent; padding: 0; - margin-left: 0.5rem; } `; diff --git a/source/frontend/src/features/documents/list/DocumentResults/__snapshots__/DocumentResults.test.tsx.snap b/source/frontend/src/features/documents/list/DocumentResults/__snapshots__/DocumentResults.test.tsx.snap index 26d900a0d3..b018674e07 100644 --- a/source/frontend/src/features/documents/list/DocumentResults/__snapshots__/DocumentResults.test.tsx.snap +++ b/source/frontend/src/features/documents/list/DocumentResults/__snapshots__/DocumentResults.test.tsx.snap @@ -6,7 +6,17 @@ exports[`Document Results Table > matches snapshot 1`] = ` class="Toastify" />
- .c0 { + .c5 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; +} + +.c0 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -35,22 +45,40 @@ exports[`Document Results Table > matches snapshot 1`] = ` height: 1.6rem; } -.c5 [id^='document-view'] { - color: #428bca; +.c6 { + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + gap: 1rem; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; + -webkit-align-content: space-between; + -ms-flex-line-pack: space-between; + align-content: space-between; } -.c5 [id^='document-delete'] { - color: #428bca; +.c6 [id^='document-processing'] { + color: #FCBA19; } -.c5 [id^='document-delete']:hover { +.c6 [id^='document-error'] { color: #CE3E39; } -.c5 .btn.btn-primary { +.c6 [id^='document-view'] { + color: #428bca; +} + +.c6 .btn.btn-primary { background-color: transparent; padding: 0; - margin-left: 0.5rem; } .c4 .tooltip-icon { @@ -88,7 +116,7 @@ exports[`Document Results Table > matches snapshot 1`] = ` class="th" colspan="1" role="columnheader" - style="box-sizing: border-box; flex: 100 0 auto; min-width: 30px; width: 100px; justify-content: left; text-align: left; flex-wrap: wrap; align-items: center; display: flex;" + style="box-sizing: border-box; flex: 30 0 auto; min-width: 30px; width: 30px; justify-content: left; text-align: left; flex-wrap: wrap; align-items: center; display: flex;" >
matches snapshot 1`] = ` class="th" colspan="1" role="columnheader" - style="box-sizing: border-box; flex: 100 0 auto; min-width: 30px; width: 100px; justify-content: left; text-align: left; flex-wrap: wrap; align-items: center; display: flex;" + style="box-sizing: border-box; flex: 40 0 auto; min-width: 30px; width: 40px; justify-content: left; text-align: left; flex-wrap: wrap; align-items: center; display: flex;" >
matches snapshot 1`] = ` class="th" colspan="1" role="columnheader" - style="box-sizing: border-box; flex: 100 0 auto; min-width: 30px; width: 100px; justify-content: left; text-align: left; flex-wrap: wrap; align-items: center; display: flex;" + style="box-sizing: border-box; flex: 20 0 auto; min-width: 30px; width: 20px; justify-content: left; text-align: left; flex-wrap: wrap; align-items: center; display: flex;" >
matches snapshot 1`] = ` class="th" colspan="1" role="columnheader" - style="box-sizing: border-box; flex: 100 0 auto; min-width: 30px; width: 100px; justify-content: left; text-align: left; flex-wrap: wrap; align-items: center; display: flex;" + style="box-sizing: border-box; flex: 20 0 auto; min-width: 30px; width: 20px; justify-content: left; text-align: left; flex-wrap: wrap; align-items: center; display: flex;" >
matches snapshot 1`] = ` class="th" colspan="1" role="columnheader" - style="box-sizing: border-box; flex: 90 0 auto; min-width: 30px; width: 90px; justify-content: left; text-align: left; flex-wrap: wrap; align-items: center; display: flex;" + style="box-sizing: border-box; flex: 10 0 auto; min-width: 30px; width: 10px; justify-content: left; text-align: left; flex-wrap: wrap; align-items: center; display: flex;" >
matches snapshot 1`] = `
GAZE @@ -305,14 +333,15 @@ exports[`Document Results Table > matches snapshot 1`] = `
DocTest.docx @@ -321,7 +350,7 @@ exports[`Document Results Table > matches snapshot 1`] = `
matches snapshot 1`] = `
Signed @@ -372,11 +401,11 @@ exports[`Document Results Table > matches snapshot 1`] = `
@@ -392,7 +421,7 @@ exports[`Document Results Table > matches snapshot 1`] = `
MOTI Plan @@ -400,14 +429,15 @@ exports[`Document Results Table > matches snapshot 1`] = `
moti_plan.txt @@ -416,7 +446,7 @@ exports[`Document Results Table > matches snapshot 1`] = `
matches snapshot 1`] = `
Amended @@ -467,15 +497,358 @@ exports[`Document Results Table > matches snapshot 1`] = `
+
+
+
+ Compensation requisition (H-120) +
+
+
+ + H120 Template-original-20240910-152332.PDF + +
+
+
+
+
+ Nov 27, 2024 +
+
+ + + + + + + +
+
+
+
+ Approved +
+
+
+ + + Upload in progress... + + + +
+
+
+
+
+
+
+ Compensation requisition (H-120) +
+
+
+ + H120 Template-original-20240902-183943.PDF + +
+
+
+
+
+ Dec 2, 2024 +
+
+ + + + + + + +
+
+
+
+ Signed +
+
+
+ + + PIMS Error + + + +
+
+
+
+
+
+
+ Compensation requisition (H-120) +
+
+
+ + H120 Template-original-20240902-190248.PDF + +
+
+
+
+
+ Dec 2, 2024 +
+
+ + + + + + + +
+
+
+
+ Amended +
+
+
+ + + MAYAN Error + + + +
+
+
+
.c0 { @@ -502,7 +875,7 @@ exports[`Document Results Table > matches snapshot 1`] = ` class="align-self-center col-auto" > - 1 - 2 of 2 + 1 - 5 of 5
renders as expected 1`] = ` class="Toastify" />
- .c6.btn { + .c5.btn { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -35,19 +35,19 @@ exports[`Document List View > renders as expected 1`] = ` cursor: pointer; } -.c6.btn .Button__value { +.c5.btn .Button__value { width: -webkit-max-content; width: -moz-max-content; width: max-content; } -.c6.btn:hover { +.c5.btn:hover { -webkit-text-decoration: underline; text-decoration: underline; opacity: 0.8; } -.c6.btn:focus { +.c5.btn:focus { outline-width: 2px; outline-style: solid; outline-color: #2E5DD7; @@ -55,31 +55,31 @@ exports[`Document List View > renders as expected 1`] = ` box-shadow: none; } -.c6.btn.btn-primary { +.c5.btn.btn-primary { color: #FFFFFF; background-color: #013366; } -.c6.btn.btn-primary:hover, -.c6.btn.btn-primary:active, -.c6.btn.btn-primary:focus { +.c5.btn.btn-primary:hover, +.c5.btn.btn-primary:active, +.c5.btn.btn-primary:focus { background-color: #1E5189; } -.c6.btn.btn-secondary { +.c5.btn.btn-secondary { color: #013366; background: none; border-color: #013366; } -.c6.btn.btn-secondary:hover, -.c6.btn.btn-secondary:active, -.c6.btn.btn-secondary:focus { +.c5.btn.btn-secondary:hover, +.c5.btn.btn-secondary:active, +.c5.btn.btn-secondary:focus { color: #FFFFFF; background-color: #013366; } -.c6.btn.btn-info { +.c5.btn.btn-info { color: #9F9D9C; border: none; background: none; @@ -87,66 +87,66 @@ exports[`Document List View > renders as expected 1`] = ` padding-right: 0.6rem; } -.c6.btn.btn-info:hover, -.c6.btn.btn-info:active, -.c6.btn.btn-info:focus { +.c5.btn.btn-info:hover, +.c5.btn.btn-info:active, +.c5.btn.btn-info:focus { color: var(--surface-color-primary-button-hover); background: none; } -.c6.btn.btn-light { +.c5.btn.btn-light { color: #FFFFFF; background-color: #606060; border: none; } -.c6.btn.btn-light:hover, -.c6.btn.btn-light:active, -.c6.btn.btn-light:focus { +.c5.btn.btn-light:hover, +.c5.btn.btn-light:active, +.c5.btn.btn-light:focus { color: #FFFFFF; background-color: #606060; } -.c6.btn.btn-dark { +.c5.btn.btn-dark { color: #FFFFFF; background-color: #474543; border: none; } -.c6.btn.btn-dark:hover, -.c6.btn.btn-dark:active, -.c6.btn.btn-dark:focus { +.c5.btn.btn-dark:hover, +.c5.btn.btn-dark:active, +.c5.btn.btn-dark:focus { color: #FFFFFF; background-color: #474543; } -.c6.btn.btn-danger { +.c5.btn.btn-danger { color: #FFFFFF; background-color: #CE3E39; } -.c6.btn.btn-danger:hover, -.c6.btn.btn-danger:active, -.c6.btn.btn-danger:focus { +.c5.btn.btn-danger:hover, +.c5.btn.btn-danger:active, +.c5.btn.btn-danger:focus { color: #FFFFFF; background-color: #CE3E39; } -.c6.btn.btn-warning { +.c5.btn.btn-warning { color: #FFFFFF; background-color: #FCBA19; border-color: #FCBA19; } -.c6.btn.btn-warning:hover, -.c6.btn.btn-warning:active, -.c6.btn.btn-warning:focus { +.c5.btn.btn-warning:hover, +.c5.btn.btn-warning:active, +.c5.btn.btn-warning:focus { color: #FFFFFF; border-color: #FCBA19; background-color: #FCBA19; } -.c6.btn.btn-link { +.c5.btn.btn-link { font-size: 1.6rem; font-weight: 400; color: var(--surface-color-primary-button-default); @@ -170,9 +170,9 @@ exports[`Document List View > renders as expected 1`] = ` text-decoration: underline; } -.c6.btn.btn-link:hover, -.c6.btn.btn-link:active, -.c6.btn.btn-link:focus { +.c5.btn.btn-link:hover, +.c5.btn.btn-link:active, +.c5.btn.btn-link:focus { color: var(--surface-color-primary-button-hover); -webkit-text-decoration: underline; text-decoration: underline; @@ -182,15 +182,15 @@ exports[`Document List View > renders as expected 1`] = ` outline: none; } -.c6.btn.btn-link:disabled, -.c6.btn.btn-link.disabled { +.c5.btn.btn-link:disabled, +.c5.btn.btn-link.disabled { color: #9F9D9C; background: none; pointer-events: none; } -.c6.btn:disabled, -.c6.btn:disabled:hover { +.c5.btn:disabled, +.c5.btn:disabled:hover { color: #9F9D9C; background-color: #EDEBE9; box-shadow: none; @@ -202,19 +202,19 @@ exports[`Document List View > renders as expected 1`] = ` cursor: not-allowed; } -.c6.Button .Button__icon { +.c5.Button .Button__icon { margin-right: 1.6rem; } -.c6.Button--icon-only:focus { +.c5.Button--icon-only:focus { outline: none; } -.c6.Button--icon-only .Button__icon { +.c5.Button--icon-only .Button__icon { margin-right: 0; } -.c3 { +.c6 { float: right; cursor: pointer; font-size: 3.2rem; @@ -235,7 +235,7 @@ exports[`Document List View > renders as expected 1`] = ` border-radius: 0.5rem; } -.c7 { +.c9 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -254,21 +254,21 @@ exports[`Document List View > renders as expected 1`] = ` margin-left: 0.5rem; } -.c9 { +.c11 { width: 1.6rem; height: 1.6rem; } -.c8 { +.c10 { width: 1.6rem; height: 1.6rem; } -.c13 { +.c15 { margin-top: 0.3rem; } -.c14 { +.c16 { min-width: 5rem; max-width: 5rem; margin-left: 1rem; @@ -277,58 +277,70 @@ exports[`Document List View > renders as expected 1`] = ` padding: 0; } -.c14:disabled { +.c16:disabled { background: white; } -.c2 { - -webkit-box-pack: justify; - -webkit-justify-content: space-between; - -ms-flex-pack: justify; - justify-content: space-between; - -webkit-align-items: end; - -webkit-box-align: end; - -ms-flex-align: end; - align-items: end; - min-height: 4.5rem; -} - -.c2 .btn { - margin: 0; +.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; } -.c4 { +.c7 { background-color: #d9eaf7; border-radius: 0.5rem 0.5rem 0rem 0rem; } -.c5 { +.c8 { border-left: 0.2rem solid white; } -.c12 [id^='document-view'] { - color: #428bca; +.c14 { + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + gap: 1rem; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; + -webkit-align-content: space-between; + -ms-flex-line-pack: space-between; + align-content: space-between; } -.c12 [id^='document-delete'] { - color: #428bca; +.c14 [id^='document-processing'] { + color: #FCBA19; } -.c12 [id^='document-delete']:hover { +.c14 [id^='document-error'] { color: #CE3E39; } -.c12 .btn.btn-primary { +.c14 [id^='document-view'] { + color: #428bca; +} + +.c14 .btn.btn-primary { background-color: transparent; padding: 0; - margin-left: 0.5rem; } -.c11 .tooltip-icon { +.c13 .tooltip-icon { color: #9F9D9C; } -.c10 { +.c12 { display: contents; text-overflow: ellipsis; overflow: hidden; @@ -338,10 +350,34 @@ exports[`Document List View > renders as expected 1`] = ` -webkit-box-orient: vertical; } -.c10 button { +.c12 button { display: contents !important; } +.c2 { + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-align-items: end; + -webkit-box-align: end; + -ms-flex-align: end; + align-items: end; + min-height: 4.5rem; +} + +.c2 .btn { + margin: 0; +} + +.c4 { + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + gap: 0.5rem; +} +
@@ -360,18 +396,55 @@ exports[`Document List View > renders as expected 1`] = `
- Documents + + Documents +
+ > +
+ +
+
renders as expected 1`] = ` class="collapse show" >
renders as expected 1`] = ` class="col-lg-auto" >
+
+
renders as expected 1`] = ` class="collapse show" >
renders as expected 1`] = ` class="col-lg-auto" >
- + diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form1/__snapshots__/ExpropriationForm1.test.tsx.snap b/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form1/__snapshots__/ExpropriationForm1.test.tsx.snap index 9ad25523cf..8381e81198 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form1/__snapshots__/ExpropriationForm1.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form1/__snapshots__/ExpropriationForm1.test.tsx.snap @@ -798,6 +798,17 @@ exports[`Expropriation Form 1 > matches snapshot 1`] = ` margin-right: 0; } +.c2.btn.btn-primary, +.c2.btn.btn-primary:active { + background-color: #42814A; +} + +.c2.btn.btn-primary:hover, +.c2.btn.btn-primary:focus { + background-color: #2e8540; + outline-color: #2e8540; +} + .c0 { -webkit-box-pack: end; -webkit-justify-content: end; @@ -830,7 +841,7 @@ exports[`Expropriation Form 1 > matches snapshot 1`] = ` class="col-auto" > - + diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form5/__snapshots__/ExpropriationForm5.test.tsx.snap b/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form5/__snapshots__/ExpropriationForm5.test.tsx.snap index 60e64c5842..c8b107caae 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form5/__snapshots__/ExpropriationForm5.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form5/__snapshots__/ExpropriationForm5.test.tsx.snap @@ -720,6 +720,17 @@ exports[`Expropriation Form 1 > matches snapshot 1`] = ` margin-right: 0; } +.c2.btn.btn-primary, +.c2.btn.btn-primary:active { + background-color: #42814A; +} + +.c2.btn.btn-primary:hover, +.c2.btn.btn-primary:focus { + background-color: #2e8540; + outline-color: #2e8540; +} + .c0 { -webkit-box-pack: end; -webkit-justify-content: end; @@ -752,7 +763,7 @@ exports[`Expropriation Form 1 > matches snapshot 1`] = ` class="col-auto" > - + diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form9/__snapshots__/ExpropriationForm9.test.tsx.snap b/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form9/__snapshots__/ExpropriationForm9.test.tsx.snap index 7ef7e89233..96678a3763 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form9/__snapshots__/ExpropriationForm9.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form9/__snapshots__/ExpropriationForm9.test.tsx.snap @@ -759,6 +759,17 @@ exports[`Expropriation Form 1 > matches snapshot 1`] = ` margin-right: 0; } +.c2.btn.btn-primary, +.c2.btn.btn-primary:active { + background-color: #42814A; +} + +.c2.btn.btn-primary:hover, +.c2.btn.btn-primary:focus { + background-color: #2e8540; + outline-color: #2e8540; +} + .c0 { -webkit-box-pack: end; -webkit-justify-content: end; @@ -791,7 +802,7 @@ exports[`Expropriation Form 1 > matches snapshot 1`] = ` class="col-auto" >