package com.android.trisolarisserver.controller import com.android.trisolarisserver.component.PropertyAccess import com.android.trisolarisserver.controller.dto.PropertyCreateRequest import com.android.trisolarisserver.controller.dto.PropertyResponse import com.android.trisolarisserver.controller.dto.PropertyUpdateRequest import com.android.trisolarisserver.controller.dto.PropertyUserResponse import com.android.trisolarisserver.controller.dto.PropertyUserRoleRequest import com.android.trisolarisserver.controller.dto.UserResponse import com.android.trisolarisserver.repo.AppUserRepo import com.android.trisolarisserver.repo.OrganizationRepo import com.android.trisolarisserver.repo.PropertyRepo import com.android.trisolarisserver.repo.PropertyUserRepo import com.android.trisolarisserver.models.property.Property import com.android.trisolarisserver.models.property.PropertyUser import com.android.trisolarisserver.models.property.PropertyUserId import com.android.trisolarisserver.models.property.Role import com.android.trisolarisserver.security.MyPrincipal import com.android.trisolarisserver.models.booking.TransportMode import org.springframework.http.HttpStatus import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController import org.springframework.web.server.ResponseStatusException import java.util.UUID @RestController class Properties( private val propertyAccess: PropertyAccess, private val propertyRepo: PropertyRepo, private val orgRepo: OrganizationRepo, private val propertyUserRepo: PropertyUserRepo, private val appUserRepo: AppUserRepo ) { @PostMapping("/orgs/{orgId}/properties") @ResponseStatus(HttpStatus.CREATED) fun createProperty( @PathVariable orgId: UUID, @AuthenticationPrincipal principal: MyPrincipal?, @RequestBody request: PropertyCreateRequest ): PropertyResponse { val user = requireUser(principal) if (user.org.id != orgId) { throw ResponseStatusException(HttpStatus.FORBIDDEN, "No access to org") } requireOrgRole(orgId, user.id!!, Role.ADMIN) if (propertyRepo.existsByOrgIdAndCode(orgId, request.code)) { throw ResponseStatusException(HttpStatus.CONFLICT, "Property code already exists for org") } val org = orgRepo.findById(orgId).orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Org not found") } val property = Property( org = org, code = request.code, name = request.name, addressText = request.addressText, timezone = request.timezone ?: "Asia/Kolkata", currency = request.currency ?: "INR", active = request.active ?: true, otaAliases = request.otaAliases?.toMutableSet() ?: mutableSetOf(), emailAddresses = request.emailAddresses?.toMutableSet() ?: mutableSetOf(), allowedTransportModes = request.allowedTransportModes?.let { parseTransportModes(it) } ?: mutableSetOf() ) val saved = propertyRepo.save(property) return saved.toResponse() } @GetMapping("/orgs/{orgId}/properties") fun listProperties( @PathVariable orgId: UUID, @AuthenticationPrincipal principal: MyPrincipal? ): List { val user = requireUser(principal) if (user.org.id != orgId) { throw ResponseStatusException(HttpStatus.FORBIDDEN, "No access to org") } val propertyIds = propertyUserRepo.findPropertyIdsByOrgAndUser(orgId, user.id!!) return propertyRepo.findAllById(propertyIds).map { it.toResponse() } } @GetMapping("/orgs/{orgId}/users") fun listUsers( @PathVariable orgId: UUID, @AuthenticationPrincipal principal: MyPrincipal? ): List { val user = requireUser(principal) if (user.org.id != orgId) { throw ResponseStatusException(HttpStatus.FORBIDDEN, "No access to org") } requireOrgRole(orgId, user.id!!, Role.ADMIN, Role.MANAGER) return appUserRepo.findByOrgId(orgId).map { it.toUserResponse() } } @GetMapping("/properties/{propertyId}/users") fun listPropertyUsers( @PathVariable propertyId: UUID, @AuthenticationPrincipal principal: MyPrincipal? ): List { requirePrincipal(principal) propertyAccess.requireMember(propertyId, principal!!.userId) propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER) val users = propertyUserRepo.findByIdPropertyId(propertyId) return users.map { PropertyUserResponse( userId = it.id.userId!!, propertyId = it.id.propertyId!!, roles = it.roles.map { role -> role.name }.toSet() ) } } @PutMapping("/properties/{propertyId}/users/{userId}/roles") fun upsertPropertyUserRoles( @PathVariable propertyId: UUID, @PathVariable userId: UUID, @AuthenticationPrincipal principal: MyPrincipal?, @RequestBody request: PropertyUserRoleRequest ): PropertyUserResponse { requirePrincipal(principal) propertyAccess.requireMember(propertyId, principal!!.userId) val actorRoles = propertyUserRepo.findRolesByPropertyAndUser(propertyId, principal.userId) val allowedRoles = when { actorRoles.contains(Role.ADMIN) -> setOf(Role.ADMIN, Role.MANAGER, Role.STAFF, Role.AGENT) actorRoles.contains(Role.MANAGER) -> setOf(Role.STAFF, Role.AGENT) else -> emptySet() } if (allowedRoles.isEmpty()) { throw ResponseStatusException(HttpStatus.FORBIDDEN, "Missing role") } val requestedRoles = try { request.roles.map { Role.valueOf(it) }.toSet() } catch (ex: IllegalArgumentException) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown role") } if (!allowedRoles.containsAll(requestedRoles)) { throw ResponseStatusException(HttpStatus.FORBIDDEN, "Role not allowed") } val property = propertyRepo.findById(propertyId).orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found") } val targetUser = appUserRepo.findById(userId).orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "User not found") } if (targetUser.org.id != property.org.id) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "User not in property org") } val propertyUser = PropertyUser( id = PropertyUserId(propertyId = propertyId, userId = userId), property = property, user = targetUser, roles = requestedRoles.toMutableSet() ) val saved = propertyUserRepo.save(propertyUser) return PropertyUserResponse( userId = saved.id.userId!!, propertyId = saved.id.propertyId!!, roles = saved.roles.map { it.name }.toSet() ) } @DeleteMapping("/properties/{propertyId}/users/{userId}") @ResponseStatus(HttpStatus.NO_CONTENT) fun deletePropertyUser( @PathVariable propertyId: UUID, @PathVariable userId: UUID, @AuthenticationPrincipal principal: MyPrincipal? ) { requirePrincipal(principal) propertyAccess.requireMember(propertyId, principal!!.userId) propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN) val id = PropertyUserId(propertyId = propertyId, userId = userId) if (propertyUserRepo.existsById(id)) { propertyUserRepo.deleteById(id) } } @PutMapping("/properties/{propertyId}") fun updateProperty( @PathVariable propertyId: UUID, @AuthenticationPrincipal principal: MyPrincipal?, @RequestBody request: PropertyUpdateRequest ): PropertyResponse { requirePrincipal(principal) propertyAccess.requireMember(propertyId, principal!!.userId) propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN) val property = propertyRepo.findById(propertyId).orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found") } if (propertyRepo.existsByOrgIdAndCodeAndIdNot(property.org.id!!, request.code, propertyId)) { throw ResponseStatusException(HttpStatus.CONFLICT, "Property code already exists for org") } property.code = request.code property.name = request.name property.addressText = request.addressText ?: property.addressText property.timezone = request.timezone ?: property.timezone property.currency = request.currency ?: property.currency property.active = request.active ?: property.active if (request.otaAliases != null) { property.otaAliases = request.otaAliases.toMutableSet() } if (request.emailAddresses != null) { property.emailAddresses = request.emailAddresses.toMutableSet() } if (request.allowedTransportModes != null) { property.allowedTransportModes = parseTransportModes(request.allowedTransportModes) } return propertyRepo.save(property).toResponse() } private fun requirePrincipal(principal: MyPrincipal?) { if (principal == null) { throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal") } } private fun requireUser(principal: MyPrincipal?): com.android.trisolarisserver.models.property.AppUser { if (principal == null) { throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal") } return appUserRepo.findById(principal.userId).orElseThrow { ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found") } } private fun requireOrgRole(orgId: UUID, userId: UUID, vararg roles: Role) { if (!propertyUserRepo.hasAnyRoleInOrg(orgId, userId, roles.toSet())) { throw ResponseStatusException(HttpStatus.FORBIDDEN, "Missing role") } } private fun parseTransportModes(modes: Set): MutableSet { return try { modes.map { TransportMode.valueOf(it) }.toMutableSet() } catch (_: IllegalArgumentException) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown transport mode") } } } private fun Property.toResponse(): PropertyResponse { val id = id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Property id missing") val orgId = org.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Org id missing") return PropertyResponse( id = id, orgId = orgId, code = code, name = name, addressText = addressText, timezone = timezone, currency = currency, active = active, otaAliases = otaAliases.toSet(), emailAddresses = emailAddresses.toSet(), allowedTransportModes = allowedTransportModes.map { it.name }.toSet() ) } private fun com.android.trisolarisserver.models.property.AppUser.toUserResponse(): UserResponse { val id = this.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "User id missing") val orgId = this.org.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Org id missing") return UserResponse( id = id, orgId = orgId, firebaseUid = firebaseUid, phoneE164 = phoneE164, name = name, disabled = disabled ) }