diff --git a/api.Tests/Services/WorkflowServiceTests.cs b/api.Tests/Services/WorkflowServiceTests.cs index 729248a..dbf8a36 100644 --- a/api.Tests/Services/WorkflowServiceTests.cs +++ b/api.Tests/Services/WorkflowServiceTests.cs @@ -162,7 +162,9 @@ public async Task TriggerWorkflow_ArgoReturnsError_MarksWorkflowAndRunFailed() outputBlobStorageLocation: _db.NewBlobStorageLocation() ); - await TriggerWorkflowInScope(workflow.Id); + await Assert.ThrowsAsync( + () => TriggerWorkflowInScope(workflow.Id) + ); await _context.Entry(workflow).ReloadAsync(TestContext.Current.CancellationToken); await _context.Entry(run).ReloadAsync(TestContext.Current.CancellationToken); diff --git a/api/Controllers/AnalysisController.cs b/api/Controllers/AnalysisController.cs index d8d884e..0853ca2 100644 --- a/api/Controllers/AnalysisController.cs +++ b/api/Controllers/AnalysisController.cs @@ -138,4 +138,22 @@ public async Task Delete([FromRoute] Guid id) return NotFound(ex.Message); } } + + [HttpGet] + [Authorize(Roles = Role.Any)] + [Route("available")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetAvailableAnalyses() + { + try + { + var availableAnalyses = await analysisService.GetAvailableAnalyses(); + return Ok(availableAnalyses); + } + catch (Exception e) + { + logger.LogError(e, "Error during GET of available analyses"); + throw; + } + } } diff --git a/api/Controllers/InspectionRecordController.cs b/api/Controllers/InspectionRecordController.cs index 1fd5e66..38b3de8 100644 --- a/api/Controllers/InspectionRecordController.cs +++ b/api/Controllers/InspectionRecordController.cs @@ -228,4 +228,48 @@ public async Task Delete([FromRoute] Guid id) return NotFound(ex.Message); } } + + [HttpPost] + [Authorize(Roles = Role.Any)] + [Route("id/{id:guid}/analyses")] + [ProducesResponseType(StatusCodes.Status202Accepted)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + [ProducesResponseType(StatusCodes.Status502BadGateway)] + public async Task AddAnalysis( + [FromRoute] Guid id, + [FromBody] AddAnalysisRequest request + ) + { + request.AnalysisName = Sanitize.SanitizeUserInput(request.AnalysisName); + try + { + await inspectionRecordService.AddAnalysis(id, request.AnalysisName); + return AcceptedAtAction(nameof(GetById), new { id }, null); + } + catch (KeyNotFoundException ex) + { + return NotFound(ex.Message); + } + catch (WorkflowTriggerFailedException ex) + { + logger.LogError(ex, "Upstream workflow trigger failed for inspection record {Id}", id); + return StatusCode(StatusCodes.Status502BadGateway, ex.Message); + } + catch (InvalidOperationException ex) + { + return Conflict(ex.Message); + } + catch (Exception e) + { + logger.LogError(e, "Error adding analysis to inspection record"); + throw; + } + } +} + +public class AddAnalysisRequest +{ + public required string AnalysisName { get; set; } } diff --git a/api/Services/AnalysisService.cs b/api/Services/AnalysisService.cs index a252807..a996104 100644 --- a/api/Services/AnalysisService.cs +++ b/api/Services/AnalysisService.cs @@ -1,7 +1,9 @@ +using api.Configurations; using api.Database.Context; using api.Database.Models; using api.Utilities; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; namespace api.Services; @@ -14,6 +16,8 @@ public interface IAnalysisService public Task> GetAnalyses(AnalysisParameters parameters); public Task Delete(Guid id); + + public Task> GetAvailableAnalyses(); } public class AnalysisParameters @@ -25,8 +29,11 @@ public class AnalysisParameters public Guid? InspectionRecordId { get; set; } } -public class AnalysisService(SaraDbContext context) : IAnalysisService +public class AnalysisService(SaraDbContext context, IOptions analysisOptions) + : IAnalysisService { + private readonly AnalysisOptions _analysisOptions = analysisOptions.Value; + public async Task ReadById(Guid id) { return await context @@ -90,4 +97,9 @@ public async Task Delete(Guid id) context.Analyses.Remove(analysis); await context.SaveChangesAsync(); } + + public Task> GetAvailableAnalyses() + { + return Task.FromResult(_analysisOptions.Analyses.Keys.ToList()); + } } diff --git a/api/Services/InspectionRecordService.cs b/api/Services/InspectionRecordService.cs index 1bd55bb..fe21c89 100644 --- a/api/Services/InspectionRecordService.cs +++ b/api/Services/InspectionRecordService.cs @@ -1,8 +1,10 @@ +using api.Configurations; using api.Database.Context; using api.Database.Models; using api.MQTT; using api.Utilities; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; namespace api.Services; @@ -25,6 +27,8 @@ InspectionRecordParameters parameters ); public Task Delete(Guid id); + + public Task AddAnalysis(Guid inspectionRecordId, string analysisName); } public class InspectionRecordParameters @@ -60,9 +64,12 @@ public class CreateInspectionRecordAnalysisGroup public class InspectionRecordService( SaraDbContext context, IAnalysisTriggerService analysisTriggerService, + IOptions analysisOptions, ILogger logger ) : IInspectionRecordService { + private readonly AnalysisOptions _analysisOptions = analysisOptions.Value; + public async Task CreateFromMqttMessage(IsarInspectionResultMessage message) { var inspectionId = Sanitize.SanitizeUserInput(message.InspectionId); @@ -253,4 +260,31 @@ InspectionRecordParameters parameters parameters.PageSize ); } + + public async Task AddAnalysis(Guid inspectionRecordId, string analysisName) + { + if (!_analysisOptions.Analyses.ContainsKey(analysisName)) + { + throw new InvalidOperationException( + $"Unknown analysis '{analysisName}'. Valid analyses are: " + + string.Join(", ", _analysisOptions.Analyses.Keys) + ); + } + + var record = + await ReadById(inspectionRecordId) + ?? throw new KeyNotFoundException( + $"Inspection record with id {inspectionRecordId} not found" + ); + + await analysisTriggerService.OnInspectionRecordCreated( + new InspectionRecordCreatedEvent + { + InspectionRecordId = record.Id, + RequiredAnalysis = [analysisName], + } + ); + + return record; + } } diff --git a/api/Services/WorkflowService.cs b/api/Services/WorkflowService.cs index d081092..ac93f74 100644 --- a/api/Services/WorkflowService.cs +++ b/api/Services/WorkflowService.cs @@ -11,6 +11,9 @@ namespace api.Services; +public class WorkflowTriggerFailedException(string message, Exception? innerException = null) + : Exception(message, innerException); + public interface IWorkflowService { public Task TriggerWorkflow(Guid workflowId); @@ -167,6 +170,11 @@ public async Task TriggerWorkflow(Guid workflowId) ); await MarkWorkflowFailed(workflow, ex.Message); + + throw new WorkflowTriggerFailedException( + $"Failed to trigger workflow '{workflow.WorkflowType}'", + ex + ); } } @@ -235,7 +243,14 @@ public async Task OnWorkflowCompleted(Guid workflowId) nextWorkflow.StepNumber ); - await TriggerWorkflow(nextWorkflow.Id); + try + { + await TriggerWorkflow(nextWorkflow.Id); + } + catch (WorkflowTriggerFailedException) + { + // Already logged and persisted inside TriggerWorkflow. + } } private async Task MarkWorkflowFailed(Workflow workflow, string errorMessage)