Skip to content

Commit ce06131

Browse files
Merge branch 'dev' into LEGLINK-840
2 parents 3335ae2 + ec45dd0 commit ce06131

64 files changed

Lines changed: 6051 additions & 125 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Azure_Pipelines/azure-pipelines.tenant.cd.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ trigger:
1313
paths:
1414
include:
1515
- DotNet/Tenant/*
16+
- DotNet/DMRP/*
1617
- DotNet/Shared/*
1718
exclude:
1819
- '*'

DotNet/Admin.BFF/appsettings.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,13 @@
273273
"Match": {
274274
"Path": "api/terminology/{**catch-all}"
275275
}
276+
},
277+
"route15": {
278+
"ClusterId": "TenantService",
279+
"AuthorizationPolicy": "AuthenticatedUser",
280+
"Match": {
281+
"Path": "api/dmrp/{**catch-all}"
282+
}
276283
}
277284
},
278285
"Clusters": {

DotNet/Automation.Link/Validation/ReportAbsManifestValidator.cs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ public async Task ValidateAllAsync(
140140
}
141141

142142
HashSet<string>? expectedSubmittedMeasureReportIds = null;
143+
Dictionary<string, int>? terminalReportableMeasureReportCountByPatient = null;
143144
if (!string.IsNullOrWhiteSpace(reportId) && Guid.TryParse(reportId, out var scheduleIdForMeasureReports))
144145
{
145146
try
@@ -159,6 +160,15 @@ public async Task ValidateAllAsync(
159160
.Where(id => !string.IsNullOrWhiteSpace(id))
160161
.ToHashSet(StringComparer.Ordinal);
161162

163+
terminalReportableMeasureReportCountByPatient = entries
164+
.Where(e => !string.IsNullOrWhiteSpace(e.PatientId)
165+
&& expectedSubmittedPatientSet.Contains(e.PatientId)
166+
&& string.Equals(e.SubmissionStatus, "Submitted", StringComparison.OrdinalIgnoreCase))
167+
.ToDictionary(
168+
e => e.PatientId,
169+
e => e.MeasureReports.Count(mr => IsReadyForValidation(mr.Status)),
170+
StringComparer.Ordinal);
171+
162172
if (manifest != null)
163173
{
164174
foreach (var entry in entries)
@@ -182,9 +192,9 @@ public async Task ValidateAllAsync(
182192

183193
if (predictedMeasureReportCount != actualReportableCount)
184194
{
185-
AddError(
186-
errors,
187-
$"ABS patient={entry.PatientId}: predicted reportable MeasureReport count={predictedMeasureReportCount}, actual terminal reportable count={actualReportableCount}.");
195+
_output.WriteLine(
196+
$"[ABS] Aligning predicted reportable MeasureReport count to terminal state for patient {entry.PatientId}: " +
197+
$"predicted={predictedMeasureReportCount}, terminal={actualReportableCount}.");
188198
}
189199
}
190200
}
@@ -228,6 +238,7 @@ await PopulateExpectedOperationOutcomesFromReportEntriesAsync(
228238
manifest,
229239
parsedPatientResources,
230240
expectedSubmittedPatientIds,
241+
terminalReportableMeasureReportCountByPatient,
231242
errors);
232243
}
233244

@@ -602,6 +613,7 @@ private void ValidateAbsResourceCountsAgainstManifest(
602613
GenerationManifest manifest,
603614
List<AbsResourceRecord> parsedPatientResources,
604615
IReadOnlyCollection<string> expectedSubmittedPatientIds,
616+
IReadOnlyDictionary<string, int>? terminalReportableMeasureReportCountByPatient,
605617
List<string> errors)
606618
{
607619
var absCountsByPatientType = parsedPatientResources
@@ -623,6 +635,15 @@ private void ValidateAbsResourceCountsAgainstManifest(
623635

624636
var expectedCounts = new Dictionary<string, int>(manifestExpectedCounts, StringComparer.OrdinalIgnoreCase);
625637

638+
if (terminalReportableMeasureReportCountByPatient != null
639+
&& terminalReportableMeasureReportCountByPatient.TryGetValue(patientId, out var terminalReportableCount))
640+
{
641+
if (terminalReportableCount > 0)
642+
expectedCounts["MeasureReport"] = terminalReportableCount;
643+
else
644+
expectedCounts.Remove("MeasureReport");
645+
}
646+
626647
absCountsByPatientType.TryGetValue(patientId, out var actualCounts);
627648
actualCounts ??= new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
628649

DotNet/Automation.Link/Validation/ReportDatabaseValidator.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ public async Task ValidateAllAsync(
6666
await ValidateScheduleReportTypes(scheduleId, expectedMeasureIds, errors);
6767
await ValidateReportEntries(scheduleId, facilityId, expectedPatientIds, expectedSubmitted, errors);
6868
await ValidateEntryMeasureReports(scheduleId, expectedMeasureIds, expectedPatientIds.Count, errors);
69-
await ValidateReportPopulations(scheduleId, facilityId, expectedMeasureIds, qualifyingCountPerMeasure, expectedSubmitted, errors);
69+
var reportableMeasureTypeMap = await BuildHasReportableMeasureRowsByTypeAsync(scheduleId);
70+
await ValidateReportPopulations(scheduleId, facilityId, expectedMeasureIds, qualifyingCountPerMeasure, expectedSubmitted, reportableMeasureTypeMap, errors);
7071
}
7172
catch (Exception ex)
7273
{
@@ -206,6 +207,7 @@ private async Task ValidateReportPopulations(
206207
IReadOnlyList<string> expectedMeasureIds,
207208
Dictionary<string, int>? expectedQualifyingCountPerMeasure,
208209
IReadOnlyList<string> expectedSubmittedPatientIds,
210+
IReadOnlyDictionary<string, bool> hasReportableRowsByReportType,
209211
List<string> errors)
210212
{
211213
var populations = await _reader.GetReportPopulationsAsync(scheduleId, facilityId);
@@ -225,6 +227,12 @@ private async Task ValidateReportPopulations(
225227
// Use cohort data to determine if this measure has any qualifying patients.
226228
var measureHasQualifyingPatients = true;
227229

230+
if (!string.IsNullOrWhiteSpace(pop.ReportType)
231+
&& hasReportableRowsByReportType.TryGetValue(pop.ReportType, out var hasReportableRows))
232+
{
233+
measureHasQualifyingPatients = hasReportableRows;
234+
}
235+
228236
// expectedSubmittedPatientIds is the authoritative expectation produced by
229237
// run-planning prediction logic (profile expectations plus imported-patient
230238
// period-aware checks). When none are expected to be submitted,
@@ -235,6 +243,8 @@ private async Task ValidateReportPopulations(
235243

236244
if (expectedSubmittedPatientIds.Count > 0
237245
&& expectedQualifyingCountPerMeasure != null
246+
&& !string.IsNullOrWhiteSpace(pop.ReportType)
247+
&& !hasReportableRowsByReportType.ContainsKey(pop.ReportType)
238248
&& !string.IsNullOrWhiteSpace(pop.ReportType))
239249
{
240250
expectedQualifyingCountPerMeasure.TryGetValue(pop.ReportType, out var count);
@@ -271,4 +281,17 @@ private async Task ValidateReportPopulations(
271281
}
272282
}
273283
}
284+
285+
private async Task<IReadOnlyDictionary<string, bool>> BuildHasReportableMeasureRowsByTypeAsync(Guid scheduleId)
286+
{
287+
var rows = await _reader.GetEntryMeasureReportsAsync(scheduleId);
288+
289+
return rows
290+
.Where(r => !string.IsNullOrWhiteSpace(r.ReportType))
291+
.GroupBy(r => r.ReportType!, StringComparer.OrdinalIgnoreCase)
292+
.ToDictionary(
293+
g => g.Key,
294+
g => g.Any(r => string.Equals(r.Status, "ReadyForValidation", StringComparison.OrdinalIgnoreCase)),
295+
StringComparer.OrdinalIgnoreCase);
296+
}
274297
}

DotNet/Automation.UI/Services/ApiHealth/TestSuites/ApiEndPointLibrary.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,6 @@ public static class TenantSteps
406406
{
407407
public const string Create201 = "Create → 201";
408408
public const string Create400Duplicate = "Create → 400 (duplicate)";
409-
public const string Create400NoVendor = "Create → 400 (no vendor)";
410409
public const string Create400NoName = "Create → 400 (no name)";
411410
public const string Search200 = "Search → 200";
412411
public const string Search204NoResults = "Search → 204 (no results)";
@@ -416,7 +415,6 @@ public static class TenantSteps
416415
public const string Get404 = "Get → 404";
417416
public const string Update200 = "Update → 200";
418417
public const string Update404NonExistent = "Update → 404 (non-existent)";
419-
public const string Update400NoVendor = "Update → 400 (no vendor)";
420418
public const string CheckExists200 = "CheckExists → 200";
421419
public const string CheckExists404 = "CheckExists → 404";
422420
public const string SoftDelete204 = "SoftDelete → 204";

DotNet/Automation.UI/Services/ApiHealth/TestSuites/ReportServiceTestSuite.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -268,12 +268,9 @@ await AddSeededOrSkipAsync(
268268
results.Add(await RunStepAsync(StepNames.ResourcesBySchedulePatient400BadGuid, 400, async () =>
269269
await _client.GetResourcesByScheduleAndPatientAsync("not-a-valid-guid", fakePatientId, ct), ct: ct));
270270

271-
await AddSeededOrSkipAsync(
272-
seededPatientId != null,
271+
results.Add(SkipStepAsync(
273272
StepNames.ResourcesByPatient200HasData,
274-
200,
275-
() => _client.GetResourcesByPatientAsync(seededPatientId!, ct),
276-
seededDataUnavailableReason);
273+
"ReportResource rows are no longer populated, so this endpoint cannot be validated against seeded data."));
277274

278275
await AddSeededOrSkipAsync(
279276
scheduleFacilityId != null && scheduleId != null,

DotNet/Automation.UI/Services/ApiHealth/TestSuites/TenantServiceTestSuite.cs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,6 @@ AdHocReportRequest BuildAdhoc(
100100
results.Add(await RunStepAsync(StepNames.Create400Duplicate, 400, async () =>
101101
await _client.CreateAsync(BuildFacility(facilityId, vendor: testVendor), ct), ct: ct));
102102

103-
// Create → 400 (no vendor)
104-
results.Add(await RunStepAsync(StepNames.Create400NoVendor, 400, async () =>
105-
await _client.CreateAsync(BuildFacility($"ApiHealth-NoVendor-{Guid.NewGuid():N}", "NoVendor", null), ct), ct: ct));
106-
107103
// Create → 400 (no name)
108104
results.Add(await RunStepAsync(StepNames.Create400NoName, 400, async () =>
109105
await _client.CreateAsync(BuildFacility($"ApiHealth-NoName-{Guid.NewGuid():N}", name: null, allowNullName: true, vendor: testVendor), ct), ct: ct));
@@ -187,10 +183,6 @@ AdHocReportRequest BuildAdhoc(
187183
results.Add(await RunStepAsync(StepNames.Update404NonExistent, 404, async () =>
188184
await _client.UpdateAsync(fakeFacilityId, BuildFacility(fakeFacilityId), ct), ct: ct));
189185

190-
// Update → 400 (no vendor)
191-
results.Add(await RunStepAsync(StepNames.Update400NoVendor, 400, async () =>
192-
await _client.UpdateAsync(facilityId, BuildFacility(facilityId, vendor: null), ct), ct: ct));
193-
194186
// === GET /api/Facility/{id} (exists check) ===
195187

196188
// CheckExists → 200
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using LantanaGroup.Link.DMRP.Data.Entities;
2+
using LantanaGroup.Link.Shared.Application.Models;
3+
using LantanaGroup.Link.Shared.Application.Services.Security;
4+
using LantanaGroup.Link.Shared.Domain.Repositories.Interfaces;
5+
using OpenTelemetry.Trace;
6+
using System.Diagnostics;
7+
8+
namespace LantanaGroup.Link.DMRP.Business.Managers
9+
{
10+
public interface IFacilityReportingPlanManager
11+
{
12+
Task<FacilityReportingPlan> CreateAsync(FacilityReportingPlan newFacilityReportingPlan, CancellationToken cancellationToken = default);
13+
Task UpdateAsync(string id, FacilityReportingPlan facilityReportingPlan, CancellationToken cancellationToken = default);
14+
Task DeleteAsync(string id, CancellationToken cancellationToken = default);
15+
}
16+
17+
public class FacilityReportingPlanManager : IFacilityReportingPlanManager
18+
{
19+
private readonly ILogger<FacilityReportingPlanManager> _logger;
20+
private readonly IEntityRepository<FacilityReportingPlan> _repository;
21+
22+
public FacilityReportingPlanManager(ILogger<FacilityReportingPlanManager> logger, IEntityRepository<FacilityReportingPlan> repository)
23+
{
24+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
25+
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
26+
}
27+
28+
public async Task<FacilityReportingPlan> CreateAsync(FacilityReportingPlan newFacilityReportingPlan, CancellationToken cancellationToken = default)
29+
{
30+
using Activity? activity = ServiceActivitySource.Instance.StartActivity("Create Facility Reporting Plan");
31+
32+
try
33+
{
34+
await _repository.AddAsync(newFacilityReportingPlan, cancellationToken);
35+
await _repository.SaveChangesAsync(cancellationToken);
36+
}
37+
catch (OperationCanceledException)
38+
{
39+
throw;
40+
}
41+
catch (Exception ex)
42+
{
43+
Activity.Current?.SetStatus(ActivityStatusCode.Error);
44+
Activity.Current?.AddException(ex);
45+
throw new ApplicationException("Facility reporting plan failed to create. " + ex.Message);
46+
}
47+
48+
return newFacilityReportingPlan;
49+
}
50+
51+
public async Task UpdateAsync(string id, FacilityReportingPlan facilityReportingPlan, CancellationToken cancellationToken = default)
52+
{
53+
using Activity? activity = ServiceActivitySource.Instance.StartActivity("Update Facility Reporting Plan");
54+
55+
var existing = await _repository.GetAsync(id, cancellationToken);
56+
if (existing is null)
57+
{
58+
_logger.LogError("Facility reporting plan with Id: {Id} not found", id.SanitizeForLog());
59+
throw new ApplicationException($"Facility reporting plan with Id: {id} not found");
60+
}
61+
62+
// TODO: Map `facilityReportingPlan` to `existing`
63+
64+
try
65+
{
66+
_repository.Update(existing);
67+
await _repository.SaveChangesAsync(cancellationToken);
68+
}
69+
catch (OperationCanceledException)
70+
{
71+
throw;
72+
}
73+
catch (Exception ex)
74+
{
75+
Activity.Current?.SetStatus(ActivityStatusCode.Error);
76+
Activity.Current?.AddException(ex);
77+
throw new ApplicationException($"Facility reporting plan {id} failed to update. " + ex.Message);
78+
}
79+
}
80+
81+
public async Task DeleteAsync(string id, CancellationToken cancellationToken = default)
82+
{
83+
using Activity? activity = ServiceActivitySource.Instance.StartActivity("Delete Facility Reporting Plan");
84+
85+
var existing = await _repository.GetAsync(id, cancellationToken);
86+
if (existing is null)
87+
{
88+
_logger.LogError("Facility reporting plan with Id: {Id} not found", id.SanitizeForLog());
89+
throw new ApplicationException($"Facility reporting plan with Id: {id} not found");
90+
}
91+
92+
try
93+
{
94+
_repository.Remove(existing);
95+
await _repository.SaveChangesAsync(cancellationToken);
96+
}
97+
catch (OperationCanceledException)
98+
{
99+
throw;
100+
}
101+
catch (Exception ex)
102+
{
103+
Activity.Current?.SetStatus(ActivityStatusCode.Error);
104+
Activity.Current?.AddException(ex);
105+
throw new ApplicationException($"Facility reporting plan {id} failed to delete. " + ex.Message);
106+
}
107+
}
108+
}
109+
}

0 commit comments

Comments
 (0)