From 5e05059e4519e199d884fa8f9e5d1de47192dae8 Mon Sep 17 00:00:00 2001 From: andreia Date: Fri, 3 Jul 2026 16:36:23 +0200 Subject: [PATCH 1/4] add date and survey name to QR code --- core/domain/build.gradle.kts | 1 + .../domain/usecases/GetLoiReportUseCase.kt | 36 ++++++++--- .../usecases/GetLoiReportUseCaseTest.kt | 62 +++++++++++++++++++ gradle/libs.versions.toml | 2 + 4 files changed, 94 insertions(+), 7 deletions(-) diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts index c1c08502fd..70f2b6cbee 100644 --- a/core/domain/build.gradle.kts +++ b/core/domain/build.gradle.kts @@ -40,6 +40,7 @@ kotlin { implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.collections.immutable) implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.datetime) } } diff --git a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt index b3fe692e3d..2c9d8c28e3 100644 --- a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt @@ -15,6 +15,11 @@ */ package org.groundplatform.domain.usecases +import kotlin.time.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.format +import kotlinx.datetime.format.DateTimeComponents +import kotlinx.datetime.offsetAt import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject @@ -28,7 +33,6 @@ import org.groundplatform.domain.model.geometry.MultiPolygon import org.groundplatform.domain.model.geometry.Point import org.groundplatform.domain.model.geometry.Polygon import org.groundplatform.domain.model.locationofinterest.LOI_NAME_PROPERTY -import org.groundplatform.domain.model.locationofinterest.LoiProperties import org.groundplatform.domain.model.locationofinterest.LoiReport import org.groundplatform.domain.repository.LocationOfInterestRepositoryInterface import org.groundplatform.domain.repository.SubmissionRepositoryInterface @@ -73,21 +77,36 @@ class GetLoiReportUseCase( submissions = submissions, ) } else null + val properties = buildMap { + loi.properties + .filter { property -> property.key == LOI_NAME_PROPERTY } + .forEach { (key, value) -> put(key, value.toJsonPrimitive()) } + submissionDetails + ?.let { mapOf(KEY_SURVEY to it.surveyName, KEY_DATE to formatIsoDateTime(it.dateMillis)) } + ?.forEach { (key, value) -> put(key, JsonPrimitive(value)) } + } return LoiReport( loiName = loiName, - geoJson = - loi.geometry.toGeoJson( - loi.properties.filter { property -> property.key == LOI_NAME_PROPERTY } - ), + geoJson = loi.geometry.toGeoJson(properties = properties), submissionDetails = submissionDetails, ) } + /** + * Formats an epoch-milliseconds timestamp as an ISO-8601 date-time with the device's UTC offset + * at that instant, e.g. `2026-07-03T14:32:00+03:00`.. + */ + private fun formatIsoDateTime(epochMillis: Long): String { + val instant = Instant.fromEpochMilliseconds(epochMillis) + val offset = TimeZone.currentSystemDefault().offsetAt(instant) + return instant.format(DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET, offset) + } + /** * Converts a [Geometry] to its GeoJSON representation as defined by * [RFC 7946](https://datatracker.ietf.org/doc/html/rfc7946). */ - private fun Geometry.toGeoJson(loiProperties: LoiProperties): JsonObject { + private fun Geometry.toGeoJson(properties: Map): JsonObject { val geometryJson = when (this) { is Point -> geoJsonObject(TYPE_POINT, coordinatesToPosition(coordinates)) @@ -100,10 +119,11 @@ class GetLoiReportUseCase( is MultiPolygon -> geoJsonObject(TYPE_MULTI_POLYGON, JsonArray(polygons.map { polygonToCoordinates(it) })) } + return JsonObject( mapOf( KEY_TYPE to JsonPrimitive(TYPE_FEATURE), - KEY_PROPERTIES to JsonObject(loiProperties.mapValues { it.value.toJsonPrimitive() }), + KEY_PROPERTIES to JsonObject(properties), KEY_GEOMETRY to geometryJson, ) ) @@ -147,6 +167,8 @@ class GetLoiReportUseCase( const val KEY_PROPERTIES = "properties" const val KEY_GEOMETRY = "geometry" const val KEY_COORDINATES = "coordinates" + const val KEY_SURVEY = "survey" + const val KEY_DATE = "date" const val TYPE_POINT = "Point" const val TYPE_LINE_STRING = "LineString" const val TYPE_POLYGON = "Polygon" diff --git a/core/domain/src/commonTest/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCaseTest.kt b/core/domain/src/commonTest/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCaseTest.kt index b922d4b79d..f21a858f74 100644 --- a/core/domain/src/commonTest/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCaseTest.kt +++ b/core/domain/src/commonTest/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCaseTest.kt @@ -19,8 +19,12 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNull +import kotlin.time.Instant import kotlinx.coroutines.test.runTest +import kotlinx.datetime.format.DateTimeComponents import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import org.groundplatform.domain.model.geometry.Coordinates import org.groundplatform.domain.model.geometry.Geometry import org.groundplatform.domain.model.geometry.LineString @@ -310,6 +314,64 @@ class GetLoiReportUseCaseTest { assertEquals(Json.parseToJsonElement(expectedGeoJson), loiReport.geoJson) } + @Test + fun `Should embed survey name and ISO-8601 date in geoJson properties when submissions exist`() = + runTest { + surveyRepository.offlineSurveys = + listOf(FakeDataGenerator.newSurvey(id = "surveyId", title = "Restoration areas")) + submissionRepository.submissions = listOf(FakeDataGenerator.newSubmission()) + loiRepository.offlineLoi = + loiRepository.offlineLoi.copy( + geometry = Point(Coordinates(lat = 1.0, lng = -1.0)), + properties = generateProperties("Point test"), + lastModified = + AuditInfo(user = FakeDataGenerator.newUser(), clientTimestamp = 1_700_000_000_000L), + ) + + val loiReport = + getLoiReportUseCase.invoke(loiName = "Point test", loiId = "loiId", surveyId = "surveyId")!! + + val properties = loiReport.geoJson["properties"]!!.jsonObject + assertEquals("Restoration areas", properties["survey"]!!.jsonPrimitive.content) + val date = properties["date"]!!.jsonPrimitive.content + assertEquals( + Instant.fromEpochMilliseconds(1_700_000_000_000L), + DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET.parse(date).toInstantUsingOffset(), + ) + } + + @Test + fun `Should not embed survey or date in geoJson properties when no submissions exist`() = + runTest { + submissionRepository.submissions = emptyList() + loiRepository.offlineLoi = + loiRepository.offlineLoi.copy( + geometry = Point(Coordinates(lat = 0.0, lng = 0.0)), + properties = generateProperties("No submissions"), + ) + + val loiReport = + getLoiReportUseCase.invoke( + loiName = "No submissions", + loiId = "loiId", + surveyId = "surveyId", + )!! + + val expectedGeoJson = + """ + { + "type": "Feature", + "properties": {"name": "No submissions"}, + "geometry": { + "type": "Point", + "coordinates": [0.000000, 0.000000] + } + } + """ + .trimIndent() + assertEquals(Json.parseToJsonElement(expectedGeoJson), loiReport.geoJson) + } + @Test fun `Should populate loiName, userName and dateMillis from the inputs`() = runTest { userRepository.currentUser = FakeDataGenerator.newUser(displayName = "John Doe") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 44e1ad2209..5663021315 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,6 +52,7 @@ kotlinVersion = "2.3.21" kotlinxBom = "1.11.0" kspVersion = "2.3.9" kotlinxCollectionsImmutable = "0.5.1" +kotlinxDatetime = "0.8.0" ktfmtVersion = "0.26.0" lifecycleVersion = "2.11.0" markdownVersion = "0.7.5" @@ -157,6 +158,7 @@ kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutine kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "coroutinesVersion" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutinesVersion" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutinesVersion" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } kotlinx-serialization-bom = { module = "org.jetbrains.kotlinx:kotlinx-serialization-bom", version.ref = "kotlinxBom" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxBom" } kotlinx-serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf" } From 04014e697fe62088906fdf5a1eee186e0b8e8375 Mon Sep 17 00:00:00 2001 From: andreia Date: Tue, 7 Jul 2026 11:17:48 +0200 Subject: [PATCH 2/4] adjust date format and differences between predefined LOI and adHoc --- .../locationofinterest/LocationOfInterest.kt | 1 + .../domain/usecases/GetLoiReportUseCase.kt | 65 ++++++++++++------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/model/locationofinterest/LocationOfInterest.kt b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/model/locationofinterest/LocationOfInterest.kt index 4ff31fe787..7e209ac7d4 100644 --- a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/model/locationofinterest/LocationOfInterest.kt +++ b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/model/locationofinterest/LocationOfInterest.kt @@ -24,6 +24,7 @@ import org.groundplatform.domain.model.mutation.Mutation typealias LoiProperties = Map const val LOI_NAME_PROPERTY = "name" +const val LOI_ID_PROPERTY = "id" fun generateProperties(loiName: String? = null): LoiProperties = loiName?.let { mapOf(LOI_NAME_PROPERTY to it) } ?: mapOf() diff --git a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt index 2c9d8c28e3..00985f0137 100644 --- a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt @@ -16,10 +16,10 @@ package org.groundplatform.domain.usecases import kotlin.time.Instant +import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone -import kotlinx.datetime.format -import kotlinx.datetime.format.DateTimeComponents -import kotlinx.datetime.offsetAt +import kotlinx.datetime.format.char +import kotlinx.datetime.toLocalDateTime import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject @@ -32,7 +32,9 @@ import org.groundplatform.domain.model.geometry.LinearRing import org.groundplatform.domain.model.geometry.MultiPolygon import org.groundplatform.domain.model.geometry.Point import org.groundplatform.domain.model.geometry.Polygon +import org.groundplatform.domain.model.locationofinterest.LOI_ID_PROPERTY import org.groundplatform.domain.model.locationofinterest.LOI_NAME_PROPERTY +import org.groundplatform.domain.model.locationofinterest.LocationOfInterest import org.groundplatform.domain.model.locationofinterest.LoiReport import org.groundplatform.domain.repository.LocationOfInterestRepositoryInterface import org.groundplatform.domain.repository.SubmissionRepositoryInterface @@ -65,10 +67,10 @@ class GetLoiReportUseCase( submissionRepositoryInterface.getSubmissions(loi).sortedByDescending { it.lastModified.clientTimestamp } + val surveyName = surveyRepositoryInterface.getOfflineSurvey(surveyId)?.title.orEmpty() val submissionDetails = if (submissions.isNotEmpty()) { val user = userRepositoryInterface.getAuthenticatedUser() - val surveyName = surveyRepositoryInterface.getOfflineSurvey(surveyId)?.title.orEmpty() LoiReport.SubmissionDetails( surveyName = surveyName, userName = user.displayName, @@ -77,36 +79,19 @@ class GetLoiReportUseCase( submissions = submissions, ) } else null - val properties = buildMap { - loi.properties - .filter { property -> property.key == LOI_NAME_PROPERTY } - .forEach { (key, value) -> put(key, value.toJsonPrimitive()) } - submissionDetails - ?.let { mapOf(KEY_SURVEY to it.surveyName, KEY_DATE to formatIsoDateTime(it.dateMillis)) } - ?.forEach { (key, value) -> put(key, JsonPrimitive(value)) } - } + return LoiReport( loiName = loiName, - geoJson = loi.geometry.toGeoJson(properties = properties), + geoJson = loi.geometry.toGeoJson(loi = loi, surveyName = surveyName), submissionDetails = submissionDetails, ) } - /** - * Formats an epoch-milliseconds timestamp as an ISO-8601 date-time with the device's UTC offset - * at that instant, e.g. `2026-07-03T14:32:00+03:00`.. - */ - private fun formatIsoDateTime(epochMillis: Long): String { - val instant = Instant.fromEpochMilliseconds(epochMillis) - val offset = TimeZone.currentSystemDefault().offsetAt(instant) - return instant.format(DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET, offset) - } - /** * Converts a [Geometry] to its GeoJSON representation as defined by * [RFC 7946](https://datatracker.ietf.org/doc/html/rfc7946). */ - private fun Geometry.toGeoJson(properties: Map): JsonObject { + private fun Geometry.toGeoJson(loi: LocationOfInterest, surveyName: String): JsonObject { val geometryJson = when (this) { is Point -> geoJsonObject(TYPE_POINT, coordinatesToPosition(coordinates)) @@ -119,6 +104,7 @@ class GetLoiReportUseCase( is MultiPolygon -> geoJsonObject(TYPE_MULTI_POLYGON, JsonArray(polygons.map { polygonToCoordinates(it) })) } + val properties = getLoiPropertiesMap(loi, surveyName) return JsonObject( mapOf( @@ -161,6 +147,37 @@ class GetLoiReportUseCase( return JsonUnquotedLiteral(value) } + private fun getLoiPropertiesMap( + loi: LocationOfInterest, + surveyName: String, + ): Map = buildMap { + loi.properties[LOI_NAME_PROPERTY]?.let { put(LOI_NAME_PROPERTY, it.toJsonPrimitive()) } + ?: loi.properties[LOI_ID_PROPERTY]?.let { put(LOI_ID_PROPERTY, it.toJsonPrimitive()) } + put(KEY_SURVEY, JsonPrimitive(surveyName)) + if (loi.isPredefined != true) { + put(KEY_DATE, JsonPrimitive(formatDateTime(loi.lastModified.clientTimestamp))) + } + } + + /** + * Formats an epoch-milliseconds timestamp as YYYYMMDD- HH:MM (24h) in the device's local time + * zone (e.g. 20260703 10:00). + */ + internal fun formatDateTime(epochMillis: Long): String { + val instant = Instant.fromEpochMilliseconds(epochMillis) + val local = instant.toLocalDateTime(TimeZone.currentSystemDefault()) + val fmt = LocalDateTime.Format { + year() + monthNumber() + day() + char(' ') + hour() + char(':') + minute() + } + return fmt.format(local) + } + private companion object { const val KEY_TYPE = "type" const val TYPE_FEATURE = "Feature" From fdfb445a7292458e02659eacd863dd300e3fa43e Mon Sep 17 00:00:00 2001 From: andreia Date: Tue, 7 Jul 2026 16:28:24 +0200 Subject: [PATCH 3/4] update unit tests --- .../usecases/GetLoiReportUseCaseTest.kt | 312 ++++++++++-------- 1 file changed, 168 insertions(+), 144 deletions(-) diff --git a/core/domain/src/commonTest/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCaseTest.kt b/core/domain/src/commonTest/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCaseTest.kt index f21a858f74..799ba07b99 100644 --- a/core/domain/src/commonTest/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCaseTest.kt +++ b/core/domain/src/commonTest/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCaseTest.kt @@ -19,12 +19,10 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNull -import kotlin.time.Instant import kotlinx.coroutines.test.runTest -import kotlinx.datetime.format.DateTimeComponents import kotlinx.serialization.json.Json -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive import org.groundplatform.domain.model.geometry.Coordinates import org.groundplatform.domain.model.geometry.Geometry import org.groundplatform.domain.model.geometry.LineString @@ -59,19 +57,18 @@ class GetLoiReportUseCaseTest { properties = generateProperties("Point test"), ) - val expectedGeoJson = - """ - { - "type": "Feature", - "properties": {"name": "Point test"}, - "geometry": { + assertGeoJson( + loiReport, + properties = expectedProperties(name = "Point test"), + geometry = + """ + { "type": "Point", "coordinates": [-89.000000, 41.000000] } - } - """ - .trimIndent() - assertEquals(Json.parseToJsonElement(expectedGeoJson), loiReport.geoJson) + """ + .trimIndent(), + ) } @Test @@ -88,21 +85,20 @@ class GetLoiReportUseCaseTest { val loiReport = invokeUseCase(geometry = Polygon(shell), properties = generateProperties("Polygon test")) - val expectedGeoJson = - """ - { - "type": "Feature", - "properties": {"name": "Polygon test"}, - "geometry": { + assertGeoJson( + loiReport, + properties = expectedProperties(name = "Polygon test"), + geometry = + """ + { "type": "Polygon", "coordinates": [ [[0.000000, 0.000000], [0.000000, 1.000000], [1.000000, 1.000000], [0.000000, 0.000000]] ] } - } - """ - .trimIndent() - assertEquals(Json.parseToJsonElement(expectedGeoJson), loiReport.geoJson) + """ + .trimIndent(), + ) } @Test @@ -131,22 +127,21 @@ class GetLoiReportUseCaseTest { properties = generateProperties("Polygon with holes test"), ) - val expectedGeoJson = - """ - { - "type": "Feature", - "properties": {"name": "Polygon with holes test"}, - "geometry": { + assertGeoJson( + loiReport, + properties = expectedProperties(name = "Polygon with holes test"), + geometry = + """ + { "type": "Polygon", "coordinates": [ [[0.000000, 0.000000], [0.000000, 10.000000], [10.000000, 10.000000], [0.000000, 0.000000]], [[2.000000, 2.000000], [2.000000, 3.000000], [3.000000, 3.000000], [2.000000, 2.000000]] ] } - } - """ - .trimIndent() - assertEquals(Json.parseToJsonElement(expectedGeoJson), loiReport.geoJson) + """ + .trimIndent(), + ) } @Test @@ -175,22 +170,21 @@ class GetLoiReportUseCaseTest { properties = generateProperties("MultiPolygon test"), ) - val expectedGeoJson = - """ - { - "type": "Feature", - "properties": {"name": "MultiPolygon test"}, - "geometry": { + assertGeoJson( + loiReport, + properties = expectedProperties(name = "MultiPolygon test"), + geometry = + """ + { "type": "MultiPolygon", "coordinates": [ [[[0.000000, 0.000000], [0.000000, 1.000000], [1.000000, 1.000000], [0.000000, 0.000000]]], [[[5.000000, 5.000000], [5.000000, 6.000000], [6.000000, 6.000000], [5.000000, 5.000000]]] ] } - } - """ - .trimIndent() - assertEquals(Json.parseToJsonElement(expectedGeoJson), loiReport.geoJson) + """ + .trimIndent(), + ) } @Test @@ -200,42 +194,63 @@ class GetLoiReportUseCaseTest { val loiReport = invokeUseCase(geometry = lineString, properties = generateProperties("LineString test")) - val expectedGeoJson = - """ - { - "type": "Feature", - "properties": {"name": "LineString test"}, - "geometry": { + assertGeoJson( + loiReport, + properties = expectedProperties(name = "LineString test"), + geometry = + """ + { "type": "LineString", "coordinates": [[20.000000, 10.000000], [40.000000, 30.000000], [60.000000, 50.000000]] } - } - """ - .trimIndent() - assertEquals(Json.parseToJsonElement(expectedGeoJson), loiReport.geoJson) + """ + .trimIndent(), + ) } @Test - fun `Should get a report with empty properties when no name is provided`() = runTest { + fun `Should still include survey and date when no name is provided`() = runTest { val loiReport = invokeUseCase( geometry = Point(Coordinates(lat = 0.0, lng = 0.0)), properties = generateProperties(), ) - val expectedGeoJson = - """ - { - "type": "Feature", - "properties": {}, - "geometry": { + assertGeoJson( + loiReport, + properties = expectedProperties(), + geometry = + """ + { "type": "Point", "coordinates": [0.000000, 0.000000] } - } - """ - .trimIndent() - assertEquals(Json.parseToJsonElement(expectedGeoJson), loiReport.geoJson) + """ + .trimIndent(), + ) + } + + @Test + fun `Should not include date for predefined LOIs`() = runTest { + val loiReport = + invokeUseCase( + geometry = Point(Coordinates(lat = 0.0, lng = 0.0)), + properties = generateProperties("Predefined test"), + isPredefined = true, + ) + + assertGeoJson( + loiReport, + properties = expectedProperties(name = "Predefined test", includeDate = false), + geometry = + """ + { + "type": "Point", + "coordinates": [0.000000, 0.000000] + } + """ + .trimIndent(), + ) } @Test @@ -269,12 +284,12 @@ class GetLoiReportUseCaseTest { val loiReport = invokeUseCase(geometry = lineString, properties = generateProperties("Rounding test")) - val expectedGeoJson = - """ - { - "type": "Feature", - "properties": {"name": "Rounding test"}, - "geometry": { + assertGeoJson( + loiReport, + properties = expectedProperties(name = "Rounding test"), + geometry = + """ + { "type": "LineString", "coordinates": [ [2.987654, 1.123457], @@ -282,99 +297,63 @@ class GetLoiReportUseCaseTest { [6.987654, 5.123457] ] } - } - """ - .trimIndent() - - assertEquals(Json.parseToJsonElement(expectedGeoJson), loiReport.geoJson) + """ + .trimIndent(), + ) } @Test - fun `Should only include name property even if more are provided`() = runTest { - val properties = - generateProperties("Name test") + - mapOf("description" to "Should be removed", "extra" to "Also removed") - + fun `Should only include loiName, surveyName and date as properties`() = runTest { val loiReport = - invokeUseCase(geometry = Point(Coordinates(lat = 0.0, lng = 0.0)), properties = properties) + invokeUseCase( + geometry = Point(Coordinates(lat = 0.0, lng = 0.0)), + properties = + generateProperties("Properties test") + + mapOf("description" to "Should be removed", "extra" to "Also removed"), + ) - val expectedGeoJson = - """ - { - "type": "Feature", - "properties": {"name": "Name test"}, - "geometry": { + assertGeoJson( + loiReport, + properties = expectedProperties(name = "Properties test"), + geometry = + """ + { "type": "Point", "coordinates": [0.000000, 0.000000] } - } - """ - .trimIndent() - - assertEquals(Json.parseToJsonElement(expectedGeoJson), loiReport.geoJson) + """ + .trimIndent(), + ) } @Test - fun `Should embed survey name and ISO-8601 date in geoJson properties when submissions exist`() = + fun `Should only include loiId, surveyName and date as properties, when no loiName is present`() = runTest { - surveyRepository.offlineSurveys = - listOf(FakeDataGenerator.newSurvey(id = "surveyId", title = "Restoration areas")) - submissionRepository.submissions = listOf(FakeDataGenerator.newSubmission()) - loiRepository.offlineLoi = - loiRepository.offlineLoi.copy( - geometry = Point(Coordinates(lat = 1.0, lng = -1.0)), - properties = generateProperties("Point test"), - lastModified = - AuditInfo(user = FakeDataGenerator.newUser(), clientTimestamp = 1_700_000_000_000L), - ) - val loiReport = - getLoiReportUseCase.invoke(loiName = "Point test", loiId = "loiId", surveyId = "surveyId")!! - - val properties = loiReport.geoJson["properties"]!!.jsonObject - assertEquals("Restoration areas", properties["survey"]!!.jsonPrimitive.content) - val date = properties["date"]!!.jsonPrimitive.content - assertEquals( - Instant.fromEpochMilliseconds(1_700_000_000_000L), - DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET.parse(date).toInstantUsingOffset(), - ) - } - - @Test - fun `Should not embed survey or date in geoJson properties when no submissions exist`() = - runTest { - submissionRepository.submissions = emptyList() - loiRepository.offlineLoi = - loiRepository.offlineLoi.copy( + invokeUseCase( geometry = Point(Coordinates(lat = 0.0, lng = 0.0)), - properties = generateProperties("No submissions"), + properties = + mapOf("id" to "loiId", "description" to "Should be removed", "extra" to "Also removed"), ) - val loiReport = - getLoiReportUseCase.invoke( - loiName = "No submissions", - loiId = "loiId", - surveyId = "surveyId", - )!! - - val expectedGeoJson = - """ - { - "type": "Feature", - "properties": {"name": "No submissions"}, - "geometry": { + assertGeoJson( + loiReport, + properties = expectedProperties(id = "loiId"), + geometry = + """ + { "type": "Point", "coordinates": [0.000000, 0.000000] } - } - """ - .trimIndent() - assertEquals(Json.parseToJsonElement(expectedGeoJson), loiReport.geoJson) + """ + .trimIndent(), + ) } @Test - fun `Should populate loiName, userName and dateMillis from the inputs`() = runTest { - userRepository.currentUser = FakeDataGenerator.newUser(displayName = "John Doe") + fun `Should populate loiName, userName, userEmail and dateMillis from the inputs`() = runTest { + userRepository.currentUser = + FakeDataGenerator.newUser(displayName = "John Doe", email = "john@example.com") submissionRepository.submissions = listOf(FakeDataGenerator.newSubmission()) loiRepository.offlineLoi = loiRepository.offlineLoi.copy( @@ -386,6 +365,7 @@ class GetLoiReportUseCaseTest { assertEquals("Test LOI", loiReport.loiName) assertEquals("John Doe", loiReport.submissionDetails!!.userName) + assertEquals("john@example.com", loiReport.submissionDetails.userEmail) assertEquals(987654321L, loiReport.submissionDetails.dateMillis) } @@ -440,9 +420,53 @@ class GetLoiReportUseCaseTest { assertNull(loiReport.submissionDetails) } - private suspend fun invokeUseCase(geometry: Geometry, properties: LoiProperties): LoiReport { + private suspend fun invokeUseCase( + geometry: Geometry, + properties: LoiProperties, + isPredefined: Boolean? = false, + ): LoiReport { + surveyRepository.offlineSurveys = + listOf(FakeDataGenerator.newSurvey(id = "surveyId", title = SURVEY_TITLE)) loiRepository.offlineLoi = - loiRepository.offlineLoi.copy(geometry = geometry, properties = properties) + loiRepository.offlineLoi.copy( + geometry = geometry, + properties = properties, + isPredefined = isPredefined, + lastModified = AuditInfo(FakeDataGenerator.newUser(), clientTimestamp = TEST_TIMESTAMP), + ) return getLoiReportUseCase.invoke("loiName", "loiId", "surveyId")!! } + + private fun expectedProperties( + name: String? = null, + id: String? = null, + includeDate: Boolean = true, + ): String { + val expectedDate = getLoiReportUseCase.formatDateTime(TEST_TIMESTAMP) + val properties = buildMap { + name?.let { put("name", it) } + id?.let { put("id", it) } + put("survey", SURVEY_TITLE) + if (includeDate) put("date", expectedDate) + } + return JsonObject(properties.mapValues { JsonPrimitive(it.value) }).toString() + } + + private fun assertGeoJson(loiReport: LoiReport, properties: String, geometry: String) { + val expected = + """ + { + "type": "Feature", + "properties": $properties, + "geometry": $geometry + } + """ + .trimIndent() + assertEquals(Json.parseToJsonElement(expected), loiReport.geoJson) + } + + private companion object { + const val SURVEY_TITLE = "Restoration areas" + const val TEST_TIMESTAMP = 0L + } } From 43abd55daa8fd0a70219bf3266ab3d8da015ccc8 Mon Sep 17 00:00:00 2001 From: andreia Date: Tue, 7 Jul 2026 17:29:21 +0200 Subject: [PATCH 4/4] fix failing test --- .../mapcontainer/HomeScreenMapContainerViewModelTest.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModelTest.kt b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModelTest.kt index 475f43cd6a..ab90ff792a 100644 --- a/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModelTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/HomeScreenMapContainerViewModelTest.kt @@ -97,6 +97,7 @@ class HomeScreenMapContainerViewModelTest : BaseHiltTest() { viewModel.onFeatureClicked(features = setOf(LOCATION_OF_INTEREST_FEATURE)) val state = viewModel.processJobMapComponentState().first() advanceUntilIdle() + val actualGeoJson = (state as JobMapComponentState.LoiSelected).loi.loiReport!!.geoJson assertThat(state) .isEqualTo( JobMapComponentState.LoiSelected( @@ -105,7 +106,11 @@ class HomeScreenMapContainerViewModelTest : BaseHiltTest() { loi = LOCATION_OF_INTEREST, submissionCount = 0, showDeleteLoiButton = true, - loiReport = LOCATION_OF_INTEREST_LOI_REPORT.copy(submissionDetails = null), + loiReport = + LOCATION_OF_INTEREST_LOI_REPORT.copy( + submissionDetails = null, + geoJson = actualGeoJson, + ), ) ) )