Skip to content

Conversation

@BadgerHobbs
Copy link
Owner

Summary

This PR adds a new bin collection data collector for North Devon Council.

  • Implements ICollector interface
  • Adds integration tests
  • Successfully tested with example postcode from issue

Closes #22

Test Summary

 ==================== Test Summary ====================
 
 --------------------- Collector ----------------------
 
 North Devon Council
 
 ------------------- Addresses (68) -------------------
 
 - 1 Westacott Meadow, Barnstaple, Westacott Meadow, Barnstaple, EX32 8QX, 010012097978
 - 10 Westacott Meadow, Barnstaple, Westacott Meadow, Barnstaple, EX32 8QX, 010012098355
 - 11 Westacott Meadow, Barnstaple, Westacott Meadow, Barnstaple, EX32 8QX, 010012098940
 - 12 Westacott Meadow, Barnstaple, Westacott Meadow, Barnstaple, EX32 8QX, 010012100424
 - 13 Westacott Meadow, Barnstaple, Westacott Meadow, Barnstaple, EX32 8QX, 010012100193
 - ...
 
 ------------------- Bin Days (16) --------------------
 
 - 08/01/2026 (1 bins):
   - Recycling
 
 - 15/01/2026 (2 bins):
   - Recycling
   - General Waste
 
 - 22/01/2026 (1 bins):
   - Recycling
 
 - 29/01/2026 (2 bins):
   - Recycling
   - General Waste
 
 - 05/02/2026 (1 bins):
   - Recycling
 
 - 12/02/2026 (2 bins):
   - Recycling
   - General Waste
 
 - 19/02/2026 (1 bins):
   - Recycling
 
 - 26/02/2026 (2 bins):
   - Recycling
   - General Waste
 
 - 05/03/2026 (1 bins):
   - Recycling
 
 - 12/03/2026 (2 bins):
   - Recycling
   - General Waste
 - ...
 
 ======================================================

Generated automatically by Codex CLI

@BadgerHobbs BadgerHobbs mentioned this pull request Jan 4, 2026
@gemini-code-assist

This comment was marked as outdated.

gemini-code-assist[bot]

This comment was marked as outdated.

@BadgerHobbs BadgerHobbs force-pushed the collector/NorthDevonCouncil-issue-22-1767495444 branch from 79e0514 to af5b993 Compare January 10, 2026 00:57
@BadgerHobbs BadgerHobbs force-pushed the collector/NorthDevonCouncil-issue-22-1767495444 branch from af5b993 to ba4f3c6 Compare January 10, 2026 00:59
@BadgerHobbs
Copy link
Owner Author

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new collector for North Devon Council. The implementation is quite complex, involving a multi-step process to retrieve data, which has been handled using the established if/else if pattern. The code is well-structured, and the inclusion of integration tests is excellent.

My review focuses on several areas for improvement:

  • Adherence to the repository's C# style guide, particularly regarding object initializers and using framework utilities.
  • Removal of unused code (usings and regex methods).
  • Opportunities to reduce significant code duplication by extracting common logic into helper methods.
  • A critical issue with hardcoded bin matching logic that should be refactored to use the configured bin types for better maintainability.

These changes will improve the code's quality, readability, and robustness.

Comment on lines 853 to 864
if (workPack.StartsWith("Waste-Black", StringComparison.OrdinalIgnoreCase))
{
binsForDate.Add(generalWasteBin);
}
else if (workPack.StartsWith("Waste-Recycling", StringComparison.OrdinalIgnoreCase))
{
binsForDate.Add(recyclingBin);
}
else
{
continue;
}
Copy link

@gemini-code-assist gemini-code-assist bot Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Replaced hardcoded bin matching logic with ProcessingUtilities.GetMatchingBins(_binTypes, workPack) in both step 8 and step 9 (retry logic). Also updated the bin type keys to match the actual data format (Waste-Black, Waste-Recycling).

Comment on lines 31 to 47
private readonly IReadOnlyCollection<Bin> _binTypes =
[
new()
{
Name = "General Waste",
Colour = BinColour.Black,
Type = BinType.Bin,
Keys = [ "Black Bin", "BlackBin" ],
},
new()
{
Name = "Recycling",
Colour = BinColour.Green,
Type = BinType.Container,
Keys = [ "Recycling" ],
},
];
Copy link

@gemini-code-assist gemini-code-assist bot Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Updated bin type keys to 'Waste-Black' and 'Waste-Recycling' to match the actual data format. Trailing commas were already present from the formatter.

private static partial Regex MonthSectionRegex();

/// <inheritdoc/>
public GetAddressesResponse GetAddresses(string postcode, ClientSideResponse? clientSideResponse)
Copy link

