added auth,property db and room db,org db

This commit is contained in:
androidlover5842
2026-01-24 16:11:40 +05:30
parent c360ff627d
commit 16f279fe5a
22 changed files with 1113 additions and 22 deletions

View File

@@ -0,0 +1,64 @@
package com.android.trisolarisserver.controller
import com.android.trisolarisserver.controller.dto.PropertyUserResponse
import com.android.trisolarisserver.controller.dto.UserResponse
import com.android.trisolarisserver.db.repo.AppUserRepo
import com.android.trisolarisserver.db.repo.PropertyUserRepo
import com.android.trisolarisserver.security.MyPrincipal
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
import org.springframework.http.HttpStatus
@RestController
@RequestMapping("/auth")
class Auth(
private val appUserRepo: AppUserRepo,
private val propertyUserRepo: PropertyUserRepo
) {
@PostMapping("/verify")
fun verify(@AuthenticationPrincipal principal: MyPrincipal?): AuthResponse {
return buildAuthResponse(principal)
}
@GetMapping("/me")
fun me(@AuthenticationPrincipal principal: MyPrincipal?): AuthResponse {
return buildAuthResponse(principal)
}
private fun buildAuthResponse(principal: MyPrincipal?): AuthResponse {
if (principal == null) {
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
}
val user = appUserRepo.findById(principal.userId).orElseThrow {
ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found")
}
val memberships = propertyUserRepo.findByIdUserId(principal.userId).map {
PropertyUserResponse(
userId = it.id.userId!!,
propertyId = it.id.propertyId!!,
roles = it.roles.map { role -> role.name }.toSet()
)
}
return AuthResponse(
user = UserResponse(
id = user.id!!,
orgId = user.org.id!!,
firebaseUid = user.firebaseUid,
phoneE164 = user.phoneE164,
name = user.name,
disabled = user.disabled
),
properties = memberships
)
}
}
data class AuthResponse(
val user: UserResponse,
val properties: List<PropertyUserResponse>
)

View File

@@ -0,0 +1,78 @@
package com.android.trisolarisserver.controller
import com.android.trisolarisserver.controller.dto.OrgCreateRequest
import com.android.trisolarisserver.controller.dto.OrgResponse
import com.android.trisolarisserver.db.repo.AppUserRepo
import com.android.trisolarisserver.db.repo.OrganizationRepo
import com.android.trisolarisserver.db.repo.PropertyUserRepo
import com.android.trisolarisserver.models.property.Organization
import com.android.trisolarisserver.models.property.Role
import com.android.trisolarisserver.security.MyPrincipal
import org.springframework.http.HttpStatus
import org.springframework.security.core.annotation.AuthenticationPrincipal
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.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
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
@RequestMapping("/orgs")
class Orgs(
private val orgRepo: OrganizationRepo,
private val appUserRepo: AppUserRepo,
private val propertyUserRepo: PropertyUserRepo
) {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun createOrg(
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: OrgCreateRequest
): OrgResponse {
val user = requireUser(principal)
val orgId = user.org.id ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Org missing")
if (!propertyUserRepo.hasAnyRoleInOrg(orgId, user.id!!, setOf(Role.ADMIN))) {
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Missing role")
}
val org = Organization().apply {
name = request.name
}
val saved = orgRepo.save(org)
return OrgResponse(
id = saved.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Org id missing"),
name = saved.name ?: ""
)
}
@GetMapping("/{orgId}")
fun getOrg(
@PathVariable orgId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
): OrgResponse {
val user = requireUser(principal)
if (user.org.id != orgId) {
throw ResponseStatusException(HttpStatus.FORBIDDEN, "No access to org")
}
val org = orgRepo.findById(orgId).orElseThrow {
ResponseStatusException(HttpStatus.NOT_FOUND, "Org not found")
}
return OrgResponse(
id = org.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Org id missing"),
name = org.name ?: ""
)
}
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")
}
}
}

View File

