From 1ebf6ab922dd9f9b51dd5db6426fdea622670c96 Mon Sep 17 00:00:00 2001 From: Jonathan Wilkinson Date: Tue, 7 Oct 2025 11:52:24 +0100 Subject: [PATCH 1/2] feat: support property data --- .../kotlin/com/ctrlhub/core/geo/Property.kt | 55 ++++++++++++- .../operations/OperationsRouterTest.kt | 80 +++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/ctrlhub/core/geo/Property.kt b/src/main/kotlin/com/ctrlhub/core/geo/Property.kt index 617e996..baaf4d0 100644 --- a/src/main/kotlin/com/ctrlhub/core/geo/Property.kt +++ b/src/main/kotlin/com/ctrlhub/core/geo/Property.kt @@ -1,6 +1,7 @@ package com.ctrlhub.core.geo import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty import com.github.jasminb.jsonapi.StringIdHandler import com.github.jasminb.jsonapi.annotations.Id import com.github.jasminb.jsonapi.annotations.Type @@ -8,5 +9,57 @@ import com.github.jasminb.jsonapi.annotations.Type @Type("properties") @JsonIgnoreProperties(ignoreUnknown = true) data class Property( - @Id(StringIdHandler::class) val id: String? = null + @Id(StringIdHandler::class) val id: String? = null, + val address: Address? = null, + val location: Location? = null, + val meters: List? = null, + val psr: Psr? = null, + val uprn: Long? = null +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class Address( + val description: String? = null, + val department: String? = null, + val organisation: String? = null, + val number: String? = null, + val name: String? = null, + val thoroughfare: String? = null, + @JsonProperty("dependent_thoroughfare") val dependentThoroughfare: String? = null, + @JsonProperty("post_town") val postTown: String? = null, + val postcode: String? = null, + @JsonProperty("po_box") val poBox: String? = null, + val country: String? = null +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class Location( + @JsonProperty("british_national_grid") val britishNationalGrid: BritishNationalGrid? = null, + @JsonProperty("lat_long") val latLong: LatLong? = null +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class BritishNationalGrid( + val easting: Int? = null, + val northing: Int? = null +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class LatLong( + val latitude: Double? = null, + val longitude: Double? = null +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class Psr( + val indicator: Boolean? = null, + val priority: String? = null, + val notes: String? = null, + val contact: String? = null +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class Meter( + val type: String? = null, + val number: Long? = null ) diff --git a/src/test/kotlin/com/ctrlhub/core/projects/operations/OperationsRouterTest.kt b/src/test/kotlin/com/ctrlhub/core/projects/operations/OperationsRouterTest.kt index 389bde0..a39c53d 100644 --- a/src/test/kotlin/com/ctrlhub/core/projects/operations/OperationsRouterTest.kt +++ b/src/test/kotlin/com/ctrlhub/core/projects/operations/OperationsRouterTest.kt @@ -141,4 +141,84 @@ class OperationsRouterTest { assertEquals("timeband-1", appointment?.timeBand?.id) } } + + @Test + fun `can retrieve operation with included properties`() { + val jsonFilePath = Paths.get("src/test/resources/projects/operations/all-operations-with-included-properties.json") + val jsonContent = Files.readString(jsonFilePath) + + val mockEngine = MockEngine { request -> + respond( + content = jsonContent, + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) + } + + val operationsRouter = OperationsRouter(httpClient = HttpClient(mockEngine).configureForTest()) + + runBlocking { + val response = operationsRouter.one( + organisationId = "123", + operationId = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", + requestParameters = OperationRequestParameters( + includes = listOf( + OperationIncludes.Properties + ) + ) + ) + + assertIs(response) + assertNotNull(response.properties, "Properties should not be null") + assertEquals(2, response.properties?.size, "Should have 2 properties") + + // Validate first property + val property1 = response.properties?.find { it.id == "f6a7b8c9-d0e1-4f2a-3b4c-5d6e7f8a9b0c" } + assertNotNull(property1, "First property should exist") + + // Validate address details + assertNotNull(property1?.address) + assertEquals("42, Lorem Street, AB12 3CD", property1?.address?.description) + assertEquals("42", property1?.address?.number) + assertEquals("Lorem House", property1?.address?.name) + assertEquals("Lorem Street", property1?.address?.thoroughfare) + assertEquals("DOLOR", property1?.address?.postTown) + assertEquals("AB12 3CD", property1?.address?.postcode) + assertEquals("United Kingdom", property1?.address?.country) + + // Validate location details + assertNotNull(property1?.location) + assertNotNull(property1?.location?.latLong) + assertEquals(51.5074, property1?.location?.latLong?.latitude) + assertEquals(-0.1278, property1?.location?.latLong?.longitude) + + // Validate meters + assertNotNull(property1?.meters) + assertEquals(1, property1?.meters?.size) + assertEquals("mprn", property1?.meters?.get(0)?.type) + assertEquals(1234567890, property1?.meters?.get(0)?.number) + + // Validate UPRN + assertEquals(100000000001, property1?.uprn) + + // Validate PSR data + assertNotNull(property1?.psr) + assertEquals(false, property1?.psr?.indicator) + + // Validate second property with PSR indicator + val property2 = response.properties?.find { it.id == "e5f6a7b8-c9d0-4e1f-2a3b-4c5d6e7f8a9b" } + assertNotNull(property2, "Second property should exist") + assertEquals("15, Dolor Avenue, EF45 6GH", property2?.address?.description) + assertEquals(100000000002, property2?.uprn) + + // Validate meters for property2 + assertNotNull(property2?.meters) + assertEquals(1, property2?.meters?.size) + assertEquals("mprn", property2?.meters?.get(0)?.type) + assertEquals(9876543210, property2?.meters?.get(0)?.number) + + // Validate PSR + assertEquals(true, property2?.psr?.indicator) + } + } } \ No newline at end of file From dd53d347031dcba4c0e30ec65c773d8bd7520fc1 Mon Sep 17 00:00:00 2001 From: Jonathan Wilkinson Date: Tue, 7 Oct 2025 11:55:09 +0100 Subject: [PATCH 2/2] fix: include test file for property hydration --- ...l-operations-with-included-properties.json | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 src/test/resources/projects/operations/all-operations-with-included-properties.json diff --git a/src/test/resources/projects/operations/all-operations-with-included-properties.json b/src/test/resources/projects/operations/all-operations-with-included-properties.json new file mode 100644 index 0000000..574493a --- /dev/null +++ b/src/test/resources/projects/operations/all-operations-with-included-properties.json @@ -0,0 +1,186 @@ +{ + "data": { + "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", + "type": "operations", + "attributes": { + "code": "", + "dates": { + "scheduled": { + "start": "2025-08-15T07:00:00Z", + "end": "2025-08-15T11:00:00Z" + } + }, + "description": "", + "done_reason": "", + "labels": [], + "name": "Lorem Ipsum Operation", + "requirements": { + "forms": [ + { + "id": "f1a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5c", + "required": true + } + ] + }, + "status": "todo" + }, + "relationships": { + "appointment": { + "data": { + "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", + "type": "appointments" + } + }, + "assignees": { + "data": [ + { + "id": "c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f", + "type": "users" + } + ] + }, + "forms": { + "data": [ + { + "id": "f1a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5c", + "type": "forms" + } + ] + }, + "organisation": { + "data": { + "id": "d4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f8a", + "type": "organisations" + } + }, + "permits": { + "data": [] + }, + "properties": { + "data": [ + { + "id": "e5f6a7b8-c9d0-4e1f-2a3b-4c5d6e7f8a9b", + "type": "properties" + }, + { + "id": "f6a7b8c9-d0e1-4f2a-3b4c-5d6e7f8a9b0c", + "type": "properties" + } + ] + }, + "scheme": { + "data": { + "id": "a7b8c9d0-e1f2-4a3b-4c5d-6e7f8a9b0c1d", + "type": "schemes" + } + }, + "streets": { + "data": [] + }, + "teams": { + "data": [] + }, + "template": { + "data": null + }, + "work_order": { + "data": { + "id": "b8c9d0e1-f2a3-4b4c-5d6e-7f8a9b0c1d2e", + "type": "work-orders" + } + } + }, + "meta": { + "created_at": "2025-08-14T09:23:43.023Z", + "updated_at": "2025-10-07T10:41:18.231Z", + "counts": { + "properties": 2, + "streets": 0 + } + } + }, + "jsonapi": { + "version": "1.0" + }, + "included": [ + { + "id": "f6a7b8c9-d0e1-4f2a-3b4c-5d6e7f8a9b0c", + "type": "properties", + "attributes": { + "address": { + "description": "42, Lorem Street, AB12 3CD", + "department": "Department of Lorem", + "organisation": "Ipsum Corporation", + "number": "42", + "name": "Lorem House", + "thoroughfare": "Lorem Street", + "dependent_thoroughfare": "Ipsum Lane", + "post_town": "DOLOR", + "postcode": "AB12 3CD", + "po_box": "", + "country": "United Kingdom" + }, + "location": { + "british_national_grid": null, + "lat_long": { + "latitude": 51.5074, + "longitude": -0.1278 + } + }, + "meters": [ + { + "type": "mprn", + "number": 1234567890 + } + ], + "psr": { + "indicator": false, + "priority": null, + "notes": null, + "contact": null + }, + "uprn": 100000000001 + } + }, + { + "id": "e5f6a7b8-c9d0-4e1f-2a3b-4c5d6e7f8a9b", + "type": "properties", + "attributes": { + "address": { + "description": "15, Dolor Avenue, EF45 6GH", + "department": "Sit Amet Department", + "organisation": "Consectetur Limited", + "number": "15", + "name": "Amet Building", + "thoroughfare": "Dolor Avenue", + "dependent_thoroughfare": "Consectetur Road", + "post_town": "ADIPISCING", + "postcode": "EF45 6GH", + "po_box": "", + "country": "United Kingdom" + }, + "location": { + "british_national_grid": null, + "lat_long": { + "latitude": 51.5145, + "longitude": -0.0945 + } + }, + "meters": [ + { + "type": "mprn", + "number": 9876543210 + } + ], + "psr": { + "indicator": true, + "priority": "high", + "notes": "Lorem ipsum dolor sit amet", + "contact": "John Doe" + }, + "uprn": 100000000002 + } + } + ] +} +