From b0a07bfcf9bebab60e96d96a1f4181275cd71805 Mon Sep 17 00:00:00 2001 From: Moley-Bot Date: Sun, 1 Feb 2026 10:24:29 +0000 Subject: [PATCH 1/3] Add collector for SalfordCityCouncil Closes #132 Generated with Codex CLI by Moley-Bot --- .../Collectors/Councils/SalfordCityCouncil.cs | 187 ++++++++++++++++++ .../Councils/SalfordCityCouncilTests.cs | 36 ++++ 2 files changed, 223 insertions(+) create mode 100644 BinDays.Api.Collectors/Collectors/Councils/SalfordCityCouncil.cs create mode 100644 BinDays.Api.IntegrationTests/Collectors/Councils/SalfordCityCouncilTests.cs diff --git a/BinDays.Api.Collectors/Collectors/Councils/SalfordCityCouncil.cs b/BinDays.Api.Collectors/Collectors/Councils/SalfordCityCouncil.cs new file mode 100644 index 0000000..c140efa --- /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", "Domestic Waste" ], + }, + new() + { + Name = "Food and Garden Waste", + Colour = BinColour.Pink, + Keys = [ "Pink lidded bin", "Food and Garden Waste" ], + }, + new() + { + Name = "Paper and Cardboard", + Colour = BinColour.Blue, + Keys = [ "Blue bin", "Paper and Card" ], + }, + new() + { + Name = "Glass, Cans and Plastics", + Colour = BinColour.Brown, + Keys = [ "Brown bin", "Bottle and Can" ], + }, + ]; + + /// + /// 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..56a89cf --- /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 + ); + } +} From 0803f86f1d6240cfdf2239afa778cc7b08f41002 Mon Sep 17 00:00:00 2001 From: Moley-Bot Date: Sun, 1 Feb 2026 10:25:13 +0000 Subject: [PATCH 2/3] Auto-format code with dotnet format Formatted by Moley-Bot --- .../Collectors/Councils/SalfordCityCouncil.cs | 374 +++++++++--------- .../Councils/SalfordCityCouncilTests.cs | 72 ++-- 2 files changed, 223 insertions(+), 223 deletions(-) diff --git a/BinDays.Api.Collectors/Collectors/Councils/SalfordCityCouncil.cs b/BinDays.Api.Collectors/Collectors/Councils/SalfordCityCouncil.cs index c140efa..4822d7c 100644 --- a/BinDays.Api.Collectors/Collectors/Councils/SalfordCityCouncil.cs +++ b/BinDays.Api.Collectors/Collectors/Councils/SalfordCityCouncil.cs @@ -1,187 +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", "Domestic Waste" ], - }, - new() - { - Name = "Food and Garden Waste", - Colour = BinColour.Pink, - Keys = [ "Pink lidded bin", "Food and Garden Waste" ], - }, - new() - { - Name = "Paper and Cardboard", - Colour = BinColour.Blue, - Keys = [ "Blue bin", "Paper and Card" ], - }, - new() - { - Name = "Glass, Cans and Plastics", - Colour = BinColour.Brown, - Keys = [ "Brown bin", "Bottle and Can" ], - }, - ]; - - /// - /// 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."); - } -} +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", "Domestic Waste" ], + }, + new() + { + Name = "Food and Garden Waste", + Colour = BinColour.Pink, + Keys = [ "Pink lidded bin", "Food and Garden Waste" ], + }, + new() + { + Name = "Paper and Cardboard", + Colour = BinColour.Blue, + Keys = [ "Blue bin", "Paper and Card" ], + }, + new() + { + Name = "Glass, Cans and Plastics", + Colour = BinColour.Brown, + Keys = [ "Brown bin", "Bottle and Can" ], + }, + ]; + + /// + /// 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 index 56a89cf..6674684 100644 --- a/BinDays.Api.IntegrationTests/Collectors/Councils/SalfordCityCouncilTests.cs +++ b/BinDays.Api.IntegrationTests/Collectors/Councils/SalfordCityCouncilTests.cs @@ -1,36 +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 - ); - } -} +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 + ); + } +} From e09a8f8f38eccfc6de0889ac737d2d9e6c487491 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 21:53:13 +0000 Subject: [PATCH 3/3] Remove unnecessary keys from SalfordCityCouncil bin types Per the style guide, only include keys that are actually matched against the data source. Removed descriptive keys since ICS calendar SUMMARY fields typically use concise color-based names. Co-authored-by: Andrew Riggs --- .../Collectors/Councils/SalfordCityCouncil.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BinDays.Api.Collectors/Collectors/Councils/SalfordCityCouncil.cs b/BinDays.Api.Collectors/Collectors/Councils/SalfordCityCouncil.cs index 4822d7c..10d4674 100644 --- a/BinDays.Api.Collectors/Collectors/Councils/SalfordCityCouncil.cs +++ b/BinDays.Api.Collectors/Collectors/Councils/SalfordCityCouncil.cs @@ -32,25 +32,25 @@ internal sealed partial class SalfordCityCouncil : GovUkCollectorBase, ICollecto { Name = "Household Waste", Colour = BinColour.Black, - Keys = [ "Black bin", "Domestic Waste" ], + Keys = [ "Black bin" ], }, new() { Name = "Food and Garden Waste", Colour = BinColour.Pink, - Keys = [ "Pink lidded bin", "Food and Garden Waste" ], + Keys = [ "Pink lidded bin" ], }, new() { Name = "Paper and Cardboard", Colour = BinColour.Blue, - Keys = [ "Blue bin", "Paper and Card" ], + Keys = [ "Blue bin" ], }, new() { Name = "Glass, Cans and Plastics", Colour = BinColour.Brown, - Keys = [ "Brown bin", "Bottle and Can" ], + Keys = [ "Brown bin" ], }, ];