| \d+<\/td> | (?Monday\s+\d{1,2}\s+[A-Za-z]+)(?:\s+\d{4})?\s+to\s+Friday[^<]*<\/td>| (?:(?Monday\s+\d{1,2}\s+[A-Za-z]+)(?:\s+\d{4})?\s+to\s+Friday)?[^<]*<\/td><\/tr>")]
+ private static partial Regex WeekRowRegex();
+
+ ///
+ /// Regex for normalizing address whitespace.
+ ///
+ [GeneratedRegex(@"\s+")]
+ private static partial Regex WhitespaceRegex();
+
+ ///
+ public GetAddressesResponse GetAddresses(string postcode, ClientSideResponse? clientSideResponse)
+ {
+ // Prepare client-side request for address lookup
+ if (clientSideResponse == null)
+ {
+ var clientSideRequest = new ClientSideRequest
+ {
+ RequestId = 1,
+ Url = $"https://www.royalgreenwich.gov.uk/site/custom_scripts/apps/waste-collection/source.php?term={Uri.EscapeDataString(postcode)}",
+ Method = "GET",
+ };
+
+ var getAddressesResponse = new GetAddressesResponse
+ {
+ NextClientSideRequest = clientSideRequest,
+ };
+
+ return getAddressesResponse;
+ }
+ // Process addresses from the response
+ else if (clientSideResponse.RequestId == 1)
+ {
+ using var document = JsonDocument.Parse(clientSideResponse.Content);
+ var rawAddresses = document.RootElement.EnumerateArray();
+
+ // Iterate through each address, and create a new address object
+ var addresses = new List();
+ foreach (var rawAddress in rawAddresses)
+ {
+ var property = WhitespaceRegex().Replace(rawAddress.GetString()!, " ").Trim();
+
+ var address = new Address
+ {
+ Property = property,
+ Postcode = postcode,
+ Uid = property,
+ };
+
+ 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 the selected address details
+ if (clientSideResponse == null)
+ {
+ var requestBody = ProcessingUtilities.ConvertDictionaryToFormData(new()
+ {
+ { "address", address.Uid! },
+ });
+
+ var clientSideRequest = new ClientSideRequest
+ {
+ RequestId = 1,
+ Url = "https://www.royalgreenwich.gov.uk/site/custom_scripts/apps/waste-collection/ajax-response-uprn.php",
+ Method = "POST",
+ Headers = new()
+ {
+ { "user-agent", Constants.UserAgent },
+ { "content-type", Constants.FormUrlEncoded },
+ },
+ Body = requestBody,
+ };
+
+ var getBinDaysResponse = new GetBinDaysResponse
+ {
+ NextClientSideRequest = clientSideRequest,
+ };
+
+ return getBinDaysResponse;
+ }
+ // Prepare client-side request for the black-top schedule page
+ else if (clientSideResponse.RequestId == 1)
+ {
+ using var document = JsonDocument.Parse(clientSideResponse.Content);
+ var collectionDay = document.RootElement.GetProperty("Day").GetString()!;
+ var frequency = document.RootElement.GetProperty("Frequency").GetString()!;
+
+ var clientSideRequest = new ClientSideRequest
+ {
+ RequestId = 2,
+ Url = "https://www.royalgreenwich.gov.uk/recycling-and-rubbish/bins-and-collections/black-top-bin-collections",
+ Method = "GET",
+ Options = new ClientSideOptions
+ {
+ Metadata =
+ {
+ { "collectionDay", collectionDay },
+ { "frequency", frequency },
+ },
+ },
+ };
+
+ var getBinDaysResponse = new GetBinDaysResponse
+ {
+ NextClientSideRequest = clientSideRequest,
+ };
+
+ return getBinDaysResponse;
+ }
+ // Process collection dates from the week schedule table
+ else if (clientSideResponse.RequestId == 2)
+ {
+ var collectionDay = clientSideResponse.Options.Metadata["collectionDay"];
+ var frequency = clientSideResponse.Options.Metadata["frequency"];
+
+ // "Weekly" addresses have every bin collected every week; "Week A" / "Week B" addresses have the general waste
+ // bin collected fortnightly on their assigned week, while recycling and food/garden waste remain weekly.
+ var isWeekly = frequency.Equals("Weekly", StringComparison.OrdinalIgnoreCase);
+ var isWeekA = frequency.Equals("Week A", StringComparison.OrdinalIgnoreCase);
+ var isWeekB = frequency.Equals("Week B", StringComparison.OrdinalIgnoreCase);
+ if (!isWeekly && !isWeekA && !isWeekB)
+ {
+ throw new InvalidOperationException($"Unsupported collection frequency: {frequency}.");
+ }
+
+ var dayOffset = collectionDay switch
+ {
+ "Monday" => 0,
+ "Tuesday" => 1,
+ "Wednesday" => 2,
+ "Thursday" => 3,
+ "Friday" => 4,
+ _ => throw new InvalidOperationException($"Unsupported collection day: {collectionDay}."),
+ };
+
+ var weeklyBins = ProcessingUtilities.GetMatchingBins(_binTypes, "Weekly Collection");
+ var generalWasteBins = ProcessingUtilities.GetMatchingBins(_binTypes, "General Waste");
+
+ // The general waste bin is added to the weeks that match the address's frequency (every week for "Weekly",
+ // or only the assigned A/B week otherwise).
+ var weekABins = isWeekly || isWeekA ? [.. weeklyBins, .. generalWasteBins] : weeklyBins;
+ var weekBBins = isWeekly || isWeekB ? [.. weeklyBins, .. generalWasteBins] : weeklyBins;
+
+ var binDays = new List();
+
+ // Iterate through each schedule row, and add bin days for the Week A and Week B Monday-to-Friday ranges
+ foreach (Match row in WeekRowRegex().Matches(clientSideResponse.Content)!)
+ {
+ var weekADate = DateUtilities.ParseDateInferringYear(row.Groups["weekA"].Value, "dddd d MMMM").AddDays(dayOffset);
+ binDays.Add(new BinDay
+ {
+ Date = weekADate,
+ Address = address,
+ Bins = weekABins,
+ });
+
+ if (row.Groups["weekB"].Success)
+ {
+ var weekBDate = DateUtilities.ParseDateInferringYear(row.Groups["weekB"].Value, "dddd d MMMM").AddDays(dayOffset);
+ binDays.Add(new BinDay
+ {
+ Date = weekBDate,
+ Address = address,
+ Bins = weekBBins,
+ });
+ }
+ }
+
+ 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/RoyalBoroughOfGreenwichTests.cs b/BinDays.Api.IntegrationTests/Collectors/Councils/RoyalBoroughOfGreenwichTests.cs
new file mode 100644
index 00000000..530b8ca9
--- /dev/null
+++ b/BinDays.Api.IntegrationTests/Collectors/Councils/RoyalBoroughOfGreenwichTests.cs
@@ -0,0 +1,34 @@
+namespace BinDays.Api.IntegrationTests.Collectors.Councils;
+
+using BinDays.Api.Collectors.Collectors.Councils;
+using BinDays.Api.IntegrationTests.Helpers;
+using System.Threading.Tasks;
+using Xunit;
+using Xunit.Abstractions;
+
+public class RoyalBoroughOfGreenwichTests
+{
+ private readonly IntegrationTestClient _client;
+ private readonly ITestOutputHelper _outputHelper;
+ private static readonly string _govUkId = new RoyalBoroughOfGreenwich().GovUkId;
+
+ public RoyalBoroughOfGreenwichTests(ITestOutputHelper outputHelper)
+ {
+ _outputHelper = outputHelper;
+ _client = new IntegrationTestClient(outputHelper);
+ }
+
+ [Theory]
+ [InlineData("SE9 2BP")] // Week A address
+ [InlineData("SE9 6DJ")] // Week B address
+ [InlineData("SE18 1HE")] // Weekly address
+ public async Task GetBinDaysTest(string postcode)
+ {
+ await TestSteps.EndToEnd(
+ _client,
+ postcode,
+ _govUkId,
+ _outputHelper
+ );
+ }
+}
| |