@@ -0,0 +1,286 @@
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.UserCreateRequest
import com.android.trisolarisserver.controller.dto.UserResponse
import com.android.trisolarisserver.db.repo.AppUserRepo
import com.android.trisolarisserver.db.repo.OrganizationRepo
import com.android.trisolarisserver.db.repo.PropertyRepo
import com.android.trisolarisserver.db.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 org.springframework.http.HttpStatus
import org.springframework.security.core.annotation.AuthenticationPrincipal
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.RequestMapping
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,
timezone = request.timezone ?: "Asia/Kolkata",
currency = request.currency ?: "INR",
active = request.active ?: true
)
val saved = propertyRepo.save(property)
return saved.toResponse()
}
@GetMapping("/orgs/{orgId}/properties")
fun listProperties(
@PathVariable orgId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
): List<PropertyResponse> {
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() }
}
@PostMapping("/orgs/{orgId}/users")
@ResponseStatus(HttpStatus.CREATED)
fun createUser(
@PathVariable orgId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: UserCreateRequest
): UserResponse {
val user = requireUser(principal)
if (user.org.id != orgId) {
throw ResponseStatusException(HttpStatus.FORBIDDEN, "No access to org")
}
requireOrgRole(orgId, user.id!!, Role.ADMIN)
if (appUserRepo.existsByFirebaseUid(request.firebaseUid)) {
throw ResponseStatusException(HttpStatus.CONFLICT, "User already exists")
}
val org = orgRepo.findById(orgId).orElseThrow {
ResponseStatusException(HttpStatus.NOT_FOUND, "Org not found")
}
val newUser = com.android.trisolarisserver.models.property.AppUser(
org = org,
firebaseUid = request.firebaseUid,
phoneE164 = request.phoneE164,
name = request.name,
disabled = request.disabled ?: false
)
val saved = appUserRepo.save(newUser)
return saved.toUserResponse()
}
@GetMapping("/orgs/{orgId}/users")
fun listUsers(
@PathVariable orgId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
): List<UserResponse> {
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<PropertyUserResponse> {
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) -> Role.entries.toSet()
actorRoles.contains(Role.MANAGER) -> setOf(Role.STAFF, Role.AGENT)
else -> emptySet()
}
if (allowedRoles.isEmpty()) {
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Missing role")
}
val requestedRoles = request.roles.map { Role.valueOf(it) }.toSet()
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.timezone = request.timezone ?: property.timezone
property.currency = request.currency ?: property.currency
property.active = request.active ?: property.active
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 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,
timezone = timezone,
currency = currency,
active = active
)
}
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
)
}

View File

