diff --git a/edifikana/api/src/commonMain/kotlin/com/cramsan/edifikana/api/EmployeeApi.kt b/edifikana/api/src/commonMain/kotlin/com/cramsan/edifikana/api/EmployeeApi.kt index 1725d2fac..ce603cc19 100644 --- a/edifikana/api/src/commonMain/kotlin/com/cramsan/edifikana/api/EmployeeApi.kt +++ b/edifikana/api/src/commonMain/kotlin/com/cramsan/edifikana/api/EmployeeApi.kt @@ -4,6 +4,7 @@ import com.cramsan.edifikana.lib.model.employee.EmployeeId import com.cramsan.edifikana.lib.model.network.employee.CreateEmployeeNetworkRequest import com.cramsan.edifikana.lib.model.network.employee.EmployeeListNetworkResponse import com.cramsan.edifikana.lib.model.network.employee.EmployeeNetworkResponse +import com.cramsan.edifikana.lib.model.network.employee.GetEmployeesForPropertyQueryParams import com.cramsan.edifikana.lib.model.network.employee.UpdateEmployeeNetworkRequest import com.cramsan.framework.annotations.api.NoPathParam import com.cramsan.framework.annotations.api.NoQueryParam @@ -64,6 +65,22 @@ object EmployeeApi : Api("employee") { responses = UniversalResponsesOnly, ) + val getEmployeesForProperty = + operation< + NoRequestBody, + GetEmployeesForPropertyQueryParams, + NoPathParam, + EmployeeListNetworkResponse, + >( + method = HttpMethod.Get, + path = "by-property", + summary = "List employees for a property", + description = + "Returns all employees (manager/security/cleaning) assigned to the given property. " + + "Requires the MANAGER role or higher on the property.", + responses = UniversalResponsesOnly, + ) + val updateEmployee = operation< UpdateEmployeeNetworkRequest, diff --git a/edifikana/api/src/commonMain/kotlin/com/cramsan/edifikana/api/PropertyApi.kt b/edifikana/api/src/commonMain/kotlin/com/cramsan/edifikana/api/PropertyApi.kt index bb45a4144..655d53dc2 100644 --- a/edifikana/api/src/commonMain/kotlin/com/cramsan/edifikana/api/PropertyApi.kt +++ b/edifikana/api/src/commonMain/kotlin/com/cramsan/edifikana/api/PropertyApi.kt @@ -1,6 +1,7 @@ package com.cramsan.edifikana.api import com.cramsan.edifikana.lib.model.network.property.CreatePropertyNetworkRequest +import com.cramsan.edifikana.lib.model.network.property.GetPropertiesQueryParams import com.cramsan.edifikana.lib.model.network.property.PropertyListNetworkResponse import com.cramsan.edifikana.lib.model.network.property.PropertyNetworkResponse import com.cramsan.edifikana.lib.model.network.property.UpdatePropertyNetworkRequest @@ -49,16 +50,18 @@ object PropertyApi : Api("property") { }, ) - val getAssignedProperties = + val getProperties = operation< NoRequestBody, - NoQueryParam, + GetPropertiesQueryParams, NoPathParam, PropertyListNetworkResponse, >( method = HttpMethod.Get, - summary = "List assigned properties", - description = "Returns all properties the authenticated user has been assigned access to.", + summary = "List properties", + description = + "Returns all properties in the organization given by the query params. " + + "Requires membership in the organization.", responses = UniversalResponsesOnly, ) diff --git a/edifikana/back-end/docs/openapi.yaml b/edifikana/back-end/docs/openapi.yaml index 78c08f623..70df66f0a 100644 --- a/edifikana/back-end/docs/openapi.yaml +++ b/edifikana/back-end/docs/openapi.yaml @@ -2837,6 +2837,54 @@ ] } }, + "/employee/by-property": { + "get": { + "operationId": "get-employee-by-property", + "tags": [ + "employee" + ], + "summary": "List employees for a property", + "description": "Returns all employees (manager/security/cleaning) assigned to the given property. Requires the MANAGER role or higher on the property.", + "parameters": [ + { + "name": "property_id", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmployeeListNetworkResponse" + } + } + } + }, + "400": { + "description": "The request was malformed or failed validation." + }, + "401": { + "description": "Authentication is required or has failed." + }, + "500": { + "description": "An unexpected server error occurred." + } + }, + "security": [ + { + "bearerAuth": [ + + ] + } + ] + } + }, "/property": { "post": { "operationId": "post-property", @@ -2889,8 +2937,18 @@ "tags": [ "property" ], - "summary": "List assigned properties", - "description": "Returns all properties the authenticated user has been assigned access to.", + "summary": "List properties", + "description": "Returns all properties in the organization given by the query params. Requires membership in the organization.", + "parameters": [ + { + "name": "organization_id", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "Successful response.", diff --git a/edifikana/back-end/src/integTest/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabaseEmployeeDatastoreIntegrationTest.kt b/edifikana/back-end/src/integTest/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabaseEmployeeDatastoreIntegrationTest.kt index 731493793..2752b26ba 100644 --- a/edifikana/back-end/src/integTest/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabaseEmployeeDatastoreIntegrationTest.kt +++ b/edifikana/back-end/src/integTest/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabaseEmployeeDatastoreIntegrationTest.kt @@ -104,6 +104,48 @@ class SupabaseEmployeeDatastoreIntegrationTest : SupabaseIntegrationTest() { assertTrue(firstNames.contains("${test_prefix}_EmployeeB")) } + @Test + fun `getEmployeesForProperty should return only employees for that property`() = runBlocking { + // Arrange + val otherPropertyId = createTestProperty("${test_prefix}_OtherProperty", testUserId!!, orgId!!) + + // Act + val result1 = employeeDatastore.createEmployee( + firstName = "${test_prefix}_EmployeeManager", + lastName = "${test_prefix}_LastA", + role = EmployeeRole.MANAGER, + idType = IdType.DNI, + propertyId = propertyId!!, + ).registerEmployeeForDeletion() + val result2 = employeeDatastore.createEmployee( + firstName = "${test_prefix}_EmployeeCleaning", + lastName = "${test_prefix}_LastB", + role = EmployeeRole.CLEANING, + idType = IdType.PASSPORT, + propertyId = propertyId!!, + ).registerEmployeeForDeletion() + val otherPropertyResult = employeeDatastore.createEmployee( + firstName = "${test_prefix}_EmployeeOtherProperty", + lastName = "${test_prefix}_LastC", + role = EmployeeRole.SECURITY, + idType = IdType.DNI, + propertyId = otherPropertyId, + ).registerEmployeeForDeletion() + assertTrue(result1.isSuccess) + assertTrue(result2.isSuccess) + assertTrue(otherPropertyResult.isSuccess) + val getResult = employeeDatastore.getEmployeesForProperty(propertyId = propertyId!!) + + // Assert + assertTrue(getResult.isSuccess) + val employees = getResult.getOrNull() + assertNotNull(employees) + val firstNames = employees.map { it.firstName } + assertTrue(firstNames.contains("${test_prefix}_EmployeeManager")) + assertTrue(firstNames.contains("${test_prefix}_EmployeeCleaning")) + assertTrue(firstNames.none { it == "${test_prefix}_EmployeeOtherProperty" }) + } + @Test fun `updateEmployee should update employee fields`() = runBlocking { // Arrange diff --git a/edifikana/back-end/src/integTest/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabasePropertyDatastoreIntegrationTest.kt b/edifikana/back-end/src/integTest/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabasePropertyDatastoreIntegrationTest.kt index 613fb46b9..98b0ccd77 100644 --- a/edifikana/back-end/src/integTest/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabasePropertyDatastoreIntegrationTest.kt +++ b/edifikana/back-end/src/integTest/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabasePropertyDatastoreIntegrationTest.kt @@ -81,8 +81,10 @@ class SupabasePropertyDatastoreIntegrationTest : SupabaseIntegrationTest() { } @Test - fun `getProperties should return all properties for user`() = runBlocking { + fun `getProperties should return all properties for the organization, not just the caller's own`() = runBlocking { // Arrange + val otherUserId = createTestUser("other-user-${test_prefix}@example.com") + val otherOrg = createTestOrganization("other-org-${test_prefix}", "") // Act val result1 = propertyDatastore.createProperty( @@ -92,24 +94,36 @@ class SupabasePropertyDatastoreIntegrationTest : SupabaseIntegrationTest() { organizationId = testOrg!!, imageUrl = "drawable:L_DEPA", ).registerPropertyForDeletion() + // Created by a different user, same org — proves this is org-scoped, not + // user-assignment-scoped like the old getAssignedProperties behavior. val result2 = propertyDatastore.createProperty( name = "${test_prefix}_PropertyB", address = "101 Sample Blvd, Testville, TV 13141", - creatorUserId = testUserId!!, + creatorUserId = otherUserId, organizationId = testOrg!!, imageUrl = "drawable:M_DEPA", ).registerPropertyForDeletion() + // Different org entirely — must not show up in testOrg's results. + val otherOrgResult = propertyDatastore.createProperty( + name = "${test_prefix}_PropertyOtherOrg", + address = "1 Other Org Way, Elsewhere, EO 99999", + creatorUserId = otherUserId, + organizationId = otherOrg, + imageUrl = "drawable:S_DEPA", + ).registerPropertyForDeletion() assertTrue(result1.isSuccess) assertTrue(result2.isSuccess) - val getAllResult = propertyDatastore.getProperties(userId = testUserId!!) + assertTrue(otherOrgResult.isSuccess) + val getAllResult = propertyDatastore.getProperties(organizationId = testOrg!!) // Assert assertTrue(getAllResult.isSuccess) val properties = getAllResult.getOrNull() assertNotNull(properties) val names = properties.map { it.name } - assertTrue(names.contains( "${test_prefix}_PropertyA")) - assertTrue(names.contains( "${test_prefix}_PropertyB")) + assertTrue(names.contains("${test_prefix}_PropertyA")) + assertTrue(names.contains("${test_prefix}_PropertyB")) + assertTrue(names.none { it == "${test_prefix}_PropertyOtherOrg" }) } @Test diff --git a/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/controller/EmployeeController.kt b/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/controller/EmployeeController.kt index 1d5189756..8abd41069 100644 --- a/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/controller/EmployeeController.kt +++ b/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/controller/EmployeeController.kt @@ -5,6 +5,7 @@ import com.cramsan.edifikana.lib.model.employee.EmployeeId import com.cramsan.edifikana.lib.model.network.employee.CreateEmployeeNetworkRequest import com.cramsan.edifikana.lib.model.network.employee.EmployeeListNetworkResponse import com.cramsan.edifikana.lib.model.network.employee.EmployeeNetworkResponse +import com.cramsan.edifikana.lib.model.network.employee.GetEmployeesForPropertyQueryParams import com.cramsan.edifikana.lib.model.network.employee.UpdateEmployeeNetworkRequest import com.cramsan.edifikana.server.controller.authentication.SupabaseContextPayload import com.cramsan.edifikana.server.service.EmployeeService @@ -108,6 +109,29 @@ class EmployeeController(private val employeeService: EmployeeService, private v return EmployeeListNetworkResponse(employees) } + /** + * Retrieves all employees assigned to a property. + * Returns a list of employees as a network response. + * Throws [UnauthorizedException] if the user does not have MANAGER role or higher for the property. + */ + + suspend fun getEmployeesForProperty( + request: OperationRequest< + NoRequestBody, + GetEmployeesForPropertyQueryParams, + NoPathParam, + ClientContext.AuthenticatedClientContext, + >, + ): EmployeeListNetworkResponse { + val propertyId = request.queryParam.propertyId + if (!rbacService.hasRoleOrHigher(request.context, propertyId, UserRole.MANAGER)) { + throw UnauthorizedException(unauthorizedMsg) + } + val employees = + employeeService.getEmployeesForProperty(propertyId).map { it.toEmployeeNetworkResponse() } + return EmployeeListNetworkResponse(employees) + } + /** * Updates an employee identified by [employeeId] with the provided request data. * Returns the updated employee as a network response. @@ -195,6 +219,9 @@ class EmployeeController(private val employeeService: EmployeeService, private v handler(api.getEmployees) { request -> getEmployees(request) } + handler(api.getEmployeesForProperty) { request -> + getEmployeesForProperty(request) + } handler(api.updateEmployee) { request -> updateEmployee(request) } diff --git a/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/controller/PropertyController.kt b/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/controller/PropertyController.kt index d0039f726..5dea00e10 100644 --- a/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/controller/PropertyController.kt +++ b/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/controller/PropertyController.kt @@ -2,6 +2,7 @@ package com.cramsan.edifikana.server.controller import com.cramsan.edifikana.api.PropertyApi import com.cramsan.edifikana.lib.model.network.property.CreatePropertyNetworkRequest +import com.cramsan.edifikana.lib.model.network.property.GetPropertiesQueryParams import com.cramsan.edifikana.lib.model.network.property.PropertyListNetworkResponse import com.cramsan.edifikana.lib.model.network.property.PropertyNetworkResponse import com.cramsan.edifikana.lib.model.network.property.UpdatePropertyNetworkRequest @@ -80,20 +81,24 @@ class PropertyController(private val propertyService: PropertyService, private v } /** - * Retrieves the list of properties assigned to the authenticated user. + * Retrieves the list of properties belonging to an organization. * Returns a list of properties as a network response. + * Throws [UnauthorizedException] if the user does not have EMPLOYEE role or higher in the organization. */ - suspend fun getAssignedProperties( + suspend fun getProperties( request: OperationRequest< NoRequestBody, - NoQueryParam, + GetPropertiesQueryParams, NoPathParam, ClientContext.AuthenticatedClientContext, >, ): PropertyListNetworkResponse { - val userId = request.context.payload.userId - val properties = propertyService.getProperties(userId = userId).map { it.toPropertyNetworkResponse() } + val organizationId = request.queryParam.organizationId + if (!rbacService.hasRoleOrHigher(request.context, organizationId, UserRole.EMPLOYEE)) { + throw UnauthorizedException(unauthorizedMsg) + } + val properties = propertyService.getProperties(organizationId).map { it.toPropertyNetworkResponse() } return PropertyListNetworkResponse(properties) } @@ -167,8 +172,8 @@ class PropertyController(private val propertyService: PropertyService, private v handler(api.getProperty) { request -> getProperty(request) } - handler(api.getAssignedProperties) { request -> - getAssignedProperties(request) + handler(api.getProperties) { request -> + getProperties(request) } handler(api.updateProperty) { request -> updateProperty(request) diff --git a/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/EmployeeDatastore.kt b/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/EmployeeDatastore.kt index b0a75da85..f017105ef 100644 --- a/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/EmployeeDatastore.kt +++ b/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/EmployeeDatastore.kt @@ -38,6 +38,14 @@ interface EmployeeDatastore { currentUser: UserId, ): Result> + /** + * Retrieves all employee members assigned to a property. Returns the [Result] of the operation with a list of + * [Employee]. + */ + suspend fun getEmployeesForProperty( + propertyId: PropertyId, + ): Result> + /** * Updates an employee member with the given details. Returns the [Result] of the operation with the updated [Employee]. */ diff --git a/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/PropertyDatastore.kt b/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/PropertyDatastore.kt index 0b922142b..daada37f8 100644 --- a/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/PropertyDatastore.kt +++ b/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/PropertyDatastore.kt @@ -30,10 +30,10 @@ interface PropertyDatastore { ): Result /** - * Retrieves all properties for a user. Returns the [Result] of the operation with a list of [Property]. + * Retrieves all properties for an organization. Returns the [Result] of the operation with a list of [Property]. */ suspend fun getProperties( - userId: UserId, + organizationId: OrganizationId, ): Result> /** diff --git a/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabaseEmployeeDatastore.kt b/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabaseEmployeeDatastore.kt index e9e88ae41..fee67f341 100644 --- a/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabaseEmployeeDatastore.kt +++ b/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabaseEmployeeDatastore.kt @@ -93,6 +93,25 @@ class SupabaseEmployeeDatastore(private val postgrest: Postgrest, private val cl .map { it.toEmployee() } } + /** + * Gets all non-deleted employees assigned to the given property. + */ + + override suspend fun getEmployeesForProperty(propertyId: PropertyId): Result> = + runSuspendCatching(TAG) { + logD(TAG, "Getting all employees for property: %s", propertyId) + + postgrest + .from(EmployeeEntity.COLLECTION) + .select { + filter { + EmployeeEntity::propertyId eq propertyId.propertyId + EmployeeEntity::deletedAt isExact null + } + }.decodeList() + .map { it.toEmployee() } + } + /** * Updates an employee's properties. Only non-null parameters are updated. */ diff --git a/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabasePropertyDatastore.kt b/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabasePropertyDatastore.kt index 49ad9d50a..5e97c087a 100644 --- a/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabasePropertyDatastore.kt +++ b/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabasePropertyDatastore.kt @@ -7,7 +7,6 @@ import com.cramsan.edifikana.lib.model.user.UserId import com.cramsan.edifikana.server.datastore.PropertyDatastore import com.cramsan.edifikana.server.datastore.supabase.models.PropertyEntity import com.cramsan.edifikana.server.datastore.supabase.models.UserPropertyMappingEntity -import com.cramsan.edifikana.server.datastore.supabase.models.UserPropertyViewEntity import com.cramsan.edifikana.server.service.models.Property import com.cramsan.framework.annotations.BackendDatastore import com.cramsan.framework.core.runSuspendCatching @@ -92,22 +91,24 @@ class SupabasePropertyDatastore(private val postgrest: Postgrest, private val cl } /** - * Gets all properties accessible to the given user. - * Uses the v_user_properties view for single-query retrieval (eliminates N+1 pattern). + * Gets all non-deleted properties belonging to the given organization. */ override suspend fun getProperties( - userId: UserId, + organizationId: OrganizationId, ): Result> = runSuspendCatching(TAG) { - logD(TAG, "Getting all properties for user: %s", userId) + logD(TAG, "Getting all properties for organization: %s", organizationId) - // Use the v_user_properties view for single-query retrieval postgrest - .from(VIEW_USER_PROPERTIES) + .from(PropertyEntity.COLLECTION) .select { - filter { UserPropertyViewEntity::userId eq userId.userId } - }.decodeList() + filter { + PropertyEntity::organizationId eq organizationId.id + PropertyEntity::deletedAt isExact null + } + order("name", order = io.github.jan.supabase.postgrest.query.Order.ASCENDING) + }.decodeList() .map { it.toProperty() } } @@ -202,6 +203,5 @@ class SupabasePropertyDatastore(private val postgrest: Postgrest, private val cl companion object { const val TAG = "SupabasePropertyDatastore" - const val VIEW_USER_PROPERTIES = "v_user_properties" } } diff --git a/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/supabase/models/UserPropertyViewEntity.kt b/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/supabase/models/UserPropertyViewEntity.kt deleted file mode 100644 index a2d7559d4..000000000 --- a/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/supabase/models/UserPropertyViewEntity.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.cramsan.edifikana.server.datastore.supabase.models - -import com.cramsan.edifikana.lib.model.common.Url -import com.cramsan.edifikana.lib.model.organization.OrganizationId -import com.cramsan.edifikana.lib.model.property.PropertyId -import com.cramsan.edifikana.lib.model.user.UserId -import com.cramsan.edifikana.server.service.models.Property -import com.cramsan.framework.annotations.DatabaseModel -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** - * Entity for the v_user_properties view. - * Contains property data plus the user_id from the mapping. - */ -@Serializable -@DatabaseModel -data class UserPropertyViewEntity( - val id: PropertyId, - val name: String, - val address: String, - @SerialName("organization_id") - val organizationId: OrganizationId, - @SerialName("image_url") - val imageUrl: Url? = null, - @SerialName("user_id") - val userId: UserId, -) { - /** - * Converts this view entity to a Property domain model. - */ - fun toProperty(): Property { - return Property( - id = id, - name = name, - address = address, - organizationId = organizationId, - imageUrl = imageUrl?.url, - ) - } -} diff --git a/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/service/EmployeeService.kt b/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/service/EmployeeService.kt index ac1bdd7b5..d7af0427c 100644 --- a/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/service/EmployeeService.kt +++ b/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/service/EmployeeService.kt @@ -69,6 +69,19 @@ class EmployeeService(private val employeeDatastore: EmployeeDatastore, private return employees } + /** + * Retrieves all employees assigned to the given property. + */ + suspend fun getEmployeesForProperty( + propertyId: PropertyId, + ): List { + logD(TAG, "getEmployeesForProperty") + return employeeDatastore + .getEmployeesForProperty( + propertyId = propertyId, + ).getOrThrow() + } + /** * Updates an employee with the provided [id] and [name]. */ diff --git a/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/service/PropertyService.kt b/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/service/PropertyService.kt index be4f89024..4d5a2baf6 100644 --- a/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/service/PropertyService.kt +++ b/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/service/PropertyService.kt @@ -2,7 +2,6 @@ package com.cramsan.edifikana.server.service import com.cramsan.edifikana.lib.model.organization.OrganizationId import com.cramsan.edifikana.lib.model.property.PropertyId -import com.cramsan.edifikana.lib.model.user.UserId import com.cramsan.edifikana.server.controller.authentication.SupabaseContextPayload import com.cramsan.edifikana.server.datastore.PropertyDatastore import com.cramsan.edifikana.server.service.models.Property @@ -53,16 +52,16 @@ class PropertyService(private val propertyDatastore: PropertyDatastore) { } /** - * Retrieves all properties. + * Retrieves all properties belonging to the given organization. */ suspend fun getProperties( - userId: UserId, + organizationId: OrganizationId, ): List { logD(TAG, "getProperties") val properties = propertyDatastore .getProperties( - userId, + organizationId, ).getOrThrow() return properties } diff --git a/edifikana/back-end/src/test/kotlin/com/cramsan/edifikana/server/controller/EmployeeControllerTest.kt b/edifikana/back-end/src/test/kotlin/com/cramsan/edifikana/server/controller/EmployeeControllerTest.kt index 37a6b815c..09ee476c0 100644 --- a/edifikana/back-end/src/test/kotlin/com/cramsan/edifikana/server/controller/EmployeeControllerTest.kt +++ b/edifikana/back-end/src/test/kotlin/com/cramsan/edifikana/server/controller/EmployeeControllerTest.kt @@ -256,6 +256,83 @@ class EmployeeControllerTest : assertEquals(expectedResponse, response.bodyAsText()) } + @Test + fun `test getEmployeesForProperty succeeds when user has required role or higher`() = + testBackEndApplication { client -> + // Arrange + val expectedResponse = readFileContent("requests/get_employees_for_property_response.json") + val employeeService = get() + val rbacService = get() + val propId = PropertyId("property123") + coEvery { + employeeService.getEmployeesForProperty(propId) + }.answers { + listOf( + Employee( + id = EmployeeId("emp123"), + firstName = "John", + lastName = "Doe", + idType = IdType.DNI, + role = EmployeeRole.MANAGER, + propertyId = propId, + ), + Employee( + id = EmployeeId("emp456"), + firstName = "Jane", + lastName = "Smith", + idType = IdType.PASSPORT, + role = EmployeeRole.CLEANING, + propertyId = propId, + ), + ) + } + val contextRetriever = get>() + val context = + ClientContext.AuthenticatedClientContext( + SupabaseContextPayload( + userInfo = mockk(), + userId = UserId("user123"), + ), + ) + coEvery { contextRetriever.getContext(any()) } returns context + coEvery { rbacService.hasRoleOrHigher(context, propId, UserRole.MANAGER) } returns true + + // Act + val response = client.get("employee/by-property?property_id=property123") + + // Assert + assertEquals(HttpStatusCode.OK, response.status) + assertEquals(expectedResponse, response.bodyAsText()) + } + + @Test + fun `test getEmployeesForProperty fails when user doesn't have required role or higher`() = + testBackEndApplication { client -> + // Arrange + val expectedResponse = "You are not authorized to perform this action in your organization." + val employeeService = get() + val rbacService = get() + val propId = PropertyId("property123") + val contextRetriever = get>() + val context = + ClientContext.AuthenticatedClientContext( + SupabaseContextPayload( + userInfo = mockk(), + userId = UserId("user123"), + ), + ) + coEvery { contextRetriever.getContext(any()) } returns context + coEvery { rbacService.hasRoleOrHigher(context, propId, UserRole.MANAGER) } returns false + + // Act + val response = client.get("employee/by-property?property_id=property123") + + // Assert + coVerify { employeeService wasNot Called } + assertEquals(HttpStatusCode.Unauthorized, response.status) + assertEquals(expectedResponse, response.bodyAsText()) + } + @Test fun `test updateEmployee succeeds when user has required role`() = testBackEndApplication { client -> diff --git a/edifikana/back-end/src/test/kotlin/com/cramsan/edifikana/server/controller/PropertyControllerTest.kt b/edifikana/back-end/src/test/kotlin/com/cramsan/edifikana/server/controller/PropertyControllerTest.kt index bab030f0b..aafa85fec 100644 --- a/edifikana/back-end/src/test/kotlin/com/cramsan/edifikana/server/controller/PropertyControllerTest.kt +++ b/edifikana/back-end/src/test/kotlin/com/cramsan/edifikana/server/controller/PropertyControllerTest.kt @@ -240,15 +240,16 @@ class PropertyControllerTest : assertEquals(expectedResponse, response.bodyAsText()) } - // TODO: Update this test and add a negative check test for ensuring user get only list of properties they're assigned @Test - fun `test getProperties`() = + fun `test getProperties succeeds when user has required role in organization`() = testBackEndApplication { client -> // Arrange val expectedResponse = readFileContent("requests/get_properties_response.json") val propertyService = get() + val rbacService = get() + val orgId = OrganizationId("org123") coEvery { - propertyService.getProperties(UserId("user123")) + propertyService.getProperties(orgId) }.answers { listOf( Property( @@ -268,22 +269,65 @@ class PropertyControllerTest : ) } val contextRetriever = get>() + val context = + ClientContext.AuthenticatedClientContext( + SupabaseContextPayload( + userInfo = mockk(), + userId = UserId("user123"), + ), + ) coEvery { contextRetriever.getContext(any()) }.answers { + context + } + coEvery { + rbacService.hasRoleOrHigher(context, orgId, UserRole.EMPLOYEE) + }.answers { + true + } + + // Act + val response = client.get("property?organization_id=org123") + + // Assert + assertEquals(HttpStatusCode.OK, response.status) + assertEquals(expectedResponse, response.bodyAsText()) + } + + @Test + fun `test getProperties fails when user doesn't have required role in organization`() = + testBackEndApplication { client -> + // Arrange + val expectedResponse = "You are not authorized to perform this action in your organization." + val propertyService = get() + val rbacService = get() + val orgId = OrganizationId("org123") + val contextRetriever = get>() + val context = ClientContext.AuthenticatedClientContext( SupabaseContextPayload( userInfo = mockk(), userId = UserId("user123"), ), ) + coEvery { + contextRetriever.getContext(any()) + }.answers { + context + } + coEvery { + rbacService.hasRoleOrHigher(context, orgId, UserRole.EMPLOYEE) + }.answers { + false } // Act - val response = client.get("property") + val response = client.get("property?organization_id=org123") // Assert - assertEquals(HttpStatusCode.OK, response.status) + coVerify { propertyService wasNot Called } + assertEquals(HttpStatusCode.Unauthorized, response.status) assertEquals(expectedResponse, response.bodyAsText()) } diff --git a/edifikana/back-end/src/test/kotlin/com/cramsan/edifikana/server/service/EmployeeServiceTest.kt b/edifikana/back-end/src/test/kotlin/com/cramsan/edifikana/server/service/EmployeeServiceTest.kt index 1fd02cddb..e1058e22b 100644 --- a/edifikana/back-end/src/test/kotlin/com/cramsan/edifikana/server/service/EmployeeServiceTest.kt +++ b/edifikana/back-end/src/test/kotlin/com/cramsan/edifikana/server/service/EmployeeServiceTest.kt @@ -153,6 +153,25 @@ class EmployeeServiceTest { coVerify { employeeDatastore.getEmployees(request) } } + /** + * Tests that getEmployeesForProperty retrieves all employees for a property and returns a list. + */ + @Test + fun `getEmployeesForProperty should call employeeDatastore and return list`() = + runTest { + // Arrange + val employeeLists = listOf(mockk(), mockk()) + val propertyId = PropertyId("property-1") + coEvery { employeeDatastore.getEmployeesForProperty(propertyId) } returns Result.success(employeeLists) + + // Act + val result = employeeService.getEmployeesForProperty(propertyId) + + // Assert + assertEquals(employeeLists, result) + coVerify { employeeDatastore.getEmployeesForProperty(propertyId) } + } + /** * Tests that updateEmployee updates an employee and returns the updated employee. */ diff --git a/edifikana/back-end/src/test/kotlin/com/cramsan/edifikana/server/service/PropertyServiceTest.kt b/edifikana/back-end/src/test/kotlin/com/cramsan/edifikana/server/service/PropertyServiceTest.kt index 674036b9b..d9bef8a31 100644 --- a/edifikana/back-end/src/test/kotlin/com/cramsan/edifikana/server/service/PropertyServiceTest.kt +++ b/edifikana/back-end/src/test/kotlin/com/cramsan/edifikana/server/service/PropertyServiceTest.kt @@ -125,24 +125,24 @@ class PropertyServiceTest { } /** - * Tests that getProperties retrieves all properties and returns a list.q + * Tests that getProperties retrieves all properties for an organization and returns a list. */ @Test fun `getProperties should call propertyDatastore and return list`() = runTest { // Arrange val propertyList = listOf(mockk(), mockk()) - coEvery { propertyDatastore.getProperties(UserId("TestId1")) } returns + coEvery { propertyDatastore.getProperties(OrganizationId("org123")) } returns Result.success( propertyList, ) // Act - val result = propertyService.getProperties(UserId("TestId1")) + val result = propertyService.getProperties(OrganizationId("org123")) // Assert assertEquals(propertyList, result) - coVerify { propertyDatastore.getProperties(UserId("TestId1")) } + coVerify { propertyDatastore.getProperties(OrganizationId("org123")) } } /** diff --git a/edifikana/back-end/src/test/resources/requests/get_employees_for_property_response.json b/edifikana/back-end/src/test/resources/requests/get_employees_for_property_response.json new file mode 100644 index 000000000..aa00005f5 --- /dev/null +++ b/edifikana/back-end/src/test/resources/requests/get_employees_for_property_response.json @@ -0,0 +1,20 @@ +{ + "content": [ + { + "id": "emp123", + "id_type": "DNI", + "first_name": "John", + "last_name": "Doe", + "role": "MANAGER", + "property_id": "property123" + }, + { + "id": "emp456", + "id_type": "PASSPORT", + "first_name": "Jane", + "last_name": "Smith", + "role": "CLEANING", + "property_id": "property123" + } + ] +} \ No newline at end of file diff --git a/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/organizationhome/OrganizationHomeScreen.kt b/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/organizationhome/OrganizationHomeScreen.kt index ef9865fdf..515e2f486 100644 --- a/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/organizationhome/OrganizationHomeScreen.kt +++ b/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/organizationhome/OrganizationHomeScreen.kt @@ -117,7 +117,9 @@ private fun HubContent( ) { when (it) { Tabs.Properties -> { - PropertiesOverviewScreen() + organizationId?.let { orgId -> + PropertiesOverviewScreen(organizationId = orgId) + } } Tabs.Employee -> { diff --git a/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/propertiesoverview/PropertiesOverviewScreen.kt b/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/propertiesoverview/PropertiesOverviewScreen.kt index 2c4681c43..9a71da575 100644 --- a/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/propertiesoverview/PropertiesOverviewScreen.kt +++ b/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/propertiesoverview/PropertiesOverviewScreen.kt @@ -30,6 +30,7 @@ import androidx.lifecycle.compose.LifecycleEventEffect import com.cramsan.edifikana.client.lib.features.home.shared.PropertyIconOptions import com.cramsan.edifikana.client.ui.components.EdifikanaImage import com.cramsan.edifikana.client.ui.components.ImageSource +import com.cramsan.edifikana.lib.model.organization.OrganizationId import com.cramsan.framework.core.compose.ui.ObserveViewModelEvents import com.cramsan.ui.components.LoadingAnimationOverlay import com.cramsan.ui.components.ScreenLayout @@ -50,6 +51,7 @@ import org.koin.compose.viewmodel.koinViewModel */ @Composable fun PropertiesOverviewScreen( + organizationId: OrganizationId, modifier: Modifier = Modifier, viewModel: PropertiesOverviewViewModel = koinViewModel(), ) { @@ -57,7 +59,7 @@ fun PropertiesOverviewScreen( // For other possible lifecycle events, see the [Lifecycle.Event] documentation. LifecycleEventEffect(Lifecycle.Event.ON_CREATE) { - viewModel.initialize() + viewModel.initialize(organizationId) } LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { // Call this feature's viewModel diff --git a/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/propertiesoverview/PropertiesOverviewViewModel.kt b/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/propertiesoverview/PropertiesOverviewViewModel.kt index 92cbdd166..983235d73 100644 --- a/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/propertiesoverview/PropertiesOverviewViewModel.kt +++ b/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/propertiesoverview/PropertiesOverviewViewModel.kt @@ -4,6 +4,7 @@ import com.cramsan.edifikana.client.lib.features.home.HomeDestination import com.cramsan.edifikana.client.lib.features.window.EdifikanaWindowsEvent import com.cramsan.edifikana.client.lib.managers.OrganizationManager import com.cramsan.edifikana.client.lib.managers.PropertyManager +import com.cramsan.edifikana.lib.model.organization.OrganizationId import com.cramsan.framework.annotations.FrontendViewModel import com.cramsan.framework.core.compose.BaseViewModel import com.cramsan.framework.core.compose.ViewModelDependencies @@ -25,13 +26,13 @@ class PropertiesOverviewViewModel( /** * Initialize the ViewModel. */ - fun initialize() { + fun initialize(organizationId: OrganizationId) { viewModelCoroutineScope.launch { updateUiState { it.copy(isLoading = true) } propertyManager - .getPropertyList() + .getPropertyList(organizationId) .onSuccess { resultList -> val uiModels = resultList.map { property -> diff --git a/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/propertydetail/PropertyDetailUIState.kt b/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/propertydetail/PropertyDetailUIState.kt index dc52ab7d0..0dba0b62f 100644 --- a/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/propertydetail/PropertyDetailUIState.kt +++ b/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/propertydetail/PropertyDetailUIState.kt @@ -1,6 +1,7 @@ package com.cramsan.edifikana.client.lib.features.home.propertydetail import com.cramsan.edifikana.client.ui.components.ImageOptionUIModel +import com.cramsan.edifikana.lib.model.organization.OrganizationId import com.cramsan.edifikana.lib.model.property.PropertyId import com.cramsan.framework.core.compose.ViewModelUIState @@ -25,6 +26,7 @@ sealed class PropertyDetailDialogState { data class PropertyDetailUIState( val isLoading: Boolean, val propertyId: PropertyId?, + val organizationId: OrganizationId? = null, val name: String, val address: String, val imageUrl: String?, diff --git a/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/propertydetail/PropertyDetailViewModel.kt b/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/propertydetail/PropertyDetailViewModel.kt index 5bb5a11da..d0f335247 100644 --- a/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/propertydetail/PropertyDetailViewModel.kt +++ b/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/propertydetail/PropertyDetailViewModel.kt @@ -48,6 +48,7 @@ class PropertyDetailViewModel( name = property.name, address = property.address, imageUrl = property.imageUrl, + organizationId = property.organizationId, ) } }.onFailure { throwable -> @@ -101,6 +102,7 @@ class PropertyDetailViewModel( name = property.name, address = property.address, imageUrl = property.imageUrl, + organizationId = property.organizationId, ) } }.onFailure { @@ -278,10 +280,11 @@ class PropertyDetailViewModel( fun deleteProperty() { viewModelCoroutineScope.launch { val propertyId = uiState.value.propertyId ?: return@launch + val organizationId = uiState.value.organizationId ?: return@launch updateUiState { it.copy(isLoading = true) } propertyManager - .removeProperty(propertyId) + .removeProperty(propertyId, organizationId) .onSuccess { emitWindowEvent( EdifikanaWindowsEvent.ShowSnackbar("Property deleted successfully"), diff --git a/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/propertyhome/PropertyHomeViewModel.kt b/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/propertyhome/PropertyHomeViewModel.kt index a7bac37d2..0ea0e7e1b 100644 --- a/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/propertyhome/PropertyHomeViewModel.kt +++ b/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/home/propertyhome/PropertyHomeViewModel.kt @@ -4,6 +4,7 @@ import com.cramsan.architecture.client.manager.PreferencesManager import com.cramsan.edifikana.client.lib.features.account.AccountDestination import com.cramsan.edifikana.client.lib.features.window.EdifikanaNavGraphDestination import com.cramsan.edifikana.client.lib.features.window.EdifikanaWindowsEvent +import com.cramsan.edifikana.client.lib.managers.OrganizationManager import com.cramsan.edifikana.client.lib.managers.PropertyManager import com.cramsan.edifikana.client.lib.settings.getLastSelectedPropertyId import com.cramsan.edifikana.client.lib.settings.setLastSelectedPropertyId @@ -22,6 +23,7 @@ class PropertyHomeViewModel( dependencies: ViewModelDependencies, private val propertyManager: PropertyManager, private val preferencesManager: PreferencesManager, + private val organizationManager: OrganizationManager, ) : BaseViewModel( dependencies, PropertyHomeUIModel.Empty, @@ -50,7 +52,17 @@ class PropertyHomeViewModel( } private suspend fun updatePropertyList() { - val properties = propertyManager.getPropertyList().getOrNull().orEmpty() + val organizationId = + organizationManager + .getOrganizations() + .getOrNull() + ?.firstOrNull() + ?.id + val properties = + organizationId + ?.let { + propertyManager.getPropertyList(it).getOrNull() + }.orEmpty() val selectedProperty = uiState.value.propertyId ?: preferencesManager.getLastSelectedPropertyId() diff --git a/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/settings/organizations/myorganizations/MyOrganizationsViewModel.kt b/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/settings/organizations/myorganizations/MyOrganizationsViewModel.kt index 33da5e762..2bb6bc4c5 100644 --- a/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/settings/organizations/myorganizations/MyOrganizationsViewModel.kt +++ b/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/features/settings/organizations/myorganizations/MyOrganizationsViewModel.kt @@ -42,13 +42,6 @@ class MyOrganizationsViewModel( val currentUserId = authManager.activeUser().value val activeOrgId = preferencesManager.getLastSelectedOrganizationId() - val propertyCountsByOrg = - propertyManager - .getPropertyList() - .getOrNull() - ?.groupingBy { it.organizationId } - ?.eachCount() - ?: emptyMap() organizationManager .getOrganizations() @@ -62,11 +55,17 @@ class MyOrganizationsViewModel( val myMembership = members.firstOrNull { it.userId == currentUserId } ?: return@mapNotNull null + // Property count is scoped per-org (matches the org-scoped + // property list endpoint) rather than computed from a single + // cross-org query, so counts reflect all of an org's properties, + // not just the ones the current user happens to be individually + // assigned to. + val propertyCount = propertyManager.getPropertyList(org.id).getOrNull()?.size ?: 0 OrgListItemUIModel( orgId = org.id, name = org.name, roleLabel = myMembership.role.toDisplayLabel(), - propertyCount = propertyCountsByOrg[org.id] ?: 0, + propertyCount = propertyCount, isActive = org.id == activeOrgId, ) }.sortedBy { it.name.lowercase() } diff --git a/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/managers/PropertyManager.kt b/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/managers/PropertyManager.kt index fa95c75d8..337e15ca1 100644 --- a/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/managers/PropertyManager.kt +++ b/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/managers/PropertyManager.kt @@ -24,12 +24,12 @@ class PropertyManager( private val dependencies: ManagerDependencies, ) { /** - * Get the list of properties. + * Get the list of properties belonging to the given organization. */ - suspend fun getPropertyList(): Result> = + suspend fun getPropertyList(organizationId: OrganizationId): Result> = dependencies.getOrCatch(TAG) { logI(TAG, "getPropertyList") - propertyService.getPropertyList().getOrThrow() + propertyService.getPropertyList(organizationId).getOrThrow() } /** @@ -131,13 +131,13 @@ class PropertyManager( } /** - * Remove the property with the given [propertyId]. + * Remove the property with the given [propertyId], belonging to [organizationId]. */ - suspend fun removeProperty(propertyId: PropertyId): Result = + suspend fun removeProperty(propertyId: PropertyId, organizationId: OrganizationId): Result = dependencies.getOrCatch(TAG) { logI(TAG, "removeProperty") propertyService.removeProperty(propertyId).requireSuccess() - propertyService.getPropertyList().requireSuccess() + propertyService.getPropertyList(organizationId).requireSuccess() } /** diff --git a/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/service/PropertyService.kt b/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/service/PropertyService.kt index 1cf905e2f..4311c618d 100644 --- a/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/service/PropertyService.kt +++ b/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/service/PropertyService.kt @@ -11,9 +11,9 @@ import com.cramsan.framework.annotations.FrontendService @FrontendService interface PropertyService { /** - * Get a list of properties associated with current user. + * Get a list of properties belonging to the given organization. */ - suspend fun getPropertyList(): Result> + suspend fun getPropertyList(organizationId: OrganizationId): Result> /** * Get the property with the given id. diff --git a/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/service/impl/PropertyServiceImpl.kt b/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/service/impl/PropertyServiceImpl.kt index ffbd6c093..6525120f5 100644 --- a/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/service/impl/PropertyServiceImpl.kt +++ b/edifikana/front-end/app/src/commonMain/kotlin/com/cramsan/edifikana/client/lib/service/impl/PropertyServiceImpl.kt @@ -5,6 +5,7 @@ import com.cramsan.edifikana.client.lib.models.PropertyModel import com.cramsan.edifikana.client.lib.service.PropertyService import com.cramsan.edifikana.lib.model.common.Url import com.cramsan.edifikana.lib.model.network.property.CreatePropertyNetworkRequest +import com.cramsan.edifikana.lib.model.network.property.GetPropertiesQueryParams import com.cramsan.edifikana.lib.model.network.property.PropertyNetworkResponse import com.cramsan.edifikana.lib.model.network.property.UpdatePropertyNetworkRequest import com.cramsan.edifikana.lib.model.organization.OrganizationId @@ -20,12 +21,12 @@ import io.ktor.client.HttpClient */ @FrontendService class PropertyServiceImpl(private val http: HttpClient) : PropertyService { - override suspend fun getPropertyList(): Result> = + override suspend fun getPropertyList(organizationId: OrganizationId): Result> = runSuspendCatching(TAG) { val response = PropertyApi - .getAssignedProperties - .buildRequest() + .getProperties + .buildRequest(queryParam = GetPropertiesQueryParams(organizationId)) .execute(http) val propertyList = response.properties.map { diff --git a/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/home/home/HomeViewModelTest.kt b/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/home/home/HomeViewModelTest.kt deleted file mode 100644 index 577d313e5..000000000 --- a/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/home/home/HomeViewModelTest.kt +++ /dev/null @@ -1,161 +0,0 @@ -package com.cramsan.edifikana.client.lib.features.home.home - -import app.cash.turbine.turbineScope -import com.cramsan.framework.test.advanceUntilIdleAndAwaitComplete -import com.cramsan.architecture.client.manager.PreferencesManager -import com.cramsan.edifikana.client.lib.features.account.AccountDestination -import com.cramsan.edifikana.client.lib.features.home.propertyhome.PropertyHomeViewModel -import com.cramsan.edifikana.client.lib.features.window.EdifikanaNavGraphDestination -import com.cramsan.edifikana.client.lib.features.window.EdifikanaWindowsEvent -import com.cramsan.edifikana.client.lib.managers.PropertyManager -import com.cramsan.edifikana.client.lib.models.PropertyModel -import com.cramsan.edifikana.client.lib.settings.EdifikanaSettingKey -import com.cramsan.edifikana.lib.model.organization.OrganizationId -import com.cramsan.edifikana.lib.model.property.PropertyId -import com.cramsan.framework.core.UnifiedDispatcherProvider -import com.cramsan.framework.core.compose.ApplicationEvent -import com.cramsan.framework.core.compose.EventBus -import com.cramsan.framework.core.compose.ViewModelDependencies -import com.cramsan.framework.core.compose.WindowEvent -import com.cramsan.framework.logging.EventLogger -import com.cramsan.framework.logging.implementation.PassthroughEventLogger -import com.cramsan.framework.logging.implementation.StdOutEventLoggerDelegate -import com.cramsan.framework.test.CollectorCoroutineExceptionHandler -import com.cramsan.framework.test.CoroutineTest -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockk -import org.junit.jupiter.api.BeforeEach -import kotlin.test.Test -import kotlin.test.assertEquals - -class HomeViewModelTest : CoroutineTest() { - - private lateinit var propertyManager: PropertyManager - private lateinit var viewModel: PropertyHomeViewModel - private lateinit var exceptionHandler: CollectorCoroutineExceptionHandler - private lateinit var applicationEventReceiver: EventBus - private lateinit var windowEventBus: EventBus - - private lateinit var preferencesManager: PreferencesManager - - @BeforeEach - fun setUp() { - EventLogger.setInstance(PassthroughEventLogger(StdOutEventLoggerDelegate())) - propertyManager = mockk() - exceptionHandler = CollectorCoroutineExceptionHandler() - applicationEventReceiver = EventBus() - windowEventBus = EventBus() - preferencesManager = mockk() - val dependencies = ViewModelDependencies( - appScope = testCoroutineScope, - dispatcherProvider = UnifiedDispatcherProvider(testCoroutineDispatcher), - coroutineExceptionHandler = exceptionHandler, - applicationEventReceiver = applicationEventReceiver, - windowEventReceiver = windowEventBus, - ) - viewModel = PropertyHomeViewModel(dependencies, propertyManager, preferencesManager) - } - - @Test - fun `test loadContent successfully loads properties`() = runCoroutineTest { - // Arrange - val organizationId = OrganizationId("org_1") - val properties = listOf( - PropertyModel( - id = PropertyId("1"), - name = "Property 1", - address = "Address 1", - organizationId = organizationId - ), - PropertyModel( - id = PropertyId("2"), - name = "Property 2", - address = "Address 2", - organizationId = organizationId - ), - ) - coEvery { propertyManager.getPropertyList() } returns Result.success(properties) - coEvery { - preferencesManager.getStringPreference(EdifikanaSettingKey.lastSelectedProperty) - } returns Result.success(null) - - // Act - viewModel.loadContent() - - // Assert - coVerify { propertyManager.getPropertyList() } - val uiState = viewModel.uiState.value - assertEquals("Property 1", uiState.label) - assertEquals(2, uiState.availableProperties.size) - } - - @Test - fun `test selectProperty updates active property`() = runCoroutineTest { - // Arrange - val propertyId = PropertyId("1") - coEvery { propertyManager.getPropertyList() } returns Result.success(emptyList()) - - // Act - viewModel.selectProperty(propertyId) - - // Assert - val uiState = viewModel.uiState.value - assertEquals(propertyId, uiState.propertyId) - } - - @Test - fun `test navigateBack emits NavigateBack event`() = runCoroutineTest { - turbineScope { - // Arrange - val turbine = windowEventBus.events.testIn(backgroundScope) - - // Act - viewModel.navigateBack() - - // Assert - assertEquals(EdifikanaWindowsEvent.NavigateBack, turbine.awaitItem()) - advanceUntilIdleAndAwaitComplete(turbine) - } - } - - @Test - fun `test navigateToAccount emits NavigateToNavGraph event`() = runCoroutineTest { - turbineScope { - // Arrange - val turbine = windowEventBus.events.testIn(backgroundScope) - - // Act - viewModel.navigateToAccount() - - // Assert - assertEquals( - EdifikanaWindowsEvent.NavigateToNavGraph( - EdifikanaNavGraphDestination.AccountNavGraphDestination - ), - turbine.awaitItem(), - ) - advanceUntilIdleAndAwaitComplete(turbine) - } - } - - @Test - fun `test navigateToNotifications emits NavigateToScreen event`() = runCoroutineTest { - turbineScope { - // Arrange - val turbine = windowEventBus.events.testIn(backgroundScope) - - // Act - viewModel.navigateToNotifications() - - // Assert - assertEquals( - EdifikanaWindowsEvent.NavigateToScreen( - AccountDestination.NotificationsDestination - ), - turbine.awaitItem(), - ) - advanceUntilIdleAndAwaitComplete(turbine) - } - } -} diff --git a/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/home/propertiesoverview/PropertiesOverviewViewModelTest.kt b/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/home/propertiesoverview/PropertiesOverviewViewModelTest.kt index d437403b8..8b8befecb 100644 --- a/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/home/propertiesoverview/PropertiesOverviewViewModelTest.kt +++ b/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/home/propertiesoverview/PropertiesOverviewViewModelTest.kt @@ -79,7 +79,7 @@ class PropertiesOverviewViewModelTest : CoroutineTest() { @Test fun `test initial ui state`() = runCoroutineTest { // Set up - coEvery { propertyManager.getPropertyList() } returns Result.success(listOf( + coEvery { propertyManager.getPropertyList(OrganizationId("org-1")) } returns Result.success(listOf( PropertyModel( id = PropertyId("property-1"), name = "Test Property 1", @@ -90,7 +90,7 @@ class PropertiesOverviewViewModelTest : CoroutineTest() { // Act val initialState = viewModel.uiState.value - viewModel.initialize() + viewModel.initialize(OrganizationId("org-1")) val loadedState = viewModel.uiState.value // Assert @@ -111,10 +111,10 @@ class PropertiesOverviewViewModelTest : CoroutineTest() { // Arrange val turbine = windowEventBus.events.testIn(backgroundScope) val error = RuntimeException("Network error") - coEvery { propertyManager.getPropertyList() } returns Result.failure(error) + coEvery { propertyManager.getPropertyList(OrganizationId("org-1")) } returns Result.failure(error) // Act - viewModel.initialize() + viewModel.initialize(OrganizationId("org-1")) // Assert val loadedState = viewModel.uiState.value diff --git a/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/home/propertydetail/PropertyDetailViewModelTest.kt b/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/home/propertydetail/PropertyDetailViewModelTest.kt index f0df78569..492fd398d 100644 --- a/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/home/propertydetail/PropertyDetailViewModelTest.kt +++ b/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/home/propertydetail/PropertyDetailViewModelTest.kt @@ -342,7 +342,7 @@ class PropertyDetailViewModelTest : CoroutineTest() { imageUrl = "drawable:CASA", ) coEvery { propertyManager.getProperty(propertyId) } returns Result.success(property) - coEvery { propertyManager.removeProperty(propertyId) } returns Result.success(Unit) + coEvery { propertyManager.removeProperty(propertyId, OrganizationId("org-1")) } returns Result.success(Unit) viewModel.initialize(propertyId) @@ -360,7 +360,7 @@ class PropertyDetailViewModelTest : CoroutineTest() { advanceUntilIdleAndAwaitComplete(turbine) } - coVerify { propertyManager.removeProperty(propertyId) } + coVerify { propertyManager.removeProperty(propertyId, OrganizationId("org-1")) } assertTrue(exceptionHandler.exceptions.isEmpty()) } @@ -375,7 +375,7 @@ class PropertyDetailViewModelTest : CoroutineTest() { imageUrl = "drawable:QUINTA", ) coEvery { propertyManager.getProperty(propertyId) } returns Result.success(property) - coEvery { propertyManager.removeProperty(propertyId) } returns Result.failure( + coEvery { propertyManager.removeProperty(propertyId, OrganizationId("org-1")) } returns Result.failure( Exception("Delete failed") ) @@ -402,6 +402,6 @@ class PropertyDetailViewModelTest : CoroutineTest() { fun `test deleteProperty without propertyId does nothing`() = runCoroutineTest { viewModel.deleteProperty() - coVerify(exactly = 0) { propertyManager.removeProperty(any()) } + coVerify(exactly = 0) { propertyManager.removeProperty(any(), any()) } } } diff --git a/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/home/propertyhome/PropertyHomeViewModelTest.kt b/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/home/propertyhome/PropertyHomeViewModelTest.kt index 7f522904f..803d98833 100644 --- a/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/home/propertyhome/PropertyHomeViewModelTest.kt +++ b/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/home/propertyhome/PropertyHomeViewModelTest.kt @@ -6,7 +6,9 @@ import com.cramsan.architecture.client.manager.PreferencesManager import com.cramsan.edifikana.client.lib.features.account.AccountDestination import com.cramsan.edifikana.client.lib.features.window.EdifikanaNavGraphDestination import com.cramsan.edifikana.client.lib.features.window.EdifikanaWindowsEvent +import com.cramsan.edifikana.client.lib.managers.OrganizationManager import com.cramsan.edifikana.client.lib.managers.PropertyManager +import com.cramsan.edifikana.client.lib.models.Organization import com.cramsan.edifikana.client.lib.models.PropertyModel import com.cramsan.edifikana.lib.model.organization.OrganizationId import com.cramsan.edifikana.lib.model.property.PropertyId @@ -36,6 +38,7 @@ class PropertyHomeViewModelTest : CoroutineTest() { private lateinit var viewModel: PropertyHomeViewModel private lateinit var propertyManager: PropertyManager private lateinit var preferencesManager: PreferencesManager + private lateinit var organizationManager: OrganizationManager private lateinit var exceptionHandler: CollectorCoroutineExceptionHandler private lateinit var applicationEventReceiver: EventBus private lateinit var windowEventBus: EventBus @@ -48,7 +51,11 @@ class PropertyHomeViewModelTest : CoroutineTest() { exceptionHandler = CollectorCoroutineExceptionHandler() propertyManager = mockk(relaxed = true) preferencesManager = mockk(relaxed = true) + organizationManager = mockk(relaxed = true) coEvery { preferencesManager.getStringPreference(any()) } returns Result.success(null) + coEvery { organizationManager.getOrganizations() } returns Result.success( + listOf(Organization(id = OrganizationId("org-1"), name = "Test Org", description = "")) + ) viewModel = PropertyHomeViewModel( dependencies = ViewModelDependencies( appScope = testCoroutineScope, @@ -59,6 +66,7 @@ class PropertyHomeViewModelTest : CoroutineTest() { ), propertyManager = propertyManager, preferencesManager = preferencesManager, + organizationManager = organizationManager, ) } @@ -80,7 +88,7 @@ class PropertyHomeViewModelTest : CoroutineTest() { organizationId = OrganizationId("org-1"), ) ) - coEvery { propertyManager.getPropertyList() } returns Result.success(properties) + coEvery { propertyManager.getPropertyList(OrganizationId("org-1")) } returns Result.success(properties) // Act viewModel.loadContent() @@ -95,7 +103,7 @@ class PropertyHomeViewModelTest : CoroutineTest() { @Test fun `test loadContent with no properties selects GoToOrganization tab`() = runCoroutineTest { // Set up - coEvery { propertyManager.getPropertyList() } returns Result.success(emptyList()) + coEvery { propertyManager.getPropertyList(OrganizationId("org-1")) } returns Result.success(emptyList()) // Act viewModel.loadContent() @@ -118,7 +126,7 @@ class PropertyHomeViewModelTest : CoroutineTest() { organizationId = OrganizationId("org-1"), ) ) - coEvery { propertyManager.getPropertyList() } returns Result.success(properties) + coEvery { propertyManager.getPropertyList(OrganizationId("org-1")) } returns Result.success(properties) // Act viewModel.selectProperty(propertyId) @@ -215,7 +223,7 @@ class PropertyHomeViewModelTest : CoroutineTest() { @Test fun `test loadContent with existing tab selection preserves tab`() = runCoroutineTest { // Set up - First load with no properties - coEvery { propertyManager.getPropertyList() } returns Result.success(emptyList()) + coEvery { propertyManager.getPropertyList(OrganizationId("org-1")) } returns Result.success(emptyList()) viewModel.loadContent() assertEquals(Tabs.GoToOrganization, viewModel.uiState.value.selectedTab) @@ -228,7 +236,7 @@ class PropertyHomeViewModelTest : CoroutineTest() { organizationId = OrganizationId("org-1"), ) ) - coEvery { propertyManager.getPropertyList() } returns Result.success(properties) + coEvery { propertyManager.getPropertyList(OrganizationId("org-1")) } returns Result.success(properties) // Act - load content again viewModel.loadContent() @@ -248,7 +256,7 @@ class PropertyHomeViewModelTest : CoroutineTest() { organizationId = OrganizationId("org-1"), ) ) - coEvery { propertyManager.getPropertyList() } returns Result.success(properties) + coEvery { propertyManager.getPropertyList(OrganizationId("org-1")) } returns Result.success(properties) // Act - first load viewModel.loadContent() diff --git a/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/settings/organizations/myorganizations/MyOrganizationsViewModelTest.kt b/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/settings/organizations/myorganizations/MyOrganizationsViewModelTest.kt index ce43ebf2d..47b12a29f 100644 --- a/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/settings/organizations/myorganizations/MyOrganizationsViewModelTest.kt +++ b/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/settings/organizations/myorganizations/MyOrganizationsViewModelTest.kt @@ -100,7 +100,7 @@ class MyOrganizationsViewModelTest : CoroutineTest() { coEvery { organizationManager.getOrganizations() } returns Result.success(listOf(org)) coEvery { membershipManager.listMembers(orgId) } returns Result.success(listOf(member)) coEvery { preferencesManager.getStringPreference(EdifikanaSettingKey.lastSelectedOrganization) } returns Result.success(orgId.id) - coEvery { propertyManager.getPropertyList() } returns Result.success(properties) + coEvery { propertyManager.getPropertyList(orgId) } returns Result.success(properties) viewModel.initialize() @@ -129,7 +129,7 @@ class MyOrganizationsViewModelTest : CoroutineTest() { coEvery { organizationManager.getOrganizations() } returns Result.success(listOf(org)) coEvery { membershipManager.listMembers(orgId) } returns Result.success(listOf(member)) coEvery { preferencesManager.getStringPreference(EdifikanaSettingKey.lastSelectedOrganization) } returns Result.success(orgId.id) - coEvery { propertyManager.getPropertyList() } returns Result.failure(RuntimeException("error")) + coEvery { propertyManager.getPropertyList(orgId) } returns Result.failure(RuntimeException("error")) viewModel.initialize() @@ -165,7 +165,7 @@ class MyOrganizationsViewModelTest : CoroutineTest() { coEvery { preferencesManager.getStringPreference(EdifikanaSettingKey.lastSelectedOrganization) } returns Result.success(activeOrgId.id) - coEvery { propertyManager.getPropertyList() } returns Result.success(emptyList()) + coEvery { propertyManager.getPropertyList(any()) } returns Result.success(emptyList()) viewModel.initialize() @@ -180,7 +180,7 @@ class MyOrganizationsViewModelTest : CoroutineTest() { fun `initialize shows snackbar on org load failure`() = runCoroutineTest { coEvery { organizationManager.getOrganizations() } returns Result.failure(RuntimeException("error")) coEvery { preferencesManager.getStringPreference(EdifikanaSettingKey.lastSelectedOrganization) } returns Result.success(null) - coEvery { propertyManager.getPropertyList() } returns Result.success(emptyList()) + coEvery { propertyManager.getPropertyList(any()) } returns Result.success(emptyList()) turbineScope { val turbine = windowEventBus.events.testIn(backgroundScope) diff --git a/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/splash/SplashViewModelTest.kt b/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/splash/SplashViewModelTest.kt index d77c117ca..dc6e2be59 100644 --- a/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/splash/SplashViewModelTest.kt +++ b/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/features/splash/SplashViewModelTest.kt @@ -5,7 +5,6 @@ import com.cramsan.edifikana.client.lib.features.window.EdifikanaNavGraphDestina import com.cramsan.edifikana.client.lib.features.window.EdifikanaWindowsEvent import com.cramsan.edifikana.client.lib.managers.AuthManager import com.cramsan.edifikana.client.lib.managers.OrganizationManager -import com.cramsan.edifikana.client.lib.managers.PropertyManager import com.cramsan.edifikana.lib.model.invite.InviteId import com.cramsan.framework.core.UnifiedDispatcherProvider import com.cramsan.framework.core.compose.ApplicationEvent @@ -35,7 +34,6 @@ class SplashViewModelTest : CoroutineTest() { private lateinit var viewModel: SplashViewModel private lateinit var authManager: AuthManager private lateinit var organizationManager: OrganizationManager - private lateinit var propertyManager: PropertyManager private lateinit var exceptionHandler: CollectorCoroutineExceptionHandler @@ -58,7 +56,6 @@ class SplashViewModelTest : CoroutineTest() { ) authManager = mockk() organizationManager = mockk() - propertyManager = mockk() viewModel = SplashViewModel( dependencies = dependencies, authManager = authManager, @@ -94,7 +91,6 @@ class SplashViewModelTest : CoroutineTest() { @Test fun `test enforceAuth when signed in`() = runCoroutineTest { coEvery { authManager.isSignedIn() } returns Result.success(true) - coEvery { propertyManager.getPropertyList() } returns Result.success(emptyList()) coEvery { organizationManager.getOrganizations() } returns Result.success(listOf(mockk())) viewModel.enforceAuth() diff --git a/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/managers/PropertyManagerTest.kt b/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/managers/PropertyManagerTest.kt index 61a096d4f..4eb313c2c 100644 --- a/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/managers/PropertyManagerTest.kt +++ b/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/managers/PropertyManagerTest.kt @@ -58,13 +58,14 @@ class PropertyManagerTest : CoroutineTest() { fun `getPropertyList returns property list`() = runCoroutineTest { // Arrange val propertyList = listOf(mockk(), mockk()) - coEvery { propertyService.getPropertyList() } returns Result.success(propertyList) + val organizationId = OrganizationId("org-1") + coEvery { propertyService.getPropertyList(organizationId) } returns Result.success(propertyList) // Act - val result = manager.getPropertyList() + val result = manager.getPropertyList(organizationId) // Assert assertTrue(result.isSuccess) assertEquals(propertyList, result.getOrNull()) - coVerify { propertyService.getPropertyList() } + coVerify { propertyService.getPropertyList(organizationId) } } /** @@ -213,11 +214,12 @@ class PropertyManagerTest : CoroutineTest() { fun `removeProperty calls service with correct arguments`() = runCoroutineTest { // Arrange val propertyId = PropertyId("property-1") + val organizationId = OrganizationId("org-1") coEvery { propertyService.removeProperty(propertyId) } returns Result.success(Unit) - coEvery { propertyService.getPropertyList() } returns Result.success(emptyList()) + coEvery { propertyService.getPropertyList(organizationId) } returns Result.success(emptyList()) // Act - val result = manager.removeProperty(propertyId) + val result = manager.removeProperty(propertyId, organizationId) // Assert assertTrue(result.isSuccess) diff --git a/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/service/impl/PropertyServiceImplTest.kt b/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/service/impl/PropertyServiceImplTest.kt index c53bf9c85..71525c074 100644 --- a/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/service/impl/PropertyServiceImplTest.kt +++ b/edifikana/front-end/app/src/jvmTest/kotlin/com/cramsan/edifikana/client/lib/service/impl/PropertyServiceImplTest.kt @@ -76,7 +76,7 @@ class PropertyServiceImplTest { } // Act - val result = service.getPropertyList() + val result = service.getPropertyList(OrganizationId("org-1")) // Assert assertTrue(result.isSuccess) @@ -114,7 +114,7 @@ class PropertyServiceImplTest { } // Act - val result = service.getPropertyList() + val result = service.getPropertyList(OrganizationId("org-1")) // Assert assertTrue(result.isSuccess) diff --git a/edifikana/models/src/commonMain/kotlin/com/cramsan/edifikana/lib/model/network/employee/GetEmployeesForPropertyQueryParams.kt b/edifikana/models/src/commonMain/kotlin/com/cramsan/edifikana/lib/model/network/employee/GetEmployeesForPropertyQueryParams.kt new file mode 100644 index 000000000..1e8bf4a22 --- /dev/null +++ b/edifikana/models/src/commonMain/kotlin/com/cramsan/edifikana/lib/model/network/employee/GetEmployeesForPropertyQueryParams.kt @@ -0,0 +1,20 @@ +package com.cramsan.edifikana.lib.model.network.employee + +import com.cramsan.edifikana.lib.model.property.PropertyId +import com.cramsan.framework.annotations.NetworkModel +import com.cramsan.framework.annotations.api.QueryParam +import io.ktor.openapi.JsonSchema +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Query parameters for listing employees assigned to a property. + */ +@NetworkModel +@Serializable +@JsonSchema.Description("Query parameters for listing employees assigned to a property, requiring a property id.") +data class GetEmployeesForPropertyQueryParams( + @SerialName("property_id") + @JsonSchema.Description("Identifier of the property to list employees for.") + val propertyId: PropertyId, +) : QueryParam diff --git a/edifikana/models/src/commonMain/kotlin/com/cramsan/edifikana/lib/model/network/property/GetPropertiesQueryParams.kt b/edifikana/models/src/commonMain/kotlin/com/cramsan/edifikana/lib/model/network/property/GetPropertiesQueryParams.kt new file mode 100644 index 000000000..8d864689a --- /dev/null +++ b/edifikana/models/src/commonMain/kotlin/com/cramsan/edifikana/lib/model/network/property/GetPropertiesQueryParams.kt @@ -0,0 +1,20 @@ +package com.cramsan.edifikana.lib.model.network.property + +import com.cramsan.edifikana.lib.model.organization.OrganizationId +import com.cramsan.framework.annotations.NetworkModel +import com.cramsan.framework.annotations.api.QueryParam +import io.ktor.openapi.JsonSchema +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Query parameters for listing properties. + */ +@NetworkModel +@Serializable +@JsonSchema.Description("Query parameters for listing properties, requiring an organization id.") +data class GetPropertiesQueryParams( + @SerialName("organization_id") + @JsonSchema.Description("Identifier of the organization to list properties for.") + val organizationId: OrganizationId, +) : QueryParam