diff --git a/BinDays.Api.Collectors/Collectors/Councils/WealdenDistrictCouncil.cs b/BinDays.Api.Collectors/Collectors/Councils/WealdenDistrictCouncil.cs new file mode 100644 index 00000000..af3ac5c3 --- /dev/null +++ b/BinDays.Api.Collectors/Collectors/Councils/WealdenDistrictCouncil.cs @@ -0,0 +1,275 @@ +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; + +/// +/// Collector implementation for Wealden District Council. +/// +internal sealed class WealdenDistrictCouncil : GovUkCollectorBase, ICollector +{ + /// + public string Name => "Wealden District Council"; + + /// + public Uri WebsiteUrl => new("https://www.wealden.gov.uk/"); + + /// + public override string GovUkId => "wealden"; + + /// + /// The list of bin types for this collector. + /// + private readonly IReadOnlyCollection _binTypes = + [ + new() + { + Name = "General Waste", + Colour = BinColour.Black, + Keys = [ "Refuse", "Rubbish" ], + Type = BinType.Bin, + }, + new() + { + Name = "Recycling", + Colour = BinColour.Green, + Keys = [ "Recycling" ], + Type = BinType.Bin, + }, + new() + { + Name = "Garden Waste", + Colour = BinColour.Brown, + Keys = [ "Garden" ], + Type = BinType.Bin, + }, + ]; + + /// + public GetAddressesResponse GetAddresses(string postcode, ClientSideResponse? clientSideResponse) + { + // Remove spaces from postcode as the Wealden API requires postcodes without spaces in form data and URL parameters + var sanitizedPostcode = postcode.Replace(" ", string.Empty); + + // Prepare client-side request for getting cookies + if (clientSideResponse == null) + { + var clientSideRequest = new ClientSideRequest + { + RequestId = 1, + Url = "https://www.wealden.gov.uk/recycling-and-waste/bin-search/", + Method = "GET", + Headers = new() + { + { "User-Agent", Constants.UserAgent }, + }, + }; + + var getAddressesResponse = new GetAddressesResponse + { + NextClientSideRequest = clientSideRequest, + }; + + return getAddressesResponse; + } + // Prepare client-side request for getting addresses + else if (clientSideResponse.RequestId == 1) + { + var requestCookies = ProcessingUtilities.ParseSetCookieHeaderForRequestCookie( + clientSideResponse.Headers["set-cookie"]! + ); + + var requestBody = ProcessingUtilities.ConvertDictionaryToFormData(new() + { + { "action", "wealden_get_properties_in_postcode" }, + { "postcode", sanitizedPostcode }, + }); + + var clientSideRequest = new ClientSideRequest + { + RequestId = 2, + Url = "https://www.wealden.gov.uk/wp-admin/admin-ajax.php", + Method = "POST", + Headers = new() + { + { "Content-Type", "application/x-www-form-urlencoded; charset=UTF-8" }, + { "X-Requested-With", "XMLHttpRequest" }, + { "cookie", requestCookies }, + { "User-Agent", Constants.UserAgent }, + }, + Body = requestBody, + }; + + var getAddressesResponse = new GetAddressesResponse + { + NextClientSideRequest = clientSideRequest, + }; + + return getAddressesResponse; + } + // Process addresses from response + else if (clientSideResponse.RequestId == 2) + { + using var jsonDoc = JsonDocument.Parse(clientSideResponse.Content); + var properties = jsonDoc.RootElement.GetProperty("properties").EnumerateArray(); + + // Iterate through each property, and create a new address object + var addresses = new List
(); + foreach (var propertyElement in properties) + { + var address = new Address + { + Property = propertyElement.GetProperty("address").GetString()!.Trim(), + Postcode = postcode, + Uid = propertyElement.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) + { + // Remove spaces from postcode as the Wealden API requires postcodes without spaces in URL parameters and cookies + var sanitizedPostcode = (address.Postcode ?? string.Empty).Replace(" ", string.Empty); + + // Prepare client-side request for getting cookies + if (clientSideResponse == null) + { + var requestUrl = $"https://www.wealden.gov.uk/recycling-and-waste/bin-search/?postcode={sanitizedPostcode}"; + + var clientSideRequest = new ClientSideRequest + { + RequestId = 1, + Url = requestUrl, + Method = "GET", + Headers = new() + { + { "User-Agent", Constants.UserAgent }, + }, + }; + + var getBinDaysResponse = new GetBinDaysResponse + { + NextClientSideRequest = clientSideRequest, + }; + + return getBinDaysResponse; + } + // Prepare client-side request for getting bin days + else if (clientSideResponse.RequestId == 1) + { + var requestCookies = ProcessingUtilities.ParseSetCookieHeaderForRequestCookie( + clientSideResponse.Headers["set-cookie"]! + ); + + var cookies = string.IsNullOrWhiteSpace(requestCookies) + ? $"c_postcode={sanitizedPostcode}" + : $"{requestCookies}; c_postcode={sanitizedPostcode}"; + + var requestBody = ProcessingUtilities.ConvertDictionaryToFormData(new() + { + { "action", "wealden_get_collections_for_uprn" }, + { "uprn", address.Uid! }, + }); + + var clientSideRequest = new ClientSideRequest + { + RequestId = 2, + Url = "https://www.wealden.gov.uk/wp-admin/admin-ajax.php", + Method = "POST", + Headers = new() + { + { "Content-Type", "application/x-www-form-urlencoded; charset=UTF-8" }, + { "X-Requested-With", "XMLHttpRequest" }, + { "cookie", cookies }, + { "User-Agent", Constants.UserAgent }, + }, + Body = requestBody, + }; + + var getBinDaysResponse = new GetBinDaysResponse + { + NextClientSideRequest = clientSideRequest, + }; + + return getBinDaysResponse; + } + // Process bin days from response + else if (clientSideResponse.RequestId == 2) + { + using var jsonDoc = JsonDocument.Parse(clientSideResponse.Content); + var collection = jsonDoc.RootElement.GetProperty("collection"); + + var binDays = new List(); + + var binCollectionProperties = new Dictionary + { + { "refuseCollectionDate", "Refuse" }, + { "recyclingCollectionDate", "Recycling" }, + { "gardenCollectionDate", "Garden" }, + }; + + // Iterate through each bin collection property, and create a new bin day object + foreach (var property in binCollectionProperties) + { + if (!collection.TryGetProperty(property.Key, out var dateElement)) + { + continue; + } + + var dateString = dateElement.GetString(); + + if (string.IsNullOrWhiteSpace(dateString)) + { + continue; + } + + var date = DateOnly.ParseExact( + dateString, + "yyyy-MM-dd'T'HH:mm:ss", + CultureInfo.InvariantCulture, + DateTimeStyles.None + ); + + var bins = ProcessingUtilities.GetMatchingBins(_binTypes, property.Value); + + var binDay = new BinDay + { + Date = date, + Address = address, + Bins = bins, + }; + + 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/WealdenDistrictCouncilTests.cs b/BinDays.Api.IntegrationTests/Collectors/Councils/WealdenDistrictCouncilTests.cs new file mode 100644 index 00000000..5bc39e2e --- /dev/null +++ b/BinDays.Api.IntegrationTests/Collectors/Councils/WealdenDistrictCouncilTests.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 WealdenDistrictCouncilTests +{ + private readonly IntegrationTestClient _client; + private static readonly ICollector _collector = new WealdenDistrictCouncil(); + private readonly CollectorService _collectorService = new([_collector]); + private readonly ITestOutputHelper _outputHelper; + + public WealdenDistrictCouncilTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + _client = new IntegrationTestClient(outputHelper); + } + + [Theory] + [InlineData("TN3 9PE")] + public async Task GetBinDaysTest(string postcode) + { + await TestSteps.EndToEnd( + _client, + _collectorService, + _collector, + postcode, + _outputHelper + ); + } +}