From af0f04fcbc077fda82f5363e7f48229e85bfc5dd Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:32:09 +0000 Subject: [PATCH] Add collector for Kirklees Council - Implements ICollector interface - Adds integration tests with new format - Successfully tested with example postcode from issue Closes #68 Co-authored-by: Andrew Riggs --- .../Collectors/Councils/KirkleesCouncil.cs | 643 ++++++++++++++++++ .../Councils/KirkleesCouncilTests.cs | 32 + 2 files changed, 675 insertions(+) create mode 100644 BinDays.Api.Collectors/Collectors/Councils/KirkleesCouncil.cs create mode 100644 BinDays.Api.IntegrationTests/Collectors/Councils/KirkleesCouncilTests.cs diff --git a/BinDays.Api.Collectors/Collectors/Councils/KirkleesCouncil.cs b/BinDays.Api.Collectors/Collectors/Councils/KirkleesCouncil.cs new file mode 100644 index 0000000..f55e8d4 --- /dev/null +++ b/BinDays.Api.Collectors/Collectors/Councils/KirkleesCouncil.cs @@ -0,0 +1,643 @@ +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.Linq; +using System.Text.Json; +using System.Text.RegularExpressions; + +/// +/// Collector implementation for Kirklees Council. +/// +internal sealed partial class KirkleesCouncil : GovUkCollectorBase, ICollector +{ + /// + public string Name => "Kirklees Council"; + + /// + public Uri WebsiteUrl => new("https://www.kirklees.gov.uk/"); + + /// + public override string GovUkId => "kirklees"; + + /// + /// The list of bin types for this collector. + /// + private readonly IReadOnlyCollection _binTypes = + [ + new() + { + Name = "General Waste", + Colour = BinColour.Grey, + Keys = [ "grey", "240d", "domestic", "grey wheelie" ], + }, + new() + { + Name = "Recycling", + Colour = BinColour.Green, + Keys = [ "green", "240g", "recycling", "green wheelie" ], + }, + ]; + + private const string _baseUrl = "https://my.kirklees.gov.uk"; + private const string _servicePath = "/service/Bins_and_recycling___Manage_your_bins"; + private const string _apiBrokerPath = "/apibroker/runLookup"; + private const string _addressLookupId = "58049013ca4c9"; + private const string _propertyDetailsLookupId = "659c2c2386104"; + private const string _binListLookupId = "65e08e60b299d"; + private const string _scheduleLookupId = "692431ec1ec18"; + private const int _dateRangeDays = 28; + + /// + /// Regex to extract the session ID (sid) from HTML content. + /// + [GeneratedRegex(@"sid=([a-f0-9]+)")] + private static partial Regex SidRegex(); + + /// + public GetAddressesResponse GetAddresses(string postcode, ClientSideResponse? clientSideResponse) + { + // Prepare client-side request for getting addresses + if (clientSideResponse == null) + { + var clientSideRequest = new ClientSideRequest + { + RequestId = 1, + Url = $"{_baseUrl}{_servicePath}", + Method = "GET", + Headers = new() + { + {"user-agent", Constants.UserAgent}, + }, + }; + + var getAddressesResponse = new GetAddressesResponse + { + NextClientSideRequest = clientSideRequest, + }; + + return getAddressesResponse; + } + // Process addresses from response + else if (clientSideResponse.RequestId == 1) + { + var setCookies = clientSideResponse.Headers["set-cookie"]; + var requestCookies = ProcessingUtilities.ParseSetCookieHeaderForRequestCookie(setCookies); + + var sid = SidRegex().Match(clientSideResponse.Content).Groups[1].Value; + + var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + var requestBody = $$""" + { + "formValues": { + "Section 1": { + "searchForAddress": { + "name": "searchForAddress", + "value": "yes", + "isMandatory": true, + "type": "radio" + }, + "Postcode": { + "name": "Postcode", + "value": "{{postcode}}", + "isMandatory": true + } + } + } + } + """; + + var clientSideRequest = new ClientSideRequest + { + RequestId = 2, + Url = $"{_baseUrl}{_apiBrokerPath}?id={_addressLookupId}&repeat_against=&noRetry=false&getOnlyTokens=undefined&log_id=&app_name=AF-Renderer::Self&_={timestamp}&sid={sid}", + Method = "POST", + Headers = new() + { + {"content-type", "application/json"}, + {"x-requested-with", "XMLHttpRequest"}, + {"cookie", requestCookies}, + {"user-agent", Constants.UserAgent}, + }, + Body = requestBody, + }; + + var getAddressesResponse = new GetAddressesResponse + { + NextClientSideRequest = clientSideRequest, + }; + + return getAddressesResponse; + } + // Map addresses from response + else if (clientSideResponse.RequestId == 2) + { + using var responseJson = JsonDocument.Parse(clientSideResponse.Content); + var rowsData = responseJson.RootElement + .GetProperty("integration") + .GetProperty("transformed") + .GetProperty("rows_data"); + + // Iterate through each address, and create a new address object + var addresses = new List
(); + foreach (var row in rowsData.EnumerateObject()) + { + var rowData = row.Value; + var uid = rowData.GetProperty("name").GetString()!; + + var address = new Address + { + Property = rowData.GetProperty("display").GetString()!.Trim(), + Street = rowData.GetProperty("Street").GetString()!.Trim(), + Town = rowData.GetProperty("Town").GetString()!.Trim(), + Postcode = postcode, + Uid = uid, + }; + + 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 = $"{_baseUrl}{_servicePath}", + Method = "GET", + Headers = new() + { + {"user-agent", Constants.UserAgent}, + }, + }; + + var getBinDaysResponse = new GetBinDaysResponse + { + NextClientSideRequest = clientSideRequest, + }; + + return getBinDaysResponse; + } + // Prepare property details request + else if (clientSideResponse.RequestId == 1) + { + var setCookies = clientSideResponse.Headers["set-cookie"]; + var requestCookies = ProcessingUtilities.ParseSetCookieHeaderForRequestCookie(setCookies); + + var sid = SidRegex().Match(clientSideResponse.Content).Groups[1].Value; + + var house = address.Property!.Split(",").FirstOrDefault()?.Split(" ").FirstOrDefault() ?? string.Empty; + + var requestBody = $$""" + { + "formValues": { + "Search": { + "PowerSuite_Available": { "name": "PowerSuite_Available", "value": "True", "isMandatory": true }, + "PowerSuite_Available1": { "name": "PowerSuite_Available1", "value": "True", "isMandatory": true }, + "customerAddress": { + "Section 1": { + "searchForAddress": { "name": "searchForAddress", "value": "yes", "isMandatory": true, "type": "radio" }, + "Postcode": { "name": "Postcode", "value": "{{address.Postcode!}}", "isMandatory": true }, + "List": { "name": "List", "value": "{{address.Uid!}}", "isMandatory": true, "type": "select", "value_label": "{{address.Property}}" }, + "House": { "name": "House", "value": "{{house}}", "isMandatory": true }, + "Street": { "name": "Street", "value": "{{address.Street!}}", "isMandatory": true }, + "Town": { "name": "Town", "value": "{{address.Town!}}", "isMandatory": true }, + "UPRN": { "name": "UPRN", "value": "{{address.Uid!}}", "isMandatory": true }, + "fullAddress": { "name": "fullAddress", "value": "{{address.Property!}}", "isMandatory": true } + } + }, + "uprn2": { "name": "uprn2", "value": "{{address.Uid!}}", "isMandatory": true }, + "validatedUPRN": { "name": "validatedUPRN", "value": "{{address.Uid!}}", "isMandatory": true }, + "suppliedUPRN": { "name": "suppliedUPRN", "value": "{{address.Uid!}}", "isMandatory": true }, + "productName": { "name": "productName", "value": "Self", "isMandatory": true }, + "uprnFinal": { "name": "uprnFinal", "value": "{{address.Uid!}}", "isMandatory": true }, + "houseFinal": { "name": "houseFinal", "value": "{{house}}", "isMandatory": true }, + "streetFinal": { "name": "streetFinal", "value": "{{address.Street!}}", "isMandatory": true }, + "townFinal": { "name": "townFinal", "value": "{{address.Town!}}", "isMandatory": true }, + "postcodeFinal": { "name": "postcodeFinal", "value": "{{address.Postcode!}}", "isMandatory": true }, + "fullAddressFinal": { "name": "fullAddressFinal", "value": "{{address.Property!}}", "isMandatory": true }, + "binsPropertyType": { + "Section 1": { + "PropertyType": { "name": "PropertyType", "value": "Residential", "isMandatory": true } + } + }, + "validPropertyFlag": { "name": "validPropertyFlag", "value": "yes", "isMandatory": true } + } + } + } + """; + + var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + var fromDate = DateTime.UtcNow.AddDays(-_dateRangeDays).ToString("dd/MM/yyyy", CultureInfo.InvariantCulture); + var toDate = DateTime.UtcNow.AddDays(_dateRangeDays).ToString("dd/MM/yyyy", CultureInfo.InvariantCulture); + + var clientSideRequest = new ClientSideRequest + { + RequestId = 2, + Url = $"{_baseUrl}{_apiBrokerPath}?id={_propertyDetailsLookupId}&repeat_against=&noRetry=false&getOnlyTokens=undefined&log_id=&app_name=AF-Renderer::Self&_={timestamp}&sid={sid}", + Method = "POST", + Headers = new() + { + {"content-type", "application/json"}, + {"x-requested-with", "XMLHttpRequest"}, + {"cookie", requestCookies}, + {"user-agent", Constants.UserAgent}, + }, + Body = requestBody, + Options = new ClientSideOptions + { + Metadata = + { + { "sid", sid }, + { "cookies", requestCookies }, + { "fromDate", fromDate }, + { "toDate", toDate }, + }, + }, + }; + + var getBinDaysResponse = new GetBinDaysResponse + { + NextClientSideRequest = clientSideRequest, + }; + + return getBinDaysResponse; + } + // Process property details and request bin list + else if (clientSideResponse.RequestId == 2) + { + using var responseJson = JsonDocument.Parse(clientSideResponse.Content); + var rowsData = responseJson.RootElement + .GetProperty("integration") + .GetProperty("transformed") + .GetProperty("rows_data"); + + var govDeliveryCategory = rowsData.EnumerateObject().First().Value + .GetProperty("GovDeliveryCategorye") + .GetString()! + .Trim(); + + var sid = clientSideResponse.Options.Metadata["sid"]; + var cookies = clientSideResponse.Options.Metadata["cookies"]; + var fromDate = clientSideResponse.Options.Metadata["fromDate"]; + var toDate = clientSideResponse.Options.Metadata["toDate"]; + + var house = address.Property!.Split(",").FirstOrDefault()?.Split(" ").FirstOrDefault() ?? string.Empty; + var currentDateTime = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); + var timeInHours = DateTime.UtcNow.ToString("HH", CultureInfo.InvariantCulture); + + var requestBody = $$""" + { + "formValues": { + "Search": { + "PowerSuite_Available": { "name": "PowerSuite_Available", "value": "True", "isMandatory": true }, + "PowerSuite_Available1": { "name": "PowerSuite_Available1", "value": "True", "isMandatory": true }, + "customerAddress": { + "Section 1": { + "searchForAddress": { "name": "searchForAddress", "value": "yes", "isMandatory": true, "type": "radio" }, + "Postcode": { "name": "Postcode", "value": "{{address.Postcode!}}", "isMandatory": true }, + "List": { "name": "List", "value": "{{address.Uid!}}", "isMandatory": true, "type": "select", "value_label": "{{address.Property}}" }, + "House": { "name": "House", "value": "{{house}}", "isMandatory": true }, + "Street": { "name": "Street", "value": "{{address.Street!}}", "isMandatory": true }, + "Town": { "name": "Town", "value": "{{address.Town!}}", "isMandatory": true }, + "UPRN": { "name": "UPRN", "value": "{{address.Uid!}}", "isMandatory": true }, + "fullAddress": { "name": "fullAddress", "value": "{{address.Property!}}", "isMandatory": true } + } + }, + "uprn2": { "name": "uprn2", "value": "{{address.Uid!}}", "isMandatory": true }, + "validatedUPRN": { "name": "validatedUPRN", "value": "{{address.Uid!}}", "isMandatory": true }, + "suppliedUPRN": { "name": "suppliedUPRN", "value": "{{address.Uid!}}", "isMandatory": true }, + "productName": { "name": "productName", "value": "Self", "isMandatory": true }, + "uprnFinal": { "name": "uprnFinal", "value": "{{address.Uid!}}", "isMandatory": true }, + "houseFinal": { "name": "houseFinal", "value": "{{house}}", "isMandatory": true }, + "streetFinal": { "name": "streetFinal", "value": "{{address.Street!}}", "isMandatory": true }, + "townFinal": { "name": "townFinal", "value": "{{address.Town!}}", "isMandatory": true }, + "postcodeFinal": { "name": "postcodeFinal", "value": "{{address.Postcode!}}", "isMandatory": true }, + "fullAddressFinal": { "name": "fullAddressFinal", "value": "{{address.Property!}}", "isMandatory": true }, + "binsPropertyType": { + "Section 1": { + "PropertyType": { "name": "PropertyType", "value": "Residential", "isMandatory": true }, + "GovDeliveryCategorye": { "name": "GovDeliveryCategorye", "value": "{{govDeliveryCategory}}", "isMandatory": true } + } + }, + "validPropertyFlag": { "name": "validPropertyFlag", "value": "yes", "isMandatory": true } + }, + "Your bins": { + "NextCollectionFromDate": { "name": "NextCollectionFromDate", "value": "{{fromDate}}", "isMandatory": true }, + "NextCollectionToDate": { "name": "NextCollectionToDate", "value": "{{toDate}}", "isMandatory": true }, + "currentDateTime": { "name": "currentDateTime", "value": "{{currentDateTime}}", "isMandatory": true }, + "timeInHours": { "name": "timeInHours", "value": "{{timeInHours}}", "isMandatory": true }, + "sameDaySubmissionFlag": { "name": "sameDaySubmissionFlag", "value": "no", "isMandatory": true }, + "maxBinAllocation": { "name": "maxBinAllocation", "value": "1", "isMandatory": true }, + "allowedBins": { "name": "allowedBins", "value": "1", "isMandatory": true } + } + } + } + """; + + var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + var clientSideRequest = new ClientSideRequest + { + RequestId = 3, + Url = $"{_baseUrl}{_apiBrokerPath}?id={_binListLookupId}&repeat_against=&noRetry=false&getOnlyTokens=undefined&log_id=&app_name=AF-Renderer::Self&_={timestamp}&sid={sid}", + Method = "POST", + Headers = new() + { + {"content-type", "application/json"}, + {"x-requested-with", "XMLHttpRequest"}, + {"cookie", cookies}, + {"user-agent", Constants.UserAgent}, + }, + Body = requestBody, + Options = new ClientSideOptions + { + Metadata = + { + { "sid", sid }, + { "cookies", cookies }, + { "govDeliveryCategory", govDeliveryCategory }, + { "fromDate", fromDate }, + { "toDate", toDate }, + }, + }, + }; + + var getBinDaysResponse = new GetBinDaysResponse + { + NextClientSideRequest = clientSideRequest, + }; + + return getBinDaysResponse; + } + // Process bin list and prepare schedule requests + else if (clientSideResponse.RequestId == 3) + { + using var responseJson = JsonDocument.Parse(clientSideResponse.Content); + var rowsData = responseJson.RootElement + .GetProperty("integration") + .GetProperty("transformed") + .GetProperty("rows_data"); + + var bins = new List(); + var binDetails = new List(); + var seenServiceItemIds = new HashSet(); + + // Iterate through each bin, and create a new bin info object + foreach (var row in rowsData.EnumerateObject()) + { + var rowData = row.Value; + var serviceItemId = rowData.GetProperty("ServiceItemID").GetString()!; + + if (!seenServiceItemIds.Add(serviceItemId)) + { + continue; + } + + var label = rowData.GetProperty("label").GetString()!.Trim(); + var roundSchedule = rowData.GetProperty("RoundSchedule").GetString()!.Trim(); + var serviceItemName = rowData.GetProperty("ServiceItemName").GetString()!.Trim(); + + var source = $"{serviceItemName} {label}"; + var binTypeService = source.Contains("240G", StringComparison.OrdinalIgnoreCase) + || source.Contains("green", StringComparison.OrdinalIgnoreCase) + ? "Recycling Collection Service" + : "Domestic Waste Collection Service"; + + bins.Add(new BinInfo(label, roundSchedule, binTypeService, serviceItemId)); + + binDetails.Add(rowData.GetProperty("BinDetails").GetString()!.Trim()); + } + + var binData = string.Join(",", binDetails); + + clientSideResponse.Options.Metadata.Add("binData", binData); + clientSideResponse.Options.Metadata.Add("binIndex", "0"); + clientSideResponse.Options.Metadata.Add("bins", JsonSerializer.Serialize(bins)); + clientSideResponse.Options.Metadata.Add("binDays", "[]"); + + var nextRequest = BuildScheduleRequest( + address, + bins[0], + clientSideResponse.Options.Metadata["sid"], + clientSideResponse.Options.Metadata["cookies"], + clientSideResponse.Options.Metadata["govDeliveryCategory"], + clientSideResponse.Options.Metadata["fromDate"], + clientSideResponse.Options.Metadata["toDate"], + binData, + 4, + clientSideResponse.Options + ); + + var getBinDaysResponse = new GetBinDaysResponse + { + NextClientSideRequest = nextRequest, + }; + + return getBinDaysResponse; + } + // Process schedule responses + else if (clientSideResponse.RequestId == 4) + { + var bins = JsonSerializer.Deserialize>(clientSideResponse.Options.Metadata["bins"]!)!; + var binIndex = int.Parse(clientSideResponse.Options.Metadata["binIndex"], CultureInfo.InvariantCulture); + var currentBin = bins[binIndex]; + + using var responseJson = JsonDocument.Parse(clientSideResponse.Content); + var rowsData = responseJson.RootElement + .GetProperty("integration") + .GetProperty("transformed") + .GetProperty("rows_data"); + + var binDays = JsonSerializer.Deserialize>(clientSideResponse.Options.Metadata["binDays"]!)!; + + // Iterate through each collection date, and create a new bin day record + foreach (var row in rowsData.EnumerateObject()) + { + var collectionData = row.Value.GetProperty("Collections").GetString()!.Trim(); + + var date = DateOnly.ParseExact( + collectionData, + "dddd d MMMM yyyy", + CultureInfo.InvariantCulture, + DateTimeStyles.None + ); + + binDays.Add(new BinDayData(date, currentBin.Label)); + } + + if (binIndex + 1 < bins.Count) + { + var nextIndex = binIndex + 1; + clientSideResponse.Options.Metadata["binIndex"] = nextIndex.ToString(CultureInfo.InvariantCulture); + clientSideResponse.Options.Metadata["binDays"] = JsonSerializer.Serialize(binDays); + + var nextRequest = BuildScheduleRequest( + address, + bins[nextIndex], + clientSideResponse.Options.Metadata["sid"], + clientSideResponse.Options.Metadata["cookies"], + clientSideResponse.Options.Metadata["govDeliveryCategory"], + clientSideResponse.Options.Metadata["fromDate"], + clientSideResponse.Options.Metadata["toDate"], + clientSideResponse.Options.Metadata["binData"], + 4, + clientSideResponse.Options + ); + + var nextBinDaysResponse = new GetBinDaysResponse + { + NextClientSideRequest = nextRequest, + }; + + return nextBinDaysResponse; + } + + var processedBinDays = new List(); + foreach (var binDay in binDays) + { + var matchedBins = ProcessingUtilities.GetMatchingBins(_binTypes, binDay.BinLabel); + + var processedBinDay = new BinDay + { + Date = binDay.Date, + Address = address, + Bins = matchedBins, + }; + + processedBinDays.Add(processedBinDay); + } + + var getBinDaysResponse = new GetBinDaysResponse + { + BinDays = ProcessingUtilities.ProcessBinDays(processedBinDays), + }; + + return getBinDaysResponse; + } + + // Throw exception for invalid request + throw new InvalidOperationException("Invalid client-side request."); + } + + + /// + /// Builds a schedule request for fetching bin collection dates. + /// + private static ClientSideRequest BuildScheduleRequest( + Address address, + BinInfo bin, + string sid, + string cookies, + string govDeliveryCategory, + string fromDate, + string toDate, + string binData, + int requestId, + ClientSideOptions options + ) + { + var house = address.Property!.Split(",").FirstOrDefault()?.Split(" ").FirstOrDefault() ?? string.Empty; + var binTypeSelect = bin.BinTypeService.Contains("Recycling", StringComparison.OrdinalIgnoreCase) ? "Recycling" : "Domestic"; + var currentDateTime = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); + var timeInHours = DateTime.UtcNow.ToString("HH", CultureInfo.InvariantCulture); + + var requestBody = $$""" + { + "formValues": { + "Search": { + "PowerSuite_Available": { "name": "PowerSuite_Available", "value": "True", "isMandatory": true }, + "PowerSuite_Available1": { "name": "PowerSuite_Available1", "value": "True", "isMandatory": true }, + "customerAddress": { + "Section 1": { + "searchForAddress": { "name": "searchForAddress", "value": "yes", "isMandatory": true, "type": "radio" }, + "Postcode": { "name": "Postcode", "value": "{{address.Postcode!}}", "isMandatory": true }, + "List": { "name": "List", "value": "{{address.Uid!}}", "isMandatory": true, "type": "select", "value_label": "{{address.Property}}" }, + "House": { "name": "House", "value": "{{house}}", "isMandatory": true }, + "Street": { "name": "Street", "value": "{{address.Street!}}", "isMandatory": true }, + "Town": { "name": "Town", "value": "{{address.Town!}}", "isMandatory": true }, + "UPRN": { "name": "UPRN", "value": "{{address.Uid!}}", "isMandatory": true }, + "fullAddress": { "name": "fullAddress", "value": "{{address.Property!}}", "isMandatory": true } + } + }, + "uprn2": { "name": "uprn2", "value": "{{address.Uid!}}", "isMandatory": true }, + "validatedUPRN": { "name": "validatedUPRN", "value": "{{address.Uid!}}", "isMandatory": true }, + "suppliedUPRN": { "name": "suppliedUPRN", "value": "{{address.Uid!}}", "isMandatory": true }, + "productName": { "name": "productName", "value": "Self", "isMandatory": true }, + "uprnFinal": { "name": "uprnFinal", "value": "{{address.Uid!}}", "isMandatory": true }, + "houseFinal": { "name": "houseFinal", "value": "{{house}}", "isMandatory": true }, + "streetFinal": { "name": "streetFinal", "value": "{{address.Street!}}", "isMandatory": true }, + "townFinal": { "name": "townFinal", "value": "{{address.Town!}}", "isMandatory": true }, + "postcodeFinal": { "name": "postcodeFinal", "value": "{{address.Postcode!}}", "isMandatory": true }, + "fullAddressFinal": { "name": "fullAddressFinal", "value": "{{address.Property!}}", "isMandatory": true }, + "binsPropertyType": { + "Section 1": { + "PropertyType": { "name": "PropertyType", "value": "Residential", "isMandatory": true }, + "GovDeliveryCategorye": { "name": "GovDeliveryCategorye", "value": "{{govDeliveryCategory}}", "isMandatory": true } + } + }, + "validPropertyFlag": { "name": "validPropertyFlag", "value": "yes", "isMandatory": true } + }, + "Your bins": { + "binTypeService": { "name": "binTypeService", "value": "{{bin.BinTypeService}}", "isMandatory": true }, + "RoundSchedule": { "name": "RoundSchedule", "value": "{{bin.RoundSchedule}}", "isMandatory": true }, + "NextCollectionFromDate": { "name": "NextCollectionFromDate", "value": "{{fromDate}}", "isMandatory": true }, + "NextCollectionToDate": { "name": "NextCollectionToDate", "value": "{{toDate}}", "isMandatory": true }, + "binData": { "name": "binData", "value": "{{binData}}", "isMandatory": true }, + "serviceItemID": { "name": "serviceItemID", "value": "{{bin.ServiceItemId}}", "isMandatory": true }, + "binTypeSelect": { "name": "binTypeSelect", "value": "{{binTypeSelect}}", "isMandatory": true }, + "currentDateTime": { "name": "currentDateTime", "value": "{{currentDateTime}}", "isMandatory": true }, + "timeInHours": { "name": "timeInHours", "value": "{{timeInHours}}", "isMandatory": true }, + "sameDaySubmissionFlag": { "name": "sameDaySubmissionFlag", "value": "no", "isMandatory": true }, + "maxBinAllocation": { "name": "maxBinAllocation", "value": "1", "isMandatory": true }, + "allowedBins": { "name": "allowedBins", "value": "1", "isMandatory": true } + } + } + } + """; + + var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + var clientSideRequest = new ClientSideRequest + { + RequestId = requestId, + Url = $"{_baseUrl}{_apiBrokerPath}?id={_scheduleLookupId}&repeat_against=&noRetry=false&getOnlyTokens=undefined&log_id=&app_name=AF-Renderer::Self&_={timestamp}&sid={sid}", + Method = "POST", + Headers = new() + { + {"content-type", "application/json"}, + {"x-requested-with", "XMLHttpRequest"}, + {"cookie", cookies}, + {"user-agent", Constants.UserAgent}, + }, + Body = requestBody, + Options = options, + }; + + return clientSideRequest; + } + + private sealed record BinInfo(string Label, string RoundSchedule, string BinTypeService, string ServiceItemId); + + private sealed record BinDayData(DateOnly Date, string BinLabel); +} diff --git a/BinDays.Api.IntegrationTests/Collectors/Councils/KirkleesCouncilTests.cs b/BinDays.Api.IntegrationTests/Collectors/Councils/KirkleesCouncilTests.cs new file mode 100644 index 0000000..5c4152b --- /dev/null +++ b/BinDays.Api.IntegrationTests/Collectors/Councils/KirkleesCouncilTests.cs @@ -0,0 +1,32 @@ +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 KirkleesCouncilTests +{ + private readonly IntegrationTestClient _client; + private readonly ITestOutputHelper _outputHelper; + private static readonly string _govUkId = new KirkleesCouncil().GovUkId; + + public KirkleesCouncilTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + _client = new IntegrationTestClient(outputHelper); + } + + [Theory] + [InlineData("WF4 4AD")] + public async Task GetBinDaysTest(string postcode) + { + await TestSteps.EndToEnd( + _client, + postcode, + _govUkId, + _outputHelper + ); + } +}