Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.cramsan.edifikana.api

import com.cramsan.edifikana.lib.model.CommonAreaId
import com.cramsan.edifikana.lib.model.PropertyId
import com.cramsan.edifikana.lib.model.network.CommonAreaListNetworkResponse
import com.cramsan.edifikana.lib.model.network.CommonAreaNetworkResponse
import com.cramsan.edifikana.lib.model.network.CreateCommonAreaNetworkRequest
import com.cramsan.edifikana.lib.model.network.UpdateCommonAreaNetworkRequest
import com.cramsan.framework.annotations.NetworkModel
import com.cramsan.framework.annotations.api.NoPathParam
import com.cramsan.framework.annotations.api.NoQueryParam
import com.cramsan.framework.annotations.api.NoRequestBody
import com.cramsan.framework.annotations.api.NoResponseBody
import com.cramsan.framework.networkapi.Api
import io.ktor.http.HttpMethod

/**
* API definition for common area operations within a property.
*
* Common areas are shared spaces (e.g. Lobby, Pool, Gym) that belong to a single property.
* All operations require MANAGER role or higher in the property's organization, depending on
* the operation. In some cases it will be ADMIN+, in others it will be MANAGER+
*/
@OptIn(NetworkModel::class)
object CommonAreaApi : Api("common-area") {

val createCommonArea = operation<
CreateCommonAreaNetworkRequest,
NoQueryParam,
NoPathParam,
CommonAreaNetworkResponse
>(HttpMethod.Post)

val getCommonArea = operation<
NoRequestBody,
NoQueryParam,
CommonAreaId,
CommonAreaNetworkResponse
>(HttpMethod.Get)

val getCommonAreasForProperty = operation<
NoRequestBody,
NoQueryParam,
PropertyId,
CommonAreaListNetworkResponse
>(HttpMethod.Get, "by-property")

val updateCommonArea = operation<
UpdateCommonAreaNetworkRequest,
NoQueryParam,
CommonAreaId,
CommonAreaNetworkResponse
>(HttpMethod.Put)

val deleteCommonArea = operation<
NoRequestBody,
NoQueryParam,
CommonAreaId,
NoResponseBody
>(HttpMethod.Delete)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package com.cramsan.edifikana.server.datastore.supabase

import com.cramsan.edifikana.lib.model.CommonAreaId
import com.cramsan.edifikana.lib.model.CommonAreaType
import com.cramsan.edifikana.lib.model.OrganizationId
import com.cramsan.edifikana.lib.model.PropertyId
import com.cramsan.edifikana.lib.model.UserId
import com.cramsan.framework.utils.uuid.UUID
import kotlinx.coroutines.runBlocking
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue

class SupabaseCommonAreaDatastoreIntegrationTest : SupabaseIntegrationTest() {

private lateinit var testPrefix: String
private var propertyId: PropertyId? = null
private var testUserId: UserId? = null
private var orgId: OrganizationId? = null

@BeforeTest
fun setup() {
testPrefix = UUID.random()
runBlocking {
testUserId = createTestUser("user-${testPrefix}@test.com")
orgId = createTestOrganization("org_$testPrefix", "")
propertyId = createTestProperty("${testPrefix}_Property", testUserId!!, orgId!!)
}
}

@Test
fun `createCommonArea should return created common area`() = runCoroutineTest {
// Arrange

// Act
val result = commonAreaDatastore.createCommonArea(
propertyId = propertyId!!,
name = "${testPrefix}_Lobby",
type = CommonAreaType.LOBBY,
description = "Main entrance",
).registerCommonAreaForDeletion()

// Assert
assertTrue(result.isSuccess)
val area = result.getOrNull()
assertNotNull(area)
assertEquals("${testPrefix}_Lobby", area.name)
assertEquals(CommonAreaType.LOBBY, area.type)
assertEquals("Main entrance", area.description)
assertEquals(propertyId, area.propertyId)
}

@Test
fun `getCommonArea should return created common area`() = runCoroutineTest {
// Arrange
val createResult = commonAreaDatastore.createCommonArea(
propertyId = propertyId!!,
name = "${testPrefix}_GetLobby",
type = CommonAreaType.LOBBY,
description = null,
).registerCommonAreaForDeletion()
assertTrue(createResult.isSuccess)
val created = createResult.getOrNull()!!

// Act
val getResult = commonAreaDatastore.getCommonArea(created.id)

// Assert
assertTrue(getResult.isSuccess)
val fetched = getResult.getOrNull()
assertNotNull(fetched)
assertEquals("${testPrefix}_GetLobby", fetched.name)
assertEquals(created.id, fetched.id)
}

@Test
fun `getCommonArea should return null when not found`() = runCoroutineTest {
// Arrange
val fakeId = CommonAreaId(UUID.random())

// Act
val result = commonAreaDatastore.getCommonArea(fakeId)

// Assert
assertTrue(result.isSuccess)
assertNull(result.getOrNull())
}

@Test
fun `getCommonAreasForProperty should return all areas for property`() = runCoroutineTest {
// Arrange

// Act
val result1 = commonAreaDatastore.createCommonArea(
propertyId = propertyId!!,
name = "${testPrefix}_Pool",
type = CommonAreaType.POOL,
description = null,
).registerCommonAreaForDeletion()
val result2 = commonAreaDatastore.createCommonArea(
propertyId = propertyId!!,
name = "${testPrefix}_Gym",
type = CommonAreaType.GYM,
description = null,
).registerCommonAreaForDeletion()
assertTrue(result1.isSuccess)
assertTrue(result2.isSuccess)
val listResult = commonAreaDatastore.getCommonAreasForProperty(propertyId!!)

// Assert
assertTrue(listResult.isSuccess)
val areas = listResult.getOrNull()
assertNotNull(areas)
val names = areas.map { it.name }
assertTrue(names.contains("${testPrefix}_Pool"))
assertTrue(names.contains("${testPrefix}_Gym"))
}

@Test
fun `getCommonAreasForProperty should not return deleted areas`() = runCoroutineTest {
// Arrange
val createResult = commonAreaDatastore.createCommonArea(
propertyId = propertyId!!,
name = "${testPrefix}_Rooftop",
type = CommonAreaType.ROOFTOP,
description = null,
).registerCommonAreaForDeletion()
assertTrue(createResult.isSuccess)
val area = createResult.getOrNull()!!
val deleteResult = commonAreaDatastore.deleteCommonArea(area.id)
assertTrue(deleteResult.isSuccess)

// Act
val listResult = commonAreaDatastore.getCommonAreasForProperty(propertyId!!)

// Assert
assertTrue(listResult.isSuccess)
val names = listResult.getOrNull()!!.map { it.name }
assertTrue(!names.contains("${testPrefix}_Rooftop"))
}

@Test
fun `updateCommonArea should update name and type`() = runCoroutineTest {
// Arrange
val createResult = commonAreaDatastore.createCommonArea(
propertyId = propertyId!!,
name = "${testPrefix}_OldName",
type = CommonAreaType.LOBBY,
description = "Original description",
).registerCommonAreaForDeletion()
assertTrue(createResult.isSuccess)
val area = createResult.getOrNull()!!

// Act
val updateResult = commonAreaDatastore.updateCommonArea(
commonAreaId = area.id,
name = "${testPrefix}_NewName",
type = CommonAreaType.GYM,
description = null,
)

// Assert
assertTrue(updateResult.isSuccess)
val updated = updateResult.getOrNull()
assertNotNull(updated)
assertEquals("${testPrefix}_NewName", updated.name)
assertEquals(CommonAreaType.GYM, updated.type)
}

@Test
fun `deleteCommonArea should soft delete and make area invisible`() = runCoroutineTest {
// Arrange
val createResult = commonAreaDatastore.createCommonArea(
propertyId = propertyId!!,
name = "${testPrefix}_ToDelete",
type = CommonAreaType.PARKING,
description = null,
).registerCommonAreaForDeletion()
assertTrue(createResult.isSuccess)
val area = createResult.getOrNull()!!

// Act
val deleteResult = commonAreaDatastore.deleteCommonArea(area.id)

// Assert
assertTrue(deleteResult.isSuccess)
assertTrue(deleteResult.getOrNull() == true)
val getResult = commonAreaDatastore.getCommonArea(area.id)
assertTrue(getResult.isSuccess)
assertNull(getResult.getOrNull())
}

@Test
fun `purgeCommonArea should hard delete the area`() = runCoroutineTest {
// Arrange
val createResult = commonAreaDatastore.createCommonArea(
propertyId = propertyId!!,
name = "${testPrefix}_ToPurge",
type = CommonAreaType.LAUNDRY,
description = null,
)
assertTrue(createResult.isSuccess)
val area = createResult.getOrNull()!!

// Act
val purgeResult = commonAreaDatastore.purgeCommonArea(area.id)

// Assert
assertTrue(purgeResult.isSuccess)
val getResult = commonAreaDatastore.getCommonArea(area.id)
assertTrue(getResult.isSuccess)
assertNull(getResult.getOrNull())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.cramsan.edifikana.server.datastore.supabase

import com.cramsan.architecture.server.test.dependencyinjection.TestArchitectureModule
import com.cramsan.architecture.server.test.dependencyinjection.integTestFrameworkModule
import com.cramsan.edifikana.lib.model.CommonAreaId
import com.cramsan.edifikana.lib.model.EmployeeId
import com.cramsan.edifikana.lib.model.EmployeeRole
import com.cramsan.edifikana.lib.model.EventLogEntryId
Expand All @@ -15,6 +16,7 @@ import com.cramsan.edifikana.lib.model.UserId
import com.cramsan.edifikana.lib.utils.requireSuccess
import com.cramsan.edifikana.server.dependencyinjection.DatastoreModule
import com.cramsan.edifikana.server.dependencyinjection.IntegTestApplicationModule
import com.cramsan.edifikana.server.service.models.CommonArea
import com.cramsan.edifikana.server.service.models.Employee
import com.cramsan.edifikana.server.service.models.EventLogEntry
import com.cramsan.edifikana.server.service.models.Invite
Expand Down Expand Up @@ -52,8 +54,10 @@ abstract class SupabaseIntegrationTest : CoroutineTest(), KoinTest {
protected val organizationDatastore: SupabaseOrganizationDatastore by inject()
protected val notificationDatastore: SupabaseNotificationDatastore by inject()
protected val membershipDatastore: SupabaseMembershipDatastore by inject()
protected val commonAreaDatastore: SupabaseCommonAreaDatastore by inject()

private val eventLogResources = mutableSetOf<EventLogEntryId>()
private val commonAreaResources = mutableSetOf<CommonAreaId>()
private val propertyResources = mutableSetOf<PropertyId>()
private val employeeResources = mutableSetOf<EmployeeId>()
private val timeCardResources = mutableSetOf<TimeCardEventId>()
Expand Down Expand Up @@ -108,6 +112,10 @@ abstract class SupabaseIntegrationTest : CoroutineTest(), KoinTest {
notificationResources.add(notificationId)
}

private fun registerCommonAreaForDeletion(commonAreaId: CommonAreaId) {
commonAreaResources.add(commonAreaId)
}

protected fun createTestUser(email: String): UserId {
val userId = runBlocking {
userDatastore.createUser(
Expand Down Expand Up @@ -217,6 +225,12 @@ abstract class SupabaseIntegrationTest : CoroutineTest(), KoinTest {
}
}

fun Result<CommonArea>.registerCommonAreaForDeletion(): Result<CommonArea> {
return this.onSuccess { commonArea ->
registerCommonAreaForDeletion(commonArea.id)
}
}

fun registerSupabaseUserForDeletion(userId: String) {
supabaseUsers.add(userId)
}
Expand Down Expand Up @@ -271,6 +285,10 @@ abstract class SupabaseIntegrationTest : CoroutineTest(), KoinTest {
results += employeeDatastore.deleteEmployee(it)
results += employeeDatastore.purgeEmployee(it)
}
commonAreaResources.forEach {
results += commonAreaDatastore.deleteCommonArea(it)
results += commonAreaDatastore.purgeCommonArea(it)
}
propertyResources.forEach {
results += propertyDatastore.deleteProperty(it)
results += propertyDatastore.purgeProperty(it)
Expand All @@ -293,6 +311,7 @@ abstract class SupabaseIntegrationTest : CoroutineTest(), KoinTest {
eventLogResources.clear()
timeCardResources.clear()
employeeResources.clear()
commonAreaResources.clear()
propertyResources.clear()
userResources.clear()
organizationResources.clear()
Expand Down
Loading
Loading