Skip to content

Commit 32bcff7

Browse files
authored
Merge pull request #7 from ctrl-hub/feat/hydrate-property-data
feat: support property data
2 parents 4370403 + dd53d34 commit 32bcff7

File tree

3 files changed

+320
-1
lines changed

3 files changed

+320
-1
lines changed
Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,65 @@
11
package com.ctrlhub.core.geo
22

33
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
4+
import com.fasterxml.jackson.annotation.JsonProperty
45
import com.github.jasminb.jsonapi.StringIdHandler
56
import com.github.jasminb.jsonapi.annotations.Id
67
import com.github.jasminb.jsonapi.annotations.Type
78

89
@Type("properties")
910
@JsonIgnoreProperties(ignoreUnknown = true)
1011
data class Property(
11-
@Id(StringIdHandler::class) val id: String? = null
12+
@Id(StringIdHandler::class) val id: String? = null,
13+
val address: Address? = null,
14+
val location: Location? = null,
15+
val meters: List<Meter>? = null,
16+
val psr: Psr? = null,
17+
val uprn: Long? = null
18+
)
19+
20+
@JsonIgnoreProperties(ignoreUnknown = true)
21+
data class Address(
22+
val description: String? = null,
23+
val department: String? = null,
24+
val organisation: String? = null,
25+
val number: String? = null,
26+
val name: String? = null,
27+
val thoroughfare: String? = null,
28+
@JsonProperty("dependent_thoroughfare") val dependentThoroughfare: String? = null,
29+
@JsonProperty("post_town") val postTown: String? = null,
30+
val postcode: String? = null,
31+
@JsonProperty("po_box") val poBox: String? = null,
32+
val country: String? = null
33+
)
34+
35+
@JsonIgnoreProperties(ignoreUnknown = true)
36+
data class Location(
37+
@JsonProperty("british_national_grid") val britishNationalGrid: BritishNationalGrid? = null,
38+
@JsonProperty("lat_long") val latLong: LatLong? = null
39+
)
40+
41+
@JsonIgnoreProperties(ignoreUnknown = true)
42+
data class BritishNationalGrid(
43+
val easting: Int? = null,
44+
val northing: Int? = null
45+
)
46+
47+
@JsonIgnoreProperties(ignoreUnknown = true)
48+
data class LatLong(
49+
val latitude: Double? = null,
50+
val longitude: Double? = null
51+
)
52+
53+
@JsonIgnoreProperties(ignoreUnknown = true)
54+
data class Psr(
55+
val indicator: Boolean? = null,
56+
val priority: String? = null,
57+
val notes: String? = null,
58+
val contact: String? = null
59+
)
60+
61+
@JsonIgnoreProperties(ignoreUnknown = true)
62+
data class Meter(
63+
val type: String? = null,
64+
val number: Long? = null
1265
)

