Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LNK-3317: Add Tenant API endpoints to generate ad hoc reports #611

Merged
69 changes: 60 additions & 9 deletions DotNet/Report/Controllers/ReportController.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using System.Net;
using LantanaGroup.Link.Report.Core;
using LantanaGroup.Link.Report.Domain;
using LantanaGroup.Link.Report.Entities;
using LantanaGroup.Link.Shared.Application.Models.Responses;
using LantanaGroup.Link.Shared.Application.Services.Security;
using Link.Authorization.Policies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Text.Json;
using LantanaGroup.Link.Report.Domain.Managers;

namespace LantanaGroup.Link.Report.Controllers
{
Expand All @@ -16,11 +17,13 @@ public class ReportController : ControllerBase
{
private readonly ILogger<ReportController> _logger;
private readonly PatientReportSubmissionBundler _patientReportSubmissionBundler;
private readonly IDatabase _database;

public ReportController(ILogger<ReportController> logger, PatientReportSubmissionBundler patientReportSubmissionBundler)
public ReportController(ILogger<ReportController> logger, PatientReportSubmissionBundler patientReportSubmissionBundler, IDatabase database)
{
_logger = logger;
_patientReportSubmissionBundler = patientReportSubmissionBundler;
_database = database;
}

/// <summary>
Expand All @@ -29,8 +32,7 @@ public ReportController(ILogger<ReportController> logger, PatientReportSubmissio
/// </summary>
/// <param name="facilityId"></param>
/// <param name="patientId"></param>
/// <param name="startDate"></param>
/// <param name="endDate"></param>
/// <param name="reportScheduleId"></param>
[HttpGet("Bundle/Patient")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PatientSubmissionModel))]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
Expand All @@ -41,17 +43,17 @@ public async Task<ActionResult<PatientSubmissionModel>> GetSubmissionBundleForPa
{
if (string.IsNullOrWhiteSpace(facilityId))
{
BadRequest("Parameter facilityId is null or whitespace");
return BadRequest("Parameter facilityId is null or whitespace");
}

if (string.IsNullOrWhiteSpace(patientId))
{
BadRequest("Parameter patientId is null or whitespace");
return BadRequest("Parameter patientId is null or whitespace");
}

if (string.IsNullOrWhiteSpace(reportScheduleId))
{
BadRequest("Parameter reportScheduleId is null or whitespace");
return BadRequest("Parameter reportScheduleId is null or whitespace");
}

var submission = await _patientReportSubmissionBundler.GenerateBundle(facilityId, patientId, reportScheduleId);
Expand All @@ -61,9 +63,58 @@ public async Task<ActionResult<PatientSubmissionModel>> GetSubmissionBundleForPa
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception in ReportController.GetSubmissionBundleForPatient for facility '{FacilityId}' and patient '{PatientId}'",HtmlInputSanitizer.SanitizeAndRemove(facilityId), HtmlInputSanitizer.Sanitize(patientId));
_logger.LogError(ex, "Exception in ReportController.GetSubmissionBundleForPatient for facility '{FacilityId}' and patient '{PatientId}'", HtmlInputSanitizer.SanitizeAndRemove(facilityId), HtmlInputSanitizer.Sanitize(patientId));
return Problem(ex.Message, statusCode: 500);
}
}

/// <summary>
/// Returns a summary of a ReportSchedule based on the provided facilityId and reportScheduleId
/// </summary>
/// <param name="facilityId"></param>
/// <param name="reportScheduleId"></param>
/// <returns></returns>
[HttpGet("Schedule")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ReportScheduleSummaryModel))]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<ReportScheduleSummaryModel>> GetReportScheduleSummary(string facilityId, string reportScheduleId)
{
if (string.IsNullOrWhiteSpace(facilityId))
{
return BadRequest("Parameter facilityId is null or whitespace");
}

if (string.IsNullOrWhiteSpace(reportScheduleId))
{
return BadRequest("Parameter reportScheduleId is null or whitespace");
}

try
{

var model = (await _database.ReportScheduledRepository.FindAsync(r => r.FacilityId == facilityId && r.Id == reportScheduleId)).SingleOrDefault();

if (model == null)
{
return Problem(detail: "No Report Schedule found for the provided FacilityId and ReportId", statusCode: (int)HttpStatusCode.NotFound);
}

return Ok(new ReportScheduleSummaryModel
{
FacilityId = facilityId,
ReportId = reportScheduleId,
StartDate = model.ReportStartDate,
EndDate = model.ReportEndDate,
SubmitReportDateTime = model.SubmitReportDateTime
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception in ReportController.GetReportScheduleSummary for facility '{FacilityId}' and report '{ReportId}'", HtmlInputSanitizer.SanitizeAndRemove(facilityId), HtmlInputSanitizer.Sanitize(reportScheduleId));
return Problem("An error occurred while retrieving the report schedule.", statusCode: (int)HttpStatusCode.InternalServerError);
}
}
}
}
14 changes: 14 additions & 0 deletions DotNet/Shared/Application/Models/Kafka/GenerateReportValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@


