diff --git a/BinDays.Api.Collectors/Collectors/Councils/SalfordCityCouncil.cs b/BinDays.Api.Collectors/Collectors/Councils/SalfordCityCouncil.cs new file mode 100644 index 0000000..10d4674 --- /dev/null +++ b/BinDays.Api.Collectors/Collectors/Councils/SalfordCityCouncil.cs @@ -0,0 +1,187 @@ +namespace BinDays.Api.Collectors.Collectors.Councils; + +using BinDays.Api.Collectors.Collectors.Vendors; +using BinDays.Api.Collectors.Models; +using BinDays.Api.Collectors.Utilities; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text.Json; +using System.Text.RegularExpressions; + +/// +/// Collector implementation for Salford City Council. +/// +internal sealed partial class SalfordCityCouncil : GovUkCollectorBase, ICollector +{ + /// + public string Name => "Salford City Council"; + + /// + public Uri WebsiteUrl => new("https://www.salford.gov.uk"); + + /// + public override string GovUkId => "salford"; + + /// + /// The list of bin types for this collector. + /// + private readonly IReadOnlyCollection _binTypes = + [ + new() + { + Name = "Household Waste", + Colour = BinColour.Black, + Keys = [ "Black bin" ], + }, + new() + { + Name = "Food and Garden Waste", + Colour = BinColour.Pink, + Keys = [ "Pink lidded bin" ], + }, + new() + { + Name = "Paper and Cardboard", + Colour = BinColour.Blue, + Keys = [ "Blue bin" ], + }, + new() + { + Name = "Glass, Cans and Plastics", + Colour = BinColour.Brown, + Keys = [ "Brown bin" ], + }, + ]; + + /// + /// Regex for ICS events. + /// + [GeneratedRegex(@"SUMMARY:(?.+?)\r?\n.*?DTSTART; ?VALUE ?= ?DATE:(?\d{8})", RegexOptions.Singleline)] + private static partial Regex BinEventRegex(); + + /// + public GetAddressesResponse GetAddresses(string postcode, ClientSideResponse? clientSideResponse) + { + // Prepare client-side request for getting addresses + if (clientSideResponse == null) + { + var clientSideRequest = new ClientSideRequest + { + RequestId = 1, + Url = "https://www.salford.gov.uk/umbraco/api/SalfordAPI/AddressSearch", + Method = "POST", + Headers = new() + { + { "user-agent", Constants.UserAgent }, + { "content-type", "application/x-www-form-urlencoded; charset=UTF-8" }, + }, + Body = $"QueryStr={postcode}", + }; + + var getAddressesResponse = new GetAddressesResponse + { + NextClientSideRequest = clientSideRequest, + }; + + return getAddressesResponse; + } + // Process addresses from response + else if (clientSideResponse.RequestId == 1) + { + using var addressesJson = JsonDocument.Parse(clientSideResponse.Content); + var addressesElement = addressesJson.RootElement.GetProperty("addresses").EnumerateArray(); + + // Iterate through each address, and create a new address object + var addresses = new List
(); + foreach (var addressElement in addressesElement) + { + var address = new Address + { + Property = addressElement.GetProperty("address").GetString()!.Trim(), + Postcode = postcode, + Uid = addressElement.GetProperty("uprn").GetString()!, + }; + + addresses.Add(address); + } + + var getAddressesResponse = new GetAddressesResponse + { + Addresses = [.. addresses], + }; + + return getAddressesResponse; + } + + // Throw exception for invalid request + throw new InvalidOperationException("Invalid client-side request."); + } + + /// + public GetBinDaysResponse GetBinDays(Address address, ClientSideResponse? clientSideResponse) + { + // Prepare client-side request for getting bin days + if (clientSideResponse == null) + { + var clientSideRequest = new ClientSideRequest + { + RequestId = 1, + Url = $"https://www.salford.gov.uk/umbraco/api/salfordapi/GetBinCollectionsICS/?UPRN={address.Uid}", + Method = "GET", + Headers = new() + { + { "user-agent", Constants.UserAgent }, + }, + }; + + var getBinDaysResponse = new GetBinDaysResponse + { + NextClientSideRequest = clientSideRequest, + }; + + return getBinDaysResponse; + } + // Process bin days from response + else if (clientSideResponse.RequestId == 1) + { + var rawCollections = BinEventRegex().Matches(clientSideResponse.Content)!; + + // Iterate through each bin collection, and create a new bin day object + var binDays = new List(); + foreach (Match rawCollection in rawCollections) + { + var summary = rawCollection.Groups["summary"].Value.Trim(); + var dateString = rawCollection.Groups["date"].Value; + + var date = DateOnly.ParseExact( + dateString, + "yyyyMMdd", + CultureInfo.InvariantCulture, + DateTimeStyles.None + ); + + var matchedBins = ProcessingUtilities.GetMatchingBins(_binTypes, summary); + + var binDay = new BinDay + { + Date = date, + Address = address, + Bins = matchedBins, + }; + + binDays.Add(binDay); + } + + var getBinDaysResponse = new GetBinDaysResponse + { + BinDays = ProcessingUtilities.ProcessBinDays(binDays), + }; + + return getBinDaysResponse; + } + + // Throw exception for invalid request + throw new InvalidOperationException("Invalid client-side request."); + } +} diff --git a/BinDays.Api.IntegrationTests/Collectors/Councils/SalfordCityCouncilTests.cs b/BinDays.Api.IntegrationTests/Collectors/Councils/SalfordCityCouncilTests.cs new file mode 100644 index 0000000..6674684 --- /dev/null +++ b/BinDays.Api.IntegrationTests/Collectors/Councils/SalfordCityCouncilTests.cs @@ -0,0 +1,36 @@ +namespace BinDays.Api.IntegrationTests.Collectors.Councils; + +using BinDays.Api.Collectors.Collectors; +using BinDays.Api.Collectors.Collectors.Councils; +using BinDays.Api.Collectors.Services; +using BinDays.Api.IntegrationTests.Helpers; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +public class SalfordCityCouncilTests +{ + private readonly IntegrationTestClient _client; + private static readonly ICollector _collector = new SalfordCityCouncil(); + private readonly CollectorService _collectorService = new([_collector]); + private readonly ITestOutputHelper _outputHelper; + + public SalfordCityCouncilTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + _client = new IntegrationTestClient(outputHelper); + } + + [Theory] + [InlineData("M27 9LF")] + public async Task GetBinDaysTest(string postcode) + { + await TestSteps.EndToEnd( + _client, + _collectorService, + _collector, + postcode, + _outputHelper + ); + } +}