@gemini-code-assist gemini-code-assist bot Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Extracted the first three request steps (session initialization and authentication) into a shared HandleSessionInitialization helper method used by both GetAddresses and GetBinDays. This eliminates the duplication.

return getBinDaysResponse;
}
// Process bin days from response
else if (clientSideResponse.RequestId == 8)
Copy link

@gemini-code-assist gemini-code-assist bot Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Extracted the duplicated parsing logic into a ParseBinDaysFromRowsData helper method that is now used by both step 8 and step 9 (retry logic).

using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Web;
Copy link

@gemini-code-assist gemini-code-assist bot Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Removed the unused using System.Web; statement.

Comment on lines 69 to 73
/// <summary>
/// Regex for matching individual collection entries.
/// </summary>
[GeneratedRegex(@"<li class=\""(?<binKey>[^\""]+)\""[^>]*>\s*<span class=\""wasteName\"">(?<dayName>[^<]+)</span>\s*<span class=\""wasteDay\"">(?<day>\d{2})</span>\s*<span class=\""wasteType\"">(?<binLabel>[^<]+)</span>", RegexOptions.Singleline)]
private static partial Regex CollectionItemRegex();
Copy link

@gemini-code-assist gemini-code-assist bot Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Removed the unused CollectionItemRegex method.

Comment on lines 81 to 85
/// <summary>
/// Regex to match month sections and their collection items.
/// </summary>
[GeneratedRegex(@"<h4>(?<monthYear>[A-Za-z]+\s+\d{4})</h4>(?<items>.*?)(?=<h4>[A-Za-z]+\s+\d{4}</h4>|<h2>|$)", RegexOptions.Singleline)]
private static partial Regex MonthSectionRegex();
Copy link

@gemini-code-assist gemini-code-assist bot Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Removed the unused MonthSectionRegex method.

// Prepare client-side request for getting location data
else if (clientSideResponse.RequestId == 2)
{
var previousCookies = clientSideResponse.Options.Metadata.GetValueOrDefault("cookies", string.Empty);
Copy link
Owner Author

@BadgerHobbs BadgerHobbs Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added validation in the \ helper method to check for the presence of the set-cookie header and throw an \ if it's missing.

{
var previousCookies = clientSideResponse.Options.Metadata.GetValueOrDefault("cookies", string.Empty);
var newCookies = ProcessingUtilities.ParseSetCookieHeaderForRequestCookie(clientSideResponse.Headers["set-cookie"]);
var cookies = CombineCookies(previousCookies, newCookies);
Copy link
Owner Author

@BadgerHobbs BadgerHobbs Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept as local method. This is the only collector using CombineCookies - I checked the codebase and no other collectors have similar logic. If it becomes common in future collectors, we can extract it to ProcessingUtilities then.

var sid = clientSideResponse.Options.Metadata.GetValueOrDefault("sid", string.Empty);
var formattedPostcode = clientSideResponse.Options.Metadata.GetValueOrDefault("postcode", string.Empty);

var requestBody = JsonSerializer.Serialize(new
Copy link
Owner Author

@BadgerHobbs BadgerHobbs Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comment explaining that many fields are empty but required by the API. Removing them could cause the API to reject the request.

return getBinDaysResponse;
}
// Prepare client-side request for getting address details
else if (clientSideResponse.RequestId == 3)
Copy link
Owner Author

@BadgerHobbs BadgerHobbs Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the multi-step session setup is required for each GetBinDays call. The API needs fresh session cookies and SID for the bin collection lookup process. Added a comment explaining this requirement.

return getBinDaysResponse;
}
// Process bin days from retry response
else if (clientSideResponse.RequestId == 9)
Copy link
Owner Author

@BadgerHobbs BadgerHobbs Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comment clarifying this. The retry response (step 9) is used when the initial request (step 8) returns no bin collection data. It retries the same request to handle cases where the API may not return data on the first attempt.

throw new InvalidOperationException("Invalid client-side request.");
}

private static string BuildCalendarRequestBody(
Copy link
Owner Author

@BadgerHobbs BadgerHobbs Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The BuildCalendarRequestBody method centralizes the complex request body structure used for calendar lookups, and is called by CreateCalendarLookupRequest. While CreateCalendarLookupRequest is only used twice, it provides clear separation of concerns. Added documentation comments to both methods to clarify their purpose.

noRetry);
}

private static ClientSideRequest CreateRunLookupRequest(
Copy link
Owner Author

@BadgerHobbs BadgerHobbs Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The \ method encapsulates the common pattern for API broker requests with timestamp and retry parameters. While it's only called from one place, it provides clear abstraction and makes the code more maintainable. Added documentation comment to clarify its purpose.

BadgerHobbs and others added 2 commits January 19, 2026 22:45
- Remove unused using System.Web statement
- Remove unused MonthSectionRegex and CollectionItemRegex methods
- Fix bin type keys to match actual data (Waste-Black, Waste-Recycling)
- Replace hardcoded bin matching with ProcessingUtilities.GetMatchingBins
- Extract duplicated bin parsing logic into ParseBinDaysFromRowsData helper
- Extract session initialization (steps 1-2) into HandleSessionInitialization helper
- Add cookie validation in session initialization
- Add comments explaining retry logic, empty request fields, and API requirements
- Add documentation comments to helper methods

All integration tests passing.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
  Formatted by Moley-Bot
@BadgerHobbs
Copy link
Owner Author

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new collector for North Devon Council, including implementation of the ICollector interface and accompanying integration tests. The collector retrieves bin collection data from the council's website. The code adheres to the repository's style guide, with some minor suggestions for improvement.

Comment on lines +30 to +46
private readonly IReadOnlyCollection<Bin> _binTypes =
[
new()
{
Name = "General Waste",
Colour = BinColour.Black,
Type = BinType.Bin,
Keys = [ "Waste-Black" ],
},
new()
{
Name = "Recycling",
Colour = BinColour.Green,
Type = BinType.Container,
Keys = [ "Waste-Recycling" ],
},
];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Consider using descriptive bin names instead of generic ones. For example, instead of "Recycling", use "Paper, Glass & Cardboard Recycling". Also, ensure that the keys accurately reflect what is being matched from the data source. See Bin Type Configuration in the style guide.

References
  1. Bin names should describe what goes in the bin, not just the container. Only add keys that will actually be matched against the data source. (link)

Comment on lines +138 to +185
// Note: Many fields below are empty but required by the API
var requestBody = JsonSerializer.Serialize(new
{
stopOnFailure = true,
usePHPIntegrations = true,
stage_id = _stageId,
stage_name = "Stage 1",
formId = _formId,
formValues = new Dictionary<string, object?>
{
["Your address"] = new
{
qsUPRN = new { value = string.Empty },
postcode_search = new { value = formattedPostcode },
chooseAddress = new { value = string.Empty },
uprnfromlookup = new { value = string.Empty },
UPRNMF = new { value = string.Empty },
FULLADDR2 = new { value = string.Empty },
},
["Calendar"] = new Dictionary<string, object?>
{
{ "FULLADDR", new { value = string.Empty } },
{ "token", new { value = string.Empty } },
{ "uPRN", new { value = string.Empty } },
{ "calstartDate", new { value = string.Empty } },
{ "calendDate", new { value = string.Empty } },
{ "details", Array.Empty<object>() },
{ "text1", new { value = string.Empty } },
{ "Results", new { value = string.Empty } },
{ "UPRN", new { value = string.Empty } },
{ "Alerts", new { value = string.Empty } },
{ "liveToken", new { value = string.Empty } },
{ "Results2", new { value = string.Empty } },
{ "USRN", new { value = string.Empty } },
{ "streetEvents", Array.Empty<object>() },
{ "EventDescription", new { value = string.Empty } },
{ "EventDate", new { value = string.Empty } },
{ "EventsDisplay", new { value = string.Empty } },
{ "Comments", new { value = string.Empty } },
{ "OutText", new { value = string.Empty } },
{ "StartDate", new { value = string.Empty } },
{ "EndDate", new { value = string.Empty } },
},
["Print version"] = new
{
OutText2 = new { value = string.Empty },
},
},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to the style guide, request bodies should be minimal and contain only the necessary fields. Review the fields included in the request body to ensure that all unnecessary fields (empty strings, null values, default values, or false booleans) are removed. See Request Bodies & JSON Payloads in the style guide.

References
  1. Remove all unnecessary fields. Only include what the API actually requires. (link)

Comment on lines +664 to +672
Method = "POST",
Body = requestBody,
Headers = new()
{
{ "content-type", "application/json" },
{ "cookie", cookies },
{ "x-requested-with", "XMLHttpRequest" },
{ "User-Agent", Constants.UserAgent },
},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

The style guide recommends using multi-line date parsing for improved readability. See Code Formatting & Style in the style guide.

var collectionDate = DateOnly.ParseExact(
					match.Groups["date"].Value,
					"dd/MM/yyyy",
					CultureInfo.InvariantCulture,
					DateTimeStyles.None
				);
References
  1. Multi-line format improves readability. (link)

Comment on lines +673 to +674
Options = new ClientSideOptions
{

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

The closing parenthesis should be on a separate line for multi-line method calls. See Code Formatting & Style in the style guide.

					DateTimeStyles.None
				);
References
  1. Consistent formatting for multi-line method calls. (link)

@BadgerHobbs BadgerHobbs added the new collector Request for a new collector to be supported label Feb 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new collector Request for a new collector to be supported

Projects

None yet

Development

Successfully merging this pull request may close these issues.

North Devon Council

3 participants