Skip to content

Conversation

@BadgerHobbs
Copy link
Owner

Summary

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

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

Closes #68

Test Summary

 ==================== Test Summary ====================
 
 --------------------- Collector ----------------------
 
 Kirklees Council
 
 ------------------- Addresses (56) -------------------
 
 - 1 Park Side, Flockton, Wakefield, WF4 4AD, Park Side, Wakefield, WF4 4AD, 83159267
 - 2 Park Side, Flockton, Wakefield, WF4 4AD, Park Side, Wakefield, WF4 4AD, 83159278
 - 3 Park Side, Flockton, Wakefield, WF4 4AD, Park Side, Wakefield, WF4 4AD, 83159289
 - 4 Park Side, Flockton, Wakefield, WF4 4AD, Park Side, Wakefield, WF4 4AD, 83159300
 - 5 Park Side, Flockton, Wakefield, WF4 4AD, Park Side, Wakefield, WF4 4AD, 83159311
 - ...
 
 --------------------- Bin Types ----------------------
 
 - General Waste (Grey)
 - Recycling (Green)
 
 -------------------- Bin Days (4) --------------------
 
 - 20/01/2026 (1 bins):
   - Recycling (Green)
 
 - 27/01/2026 (1 bins):
   - General Waste (Grey)
 
 - 03/02/2026 (1 bins):
   - Recycling (Green)
 
 - 10/02/2026 (1 bins):
   - General Waste (Grey)
 
 ======================================================

Generated automatically by Moley-Bot using Codex CLI

Closes #68

Generated with Codex CLI by Moley-Bot
@gemini-code-assist
Copy link

Summary of Changes

Hello @BadgerHobbs, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new data collector specifically designed for Kirklees Council, enabling the system to programmatically retrieve bin collection schedules. The implementation involves a multi-step process of client-side requests to interact with the council's web service, first to identify addresses based on a postcode, and then to fetch detailed bin collection dates for a selected property. This enhancement expands the system's coverage to a new local authority, improving its utility for users in the Kirklees area.

Highlights

  • New Collector Implementation: Implemented a new data collector for Kirklees Council, adhering to the ICollector interface for standardized bin collection data retrieval.
  • Integration Tests: Added comprehensive integration tests to validate the functionality and reliability of the new Kirklees Council collector.
  • Functionality Verified: Successfully verified the collector's operation using an example postcode (WF4 4AD), confirming accurate bin collection data retrieval and addressing issue Kirklees Council #68.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

  Formatted by Moley-Bot
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 Kirklees Council, which involves a complex multi-step process to retrieve bin collection data. The implementation correctly follows the project's pattern for handling client-side requests and state management. The integration test is also well-formed and adheres to the provided template.

My review identifies several minor areas for improvement, primarily related to adherence to the repository's C# style guide. These include consistently using trailing commas, avoiding inline returns for complex objects, and extracting hardcoded values like URLs and date ranges into constants for better maintainability. I've also noted a small code formatting issue. Addressing these points will enhance code quality and consistency with the rest of the project.

Comment on lines +46 to +50
/// <summary>
/// Regex to extract the session ID (sid) from HTML content.
/// </summary>
[GeneratedRegex(@"sid=([a-f0-9]+)")]
private static partial Regex SidRegex();

Choose a reason for hiding this comment

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

medium

For better maintainability and to avoid magic strings, consider extracting the hardcoded API URLs and URL fragments into private const string fields. This aligns with the style guide's recommendation for managing configuration values (lines 80-83, 107-109).

For example:

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";
// ... etc.
References
  1. Store API keys or other secrets as private const string fields within the collector class. Do not expose them publicly. (link)

Comment on lines +217 to +218
{ "fromDate", DateTime.UtcNow.AddDays(-28).ToString("dd/MM/yyyy", CultureInfo.InvariantCulture) },
{ "toDate", DateTime.UtcNow.AddDays(28).ToString("dd/MM/yyyy", CultureInfo.InvariantCulture) },

Choose a reason for hiding this comment

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

low

The number 28 is used to define the date range for fetching bin collections. To improve readability and maintainability, consider extracting this magic number into a named private const int at the top of the class.

For example:

private const int _dateRangeDays = 28;

// ... later in the code
{ "fromDate", DateTime.UtcNow.AddDays(-_dateRangeDays).ToString(...) },
{ "toDate", DateTime.UtcNow.AddDays(_dateRangeDays).ToString(...) },