@@ -0,0 +1,136 @@
package com.android.trisolarisserver.controller
import com.android.trisolarisserver.component.PropertyAccess
import com.android.trisolarisserver.controller.dto.RoomTypeResponse
import com.android.trisolarisserver.controller.dto.RoomTypeUpsertRequest
import com.android.trisolarisserver.db.repo.PropertyRepo
import com.android.trisolarisserver.db.repo.RoomRepo
import com.android.trisolarisserver.db.repo.RoomTypeRepo
import com.android.trisolarisserver.models.property.Role
import com.android.trisolarisserver.models.room.RoomType
import com.android.trisolarisserver.security.MyPrincipal
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.RequestMapping
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
@RequestMapping("/properties/{propertyId}/room-types")
class RoomTypes(
private val propertyAccess: PropertyAccess,
private val roomTypeRepo: RoomTypeRepo,
private val roomRepo: RoomRepo,
private val propertyRepo: PropertyRepo
) {
@GetMapping
fun listRoomTypes(
@PathVariable propertyId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
): List<RoomTypeResponse> {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
return roomTypeRepo.findByPropertyIdOrderByCode(propertyId).map { it.toResponse() }
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun createRoomType(
@PathVariable propertyId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: RoomTypeUpsertRequest
): RoomTypeResponse {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
if (roomTypeRepo.existsByPropertyIdAndCode(propertyId, request.code)) {
throw ResponseStatusException(HttpStatus.CONFLICT, "Room type code already exists for property")
}
val property = propertyRepo.findById(propertyId).orElseThrow {
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
}
val roomType = RoomType(
property = property,
code = request.code,
name = request.name,
baseOccupancy = request.baseOccupancy ?: 2,
maxOccupancy = request.maxOccupancy ?: 3
)
return roomTypeRepo.save(roomType).toResponse()
}
@PutMapping("/{roomTypeId}")
fun updateRoomType(
@PathVariable propertyId: UUID,
@PathVariable roomTypeId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: RoomTypeUpsertRequest
): RoomTypeResponse {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
val roomType = roomTypeRepo.findByIdAndPropertyId(roomTypeId, propertyId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room type not found")
if (roomTypeRepo.existsByPropertyIdAndCodeAndIdNot(propertyId, request.code, roomTypeId)) {
throw ResponseStatusException(HttpStatus.CONFLICT, "Room type code already exists for property")
}
roomType.code = request.code
roomType.name = request.name
roomType.baseOccupancy = request.baseOccupancy ?: roomType.baseOccupancy
roomType.maxOccupancy = request.maxOccupancy ?: roomType.maxOccupancy
return roomTypeRepo.save(roomType).toResponse()
}
@DeleteMapping("/{roomTypeId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun deleteRoomType(
@PathVariable propertyId: UUID,
@PathVariable roomTypeId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
) {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
val roomType = roomTypeRepo.findByIdAndPropertyId(roomTypeId, propertyId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room type not found")
if (roomRepo.existsByPropertyIdAndRoomTypeId(propertyId, roomTypeId)) {
throw ResponseStatusException(HttpStatus.CONFLICT, "Cannot delete room type with rooms")
}
roomTypeRepo.delete(roomType)
}
private fun requirePrincipal(principal: MyPrincipal?) {
if (principal == null) {
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
}
}
}
private fun RoomType.toResponse(): RoomTypeResponse {
val id = id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Room type id missing")
val propertyId = property.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Property id missing")
return RoomTypeResponse(
id = id,
propertyId = propertyId,
code = code,
name = name,
baseOccupancy = baseOccupancy,
maxOccupancy = maxOccupancy
)
}

View File

@@ -1,18 +1,201 @@
package com.android.trisolarisserver.controller
import com.android.trisolarisserver.component.PropertyAccess
import org.springframework.stereotype.Component
import com.android.trisolarisserver.controller.dto.RoomAvailabilityResponse
import com.android.trisolarisserver.controller.dto.RoomBoardResponse
import com.android.trisolarisserver.controller.dto.RoomBoardStatus
import com.android.trisolarisserver.controller.dto.RoomResponse
import com.android.trisolarisserver.controller.dto.RoomUpsertRequest
import com.android.trisolarisserver.db.repo.PropertyRepo
import com.android.trisolarisserver.db.repo.PropertyUserRepo
import com.android.trisolarisserver.db.repo.RoomRepo
import com.android.trisolarisserver.db.repo.RoomStayRepo
import com.android.trisolarisserver.db.repo.RoomTypeRepo
import com.android.trisolarisserver.models.room.Room
import com.android.trisolarisserver.models.property.Role
import com.android.trisolarisserver.security.MyPrincipal
import org.springframework.http.HttpStatus
import org.springframework.security.core.annotation.AuthenticationPrincipal
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.RequestMapping
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
import java.util.UUID
class Rooms {
@RestController
@RequestMapping("/properties/{propertyId}/rooms")
class Rooms(
private val propertyAccess: PropertyAccess,
private val roomRepo: RoomRepo,
private val roomStayRepo: RoomStayRepo,
private val propertyRepo: PropertyRepo,
private val roomTypeRepo: RoomTypeRepo,
private val propertyUserRepo: PropertyUserRepo
) {
// private val propertyAccess: PropertyAccess;
// @GetMapping("/properties/{propertyId}/rooms/free")
// fun freeRooms(@PathVariable propertyId: UUID, principal: MyPrincipal): List<RoomDto> {
// propertyAccess.requireMember(propertyId, principal.userId)
// return roomService.freeRooms(propertyId)
// }
@GetMapping
fun listRooms(
@PathVariable propertyId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
): List<RoomResponse> {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
val roles = propertyUserRepo.findRolesByPropertyAndUser(propertyId, principal.userId)
val rooms = roomRepo.findByPropertyIdOrderByRoomNumber(propertyId)
if (isAgentOnly(roles)) {
val occupiedRoomIds = roomStayRepo.findOccupiedRoomIds(propertyId).toHashSet()
return rooms
.filter { it.active && !it.maintenance && !occupiedRoomIds.contains(it.id) }
.map { it.toRoomResponse() }
}
return rooms
.map { it.toRoomResponse() }
}
}
@GetMapping("/board")
fun roomBoard(
@PathVariable propertyId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
): List<RoomBoardResponse> {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
val rooms = roomRepo.findByPropertyIdOrderByRoomNumber(propertyId)
val occupiedRoomIds = roomStayRepo.findOccupiedRoomIds(propertyId).toHashSet()
val roles = propertyUserRepo.findRolesByPropertyAndUser(propertyId, principal.userId)
val mapped = rooms.map { room ->
val status = when {
room.maintenance -> RoomBoardStatus.MAINTENANCE
!room.active -> RoomBoardStatus.INACTIVE
occupiedRoomIds.contains(room.id) -> RoomBoardStatus.OCCUPIED
else -> RoomBoardStatus.FREE
}
RoomBoardResponse(
roomNumber = room.roomNumber,
roomTypeName = room.roomType.name,
status = status
)
}
return if (isAgentOnly(roles)) mapped.filter { it.status == RoomBoardStatus.FREE } else mapped
}
@GetMapping("/availability")
fun roomAvailability(
@PathVariable propertyId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
): List<RoomAvailabilityResponse> {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
val rooms = roomRepo.findByPropertyIdOrderByRoomNumber(propertyId)
val occupiedRoomIds = roomStayRepo.findOccupiedRoomIds(propertyId).toHashSet()
val freeRooms = rooms.filter { it.active && !it.maintenance && !occupiedRoomIds.contains(it.id) }
val grouped = freeRooms.groupBy { it.roomType.name }
return grouped.entries.map { (typeName, roomList) ->
RoomAvailabilityResponse(
roomTypeName = typeName,
freeRoomNumbers = roomList.map { it.roomNumber }
)
}.sortedBy { it.roomTypeName }
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun createRoom(
@PathVariable propertyId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: RoomUpsertRequest
): RoomResponse {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
if (roomRepo.existsByPropertyIdAndRoomNumber(propertyId, request.roomNumber)) {
throw ResponseStatusException(HttpStatus.CONFLICT, "Room number already exists for property")
}
val property = propertyRepo.findById(propertyId).orElseThrow {
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
}
val roomType = roomTypeRepo.findByIdAndPropertyId(request.roomTypeId, propertyId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room type not found")
val room = Room(
property = property,
roomType = roomType,
roomNumber = request.roomNumber,
floor = request.floor,
hasNfc = request.hasNfc,
active = request.active,
maintenance = request.maintenance,
notes = request.notes
)
return roomRepo.save(room).toRoomResponse()
}
@PutMapping("/{roomId}")
fun updateRoom(
@PathVariable propertyId: UUID,
@PathVariable roomId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: RoomUpsertRequest
): RoomResponse {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
val room = roomRepo.findByIdAndPropertyId(roomId, propertyId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room not found for property")
if (roomRepo.existsByPropertyIdAndRoomNumberAndIdNot(propertyId, request.roomNumber, roomId)) {
throw ResponseStatusException(HttpStatus.CONFLICT, "Room number already exists for property")
}
val roomType = roomTypeRepo.findByIdAndPropertyId(request.roomTypeId, propertyId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room type not found")
room.roomNumber = request.roomNumber
room.floor = request.floor
room.roomType = roomType
room.hasNfc = request.hasNfc
room.active = request.active
room.maintenance = request.maintenance
room.notes = request.notes
return roomRepo.save(room).toRoomResponse()
}
private fun requirePrincipal(principal: MyPrincipal?) {
if (principal == null) {
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
}
}
private fun isAgentOnly(roles: Set<Role>): Boolean {
if (!roles.contains(Role.AGENT)) return false
val privileged = setOf(Role.ADMIN, Role.MANAGER, Role.STAFF, Role.HOUSEKEEPING, Role.FINANCE)
return roles.none { it in privileged }
}
}
private fun Room.toRoomResponse(): RoomResponse {
val roomId = id ?: throw IllegalStateException("Room id is null")
val roomTypeId = roomType.id ?: throw IllegalStateException("Room type id is null")
return RoomResponse(
id = roomId,
roomNumber = roomNumber,
floor = floor,
roomTypeId = roomTypeId,
roomTypeName = roomType.name,
hasNfc = hasNfc,
active = active,
maintenance = maintenance,
notes = notes
)
}

View File

@@ -0,0 +1,64 @@
package com.android.trisolarisserver.controller.dto
import java.util.UUID
data class OrgCreateRequest(
val name: String
)
data class OrgResponse(
val id: UUID,
val name: String
)
data class PropertyCreateRequest(
val code: String,
val name: String,
val timezone: String? = null,
val currency: String? = null,
val active: Boolean? = null
)
data class PropertyUpdateRequest(
val code: String,
val name: String,
val timezone: String? = null,
val currency: String? = null,
val active: Boolean? = null
)
data class PropertyResponse(
val id: UUID,
val orgId: UUID,
val code: String,
val name: String,
val timezone: String,
val currency: String,
val active: Boolean
)
data class UserCreateRequest(
val firebaseUid: String,
val phoneE164: String? = null,
val name: String? = null,
val disabled: Boolean? = null
)
data class UserResponse(
val id: UUID,
val orgId: UUID,
val firebaseUid: String?,
val phoneE164: String?,
val name: String?,
val disabled: Boolean
)
data class PropertyUserRoleRequest(
val roles: Set<String>
)
data class PropertyUserResponse(
val userId: UUID,
val propertyId: UUID,
val roles: Set<String>
)

View File

@@ -0,0 +1,43 @@
package com.android.trisolarisserver.controller.dto
import java.util.UUID
data class RoomResponse(
val id: UUID,
val roomNumber: Int,
val floor: Int?,
val roomTypeId: UUID,
val roomTypeName: String,
val hasNfc: Boolean,
val active: Boolean,
val maintenance: Boolean,
val notes: String?
)
data class RoomBoardResponse(
val roomNumber: Int,
val roomTypeName: String,
val status: RoomBoardStatus
)
data class RoomAvailabilityResponse(
val roomTypeName: String,
val freeRoomNumbers: List<Int>
)
enum class RoomBoardStatus {
FREE,
OCCUPIED,
MAINTENANCE,
INACTIVE
}
data class RoomUpsertRequest(
val roomNumber: Int,
val floor: Int?,
val roomTypeId: UUID,
val hasNfc: Boolean,
val active: Boolean,
val maintenance: Boolean,
val notes: String?
)

View File

@@ -0,0 +1,19 @@
package com.android.trisolarisserver.controller.dto
import java.util.UUID
data class RoomTypeUpsertRequest(
val code: String,
val name: String,
val baseOccupancy: Int? = null,
val maxOccupancy: Int? = null
)
data class RoomTypeResponse(
val id: UUID,
val propertyId: UUID,
val code: String,
val name: String,
val baseOccupancy: Int,
val maxOccupancy: Int
)