namespace LantanaGroup.Link.Shared.Application.Models.Kafka
{
public class GenerateReportValue
{
public string? ReportId { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public List<string>? ReportTypes { get; set; }
public List<string>? PatientIds { get; set; }
public bool BypassSubmission { get; set; }
}
}
3 changes: 2 additions & 1 deletion DotNet/Shared/Application/Models/KafkaTopic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,6 @@ public enum KafkaTopic
SubmitReportRetry,
[StringValue("ReportScheduled-Retry")]
ReportScheduledRetry,
MeasureEvaluated
MeasureEvaluated,
GenerateReportRequested
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace LantanaGroup.Link.Shared.Application.Models.Responses
{
public class ReportScheduleSummaryModel
{
public string ReportId { get; set; }
public string FacilityId { get; set; }
public DateTime StartDate { get; set; } = default;
public DateTime EndDate { get; set; } = default;
public DateTime? SubmitReportDateTime { get; set; }
}
}
176 changes: 171 additions & 5 deletions DotNet/Tenant/Controllers/FacilityController.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
using AutoMapper;
using System.ComponentModel.DataAnnotations;
using AutoMapper;
using Confluent.Kafka;
using LantanaGroup.Link.Shared.Application.Enums;
using LantanaGroup.Link.Shared.Application.Interfaces;
using LantanaGroup.Link.Shared.Application.Models;
using LantanaGroup.Link.Shared.Application.Models.Configs;
using LantanaGroup.Link.Shared.Application.Models.Kafka;
using LantanaGroup.Link.Shared.Application.Models.Responses;
using LantanaGroup.Link.Tenant.Entities;
using LantanaGroup.Link.Tenant.Interfaces;
using LantanaGroup.Link.Tenant.Models;
using LantanaGroup.Link.Tenant.Services;
using Link.Authorization.Policies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Quartz;
using System.Diagnostics;
using LantanaGroup.Link.Shared.Application.Enums;
using LantanaGroup.Link.Shared.Application.Models.Responses;
using LantanaGroup.Link.Tenant.Interfaces;
using System.Net;
using Microsoft.Extensions.Options;
using System.Threading;
using Microsoft.IdentityModel.Tokens;

namespace LantanaGroup.Link.Tenant.Controllers
{
Expand All @@ -29,8 +39,12 @@ public class FacilityController : ControllerBase

private readonly ISchedulerFactory _schedulerFactory;

private readonly IKafkaProducerFactory<string, GenerateReportValue> _adHocKafkaProducerFactory;

private readonly IHttpClientFactory _httpClient;
private readonly ServiceRegistry _serviceRegistry;

public FacilityController(ILogger<FacilityController> logger, IFacilityConfigurationService facilityConfigurationService, ISchedulerFactory schedulerFactory)
public FacilityController(ILogger<FacilityController> logger, IFacilityConfigurationService facilityConfigurationService, ISchedulerFactory schedulerFactory, IKafkaProducerFactory<string, GenerateReportValue> adHocKafkaProducerFactory, IOptions<ServiceRegistry> serviceRegistry, IHttpClientFactory httpClient)
{

_facilityConfigurationService = facilityConfigurationService;
Expand All @@ -54,6 +68,9 @@ public FacilityController(ILogger<FacilityController> logger, IFacilityConfigura

_mapperModelToDto = configModelToDto.CreateMapper();
_mapperDtoToModel = configDtoToModel.CreateMapper();
_adHocKafkaProducerFactory = adHocKafkaProducerFactory;
_serviceRegistry = serviceRegistry?.Value ?? throw new ArgumentNullException(nameof(serviceRegistry));
_httpClient = httpClient;
}

/// <summary>
Expand Down Expand Up @@ -271,5 +288,154 @@ public async Task<IActionResult> DeleteFacility(string facilityId, CancellationT
return NoContent();
}

/// <summary>
/// Generat
/// </summary>
/// <param name="facilityId"></param>
/// <param name="request"></param>
/// <returns></returns>
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[HttpPost("AdHocReport")]
MontaltoNick marked this conversation as resolved.
Show resolved Hide resolved
public async Task<IActionResult> GenerateAdHocReport(string facilityId, AdHocReportRequest request)
{
if (string.IsNullOrEmpty(facilityId) || await _facilityConfigurationService.GetFacilityByFacilityId(facilityId, CancellationToken.None) == null)
{
return BadRequest("Facility does not exist.");
}

if (request.ReportTypes == null || request.ReportTypes.Count == 0)
{
return BadRequest("ReportTypes must be provided.");
}

if (request.StartDate == null || request.StartDate == DateTime.MinValue)
{
return BadRequest("StartDate must be provided.");
}

if (request.EndDate == null || request.EndDate == DateTime.MinValue)
{
return BadRequest("EndDate must be provided.");
}

if (request.EndDate <= request.StartDate)
{
return BadRequest("EndDate must be after StartDate.");
}

try
{
foreach (var rt in request.ReportTypes)
{
//this will throw an ApplicationException if the Measure Definition does not exist.
await _facilityConfigurationService.MeasureDefinitionExists(rt);
}

var producerConfig = new ProducerConfig();

using var producer = _adHocKafkaProducerFactory.CreateProducer(producerConfig);

var headers = new Headers
{
{ "X-Report-Tracking-Id", System.Text.Encoding.ASCII.GetBytes(Guid.NewGuid().ToString()) }
};

var message = new Message<string, GenerateReportValue>
{
Key = facilityId,
Headers = headers,
Value = new GenerateReportValue
{
StartDate = request.StartDate,
EndDate = request.EndDate,
ReportTypes = request.ReportTypes,
PatientIds = request.PatientIds,
BypassSubmission = request.BypassSubmission?? false
},
};

await producer.ProduceAsync(KafkaTopic.GenerateReportRequested.ToString(), message, CancellationToken.None);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception encountered in FacilityController.GenerateAdHocReport");
return Problem("An internal server error occurred.", statusCode: 500);
}

return Ok();
}

[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[HttpPost("RegenerateReport")]
MontaltoNick marked this conversation as resolved.
Show resolved Hide resolved
public async Task<IActionResult> RegenerateReport(string facilityId, RegenerateReportRequest request)
{
if (string.IsNullOrEmpty(facilityId) || await _facilityConfigurationService.GetFacilityByFacilityId(facilityId, CancellationToken.None) == null)
{
return BadRequest("Facility does not exist.");
}

if (string.IsNullOrEmpty(request.ReportId))
{
return BadRequest("ReportId must be provided.");
}

try
{
var httpClient = _httpClient.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(30);

string requestUrl = $"{_serviceRegistry.ReportServiceApiUrl.Trim('/')}/Report/Schedule?FacilityId={facilityId}&reportScheduleId={request.ReportId}";

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var response = await httpClient.GetAsync(requestUrl, cts.Token);

if (!response.IsSuccessStatusCode)
{
throw new Exception(
$"Report Service Call unsuccessful: StatusCode: {response.StatusCode} | Response: {await response.Content.ReadAsStringAsync(CancellationToken.None)} | Query URL: {requestUrl}");
}

var reportScheduleSummary = (ReportScheduleSummaryModel?)await response.Content.ReadFromJsonAsync(typeof(ReportScheduleSummaryModel), CancellationToken.None);

if (reportScheduleSummary == null)
{
return Problem("No ReportSchedule found for the provided ReportScheduleId", statusCode: (int)HttpStatusCode.NotFound);
}

var producerConfig = new ProducerConfig();

using var producer = _adHocKafkaProducerFactory.CreateProducer(producerConfig);

var headers = new Headers
{
{ "X-Report-Tracking-Id", System.Text.Encoding.ASCII.GetBytes(Guid.NewGuid().ToString()) }
};

var message = new Message<string, GenerateReportValue>
{
Key = reportScheduleSummary.FacilityId,
Headers = headers,
Value = new GenerateReportValue()
{
ReportId = reportScheduleSummary.ReportId,
BypassSubmission = request.BypassSubmission ?? false
},
};

await producer.ProduceAsync(KafkaTopic.GenerateReportRequested.ToString(), message, CancellationToken.None);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception encountered in FacilityController.RegenerateReport");
return Problem("An internal server error occurred.", statusCode: 500);
}

return Ok();
}
}
}
3 changes: 2 additions & 1 deletion DotNet/Tenant/Interfaces/IFacilityConfigurationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ public interface IFacilityConfigurationService
Task CreateFacility(FacilityConfigModel newFacility, CancellationToken cancellationToken);
Task<List<FacilityConfigModel>> GetAllFacilities(CancellationToken cancellationToken = default);
Task<PagedConfigModel<FacilityConfigModel>> GetFacilities(string? facilityId, string? facilityName, string? sortBy, SortOrder? sortOrder, int pageSize = 10, int pageNumber = 1, CancellationToken cancellationToken = default);
Task<FacilityConfigModel> GetFacilityByFacilityId(string facilityId, CancellationToken cancellationToken);
Task<FacilityConfigModel?> GetFacilityByFacilityId(string facilityId, CancellationToken cancellationToken);
MontaltoNick marked this conversation as resolved.
Show resolved Hide resolved
Task<FacilityConfigModel> GetFacilityById(string id, CancellationToken cancellationToken);
Task<string> RemoveFacility(string facilityId, CancellationToken cancellationToken);
Task<string> UpdateFacility(string id, FacilityConfigModel newFacility, CancellationToken cancellationToken = default);
Task MeasureDefinitionExists(String reportType);
}
}
12 changes: 12 additions & 0 deletions DotNet/Tenant/Models/AdHocReportRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace LantanaGroup.Link.Tenant.Models
{
public class AdHocReportRequest
{
public bool? BypassSubmission { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public List<string>? ReportTypes { get; set; }
public List<string>? PatientIds { get; set; }

}
}
Loading
Loading