Skip to content

Commit fcf39ec

Browse files
pchoi-aljsed-nhs
andauthored
APPT-2014 Added Regional Name, ICB Name and Accessibility attributes to the Master Site Report (#1632)
# Description Regional Name, ICB Name and Accessibility attributes were added to the Master Site Report. The accessibility display as a true or false. - Site Name - ODS Code - Site Type - Region - **Regional Name** - ICB - **ICB Name** - GUID - IsDeleted - Status - Long - Lat - Address - **Accessible toilet** - **Braille translation service** - **Disabled car parking** - **Car parking** - **Induction loop** - **Sign language service** - **Step free access** - **Text relay** - **Wheelchair access** Fixes # (issue) # Checklist: - [ ] My work is behind a feature toggle (if appropriate) - [ ] If my work is behind a feature toggle, I've added a full suite of tests for both the ON and OFF state - [ ] The ticket number is in the Pull Request title, with format "APPT-XXX: My Title Here" - [ ] I have ran npm tsc / lint (in the future these will be ran automatically) - [ ] My code generates no new .NET warnings (in the future these will be treated as errors) - [ ] If I've added a new Function, it is disabled in all but one of the terraform groups (e.g. http_functions) - [ ] If I've added a new Function, it has both unit and integration tests. Any request body validators have unit tests also - [ ] If I've made UI changes, I've added appropriate Playwright and Jest tests - [ ] If I've added/updated an end-point, I've added the appropriate annotations and tested the Swagger documentation reflects the change --------- Co-authored-by: jsed-nhs <193929923+jsed-nhs@users.noreply.github.com>
1 parent 28b6fd5 commit fcf39ec

38 files changed

Lines changed: 603 additions & 314 deletions

File tree

src/api/Nhs.Appointments.Api/Functions/HttpFunctions/GetReportMasterSiteList.cs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,22 @@
99
using Nhs.Appointments.Api.Models;
1010
using Nhs.Appointments.Core.Features;
1111
using Nhs.Appointments.Core.Inspectors;
12+
using Nhs.Appointments.Core.OdsCodes;
1213
using Nhs.Appointments.Core.Reports.MasterSiteList;
1314
using Nhs.Appointments.Core.Sites;
1415
using Nhs.Appointments.Core.Users;
1516
using System;
1617
using System.Collections.Generic;
18+
using System.Linq;
1719
using System.Net;
1820
using System.Threading.Tasks;
1921

2022
namespace Nhs.Appointments.Api.Functions.HttpFunctions;
2123

2224
public class GetReportMasterSiteListFunction(
2325
ISiteService siteService,
26+
IWellKnowOdsCodesService wellKnowOdsCodesService,
27+
IAccessibilityDefinitionsService accessibilityDefinitionsService,
2428
IMasterSiteListReportCsvWriter masterSiteListReportCsvWriter,
2529
IFeatureToggleHelper featureToggleHelper,
2630
IValidator<EmptyRequest> validator,
@@ -50,12 +54,27 @@ public override async Task<IActionResult> RunAsync(
5054

5155
protected override string ResponseType => ApiResponseType.File;
5256

53-
protected override async Task<ApiResult<FileResponse>> HandleRequest(EmptyRequest request,
54-
ILogger logger)
57+
protected override async Task<ApiResult<FileResponse>> HandleRequest(EmptyRequest request, ILogger logger)
5558
{
5659
var sites = await siteService.GetAllSites(includeDeleted: true, ignoreCache: true);
5760

58-
var csv = await masterSiteListReportCsvWriter.CompileMasterSiteListReportCsv(sites);
61+
// Fetch ODS lookups AND Accessibility Definitions
62+
var odsLookup = await wellKnowOdsCodesService.GetWellKnownOdsCodeEntries();
63+
var accessibilityDefinitions = await accessibilityDefinitionsService.GetAccessibilityDefinitions();
64+
65+
// Map to SiteForReport
66+
var sitesForReport = sites.Select(site =>
67+
{
68+
var regionalName = odsLookup.FirstOrDefault(l => l.OdsCode == site.Region)?.DisplayName ?? "";
69+
var icbName = odsLookup.FirstOrDefault(l => l.OdsCode == site.IntegratedCareBoard)?.DisplayName ?? "";
70+
71+
return new SiteForReport(site, regionalName, icbName);
72+
});
73+
74+
// Pass the enriched collection to the writer
75+
// If your CsvWriter needs the definitions to build headers,
76+
// you would pass 'accessibilityDefinitions' here too.
77+
var csv = await masterSiteListReportCsvWriter.CompileMasterSiteListReportCsv(sitesForReport, accessibilityDefinitions);
5978

6079
return ApiResult<FileResponse>.Success(new FileResponse(csv.fileName, csv.fileContent));
6180
}

src/api/Nhs.Appointments.Core/Reports/MasterSiteList/IMasterSiteListReportCsvWriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ namespace Nhs.Appointments.Core.Reports.MasterSiteList;
44

55
public interface IMasterSiteListReportCsvWriter
66
{
7-
Task<(string fileName, MemoryStream fileContent)> CompileMasterSiteListReportCsv(IEnumerable<Site> sites);
7+
Task<(string fileName, MemoryStream fileContent)> CompileMasterSiteListReportCsv(IEnumerable<SiteForReport> sites, IEnumerable<AccessibilityDefinition> definitions);
88
}
Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,56 @@
1+
using MassTransit;
12
using Nhs.Appointments.Core.Reports.Helpers;
23
using Nhs.Appointments.Core.Sites;
34

45
namespace Nhs.Appointments.Core.Reports.MasterSiteList;
56

67
public class MasterSiteListReportCsvWriter(TimeProvider timeProvider) : IMasterSiteListReportCsvWriter
78
{
8-
public async Task<(string fileName, MemoryStream fileContent)> CompileMasterSiteListReportCsv(IEnumerable<Site> sites)
9+
public async Task<(string fileName, MemoryStream fileContent)> CompileMasterSiteListReportCsv(IEnumerable<SiteForReport> sites, IEnumerable<AccessibilityDefinition> accessibilityDefinitions)
910
{
1011
var fileName = BuildFileName();
1112

1213
var memoryStream = new MemoryStream();
1314
await using var streamWriter = new StreamWriter(memoryStream);
14-
await CompileCsv(streamWriter, sites);
15+
await CompileCsv(streamWriter, sites, accessibilityDefinitions);
1516
return (fileName, memoryStream);
1617
}
1718

1819
private string BuildFileName() =>
1920
$"MasterSiteListReport_{timeProvider.GetUtcNow():yyyyMMddhhmmss}.csv";
2021

21-
private async Task CompileCsv(TextWriter csvWriter, IEnumerable<Site> sites)
22+
private static async Task CompileCsv(StreamWriter csvWriter, IEnumerable<SiteForReport> sites, IEnumerable<AccessibilityDefinition> accessibilityDefinitions)
2223
{
23-
await csvWriter.WriteLineAsync(string.Join(',', MasterSiteListReportMap.Headers()));
24+
var headers = MasterSiteListReportMap.Headers(accessibilityDefinitions);
25+
await csvWriter.WriteLineAsync(string.Join(',', headers));
2426

2527
foreach (var site in sites)
2628
{
27-
try
29+
var row = new List<string>
2830
{
29-
await csvWriter.WriteLineAsync(string.Join(',',
30-
CsvFormatter.FormatValue(MasterSiteListReportMap.SiteName(site)),
31-
CsvFormatter.FormatValue(MasterSiteListReportMap.OdsCode(site)),
32-
CsvFormatter.FormatValue(MasterSiteListReportMap.SiteType(site)),
33-
CsvFormatter.FormatValue(MasterSiteListReportMap.Region(site)),
34-
CsvFormatter.FormatValue(MasterSiteListReportMap.ICB(site)),
35-
CsvFormatter.FormatValue(MasterSiteListReportMap.Guid(site)),
36-
MasterSiteListReportMap.IsDeleted(site),
37-
CsvFormatter.FormatValue(MasterSiteListReportMap.Status(site)),
38-
MasterSiteListReportMap.Longitude(site),
39-
MasterSiteListReportMap.Latitude(site),
40-
CsvFormatter.FormatValue(MasterSiteListReportMap.Address(site))
41-
));
42-
43-
}
44-
catch(Exception ex)
31+
CsvFormatter.FormatValue(MasterSiteListReportMap.SiteName(site)),
32+
CsvFormatter.FormatValue(MasterSiteListReportMap.OdsCode(site)),
33+
CsvFormatter.FormatValue(MasterSiteListReportMap.SiteType(site)),
34+
CsvFormatter.FormatValue(MasterSiteListReportMap.Region(site)),
35+
CsvFormatter.FormatValue(MasterSiteListReportMap.RegionalName(site)),
36+
CsvFormatter.FormatValue(MasterSiteListReportMap.ICB(site)),
37+
CsvFormatter.FormatValue(MasterSiteListReportMap.IcbName(site)),
38+
CsvFormatter.FormatValue(MasterSiteListReportMap.Guid(site)),
39+
MasterSiteListReportMap.IsDeleted(site).ToString().ToLower(),
40+
CsvFormatter.FormatValue(MasterSiteListReportMap.Status(site)),
41+
MasterSiteListReportMap.Longitude(site).ToString(),
42+
MasterSiteListReportMap.Latitude(site).ToString(),
43+
CsvFormatter.FormatValue(MasterSiteListReportMap.Address(site))
44+
};
45+
46+
// Dynamically add the accessibility values for the remaining headers
47+
var accessibilityHeaders = headers.Skip(13);
48+
foreach (var headerName in accessibilityHeaders)
4549
{
46-
50+
row.Add(MasterSiteListReportMap.GetAccessibilityValue(site, headerName, accessibilityDefinitions));
4751
}
52+
53+
await csvWriter.WriteLineAsync(string.Join(',', row));
4854
}
4955
}
5056
}

src/api/Nhs.Appointments.Core/Reports/MasterSiteList/MasterSiteListReportMap.cs

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,41 @@ namespace Nhs.Appointments.Core.Reports.MasterSiteList;
44

55
public static class MasterSiteListReportMap
66
{
7-
public static string[] Headers()
7+
public static string[] Headers(IEnumerable<AccessibilityDefinition> accessibilityDefinitions)
88
{
9-
var siteHeaders = new[]
9+
var headers = new List<string>
1010
{
11-
"Site Name", "ODS Code", "Site Type", "Region", "ICB", "GUID", "IsDeleted", "Status", "Long", "Lat", "Address"
11+
"Site Name", "ODS Code", "Site Type", "Region", "Regional Name",
12+
"ICB", "ICB Name", "GUID", "IsDeleted", "Status", "Long", "Lat", "Address"
1213
};
1314

14-
return siteHeaders;
15+
headers.AddRange(accessibilityDefinitions.Select(d => d.DisplayName));
16+
return headers.ToArray();
1517
}
1618

17-
public static string SiteName(Site site) => site.Name;
18-
public static string OdsCode(Site site) => site.OdsCode;
19-
public static string SiteType(Site site) => site.Type;
20-
public static string Region(Site site) => site.Region;
21-
public static string ICB(Site site) => site.IntegratedCareBoard;
22-
public static string Guid(Site site) => site.Id;
23-
public static bool IsDeleted(Site site) => site.isDeleted ?? false;
24-
public static string Status(Site site) => site.status is not null ? site.status.ToString() : SiteStatus.Online.ToString();
25-
public static double Longitude(Site site) => site.Coordinates?.Longitude ?? 0;
26-
public static double Latitude(Site site) => site.Coordinates?.Latitude ?? 0;
27-
public static string Address(Site site) => site.Address;
19+
public static string SiteName(SiteForReport site) => site.Name;
20+
public static string OdsCode(SiteForReport site) => site.OdsCode;
21+
public static string SiteType(SiteForReport site) => site.Type;
22+
public static string Region(SiteForReport site) => site.Region;
23+
public static string RegionalName(SiteForReport site) => site.RegionalName;
24+
public static string ICB(SiteForReport site) => site.IntegratedCareBoard;
25+
public static string IcbName(SiteForReport site) => site.IntegratedCareBoardName;
26+
public static string Guid(SiteForReport site) => site.Id;
27+
public static bool IsDeleted(SiteForReport site) => site.isDeleted ?? false;
28+
public static string Status(SiteForReport site) => site.status?.ToString() ?? SiteStatus.Online.ToString();
29+
public static double Longitude(SiteForReport site) => site.Coordinates?.Longitude ?? 0;
30+
public static double Latitude(SiteForReport site) => site.Coordinates?.Latitude ?? 0;
31+
public static string Address(SiteForReport site) => site.Address;
2832

33+
public static string GetAccessibilityValue(SiteForReport site, string headerName, IEnumerable<AccessibilityDefinition> accessibilityDefinitions)
34+
{
35+
var accessibilityDefinition = accessibilityDefinitions.FirstOrDefault(d => d.DisplayName == headerName);
36+
if (accessibilityDefinition == null)
37+
{
38+
return "false";
39+
}
40+
41+
var match = site.Accessibilities?.FirstOrDefault(a => a.Id == accessibilityDefinition.Id);
42+
return (match != null && match.Value == "true") ? "true" : "false";
43+
}
2944
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using Nhs.Appointments.Core.Sites;
2+
3+
namespace Nhs.Appointments.Core.Reports.MasterSiteList;
4+
5+
public record SiteForReport(
6+
Site Site,
7+
string RegionalName,
8+
string IntegratedCareBoardName) : Site(Site.Id, Site.Name, Site.Address, Site.PhoneNumber, Site.OdsCode, Site.Region, Site.IntegratedCareBoard, Site.InformationForCitizens, Site.Accessibilities, Site.location, Site.status, Site.isDeleted, Site.Type);

src/api/Nhs.Appointments.Persistance/SiteStore.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public async Task<int> GetReferenceNumberGroup(string site)
2828
public async Task<OperationResult> UpdateSiteReferenceDetails(string siteId, string odsCode, string icb, string region)
2929
{
3030
var originalDocument = await GetOrDefault(siteId);
31-
if (originalDocument == null ||
31+
if (originalDocument == null ||
3232
!ValidateUpdateToSiteAllowed(originalDocument))
3333
{
3434
return new OperationResult(false, "The specified site was not found.");
@@ -89,7 +89,7 @@ public async Task<OperationResult> UpdateSiteDetails(string siteId, string name,
8989
decimal? longitude, decimal? latitude, string type = null)
9090
{
9191
decimal?[] coords = (longitude != null) & (latitude != null) ? [longitude, latitude] : [];
92-
92+
9393
var originalDocument = await GetOrDefault(siteId);
9494
if (originalDocument == null ||
9595
!ValidateUpdateToSiteAllowed(originalDocument))
@@ -117,7 +117,7 @@ public async Task<OperationResult> UpdateSiteDetails(string siteId, string name,
117117
await cosmosStore.PatchDocument(documentType, siteId, [.. detailsPatchOperations]);
118118
return new OperationResult(true);
119119
}
120-
120+
121121
private async Task<Site> GetOrDefault(string siteId)
122122
{
123123
try
@@ -196,7 +196,7 @@ public async Task<OperationResult> UpdateSiteStatusAsync(string siteId, SiteStat
196196
? PatchOperation.Add("/status", status)
197197
: PatchOperation.Replace("/status", status);
198198

199-
PatchOperation[] patchOperations = [ patchOperation ];
199+
PatchOperation[] patchOperations = [patchOperation];
200200

201201
await cosmosStore.PatchDocument(documentType, siteId, patchOperations);
202202
return new OperationResult(true);
@@ -223,7 +223,7 @@ public async Task<OperationResult> ToggleSiteSoftDeletionAsync(string siteId)
223223
: PatchOperation.Replace("/isDeleted", !originalDocument.isDeleted);
224224

225225
await cosmosStore.PatchDocument(docType, siteId, [patchOperation]);
226-
return new OperationResult(true);
226+
return new OperationResult(true);
227227
}
228228

229229
private static Site MapToSite(SiteDocument siteDocument)

src/client/testing/tests-v2/reports/download-reports.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,11 +293,22 @@ const expectedAllSitesReportHeaders = [
293293
'ODS Code',
294294
'Site Type',
295295
'Region',
296+
'Regional Name',
296297
'ICB',
298+
'ICB Name',
297299
'GUID',
298300
'IsDeleted',
299301
'Status',
300302
'Long',
301303
'Lat',
302304
'Address',
305+
'Accessible toilet',
306+
'Braille translation service',
307+
'Disabled car parking',
308+
'Car parking',
309+
'Induction loop',
310+
'Sign language service',
311+
'Step free access',
312+
'Text relay',
313+
'Wheelchair access',
303314
];

tests/Nhs.Appointments.Api.Integration/Scenarios/AccessibilityDefinitions/AccessibilityDefinitions.feature

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,25 @@ Feature: Get Accessibility Definitions
22

33
Scenario: Retrieve list of accessibility definitions
44
Given There are existing system accessibilities
5-
| Id | DisplayName |
6-
| definition_one/accessibility_one | Accessibility One |
7-
| definition_one/accessibility_two | Accessibility Two |
8-
| definition_two/accessibility_one | Accessibility One |
9-
| definition_two/accessibility_two | Accessibility Two |
5+
| Id | DisplayName |
6+
| accessibility/accessible_toilet | Accessible toilet |
7+
| accessibility/braille_translation_service | Braille translation service |
8+
| accessibility/disabled_car_parking | Disabled car parking |
9+
| accessibility/car_parking | Car parking |
10+
| accessibility/induction_loop | Induction loop |
11+
| accessibility/sign_language_service | Sign language service |
12+
| accessibility/step_free_access | Step free access |
13+
| accessibility/text_relay | Text relay |
14+
| accessibility/wheelchair_access | Wheelchair access |
1015
When I query for all accessibility definitions
1116
Then the following accessibility definitions are returned
12-
| Id | DisplayName |
13-
| definition_one/accessibility_one | Accessibility One |
14-
| definition_one/accessibility_two | Accessibility Two |
15-
| definition_two/accessibility_one | Accessibility One |
16-
| definition_two/accessibility_two | Accessibility Two |
17+
| Id | DisplayName |
18+
| accessibility/accessible_toilet | Accessible toilet |
19+
| accessibility/braille_translation_service | Braille translation service |
20+
| accessibility/disabled_car_parking | Disabled car parking |
21+
| accessibility/car_parking | Car parking |
22+
| accessibility/induction_loop | Induction loop |
23+
| accessibility/sign_language_service | Sign language service |
24+
| accessibility/step_free_access | Step free access |
25+
| accessibility/text_relay | Text relay |
26+
| accessibility/wheelchair_access | Wheelchair access |

tests/Nhs.Appointments.Api.Integration/Scenarios/BaseFeatureSteps.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Azure.Storage.Blobs;
1111
using FluentAssertions;
1212
using Gherkin.Ast;
13+
using MassTransit;
1314
using Microsoft.Azure.Cosmos;
1415
using Microsoft.Azure.Cosmos.Linq;
1516
using Microsoft.Extensions.Logging;
@@ -24,6 +25,15 @@
2425
using Nhs.Appointments.Core.Sites;
2526
using Nhs.Appointments.Persistance;
2627
using Nhs.Appointments.Persistance.Models;
28+
using System;
29+
using System.Collections.Generic;
30+
using System.Linq;
31+
using System.Linq.Expressions;
32+
using System.Net;
33+
using System.Net.Http;
34+
using System.Text.RegularExpressions;
35+
using System.Threading;
36+
using System.Threading.Tasks;
2737
using Xunit;
2838
using Xunit.Gherkin.Quick;
2939
using Feature = Xunit.Gherkin.Quick.Feature;
@@ -940,14 +950,17 @@ public async Task AssertSessionNoLongerExists(string dateString)
940950
[And("the following sites exist in the system")]
941951
public async Task SetUpSites(DataTable dataTable)
942952
{
953+
// Extract headers from the first row to see what columns are available
954+
var headers = dataTable.Rows.First().Cells.Select(c => c.Value).ToList();
955+
943956
var sites = dataTable.Rows.Skip(1).Select(row => new SiteDocument
944957
{
945958
Id = GetSiteId(dataTable.GetRowValueOrDefault(row, "Site")),
946959
Name = dataTable.GetRowValueOrDefault(row, "Name"),
947960
Address = dataTable.GetRowValueOrDefault(row, "Address"),
948961
PhoneNumber = dataTable.GetRowValueOrDefault(row, "PhoneNumber"),
949962
OdsCode = dataTable.GetRowValueOrDefault(row, "OdsCode"),
950-
Region = dataTable.GetRowValueOrDefault(row, "Region"),
963+
Region = dataTable.GetRowValueOrDefault(row, "Region"),
951964
IntegratedCareBoard = dataTable.GetRowValueOrDefault(row, "ICB"),
952965
InformationForCitizens = dataTable.GetRowValueOrDefault(row, "InformationForCitizens"),
953966
DocumentType = "site",
@@ -974,6 +987,8 @@ public async Task SetUpSites(DataTable dataTable)
974987
[And("the following default site exists in the system")]
975988
public async Task SetUpSingleDefaultSite(DataTable dataTable)
976989
{
990+
var headers = dataTable.Rows.First().Cells.Select(c => c.Value).ToList();
991+
977992
var site = dataTable.Rows.Skip(1).Take(1).Select(row => new SiteDocument
978993
{
979994
Id = GetSiteId(),
@@ -1005,6 +1020,8 @@ public async Task SetUpSingleDefaultSite(DataTable dataTable)
10051020
[Given("the following sites with valid Guid IDs exist in the system")]
10061021
public async Task SetupSitesWithValidGuidIds(DataTable dataTable)
10071022
{
1023+
var headers = dataTable.Rows.First().Cells.Select(c => c.Value).ToList();
1024+
10081025
var sites = dataTable.Rows.Skip(1).Select(row => new SiteDocument
10091026
{
10101027
Id = dataTable.GetRowValueOrDefault(row, "Site"),

tests/Nhs.Appointments.Api.Integration/Scenarios/Reports/MasterSiteList/GetMasterSiteListReport_Disabled.feature

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Feature: Download Master Site List Report
22

33
Scenario: Cannot download master site list report when toggle is disabled
44
Given the following sites exist in the system
5-
| Site | Name | Address | PhoneNumber | OdsCode | Region | ICB | InformationForCitizens | Accessibilities | Longitude | Latitude | Type | IsDeleted |
6-
| d8ce8d0a-6c95-421b-9136-9865fba555a1 | Site-1 | 1 Roadside | 0113 1111111 | J12 | R1 | ICB1 | Info 1 | accessibility/attr_one=true | -60 | -60 | GP Practice | false |
5+
| Site | Name | Address | PhoneNumber | OdsCode | Region | ICB | InformationForCitizens | Accessibilities | Longitude | Latitude | Type | IsDeleted | Status |
6+
| d8ce8d0a-6c95-421b-9136-9865fba555a1 | Site-1 | 1 Roadside | 0113 1111111 | J12 | R1 | ICB1 | Info 1 | accessibility/attr_one=true | -60 | -60 | GP Practice | false | Online |
77
When I request master site list report
88
Then the call should fail with 501

0 commit comments

Comments
 (0)