src/test/kotlin/com/ctrlhub/core/projects/operations/OperationsRouterTest.kt

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,84 @@ class OperationsRouterTest {
141141
assertEquals("timeband-1", appointment?.timeBand?.id)
142142
}
143143
}
144+
145+
@Test
146+
fun `can retrieve operation with included properties`() {
147+
val jsonFilePath = Paths.get("src/test/resources/projects/operations/all-operations-with-included-properties.json")
148+
val jsonContent = Files.readString(jsonFilePath)
149+
150+
val mockEngine = MockEngine { request ->
151+
respond(
152+
content = jsonContent,
153+
status = HttpStatusCode.OK,
154+
headers = headersOf(HttpHeaders.ContentType, "application/json")
155+
)
156+
}
157+
158+
val operationsRouter = OperationsRouter(httpClient = HttpClient(mockEngine).configureForTest())
159+
160+
runBlocking {
161+
val response = operationsRouter.one(
162+
organisationId = "123",
163+
operationId = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
164+
requestParameters = OperationRequestParameters(
165+
includes = listOf(
166+
OperationIncludes.Properties
167+
)
168+
)
169+
)
170+
171+
assertIs<Operation>(response)
172+
assertNotNull(response.properties, "Properties should not be null")
173+
assertEquals(2, response.properties?.size, "Should have 2 properties")
174+
175+
// Validate first property
176+
val property1 = response.properties?.find { it.id == "f6a7b8c9-d0e1-4f2a-3b4c-5d6e7f8a9b0c" }
177+
assertNotNull(property1, "First property should exist")
178+
179+
// Validate address details
180+
assertNotNull(property1?.address)
181+
assertEquals("42, Lorem Street, AB12 3CD", property1?.address?.description)
182+
assertEquals("42", property1?.address?.number)
183+
assertEquals("Lorem House", property1?.address?.name)
184+
assertEquals("Lorem Street", property1?.address?.thoroughfare)
185+
assertEquals("DOLOR", property1?.address?.postTown)
186+
assertEquals("AB12 3CD", property1?.address?.postcode)
187+
assertEquals("United Kingdom", property1?.address?.country)
188+
189+
// Validate location details
190+
assertNotNull(property1?.location)
191+
assertNotNull(property1?.location?.latLong)
192+
assertEquals(51.5074, property1?.location?.latLong?.latitude)
193+
assertEquals(-0.1278, property1?.location?.latLong?.longitude)
194+
195+
// Validate meters
196+
assertNotNull(property1?.meters)
197+
assertEquals(1, property1?.meters?.size)
198+
assertEquals("mprn", property1?.meters?.get(0)?.type)
199+
assertEquals(1234567890, property1?.meters?.get(0)?.number)
200+
201+
// Validate UPRN
202+
assertEquals(100000000001, property1?.uprn)
203+
204+
// Validate PSR data
205+
assertNotNull(property1?.psr)
206+
assertEquals(false, property1?.psr?.indicator)
207+
208+
// Validate second property with PSR indicator
209+
val property2 = response.properties?.find { it.id == "e5f6a7b8-c9d0-4e1f-2a3b-4c5d6e7f8a9b" }
210+
assertNotNull(property2, "Second property should exist")
211+
assertEquals("15, Dolor Avenue, EF45 6GH", property2?.address?.description)
212+
assertEquals(100000000002, property2?.uprn)
213+
214+
// Validate meters for property2
215+
assertNotNull(property2?.meters)
216+
assertEquals(1, property2?.meters?.size)
217+
assertEquals("mprn", property2?.meters?.get(0)?.type)
218+
assertEquals(9876543210, property2?.meters?.get(0)?.number)
219+
220+
// Validate PSR
221+
assertEquals(true, property2?.psr?.indicator)
222+
}
223+
}
144224
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
{
2+
"data": {
3+
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
4+
"type": "operations",
5+
"attributes": {
6+
"code": "",
7+
"dates": {
8+
"scheduled": {
9+
"start": "2025-08-15T07:00:00Z",
10+
"end": "2025-08-15T11:00:00Z"
11+
}
12+
},
13+
"description": "",
14+
"done_reason": "",
15+
"labels": [],
16+
"name": "Lorem Ipsum Operation",
17+
"requirements": {
18+
"forms": [
19+
{
20+
"id": "f1a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5c",
21+
"required": true
22+
}
23+
]
24+
},
25+
"status": "todo"
26+
},
27+
"relationships": {
28+
"appointment": {
29+
"data": {
30+
"id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
31+
"type": "appointments"
32+
}
33+
},
34+
"assignees": {
35+
"data": [
36+
{
37+
"id": "c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f",
38+
"type": "users"
39+
}
40+
]
41+
},
42+
"forms": {
43+
"data": [
44+
{
45+
"id": "f1a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5c",
46+
"type": "forms"
47+
}
48+
]
49+
},
50+
"organisation": {
51+
"data": {
52+
"id": "d4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f8a",
53+
"type": "organisations"
54+
}
55+
},
56+
"permits": {
57+
"data": []
58+
},
59+
"properties": {
60+
"data": [
61+
{
62+
"id": "e5f6a7b8-c9d0-4e1f-2a3b-4c5d6e7f8a9b",
63+
"type": "properties"
64+
},
65+
{
66+
"id": "f6a7b8c9-d0e1-4f2a-3b4c-5d6e7f8a9b0c",
67+
"type": "properties"
68+
}
69+
]
70+
},
71+
"scheme": {
72+
"data": {
73+
"id": "a7b8c9d0-e1f2-4a3b-4c5d-6e7f8a9b0c1d",
74+
"type": "schemes"
75+
}
76+
},
77+
"streets": {
78+
"data": []
79+
},
80+
"teams": {
81+
"data": []
82+
},
83+
"template": {
84+
"data": null
85+
},
86+
"work_order": {
87+
"data": {
88+
"id": "b8c9d0e1-f2a3-4b4c-5d6e-7f8a9b0c1d2e",
89+
"type": "work-orders"
90+
}
91+
}
92+
},
93+
"meta": {
94+
"created_at": "2025-08-14T09:23:43.023Z",
95+
"updated_at": "2025-10-07T10:41:18.231Z",
96+
"counts": {
97+
"properties": 2,
98+
"streets": 0
99+
}
100+
}
101+
},
102+
"jsonapi": {
103+
"version": "1.0"
104+
},
105+
"included": [
106+
{
107+
"id": "f6a7b8c9-d0e1-4f2a-3b4c-5d6e7f8a9b0c",
108+
"type": "properties",
109+
"attributes": {
110+
"address": {
111+
"description": "42, Lorem Street, AB12 3CD",
112+
"department": "Department of Lorem",
113+
"organisation": "Ipsum Corporation",
114+
"number": "42",
115+
"name": "Lorem House",
116+
"thoroughfare": "Lorem Street",
117+
"dependent_thoroughfare": "Ipsum Lane",
118+
"post_town": "DOLOR",
119+
"postcode": "AB12 3CD",
120+
"po_box": "",
121+
"country": "United Kingdom"
122+
},
123+
"location": {
124+
"british_national_grid": null,
125+
"lat_long": {
126+
"latitude": 51.5074,
127+
"longitude": -0.1278
128+
}
129+
},
130+
"meters": [
131+
{
132+
"type": "mprn",
133+
"number": 1234567890
134+
}
135+
],
136+
"psr": {
137+
"indicator": false,
138+
"priority": null,
139+
"notes": null,
140+
"contact": null
141+
},
142+
"uprn": 100000000001
143+
}
144+
},
145+
{
146+
"id": "e5f6a7b8-c9d0-4e1f-2a3b-4c5d6e7f8a9b",
147+
"type": "properties",
148+
"attributes": {
149+
"address": {
150+
"description": "15, Dolor Avenue, EF45 6GH",
151+
"department": "Sit Amet Department",
152+
"organisation": "Consectetur Limited",
153+
"number": "15",
154+
"name": "Amet Building",
155+
"thoroughfare": "Dolor Avenue",
156+
"dependent_thoroughfare": "Consectetur Road",
157+
"post_town": "ADIPISCING",
158+
"postcode": "EF45 6GH",
159+
"po_box": "",
160+
"country": "United Kingdom"
161+
},
162+
"location": {
163+
"british_national_grid": null,
164+
"lat_long": {
165+
"latitude": 51.5145,
166+
"longitude": -0.0945
167+
}
168+
},
169+
"meters": [
170+
{
171+
"type": "mprn",
172+
"number": 9876543210
173+
}
174+
],
175+
"psr": {
176+
"indicator": true,
177+
"priority": "high",
178+
"notes": "Lorem ipsum dolor sit amet",
179+
"contact": "John Doe"
180+
},
181+
"uprn": 100000000002
182+
}
183+
}
184+
]
185+
}
186+

0 commit comments

Comments
 (0)