Comment on lines +464 to +489
private static Dictionary<string, object> BuildAddressSearchBody(string postcode)
{
return new Dictionary<string, object>
{
{
"formValues",
new Dictionary<string, object>
{
{
"Section 1",
new Dictionary<string, object>
{
{ "searchForAddress", CreateField("searchForAddress", "yes", true, "radio") },
{ "Postcode", CreateField("Postcode", postcode, true) },
{ "postcode", CreateField("postcode", string.Empty, true) },
{ "house", CreateField("house", string.Empty, true) },
{ "flat", CreateField("flat", string.Empty, true) },
{ "street", CreateField("street", string.Empty, true) },
{ "town", CreateField("town", string.Empty, true) },
{ "fullAddress", CreateField("fullAddress", string.Empty, true) },
}
},
}
},
};
}

Choose a reason for hiding this comment

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

low

This method uses an inline return new Dictionary..., which violates the style guide (rule #337) that requires using a separate variable declaration before returning. This improves readability and makes debugging easier.

This pattern is also repeated in the BuildSearchSection and CreateField helper methods.

	private static Dictionary<string, object> BuildAddressSearchBody(string postcode)
	{
		var body = new Dictionary<string, object>
		{
			{
				"formValues",
				new Dictionary<string, object>
				{
					{
						"Section 1",
						new Dictionary<string, object>
						{
							{ "searchForAddress", CreateField("searchForAddress", "yes", true, "radio") },
							{ "Postcode", CreateField("Postcode", postcode, true) },
							{ "postcode", CreateField("postcode", string.Empty, true) },
							{ "house", CreateField("house", string.Empty, true) },
							{ "flat", CreateField("flat", string.Empty, true) },
							{ "street", CreateField("street", string.Empty, true) },
							{ "town", CreateField("town", string.Empty, true) },
							{ "fullAddress", CreateField("fullAddress", string.Empty, true) },
						}
					},
				}
			},
		};
		return body;
	}
References
  1. Always use separate variable declarations, not inline returns. (link)

@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 Kirklees Council, which is a complex, multi-step implementation. The code is well-structured and successfully implements the required logic. My review focuses on improving adherence to the repository's C# style guide, particularly concerning the construction of JSON request bodies, the implementation of helper methods, and consistent use of modern C# syntax. Refactoring these areas will significantly improve the code's readability and maintainability.

Comment on lines +84 to +85
var requestBodyObject = BuildAddressSearchBody(postcode);
var requestBody = JsonSerializer.Serialize(requestBodyObject);

Choose a reason for hiding this comment

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

medium

This approach of building the request body using a helper method (BuildAddressSearchBody) and JsonSerializer.Serialize violates a few style guide rules:

  1. Single-Use Helper: BuildAddressSearchBody is only called once, and the style guide (rule #208) recommends inlining single-use helpers.
  2. JSON Payload Construction: The guide (rule #316) strongly prefers using raw string literals ($$"""..."""`) over nested dictionaries for building JSON payloads, as they are more readable.

I suggest inlining the logic and converting it to a raw string literal. This also allows you to create a minimal payload, as recommended by rule #228.

var requestBody = $$`"`{
    "formValues": {
        "Section 1": {
            "Postcode": { "name": "Postcode", "value": "{{postcode}}", "isMandatory": true },
            "searchForAddress": { "name": "searchForAddress", "value": "yes", "isMandatory": true, "type": "radio" }
        }
    }
}`"`;
References
  1. Building JSON payloads: Use raw string literals with interpolation ($$"""...""") instead of nested Dictionary structures for better readability. (link)
  2. Create helpers only for duplication (2-3+ uses): Don't extract single-use methods for 'organizational purposes'. (link)

Comment on lines +555 to +584
private static Dictionary<string, object> CreateField(string name, string value, bool isMandatory, string type = "text", string? valueLabel = null)
{
return new Dictionary<string, object>
{
{ "name", name },
{ "type", type },
{ "id", string.Empty },
{ "value_changed", true },
{ "section_id", string.Empty },
{ "label", name },
{ "value_label", valueLabel ?? string.Empty },
{ "hasOther", false },
{ "value", value },
{ "path", string.Empty },
{ "valid", true },
{ "totals", string.Empty },
{ "suffix", string.Empty },
{ "prefix", string.Empty },
{ "summary", string.Empty },
{ "hidden", false },
{ "_hidden", false },
{ "isSummary", false },
{ "staticMap", false },
{ "isMandatory", isMandatory },
{ "isRepeatable", false },
{ "currencyPrefix", string.Empty },
{ "decimalPlaces", string.Empty },
{ "hash", string.Empty },
};
}

Choose a reason for hiding this comment

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

medium

This CreateField helper and the other request-building helpers in this file violate several style guide rules:

  • Bloated Payloads: CreateField adds many fields with default or empty values (e.g., id, path, summary). This violates the minimal payload principle (rule #228).
  • Nested Dictionaries: The helpers use nested Dictionary<string, object> to build JSON. The style guide (rule #316) strongly prefers raw string literals ($$"""..."""`) for readability.
  • Single-Use Helpers: BuildAddressSearchBody and GetBinTypeService are only used once and should be inlined (rule #208).
  • Missing Documentation: None of the private helper methods have the required XML documentation (rule #222).

I recommend a significant refactor to remove CreateField and the single-use helpers. The request bodies should be built directly where they are used with minimal raw string literals.

References
  1. Remove empty/null/default/false fields: Don't include fields in request bodies that have empty strings, null values, default values, or false booleans unless the API strictly requires them. (link)
  2. Building JSON payloads: Use raw string literals with interpolation ($$"""...""") instead of nested Dictionary structures for better readability. (link)
  3. Create helpers only for duplication (2-3+ uses). Don't extract single-use methods. (link)
  4. Always add XML documentation (/// <summary>) for helper methods. (link)

Comment on lines +30 to +44
private readonly IReadOnlyCollection<Bin> _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" ],
},
];

Choose a reason for hiding this comment

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

low

The collection initializer is missing trailing commas. The repository style guide (rule #501) requires trailing commas in multi-line initializers to improve maintainability and make future diffs cleaner.

private readonly IReadOnlyCollection<Bin> _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" ],
		},
	];
References
  1. Always use trailing commas after every property in multi-line initializers. (link)

Comment on lines +127 to +134
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,
};

Choose a reason for hiding this comment

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

low

This object initializer is missing a trailing comma after the last property. According to the repository style guide (rule #501), trailing commas should always be used in multi-line initializers.

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,
				};
References
  1. Always use trailing commas after every property in multi-line initializers. (link)

Comment on lines +318 to +330
var bins = new List<BinInfo>();
var binDetails = new List<string>();

// 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 (bins.Any(x => x.ServiceItemId == serviceItemId))
{
continue;
}

Choose a reason for hiding this comment

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

low

Using List.Any() inside a loop to check for duplicates can be inefficient for larger datasets. A more performant approach is to use a HashSet<string> to track seen serviceItemIds. Checking for existence with HashSet.Add() is an O(1) operation, which is more efficient than the current O(n) check.

var bins = new List<BinInfo>();
			var binDetails = new List<string>();
			var seenServiceItemIds = new HashSet<string>();

			// 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;
				}

Comment on lines +344 to +358
var metadata = new ClientSideOptions
{
Metadata =
{
{ "sid", clientSideResponse.Options.Metadata["sid"] },
{ "cookies", clientSideResponse.Options.Metadata["cookies"] },
{ "govDeliveryCategory", clientSideResponse.Options.Metadata["govDeliveryCategory"] },
{ "fromDate", clientSideResponse.Options.Metadata["fromDate"] },
{ "toDate", clientSideResponse.Options.Metadata["toDate"] },
{ "binData", binData },
{ "binIndex", "0" },
{ "bins", JsonSerializer.Serialize(bins) },
{ "binDays", "[]" },
},
};

Choose a reason for hiding this comment

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

low

A new ClientSideOptions object is created here, and several metadata values are copied from the incoming clientSideResponse. This is redundant and inconsistent with the logic in the next step (RequestId == 4), which modifies the existing options. To simplify, you can add the new metadata keys directly to clientSideResponse.Options.Metadata.

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 metadata = clientSideResponse.Options;

Comment on lines +508 to +513
{ "Postcode", CreateField("Postcode", address.Postcode ?? string.Empty, true) },
{ "List", CreateField("List", address.Uid ?? string.Empty, true, "select", address.Property) },
{ "House", CreateField("House", house, true) },
{ "Street", CreateField("Street", address.Street ?? string.Empty, true) },
{ "Town", CreateField("Town", address.Town ?? string.Empty, true) },
{ "UPRN", CreateField("UPRN", address.Uid ?? string.Empty, true) },

Choose a reason for hiding this comment

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

low

The null-coalescing operator (?? string.Empty) is used here for address properties that are expected to exist. The repository style guide (rule #391) recommends using the null-forgiving operator (!) to ensure the collector fails fast if required data is missing.

							{ "Postcode", CreateField("Postcode", address.Postcode!, true) },
							{ "List", CreateField("List", address.Uid!, true, "select", address.Property) },
							{ "House", CreateField("House", house, true) },
							{ "Street", CreateField("Street", address.Street!, true) },
							{ "Town", CreateField("Town", address.Town!, true) },
							{ "UPRN", CreateField("UPRN", address.Uid!, true) },
References
  1. Use null-forgiving operator ! when values are required: When a value must be non-null for the collector to work properly, use the null-forgiving operator instead of defensive null checks. (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.

Kirklees Council

2 participants